116 votes

Assignation dans une expression lambda en Python

J'ai une liste d'objets et je veux supprimer tous les objets qui sont vides sauf un, en utilisant filter et un lambda expression.

Par exemple, si l'entrée est :

[Object(name=""), Object(name="fake_name"), Object(name="")]

...alors la sortie devrait être :

[Object(name=""), Object(name="fake_name")]

Existe-t-il un moyen d'ajouter une affectation à une lambda expression ? Par exemple :

flag = True 
input = [Object(name=""), Object(name="fake_name"), Object(name="")] 
output = filter(
    (lambda o: [flag or bool(o.name), flag = flag and bool(o.name)][0]),
    input
)

1 votes

Non. Mais vous n'avez pas besoin de ça. En fait, je pense que ce serait une façon assez obscure d'y parvenir, même si ça marchait.

8 votes

Pourquoi ne pas simplement passer une vieille fonction ordinaire dans le filtre ?

5 votes

Je voulais utiliser lambda juste pour que ce soit une solution vraiment compacte. Je me souviens qu'en OCaml, je pouvais enchaîner des instructions d'impression avant l'expression de retour, et j'ai pensé que cela pourrait être reproduit en Python.

229voto

Jeremy Banks Points 32470

L'opérateur d'expression d'affectation := ajouté dans Python 3.8 prend en charge l'affectation à l'intérieur des expressions lambda. Cet opérateur ne peut apparaître qu'à l'intérieur d'une expression entre parenthèses. (...) entre parenthèses [...] ou contreventée {...} pour des raisons syntaxiques. Par exemple, nous pourrons écrire ce qui suit :

import sys
say_hello = lambda: (
    message := "Hello world",
    sys.stdout.write(message + "\n")
)[-1]
say_hello()

Dans Python 2, il était possible d'effectuer des affectations locales comme effet secondaire des compréhensions de listes.

import sys
say_hello = lambda: (
    [None for message in ["Hello world"]],
    sys.stdout.write(message + "\n")
)[-1]
say_hello()

Cependant, il n'est pas possible d'utiliser l'un ou l'autre dans votre exemple car votre variable flag se trouve dans une portée externe, et non dans la lambda de l'entreprise. Cela n'a rien à voir avec lambda C'est le comportement général de Python 2. Python 3 vous permet de contourner ce problème grâce à la fonction nonlocal à l'intérieur de def mais nonlocal ne peut pas être utilisé à l'intérieur lambda s.

Il existe une solution de contournement (voir ci-dessous), mais tant que nous sommes sur le sujet...


Dans certains cas, vous pouvez l'utiliser pour tout faire à l'intérieur d'une lambda :

(lambda: [
    ['def'
        for sys in [__import__('sys')]
        for math in [__import__('math')]

        for sub in [lambda *vals: None]
        for fun in [lambda *vals: vals[-1]]

        for echo in [lambda *vals: sub(
            sys.stdout.write(u" ".join(map(unicode, vals)) + u"\n"))]

        for Cylinder in [type('Cylinder', (object,), dict(
            __init__ = lambda self, radius, height: sub(
                setattr(self, 'radius', radius),
                setattr(self, 'height', height)),

            volume = property(lambda self: fun(
                ['def' for top_area in [math.pi * self.radius ** 2]],

                self.height * top_area))))]

        for main in [lambda: sub(
            ['loop' for factor in [1, 2, 3] if sub(
                ['def'
                    for my_radius, my_height in [[10 * factor, 20 * factor]]
                    for my_cylinder in [Cylinder(my_radius, my_height)]],

                echo(u"A cylinder with a radius of %.1fcm and a height "
                     u"of %.1fcm has a volume of %.1fcm³."
                     % (my_radius, my_height, my_cylinder.volume)))])]],

    main()])()

Un cylindre d'un rayon de 10 cm et d'une hauteur de 20 cm a un volume de 6283,2 cm³.
Un cylindre d'un rayon de 20 cm et d'une hauteur de 40 cm a un volume de 50265,5 cm³.
Un cylindre d'un rayon de 30 cm et d'une hauteur de 60 cm a un volume de 169646 cm³.

S'il te plaît, ne le fais pas.


...pour en revenir à votre exemple initial : bien que vous ne puissiez pas effectuer d'affectations à l'élément flag dans la portée externe, vous pouvez utiliser des fonctions pour modifier la valeur précédemment attribuée.

Par exemple, flag pourrait être un objet dont .value que nous définissons en utilisant setattr :

flag = Object(value=True)
input = [Object(name=''), Object(name='fake_name'), Object(name='')] 
output = filter(lambda o: [
    flag.value or bool(o.name),
    setattr(flag, 'value', flag.value and bool(o.name))
][0], input)

[Object(name=''), Object(name='fake_name')]

Si nous voulions respecter le thème ci-dessus, nous pourrions utiliser une compréhension de liste au lieu de setattr :

    [None for flag.value in [bool(o.name)]]

Mais en réalité, dans un code sérieux, vous devriez toujours utiliser une définition de fonction ordinaire au lieu d'un fichier de type lambda si vous allez faire une mission extérieure.

flag = Object(value=True)
def not_empty_except_first(o):
    result = flag.value or bool(o.name)
    flag.value = flag.value and bool(o.name)
    return result
input = [Object(name=""), Object(name="fake_name"), Object(name="")] 
output = filter(not_empty_except_first, input)

0 votes

Le dernier exemple de cette réponse ne produit pas la même sortie que l'exemple, mais il me semble que la sortie de l'exemple est incorrecte.

0 votes

En bref, cela se résume à : utiliser .setattr() et ses semblables ( dictionnaires devrait aussi faire l'affaire, par exemple) pour intégrer des effets secondaires dans du code fonctionnel, un code sympa de @JeremyBanks a été montré :)

0 votes

Merci pour la remarque sur le assignment operator !

38voto

Ivo van der Wijk Points 7239

Vous ne pouvez pas vraiment maintenir l'état dans un filter / lambda (à moins d'abuser de l'espace de nom global). Vous pouvez cependant réaliser quelque chose de similaire en utilisant le résultat accumulé qui est passé dans une expression de type reduce() expression :

>>> f = lambda a, b: (a.append(b) or a) if (b not in a) else a
>>> input = ["foo", u"", "bar", "", "", "x"]
>>> reduce(f, input, [])
['foo', u'', 'bar', 'x']
>>> 

Vous pouvez, bien sûr, modifier un peu la condition. Dans ce cas, elle filtre les doublons, mais vous pouvez aussi utiliser a.count("") par exemple, pour ne restreindre que les chaînes de caractères vides.

Inutile de dire que vous pouvez le faire, mais que vous ne devriez vraiment pas :)

Enfin, vous peut faire quoi que ce soit en Python pur lambda : http://vanderwijk.info/blog/pure-lambda-calculus-python/

17voto

Gabi Purcaru Points 15158

Il n'y a pas besoin d'utiliser un lambda, quand on peut enlever tous les nuls, et en remettre un si la taille de l'entrée change :

input = [Object(name=""), Object(name="fake_name"), Object(name="")] 
output = [x for x in input if x.name]
if(len(input) != len(output)):
    output.append(Object(name=""))

1 votes

Je pense que vous avez une petite erreur dans votre code. La deuxième ligne devrait être output = [x for x in input if x.name] .

1 votes

L'ordre des éléments peut être important.

15voto

Ethan Furman Points 12683

Affectation normale ( = ) n'est pas possible à l'intérieur d'un lambda bien qu'il soit possible d'effectuer divers tours de passe-passe avec l'expression setattr et des amis.

Pourtant, la solution à votre problème est en fait assez simple :

input = [Object(name=""), Object(name="fake_name"), Object(name="")]
output = filter(
    lambda o, _seen=set():
        not (not o and o in _seen or _seen.add(o)),
    input
    )

ce qui vous donnera

[Object(Object(name=''), name='fake_name')]

Comme vous pouvez le voir, il garde la première instance vide au lieu de la dernière. Si vous avez besoin de la dernière à la place, inversez la liste en entrant dans le fichier filter et inverser la liste qui sort de filter :

output = filter(
    lambda o, _seen=set():
        not (not o and o in _seen or _seen.add(o)),
    input[::-1]
    )[::-1]

ce qui vous donnera

[Object(name='fake_name'), Object(name='')]

Une chose dont il faut être conscient : pour que cela fonctionne avec des objets arbitraires, ces objets doivent implémenter correctement la fonction __eq__ et __hash__ comme expliqué ici .

6voto

Jon Clements Points 51556

Si au lieu de flag = True nous pouvons faire une importation à la place, alors je pense que cela répond aux critères :

>>> from itertools import count
>>> a = ['hello', '', 'world', '', '', '', 'bob']
>>> filter(lambda L, j=count(): L or not next(j), a)
['hello', '', 'world', 'bob']

Ou peut-être que le filtre est mieux écrit comme :

>>> filter(lambda L, blank_count=count(1): L or next(blank_count) == 1, a)

Ou, pour un simple booléen, sans aucune importation :

filter(lambda L, use_blank=iter([True]): L or next(use_blank, False), a)

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