Вопрос-Ответ

Why does += behave unexpectedly on lists?

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

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

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

2023-07-17 15:58 python