Неожиданный вывод из списка (генератор)
У меня есть список и функция lambda
, определенная как
In [1]: i = lambda x: a[x]
In [2]: alist = [(1, 2), (3, 4)]
Затем я пробую два разных метода для вычисления простой суммы
Первый метод.
In [3]: [i(0) + i(1) for a in alist]
Out[3]: [3, 7]
Второй метод.
In [4]: list(i(0) + i(1) for a in alist)
Out[4]: [7, 7]
Оба результата неожиданно отличаются друг от друга. Почему это происходит?
Ответы
Ответ 1
Это поведение было исправлено в python 3. Когда вы используете понимание списка [i(0) + i(1) for a in alist]
, вы определяете a
в своей области, доступной для i
. В новой сессии list(i(0) + i(1) for a in alist)
будет выдаваться ошибка.
>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> list(i(0) + i(1) for a in alist)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <genexpr>
File "<stdin>", line 1, in <lambda>
NameError: global name 'a' is not defined
Понимание списка не является генератором: выражения генератора и понимание списков.
Выражения генератора окружены скобками ( "()" ) и списком Понимание заключено в квадратные скобки ( "[]" ).
В вашем примере list()
, поскольку класс имеет свою собственную область переменных и он имеет доступ к глобальным переменным не более. Когда вы используете это, i
будет искать a
внутри этой области. Попробуйте это в новом сеансе:
>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> [i(0) + i(1) for a in alist]
[3, 7]
>>> a
(3, 4)
Сравните это с другим сеансом:
>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> l = (i(0) + i(1) for a in alist)
<generator object <genexpr> at 0x10e60db90>
>>> a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> [x for x in l]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <genexpr>
File "<stdin>", line 1, in <lambda>
NameError: global name 'a' is not defined
Когда вы запустите list(i(0) + i(1) for a in alist)
, вы передадите генератор (i(0) + i(1) for a in alist)
в класс list
, который он попытается преобразовать в список в своей области, прежде чем вернуть список. Для этого генератора, который не имеет доступа к лямбда-функции, переменная a
не имеет смысла.
Объект-генератор <generator object <genexpr> at 0x10e60db90>
потерял имя переменной a
. Затем, когда list
пытается вызвать генератор, функция лямбда вызовет ошибку для undefined a
.
Поведение списков в отличие от генераторов также упоминалось здесь:
Сопоставление списков также "утечки" их переменной цикла в окружающий объем. Это также изменится в Python 3.0, так что семантическое определение понимания списка в Python 3.0 будет эквивалентно списку(). Python 2.4 и выше должно выдавать предупреждение о сохранении, если цикл понимания списка переменная имеет то же имя, что и переменная, используемая в окружающий объем.
В python3:
>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> [i(0) + i(1) for a in alist]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <listcomp>
File "<stdin>", line 1, in <lambda>
NameError: name 'a' is not defined
Ответ 2
Вы должны сделать a
параметр для вашей лямбда-функции. Это работает как ожидалось:
In [10]: alist = [(1, 2), (3, 4)]
In [11]: i = lambda a, x: a[x]
In [12]: [i(a, 0) + i(a, 1) for a in alist]
Out[12]: [3, 7]
In [13]: list(i(a, 0) + i(a, 1) for a in alist)
Out[13]: [3, 7]
Альтернативный способ получить тот же результат:
In [14]: [sum(a) for a in alist]
Out[14]: [3, 7]
РЕДАКТИРОВАТЬ, этот ответ является простым проходом и не является реальным ответом на вопрос. Наблюдаемый эффект немного сложнее, см. Мой другой ответ.
Ответ 3
Важные вещи для понимания здесь:
-
выражения генератора будут создавать объекты функции внутри, но понимание списков не будет.
-
они оба привяжут переменную цикла к значениям, и переменные цикла будут в текущей области, если они еще не созданы.
Давайте посмотрим на байтовые коды выражения генератора
>>> dis(compile('(i(0) + i(1) for a in alist)', 'string', 'exec'))
1 0 LOAD_CONST 0 (<code object <genexpr> at ...>)
3 MAKE_FUNCTION 0
6 LOAD_NAME 0 (alist)
9 GET_ITER
10 CALL_FUNCTION 1
13 POP_TOP
14 LOAD_CONST 1 (None)
17 RETURN_VALUE
Он загружает объект кода, а затем делает его функцией. Давайте посмотрим на фактический объект кода.
>>> dis(compile('(i(0) + i(1) for a in alist)', 'string', 'exec').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 27 (to 33)
6 STORE_FAST 1 (a)
9 LOAD_GLOBAL 0 (i)
12 LOAD_CONST 0 (0)
15 CALL_FUNCTION 1
18 LOAD_GLOBAL 0 (i)
21 LOAD_CONST 1 (1)
24 CALL_FUNCTION 1
27 BINARY_ADD
28 YIELD_VALUE
29 POP_TOP
30 JUMP_ABSOLUTE 3
>> 33 LOAD_CONST 2 (None)
36 RETURN_VALUE
Как вы видите здесь, текущее значение из итератора сохраняется в переменной a
. Но так как мы делаем этот объект функции, созданный a
будет виден только внутри выражения генератора.
Но в случае понимания списка
>>> dis(compile('[i(0) + i(1) for a in alist]', 'string', 'exec'))
1 0 BUILD_LIST 0
3 LOAD_NAME 0 (alist)
6 GET_ITER
>> 7 FOR_ITER 28 (to 38)
10 STORE_NAME 1 (a)
13 LOAD_NAME 2 (i)
16 LOAD_CONST 0 (0)
19 CALL_FUNCTION 1
22 LOAD_NAME 2 (i)
25 LOAD_CONST 1 (1)
28 CALL_FUNCTION 1
31 BINARY_ADD
32 LIST_APPEND 2
35 JUMP_ABSOLUTE 7
>> 38 POP_TOP
39 LOAD_CONST 2 (None)
42 RETURN_VALUE
Нет явного создания функции, и в текущей области создается переменная a
. Итак, a
просочится в текущую область.
При таком понимании давайте подходим к вашей проблеме.
>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
Теперь, когда вы создаете список с пониманием,
>>> [i(0) + i(1) for a in alist]
[3, 7]
>>> a
(3, 4)
вы можете видеть, что a
просочился в текущую область действия и по-прежнему привязан к последнему значению с итерации.
Итак, когда вы повторяете выражение генератора после понимания списка, функция lambda
использует утечку a
. Вот почему вы получаете [7, 7]
, так как a
все еще привязан к (3, 4)
.
Но если вы сначала выберете выражение генератора, то a
будет привязано к значениям из alist
и не будет просочиться в текущую область, поскольку выражение генератора станет функцией. Таким образом, когда функция lambda
пытается получить доступ к a
, она не может найти ее нигде. Вот почему он не работает с ошибкой.
Примечание. То же поведение не наблюдается в Python 3.x, поскольку утечка предотвращается путем создания функций для понимания списков. Возможно, вы захотите прочитать об этом в истории блога Python, "Из представлений списков в выражения генератора" , написанных самим Гвидо.
Ответ 4
См. мой другой ответ для обходного пути. Но, думая немного больше, проблема, кажется, немного сложнее. Я думаю, что здесь есть несколько вопросов:
-
Когда вы выполняете i = lambda x: a[x]
, переменная a
не является параметром
к функции, это называется
closure. Это то же самое и для лямбда-выражений, и для определения нормальной функции.
-
Python, по-видимому, выполняет "позднюю привязку", что означает, что значение переменных, которые вы закрыли, просматривается только в тот момент, когда вы вызываете функцию. Это может привести к различным неожиданным результатам.
-
В Python 2 существует разница между перечнями списков, которые вытесняют их переменную цикла, и выражения генератора, в которых переменная цикла не течет (см. этот PEP). Эта разница была удалена в Python 3, где понимание списка является ярлыком для list(generater_expression)
. Я не уверен, но это, вероятно, означает, что представления Python2 выполняются во внешней области, в то время как выражения генераторов и представления Python3 создают собственную внутреннюю область.
Демонстрация (в Python2):
In [1]: def f(): # closes over a from global scope
...: return 2 * a
...:
In [2]: list(f() for a in range(5)) # does not find a in global scope
[...]
NameError: global name 'a' is not defined
In [3]: [f() for a in range(5)]
# executes in global scope, so f finds a. Also leaks a=8
Out[3]: [0, 2, 4, 6, 8]
In [4]: list(f() for a in range(5)) # finds a=8 in global scope
Out[4]: [8, 8, 8, 8, 8]
В Python3:
In [1]: def f():
...: return 2 * a
...:
In [2]: list(f() for a in range(5))
# does not find a in global scope, does not leak a
[...]
NameError: name 'a' is not defined
In [3]: [f() for a in range(5)]
# does not find a in global scope, does not leak a
[...]
NameError: name 'a' is not defined
In [4]: list(f() for a in range(5)) # a still undefined
[...]
NameError: name 'a' is not defined
Ответ 5
a
находится в глобальной области.
Поэтому он должен давать ошибку
Решение:
i = lambda a, x: a[x]
Ответ 6
После выполнения [i(0) + i(1) for a in alist]
a
становится (3,4)
.
Затем, когда выполняется следующая строка:
list(i(0) + i(1) for a in alist)
Значение (3,4)
используется как время лямбда-функцией i
как значение a
, поэтому оно печатает [7,7].
Вместо этого вы должны определить свои лямбда-функции, имеющие два параметра a
и x
.
i = lambda a,x : a[x]