Почему += неожиданно ведет себя в списках?
Оператор +=
в 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
кажется, ведет себя так, как я ожидал бы, что все будет вести себя.
Оператор +=
называется "составным оператором присваивания".
Переведено автоматически
Ответ 1
Общий ответ заключается в том, что +=
пытается вызвать __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
эквивалентны. Это то, что позволяет использовать +=
для неизменяемых типов, что может показаться странным дизайнерским решением, пока вы не учтете, что в противном случае вы не смогли бы использовать +=
для неизменяемых типов, таких как numbers!
Ответ 2
Для общего случая см. Ответ Скотта Гриффита. Однако при работе со списками, подобными вашему, +=
оператор является сокращением от someListObject.extend(iterableObject)
. Смотрите документацию extend().
Функция extend
добавит все элементы параметра в список.
При выполнении foo += something
вы изменяете список foo
на месте, таким образом, вы не изменяете ссылку, на которую указывает имя foo
, но вы изменяете объект list напрямую. С помощью foo = foo + something
вы фактически создаете новый список.
Этот пример кода объяснит это:
>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216
Обратите внимание, как изменяется ссылка при переназначении нового списка на l
.
Поскольку bar
является переменной класса, а не переменной экземпляра, изменение на месте повлияет на все экземпляры этого класса. Но при переопределении self.bar
у экземпляра будет отдельная переменная экземпляра self.bar
, не затрагивающая другие экземпляры класса.
Ответ 3
Проблема здесь в том, bar
определяется как атрибут класса, а не переменная экземпляра.
В foo
, атрибут класса изменен в init
методе, вот почему затронуты все экземпляры.
В foo2
переменная экземпляра определяется с использованием (пустого) атрибута класса, и каждый экземпляр получает свой собственный bar
.
"Правильной" реализацией было бы:
class foo:
def __init__(self, x):
self.bar = [x]
Конечно, атрибуты класса полностью легальны. Фактически, вы можете получить к ним доступ и изменять их, не создавая экземпляр класса, подобный этому:
class foo:
bar = []
foo.bar = [x]
Ответ 4
Здесь задействованы две вещи:
1. class attributes and instance attributes
2. difference between the operators + and += for lists
+
оператор вызывает __add__
метод для списка. Он берет все элементы из своих операндов и создает новый список, содержащий эти элементы с сохранением их порядка.
+=
оператор вызывает __iadd__
метод в списке. Он принимает iterable и добавляет все элементы iterable в список на месте. Он не создает новый объект list .
В классе foo
оператор self.bar += [x]
не является оператором присваивания, но фактически преобразуется в
self.bar.__iadd__([x]) # modifies the class attribute
который изменяет список на месте и действует как метод list extend
.
В классе foo2
, наоборот, оператор присваивания в init
методе
self.bar = self.bar + [x]
может быть расшифрован как:
У экземпляра нет атрибута bar
(хотя есть атрибут class с тем же именем), поэтому он обращается к атрибуту class bar
и создает новый список, добавляя к нему x
. Оператор преобразуется в:
self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute
Затем он создает атрибут экземпляра bar
и присваивает ему вновь созданный список. Обратите внимание, что bar
в rhs присваивание отличается от bar
в lhs.
Для экземпляров класса 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 []
Надеюсь, это все прояснит.