5
>>> a = [1,2,3,4,5,]
>>> for x in a:   x=1
>>> 
>>> a
[1, 2, 3, 4, 5]
>>> 

Вопрос - почему а не равен [1,1,1,1,1]?? Неужели в цикле создается копия для каждого х?

как сделать "правильно" я понимаю. Просто интересен механизм цикла ...ведь для списка работает:

>>> a = [[1],[2],[3]]
>>> for x in a: x.append(33)

>>> 
>>> a
[[1, 33], [2, 33], [3, 33]]

а присваивание тоже "не работает" со ссылкой ...

>>> a
[[1, 33], [2, 33], [3, 33]]
>>> for x in a:  x = [22]

>>> 
>>> a
[[1, 33], [2, 33], [3, 33]]
>>> 

Ладно - последние два примера, которые мне вообще кажутся странными:

>>> a = [[1],[2],[3]]
>>> a
[[1], [2], [3]]
>>> for x in a: x = x +[11]

>>> a
[[1], [2], [3]]
>>> for x in a: x+=[11]

>>> a
[[1, 11], [2, 11], [3, 11]]
>>> 

где это и как объясняется?

insolor
  • 49,104

2 Answers2

8

Q: Как реализован цикл for?

for x in a: x = 1 можно себе представить как:

it = iter(a)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    else:
        x = 1

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

Q: почему а не равен [1,1,1,1,1]?

a список содержит ссылки на самые обычные объекты. Объект никак не меняется от того, что какие-то списки могут ссылки на него содержать. Поэтому a не равен [1,1,1,1,1] по той же причине что и

x = 2
x = 1

не делает 2 == 1. И даже если бы мы изменяемые объекты здесь использовали, такие как списки, из:

x = [1]
x = [2]

не следует, что [1] == [2].

Подробнее: на первой итерации x = next(it) заставляет x имя ссылаться на тот же объект, что и a[0], то есть x is a[0] в этот момент. Затем x = 1 заставляет x имя ссылаться на единичку. Можно себе это представлять, как ярлык с одного предмета на другой переместили:

c одного объекта a=1 a1tag на другой объект ярлык перевесили a1a2tag a=2 . От этого изначальный объект никак не меняется, иначе даже после for x in L: pass у вас стал бы весь список последнему элементу равен L = [5,5,5,5,5].

два примера, которые мне вообще кажутся странными:

Для примера a = [[1],[2],[3]] циклы с x = x + [1] против x += [1] неравнозначны.

Первый случай аналогичен предыдущим (простое присваивание): сумма x + [1] создаёт новый список, к которому прикрепляется имя x — ярлык со старого списка снимается и переносится на новый.

Второй пример использует += операцию, которая для списков по месту изменение проводит — это равнозначно x.extend([1]), что не создаёт новый список, а добавляет элементы из аргумента к старому списку. В последнем случае и до и после x имя продолжает ссылаться на один и тот же объект (тот же список).

где это и как объясняется?

Чтобы узнать, что += делает, просто вызовите help('+=') в Питон REPL. Конкретно для изменяемых последовательностей (таких как списки) поведение s += t равнозначно s.extend(t).

jfs
  • 52,361
5

Каждый раз в переменную записывается значение из списка (не копия), но при записи другого значения в эту же переменную, значение записывается именно в переменную, а не в список.

Правильно делать так:

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

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

print(a)  # [1, 1, 1, 1, 1]

Можно удостовериться, что в переменную попадает не копия, а сам объект из списка. Только список лучше брать из чисел больше 256, т.к. для Python каждое число от -5 до 256 является одним и тем же объектом, т.е.

>>> a = 256
>>> b = 256
>>> a == b
True
>>> a is b
True  # один и тот же объект

>>> a = 257
>>> b = 257
>>> a == b
True
>>> a is b
False  # разные объекты

Внимание: данное поведение не гарантируется, в разных реализациях и версиях языка внутренняя реализация объектов целых чисел может различаться.

Для примера берем числа от 1000 до 1004:

>>> a = list(range(1000, 1005))

>>> a
[1000, 1001, 1002, 1003, 1004]

>>> for i, x in enumerate(a):
...     print(a[i] is x)
True
True
True
True
True

Видим, что в переменной x каждый раз оказывается тот же объект, что и в списке по индексу.

Обновление.

По поводу append. В самом первом примере вы изменяете значение в переменной, никак не связанной со списком, по сути просто в переменную записываете новое значение (переменная сначала "указывала" на объект внутри списка, а после присваивания начала указывать на другой объект, список при этом не изменяется).

В случае с append, вы изменяете сам объект (его "внутренности"), полученный из списка (как я уже показал выше, при итерации по списку в переменную попадает сам объект, а не его копия). Т.к. это тот же самый объект, что лежит в списке, то видим, что и в самом списке объект меняется.

Обновление 2

= и += это разные операторы, работающие по-разному. += может изменять исходный объект (если объект изменяемый, например если это список, но не число, ну и в зависимости от реализации метода __iadd__, если это какой-то свой объект), а = явно заменяет значение переменной, но не изменяет объект, который лежал в переменной до присваивания.

insolor
  • 49,104
  • Мне кажется, в ответе стоит как-либо упомянуть термин «ссылка» – andreymal Nov 28 '17 at 16:50
  • 1
    @andreymal, я думаю не стоит, чтобы не путать с ссылками в C++, где ссылки работают совершенно не так, как переменные в Python. – insolor Nov 28 '17 at 16:51
  • Хм, кроме того, что в C++ ссылку нельзя заменить на другую, ещё какие-то отличия есть? – andreymal Nov 28 '17 at 16:52
  • @andreymal, если в функцию значение передано по ссылке, то при изменении значения внутри функции значение изменится и в переменной снаружи функции. В Python такое не сработает. – insolor Nov 28 '17 at 16:53
  • @insolor, спасибо за ответ... но я - хотя это и не по правилам - расширил вопрос... можете прояснить с аппендом? – Vasyl Kolomiets Nov 28 '17 at 16:54
  • @insolor Потому что в Python это так-то создание новой переменной, а не замена ссылки :) Короче, отличий между ссылками в C++ и Python нет, кроме синтаксических :) – andreymal Nov 28 '17 at 16:54
  • @andreymal, в Python нет ссылок, а есть переменные. Да, по сути это ссылки, но не те ссылки, что в C++. – insolor Nov 28 '17 at 16:56
  • @insolor ну, реальную разницу между ними вы мне так и не привели ¯\(ツ) – andreymal Nov 28 '17 at 16:57
  • @andreymal, привел, это реальная разница. – insolor Nov 28 '17 at 16:57
  • @insolor это разница в синтаксисе, не имеющая никакого отношения к ссылкам. В питоне можно создавать новую переменную, перекрывающую старую (точнее, перекрывающую аргумент функции), а в C++ нельзя. Ссылок это никак не касается. Я продолжу считать ссылки в Python ссылками и учить всех называть их именно так :) – andreymal Nov 28 '17 at 16:58
  • @andreymal, ваше право. – insolor Nov 28 '17 at 17:02
  • 1
    @VasylKolomiets, дополнил ответ. – insolor Nov 28 '17 at 17:02
  • «если в функцию значение передано по ссылке, то при изменении ... значение изменится и .. снаружи функции. В Python такое не сработает. » — кстати, я тут задумался, а ведь это утверждение некорректно. И в C++, и в Python можно изменять значение по ссылке, если это значение изменяемо. В питоне неизменяемы числа, строки и.т. п., в то время как в C++ изменяемо всё. В питоне можно передать массив в функцию, и функция его может его изменить, и массив изменится и снаружи тоже. И в C++ так тоже можно. То есть разницы между ссылками опять нет, а вы ещё и вводите людей, включая меня, в заблуждение) – andreymal Nov 28 '17 at 17:09
  • Благодарю, @insolor. последний раз дополнил вопрос )))) получается, что интерпретатор "видя присваивание" делает запись в другой объект, создавая его на лету? Бред. Или я так и не понял... – Vasyl Kolomiets Nov 28 '17 at 17:12
  • 2
    @VasylKolomiets, ну так = и += это разные операторы, работающие по-разному. += может изменять исходный объект (если он изменяемый, например если это список, но не число, ну и в зависимости от реализации метода __iadd__, если это какой-то свой объект), а = явно заменяет значение переменной, но не изменяет объект, который лежал в переменной до присваивания. – insolor Nov 28 '17 at 17:19
  • @insolor. Да. дошло. но это как-то неожиданно... Тут надо быть начеку... – Vasyl Kolomiets Nov 28 '17 at 17:22
  • когда обсуждаете проблемы связанные с присваиванием, используйте термин "имя" вместо "переменная". В общем случае (когда проблема не относится к именно к употреблению имён в Питоне) можно использовать менее специфичную для Питона терминологию. Other languages have "variables". Кстати, не стоит особенности реализации какой-либо специфичной версии CPython выдавать за общие факты о Питоне (кэширование -5..256 не гарантировано языком) – jfs Nov 30 '17 at 11:16
  • @andreymal: С++ чрезвычайно сложный язык, поэтому не стоит из него никакие понятия приводить. Если не упоминать C++, то слово ссылка можно использовать (спецификация Питона явно говорит, что контейнеры [такие как список] содержат ссылки). – jfs Nov 30 '17 at 11:22
  • @jfs это к insolor, он речь про C++ завёл) – andreymal Nov 30 '17 at 11:27