Assegnazione all'interno dell'espressione lambda in Python


105

Ho un elenco di oggetti e desidero rimuovere tutti gli oggetti vuoti tranne uno, utilizzando filtere lambdaun'espressione.

Ad esempio se l'ingresso è:

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

... allora l'output dovrebbe essere:

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

C'è un modo per aggiungere un compito a lambdaun'espressione? Per esempio:

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
No. Ma non ne hai bisogno. In realtà penso che sarebbe un modo piuttosto oscuro per ottenere questo risultato anche se funzionasse.

8
Perché non passare semplicemente una vecchia funzione normale al filtro?
DFB

5
Volevo usare lambda solo per essere una soluzione davvero compatta. Ricordo che in OCaml potevo concatenare le dichiarazioni di stampa prima dell'espressione di ritorno, pensavo che questo potesse essere replicato in Python
Cat

È abbastanza doloroso essere nel flusso dello sviluppo di un pipeilne incatenato e poi realizzare: "oh, voglio creare una temp var per rendere il flusso più chiaro" o "voglio registrare questo passaggio intermedio": e poi devi saltare da qualche altra parte per creare una funzione per farlo: e denominare quella funzione e tenerne traccia, anche se è usata in un solo posto.
javadba

Risposte:


215

L'operatore di espressione di assegnazione :=aggiunto in Python 3.8 supporta l'assegnazione all'interno di espressioni lambda. Questo operatore può essere visualizzato solo all'interno di un'espressione tra parentesi (...), tra parentesi [...]o tra parentesi {...}per motivi sintattici. Ad esempio, saremo in grado di scrivere quanto segue:

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

In Python 2, era possibile eseguire assegnazioni locali come effetto collaterale della comprensione delle liste.

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

Tuttavia, non è possibile utilizzare nessuno di questi nel tuo esempio perché la tua variabile flagè in un ambito esterno, non in quello di lambda. Questo non ha a che fare con lambda, è il comportamento generale in Python 2. Python 3 ti consente di aggirare questo problema con la nonlocalparola chiave all'interno di defs, ma nonlocalnon può essere usato all'internolambda s.

C'è una soluzione alternativa (vedi sotto), ma visto che siamo in argomento ...


In alcuni casi puoi usarlo per fare tutto all'interno di 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 cilindro con un raggio di 10,0 cm e un'altezza di 20,0 cm ha un volume di 6283,2 cm³.
Un cilindro con un raggio di 20,0 cm e un'altezza di 40,0 cm ha un volume di 50265,5 cm³.
Un cilindro con un raggio di 30,0 cm e un'altezza di 60,0 cm ha un volume di 169646,0 cm³.

Per favore non farlo.


... tornando al tuo esempio originale: anche se non puoi eseguire compiti nel file flag variabile nell'ambito esterno, è possibile utilizzare funzioni per modificare il valore assegnato in precedenza.

Ad esempio, flagpotrebbe essere un oggetto di cui .valueabbiamo impostato utilizzando 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')]

Se volessimo adattare il tema di cui sopra, potremmo usare una comprensione dell'elenco invece di setattr:

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

Ma in realtà, nel codice serio dovresti sempre usare una definizione di funzione regolare invece di una lambdase hai intenzione di fare assegnazioni esterne.

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)

L'ultimo esempio in questa risposta non produce lo stesso output dell'esempio, ma mi sembra che l'output dell'esempio non sia corretto.
Jeremy

in breve, questo si riduce a: use .setattr()and alikes (i dizionari dovrebbero fare altrettanto, per esempio) per hackerare gli effetti collaterali in codice funzionale comunque, è stato mostrato un fantastico codice di @JeremyBanks :)
jno

Grazie per la nota sul assignment operator!
javadba

37

Non è possibile mantenere lo stato in un'espressione filter/ lambda(a meno che non si abusi dello spazio dei nomi globale). Puoi tuttavia ottenere qualcosa di simile usando il risultato accumulato passato in reduce()un'espressione:

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

Puoi, ovviamente, modificare un po 'la condizione. In questo caso filtra i duplicati, ma puoi anche usarea.count("") , ad esempio, per limitare solo le stringhe vuote.

Inutile dire che puoi farlo ma davvero non dovresti. :)

Infine, puoi fare qualsiasi cosa in puro Python lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/


17

Non è necessario utilizzare un lambda, quando è possibile rimuovere tutti quelli nulli e rimetterne uno se la dimensione dell'input cambia:

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
Penso che tu abbia un piccolo errore nel tuo codice. La seconda riga dovrebbe essere output = [x for x in input if x.name].
halex

L'ordine degli elementi può essere importante.
MAnyKey

15

L'assegnazione normale ( =) non è possibile all'interno di lambdaun'espressione, sebbene sia possibile eseguire vari trucchi con setattre gli amici.

Risolvere il tuo problema, tuttavia, è in realtà abbastanza semplice:

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
    )

che ti darà

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

Come puoi vedere, mantiene la prima istanza vuota invece dell'ultima. Se invece hai bisogno dell'ultimo, inverti l'elenco in filterentrata e inverti l'elenco in uscita da filter:

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

che ti darà

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

Una cosa di cui essere consapevoli: affinché funzioni con oggetti arbitrari, quegli oggetti devono essere implementati correttamente __eq__e __hash__come spiegato qui .


7

AGGIORNAMENTO :

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

o utilizzando filtere lambda:

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

Risposta precedente

OK, sei bloccato sull'uso di filtro e lambda?

Sembra che questo sarebbe meglio servito con una comprensione del dizionario,

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

Penso che il motivo per cui Python non consente l'assegnazione in un lambda sia simile al motivo per cui non consente l'assegnazione in una comprensione e questo ha qualcosa a che fare con il fatto che queste cose vengono valutate sul Clato e quindi possono darci un aumento della velocità. Almeno questa è la mia impressione dopo aver letto uno dei saggi di Guido .

La mia ipotesi è che questo andrebbe anche contro la filosofia di avere un modo giusto di fare qualsiasi cosa in Python.


Quindi questo non è del tutto corretto. Non manterrà l'ordine, né conserverà i duplicati di oggetti con stringhe non vuote.
JPvdMerwe

7

TL; DR: quando si utilizzano idiomi funzionali è meglio scrivere codice funzionale

Come molte persone hanno sottolineato, in Python l'assegnazione di lambda non è consentita. In generale, quando si utilizzano idiomi funzionali, è meglio pensare in modo funzionale, il che significa, ove possibile, nessun effetto collaterale e nessun incarico.

Ecco una soluzione funzionale che utilizza un lambda. Ho assegnato il lambda a fnper chiarezza (e perché è diventato un po 'lungo).

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

Puoi anche fare questo accordo con gli iteratori piuttosto che con gli elenchi cambiando leggermente le cose. Hai anche alcune importazioni diverse.

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

È sempre possibile rioganizzare il codice per ridurre la lunghezza delle istruzioni.


6

Se invece di flag = Truepossiamo fare un'importazione, allora penso che soddisfi i criteri:

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

O forse il filtro è meglio scritto come:

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

Oppure, solo per un semplice booleano, senza alcuna importazione:

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

6

Il modo pitonico per tenere traccia dello stato durante l'iterazione è con i generatori. Il modo in cui itertools è abbastanza difficile da capire IMHO e cercare di hackerare lambda per farlo è semplicemente sciocco. Proverei:

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

Nel complesso, la leggibilità supera ogni volta la compattezza.


4

No, non puoi inserire un compito all'interno di un lambda a causa della sua stessa definizione. Se lavori utilizzando la programmazione funzionale, devi presumere che i tuoi valori non siano modificabili.

Una soluzione sarebbe il seguente codice:

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 )

4

Se hai bisogno di un lambda per ricordare lo stato tra le chiamate, consiglierei una funzione dichiarata nello spazio dei nomi locale o una classe con un sovraccarico __call__ . Ora che tutti i miei avvertimenti contro ciò che stai cercando di fare sono fuori mano, possiamo arrivare a una risposta effettiva alla tua domanda.

Se hai davvero bisogno di avere il tuo lambda per avere un po 'di memoria tra le chiamate, puoi definirlo come:

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

Quindi devi solo passare fa filter(). Se ne hai davvero bisogno, puoi recuperare il valore di flagcon quanto segue:

f.__defaults__[0]["flag"]

In alternativa, è possibile modificare lo spazio dei nomi globale modificando il risultato di globals(). Sfortunatamente, non puoi modificare lo spazio dei nomi locale nello stesso modo in cui la modifica del risultato di locals()non influisce sullo spazio dei nomi locale.


O semplicemente utilizzare il Lisp originale: (let ((var 42)) (lambda () (setf var 43))).
Kaz

4

È possibile utilizzare una funzione di associazione per utilizzare una pseudo espressione lambda a più istruzioni. Quindi puoi usare una classe wrapper per un Flag per abilitare l'assegnazione.

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)

0

Una specie di soluzione alternativa, ma l'assegnazione in lambda è comunque illegale, quindi non ha molta importanza. Puoi usare la exec()funzione incorporata per eseguire l'assegnazione dall'interno di lambda, come questo esempio:

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

-2

in primo luogo, non è necessario utilizzare un'assegnazione locale per il lavoro, basta controllare la risposta sopra

secondo, è semplice usare locals () e globals () per ottenere la tabella delle variabili e quindi modificare il valore

controlla questo codice di esempio:

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

se devi cambiare l'aggiunta di una variabile globale al tuo ambiente, prova a sostituire locals () con globals ()

l'elenco comp di python è interessante ma la maggior parte del progetto tradizionale non lo accetta (come flask: [)

spero che possa aiutare


2
Non puoi usarlo locals(), dice esplicitamente nella documentazione che cambiarlo in realtà non cambia l'ambito locale (o almeno non sempre). globals()d'altra parte funziona come previsto.
JPvdMerwe

@JPvdMer, ci proviamo, non seguire ciecamente il documento. e l'assegnazione in lambda sta già infrangendo la regola
jyf1987

3
Sfortunatamente funziona solo nello spazio dei nomi globale, nel qual caso dovresti davvero usare globals(). pastebin.com/5Bjz1mR4 (testato sia in 2.6 che in 3.2) lo dimostra.
JPvdMerwe
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.