51 votes

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

Je regardais dans 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 un dict de paramètres et les transformer en une liste de paramètres pour une commande shell. On dirait qu'il utilise yield à l'intérieur d'un générateur de compréhension, ce qui me semblait impossible... ?

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

Comment cela fonctionne-t-il ?

48voto

BrenBarn Points 63718

Depuis Python 2.5, yield <value> est une expression, pas une déclaration. Voir PEP 342 .

Le code est hideusement et inutilement laid, mais il 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 ce fonctionnement :

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

En fait, l'utilisation de yield dans l'expression du générateur lui fait produire deux valeurs pour chaque valeur de l'itérable source. Au fur et à mesure que l'expression du générateur itère sur la liste de chaînes de caractères, à chaque itération, la fonction yield x donne d'abord une chaîne de caractères à partir de la liste. L'expression cible du genexp est f((yield x)) Ainsi, 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 retourne toujours la chaîne d'option "-o" . Ainsi, à chaque étape du générateur, celui-ci produit d'abord la chaîne clé-valeur (par exemple, "x=1" ), alors "-o" . L'extérieur list(reversed(list(...))) fait juste une liste à partir de ce générateur et l'inverse pour que le "-o" viendront avant chaque option au lieu de venir après.

Cependant, il n'y a aucune raison de procéder de cette manière. Il existe un certain nombre d'alternatives beaucoup plus lisibles. La 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 laconique et "intelligent", vous pouvez toujours faire simplement

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

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

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

Vous devriez envisager d'organiser une "intervention" pour celui qui a mis cela dans votre codebase.

19voto

Pavel Anossov Points 23610

Oh mon dieu. En gros, ça se résume à ça :

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, lorsqu'il est itéré, le générateur donne les résultats suivants x (un membre de kvs ) et ensuite la valeur de retour de f qui est toujours -o le tout en une seule itération sur kvs . Quoi que yield x retourne et ce qui est passé à f est ignorée.

É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 de nombreux moyens de faire cela de manière beaucoup plus simple, bien sûr. Même avec le truc original à double rendement, le tout aurait pu être

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X