Zuweisung innerhalb des Lambda-Ausdrucks in Python

105

Ich habe eine Liste von Objekten und möchte alle Objekte entfernen, die leer sind, bis auf eines, using filterund einen lambdaAusdruck.

Zum Beispiel, wenn die Eingabe ist:

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

... dann sollte die Ausgabe sein:

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

Gibt es eine Möglichkeit, einem lambdaAusdruck eine Zuweisung hinzuzufügen ? Beispielsweise:

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
)
Katze
quelle
1
Nein, aber das brauchst du nicht. Eigentlich denke ich, dass es ein ziemlich dunkler Weg wäre, dies zu erreichen, selbst wenn es funktionieren würde.
8
Warum nicht einfach eine normale alte Funktion an den Filter übergeben?
dfb
5
Ich wollte Lambda verwenden, damit es eine wirklich kompakte Lösung ist. Ich erinnere mich, dass ich in OCaml Druckanweisungen vor dem Rückgabeausdruck verketten konnte, dachte, dies könnte in Python
Cat
Es ist ziemlich schmerzhaft, im Fluss der Entwicklung einer verketteten Pipeilne zu sein und dann zu erkennen: "Oh, ich möchte eine temporäre Variable erstellen, um den Fluss klarer zu machen" oder "Ich möchte diesen Zwischenschritt protokollieren": und dann muss man springen woanders, um eine Funktion dafür zu erstellen: und benenne diese Funktion und verfolge sie - obwohl sie nur an einer Stelle verwendet wird.
Javadba

Antworten:

215

Der :=in Python 3.8 hinzugefügte Zuweisungsausdrucksoperator unterstützt die Zuweisung innerhalb von Lambda-Ausdrücken. Dieser Operator kann aus syntaktischen Gründen nur in einem Ausdruck in Klammern (...), Klammern [...]oder Klammern angezeigt {...}werden. Zum Beispiel können wir Folgendes schreiben:

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

In Python 2 war es möglich, lokale Zuweisungen als Nebeneffekt des Listenverständnisses durchzuführen.

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

In Ihrem Beispiel ist es jedoch nicht möglich, eines dieser Elemente zu verwenden, da sich Ihre Variable flagin einem äußeren Bereich befindet, nicht im lambdaBereich des. Dies hat nichts mit lambdadem allgemeinen Verhalten in Python 2 zu tun. Mit Python 3 können Sie dies mit dem nonlocalSchlüsselwort in defs umgehen, nonlocalkönnen jedoch nicht in lambdas verwendet werden.

Es gibt eine Problemumgehung (siehe unten), aber während wir beim Thema sind ...


In einigen Fällen können Sie dies verwenden, um alles innerhalb eines 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()])()

Ein Zylinder mit einem Radius von 10,0 cm und einer Höhe von 20,0 cm hat ein Volumen von 6283,2 cm³.
Ein Zylinder mit einem Radius von 20,0 cm und einer Höhe von 40,0 cm hat ein Volumen von 50265,5 cm³.
Ein Zylinder mit einem Radius von 30,0 cm und einer Höhe von 60,0 cm hat ein Volumen von 169646,0 cm³.

Bitte nicht.


... zurück zu Ihrem ursprünglichen Beispiel: Obwohl Sie der flagVariablen im äußeren Bereich keine Zuweisungen vornehmen können, können Sie den zuvor zugewiesenen Wert mithilfe von Funktionen ändern.

Zum Beispiel flagkönnte ein Objekt sein, dessen .valuewir setzen mit 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')]

Wenn wir das obige Thema anpassen wollten, könnten wir ein Listenverständnis verwenden, anstatt setattr:

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

Aber wirklich, in seriösem Code sollten Sie immer eine reguläre Funktionsdefinition anstelle einer verwenden, lambdawenn Sie eine äußere Zuweisung vornehmen möchten.

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)
Jeremy Banks
quelle
Das letzte Beispiel in dieser Antwort erzeugt nicht die gleiche Ausgabe wie das Beispiel, aber es sieht für mich so aus, als ob die Beispielausgabe falsch ist.
Jeremy Banks
Kurz gesagt, dies läuft darauf hinaus: Verwenden .setattr()und gleich ( Wörterbücher sollten es auch tun), um Nebenwirkungen in Funktionscode zu hacken, cooler Code von @JeremyBanks wurde gezeigt :)
jno
Danke für den Hinweis auf die assignment operator!
Javadba
37

Sie können den Status in einem filter/ lambdaAusdruck nicht wirklich beibehalten (es sei denn, Sie missbrauchen den globalen Namespace). Sie können jedoch etwas Ähnliches erreichen, indem Sie das akkumulierte Ergebnis in einem reduce()Ausdruck weitergeben:

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

Sie können den Zustand natürlich ein wenig anpassen. In diesem Fall werden Duplikate herausgefiltert, aber Sie können a.count("")beispielsweise auch nur leere Zeichenfolgen einschränken.

Unnötig zu sagen, dass Sie dies tun können, aber Sie sollten es wirklich nicht tun. :) :)

Schließlich können Sie alles in reinem Python tun lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/

Ivo van der Wijk
quelle
17

Es ist nicht erforderlich, ein Lambda zu verwenden, wenn Sie alle Nullen entfernen und eines zurücksetzen können, wenn sich die Eingabegröße ändert:

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
quelle
1
Ich denke, Sie haben einen kleinen Fehler in Ihrem Code. Die zweite Zeile sollte sein output = [x for x in input if x.name].
Halex
Die Reihenfolge der Elemente kann wichtig sein.
MAnyKey
15

Eine normale Zuweisung ( =) ist innerhalb eines lambdaAusdrucks nicht möglich , obwohl es möglich ist, verschiedene Tricks mit setattrund mit Freunden auszuführen .

Die Lösung Ihres Problems ist jedoch recht einfach:

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
    )

was dir geben wird

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

Wie Sie sehen können, wird die erste leere Instanz anstelle der letzten beibehalten. Wenn Sie stattdessen die letzte benötigen, kehren Sie die Liste um, in die Sie gehen filter, und kehren Sie die Liste um, aus der Folgendes hervorgeht filter:

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

was dir geben wird

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

Eines ist zu beachten: Damit dies mit beliebigen Objekten funktioniert, müssen diese Objekte ordnungsgemäß implementiert werden __eq__und __hash__wie hier erläutert .

Ethan Furman
quelle
7

UPDATE :

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

oder mit filterund lambda:

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

Vorherige Antwort

OK, verwenden Sie Filter und Lambda?

Es scheint, als wäre dies besser mit einem Wörterbuchverständnis bedient,

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

Ich denke, der Grund, warum Python keine Zuweisung in einem Lambda zulässt, ist ähnlich wie der Grund, warum es keine Zuweisung in einem Verständnis zulässt, und das hat etwas damit zu tun, dass diese Dinge nebenbei ausgewertet werden Cund uns somit eine geben können Geschwindigkeitssteigerung. Zumindest ist das mein Eindruck, nachdem ich einen von Guidos Essays gelesen habe .

Ich vermute, dies würde auch gegen die Philosophie verstoßen, einen richtigen Weg zu haben, um irgendetwas in Python zu tun.

Milchpfleger
quelle
Das ist also nicht ganz richtig. Es wird weder die Reihenfolge noch Duplikate von Objekten mit nicht leeren Zeichenfolgen beibehalten.
JPvdMerwe
7

TL; DR: Wenn Sie funktionale Redewendungen verwenden, ist es besser, funktionalen Code zu schreiben

Wie viele Leute darauf hingewiesen haben, ist in Python die Zuweisung von Lambdas nicht erlaubt. Wenn Sie funktionale Redewendungen verwenden, sollten Sie im Allgemeinen besser funktional denken, was bedeutet, dass nach Möglichkeit keine Nebenwirkungen und keine Aufgaben auftreten.

Hier ist eine funktionale Lösung, die ein Lambda verwendet. Ich habe das Lambda der fnKlarheit halber zugewiesen (und weil es etwas langwierig wurde).

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

Sie können sich auch mit Iteratoren und nicht mit Listen befassen, indem Sie die Dinge ein wenig ändern. Sie haben auch verschiedene Importe.

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

Sie können den Code jederzeit umbenennen, um die Länge der Anweisungen zu verringern.

Dietbuddha
quelle
6

Wenn flag = Truewir stattdessen einen Import durchführen können, dann entspricht dies meiner Meinung nach den Kriterien:

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

Oder vielleicht ist der Filter besser geschrieben als:

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

Oder nur für einen einfachen Booleschen Wert ohne Importe:

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

Die pythonische Methode zum Verfolgen des Status während der Iteration sind Generatoren. Der itertools-Weg ist meiner Meinung nach ziemlich schwer zu verstehen, und der Versuch, Lambdas zu hacken, um dies zu tun, ist einfach albern. Ich würde versuchen:

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

Insgesamt übertrifft die Lesbarkeit jedes Mal die Kompaktheit.

user2735379
quelle
4

Nein, Sie können eine Zuordnung aufgrund ihrer eigenen Definition nicht in ein Lambda einfügen. Wenn Sie mit funktionaler Programmierung arbeiten, müssen Sie davon ausgehen, dass Ihre Werte nicht veränderbar sind.

Eine Lösung wäre der folgende Code:

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

Wenn Sie ein Lambda benötigen, um sich den Status zwischen Aufrufen zu merken, würde ich entweder eine im lokalen Namespace deklarierte Funktion oder eine Klasse mit einer Überladung empfehlen __call__. Jetzt, da alle meine Warnungen vor dem, was Sie versuchen, aus dem Weg sind, können wir eine tatsächliche Antwort auf Ihre Frage erhalten.

Wenn Sie Ihr Lambda wirklich benötigen, um zwischen den Aufrufen Speicher zu haben, können Sie es wie folgt definieren:

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

Dann brauchen Sie nur passieren fzu filter(). Wenn Sie es wirklich brauchen, können Sie den Wert von flagmit den folgenden zurückerhalten :

f.__defaults__[0]["flag"]

Alternativ können Sie den globalen Namespace ändern, indem Sie das Ergebnis von ändern globals(). Leider können Sie den lokalen Namespace nicht auf die gleiche Weise ändern, wie das Ändern des Ergebnisses von locals()den lokalen Namespace nicht beeinflusst.

JPvdMerwe
quelle
Oder verwenden Sie einfach das Original Lisp : (let ((var 42)) (lambda () (setf var 43))).
Kaz
4

Sie können eine Bindefunktion verwenden, um ein Lambda mit mehreren Pseudoanweisungen zu verwenden. Anschließend können Sie eine Wrapper-Klasse für ein Flag verwenden, um die Zuweisung zu aktivieren.

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

Eine Art chaotischer Workaround, aber die Zuweisung in Lambdas ist sowieso illegal, also spielt es keine Rolle. Sie können die integrierte exec()Funktion verwenden, um die Zuweisung innerhalb des Lambda auszuführen, wie in diesem Beispiel:

>>> 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
Benutzer 12692182
quelle
-2

Zunächst müssen Sie für Ihren Job keine lokale Zuordnung verwenden. Überprüfen Sie einfach die obige Antwort

Zweitens ist es einfach, mit local () und globals () die Variablentabelle abzurufen und dann den Wert zu ändern

Überprüfen Sie diesen Beispielcode:

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

Wenn Sie das Add eine globale Variable auf Ihre environ ändern müssen, versuchen zu ersetzen Einheimischen () mit Globals ()

Pythons List Comp ist cool, aber die meisten der drei Projekte akzeptieren dies nicht (wie flask: [)

hoffe es könnte helfen

jyf1987
quelle
2
Sie können nicht verwenden locals(), es wird ausdrücklich in der Dokumentation angegeben, dass eine Änderung nicht den lokalen Bereich ändert (oder zumindest nicht immer). globals()funktioniert dagegen wie erwartet.
JPvdMerwe
@JPvdMerwe versuchen Sie es einfach, folgen Sie dem Dokument nicht blind. und die Zuordnung in Lambda
verstößt
3
Es funktioniert leider nur im globalen Namespace. In diesem Fall sollten Sie es wirklich verwenden globals(). pastebin.com/5Bjz1mR4 (getestet in 2.6 und 3.2) beweist es.
JPvdMerwe