Почему Python создает только копию отдельного элемента при повторе списка?

Я только понял, что в Python, если писать

for i in a:
    i += 1

Элементы исходного списка a на самом деле вообще не будут влиять, так как переменная i оказывается просто копией исходного элемента в a.

Чтобы изменить исходный элемент,

for index, i in enumerate(a):
    a[index] += 1

.

Я был очень удивлен этим поведением. Это кажется очень противоречивым, по-видимому, отличным от других языков и привело к ошибкам в моем коде, который мне пришлось отлаживать в течение долгого времени сегодня.

Я читал Python Tutorial раньше. Чтобы быть уверенным, я проверил книгу еще раз, и он даже не упоминает об этом.

Какова причина этого дизайна? Ожидается ли, что это стандартная практика на многих языках, поэтому учебник полагает, что читатели должны получить его естественным путем? В каких других языках присутствует такое же поведение на итерации, на что я должен обратить внимание в будущем?

28 голосов | спросил xji 29 Jpm1000000pmSun, 29 Jan 2017 20:32:20 +030017 2017, 20:32:20

6 ответов


68

Я уже ответил на аналогичный вопрос в последнее время, и очень важно понять, что += могут иметь разные значения:

  • Если тип данных реализует добавление на месте (т.е. имеет корректно функционирующую функцию __iadd__), то данные, которые ---- +: = 2 =: + ---- относится к обновлению (не имеет значения, находится ли он в списке или где-то еще).

  • Если тип данных не реализует метод i __iadd__ - это просто синтаксический сахар для i += x, поэтому новое значение создается и назначается имя переменной i = i + x.

  • Если тип данных реализует i, но он делает что-то странное. Возможно, это обновление ... или нет - это зависит от того, что там реализовано.

Целочисленные числа Pythons, float, строки не реализуют __iadd__, чтобы они не обновлялись на месте. Однако другие типы данных, такие как __iadd__ или numpy.array s реализуют его и будут вести себя так, как вы ожидали. Так что это не вопрос копирования или отсутствия копии при итерации (обычно это не делает копии для list s и list s - но это также зависит от реализации контейнеров tuple и __iter__!) - это больше зависит от типа данных, который вы сохранили в своем __getitem__

ответил MSeifert 29 Jpm1000000pmSun, 29 Jan 2017 23:33:41 +030017 2017, 23:33:41
18

Уточнение - терминология

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

Так как искатель явно исходит из фона C ++, и поскольку это различие - которое требуется для объяснения - не существует в Python, я решил использовать терминологию C ++, которая:

  • Значение : фактические данные, которые находятся в памяти. void foo(int x); является сигнатурой функции, которая получает целое число по значению .
  • Указатель : адрес памяти, рассматриваемый как значение. Может быть отложен для доступа к памяти, на которую он указывает. void foo(int* x); является сигнатурой функции, которая получает целое число указателем .
  • Ссылка : Сахар вокруг указателей. Существует указатель за кулисами, но вы можете получить доступ только к отложенному значению и не можете изменить адрес, на который он указывает. void foo(int& x); является сигнатурой функции, которая получает целое число по ссылке .

Что значит «отличное от других языков»? Большинство языков, которые, как я знаю, поддерживают каждую петлю, копируют элемент, если специально не указано иное.

В частности, для Python (хотя многие из этих причин могут применяться к другим языкам с похожими архитектурными или философскими концепциями):

  1. Это может привести к ошибкам для людей, которые не знают об этом, но альтернативное поведение может вызвать ошибки даже для тех, кто знает . Когда вы назначаете переменную (i), вы обычно не останавливаетесь и учитываете все другие переменные, которые будут изменены из-за этого ( ---- +: = 4 = + ----). Ограничение области действия, на которой вы работаете, является основным фактором предотвращения спагетти-кода, поэтому итерация по копии обычно является по умолчанию даже на языках, поддерживающих итерацию по ссылке.

  2. Переменные Python всегда представляют собой один указатель, поэтому дешево итератировать по копиям - дешевле, чем повторять по ссылке, что потребует дополнительной отсрочки при каждом обращении к значению.

  3. Python не имеет понятия ссылочных переменных, например - C ++. То есть все переменные в Python на самом деле являются ссылками, но в том смысле, что они являются указателями, а не заглавными константными ссылками, такими как C ++ a. Поскольку эта концепция не существует в Python, реализация итерации по ссылке - не говоря уже о том, чтобы сделать ее по умолчанию! - потребует дополнительной сложности для байт-кода.

  4. Оператор type& name Python работает не только на массивах, но и на более общей концепции генераторов. За кулисами Python вызывает for на ваших массивах, чтобы получить объект, который при вызове iter - возвращает следующий элемент или next sa raise. Существует несколько способов реализации генераторов в Python, и было бы намного сложнее реализовать их для итерации по ссылке.

ответил Idan Arye 29 Jpm1000000pmSun, 29 Jan 2017 21:04:14 +030017 2017, 21:04:14
12

Ни один из ответов здесь не дает вам никакого кода для работы, чтобы действительно проиллюстрировать why это происходит на земле Python. И это забавно смотреть на более глубокий подход, так что здесь идет.

Основная причина, по которой это не работает, поскольку вы ожидаете, потому что в Python, когда вы пишете:

i += 1

он не делает то, что, по вашему мнению, делает. Целые числа неизменны. Это можно увидеть, когда вы посмотрите, что на самом деле находится на Python:

a = 0
print('ID of the first integer:', id(a))
a += 1
print('ID of the first integer +=1:', id(a))

Функция id представляет собой уникальное и постоянное значение для объекта в его жизни. По идее, он свободно сопоставляется с адресом памяти в C /C ++. Выполнение приведенного выше кода:

ID of the first integer: 140444342529056
ID of the first integer +=1: 140444342529088

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

Однако с объектом все работает по-другому. Я перезаписал здесь оператор +=:

class CustomInt:
  def __iadd__(self, other):
    # Override += 1 for this class
    self.value = self.value + other.value
    return self

  def __init__(self, v):
    self.value = v

ints = []
for i in range(5):
  int = CustomInt(i)
  print('ID={}, value={}'.format(id(int), i))
  ints.append(int)


for i in ints:
  i += CustomInt(i.value)

print("######")
for i in ints:
  print('ID={}, value={}'.format(id(i), i.value))

Выполнение этого результата приводит к следующему выводу:

ID=140444284275400, value=0
ID=140444284275120, value=1
ID=140444284275064, value=2
ID=140444284310752, value=3
ID=140444284310864, value=4
######
ID=140444284275400, value=0
ID=140444284275120, value=2
ID=140444284275064, value=4
ID=140444284310752, value=6
ID=140444284310864, value=8

Обратите внимание, что атрибут id в этом случае фактически является тем же для обеих итераций, даже если значение объекта отличается (вы также можете найти id значения int, которое будет выполняться, которое будет меняться, поскольку оно мутирует, потому что целые числа неизменяемы).

Сравните это с тем, когда вы выполняете одно и то же упражнение с неизменяемым объектом:

ints_primitives = []
for i in range(5):
  int = i
  ints_primitives.append(int)
  print('ID={}, value={}'.format(id(int), i))

print("######")
for i in ints_primitives:
  i += 1
  print('ID={}, value={}'.format(id(int), i))


print("######")
for i in ints_primitives:
  print('ID={}, value={}'.format(id(i), i))

Выводится:

ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
######
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ID=140023258889408, value=5
######
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4

Несколько вещей здесь, чтобы заметить. Во-первых, в цикле с кодом += вы больше не добавляете к исходному объекту. В этом случае, поскольку ints относятся к неизменяемым типам в Python , python использует другой идентификатор. Также интересно отметить, что Python использует один и тот же базовый код id для нескольких переменных с тем же неизменяемым значением:

a = 1999
b = 1999
c = 1999

print('id a:', id(a))
print('id b:', id(b))
print('id c:', id(c))

id a: 139846953372048
id b: 139846953372048
id c: 139846953372048

tl; dr - у Python есть несколько неизменных типов, которые вызывают поведение, которое вы видите. Для всех изменяемых типов ваши ожидания верны.

ответил Elysian Fields 30 Jam1000000amMon, 30 Jan 2017 02:35:57 +030017 2017, 02:35:57
6

@ Ответ Идана хорошо объясняет, почему Python не обрабатывает переменную цикла как указатель так, как вы могли бы на C, но стоит более подробно объяснить, как распаковываются фрагменты кода, как в Python много простых - Исходные биты кода будут фактически вызваны на встроенные методы . Чтобы взять свой первый пример

for i in a:
    i += 1

Есть две вещи для распаковки: синтаксис for _ in _: и _ += _. Чтобы сначала взять цикл for, как и другие языки, Python имеет цикл for-each, который по существу является синтаксическим сахаром для шаблона итератора. В Python итератор представляет собой объект, который определяет ---- +: = 4 =: + ---- , который возвращает текущий элемент в последовательности, переходит к следующему и поднимет .__next__(self), когда в последовательности больше нет элементов. Iterable - это объект, который определяет StopIteration, который возвращает итератор.

(NB: a .__iter__(self) также является Iterator и возвращает себя из метода Iterable.)

Python обычно имеет встроенную функцию, которая делегирует пользовательский метод двойного подчеркивания. Таким образом, он имеет .__iter__(self) , который разрешает iter(o) и o.__iter__(), который разрешает next(o). Обратите внимание, что эти встроенные функции часто будут использовать разумное определение по умолчанию, если метод, который они делегируют, не определен. Например, o.__next__() обычно разрешается len(o) но если этот метод не определен, он попробует o.__len__().

A для цикла по существу определяется в терминах iter(o).__len__(), next() и более основных структур управления. В общем случае код

iter()

будет распаковаться на что-то вроде

for i in %EXPR%:
    %LOOP%

Итак, в этом случае

_a_iter = iter(%EXPR%)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    %LOOP%

распаковывается на

for i in a:
    i += 1

Другая половина этого кода _a_iter = iter(a) # = a.__iter__() while True: try: i = next(_a_iter) # = _a_iter.__next__() except StopIteration: break i += 1 . Обычно i += 1 распаковывается на %ASSIGN% += %EXPR%. Здесь %ASSIGN% = %ASSIGN%.__iadd__(%EXPR%) делает дополнение и возвращает себя.

(NB Это другой случай, когда Python будет выбирать альтернативу, если основной метод не определен. Если объект не реализует __iadd__(self, other) он вернется на __iadd__. На самом деле это делает это в этом случае как __add__ не реализует int - это имеет смысл, потому что они неизменяемы и поэтому не могут быть изменены на месте .)

Итак, ваш код выглядит как

__iadd__

, где мы можем определить

_a_iter = iter(a)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    i = iadd(i,1)

В вашем втором бите кода немного больше. Нам нужно знать две новые вещи: def iadd(o, v): try: return o.__iadd__(v) except AttributeError: return o.__add__(v) распаковывается на %ARG%[%KEY%] = %VALUE% и (%ARG%).__setitem__(%KEY%, %VALUE%) распаковывается на %ARG%[%KEY%]. Объединяя эти знания, мы получаем (%ARG%).__getitem__(%KEY%) распаковали до a[ix] += 1 (снова: a.__setitem__(ix, a.__getitem__(ix).__add__(1)), а не__add__, потому что __iadd__ не реализуется ints ). Наш окончательный код выглядит следующим образом:

__iadd__

Чтобы ответить на ваш вопрос о том, почему первый не изменяет список во время второго, в нашем первом фрагменте мы получаем _a_iter = iter(enumerate(a)) while True: try: index, i = next(_a_iter) except StopIteration: break a.__setitem__(index, iadd(a.__getitem__(index), 1)) from i, что означает next(_a_iter) будет i. Поскольку int не может быть изменен на месте, int ничего не делает для списка. В нашем втором случае мы снова не модифицируем i += 1, но изменяем список, вызывая int

Причиной всего этого сложного упражнения является то, что я думаю, что он учит следующему уроку о Python:

  1. Цена на читаемость Python заключается в том, что он называет эти магические методы двойной оценки все время.
  2. Поэтому, чтобы иметь возможность по-настоящему понять какой-либо фрагмент кода Python, вы должны понимать эти переводы, которые он делает.

Двойные методы подчеркивания являются препятствием при запуске, но они необходимы для поддержки репутации «runnable pseudocode» Python. Порядочный программист на Python будет иметь полное понимание этих методов и способов их вызова и будет определять их везде, где это имеет смысл.

Изменить : @deltab скорректировал мое неряшливое использование термина «коллекция».

ответил walpen 29 Jpm1000000pmSun, 29 Jan 2017 23:56:27 +030017 2017, 23:56:27
2

+= работает по-разному в зависимости от того, является ли текущее значение изменчивым или неизменным . Это была основная причина, по которой он долгое время должен был реализоваться в Python, поскольку разработчики Python боялись, что это будет запутать.

Если i - это int, то он не может быть изменен, поскольку ints неизменяемы, и, следовательно, if значение i изменяется, то оно обязательно должно указывать на другой объект:

>>> i=3
>>> id(i)
14336296
>>> i+=1
>>> id(i)
14336272   # Other object

Однако, если левая сторона является изменчивой , то + = может ее реально изменить; например, если это список:

>>> i=[]
>>> id(i)
140257231883944
>>> i+=[1]
>>> id(i)
140257231883944  # Still the same object!

В вашем цикле for, i относится к каждому элементу a по очереди. Если они являются целыми числами, тогда применяется первый случай, и результат i += 1 должен быть таким, что он относится к другому целочисленному объекту. Список a, конечно же, имеет те же самые элементы, которые он всегда имел.

ответил RemcoGerlich 29 Jpm1000000pmSun, 29 Jan 2017 22:35:00 +030017 2017, 22:35:00
1

Цикл здесь не имеет значения. Подобно функциональным параметрам или аргументам, настройка цикла for для этого является по существу просто фантазийным назначением.

Целые числа неизменяемы. Единственный способ изменить их - создать новое целое число и присвоить его тому же имени, что и оригинал.

Семантика Python для сопоставления назначения непосредственно на C (неудивительно заданные указатели PyObject * CPython), при этом единственными предостережениями являются то, что все является указателем, и вам не разрешено иметь двойные указатели. Рассмотрим следующий код:

a = 1
b = a
b += 1
print(a)

Что происходит? Он печатает 1. Зачем? Фактически это примерно эквивалентно следующему C-коду:

i64* a = malloc(sizeof(i64));
*a = 1;
i64* b = a;
i64* tmp = malloc(sizeof(i64));
tmp = *b + 1;
b = tmp;
printf("%d\n", *a);

В C-коде очевидно, что значение a полностью не затронуто.

Что касается того, почему списки, похоже, работают, ответ в основном заключается в том, что вы назначаете одноименное имя. Списки являются изменяемыми. Идентификация объекта с именем a[0] изменится, но a[0] по-прежнему является допустимым именем. Вы можете проверить это с помощью следующего кода:

x = 1
a = [x]
print(a[0] is x)
a[0] += 1
print(a[0] is x)

Но это не относится к спискам. Замените a[0] в этом коде с помощью y и вы получите тот же результат.

ответил Kevin Mills 30 Jpm1000000pmMon, 30 Jan 2017 20:57:52 +030017 2017, 20:57:52

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132