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

Это скорее концептуальный вопрос. Недавно я увидел фрагмент кода в Python (он работал в версии 2.7, и он также мог быть запущен в версии 2.5), в котором цикл for использовал одно и то же имя для списка, который был переименован, и элемента в списке, который поражает меня как плохую практику, так и то, что не должно работать вообще.

Например:

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

Урожайность:

1
2
3
4
5
5

Теперь для меня имеет смысл, что последнее значение будет последним значением, назначенным x из цикла, но я не понимаю, почему вы сможете использовать одно и то же имя переменной для обеих частей for и выполняйте функцию по назначению. Являются ли они в разных областях? Что происходит под капотом, что позволяет что-то подобное работать?

Ответ 1

Что сообщает dis:

Python 3.4.1 (default, May 19 2014, 13:10:29)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from dis import dis
>>> dis("""x = [1,2,3,4,5]
... for x in x:
...     print(x)
... print(x)""")

  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 LOAD_CONST               2 (3)
              9 LOAD_CONST               3 (4)
             12 LOAD_CONST               4 (5)
             15 BUILD_LIST               5
             18 STORE_NAME               0 (x)

  2          21 SETUP_LOOP              24 (to 48)
             24 LOAD_NAME                0 (x)
             27 GET_ITER
        >>   28 FOR_ITER                16 (to 47)
             31 STORE_NAME               0 (x)

  3          34 LOAD_NAME                1 (print)
             37 LOAD_NAME                0 (x)
             40 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             43 POP_TOP
             44 JUMP_ABSOLUTE           28
        >>   47 POP_BLOCK

  4     >>   48 LOAD_NAME                1 (print)
             51 LOAD_NAME                0 (x)
             54 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             57 POP_TOP
             58 LOAD_CONST               5 (None)
             61 RETURN_VALUE

Ключевыми битами являются разделы 2 и 3 - мы выставляем значение из x (24 LOAD_NAME 0 (x)), а затем получаем его итератор (27 GET_ITER) и начинаем итерацию по нему (28 FOR_ITER). Python никогда не возвращается, чтобы снова загрузить итератор.

Кроме того: не имеет смысла делать это, поскольку у него уже есть итератор, и как Абхиджит указывает в своем ответе, раздел 7.3 спецификации Python действительно требует такого поведения).

Когда имя x переписывается, чтобы указать на каждое значение внутри списка, ранее известного как x Python не имеет проблем с поиском итератора, потому что ему никогда не нужно снова искать имя x завершите протокол итерации.

Ответ 2

Использование кода примера в качестве базовой ссылки

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

Я хочу, чтобы вы отсылали раздел 7.3. Инструкция for в руководстве

Выдержка 1

Список выражений оценивается один раз; он должен давать итерабельную объект. Итератор создается для результата expression_list.

Это означает, что ваша переменная x, которая является символическим именем объекта list: [1,2,3,4,5], вычисляется для итерируемого объекта. Даже если переменная, символическая ссылка изменяет свою принадлежность, поскольку список выражений не оценивается снова, нет никакого влияния на итерируемый объект, который уже был оценен и сгенерирован.

Примечание

  • Все в Python - это Object, имеет идентификатор, атрибуты и методы.
  • Переменные - это символическое имя, ссылка на один и только один объект в любом конкретном экземпляре.
  • Переменные во время выполнения могут изменить свою принадлежность, то есть могут ссылаться на какой-то другой объект.

Выдержка 2

Затем пакет выполняется один раз для каждого элемента, предоставленного итератора в порядке возрастания индексов.

Здесь набор относится к итератору, а не к списку выражений. Таким образом, для каждой итерации итератор выполняется, чтобы получить следующий элемент вместо обращения к исходному списку-выражению.

Ответ 3

Для этого нужно работать таким образом, если вы об этом подумаете. Выражение для последовательности цикла for может быть любым:

binaryfile = open("file", "rb")
for byte in binaryfile.read(5):
    ...

Мы не можем запросить последовательность на каждом проходе через цикл, или здесь мы закончим чтение из следующей партии по 5 байт во второй раз. Естественно, Python должен каким-то образом сохранить результат выражения в частном порядке до начала цикла.


Являются ли они в разных областях?

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

x = [1,2,3,4,5]
loc = locals()
for x in x:
    print locals() is loc  # True
    print loc["x"]  # 1
    break

Что происходит под капотом, что позволяет что-то вроде этого работать?

Шон Виейра показал, что происходит под капотом, но чтобы описать его в более читаемом коде python, ваш цикл for по существу эквивалентен этому while:

it = iter(x)
while True:
    try:
        x = it.next()
    except StopIteration:
        break
    print x

Это отличается от традиционного подхода индексирования к итерации, который вы видели в более старых версиях Java, например:

for (int index = 0; index < x.length; index++) {
    x = x[index];
    ...
 }

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

Однако при первом подходе первая строка (it = iter(x)) запрашивает объект iterator, который фактически отвечает за предоставление следующий пункт с этого момента. Последовательность, в которой x изначально указывала, что больше не нужно обращаться напрямую.

Ответ 4

Это разница между переменной (x) и объектом, на которую она указывает (список). Когда цикл for начинается, Python захватывает внутреннюю ссылку на объект, на который указывает x. Он использует объект, а не тот, к которому х ссылается в любой момент времени.

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

Ответ 5

В основном цикл for принимает в списке x, а затем, сохраняя это как временную переменную, re присваивает x каждому значению во временной переменной. Таким образом, x теперь является последним значением в списке.

>>> x = [1, 2, 3]
>>> [x for x in x]
[1, 2, 3]
>>> x
3
>>> 

Также как в этом:

>>> def foo(bar):
...     return bar
... 
>>> x = [1, 2, 3]
>>> for x in foo(x):
...     print x
... 
1
2
3
>>> 

В этом примере x сохраняется в foo() как bar, поэтому, хотя x переназначается, он все еще существует (ed) в foo(), чтобы мы могли использовать его для запуска нашей for.

Ответ 6

x больше не относится к исходному списку x, и поэтому нет путаницы. В принципе, python помнит, как он перебирает исходный список x, но как только вы начнете назначать итерационное значение (0,1,2 и т.д.) На имя x, оно больше не относится к оригиналу x список. Имя присваивается значению итерации.

In [1]: x = range(5)

In [2]: x
Out[2]: [0, 1, 2, 3, 4]

In [3]: id(x)
Out[3]: 4371091680

In [4]: for x in x:
   ...:     print id(x), x
   ...:     
140470424504688 0
140470424504664 1
140470424504640 2
140470424504616 3
140470424504592 4

In [5]: id(x)
Out[5]: 140470424504592