Списки розуміння відновлює імена навіть після сфери розуміння. Чи це правильно?


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
Це порушує принцип найменшого здивування. Він також не згадується в документації пітона про розуміння списку, що, однак, кілька разів згадує, наскільки вони прості та зручні. По суті це міна, яка існувала поза моєю мовною моделлю, і тому мені було неможливо передбачити.
Джабаву Адамс

33
+1 за "жорстоке джерело помилок". Слово "жорстокий" цілком виправдано.
Натаніел

3
Єдине "жорстоке", що я бачу тут, - це ваша конвенція про іменування. Це вже не 80-ті, ви не обмежені 3-ма символами змінних.
UloPe

5
Примітка: довідки про робить стан, список осягнення еквівалентно явним for-loop конструкції і for-loops витоку змінних . Тож це було не явно, але було неявно зазначено.
Бакуріу

Відповіді:


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) :-) надрукує "раніше", довівши, що "x" у розумінні списку тимчасово тініє, але не перекриває "x" в навколишніх сфера застосування.


14
Додам, що хоча Гуїдо називає це "маленьким брудним секретом", багато хто вважав це особливістю, а не помилкою.
Стівен Румбальський

38
Також зауважте, що зараз у 2.7, розуміння наборів та словників (та генераторів) є приватні сфери застосування, але розуміння списку все ще немає. Хоча це має певний сенс у тому, що колишні всі були підпорядковані Python 3, це насправді контраст зі змінами списків.
Метт Б.

7
Я знаю, що це шалено старе питання, але чому деякі вважали це особливістю мови? Чи є щось на користь такого виду змінної протікання?
Mathias Müller

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

1
Проблема тут полягає не в тому, щоб отримати доступ до оточуючої сфери розуміння списку, а прив'язувати область розуміння списку, що впливає на навколишнє поле.
Феліпе Гонсальвес Маркіс

48

Так, список розумінь "просочує" свою змінну в Python 2.x, як і для циклів.

В ретроспективі це було визнано помилкою, і цього вдалося уникнути генераторними виразами. EDIT: Як зазначає Метт Б., цього також було уникнено, коли синтаксиси набору та словника для розуміння словника підтримувалися з 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 запровадить :=оператор присвоєння, який свідомо витікає з розумінь та генераторних виразів! Це мотивовано істотою 2 випадки використання: захоплення «свідок» від раннього припинення функцій , як 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]

Дивіться Додаток В для точного визначення обсягу. Змінна призначається в найближчому оточенні 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-loop, змінна стає перерозподіленою за межі області.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.