Чому я можу використовувати одне і те ж ім'я для ітератора та послідовності в циклі Python for?


79

Це більше концептуальне питання. Нещодавно я побачив шматок коду в 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
Як цікавий мислительський експеримент: визначте функцію printAndReturn, яка бере аргумент, друкує його і повертає. Тоді for i in printAndReturn [1,2,3,4,5] …, скільки разів слід [1,2,3,4,5]друкувати?
Джошуа Тейлор

1
Примітка щодо сфери дії, оскільки ніхто інший безпосередньо про це не згадував: Python має функціонування на рівні функції, але нічого подібного до рівня блокування на рівні C. Отже, внутрішня та зовнішня частини forциклу мають однакові рамки.
Izkata

Я виправив заголовок питання, оскільки це було дещо оманливим. Те, що щось є поганою практикою, не означає, що воно не працює. Може бути так, що він схильний до помилок, або його важко читати / підтримувати тощо
Ніко

Дякую. Я повністю згоден, що це був поганий заголовок, я просто не знав, як його назвати спочатку.
Густав

це працює і в php, for ($x as $x)але є потворним кодом IMO
chiliNUT

Відповіді:


67

Що 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перезаписується, щоб вказувати на кожне значення всередині списку, раніше відомого як xPython, не виникає проблем з пошуком ітератора, оскільки йому більше ніколи не потрібно дивитись на ім'я, xщоб закінчити протокол ітерації.


8
"Python більше ніколи не повертається, щоб завантажити ітератор (не має сенсу це робити, оскільки у нього вже є ітератор)." Це описує поведінку, яку ви спостерігаєте при розбиранні, але там не сказано, має це бути так чи ні; У відповіді Абхіджіта наводиться керівництво, де це насправді вказано.
Джошуа Тейлор

42

Використання вашого прикладу коду як основного посилання

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

Я хотів би, щоб ви звернулись до розділу 7.3. Заява for у посібнику

Витяг 1

Список виразів оцінюється один раз; це повинно давати об'єкт, що підлягає ітерації. Ітератор створюється для результату списку виразів.

Це означає, що ваша змінна x, що є символічною назвою об’єкта list: [1,2,3,4,5]обчислюється до об’єкта, що підлягає ітерації. Навіть якщо змінна, символічне посилання змінює свою прихильність, оскільки список виразів не обчислюється знову, це не впливає на ітерабельний об'єкт, який вже обчислювався та генерувався.

Примітка

  • Усе в Python є об’єктом, має ідентифікатор, атрибути та методи.
  • Змінні - це символічне ім'я, посилання на один і тільки один об'єкт у кожному конкретному випадку.
  • Змінні під час виконання можуть змінити свою відданість, тобто можуть посилатися на якийсь інший об’єкт.

Витяг 2

Потім набір виконується один раз для кожного елемента, наданого ітератором, у порядку зростання індексів.

Тут набір посилається на ітератор, а не на список виразів. Отже, для кожної ітерації ітератор виконується, щоб отримати наступний елемент замість посилання на вихідний список виразів.


5

Потрібно, щоб це працювало таким чином, якщо ви задумаєтесь. Вираз послідовності forциклу може бути будь-яким:

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

Ми не можемо запитувати послідовність при кожному проходженні через цикл, інакше ми в кінцевому підсумку вдруге прочитаємо з наступної партії по 5 байт. Природно, що Python повинен певним чином зберігати результат виразу приватно перед початком циклу.


Вони в різних сферах?

Ні. Для підтвердження цього ви можете залишити посилання на оригінальний словник області ( local () ) і помітити, що ви фактично використовуєте однакові змінні всередині циклу:

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

Що відбувається під капотом, що дозволяє щось подібне працювати?

Шон Вієйра точно показав, що відбувається під капотом, але щоб описати це в більш читабельному коді пітона, ваш 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];
    ...
 }

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

Однак при попередньому підході перший рядок ( it = iter(x)) запитує об'єкт-ітератор, який фактично відповідає за надання наступного елемента з цього моменту. Послідовність, на яку xспочатку вказував, більше не потребує безпосереднього доступу.


4

Це різниця між змінною (x) та об’єктом, на який вона вказує (список). Коли цикл for запускається, Python захоплює внутрішнє посилання на об'єкт, на який вказує x. Він використовує об'єкт, а не те, на що х посилається в будь-який момент часу.

Якщо ви перепризначите x, цикл for не зміниться. Якщо x вказує на змінний об’єкт (наприклад, список), і ви змінюєте цей об’єкт (наприклад, видаляєте елемент), результати можуть бути непередбачуваними.


3

В основному, цикл for бере список x, а потім, зберігаючи це як тимчасову змінну, повторно присвоює 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()as bar, тому, хоча xвін перепризначається, він все ще існує (ed) в, foo()щоб ми могли використовувати його для запуску нашого forциклу.


Власне, в останньому прикладі я не думаю x, що його перепризначають. Локальна змінна barстворюється в fooі присвоюється значення x. fooпотім повертає це значення у вигляді об'єкта, який використовується в forумові. Таким чином, змінна xніколи не була перепризначена у другому прикладі. Я згоден з першим.
Тоніо

@Tonio xвсе ще є змінною ітерації, і таким чином приймає нове значення для кожного циклу. Після циклу xдорівнює 3в обох випадках.
Пітер Гібсон,

@PeterGibson Ви абсолютно праві, це пройшло повз моєї уваги.
Тоніо

Якби це була "нова змінна" всередині циклу, то як же після того, як цикл xтримається 3і not [1,2,3] `?
Джошуа Тейлор

@JoshuaTaylor У python змінна індексу циклу лексично масштабується до блоку, в якому відбувся цикл for.
HennyH

1

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

2
Він не стільки робить копію списку діапазонів (оскільки зміни в списку все одно дадуть невизначену поведінку в ітерації). xпросто припиняє посилатися на список діапазонів і замість цього йому присвоюються нові значення ітерації. Список діапазонів все ще існує недоторканим. Якщо поглянути на значення xafter циклу, воно буде4
Peter Gibson

"x більше не відноситься до оригіналу x", про який xніколи не говорилося x; xпосилається на послідовність. Потім це стосувалося 1, потім - 2тощо.
Джошуа Тейлор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.