Map vs list; почему различное поведение?
В ходе реализации алгоритма "Variable Elimination" для программы Bayes Nets я обнаружил неожиданную ошибку, которая была результатом итеративного преобразования карты последовательности объектов.
Для простоты я использую здесь аналогичный фрагмент кода:
>>> nums = [1, 2, 3]
>>> for x in [4, 5, 6]:
... # Uses n if x is odd, uses (n + 10) if x is even
... nums = map(
... lambda n: n if x % 2 else n + 10,
... nums)
...
>>> list(nums)
[31, 32, 33]
Это, безусловно, неправильный результат. Поскольку [4, 5, 6] содержит два четных числа, 10
следует добавлять к каждому элементу не более двух раз. Я тоже получал неожиданное поведение в алгоритме VE, поэтому я изменил его, чтобы преобразовать итератор map
в list
после каждой итерации.
>>> nums = [1, 2, 3]
>>> for x in [4, 5, 6]:
... # Uses n if x is odd, uses (n + 10) if x is even
... nums = map(
... lambda n: n if x % 2 else n + 10,
... nums)
... nums = list(nums)
...
>>> list(nums)
[21, 22, 23]
Из моего понимания итераций эта модификация не должна ничего менять, но это так. Очевидно, что преобразование n + 10
для случая not x % 2
применяется один раз в версии list
-ed.
Программа My Bayes Nets также работала после обнаружения этой ошибки, но я ищу объяснение, почему это произошло.
Ответы
Ответ 1
Ответ очень прост: map
является lazy в Python 3, он возвращает итерируемый объект (в Python 2 он возвращает list
). Позвольте мне добавить некоторые результаты в ваш пример:
In [6]: nums = [1, 2, 3]
In [7]: for x in [4, 5, 6]:
...: nums = map(lambda n: n if x % 2 else n + 10, nums)
...: print(x)
...: print(nums)
...:
4
<map object at 0x7ff5e5da6320>
5
<map object at 0x7ff5e5da63c8>
6
<map object at 0x7ff5e5da6400>
In [8]: print(x)
6
In [9]: list(nums)
Out[9]: [31, 32, 33]
Обратите внимание на In[8]
- значение x
равно 6. Мы также могли бы преобразовать функцию lambda
, переданную в map
, чтобы отслеживать значение x
:
In [10]: nums = [1, 2, 3]
In [11]: for x in [4, 5, 6]:
....: nums = map(lambda n: print(x) or (n if x % 2 else n + 10), nums)
....:
In [12]: list(nums)
6
6
6
6
6
6
6
6
6
Out[12]: [31, 32, 33]
Поскольку map
ленив, он вычисляет при вызове list
. Однако значение x
равно 6
, и именно поэтому оно создает запутанный вывод. Оценка nums
внутри цикла дает ожидаемый результат.
In [13]: nums = [1, 2, 3]
In [14]: for x in [4, 5, 6]:
....: nums = map(lambda n: print(x) or (n if x % 2 else n + 10), nums)
....: nums = list(nums)
....:
4
4
4
5
5
5
6
6
6
In [15]: nums
Out[15]: [21, 22, 23]
Ответ 2
Проблема связана с тем, как доступ к переменной x
осуществляется с помощью создаваемых вами лямбда-функций. Способ работы Python работает, функции лямбда всегда будут использовать последнюю версию x
из внешней области, когда они вызывают, а не значение, которое оно имело, когда они были определены.
Так как map
ленив, лямбда-функции не вызываются до цикла (когда вы потребляете вложенный map
, передавая их list
), и поэтому все они используют последний x
значение.
Чтобы каждая функция лямбда сохраняла значение x
, когда они определены, добавьте x=x
следующим образом:
lambda n, x=x: n if x % 2 else n + 10
Указывает аргумент и значение по умолчанию. Значение по умолчанию будет оцениваться во время определения лямбда, поэтому, когда lambda будет вызван позже (без второго аргумента), x
внутри выражения будет сохраненным значением по умолчанию.
Ответ 3
Если вы хотите использовать ленивую версию, вам нужно исправить x
в каждом цикле.
functools.partial делает именно это:
from functools import partial
def myfilter(n, x):
return n if x % 2 else n + 10
nums = [1, 2, 3]
for x in [4, 5, 6]:
f = partial(myfilter, x=x)
nums = map(f, nums)
>>> list(nums)
[21, 22, 23]