Создание функции itertools.product с неожиданными результатами

У меня возникли небольшие проблемы с пониманием результатов приведенного ниже фрагмента, и я думаю, что это потому, что я запутался в привязке функций. Почему следующие фрагменты дают разные результаты?

import itertools

def make_funcs(lst):
    for val in lst:
        def f():
            return sum(1 for i in range(10) if i > val)
        f.func_name = ">" + str(val)
        yield f
## examples:
for f in make_funcs(range(2)):
    print(f.func_name, f())
## prints:
>0 9
>1 8

## works as expected:
for f in make_funcs(range(2)):
    for g in make_funcs(range(2)):
        print(f.func_name, g.func_name, f() + g())

## prints:
>0 >0 18
>0 >1 17
>1 >0 17
>1 >1 16

в то время как с другой стороны:

## provides results that are counter-intuitive (to me, at least)
for f, g in itertools.product(make_funcs(range(2)), make_funcs(range(2))):
    print(f.func_name, g.func_name, f() + g())

## prints:
>0 >0 16
>0 >1 16
>1 >0 16
>1 >1 16

Мне кажется, что это только захват / использование /binding последняя переменная в каждом неявном for- цикл для вычисления, даже если он выбирает правильную переменную для имен функций.

Что мне не хватает в определениях области действия или определениях функций или замыканиях (или что-то еще), которые вызывают эти результаты?

NB: если какой-либо из тегов, которые я поставил на этот вопрос, не имеет значения, не стесняйтесь удалять их - я помещаю их все, потому что я не уверен, в чем проблема.

2 ответа

Решение

Все функции по-прежнему ссылаются на переменную val,

def make_funcs(lst):
    a = []
    for val in lst:
        def f():
            return sum(1 for i in range(10) if i > val)
        f.func_name = ">" + str(val)
        a.append(f)
    return a

В результате все отпечатки становятся "не интуитивными".

def make_funcs(lst):
    a = []
    for val in lst:
        def f():
            return sum(1 for i in range(10) if i > val)
        f.func_name = ">" + str(val)
        a.append(f)
    val = 10
    return a

Всегда приводит к 0.

Однако, поскольку вы используете генератор, значение val изменяется только после того, как он был использован, так что, кажется, все хорошо. При использовании itertools.product, документы говорят, что это делает это:

def product(*args, repeat=1):
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
    pools = [tuple(pool) for pool in args] * repeat
    result = [[]]
    for pool in pools:
        result = [x+[y] for x in result for y in pool]
    for prod in result:
        yield tuple(prod)

Это означает, что он сначала перебирает оба генератора (эффективно изменяя значение val четыре раза) и только потом вычисляет результаты.

Это все происходит потому, что val определяется в объеме make_funcs (а не в объеме f), поэтому, если второй вызов генератора изменяет значение valвсе функции ссылаются на новое значение.

Изменить: Пожалуйста, также прочитайте ответ @newacct

Другой ответ объясняет, почему вы видите то, что видите - это потому, что функции захватывают внешние переменные по ссылке, и то же самое val Переменная фиксируется несколькими функциями, которые видят изменения в переменной.

Чтобы добавить, если вы хотите избежать этого, если вы хотите захватить по значению, один из способов сделать это - использовать параметр и аргумент по умолчанию во внутренней функции:

def make_funcs(lst):
    for val in lst:
        def f(val=val):
            return sum(1 for i in range(10) if i > val)
        f.func_name = ">" + str(val)
        yield f
Другие вопросы по тегам