Affectation dans une expression lambda en Python

105

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

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 lambdaexpression? 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
)
Chat
la source
1
Non, mais vous n'en avez pas besoin. En fait, je pense que ce serait une façon assez obscure d'y parvenir même si cela fonctionnait.
8
Pourquoi ne pas simplement passer une ancienne fonction ordinaire dans le filtre?
dfb
5
Je voulais utiliser lambda juste pour que ce soit une solution vraiment compacte. Je me souviens que dans OCaml, je pouvais enchaîner les instructions d'impression avant l'expression de retour, je pensais que cela pourrait être répliqué en Python
Cat
Il est assez pénible d'être dans le flux de développement d'une pipeilne enchaînée puis de se rendre compte: "oh je veux créer une variable temp pour rendre le flux plus clair" ou "je veux enregistrer cette étape intermédiaire": et puis il faut sauter ailleurs pour créer une fonction pour le faire: et nommer cette fonction et en garder une trace - même si elle n'est utilisée qu'à un seul endroit.
javadba

Réponses:

215

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 expression (...)entre parenthèses , entre crochets [...]ou accolades {...}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 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 l'un ou l'autre de ces éléments dans votre exemple car votre variable se flagtrouve dans une portée externe et non dans la lambdaportée de. Cela n'a pas à voir avec lambda, c'est le comportement général de Python 2. Python 3 vous permet de contourner cela avec le nonlocalmot - clé à l'intérieur de defs, mais nonlocalne peut pas être utilisé dans lambdas.

Il y a une solution de contournement (voir ci-dessous), mais pendant 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 d'un rayon de 10,0 cm et d'une hauteur de 20,0 cm a un volume de 6283,2 cm³.
Un cylindre d'un rayon de 20,0 cm et d'une hauteur de 40,0 cm a un volume de 50265,5 cm³.
Un cylindre d'un rayon de 30,0cm et d'une hauteur de 60,0cm a un volume de 169646,0cm³.

Veuillez ne pas le faire.


... retour à votre exemple d'origine: bien que vous ne puissiez pas effectuer de tâches flag variable dans la portée externe, vous pouvez utiliser des fonctions pour modifier la valeur précédemment attribuée.

Par exemple, flagpourrait être un objet dont .valuenous 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 adapter 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 vraiment, dans un code sérieux, vous devriez toujours utiliser une définition de fonction régulière au lieu d'une lambdasi vous allez faire 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)
Jérémie
la source
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.
Jeremy
en bref, cela se résume à: utiliser .setattr()et semblables (les dictionnaires devraient faire aussi bien, par exemple) pour pirater les effets secondaires dans le code fonctionnel de toute façon, du code sympa de @JeremyBanks a été montré :)
jno
Thx pour la note sur le assignment operator!
javadba
37

Vous ne pouvez pas vraiment maintenir l'état dans une expression filter/ lambda(à moins d'abuser de l'espace de noms global). Vous pouvez cependant obtenir quelque chose de similaire en utilisant le résultat accumulé transmis dans une 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, il filtre les doublons, mais vous pouvez également utilisera.count("") , par exemple, pour limiter uniquement les chaînes vides.

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

Enfin, vous pouvez tout faire en Python pur lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/

Ivo van der Wijk
la source
17

Il n'est pas nécessaire d'utiliser un lambda, lorsque vous pouvez supprimer tous les null, et en remettre un si la taille d'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=""))
Gabi Purcaru
la source
1
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].
halex
L'ordre des éléments peut être important.
MAnyKey
15

L'affectation normale ( =) n'est pas possible dans une lambdaexpression, bien qu'il soit possible d'effectuer diverses astuces avec setattret entre amis.

Cependant, résoudre 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
    )

qui te donnera

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

Comme vous pouvez le voir, il conserve la première instance vide au lieu de la dernière. Si vous avez besoin du dernier à la place, inversez la liste entrant dans filteret inversez 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]

qui te donnera

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

Une chose à savoir: pour que cela fonctionne avec des objets arbitraires, ces objets doivent être correctement implémentés __eq__et __hash__comme expliqué ici .

Ethan Furman
la source
7

MISE À JOUR :

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

ou en utilisant filteret lambda:

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

Réponse précédente

OK, êtes-vous bloqué sur l'utilisation du filtre et du lambda?

Il semble que ce 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'affectation dans un lambda est similaire à la raison pour laquelle il n'autorise pas l'affectation dans une compréhension et cela a quelque chose à voir avec le fait que ces choses sont évaluées sur le Ccôté et peuvent donc nous donner un augmentation de la vitesse. Du moins, c'est mon impression après avoir lu l' un des essais de Guido .

Je suppose que cela irait également à l'encontre de la philosophie d'avoir une seule bonne façon de faire une seule chose en Python.

laitier
la source
Donc ce n'est pas tout à fait vrai. Il ne conservera pas l'ordre et ne conservera pas non plus les doublons d'objets sans chaîne vide.
JPvdMerwe
7

TL; DR: Lorsque vous utilisez des idiomes fonctionnels, il est préférable d'écrire du code fonctionnel

Comme beaucoup de gens l'ont souligné, en Python, l'attribution de lambdas n'est pas autorisée. En général, lorsque vous utilisez des idiomes fonctionnels, il vaut mieux penser de manière fonctionnelle, ce qui signifie dans la mesure du possible pas d'effets secondaires et pas de tâches.

Voici une solution fonctionnelle qui utilise un lambda. J'ai assigné le lambda à fnpour 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 faire cette affaire avec des itérateurs plutôt que des listes en changeant un peu 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 instructions.

régime bouddha
la source
6

Si au lieu de cela, flag = Truenous 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 suit:

>>> 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)
Jon Clements
la source
6

La manière pythonique de suivre l'état pendant l'itération est d'utiliser des générateurs. La manière itertools est assez difficile à comprendre à mon humble avis et essayer de pirater des 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))

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

user2735379
la source
4

Non, vous ne pouvez pas placer une affectation dans un lambda en raison de sa propre définition. Si vous utilisez la programmation fonctionnelle, vous devez supposer que vos valeurs ne sont pas modifiables.

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 )
Baltasarq
la source
4

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

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

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 flagavec les éléments suivants:

f.__defaults__[0]["flag"]

Vous pouvez également modifier l'espace de noms 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 que la modification du résultat de locals()n'affecte pas l'espace de noms local.

JPvdMerwe
la source
Ou tout simplement utiliser le Lisp d' origine: (let ((var 42)) (lambda () (setf var 43))).
Kaz
4

Vous pouvez utiliser une fonction de liaison pour utiliser une pseudo-instruction lambda à plusieurs instructions. 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)
pyrospade
la source
0

Une sorte de solution de contournement compliquée, mais l'affectation dans les lambdas est de toute façon illégale, donc cela n'a pas vraiment d'importance. Vous pouvez utiliser la exec()fonction intégrée pour exécuter l'affectation à partir de l'intérieur du lambda, comme cet exemple:

>>> val
Traceback (most recent call last):
  File "<pyshell#31>", line 1, in <module>
    val
NameError: name 'val' is not defined
>>> d = lambda: exec('val=True', globals())
>>> d()
>>> val
True
Utilisateur 12692182
la source
-2

d'abord, vous n'avez pas besoin d'utiliser une affectation locale pour votre travail, vérifiez simplement la réponse ci-dessus

deuxièmement, il est simple d'utiliser locals () et globals () pour obtenir la table des variables, puis changer la valeur

vérifiez cet exemple de code:

print [locals().__setitem__('x', 'Hillo :]'), x][-1]

si vous devez changer l'ajout d'une variable globale à votre environnement, essayez de remplacer locals () par globals ()

La liste de compilation de python est cool mais la plupart des projets triditionnels n'acceptent pas cela (comme flask: [)

j'espère que ça pourrait aider

jyf1987
la source
2
Vous ne pouvez pas utiliser locals(), il dit explicitement dans la documentation que le changer ne change pas réellement la portée locale (ou du moins ce ne sera pas toujours). globals()d'autre part fonctionne comme prévu.
JPvdMerwe
@JPvdMer, essayez-le, ne suivez pas le document à l'aveuglette. et l'affectation dans lambda enfreint déjà la règle
jyf1987
3
Cela ne fonctionne malheureusement que dans l'espace de noms global, auquel cas vous devriez vraiment l'utiliser globals(). pastebin.com/5Bjz1mR4 (testé en 2.6 et 3.2) le prouve.
JPvdMerwe