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

118

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

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похоже, ведет себя так, как я ожидал, что все будет вести себя.

+=Оператор называется «соединение оператор присваивания».

eucalculia
источник
увидеть разницу между 'extend' и 'append' в списке тоже
N 1.1
3
Я не думаю, что это показывает что-то не так с Python. Большинство языков даже не позволяют использовать +оператор для массивов. Я думаю, что в этом случае есть смысл +=добавить.
Skilldrick
4
Официально это называется «расширенное задание».
Мартейн Питерс

Ответы:

138

Общий ответ заключается в том, что он +=пытается вызвать __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эквивалентны. Это то, что позволяет вам использовать +=для неизменяемых типов, что может показаться странным дизайнерским решением, пока вы не решите, что в противном случае вы не могли бы использовать +=для неизменных типов, таких как числа!

Скотт Гриффитс
источник
4
Существует также __radd__метод, который иногда может вызываться (это актуально для выражений, которые в основном включают подклассы).
jfs
2
В перспективе: + = полезно, если важны память и скорость
Норфельдт
3
Зная, что на +=самом деле список расширяется , это объясняет, почему x = []; x = x + {}дает TypeErrorвремя x = []; x += {}просто возвращается [].
zezollo
96

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

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не влияя на другие экземпляры класса.

AndiDog
источник
7
Это не всегда верно: a = 1; а + = 1; является допустимым Python, но у int нет никаких методов "extend ()". Вы не можете обобщать это.
e-satis
2
Проведя несколько тестов, Скотт Гриффитс понял все правильно, так что -1 для вас.
e-satis
11
@ e-statis: ОП четко говорил о списках, и я четко заявил, что я говорю и о списках. Я ничего не обобщаю.
AndiDog
Удалил -1, ответ достаточно хороший. Я все же считаю, что ответ Гриффитса лучше.
e-satis
Сначала кажется странным думать, что a += bэто отличается от a = a + bдвух списков aи b. Но в этом есть смысл; extendбудет чаще предполагаться для работы со списками, а не для создания новой копии всего списка, которая будет иметь более высокую временную сложность. Если разработчикам нужно быть осторожными, чтобы не изменять исходные списки на месте, то кортежи - лучший вариант в качестве неизменяемых объектов. +=с кортежами не может изменять исходный кортеж.
Пранджал Миттал
22

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

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

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

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

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

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

class foo:
    bar = []

foo.bar = [x]
Кан Берк Гюдер
источник
8

Здесь задействованы две вещи:

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

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

Аджай
источник
5

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

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

  1. «особенное», возможно, незамеченное поведение списков с +=(как заявил Скотт Гриффитс )
  2. тот факт, что задействованы атрибуты класса, а также атрибуты экземпляра (как заявил Кан Берк Бюдер )

В классе 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).

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

glglgl
источник
5

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

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

...

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

mwardm
источник
1
>>> 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])
tanglei
источник
0
>>> 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

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

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

Рошан хорошо
источник
# ID такой же для списков
roshan ok