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

Оператор += в 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 

OUTPUT

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

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

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

Ответ 1

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

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

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

Ответ 2

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

Ответ 3

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

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

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

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

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

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

class foo:
    bar = []

foo.bar = [x]

Ответ 4

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

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

  • "особое", возможно, незаметное поведение списков с += (как указано Scott Griffiths)
  • задействован атрибут класса, а также атрибуты экземпляра (как указано Can Berk Büder)

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

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

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

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

f = foo(1) # adds 1 to the class bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class 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) (не забудьте добавить дополнительный (), если вы находитесь на Python3).

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

Ответ 5

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

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

...

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

Ответ 6

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

Ответ 7

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

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 (есть атрибут класса с тем же именем), поэтому он обращается к атрибуту class bar и создает новый список, добавляя к нему x. Утверждение переводится на:

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

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

Для экземпляров класса foo, bar - атрибут класса, а не атрибут экземпляра. Следовательно, любое изменение атрибута class 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 []  

Надеюсь, что это очистит.