Как избежать совместного использования данных класса между экземплярами?

146

Что я хочу, это поведение:

class a:
    list = []

x = a()
y = a()

x.list.append(1)
y.list.append(2)
x.list.append(3)
y.list.append(4)

print(x.list) # prints [1, 3]
print(y.list) # prints [2, 4]

Конечно, что действительно происходит, когда я печатаю:

print(x.list) # prints [1, 2, 3, 4]
print(y.list) # prints [1, 2, 3, 4]

Очевидно, они делятся данными в классе a. Как получить отдельные экземпляры для достижения желаемого поведения?

8steve8
источник
14
Пожалуйста, не используйте listв качестве имени атрибута. listявляется встроенной функцией для создания нового списка Вы должны написать имя классов с заглавной буквы.
Марк Тудури

Ответы:

147

Вы хотите это:

class a:
    def __init__(self):
        self.list = []

Объявление переменных внутри объявления класса делает их членами класса, а не членами экземпляра. Объявление их внутри __init__метода гарантирует, что новый экземпляр членов будет создан рядом с каждым новым экземпляром объекта, что является поведением, которое вы ищете.

abyx
источник
5
Дополнительное уточнение: если вы переназначите свойство list в одном из экземпляров, это не повлияет на другие. Поэтому, если вы сделали что-то подобное x.list = [], вы можете изменить это и не влиять на других. Проблема, с которой вы сталкиваетесь, состоит в том, что они представляют собой один x.listи y.listтот же список, поэтому, когда вы вызываете append для одного, это влияет на другой.
Мэтт Moriarity
Но почему это происходит только для списка? Когда я объявил целое число или строку за пределами инициализации , он не был разделен между объектами? Кто-нибудь может поделиться какой-нибудь документальной ссылкой на эту концепцию?
Амаль Ц
1
@AmalTs Похоже, вы не понимаете, как работает назначение в Python. Смотрите это видео или этот пост . Поведение, которое вы видите, вызвано тем, что вы изменяете списки, но связываете ссылки на целые и строки.
Андрей Беньковский
4
@AmalTs Примечание: считается плохой практикой использовать атрибуты класса в качестве «ленивых» значений по умолчанию для атрибутов экземпляра. Даже если атрибуты имеют неизменный тип, лучше назначить их внутри __init__.
Андрей Беньковский
21

Принятый ответ работает, но немного больше объяснений не повредит.

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

В исходном коде значение не присваивается listатрибуту после создания экземпляра; поэтому он остается атрибутом класса. Определение списка внутри __init__работает, потому что __init__вызывается после создания экземпляра. Кроме того, этот код также будет производить желаемый результат:

>>> class a:
    list = []

>>> y = a()
>>> x = a()
>>> x.list = []
>>> y.list = []
>>> x.list.append(1)
>>> y.list.append(2)
>>> x.list.append(3)
>>> y.list.append(4)
>>> print(x.list)
[1, 3]
>>> print(y.list)
[2, 4]

Однако запутанный сценарий в вопросе никогда не случится с неизменными объектами, такими как числа и строки, потому что их значение нельзя изменить без присваивания. Например, код, похожий на оригинал со строковым атрибутом, работает без проблем:

>>> class a:
    string = ''


>>> x = a()
>>> y = a()
>>> x.string += 'x'
>>> y.string += 'y'
>>> x.string
'x'
>>> y.string
'y'

Итак, подведем итог: атрибуты класса становятся атрибутами экземпляра тогда и только тогда, когда им присваивается значение после создания экземпляра, находящегося в __init__методе или нет . Это хорошо, потому что таким образом вы можете иметь статические атрибуты, если вы никогда не назначаете значение атрибуту после создания экземпляра.

jurgenreza
источник
11

Вы объявили «список» как «свойство уровня класса», а не как «свойство уровня экземпляра». Для того чтобы свойства находились в области видимости на уровне экземпляра, вам необходимо инициализировать их, ссылаясь на параметр «self» в __init__методе (или в других местах, в зависимости от ситуации).

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

jldupont
источник
11

Хотя принятый ответ находится на месте, я хотел бы добавить немного описания.

Давайте сделаем небольшое упражнение

Прежде всего определите класс следующим образом:

class A:
    temp = 'Skyharbor'

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

    def change(self, y):
        self.temp = y

Так что у нас здесь?

  • У нас есть очень простой класс, который имеет атрибут temp который является строкой
  • __init__Метод , который устанавливаетself.x
  • Набор методов изменения self.temp

Довольно прямо вперед, да? Теперь давайте начнем играть с этим классом. Давайте сначала инициализируем этот класс:

a = A('Tesseract')

Теперь сделайте следующее:

>>> print(a.temp)
Skyharbor
>>> print(A.temp)
Skyharbor

Ну, a.tempработал, как ожидалось, но как, черт возьми, A.tempработал? Ну, это сработало, потому что temp является атрибутом класса. Все в питоне является объектом. Здесь А также является объектом класса type. Таким образом, атрибут temp является атрибутом, хранящимся в Aклассе, и если вы измените значение temp через A(а не через экземпляр a), измененное значение будет отражено во всех экземплярахA класса. Давайте продолжим и сделаем это:

>>> A.temp = 'Monuments'
>>> print(A.temp)
Monuments
>>> print(a.temp)
Monuments

Интересно не так ли? И обратите внимание, что id(a.temp)иid(A.temp) все те же .

Любой объект Python автоматически получает __dict__атрибут, который содержит его список атрибутов. Давайте исследуем, что этот словарь содержит для наших примеров объектов:

>>> print(A.__dict__)
{
    'change': <function change at 0x7f5e26fee6e0>,
    '__module__': '__main__',
    '__init__': <function __init__ at 0x7f5e26fee668>,
    'temp': 'Monuments',
    '__doc__': None
}
>>> print(a.__dict__)
{x: 'Tesseract'}

Обратите внимание, что tempатрибут указан средиA атрибутов класса, а xдля экземпляра.

Так почему же мы получаем определенное значение, a.tempесли оно даже не указано для экземпляра a? Ну, это магия __getattribute__()метода. В Python пунктирный синтаксис автоматически вызывает этот метод, поэтому, когда мы пишемa.temp , Python выполняетa.__getattribute__('temp') . Этот метод выполняет действие поиска атрибута, то есть находит значение атрибута, просматривая разные места.

Стандартная реализация __getattribute__()ищет сначала внутренний словарь ( dict ) объекта, а затем тип самого объекта. В этом случае a.__getattribute__('temp')выполняется первымa.__dict__['temp'] а затемa.__class__.__dict__['temp']

Хорошо, теперь давайте использовать наш changeметод:

>>> a.change('Intervals')
>>> print(a.temp)
Intervals
>>> print(A.temp)
Monuments

Ну, теперь, когда мы использовали self, print(a.temp)дает нам другое значение print(A.temp).

Теперь, если мы сравним id(a.temp)и id(A.temp), они будут другими.

Swapnil
источник
3

Да, вы должны объявить в «конструкторе», если хотите, чтобы список стал свойством объекта, а не свойством класса.

osanchezmon
источник
3

Таким образом, почти каждый ответ здесь, кажется, пропускает определенный момент. Переменные класса никогда не становятся переменными экземпляра, как показано в коде ниже. Используя метакласс для перехвата присваивания переменных на уровне класса, мы можем видеть, что когда a.myattr переназначается, магический метод назначения поля в классе не вызывается. Это потому, что присваивание создает новую переменную экземпляра . Это поведение не имеет абсолютно никакого отношения к переменной класса, что продемонстрировано вторым классом, который не имеет переменных класса и все же позволяет присваивать поля.

class mymeta(type):
    def __init__(cls, name, bases, d):
        pass

    def __setattr__(cls, attr, value):
        print("setting " + attr)
        super(mymeta, cls).__setattr__(attr, value)

class myclass(object):
    __metaclass__ = mymeta
    myattr = []

a = myclass()
a.myattr = []           #NOTHING IS PRINTED
myclass.myattr = [5]    #change is printed here
b = myclass()
print(b.myattr)         #pass through lookup on the base class

class expando(object):
    pass

a = expando()
a.random = 5            #no class variable required
print(a.random)         #but it still works

IN SHORT Переменные класса не имеют ничего общего с переменными экземпляра.

Более ясно, что они просто попадают в область поиска экземпляров. Переменные класса на самом деле являются переменными экземпляра самого объекта класса. Вы также можете иметь переменные метакласса, если хотите, потому что сами метаклассы тоже являются объектами. Все является объектом, независимо от того, используется ли он для создания других объектов или нет, поэтому не ограничивайтесь семантикой использования слова в классе других языков. В Python класс - это просто объект, который используется для определения того, как создавать другие объекты и как они будут себя вести. Метаклассы - это классы, которые создают классы, просто чтобы проиллюстрировать этот момент.

TheQabalist
источник
0

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

Из объектов и классов Python от Programiz.com :

__init__()функция. Эта специальная функция вызывается всякий раз, когда создается новый объект этого класса.

Этот тип функции также называют конструкторами в объектно-ориентированном программировании (ООП). Обычно мы используем его для инициализации всех переменных.

Например:

class example:
    list=[] #This is class variable shared by all instance
    def __init__(self):
        self.list = [] #This is instance variable referred to specific instance
Проеш Бхумик
источник