Python: Dlaczego funkcja funools.partial jest konieczna?

193

Częściowe zastosowanie jest fajne. Jaką funkcjonalność functools.partialoferuje nie można uzyskać przez lambdas?

>>> sum = lambda x, y : x + y
>>> sum(1, 2)
3
>>> incr = lambda y : sum(1, y)
>>> incr(2)
3
>>> def sum2(x, y):
    return x + y

>>> incr2 = functools.partial(sum2, 1)
>>> incr2(4)
5

Czy jest w functoolsjakiś sposób bardziej wydajny lub czytelny?

Nick Heiner
źródło

Odpowiedzi:

266

Jaką funkcjonalność functools.partialoferuje nie można uzyskać przez lambdas?

Niewiele pod względem dodatkowej funkcjonalności (ale zobacz później) - a czytelność zależy od obserwatora.
Większość osób, które są obeznane z funkcjonalnymi językami programowania (szczególnie te z rodzin Lisp / Scheme) wydają się lubić lambda- mówię „większość”, zdecydowanie nie wszystkie, ponieważ Guido i ja z pewnością należą do osób „zaznajomionych” (itp.) ), ale myślę o lambdaanomalii odleżyn w Pythonie ...
Był skruszony, że kiedykolwiek zaakceptował go w Pythonie, podczas gdy planował usunąć go z Pythona 3, jako jedną z „usterek Pythona”.
W pełni go w tym wspierałem. (Uwielbiam lambda w Schemacie ... podczas gdy jego ograniczenia w Pythonie i dziwny sposób, że po prostu nie robi resztą języka spraw, by moja skóra czołgała się).

Nie dotyczy to jednak hord lambdakochanków - którzy zainscenizowali jedną z najbliższych rzeczy buntu, jaką kiedykolwiek widzieliśmy w historii Pythona, dopóki Guido nie wycofał się i nie zdecydował się odejść lambda.
Kilka możliwych dodatków do functools(aby funkcje zwracały stałe, tożsamość, itp.) nie nastąpiło (aby uniknąć jawnego powielania większej liczby lambdafunkcji), chociaż partialoczywiście pozostało (nie jest to całkowite powielanie, ani nie jest odrażeniem).

Pamiętaj, że lambdaciało jest ograniczone do wyrażania się , więc ma ograniczenia. Na przykład...:

>>> import functools
>>> f = functools.partial(int, base=2)
>>> f.args
()
>>> f.func
<type 'int'>
>>> f.keywords
{'base': 2}
>>> 

functools.partialZwrócona funkcja jest ozdobiona atrybutami użytecznymi do introspekcji - funkcja ta jest zawijana oraz jakie argumenty pozycyjne i nazwane w niej naprawia. Co więcej, wymienione argumenty można od razu zastąpić („ustalanie” jest raczej w pewnym sensie ustawieniem domyślnym):

>>> f('23', base=10)
23

Tak więc, jak widać, jest to definely nie jest tak proste jak lambda s: int(s, base=2)! -)

Tak, to mogłoby wykrzywiać swoją lambda dać trochę tego - na przykład dla słowa kluczowego-nadrzędnych,

>>> f = lambda s, **k: int(s, **dict({'base': 2}, **k))

ale mam wielką nadzieję, że nawet najbardziej zagorzały lambdakochanek nie uważa tego horroru za bardziej czytelny niż partialwezwanie! -). Część „ustawianie atrybutów” jest jeszcze trudniejsza, ponieważ ograniczenie Pythona do „ciała jest pojedynczym wyrażeniem” lambda(plus fakt, że przypisanie nigdy nie może być częścią wyrażenia w Pythonie)… w rezultacie „fałszujesz przypisania w wyrażeniu” poprzez rozciągnięcie zrozumienia listy znacznie przekraczając jej granice projektowe ...:

>>> f = [f for f in (lambda f: int(s, base=2),)
           if setattr(f, 'keywords', {'base': 2}) is None][0]

Teraz połączyć przeciążania nazwanych argumentów plus ustawienie trzech atrybutów w jednej wypowiedzi, i powiedz mi, w jaki sposób czytelny , że będzie ...!

Alex Martelli
źródło
2
Tak, powiedziałbym, że dodatkowa funkcjonalność, o functools.partialktórej wspomniałeś, sprawia, że ​​jest lepsza niż lambda. Być może jest to temat innego postu, ale o co tak bardzo cię martwisz na poziomie projektowania lambda?
Nick Heiner,
11
@Rosarch, jak powiedziałem: po pierwsze, że ograniczenia (Python ostro odróżnia wyrażenia i wypowiedzi - nie wiele nie można zrobić, albo nie potrafi sensownie , w ramach jednego wyrazu, i to właśnie ciało lambda za to ); po drugie, to absolutnie dziwny cukier składniowy. Gdybym mógł cofnąć się w czasie i zmienić jedną rzecz w Pythonie, byłoby to absurdalne, bezsensowne, obrzydzenie defi lambdasłowa kluczowe: uczyń je oba function(wybór jednej nazwy JavaScript jest naprawdę słuszny), a przynajmniej 1/3 moich zastrzeżeń zniknie ! -). Jak powiedziałem, nie mam nic przeciwko lambda w Lisp ...! -)
Alex Martelli
1
@Alex Martelli, Dlaczego Guido ustanowił takie ograniczenie dla lambda: „ciało jest jednym wyrazem”? Ciało lambda C # może być czymkolwiek ważnym w ciele funkcji. Dlaczego Guido po prostu nie usunie ograniczenia dla python lambda?
Peter Long,
3
@PeterLong Mam nadzieję, że Guido może odpowiedzieć na twoje pytanie. Istotą tego jest to, że byłoby to zbyt skomplikowane i że i tak możesz użyć def. Nasz życzliwy przywódca przemówił!
new123456
5
@AlexMartelli DropBox miał interesujący wpływ na Guido - twitter.com/gvanrossum/status/391769557758521345
David,
83

Oto przykład pokazujący różnicę:

In [132]: sum = lambda x, y: x + y

In [133]: n = 5

In [134]: incr = lambda y: sum(n, y)

In [135]: incr2 = partial(sum, n)

In [136]: print incr(3), incr2(3)
8 8

In [137]: n = 9

In [138]: print incr(3), incr2(3)
12 8

Te posty Ivana Moore'a rozwijają „ograniczenia lambda” i zamknięcia w pythonie:

ars
źródło
1
Dobry przykład. Wydaje mi się, że to raczej „błąd” w stosunku do lambdy, ale rozumiem, że inni mogą się nie zgadzać. (Coś podobnego dzieje się z zamknięciami zdefiniowanymi w pętli, zaimplementowanymi w kilku językach programowania.)
ShreevatsaR
28
Rozwiązaniem tego „dylematu wczesnego i późnego wiązania” jest jawne użycie wczesnego wiązania, kiedy tego chcesz lambda y, n=n: .... Późne wiązanie (imion występujących tylko w ciele funkcji, a nie w jego deflub jego odpowiednik lambda) jest coś , ale to błąd, jak już pokazano na długości w długich odpowiedzi tak w przeszłości: ty wczesnego wiążą się wyraźnie, kiedy to, co chcesz, użyj domyślnego późnego wiązania, gdy tego właśnie chcesz, a to jest dokładnie właściwy wybór projektu, biorąc pod uwagę kontekst reszty projektu Pythona.
Alex Martelli,
1
@Alex Martelli: Tak, przepraszam. Po prostu nie przyzwyczajam się do późnego wiązania, być może dlatego, że kiedy definiuję funkcje, tak naprawdę definiuję coś na dobre, a nieoczekiwane niespodzianki powodują tylko bóle głowy. (Więcej gdy próbuję zrobić funkcjonalne rzeczy w Javascript niż w Pythonie, choć.) Rozumiem, że wielu ludzi wygodne z późnego wiązania, i że jest to zgodne z resztą konstrukcji Pythona. Nadal chciałbym przeczytać inne twoje długie odpowiedzi SO - linki? :-)
ShreevatsaR
3
Alex ma rację, to nie błąd. Ale to „gotcha”, która łapie wielu entuzjastów lambda. Po stronie „błędów” argumentu typu haskel / funkcjonalnego, patrz post Andreja Bauera: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ach tak, dziękuję za link do posta Andreja Bauera. Tak, skutki późnego wiązania są z pewnością czymś, co my, typy matematyczne (gorzej, na tle Haskell), znajdujemy rażąco nieoczekiwane i szokujące. :-) Nie jestem pewien, czy posunę się tak daleko do profesora Bauera i nazwałbym to błędem projektowym, ale programiści mają trudności z całkowitym przełączaniem się między jednym sposobem myślenia a drugim. (A może to po prostu moje niewystarczające doświadczenie w Pythonie).
ShreevatsaR
26

W najnowszych wersjach Pythona (> = 2.7), można , ale nie :picklepartiallambda

>>> pickle.dumps(partial(int))
'cfunctools\npartial\np0\n(c__builtin__\nint\np1\ntp2\nRp3\n(g1\n(tNNtp4\nb.'
>>> pickle.dumps(lambda x: int(x))
Traceback (most recent call last):
  File "<ipython-input-11-e32d5a050739>", line 1, in <module>
    pickle.dumps(lambda x: int(x))
  File "/usr/lib/python2.7/pickle.py", line 1374, in dumps
    Pickler(file, protocol).dump(obj)
  File "/usr/lib/python2.7/pickle.py", line 224, in dump
    self.save(obj)
  File "/usr/lib/python2.7/pickle.py", line 286, in save
    f(self, obj) # Call unbound method with explicit self
  File "/usr/lib/python2.7/pickle.py", line 748, in save_global
    (obj, module, name))
PicklingError: Can't pickle <function <lambda> at 0x1729aa0>: it's not found as __main__.<lambda>
Fred Foo
źródło
1
Niestety funkcje częściowe nie działają multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting
3
@wting Ten post pochodzi z 2010 roku. partialjest dostępny w Pythonie 2.7.
Fred Foo
22

Czy Funools jest w jakiś sposób bardziej wydajne?

Jako częściową odpowiedź na to pytanie postanowiłem przetestować wydajność. Oto mój przykład:

from functools import partial
import time, math

def make_lambda():
    x = 1.3
    return lambda: math.sin(x)

def make_partial():
    x = 1.3
    return partial(math.sin, x)

Iter = 10**7

start = time.clock()
for i in range(0, Iter):
    l = make_lambda()
stop = time.clock()
print('lambda creation time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    l()
stop = time.clock()
print('lambda execution time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    p = make_partial()
stop = time.clock()
print('partial creation time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    p()
stop = time.clock()
print('partial execution time {}'.format(stop - start))

w Pythonie 3.3 daje:

lambda creation time 3.1743163756961392
lambda execution time 3.040552701787919
partial creation time 3.514482823352731
partial execution time 1.7113973411608114

Co oznacza, że ​​częściowe potrzebuje trochę więcej czasu na stworzenie, ale znacznie mniej czasu na wykonanie. Może to być efekt wczesnego i późnego wiązania, które są omówione w odpowiedzi z ars .

Trilarion
źródło
3
Co ważniejsze, partialjest napisany w C, a nie w czystym Pythonie, co oznacza, że ​​może produkować bardziej wydajne wywołanie niż tworzenie funkcji wywołującej inną funkcję.
chepner
12

Oprócz wspomnianej dodatkowej funkcjonalności Alex, kolejną zaletą funools.partial jest szybkość. Dzięki częściowemu możesz uniknąć konstruowania (i niszczenia) kolejnej ramki stosu.

Ani funkcja generowana przez częściową, ani lambdas nie ma domyślnie ciągów dokumentów (chociaż można ustawić ciąg dokumentów dla dowolnych obiektów przez __doc__).

Więcej informacji można znaleźć na tym blogu: Aplikacja do funkcji częściowych w języku Python

Leonardo.Z
źródło
Jeśli sprawdziłeś przewagę prędkości, jakiej poprawy prędkości częściowej w stosunku do lambda można oczekiwać?
Trilarion
1
Kiedy mówisz, że docstring jest dziedziczony, do której wersji Pythona się odwołujesz? W Python 2.7.15 i Python 3.7.2 nie są dziedziczone. Co jest dobre, ponieważ oryginalny dokument nie musi być poprawny dla funkcji z częściowo zastosowanymi argumentami.
stycznia
W przypadku python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): „ atrybuty name i doc nie są tworzone automatycznie”. To samo dla 3. [5-7].
Yaroslav Nikitenko
W twoim linku jest błąd: log_info = częściowy (log_template, level = "info") - nie jest to możliwe, ponieważ poziom nie jest argumentem słowa kluczowego w przykładzie. Zarówno python 2, jak i 3 mówią: „TypeError: log_template () otrzymał wiele wartości dla argumentu„ poziom ””.
Yaroslav Nikitenko
W rzeczywistości ręcznie utworzyłem częściowy (f) i daje on pole doc jako „częściowe (func, * args, ** keywords) - nowa funkcja z częściowym zastosowaniem \ n podanych argumentów i słów kluczowych. \ N” (oba dla python 2 i 3).
Yaroslav Nikitenko
1

Rozumiem cel najszybciej w trzecim przykładzie.

Kiedy parsuję lambdas, oczekuję większej złożoności / osobliwości niż oferowana bezpośrednio przez bibliotekę standardową.

Zauważysz również, że trzeci przykład jest jedynym, który nie zależy od pełnego podpisu sum2; dzięki czemu jest nieco luźniej sprzężony.

Jon-Eric
źródło
1
Hm, jestem w rzeczywistości przeciwnego zdania, analizowanie functools.partialpołączenia zajęło mi dużo więcej czasu , podczas gdy lambda są oczywiste.
David Z