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

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 также работала после обнаружения этой ошибки, но я ищу объяснение, почему это произошло.

4b9b3361

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