Почему += ведет себя неожиданно в списках?
+=
оператор в 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
кажется, ведет себя так, как я ожидал бы, что вещи будут вести себя.
+=
Оператор называется "оператором составного присваивания".
9 ответов
Общий ответ таков: +=
пытается позвонить __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
эквивалентны. Это то, что позволяет вам использовать +=
для неизменяемых типов, что может показаться странным дизайнерским решением, пока вы не решите, что иначе вы не сможете использовать +=
на неизменяемые типы, такие как числа!
Для общего случая см . Ответ Скотта Гриффита. При работе со списками, такими как вы, +=
оператор является сокращением для 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
не затрагивая другие экземпляры класса.
Проблема здесь в том, bar
определяется как атрибут класса, а не переменная экземпляра.
В foo
атрибут класса изменяется в init
метод, поэтому все экземпляры затронуты.
В foo2
переменная экземпляра определяется с использованием (пустого) атрибута класса, и каждый экземпляр получает свой собственный bar
,
"Правильная" реализация будет:
class foo:
def __init__(self, x):
self.bar = [x]
Конечно, атрибуты класса полностью законны. Фактически, вы можете получить к ним доступ и изменить их, не создавая экземпляр класса следующим образом:
class foo:
bar = []
foo.bar = [x]
Здесь есть две вещи:
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 []
Надеюсь, это прояснит ситуацию.
Хотя прошло много времени и было сказано много правильных вещей, нет ответа, который объединяет оба эффекта.
У вас есть 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).
Кстати: +=
Оператор называется "расширенным назначением" и, как правило, предназначен для выполнения модификаций на месте, насколько это возможно.
Другие ответы, по-видимому, в значительной степени охватывают это, хотя, кажется, стоит цитировать и ссылаться на PEP 203 " Расширенные назначения":
Они [расширенные операторы присваивания] реализуют тот же оператор, что и их обычная двоичная форма, за исключением того, что операция выполняется "на месте", когда объект левой стороны поддерживает ее, и что левая часть вычисляется только один раз.
...
Идея расширенного присваивания в Python заключается в том, что это не просто более простой способ записать обычную практику хранения результата бинарной операции в его левом операнде, но также способ для левого операнда, о котором идет речь Знайте, что он должен работать "над собой", а не создавать измененную копию самого себя.
>>> 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://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/
>>> 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])