Почему += ведет себя неожиданно в списках?

+= оператор в Python, кажется, работает неожиданно в списках. Кто-нибудь может сказать мне, что здесь происходит?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

ВЫХОД

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += bar кажется, влияет на каждый экземпляр класса, тогда как foo = foo + bar кажется, ведет себя так, как я ожидал бы, что вещи будут вести себя.

+= Оператор называется "оператором составного присваивания".

9 ответов

Общий ответ таков: += пытается позвонить __iadd__ специальный метод, и если он недоступен, он пытается использовать __add__ вместо. Таким образом, проблема заключается в разнице между этими специальными методами.

__iadd__ специальный метод для добавления на месте, то есть он мутирует объект, на который он воздействует. __add__ специальный метод возвращает новый объект и также используется для стандарта + оператор.

Итак, когда += Оператор используется на объекте, который имеет __iadd__ Определен объект, изменен на месте. В противном случае он будет пытаться использовать равнину __add__ и вернуть новый объект.

Вот почему для изменчивых типов, таких как списки += изменяет значение объекта, тогда как для неизменяемых типов, таких как кортежи, строки и целые числа, вместо этого возвращается новый объект (a += b становится эквивалентным a = a + b).

Для типов, которые поддерживают оба __iadd__ а также __add__ поэтому вы должны быть осторожны, какой вы используете. a += b позвоню __iadd__ и мутировать a, в то время как a = a + b создаст новый объект и назначит его a, Они не одна и та же операция!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

Для неизменяемых типов (где у вас нет __iadd__) a += b а также a = a + b эквивалентны. Это то, что позволяет вам использовать += для неизменяемых типов, что может показаться странным дизайнерским решением, пока вы не решите, что иначе вы не сможете использовать += на неизменяемые типы, такие как числа!

Для общего случая см . Ответ Скотта Гриффита. При работе со списками, такими как вы, += оператор является сокращением для someListObject.extend(iterableObject), См. Документацию exte ().

extend Функция добавит все элементы параметра в список.

При выполнении foo += something вы изменяете список foo на месте, таким образом, вы не измените ссылку, что имя foo указывает на, но вы изменяете объект списка напрямую. С foo = foo + somethingвы на самом деле создаете новый список.

Этот пример кода объяснит это:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

Обратите внимание, как изменяется ссылка, когда вы переназначаете новый список на l,

Как bar является переменной класса вместо переменной экземпляра, изменение на месте повлияет на все экземпляры этого класса. Но при переопределении self.bar, экземпляр будет иметь отдельную переменную экземпляра self.bar не затрагивая другие экземпляры класса.

Проблема здесь в том, bar определяется как атрибут класса, а не переменная экземпляра.

В fooатрибут класса изменяется в init метод, поэтому все экземпляры затронуты.

В foo2переменная экземпляра определяется с использованием (пустого) атрибута класса, и каждый экземпляр получает свой собственный bar,

"Правильная" реализация будет:

class foo:
    def __init__(self, x):
        self.bar = [x]

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

class foo:
    bar = []

foo.bar = [x]

Здесь есть две вещи:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+ Оператор вызывает __add__ метод в списке. Он берет все элементы из своих операндов и создает новый список, содержащий эти элементы, сохраняя их порядок.

+= звонки оператора __iadd__ метод в списке. Он принимает итерируемое и добавляет все элементы итерируемого к списку на месте. Он не создает новый объект списка.

В классе foo заявление self.bar += [x] не является оператором присваивания, но фактически переводится в

self.bar.__iadd__([x])  # modifies the class attribute  

который изменяет список на месте и действует как метод списка extend,

В классе foo2напротив, оператор присваивания в init метод

self.bar = self.bar + [x]  

может быть разобрано как:
Экземпляр не имеет атрибута bar (хотя есть атрибут класса с тем же именем), поэтому он обращается к атрибуту класса bar и создает новый список, добавляя x к этому. Заявление переводится как:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

Затем он создает атрибут экземпляра bar и присваивает ему вновь созданный список. Обратите внимание, что bar на правой стороне задания отличается от bar на лхс.

Для экземпляров класса foo, bar является атрибутом класса, а не атрибутом экземпляра. Отсюда любое изменение атрибута класса bar будет отражено для всех случаев.

Напротив, каждый экземпляр класса foo2 имеет собственный атрибут экземпляра bar который отличается от атрибута класса с тем же именем bar,

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

Надеюсь, это прояснит ситуацию.

Хотя прошло много времени и было сказано много правильных вещей, нет ответа, который объединяет оба эффекта.

У вас есть 2 эффекта:

  1. "особенное", возможно, незамеченное поведение списков с += (как сказал Скотт Гриффитс)
  2. тот факт, что атрибуты класса, а также атрибуты экземпляров участвуют (как заявил Can Berk Büder)

В классе foo, __init__ Метод изменяет атрибут класса. Это потому что self.bar += [x] переводит на self.bar = self.bar.__iadd__([x]), __iadd__() предназначен для модификации на месте, поэтому он изменяет список и возвращает ссылку на него.

Обратите внимание, что экземпляр dict изменен, хотя это обычно не требуется, поскольку класс dict уже содержит такое же назначение. Так что эта деталь остается практически незамеченной - за исключением случаев, когда вы делаете foo.bar = [] после этого. Здесь случаи bar остается неизменным благодаря указанному факту.

В классе foo2Однако, класс bar используется, но не трогается. Вместо этого [x] добавляется к нему, образуя новый объект, как self.bar.__add__([x]) здесь вызывается, который не изменяет объект. Затем результат помещается в экземпляр dict, давая экземпляру новый список как dict, в то время как атрибут класса остается измененным.

Различие между ... = ... + ... а также ... += ... влияет также на задания потом:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

Вы можете проверить идентичность объектов с print id(foo), id(f), id(g) (не забудьте дополнительное ()s, если вы находитесь на Python3).

Кстати: += Оператор называется "расширенным назначением" и, как правило, предназначен для выполнения модификаций на месте, насколько это возможно.

Другие ответы, по-видимому, в значительной степени охватывают это, хотя, кажется, стоит цитировать и ссылаться на PEP 203 " Расширенные назначения":

Они [расширенные операторы присваивания] реализуют тот же оператор, что и их обычная двоичная форма, за исключением того, что операция выполняется "на месте", когда объект левой стороны поддерживает ее, и что левая часть вычисляется только один раз.

...

Идея расширенного присваивания в Python заключается в том, что это не просто более простой способ записать обычную практику хранения результата бинарной операции в его левом операнде, но также способ для левого операнда, о котором идет речь Знайте, что он должен работать "над собой", а не создавать измененную копию самого себя.

>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

Мы видим, что когда мы пытаемся изменить неизменяемый объект (в данном случае целое число), Python вместо этого просто предоставляет нам другой объект. С другой стороны, мы можем вносить изменения в изменяемый объект (список) и оставлять его неизменным во всем.

ссылка: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Также см. Ссылку ниже, чтобы понять мелкую и глубокую копию.

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])

listname.extend() отлично подходит для этой цели :)

Другие вопросы по тегам