Przypisanie wewnątrz wyrażenia lambda w Pythonie


105

Mam listę obiektów i chcę usunąć wszystkie obiekty, które są puste, z wyjątkiem jednego, używając filteri lambdawyrażenia.

Na przykład, jeśli dane wejściowe to:

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

... wtedy wynik powinien wyglądać następująco:

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

Czy istnieje sposób na dodanie przypisania do lambdawyrażenia? Na przykład:

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
Nie. Ale nie potrzebujesz tego. Właściwie myślę, że byłby to dość niejasny sposób osiągnięcia tego, nawet gdyby zadziałał.

8
Dlaczego nie przekazać zwykłej starej funkcji do filtra?
dfb

5
Chciałem użyć lambdy tylko po to, aby było to naprawdę kompaktowe rozwiązanie. Pamiętam, że w OCaml mogłem łączyć instrukcje drukowania przed wyrażeniem powrotu, pomyślałem, że można to powtórzyć w Pythonie
Cat

To dość bolesne, gdy jesteś w trakcie opracowywania połączonego łańcucha, a następnie zdaj sobie sprawę: „och, chcę utworzyć zmienną tymczasową, aby przepływ był bardziej przejrzysty” lub „chcę zarejestrować ten etap pośredni”: i wtedy musisz skoczyć gdzie indziej, aby utworzyć funkcję, która to zrobi: nazwij tę funkcję i śledź ją - nawet jeśli jest używana tylko w jednym miejscu.
javadba

Odpowiedzi:


215

Operator wyrażenia przypisania :=dodany w Pythonie 3.8 obsługuje przypisanie wewnątrz wyrażeń lambda. Ten operator może występować tylko w wyrażeniu w nawiasach (...), w nawiasach [...]lub w nawiasach {...}ze względów składniowych. Na przykład będziemy mogli napisać:

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

W Pythonie 2 możliwe było wykonywanie lokalnych przypisań jako efekt uboczny składanych list.

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

Jednak nie jest możliwe użycie żadnego z tych w twoim przykładzie, ponieważ twoja zmienna flagznajduje się w zakresie zewnętrznym, a nie lambdazasięgu. Nie ma to nic wspólnego lambda, jest to ogólne zachowanie w Pythonie 2. Python 3 pozwala ominąć ten problem za pomocą nonlocalsłowa kluczowego wewnątrz defs, ale nonlocalnie można go używać wewnątrz lambdas.

Istnieje obejście (patrz poniżej), ale skoro już jesteśmy w temacie ...


W niektórych przypadkach możesz tego użyć, aby zrobić wszystko w 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()])()

Cylinder o promieniu 10,0 cm i wysokości 20,0 cm ma objętość 6283,2 cm³.
Cylinder o promieniu 20,0 cm i wysokości 40,0 cm ma objętość 50265,5 cm³.
Cylinder o promieniu 30,0 cm i wysokości 60,0 cm ma objętość 169646,0 cm³.

Proszę nie.


... wracając do pierwotnego przykładu: chociaż nie możesz wykonywać przypisań do flagzmiennej w zakresie zewnętrznym, możesz użyć funkcji do zmodyfikowania poprzednio przypisanej wartości.

Na przykład flagmoże to być obiekt, którego .valueustawiamy za pomocą 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')]

Gdybyśmy chcieli dopasować powyższy motyw, moglibyśmy użyć rozumienia list zamiast setattr:

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

Ale tak naprawdę w poważnym kodzie zawsze powinieneś używać zwykłej definicji funkcji zamiast a, lambdajeśli zamierzasz wykonywać zewnętrzne przypisanie.

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)

Ostatni przykład w tej odpowiedzi nie daje takich samych wyników jak przykład, ale wydaje mi się, że przykładowe dane wyjściowe są niepoprawne.
Jeremy

w skrócie sprowadza się to do: use .setattr()i alikes ( słowniki też powinny robić np.) do włamania efektów ubocznych do kodu funkcjonalnego, pokazany został fajny kod @JeremyBanks :)
jno

Dzięki za uwagę na assignment operator!
javadba

37

Nie możesz naprawdę utrzymywać stanu w wyrażeniu filter/ lambda(chyba że nadużywasz globalnej przestrzeni nazw). Możesz jednak osiągnąć coś podobnego, używając skumulowanego wyniku przekazywanego w reduce()wyrażeniu:

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

Możesz oczywiście nieco poprawić stan. W tym przypadku odfiltrowuje duplikaty, ale możesz również użyć a.count(""), na przykład, aby ograniczyć tylko puste ciągi.

Nie trzeba dodawać, że możesz to zrobić, ale naprawdę nie powinieneś. :)

Wreszcie możesz zrobić wszystko w czystym Pythonie lambda: http://vanderwijk.info/blog/pure-lambda-calculus-python/


17

Nie ma potrzeby używania lambdy, kiedy możesz usunąć wszystkie puste i wstawić z powrotem, jeśli zmieni się rozmiar wejściowy:

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
Myślę, że masz mały błąd w swoim kodzie. Druga linia powinna być output = [x for x in input if x.name].
halex

Kolejność elementów może być ważna.
MAnyKey

15

Normalne przypisanie ( =) nie jest możliwe wewnątrz lambdawyrażenia, chociaż można wykonywać różne sztuczki z setattrprzyjaciółmi i znajomymi.

Jednak rozwiązanie problemu jest w rzeczywistości dość proste:

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
    )

który ci da

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

Jak widać, zachowuje pierwszą pustą instancję zamiast ostatniej. Jeśli zamiast tego potrzebujesz ostatniej, odwróć listę przechodzącą do filteri odwróć listę wychodzącą z filter:

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

który ci da

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

Jedna rzecz, o której należy pamiętać: aby to działało z dowolnymi obiektami, obiekty te muszą poprawnie implementować __eq__i __hash__jak wyjaśniono tutaj .


7

AKTUALIZACJA :

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

lub używając filteri lambda:

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

Poprzednia odpowiedź

OK, czy utkniesz na używaniu filtra i lambdy?

Wygląda na to, że lepiej byłoby to zrobić ze zrozumieniem słownikowym,

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

Myślę, że powód, dla którego Python nie zezwala na przypisanie w lambdzie, jest podobny do tego, dlaczego nie pozwala na przypisanie w zrozumieniu i ma to coś wspólnego z faktem, że te rzeczy są oceniane z Cboku, a zatem mogą dać nam wzrost prędkości. Przynajmniej takie mam wrażenie po przeczytaniu jednego z esejów Guido .

Domyślam się, że byłoby to również sprzeczne z filozofią posiadania jednego właściwego sposobu robienia dowolnej rzeczy w Pythonie.


Więc to nie jest do końca w porządku. Nie zachowa porządku ani nie zachowa duplikatów obiektów niepustych.
JPvdMerwe

7

TL; DR: Korzystając z idiomów funkcjonalnych, lepiej jest pisać kod funkcjonalny

Jak wiele osób zauważyło, w Pythonie przypisywanie lambd jest niedozwolone. Ogólnie rzecz biorąc, używając idiomów funkcjonalnych, lepiej jest myśleć w sposób funkcjonalny, co oznacza, że ​​wszędzie tam, gdzie to możliwe, nie ma żadnych skutków ubocznych i żadnych przypisań.

Oto funkcjonalne rozwiązanie wykorzystujące lambdę. Przypisałem lambdę fndla jasności (i dlatego, że jest trochę za długa).

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

Możesz także zrobić to z iteratorami zamiast list, zmieniając trochę rzeczy. Masz również kilka różnych importów.

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

Zawsze możesz zmienić kod w celu zmniejszenia długości instrukcji.


6

Jeśli zamiast tego flag = Truemożemy wykonać import, myślę, że spełnia to kryteria:

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

A może filtr lepiej napisać jako:

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

Lub po prostu dla prostej wartości logicznej, bez importu:

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

6

Pythonowym sposobem śledzenia stanu podczas iteracji są generatory. Metoda itertools jest dość trudna do zrozumienia IMHO, a próba hakowania lambd, aby to zrobić, jest po prostu głupia. Spróbuję:

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

Ogólnie rzecz biorąc, czytelność za każdym razem przewyższa zwartość.


4

Nie, nie możesz umieścić przypisania wewnątrz lambdy z powodu jej własnej definicji. Jeśli pracujesz z programowaniem funkcjonalnym, musisz założyć, że twoje wartości nie są zmienne.

Jednym z rozwiązań byłby następujący kod:

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

Jeśli potrzebujesz lambdy do zapamiętania stanu między wywołaniami, poleciłbym albo funkcję zadeklarowaną w lokalnej przestrzeni nazw, albo klasę z przeciążoną __call__ . Teraz, gdy wszystkie moje ostrzeżenia dotyczące tego, co próbujesz zrobić, są już na uboczu, możemy uzyskać rzeczywistą odpowiedź na Twoje pytanie.

Jeśli naprawdę potrzebujesz mieć swoją lambdę, aby mieć trochę pamięci między wywołaniami, możesz to zdefiniować w następujący sposób:

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

Następnie wystarczy przejść fdo filter(). Jeśli naprawdę potrzebujesz, możesz odzyskać wartość, flagwykonując następujące czynności:

f.__defaults__[0]["flag"]

Alternatywnie możesz zmodyfikować globalną przestrzeń nazw, modyfikując wynik globals(). Niestety, nie można modyfikować lokalnej przestrzeni nazw w taki sam sposób, jak modyfikowanie wyniku locals()nie wpływa na lokalną przestrzeń nazw.


Lub po prostu użyć oryginalnego Lisp: (let ((var 42)) (lambda () (setf var 43))).
Kaz

4

Możesz użyć funkcji bind, aby użyć pseudo-wyrażenia lambda. Następnie możesz użyć klasy opakowania dla flagi, aby umożliwić przypisanie.

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

Trochę niechlujne obejście, ale przypisanie w lambdach i tak jest nielegalne, więc nie ma to większego znaczenia. Możesz użyć wbudowanegoexec() funkcji aby uruchomić przypisanie z wnętrza lambda, tak jak w tym przykładzie:

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

Po pierwsze, nie musisz używać lokalnego przydziału do swojej pracy, po prostu sprawdź powyższą odpowiedź

po drugie, w prosty sposób można użyć locals () i globals () do pobrania tabeli zmiennych, a następnie do zmiany wartości

sprawdź ten przykładowy kod:

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

jeśli trzeba zmienić dodać zmienną globalną do Environ, spróbuj zastąpić mieszkańców () z globalnych ()

lista pythona comp jest fajna, ale większość projektów triditional nie akceptuje tego (jak flask: [)

mam nadzieję, że to pomoże


2
Nie możesz tego użyć locals(), w dokumentacji jest wyraźnie napisane, że jego zmiana w rzeczywistości nie zmienia zakresu lokalnego (a przynajmniej nie zawsze). globals()z drugiej strony działa zgodnie z oczekiwaniami.
JPvdMerwe

@JPvdMerwe po prostu spróbuj, nie podążaj ślepo za dokumentem. a przypisanie w
lambdzie

3
Niestety działa tylko w globalnej przestrzeni nazw, w takim przypadku naprawdę powinieneś używać globals(). pastebin.com/5Bjz1mR4 (testowane zarówno w 2.6, jak i 3.2) to potwierdza.
JPvdMerwe,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.