В чем разница между атрибутами класса и экземпляра?

132

Есть ли значимое различие между:

class A(object):
    foo = 5   # some default value

против

class B(object):
    def __init__(self, foo=5):
        self.foo = foo

Если вы создаете много экземпляров, есть ли разница в производительности или требованиях к пространству для двух стилей? Когда вы читаете код, считаете ли вы, что значения этих двух стилей существенно различаются?

Дэн Хомерик
источник
1
Я только что понял, что был задан аналогичный вопрос, и здесь дан ответ: stackoverflow.com/questions/206734/… Следует ли мне удалить этот вопрос?
Дэн Хомерик
2
Это ваш вопрос, не стесняйтесь его удалять. Если это твое, зачем спрашивать чье-то мнение?
S.Lott

Ответы:

146

Помимо соображений производительности, существует значительная семантическая разница. В случае атрибута класса упоминается только один объект. В экземпляре-атрибут-установлен-при-создании может быть ссылка на несколько объектов. Например

>>> class A: foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo
[5]
>>> class A:
...  def __init__(self): self.foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo    
[]
Алекс Ковентри
источник
4
Общие только изменяемые типы. Как и для, intи strони по-прежнему связаны с каждым экземпляром, а не с классом.
Бабу
11
@Babu: Нет, intи strявляются также общими, точно таким же образом. Вы можете легко это проверить с помощью isили id. Или просто посмотрите на каждый экземпляр __dict__и класс __dict__. Просто обычно не имеет большого значения, являются ли неизменяемые типы общими или нет.
abarnert
17
Однако учтите, что если вы это сделаете a.foo = 5, то в обоих случаях вы увидите b.fooвозврат []. Это потому, что в первом случае вы перезаписываете атрибут класса a.fooновым атрибутом экземпляра с тем же именем.
Константин Шуберт
39

Разница в том, что атрибут класса используется всеми экземплярами. Атрибут экземпляра уникален для этого экземпляра.

Если исходить из C ++, атрибуты класса больше похожи на статические переменные-члены.

Питер Шиннерс
источник
1
Разве не являются общими только изменяемые типы? Принятый ответ показывает список, который работает, но если это int, он выглядит таким же, как attr экземпляра: >>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5
Rafe
6
@Rafe: Нет, все типы являются общими. Причина, по которой вы сбиты с толку, заключается в том, что то, что a.foo.append(5)изменяет значение, на которое a.fooссылается, a.foo = 5превращается a.fooв новое имя для значения 5. Итак, вы получаете атрибут экземпляра, который скрывает атрибут класса. Попробуйте то же самое a.foo = 5в версии Алекса, и вы увидите, что ничего b.fooне изменилось.
abarnert
30

Вот очень хороший пост и резюмируйте его, как показано ниже.

class Bar(object):
    ## No need for dot syntax
    class_var = 1

    def __init__(self, i_var):
        self.i_var = i_var

## Need dot syntax as we've left scope of class namespace
Bar.class_var
## 1
foo = MyClass(2)

## Finds i_var in foo's instance namespace
foo.i_var
## 2

## Doesn't find class_var in instance namespace…
## So look's in class namespace (Bar.__dict__)
foo.class_var
## 1

И в наглядной форме

введите описание изображения здесь

Назначение атрибута класса

  • Если атрибут класса установлен путем доступа к классу, он переопределит значение для всех экземпляров

    foo = Bar(2)
    foo.class_var
    ## 1
    Bar.class_var = 2
    foo.class_var
    ## 2
  • Если переменная класса установлена ​​путем доступа к экземпляру, она переопределит значение только для этого экземпляра . Это существенно переопределяет переменную класса и превращает ее в переменную экземпляра, доступную интуитивно только для этого экземпляра .

    foo = Bar(2)
    foo.class_var
    ## 1
    foo.class_var = 2
    foo.class_var
    ## 2
    Bar.class_var
    ## 1

Когда бы вы использовали атрибут класса?

  • Хранение констант . Поскольку к атрибутам класса можно получить доступ как к атрибутам самого класса, часто полезно использовать их для хранения общеклассовых, зависящих от класса констант.

    class Circle(object):
         pi = 3.14159
    
         def __init__(self, radius):
              self.radius = radius   
        def area(self):
             return Circle.pi * self.radius * self.radius
    
    Circle.pi
    ## 3.14159
    c = Circle(10)
    c.pi
    ## 3.14159
    c.area()
    ## 314.159
  • Определение значений по умолчанию . В качестве тривиального примера мы можем создать ограниченный список (т. Е. Список, который может содержать только определенное количество элементов или меньше) и выбрать ограничение по умолчанию в 10 элементов.

    class MyClass(object):
        limit = 10
    
        def __init__(self):
            self.data = []
        def item(self, i):
            return self.data[i]
    
        def add(self, e):
            if len(self.data) >= self.limit:
                raise Exception("Too many elements")
            self.data.append(e)
    
     MyClass.limit
     ## 10
zangw
источник
19

Поскольку люди в комментариях здесь и в двух других вопросах, отмеченных как дубликаты, все, похоже, не понимают этого, я думаю, что стоит добавить дополнительный ответ поверх ответа Алекса Ковентри .

Тот факт, что Алекс присваивает значение изменяемого типа, например списка, не имеет ничего общего с тем, являются ли вещи общими или нет. Мы можем увидеть это с помощью idфункции или isоператора:

>>> class A: foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
True
>>> class A:
...     def __init__(self): self.foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
False

(Если вам интересно, почему я использовал object()вместо, скажем, 5это, чтобы избежать столкновения с двумя целыми другими проблемами, о которых я не хочу здесь вдаваться; по двум разным причинам полностью отдельно созданные 5s могут оказаться тот же экземпляр числа 5. Но полностью отдельно созданные object()s не могут.)


Итак, почему a.foo.append(5)в примере Алекса влияет b.foo, а a.foo = 5в моем - нет? Ну, попробуйте a.foo = 5в примере Алекса, и обратите внимание , что это не влияет b.fooтам либо .

a.foo = 5просто превращается a.fooв имя для 5. Это не влияет b.fooни на какое другое имя для старого значения, которое a.fooиспользовалось для обозначения. * Немного сложно создать атрибут экземпляра, скрывающий атрибут класса, ** но как только вы это получите, ничего сложного не будет происходит здесь.


Надеюсь, теперь очевидно, почему Алекс использовал список: тот факт, что вы можете изменить список, означает, что легче показать, что две переменные называют один и тот же список, а также означает, что в реальном коде более важно знать, есть ли у вас два списка или два имени для одного и того же списка.


* Путаница для людей, пришедших с такого языка, как C ++, заключается в том, что в Python значения не хранятся в переменных. Ценности живут в мире ценностей сами по себе, переменные - это просто имена значений, а присваивание просто создает новое имя для значения. Если это помогает, думайте о каждой переменной Python как о файле, а shared_ptr<T>не о T.

** Некоторые люди пользуются этим, используя атрибут класса как «значение по умолчанию» для атрибута экземпляра, который экземпляры могут или не могут устанавливать. В некоторых случаях это может быть полезно, но также может сбивать с толку, поэтому будьте осторожны.

abarnert
источник
0

Есть еще одна ситуация.

Атрибуты класса и экземпляра - это дескриптор .

# -*- encoding: utf-8 -*-


class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        return self.val


class Base(object):
    attr_1 = RevealAccess(10, 'var "x"')

    def __init__(self):
        self.attr_2 = RevealAccess(10, 'var "x"')


def main():
    b = Base()
    print("Access to class attribute, return: ", Base.attr_1)
    print("Access to instance attribute, return: ", b.attr_2)

if __name__ == '__main__':
    main()

Выше будет вывод:

('Access to class attribute, return: ', 10)
('Access to instance attribute, return: ', <__main__.RevealAccess object at 0x10184eb50>)

Один и тот же тип доступа к экземпляру через класс или экземпляр возвращает другой результат!

И я нашел в определении c.PyObject_GenericGetAttr, и отличный пост .

объяснять

Если атрибут найден в словаре классов, составляющих. MRO объектов, затем проверьте, указывает ли искомый атрибут на дескриптор данных (который является не чем иным, как классом, реализующим __get__и __set__методы, и методы). Если это так, разрешите поиск атрибутов, вызвав __get__метод дескриптора данных (строки 28–33).

SimonShyu
источник