web-dev-qa-db-fra.com

Comment fonctionne cette compréhension lambda / rendement / générateur?

Je regardais ma base de code aujourd'hui et j'ai trouvé ceci:

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')
                )
            )
        )

Il semble prendre une dictée de paramètres et les transformer en une liste de paramètres pour une commande Shell. On dirait qu'il utilise le rendement dans une compréhension de générateur, ce que je pensais être impossible ...?

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

Comment ça marche?

51
Dog

Puisque Python 2.5, yield <value> Est une expression, pas une instruction. Voir PEP 342 .

Le code est hideusement et inutilement laid, mais c'est légal. Son astuce centrale consiste à utiliser f((yield x)) à l'intérieur de l'expression du générateur. Voici un exemple plus simple de comment cela fonctionne:

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

Fondamentalement, l'utilisation de yield dans l'expression du générateur lui fait produire deux valeurs pour chaque valeur de l'itérable source. Lorsque l'expression du générateur parcourt la liste des chaînes, à chaque itération, le yield x Renvoie d'abord une chaîne de la liste. L'expression cible de genexp est f((yield x)), donc pour chaque valeur de la liste, le "résultat" de l'expression du générateur est la valeur de f((yield x)). Mais f ignore simplement son argument et renvoie toujours la chaîne d'option "-o". Ainsi, à chaque étape du générateur, il génère d'abord la chaîne de valeurs-clés (par exemple, "x=1"), Puis "-o". La fonction list(reversed(list(...))) externe fait juste une liste de ce générateur, puis l'inverse pour que les "-o" S viennent avant chaque option au lieu de l'après.

Cependant, il n'y a aucune raison de procéder de cette façon. Il existe un certain nombre d'alternatives beaucoup plus lisibles. Le plus explicite est peut-être simplement:

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

Même si vous aimez le code concis et "intelligent", vous pouvez toujours

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

La compréhension de la liste kvs elle-même est un mélange bizarre de tentative de lisibilité et d'illisibilité. Il s’écrit plus simplement:

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

Vous devriez envisager d'organiser une "intervention" pour quiconque a mis cela dans votre base de code.

48
BrenBarn

Oh mon Dieu. Fondamentalement, cela se résume à ceci:

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))

Ainsi, une fois itéré, le générateur génère x (un membre de kvs), puis la valeur de retour de f, qui est toujours -o, tout en une itération sur kvs. Peu importe yield x retourne et ce qui est passé à f est ignoré.

Équivalents:

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'

Il existe bien sûr de nombreuses façons de procéder de cette manière. Même avec l'astuce originale à double rendement, le tout aurait pu être

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