web-dev-qa-db-fra.com

Assignation dans l'expression lambda dans Python

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

Par exemple si l'entrée est:

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

... alors le résultat devrait être:

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

Est-il possible d'ajouter une affectation à une expression lambda? 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
)
88
Cat

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 que dans une parenthèse (...), entre crochets [...], ou renforcé {...} expression 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 en tant qu'effet secondaire de la compréhension de liste.

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 ces éléments dans votre exemple car votre variable flag se trouve dans une étendue externe, pas dans celle de lambda. Cela n’a rien à voir avec lambda, c’est le comportement général dans Python 2. Python 3 vous permet de contourner ce problème avec le nonlocal mot-clé à l'intérieur de defs, mais nonlocal ne peut pas être utilisé à l'intérieur de lambdas.

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


Dans certains cas, vous pouvez utiliser ceci pour tout faire à l'intérieur d'un 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 de rayon 10,0 cm et de hauteur 20,0 cm a un volume de 6283,2 cm³.
Un cylindre de rayon 20,0 cm et de hauteur 40,0 cm a un volume de 50 265,5 cm³.
Un cylindre de rayon 30,0 cm et de hauteur 60,0 cm a un volume de 169646,0 cm³.

S'il vous plaît ne pas.


... revenons à votre exemple d'origine: bien que vous ne puissiez pas effectuer d'affectations à la variable flag dans l'étendue externe, vous pouvez utiliser des fonctions pour modifier la valeur affectée précédemment.

Par exemple, flag pourrait être un objet dont .value nous avons défini avec 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 adapter le thème ci-dessus, nous pourrions utiliser une liste de compréhension au lieu de setattr:

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

Mais en réalité, dans le code sérieux, vous devriez toujours utiliser une définition de fonction normale au lieu de lambda si vous allez effectuer une affectation externe.

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)
197
Jeremy Banks

Vous ne pouvez pas vraiment conserver l'état dans une expression filter/lambda (sauf en cas d'utilisation abusive de l'espace de noms global). Vous pouvez cependant obtenir quelque chose de similaire en utilisant le résultat accumulé dans une expression reduce():

>>> 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, les doublons sont filtrés, mais vous pouvez également utiliser a.count(""), par exemple, pour limiter uniquement les chaînes vides.

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

Enfin, vous pouvez faire n'importe quoi en pure Python lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/

33
Ivo van der Wijk

Il n'est pas nécessaire d'utiliser un lambda, lorsque vous pouvez supprimer tous ceux qui sont 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=""))
16
Gabi Purcaru

Affectation normale (=) n'est pas possible dans une expression lambda, bien qu'il soit possible d'effectuer diverses astuces avec setattr et avec des amis.

Résoudre votre problème, cependant, est en réalité 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
    )

qui te donnera

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

Comme vous pouvez le constater, la première instance vide est conservée au lieu de la dernière. Si vous avez besoin du dernier, inversez la liste dans filter et inversez la liste issue de filter:

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

qui te donnera

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

Une chose à savoir: pour que cela fonctionne avec des objets arbitraires, ces objets doivent correctement implémenter __eq__ et __hash__ _ comme expliqué ici .

11
Ethan Furman

[~ # ~] met à jour [~ # ~] :

[o for d in [{}] for o in lst if o.name != "" or d.setdefault("", o) == o]

ou en utilisant filter et lambda:

flag = {}
filter(lambda o: bool(o.name) or flag.setdefault("", o) == o, lst)

Réponse précédente

OK, êtes-vous bloqué pour utiliser le filtre et le lambda?

Il semble que cela serait mieux servi avec une compréhension du dictionnaire,

{o.name : o for o in input}.values()

Je pense que la raison pour laquelle Python n'autorise pas l'assignation dans un lambda est semblable à la raison pour laquelle il ne permet pas l'assignation dans une compréhension et cela a quelque chose à voir avec le fait que ces choses sont évaluées sur le côté C et peut donc nous donner une augmentation de vitesse. C'est du moins mon impression après avoir lu l'un des essais de Guido .

À mon avis, cela irait également à l’encontre de la philosophie qui consiste à avoir un la bonne façon de faire quelque chose en Python.

6
milkypostman

TL; DR: Lors de l'utilisation d'idiomes fonctionnels, il est préférable d'écrire du code fonctionnel

Comme beaucoup de personnes l’ont souligné, dans Python lambdas, l’affectation n’est pas autorisée. En général, lorsqu’on utilise des idiomes fonctionnels, il est préférable de penser de manière fonctionnelle, ce qui signifie, dans la mesure du possible, pas d’effets secondaires ni d’assignations.

Voici une solution fonctionnelle qui utilise un lambda. J'ai assigné le lambda à fn pour plus de clarté (et parce qu'il est un peu long).

from operator import add
from itertools import ifilter, ifilterfalse
fn = lambda l, pred: add(list(ifilter(pred, iter(l))), [ifilterfalse(pred, iter(l)).next()])
objs = [Object(name=""), Object(name="fake_name"), Object(name="")]
fn(objs, lambda o: o.name != '')

Vous pouvez également conclure cet accord avec des itérateurs plutôt que des listes en modifiant légèrement les choses. Vous avez également des importations différentes.

from itertools import chain, islice, ifilter, ifilterfalse
fn = lambda l, pred: chain(ifilter(pred, iter(l)), islice(ifilterfalse(pred, iter(l)), 1))

Vous pouvez toujours réorganiser le code pour réduire la longueur des déclarations.

5
dietbuddha

La manière pythonique de suivre l'état pendant l'itération est avec des générateurs. La façon dont itertools est assez difficile à comprendre à mon humble avis et essayer de pirater les lambdas pour le faire est tout simplement ridicule. J'essaierais:

def keep_last_empty(input):
    last = None
    for item in iter(input):
        if item.name: yield item
        else: last = item
    if last is not None: yield last

output = list(keep_last_empty(input))

Globalement, la lisibilité l'emporte à chaque fois sur la compacité.

5
user2735379

Si au lieu de flag = True on peut 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 le filtre est mieux écrit comme:

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

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

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

Si vous avez besoin d'un lambda pour vous souvenir de l'état entre les appels, je vous recommanderais soit une fonction déclarée dans l'espace de noms local, soit une classe avec un __call__ Surchargé. Maintenant que toutes mes mises en garde contre ce que vous essayez de faire sont hors de propos, nous pouvons obtenir une réponse concrète à votre question.

Si vous avez vraiment besoin de votre lambda pour avoir de la mémoire entre les appels, vous pouvez le définir comme suit:

f = lambda o, ns = {"flag":True}: [ns["flag"] or o.name, ns.__setitem__("flag", ns["flag"] and o.name)][0]

Ensuite, il vous suffit de passer f à filter(). Si vous en avez vraiment besoin, vous pouvez récupérer la valeur de flag avec les éléments suivants:

f.__defaults__[0]["flag"]

Vous pouvez également modifier l’espace de nom global en modifiant le résultat de globals(). Malheureusement, vous ne pouvez pas modifier l'espace de noms local de la même manière, car modifier le résultat de locals() n'affecte pas l'espace de noms local.

3
JPvdMerwe

Non, vous ne pouvez pas mettre une tâche dans un lambda à cause de sa propre définition. Si vous utilisez une programmation fonctionnelle, vous devez alors supposer que vos valeurs ne sont pas mutables.

Une solution serait le code suivant:

output = lambda l, name: [] if l==[] \
             else [ l[ 0 ] ] + output( l[1:], name ) if l[ 0 ].name == name \
             else output( l[1:], name ) if l[ 0 ].name == "" \
             else [ l[ 0 ] ] + output( l[1:], name )
3
Baltasarq

Vous pouvez utiliser une fonction bind pour utiliser une pseudo-instruction lambda à déclarations multiples. Ensuite, vous pouvez utiliser une classe wrapper pour un indicateur afin d'activer l'affectation.

bind = lambda x, f=(lambda y: y): f(x)

class Flag(object):
    def __init__(self, value):
        self.value = value

    def set(self, value):
        self.value = value
        return value

input = [Object(name=""), Object(name="fake_name"), Object(name="")]
flag = Flag(True)
output = filter(
            lambda o: (
                bind(flag.value, lambda orig_flag_value:
                bind(flag.set(flag.value and bool(o.name)), lambda _:
                bind(orig_flag_value or bool(o.name))))),
            input)
3
pyrospade