Python: Perché è necessario functools.partial?


193

L'applicazione parziale è fantastica. Quale funzionalità functools.partialoffre che non è possibile superare 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

È in functoolsqualche modo più efficiente o leggibile?

Risposte:


266

Quale funzionalità functools.partialoffre che non è possibile superare lambdas?

Non molto in termini di funzionalità extra (ma, vedi più avanti) - e la leggibilità è negli occhi di chi guarda.
La maggior parte delle persone che hanno familiarità con i linguaggi di programmazione funzionale (in particolare quelli delle famiglie Lisp / Scheme) sembrano lambdaproprio bene - dico "la maggior parte", sicuramente non tutti, perché Guido ed io siamo sicuramente tra quelli "familiari" (ecc. ) eppure lambdaconsidera un'anomalia del pugno nell'occhio in Python ...
Si pentì di non averlo mai accettato in Python mentre aveva pianificato di rimuoverlo da Python 3, come uno dei "difetti di Python".
L'ho pienamente supportato in questo. (Adoro lambda in Scheme ... mentre i suoi limiti in Python e il modo strano in cui non funziona con il resto della lingua, strisciare la pelle).

Non è così, tuttavia, per le orde di lambdainnamorati, che hanno messo in scena una delle cose più vicine a una ribellione mai vista nella storia di Python, fino a quando Guido non ha fatto un passo indietro e ha deciso di abbandonare lambda.
Diverse possibili aggiunte a functools(per rendere le funzioni che restituiscono costanti, identità, ecc.) non è accaduto (per evitare di duplicare esplicitamente più lambdafunzionalità), anche se partialovviamente è rimasto (non è una duplicazione totale , né è un pugno nell'occhio).

Ricorda che lambdail corpo è limitato per essere un'espressione , quindi ha dei limiti. Per esempio...:

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

functools.partialLa funzione restituita è decorata con attributi utili per l'introspezione - la funzione che sta avvolgendo, e quali argomenti posizionali e nominati correggono in essa. Inoltre, gli argomenti nominati possono essere sovrascritti (il "fissaggio" è piuttosto, in un certo senso, l'impostazione delle impostazioni predefinite):

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

Quindi, come vedi, non è sicuramente così semplice lambda s: int(s, base=2)! -)

Sì, si potrebbe contorcere il tuo lambda per darvi alcune di queste - ad esempio, per la parola chiave-override,

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

ma spero vivamente che anche il più ardente lambdaamante non consideri questo orrore più leggibile della partialchiamata! -). La parte "impostazione dell'attributo" è ancora più difficile, a causa del limite "Python di una singola espressione" lambda(oltre al fatto che l'assegnazione non può mai far parte di un'espressione Python) ... si finisce per "falsificare le assegnazioni all'interno di un'espressione" allungando la comprensione della lista ben oltre i suoi limiti di progettazione ...:

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

Ora unire l'overridability-argomenti con nome, oltre alla cornice di tre attributi, in una singola espressione, e mi dica quanto sia leggibile , che sta per essere ...!


2
Sì, direi che la funzionalità extra di functools.partialcui hai parlato la rende superiore a lambda. Forse questo è l'argomento di un altro post, ma cos'è a livello di design che ti preoccupa così tanto lambda?
Nick Heiner,

11
@Rosarch, come ho detto: in primo luogo, limitazioni (Python distingue nettamente le espressioni e le dichiarazioni - c'è molto non si può fare, o non può fare sensibilmente , all'interno di una singola espressione, e questo è ciò che il corpo di un lambda è ); secondo, il suo zucchero di sintassi assolutamente strano. Se potessi tornare indietro nel tempo e cambiare una cosa in Python, sarebbe l'assurdo, insignificante, pugno nell'occhio defe lambdaparole chiave: renderli entrambi function(una scelta di nome Javascript ha funzionato davvero bene) e almeno 1/3 delle mie obiezioni svanirebbero ! -). Come ho detto, non ho alcuna obiezione a Lambda a Lisp ...! -)
Alex Martelli,

1
@Alex Martelli, Perché Guido ha fissato una tale limitazione per lambda: "il corpo è una singola espressione"? Il corpo lambda di C # potrebbe essere qualcosa di valido nel corpo di una funzione. Perché Guido non rimuove semplicemente la limitazione per Python Lambda?
Peter Long,

3
@PeterLong Speriamo che Guido possa rispondere alla tua domanda. L'essenza di ciò è che sarebbe troppo complesso e che puoi defcomunque usarlo . Il nostro leader benevolo ha parlato!
nuovo123456

5
@AlexMartelli DropBox ha avuto un'influenza interessante su Guido - twitter.com/gvanrossum/status/391769557758521345
David

83

Bene, ecco un esempio che mostra una differenza:

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

Questi post di Ivan Moore ampliano le "limitazioni di lambda" e le chiusure in pitone:


1
Buon esempio. Per me, questo sembra più un "bug" con lambda, in realtà, ma capisco che altri potrebbero non essere d'accordo. (Qualcosa di simile accade con le chiusure definite all'interno di un ciclo, come implementato in diversi linguaggi di programmazione.)
ShreevatsaR

28
La soluzione a questo "dilemma di associazione anticipata o tardiva" è di usare esplicitamente l'associazione anticipata, quando lo si desidera, entro lambda y, n=n: .... L'associazione tardiva (di nomi che appaiono solo nel corpo di una funzione, non nella sua defo equivalente lambda) è tutt'altro che un bug, come ho mostrato a lungo in lunghe risposte SO in passato: ti leghi in modo esplicito quando è quello che vuoi, utilizzare l'impostazione predefinita late-binding quando che è ciò che si vuole, e questo è esattamente la scelta modello giusto dato il contesto del resto del disegno di Python.
Alex Martelli,

1
@Alex Martelli: Sì, scusa. Non riesco ad abituarmi correttamente al late late binding, forse perché, quando definisco le funzioni, sto effettivamente definendo qualcosa per sempre, e le sorprese inaspettate mi causano solo mal di testa. (Più quando provo a fare cose funzionali in Javascript che in Python, però.) Capisco che molte persone si sentono a proprio agio con l'associazione tardiva e che è coerente con il resto del design di Python. Mi piacerebbe comunque leggere le tue altre lunghe risposte SO, comunque - link? :-)
ShreevatsaR

3
Alex ha ragione, non è un bug. Ma è un "gotcha" che intrappola molti appassionati di lambda. Per il lato "bug" dell'argomento da un tipo haskel / funzionale, vedi il post di Andrej Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars

@ars: Ah sì, grazie per il link al post di Andrej Bauer. Sì, gli effetti dell'associazione tardiva sono certamente qualcosa che noi matematici (peggio, con un background di Haskell) continuiamo a trovare gravemente inaspettato e scioccante. :-) Non sono sicuro di andare fino al Prof. Bauer e di definirlo un errore di progettazione, ma è difficile per i programmatori umani passare completamente da un modo di pensare all'altro. (O forse questa è solo la mia insufficiente esperienza di Python.)
ShreevatsaR

26

Nelle ultime versioni di Python (> = 2.7), puoi picklea partial, ma non a lambda:

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

1
Sfortunatamente le funzioni parziali non riescono a decifrare multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting

3
@wting Quel post è del 2010. partialè selezionabile in Python 2.7.
Fred Foo,

23

Le funzioni sono in qualche modo più efficienti ..?

In parte come risposta a questo ho deciso di testare le prestazioni. Ecco il mio esempio:

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

su Python 3.3 fornisce:

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

Ciò significa che il parziale richiede un po 'più di tempo per la creazione, ma considerevolmente meno tempo per l'esecuzione. Questo può benissimo essere l'effetto dell'associazione precoce e tardiva di cui si discute nella risposta di ars .


3
Ancora più importante, partialè scritto in C, piuttosto che in puro Python, il che significa che può produrre un callable più efficiente della semplice creazione di una funzione che chiama un'altra funzione.
Chepner,

12

Oltre alla funzionalità extra menzionata da Alex, un altro vantaggio di functools.partial è la velocità. Con il parziale puoi evitare di costruire (e distruggere) un altro stack frame.

Né la funzione generata da partial né lambdas hanno dotstring per impostazione predefinita (sebbene sia possibile impostare la stringa doc per qualsiasi oggetto tramite __doc__).

Puoi trovare maggiori dettagli in questo blog: Applicazione di funzioni parziali in Python


Se hai testato il vantaggio della velocità, quale miglioramento della velocità parziale rispetto a lambda è prevedibile?
Trilarion

1
Quando dici che il docstring è ereditato, a quale versione di Python ti riferisci? In Python 2.7.15 e Python 3.7.2 non sono ereditati. Il che è positivo, poiché la documentazione originale non è necessariamente corretta per la funzione con argomenti parzialmente applicati.
gennaio

Per python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): "il nome e gli attributi doc non vengono creati automaticamente". Lo stesso per 3. [5-7].
Yaroslav Nikitenko,

C'è un errore nel tuo link: log_info = partial (log_template, level = "info") - non è possibile perché level non è un argomento di parola chiave nell'esempio. Sia python 2 che 3 dicono: "TypeError: log_template () ha ottenuto più valori per l'argomento 'livello'".
Yaroslav Nikitenko,

In effetti, ho creato una parziale (f) a mano e dà il campo doc come 'parziale (parole chiave, func, * args, **) - nuova funzione con applicazione parziale \ n degli argomenti e delle parole chiave dati. \ N' (entrambi per Python 2 e 3).
Yaroslav Nikitenko,

1

Comprendo l'intento più rapidamente nel terzo esempio.

Quando analizzo lambdas, mi aspetto più complessità / stranezza rispetto a quanto offerto direttamente dalla libreria standard.

Inoltre, noterai che il terzo esempio è l'unico che non dipende dalla firma completa di sum2; rendendolo quindi leggermente più allentato.


1
Hm, sono in realtà della persuasione opposta, ho impiegato molto più tempo per analizzare la functools.partialchiamata, mentre le lambda sono evidenti.
David Z,
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.