Как это понимание лямбда/выход/генератор работает?

Я просматривал свою кодовую базу сегодня и нашел это:

def optionsToArgs(options, separator='='):
    kvs = [
        (
            "%(option)s%(separator)s%(value)s" %  
            {'option' : str(k), 'separator' : separator, 'value' : str(v)}
        ) for k, v in options.items()
    ]
    return list(
        reversed(
            list(
                    (lambda l, t: 
                        (lambda f: 
                            (f((yield x)) for x in l)
                        )(lambda _: t)
                    )(kvs, '-o')
                )
            )
        )

Кажется, он принимает параметр параметров и превращает их в список параметров для команды оболочки. Похоже, что использование урожая в понимании генератора, которое я считал невозможным...?

>>> optionsToArgs({"x":1,"y":2,"z":3})
['-o', 'z=3', '-o', 'x=1', '-o', 'y=2']

Как это работает?

Ответы

Ответ 1

Так как Python 2.5, yield <value> является выражением, а не выражением. См. PEP 342.

Код ужасно и излишне уродливый, но он законный. Его центральный трюк использует f((yield x)) внутри выражения генератора. Вот более простой пример того, как это работает:

>>> def f(val):
...     return "Hi"
>>> x = [1, 2, 3]
>>> list(f((yield a)) for a in x)
[1, 'Hi', 2, 'Hi', 3, 'Hi']

В принципе, использование выражения yield в выражении генератора заставляет его создавать два значения для каждого значения в исходном итерабельном. Поскольку выражение генератора повторяется по списку строк, на каждой итерации yield x сначала выводит строку из списка. Целевое выражение гена xp f((yield x)), поэтому для каждого значения в списке "результат" выражения генератора представляет собой значение f((yield x)). Но f просто игнорирует свой аргумент и всегда возвращает строку опций "-o". Таким образом, каждый шаг через генератор дает сначала строку значений ключа (например, "x=1"), затем "-o". Внешний list(reversed(list(...))) просто делает список из этого генератора, а затем меняет его так, что "-o" будет использоваться перед каждым вариантом, а не после.

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

kvs = [...] # same list comprehension can be used for this part
result = []
for keyval in kvs:
   result.append("-o")
   result.append(keyval)
return result

Даже если вам нужен краткий "умный" код, вы все равно можете просто сделать

return sum([["-o", keyval] for keyval in kvs], [])

Само понимание списка kvs - это странное сочетание попыток чтения и нечитаемости. Это проще написано:

kvs = [str(optName) + separator + str(optValue) for optName, optValue in options.items()]

Вам следует подумать о том, чтобы организовать "вмешательство" для тех, кто положил это в свою кодовую базу.

Ответ 2

О боже. В принципе, это сводится к следующему:

def f(_):              # I'm the lambda _: t
    return '-o'

def thegenerator():   # I'm (f((yield x)) for x in l)
    for x in kvs:
        yield f((yield x))

Таким образом, при повторном запуске генератор дает x (член kvs), а затем возвращаемое значение f, которое всегда -o, все в одной итерации над kvs. Независимо от того, что yield x возвращает и что передается в f, игнорируется.

Эквиваленты:

def thegenerator():   # I'm (f((yield x)) for x in l)
    for x in kvs:
        whatever = (yield x)
        yield f(whatever)

def thegenerator():   # I'm (f((yield x)) for x in l)
    for x in kvs:
        yield x
        yield f(None)

def thegenerator():   # I'm (f((yield x)) for x in l)
    for x in kvs:
        yield x
        yield '-o'

Есть много способов сделать это намного проще, конечно. Даже с оригинальным трюком с двойным уроком все могло быть

return list(((lambda _: '-o')((yield x)) for x in kvs))[::-1]