Чому + = поводиться несподівано у списках?


118

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

+=Оператор називається «з'єднання оператор присвоювання».


побачити різницю між "продовжити" та "додати" у списку
N 1.1,

3
Я не думаю, що це свідчить про щось не так з Python. Більшість мов навіть не дозволяють вам використовувати +оператор у масивах. Я думаю, що в цьому випадку є сенс, який +=би додався.
Skilldrick

4
Офіційно це називається «розширене завдання».
Martijn Pieters

Відповіді:


138

Загальна відповідь полягає в тому, що +=намагається викликати __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є рівнозначними. Це те, що дозволяє використовувати +=для непорушних типів, що може здатися дивним дизайнерським рішенням, поки ви не зважаєте на те, що в іншому випадку ви не могли б використовувати такі +=непорушні типи, як числа!


4
Існує також __radd__метод, який іноді можна назвати (він стосується виразів, які переважно включають підкласи).
jfs

2
В перспективі: + = корисно, якщо важлива пам'ять та швидкість
Норфельдт,

3
Знаючи, що +=насправді розширює список, це пояснює, чому x = []; x = x + {}дає TypeErrorчас x = []; x += {}лише повертається [].
zezollo

96

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


7
Це не завжди вірно: a = 1; a + = 1; є дійсним Python, але в ints немає жодних методів "extension ()". Ви не можете це узагальнити.
e-satis

2
Зробив кілька випробувань, Скотт Гріффітс зрозумів це правильно, тож -1 для вас.
e-satis

11
@ e-statis: ОП чітко говорила про списки, і я чітко заявив, що я говорю і про списки. Я нічого не узагальнюю.
AndiDog

Вилучено -1, відповідь добре проаналізована. Я все ще думаю, що відповідь Гріффітса є кращою.
e-satis

Спочатку дивно думати, що a += bце відрізняється від a = a + bдвох списків aі b. Але це має сенс; extendЧастіше буде задумане робити зі списками, а не створювати нову копію всього списку, яка матиме більш високу часову складність. Якщо розробникам потрібно бути обережними, щоб вони не змінювали оригінальні списки на місці, то кортежі є кращим варіантом - незмінні об’єкти. +=з кортежами не може змінити початковий кортеж.
Pranjal Mittal

22

Проблема тут полягає в тому, що barвін визначається як атрибут класу, а не змінна інстанція.

В foo, атрибут класу модифікується в initметоді, тому впливають всі екземпляри.

У foo2змінної примірника визначається за допомогою атрибуту класу (порожній), і кожен екземпляр отримує свій власний bar.

"Правильною" реалізацією було б:

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

Звичайно, атрибути класу є абсолютно законними. Насправді ви можете отримати доступ до них та змінити їх, не створюючи примірник класу, як цей:

class foo:
    bar = []

foo.bar = [x]

8

Тут беруть участь дві речі:

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

Сподіваюсь, це очищує речі.


5

Хоча минуло багато часу і було сказано багато правильних речей, немає жодної відповіді, які зв'язують обидва ефекти.

У вас є 2 ефекти:

  1. "особлива", можливо, непомітна поведінка списків +=(як це заявив Скотт Гріффітс )
  2. той факт, що залучаються атрибути класу, а також атрибути екземпляра (як заявляє Кан Берк Бюдер )

У класі foo, то __init__метод змінює атрибут класу. Це тому, що self.bar += [x]перекладається на self.bar = self.bar.__iadd__([x]). __iadd__()призначений для зміни на місці, тому він змінює список і повертає посилання на нього.

Зауважте, що дікт екземпляра модифікований, хоча це, як правило, не буде необхідним, оскільки дікт класу вже містить те саме призначення. Тож ця деталь залишається майже непоміченою - за винятком випадків, коли ви займаєтесь foo.bar = []згодом. Тут екземпляри barзалишаються однаковими завдяки зазначеному факту.

У класі foo2, однак, клас barвикористовується, але не торкається. Натомість [x]до нього додається a , утворюючи новий об'єкт, як self.bar.__add__([x])тут називають, який не змінює об'єкт. Результат вводиться в дікт екземпляра, надаючи екземпляру новий список як дикт, а атрибут класу залишається модифікованим.

Відмінність між ними ... = ... + ...та їх призначеннями також ... += ...впливає:

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).

BTW: +=Оператор називається "розширеним призначенням" і, як правило, призначений робити зміни, наскільки це можливо.


5

Інші відповіді, здається, в значній мірі охоплюють це, хоча, здається, варто навести цитування та посилання на Доповнені завдання PEP 203 :

Вони [оператори розширеного призначення] реалізують той самий оператор, що і їх звичайний бінарний вигляд, за винятком того, що операція виконується "на місці", коли об'єкт лівої сторони підтримує її, і ліва частина оцінюється лише один раз.

...

Ідея розширеного призначення в Python полягає в тому, що це не просто простий спосіб записати загальну практику зберігання результату бінарної операції в її лівому операнді, але і спосіб для лівого операнда, про який йде мова, знайте, що він повинен працювати "на себе", а не створювати модифіковану копію себе.


1
>>> 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])

0
>>> 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://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Також зверніться до URL-адреси нижче, щоб зрозуміти дрібну копію та глибоку копію

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.