Я только что понял, что в Python, если кто-то пишет
for i in a:
i += 1
Элементы исходного списка a
фактически не будут затронуты вообще, поскольку переменная i
оказывается просто копией исходного элемента в a
.
Для того, чтобы изменить оригинальный элемент,
for index, i in enumerate(a):
a[index] += 1
будет необходимо.
Я был очень удивлен таким поведением. Это кажется очень нелогичным, по-видимому, отличается от других языков и привело к ошибкам в моем коде, которые мне пришлось долго отлаживать сегодня.
Я читал Учебник по Python раньше. Просто чтобы быть уверенным, я только что проверил книгу только сейчас, и она даже не упоминает об этом поведении вообще.
В чем причина этого дизайна? Ожидается ли, что это будет стандартная практика на многих языках, так что учебник считает, что читатели должны понимать это естественно? На каких других языках присутствует такое же поведение на итерации, на что я должен обратить внимание в будущем?
i
является неизменным или вы выполняете операцию без мутаций. С вложенным спискомfor i in a: a.append(1)
было бы другое поведение; Python не копирует вложенные списки. Однако целые числа неизменны, и сложение возвращает новый объект, но не меняет старый.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. То же самое в C #i = i + 1
повлиятьa
?Ответы:
Я уже отвечал на аналогичный вопрос в последнее время, и очень важно понимать, что
+=
может иметь разные значения:Если тип данных реализует сложение на месте (т. Е. Имеет правильно работающую
__iadd__
функцию), то данные, на которые онi
ссылается, обновляются (не имеет значения, находится ли он в списке или где-то еще).Если тип данных не реализует
__iadd__
метод,i += x
оператор является просто синтаксическим сахаромi = i + x
, поэтому создается новое значение и присваивается имя переменнойi
.Если тип данных реализует,
__iadd__
но он делает что-то странное. Вполне возможно, что он обновляется ... или нет - это зависит от того, что там реализовано.Целые числа, числа с плавающей точкой, строки Pythons не реализуются,
__iadd__
поэтому они не будут обновлены на месте. Однако другие типы данных, такие какnumpy.array
илиlist
s, реализуют это и будут вести себя так, как вы ожидали. Так что это не вопрос копирования или отсутствия копирования при итерации (обычно это не делает копии дляlist
s иtuple
s - но это также зависит от реализации контейнеров__iter__
и__getitem__
метода!) - это больше зависит от типа данных вы сохранили в своемa
.источник
Уточнение - терминология
Python не различает понятия ссылки и указателя . Обычно они просто используют термин « ссылка» , но если вы сравните с такими языками, как C ++, у которых есть такое различие - это намного ближе к указателю .
Поскольку спрашивающий явно исходит из фона C ++, и поскольку это различие - которое требуется для объяснения - не существует в Python, я решил использовать терминологию C ++, которая:
void foo(int x);
является сигнатурой функции, которая получает целое число по значению .void foo(int* x);
является сигнатурой функции, которая получает целое число по указателю .void foo(int& x);
является сигнатурой функции, которая получает целое число по ссылке .Что вы имеете в виду "отличается от других языков"? Большинство языков, которые, как мне известно, поддерживают циклы для каждого, копируют элемент, если не указано иное.
Специально для Python (хотя многие из этих причин могут относиться к другим языкам с похожими архитектурными или философскими концепциями):
Такое поведение может вызвать ошибки у людей, которые не знают об этом, но альтернативное поведение может вызвать ошибки даже у тех, кто знает об этом. Когда вы присваиваете переменную (
i
), вы обычно не останавливаетесь и учитываете все другие переменные, которые будут изменены из-за этого (a
). Ограничение области действия, над которой вы работаете, является основным фактором предотвращения спагетти-кода, и поэтому итерация по копии обычно используется по умолчанию даже в языках, которые поддерживают итерацию по ссылке.Переменные Python всегда являются одним указателем, поэтому итерация по копии обходится дешевле - дешевле, чем итерация по ссылке, что потребует дополнительной отсрочки при каждом доступе к значению.
Python не имеет понятия ссылочных переменных, таких как, например, C ++. То есть все переменные в Python на самом деле являются ссылками, но в том смысле, что они являются указателями, а не закулисными ссылками на constat, такими как
type& name
аргументы C ++ . Так как эта концепция не существует в Python, реализация итерации по ссылке - не говоря уже о том, чтобы сделать ее по умолчанию! - потребует добавления большей сложности к байт-коду.for
Заявление Python работает не только над массивами, но и над более общей концепцией генераторов. За кулисами Python вызываетiter
ваши массивы, чтобы получить объект, который - когда вы вызываетеnext
его - либо возвращает следующий элемент, либоraise
saStopIteration
. Есть несколько способов реализовать генераторы в Python, и было бы намного сложнее реализовать их для итерации по ссылке.источник
*it = ...
- но этот вид синтаксиса уже указывает, что вы изменяете что-то где-то еще - что делает причину № 1 меньшей проблемой. Причины № 2 и № 3 также не применяются, потому что в С ++ копирование обходится дорого, и существует понятие ссылочных переменных. Что касается причины № 4 - возможность возврата ссылки позволяет простую реализацию для всех случаев.Ни один из ответов здесь не дает вам никакого кода для работы, чтобы действительно проиллюстрировать, почему это происходит на земле Python. И это интересно смотреть на более глубокий подход, так что здесь идет.
Основная причина того, что это не работает так, как вы ожидаете, заключается в том, что в Python, когда вы пишете:
это не делает то, что вы думаете, что делает. Целые числа неизменны. Это можно увидеть, когда вы посмотрите, что на самом деле представляет собой объект в Python:
Функция id представляет собой уникальное и постоянное значение для объекта в его жизни. Концептуально он слабо сопоставляется с адресом памяти в C / C ++. Выполнение приведенного выше кода:
Это означает, что первое
a
больше не совпадает со вторымa
, потому что их идентификаторы разные. Фактически они находятся в разных местах памяти.Однако с объектом все работает иначе. Я переписал
+=
оператор здесь:Выполнение этого приводит к следующему выводу:
Обратите внимание, что атрибут id в этом случае фактически одинаков для обеих итераций, даже если значение объекта различно (вы также можете найти
id
значение int, которое содержит объект, которое будет меняться по мере его изменения - потому что целые числа неизменны).Сравните это с тем, когда вы выполняете то же упражнение с неизменным объектом:
Это выводит:
Несколько вещей здесь, чтобы заметить. Во-первых, в цикле с
+=
, вы больше не добавляете к исходному объекту. В этом случае, поскольку целые числа входят в число неизменяемых типов в Python , python использует другой идентификатор. Также интересно отметить, что Python использует один и тот же базовый элементid
для нескольких переменных с одинаковым неизменным значением:tl; dr - Python имеет несколько неизменяемых типов, которые вызывают поведение, которое вы видите. Для всех изменчивых типов ваше ожидание верное.
источник
Ответ @ Idan хорошо объясняет, почему Python не обрабатывает переменную цикла как указатель так, как вы это делаете в C, но стоит более подробно объяснить, как распаковываются фрагменты кода, поскольку в Python много простых на первый взгляд битов. кода на самом деле будут вызовы встроенных методов . Взять ваш первый пример
Необходимо распаковать две вещи:
for _ in _:
синтаксис и_ += _
синтаксис. Прежде всего, для цикла for, как и в других языках, в Python естьfor-each
цикл, который по сути является синтаксическим сахаром для шаблона итератора. В Python итератор - это объект, который определяет.__next__(self)
метод, который возвращает текущий элемент в последовательности, переходит к следующему и вызывает значение,StopIteration
когда в последовательности больше нет элементов. Iterable это объект , который определяет.__iter__(self)
метод , который возвращает итератор.(Примечание: an
Iterator
также является anIterable
и возвращает себя из своего.__iter__(self)
метода.)Python обычно имеет встроенную функцию, которая делегирует пользовательский метод двойного подчеркивания. Так что есть то,
iter(o)
что разрешаетo.__iter__()
иnext(o)
что решаетo.__next__()
. Обратите внимание, что эти встроенные функции часто пытаются использовать разумное определение по умолчанию, если метод, которому они делегируют, не определен. Например,len(o)
обычно разрешается,o.__len__()
но если этот метод не определен, он попытаетсяiter(o).__len__()
.Для цикла, по существу , определяется в терминах
next()
,iter()
и более основных структур управления. В общем кодбудет распакован что-то вроде
Так что в этом случае
распаковывается в
Другая половина этого есть
i += 1
. В общем%ASSIGN% += %EXPR%
распаковывается%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Здесь__iadd__(self, other)
делает на месте сложение и возвращает себя.(NB. Это еще один случай, когда Python выберет альтернативу, если основной метод не определен. Если объект не реализует,
__iadd__
он откатится назад__add__
. Он фактически делает это в этом случае, так какint
не реализует__iadd__
- что имеет смысл, потому что они являются неизменными и поэтому не могут быть изменены на месте.)Итак, ваш код здесь выглядит
где мы можем определить
В вашем втором фрагменте кода происходит нечто большее. Две новые вещи, которые мы должны знать, это то, что они
%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__
потому, что__iadd__
не реализуются целыми числами). Наш окончательный код выглядит так:На самом деле ответить на ваш вопрос, почему первый один не изменяет список , а второй делает в нашем первом фрагменте кода мы получаем
i
отnext(_a_iter)
, а это значит ,i
будетint
. Такint
как нельзя изменить на месте,i += 1
ничего не делает в списке. Во втором случае мы снова не модифицируем,int
а модифицируем список, вызывая__setitem__
.Причина всего этого сложного упражнения в том, что я думаю, что он преподает следующий урок о Python:
Методы двойного подчеркивания являются препятствием при запуске, но они необходимы для поддержки репутации Python "runnable pseudocode". Приличный программист на Python хорошо разберется в этих методах и способах их вызова и определит их везде, где это имеет смысл.
Редактировать : @deltab исправил мое небрежное использование термина «коллекция».
источник
__len__
и__contains__
+=
работает по-разному в зависимости от того, является ли текущее значение изменчивым или неизменным . Это было главной причиной того, что его реализация в Python выглядит долгое время, так как разработчики Python боялись, что это будет сбивать с толку.Если
i
int, то его нельзя изменить, так как int неизменны, и, следовательно, если значениеi
изменяется, то оно обязательно должно указывать на другой объект:Однако, если левая часть является изменчивой , то + = может фактически изменить ее; как если бы это был список:
В вашем цикле for,
i
относится к каждому элементу поa
очереди. Если это целые числа, то применяется первый случай, и результатомi += 1
должно быть то, что он ссылается на другой целочисленный объект. Список,a
конечно, по-прежнему имеет те же элементы, что и всегда.источник
i = 1
устанавливаетсяi
неизменяемый целочисленный объект, тоi = []
следует устанавливатьi
неизменяемый объект списка. Другими словами, почему целочисленные объекты неизменяемы и объекты списков изменчивы? Я не вижу никакой логики за этим.list
реализует методы, которые изменяют его содержимое,int
нет.[]
является объектом изменяемого списка, иi = []
позволяетi
ссылаться на этот объект.+=
оператор / метод, чтобы вести себя одинаково (принцип наименьшего удивления) для обоих типов: либо изменить исходный объект, либо вернуть измененную копию как для целых чисел, так и для списков.+=
удивительно в Python, но чувствовалось, что другие упомянутые вами варианты также были бы удивительными или, по крайней мере, менее практичными (изменение исходного объекта не может быть выполнено с использованием наиболее распространенного типа значения). Вы используете + = с, ints. И копирование всего списка намного дороже, чем его изменение, Python не копирует такие вещи, как списки и словари, если это явно не указано). Тогда это была огромная дискуссия.Цикл здесь не имеет значения. Подобно параметрам или аргументам функции, установка подобного цикла по сути является просто необычным назначением.
Целые числа неизменны. Единственный способ изменить их - создать новое целое число и присвоить ему то же имя, что и у оригинала.
Семантика Python для присвоения отображается непосредственно на C (неудивительно, учитывая указатели Pyybject * CPython), с единственными оговорками, что все является указателем, и вам не разрешено иметь двойные указатели. Рассмотрим следующий код:
Что случилось? Это печатает
1
. Зачем? На самом деле это примерно эквивалентно следующему коду C:В коде C очевидно, что значение
a
полностью не затронуто.Что касается того, почему списки, кажется, работают, ответ в основном заключается в том, что вы присваиваете одно имя. Списки изменчивы. Идентификация названного объекта
a[0]
изменится, ноa[0]
все еще будет действительным именем. Вы можете проверить это с помощью следующего кода:Но это не специально для списков. Замените
a[0]
этот код на,y
и вы получите точно такой же результат.источник