Почему b + = (4,) работает, а b = b + (4,) не работает, когда b является списком?

77

Если мы возьмем, b = [1,2,3]и если мы попытаемся сделать:b+=(4,)

Он возвращается b = [1,2,3,4], но если мы попытаемся b = b + (4,)это сделать, это не сработает.

b = [1,2,3]
b+=(4,) # Prints out b = [1,2,3,4]
b = b + (4,) # Gives an error saying you can't add tuples and lists

Я ожидал b+=(4,)потерпеть неудачу, так как вы не можете добавить список и кортеж, но это сработало. Поэтому я пытался b = b + (4,)ожидать того же результата, но это не сработало.

Супун Дасанта Куруппу
источник
4
Я верю, что ответ можно найти здесь .
19
Сначала я неправильно понял это и попытался закрыть его как слишком широкий, затем отозвал его. Тогда я подумал, что это должен быть дубликат, но я не только не смог повторно подать голос, я вырвал свои волосы, пытаясь найти другие ответы, подобные этим. : /
Карл
Очень похожий вопрос: stackoverflow.com/questions/58048664/…
sanyash

Ответы:

70

Проблема с вопросами «почему» заключается в том, что обычно они могут означать несколько разных вещей. Я постараюсь ответить на каждый вопрос, который, я думаю, вы могли иметь в виду.

"Почему это может работать по-другому?" на что отвечает, например, это . По сути, +=пытается использовать различные методы объекта: __iadd__(который проверяется только с левой стороны), vs __add__и __radd__(«обратное добавление», проверяется с правой стороны, если с левой стороны нет __add__) для +.

«Что именно делает каждая версия?» Короче говоря, list.__iadd__метод делает то же самое, что и list.extend(но из-за языкового дизайна, есть еще задание назад).

Это также означает, например, что

>>> a = [1,2,3]
>>> b = a
>>> a += [4] # uses the .extend logic, so it is still the same object
>>> b # therefore a and b are still the same list, and b has the `4` added
[1, 2, 3, 4]
>>> b = b + [5] # makes a new list and assigns back to b
>>> a # so now a is a separate list and does not have the `5`
[1, 2, 3, 4]

+Конечно, создает новый объект, но явно требует другой список вместо того, чтобы пытаться извлечь элементы из другой последовательности.

«Почему это полезно для + =? Это более эффективно; extendметод не должен создавать новый объект. Конечно, иногда это дает некоторые неожиданные эффекты (как выше), и в целом Python на самом деле не касается эффективности. Но эти решения были приняты давно.

«В чем причина запрета добавления списков и кортежей с +?» Смотрите здесь (спасибо, @ splash58); одна идея состоит в том, что (tuple + list) должен производить тот же тип, что и (list + tuple), и не ясно, какой тип должен быть результатом. +=не имеет этой проблемы, потому что, a += bочевидно, не должен менять тип a.

Карл Кнехтель
источник
2
Нормально, совершенно верно. И списки не используют |, так что это как бы разрушает мой пример. Если позже я подумаю о более ясном примере, я его поменяю.
Карл
1
Кстати, |для множеств является коммутирующим оператором, но +для списков - нет. По этой причине я не думаю, что аргумент о неоднозначности типов особенно силен. Поскольку оператор не коммутирует, зачем требовать то же самое для типов? Можно просто согласиться с тем, что результат имеет тип lhs. С другой стороны, ограничивая list + iterator, разработчик должен быть более откровенным в своих намерениях. Если вы хотите создать новый список , который содержит материал , из aпродолженную материала из bуже есть способ сделать это: new = a.copy(); new += b. Это еще одна линия, но кристально чистая.
a_guest
Причина, по которой a += bведет себя иначе, чем a = a + bне эффективность. Это на самом деле, что Гвидо посчитал выбранное поведение менее запутанным. Представьте себе функцию, получающую список в aкачестве аргумента, а затем выполняющую a += [1, 2, 3]. Этот синтаксис, безусловно, выглядит так, как будто он изменяет список на месте, а не создает новый список, поэтому было принято решение, что он должен вести себя в соответствии с тем, что большинство людей думает об ожидаемом результате. Тем не менее, механизм также должен был работать для неизменяемых типов, таких как ints, что привело к текущему дизайну.
Свен Марнач
Я лично думаю, что дизайн на самом деле более запутанный, чем просто a += bсокращение a = a + b, как это сделала Руби, но я могу понять, как мы туда попали.
Свен Марнач
21

Они не эквивалентны:

b += (4,)

является сокращением для:

b.extend((4,))

в то время как +объединяет списки, поэтому:

b = b + (4,)

вы пытаетесь объединить кортеж в список

alfasin
источник
14

Когда вы делаете это:

b += (4,)

преобразуется в это:

b.__iadd__((4,)) 

Под капотом он вызывает b.extend((4,)), extendпринимает итератор и вот почему это тоже работает:

b = [1,2,3]
b += range(2)  # prints [1, 2, 3, 0, 1]

но когда вы делаете это:

b = b + (4,)

преобразуется в это:

b = b.__add__((4,)) 

принять только список объектов.

Charif DZ
источник
4

Из официальных документов для изменяемых типов последовательностей оба:

s += t
s.extend(t)

определяются как:

распространяется sс содержаниемt

Что отличается от того, что определяется как:

s = s + t    # not equivalent in Python!

Это также означает , что будет работать любой тип последовательностиt , включая кортеж, как в вашем примере.

Но это также работает для диапазонов и генераторов! Например, вы также можете сделать:

s += range(3)
Желудь
источник
3

Подобные «расширенные» операторы присваивания +=были введены в Python 2.0, который был выпущен в октябре 2000 года. Дизайн и обоснование описаны в PEP 203 . Одной из заявленных целей этих операторов была поддержка операций на месте. Письмо

a = [1, 2, 3]
a += [4, 5, 6]

предполагается обновить список a на месте . Это имеет значение, если есть другие ссылки на список a, например, когда aбыл получен в качестве аргумента функции.

Однако операция не всегда может происходить на месте, так как многие типы Python, включая целые числа и строки, являются неизменяемыми , поэтому, например, i += 1для целого числа iне может работать на месте.

Таким образом, операторы расширенного присваивания должны были работать, когда это возможно, и создавать новый объект в противном случае. Для облегчения этих целей проектирования выражение x += yбыло задано следующим образом:

  • Если x.__iadd__определено, x.__iadd__(y)оценивается.
  • В противном случае, если x.__add__реализуется x.__add__(y), оценивается.
  • В противном случае, если y.__radd__реализуется y.__radd__(x), оценивается.
  • В противном случае выведите ошибку.

Первый результат, полученный этим процессом, будет возвращен x(если только этот результат не является NotImplementedодиночным, в этом случае поиск продолжается со следующего шага).

Этот процесс позволяет реализовывать типы, которые поддерживают модификацию на месте __iadd__(). Типы, которые не поддерживают модификацию на месте, не должны добавлять какие-либо новые магические методы, так как Python автоматически вернется к существенному x = x + y.

Итак, давайте наконец подойдем к вашему актуальному вопросу - почему вы можете добавить кортеж в список с помощью оператора расширенного присваивания. По памяти история этого была примерно такой: list.__iadd__()метод был реализован для простого вызова уже существующего list.extend()метода в Python 2.0. Когда в Python 2.1 были представлены итераторы, list.extend()метод был обновлен, чтобы принимать произвольные итераторы. Конечным результатом этих изменений было то, что my_list += my_tupleработало начиная с Python 2.1. Однако list.__add__()метод никогда не должен был поддерживать произвольные итераторы в качестве правого аргумента - это считалось неуместным для строго типизированного языка.

Лично я считаю, что реализация расширенных операторов оказалась слишком сложной в Python. У него много неожиданных побочных эффектов, например, этот код:

t = ([42], [43])
t[0] += [44]

Возникает вторая строка TypeError: 'tuple' object does not support item assignment, но операция все равно успешно выполняется - tбудет ([42, 44], [43])после выполнения строки, которая вызывает ошибку.

Свен Марнах
источник
Браво! Наличие ссылки на ПКП особенно полезно. Я добавил ссылку на другом конце к предыдущему вопросу SO о поведении списка в кортеже. Когда я смотрю на то , что Python был похож до 2.3 или так, что кажется практически непригодным для использования по сравнению с сегодня ... (и я до сих пор имею смутное воспоминание о попытке и не в состоянии получить 1,5 сделать что - нибудь полезное на очень старый Mac)
Карл
2

Большинство людей ожидают, что X + = Y будет эквивалентно X = X + Y. Действительно, в Справочнике по карманам Python (4-е издание) Марка Латца говорится на странице 57 «Следующие два формата примерно эквивалентны: X = X + Y, X + = Y ". Однако люди, которые указали Python, не сделали их эквивалентными. Возможно, это была ошибка, которая приведет к часам отладки разочарованными программистами до тех пор, пока Python остается в использовании, но теперь Python такой же, как и он. Если X является изменяемым типом последовательности, X + = Y эквивалентно X.extend (Y), а не X = X + Y.

zizzler
источник
> Возможно, это была ошибка, из-за которой разочарованные программисты тратят часы на отладку до тех пор, пока Python остается в использовании <- вы действительно страдали из-за этого? Вы, кажется, говорите из опыта. Я бы очень хотел услышать вашу историю.
Veky
1

Как объясняется здесь , если метод arrayне реализует __iadd__, b+=(4,)он будет просто кратким, b = b + (4,)но, очевидно, это не так, так arrayже как и __iadd__метод реализации . Видимо реализация __iadd__метода примерно такая:

def __iadd__(self, x):
    self.extend(x)

Однако мы знаем, что приведенный выше код не является реальной реализацией __iadd__метода, но мы можем предположить и принять, что есть что-то вроде extendметода, который принимает tuppleвходные данные.

Hamidreza
источник