Почему + = ведет себя неожиданно в списках? - PullRequest
97 голосов
/ 27 февраля 2010

Оператор += в 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, кажется, ведет себя так, как я ожидаю, что вещи будут себя вести.

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

Ответы [ 7 ]

110 голосов
/ 27 февраля 2010

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

89 голосов
/ 27 февраля 2010

Для общего случая см. ответ Скотта Гриффита . Однако при работе со списками, такими как вы, оператор += является сокращением для 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, не затрагивая другие экземпляры класса.

22 голосов
/ 27 февраля 2010

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

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

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

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

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

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

class foo:
    bar = []

foo.bar = [x]
5 голосов
/ 12 июня 2013

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

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

...

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

5 голосов
/ 11 ноября 2012

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

У вас есть 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).

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

4 голосов
/ 25 октября 2013

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

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

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

1 голос
/ 13 марта 2012
>>> 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])
...