Понимание списка повторно связывает имена даже после объема понимания. Это правильно?


118

У понимания есть некоторые неожиданные взаимодействия с областью видимости. Это ожидаемое поведение?

У меня есть способ:

def leave_room(self, uid):
  u = self.user_by_id(uid)
  r = self.rooms[u.rid]

  other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
  other_us = [self.user_by_id(uid) for uid in other_uids]

  r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above

  # Interestingly, it's rebound to the last uid in the list, so the error only shows
  # up when len > 1

Рискуя ныть, это серьезный источник ошибок. Когда я пишу новый код, я просто иногда нахожу очень странные ошибки из-за повторной привязки - даже сейчас, когда я знаю, что это проблема. Мне нужно сделать правило вроде «всегда предварять временные переменные в понимании списка с подчеркиванием», но даже это не является надежным.

Тот факт, что есть случайное ожидание с бомбой замедленного действия, сводит на нет всю приятную «простоту использования» понимания списков.


7
-1: «Брутальный источник ошибок»? Едва. Почему выбрали такой аргументированный термин? Как правило, самые дорогие ошибки - это недопонимание требований и простые логические ошибки. Ошибки такого рода были стандартной проблемой для многих языков программирования. Почему называют это «жестоким»?
S.Lott

44
Это нарушает принцип наименьшего удивления. Это также не упоминается в документации python о понимании списков, но, тем не менее, несколько раз упоминается, насколько они просты и удобны. По сути, это мина, существовавшая вне моей языковой модели, и поэтому я не мог ее предвидеть.
Джабаву Адамс

33
+1 за «жестокий источник ошибок». Слово «жестокий» полностью оправдано.
Натаниэль

3
Единственная «жестокая» вещь, которую я здесь вижу, - это ваше соглашение об именах. Это уже не 80-е, вы не ограничены трехсимвольными именами переменных.
UloPe 02

5
Примечание: в документации действительно указано , что понимание списка эквивалентно явным forпеременным -loop и for-loops для утечки . Так что это не было явным, но было заявлено неявно.
Bakuriu 04

Ответы:


172

Понимание списков приводит к утечке переменной управления циклом в Python 2, но не в Python 3. Вот Гвидо ван Россум (создатель Python), объясняющий историю этого:

Мы также внесли еще одно изменение в Python 3, чтобы улучшить эквивалентность между пониманием списков и выражениями генератора. В Python 2 понимание списка «пропускает» переменную управления циклом в окружающую область:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

Это был артефакт оригинальной реализации понимания списков; это был один из «маленьких грязных секретов» Python в течение многих лет. Это началось как преднамеренный компромисс, чтобы сделать понимание списка ослепительно быстрым, и хотя это не было распространенной ловушкой для новичков, это определенно иногда ужалило людей. Для выражений генератора мы не могли этого сделать. Выражения генератора реализуются с помощью генераторов, для выполнения которых требуется отдельный фрейм исполнения. Таким образом, выражения генератора (особенно если они повторяются в короткой последовательности) были менее эффективны, чем понимание списков.

Однако в Python 3 мы решили исправить «маленький грязный секрет» понимания списков, используя ту же стратегию реализации, что и для выражений генератора. Таким образом, в Python 3 приведенный выше пример (после модификации для использования print (x) :-) будет печатать 'before', доказывая, что 'x' в понимании списка временно затеняет, но не отменяет 'x' в окружающем объем.


14
Я добавлю, что, хотя Гвидо называет это «маленьким грязным секретом», многие считают это особенностью, а не ошибкой.
Стивен Румбальский

38
Также обратите внимание, что теперь в 2.7 у интерпретаций множеств и словарей (и генераторов) есть частные области видимости, но у понимания списков по-прежнему нет. Хотя это имеет некоторый смысл, поскольку все первые были перенесены из Python 3, это действительно создает резкий контраст с пониманием списков.
Мэтт Б.

7
Я знаю, что это безумно старый вопрос, но почему некоторые сочли это особенностью языка? Есть ли что-нибудь в пользу такой утечки переменных?
Матиас Мюллер

2
for: утечка циклов имеет веские причины, особенно. для доступа к последнему значению после раннего break- но не имеет отношения к пониманию. Я вспоминаю некоторые обсуждения comp.lang.python, в которых люди хотели назначать переменные в середине выражения. Менее безумный путь обнаружено одним значения для положения , например. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], но ему просто нужна переменная, локальная для понимания, и она так же хорошо работает в Python 3. Я думаю, что «утечка» была единственным способом сделать переменную видимой вне выражения. Все согласились, что эти методы ужасны :-)
Бени Чернявский-Паскин

1
Проблема здесь не в доступе к окружающей области понимания списка, а в привязке к области понимания списка, влияющей на окружающую область.
Felipe Gonçalves Marques

48

Да, понимание списков «пропускает» свою переменную в Python 2.x, как и в циклах for.

Оглядываясь назад, это было признано ошибкой, и ее удалось избежать с помощью выражений генератора. РЕДАКТИРОВАТЬ: как отмечает Мэтт Б., этого также удалось избежать, когда синтаксисы набора и понимания словаря были перенесены из Python 3.

Поведение списков должно быть таким же, как в Python 2, но полностью исправлено в Python 3.

Это означает, что во всех:

list(x for x in a if x>32)
set(x//4 for x in a if x>32)         # just another generator exp.
dict((x, x//16) for x in a if x>32)  # yet another generator exp.
{x//4 for x in a if x>32}            # 2.7+ syntax
{x: x//16 for x in a if x>32}        # 2.7+ syntax

xвсегда локальна для выражения в то время как эти:

[x for x in a if x>32]
set([x//4 for x in a if x>32])         # just another list comp.
dict([(x, x//16) for x in a if x>32])  # yet another list comp.

в Python 2.x все утечки xпеременной в окружающую область видимости.


ОБНОВЛЕНИЕ для Python 3.8 (?) : PEP 572 представит :=оператор присваивания, который намеренно выходит за пределы понимания и выражений генератора! По сути, это мотивировано двумя вариантами использования: захват «свидетеля» из функций раннего завершения, таких как any()и all():

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

и обновление изменяемого состояния:

total = 0
partial_sums = [total := total + v for v in values]

См. Приложение B для точного определения объема. Переменная назначается в ближайшем окружении defили lambda, если эта функция не объявляет ее nonlocalили global.


7

Да, присваивание происходит там, как в forцикле. Никакой новой области не создается.

Это определенно ожидаемое поведение: в каждом цикле значение привязывается к указанному вами имени. Например,

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234

Как только это распознано, кажется, что этого достаточно легко избежать: не использовать существующие имена для переменных в пределах понимания.


2

Интересно, что это не влияет на словарь или понимание набора.

>>> [x for x in range(1, 10)]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x
9
>>> {x for x in range(1, 5)}
set([1, 2, 3, 4])
>>> x
9
>>> {x:x for x in range(1, 100)}
{1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}
>>> x
9

Однако это было исправлено в 3, как указано выше.


Этот синтаксис вообще не работает в Python 2.6. Вы говорите о Python 2.7?
Пол Холлингсворт

Python 2.6 имеет только понимание списков, как и Python 3.0. 3.1 добавил набор и словарь, и они были перенесены в 2.7. Извините, если это было непонятно. Он должен был указать на ограничение другого ответа, и к каким версиям он применяется, не совсем однозначно.
Крис Трэверс

Хотя я могу представить аргумент о том, что есть случаи, когда использование python 2.7 для нового кода имеет смысл, я не могу сказать то же самое о python 2.6 ... Даже если 2.6 - это то, что идет с вашей ОС, вы не застряли с Это. Подумайте об установке virtualenv и использовании 3.6 для нового кода!
Alex L

Вопрос о Python 2.6 может возникнуть при поддержке существующих устаревших систем. Так что как историческая справка это не совсем неуместно. То же самое с 3.0 (ick)
Крис Трэверс

Извините, если я звучу грубо, но это никак не отвечает на вопрос. Это лучше подходит в качестве комментария.
0xc0de

1

какое-то обходное решение для python 2.6, когда такое поведение нежелательно

# python
Python 2.6.6 (r266:84292, Aug  9 2016, 06:11:56)
Type "help", "copyright", "credits" or "license" for more information.
>>> x=0
>>> a=list(x for x in xrange(9))
>>> x
0
>>> a=[x for x in xrange(9)]
>>> x
8

-1

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

i = 1 print (i) print ([i in range (5)]) print (i) Значение i останется только 1.

Теперь просто используйте цикл for, значение i будет переназначено.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.