Почему + = ведет себя непредсказуемо в списках?
Оператор +=
в 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 []
Надеюсь, что это очистит.