Чим відрізняються атрибути класу та екземпляра?


132

Чи є якась змістовна різниця між:

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

vs.

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

Якщо ви створюєте безліч примірників, чи є різниця у продуктивності чи просторі для двох стилів? Читаючи код, чи вважаєте ви значення двох стилів суттєво різними?


1
Я щойно зрозумів, що тут задали і відповіли на подібне запитання: stackoverflow.com/questions/206734/… Чи слід видалити це запитання?
Ден Гомерік

2
Це ваше питання, сміливо видаляйте його. Оскільки це ваше, навіщо питати думку когось іншого?
С.Лотт

Відповіді:


146

Крім міркувань щодо продуктивності, існує значна смислова різниця. У випадку атрибуту класу, на який посилається лише один об'єкт. В екземплярі-атрибут-set-at-instantiation може бути кілька об'єктів, на які посилається. Наприклад

>>> 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

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 значення не зберігаються у змінних. Значення відпадають у value-land, самі по собі змінні є лише іменами значень, а присвоєння просто створює нове ім'я для значення. Якщо це допомагає, подумайте про кожну змінну Python як shared_ptr<T>а T.

** Деякі люди користуються цим, використовуючи атрибут класу як "значення за замовчуванням" для атрибута екземпляра, який екземпляри можуть встановлювати або не встановлювати. Це може бути корисно в деяких випадках, але це також може бути бентежно, тому будьте обережні.


0

Є ще одна ситуація.

Атрибутами класу та екземпляра є Descriptor .

# -*- 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).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.