Подтвердить что ты не робот

Неожиданный вывод из списка (генератор)

У меня есть список и функция 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]

Оба результата неожиданно отличаются друг от друга. Почему это происходит?

4b9b3361

Ответ 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]