Python: Warum ist functools.partial notwendig?

193

Teilanwendung ist cool. Welche Funktionalität functools.partialbietet Sie, die Sie nicht durch Lambdas bekommen können?

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

Ist functoolsirgendwie effizienter oder lesbar?

Nick Heiner
quelle

Antworten:

265

Welche Funktionalität functools.partialbietet Sie, die Sie nicht durch Lambdas bekommen können?

Nicht viel an zusätzlicher Funktionalität (siehe später) - und die Lesbarkeit liegt im Auge des Betrachters.
Die meisten Leute, die mit funktionalen Programmiersprachen vertraut sind (insbesondere die in den Lisp / Scheme-Familien), scheinen es gut zu mögen lambda- ich sage "die meisten", definitiv nicht alle, weil Guido und ich sicherlich zu denen gehören, die "vertraut sind" (etc. ) dennoch lambdaals Schandfleck-Anomalie in Python betrachten ...
Er bereute es, es jemals in Python akzeptiert zu haben, während er vorhatte, es aus Python 3 zu entfernen, als eine von "Pythons Pannen".
Ich habe ihn dabei voll unterstützt. (Ich liebe lambda in Scheme ... während seine Einschränkungen in Python und die seltsame Art und Weise, wie es einfach nicht funktioniert. mit dem Rest der Sprache meine Haut kriechen lassen).

Nicht so jedoch für die Horden von lambdaLiebhabern - der als einer der engstenen Dinge zu einem Aufstand inszeniert jemals in Python Geschichte gesehen, bis Guido Rückzieher und beschlossen , zu verlassen lambda. In
mehr möglichen Ergänzungen functools(um Funktionen Rückkehr Konstanten, Identität, etc) ist nicht aufgetreten (um zu vermeiden, dass mehr lambdaFunktionen explizit dupliziert werden ), ist partialaber natürlich geblieben (es ist keine vollständige Duplizierung, noch ist es ein Dorn im Auge).

Denken Sie daran, dass lambdader Körper nur ein Ausdruck sein kann , daher gibt es Einschränkungen. Beispielsweise...:

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

functools.partialDie zurückgegebene Funktion ist mit Attributen versehen, die für die Selbstbeobachtung nützlich sind - der Funktion, die sie umschließt, und den darin festgelegten positionellen und benannten Argumenten. Darüber hinaus können die genannten Argumente sofort wieder überschrieben werden (die "Korrektur" ist in gewissem Sinne eher die Einstellung von Standardeinstellungen):

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

Wie Sie sehen, ist es definitiv nicht so einfach wie lambda s: int(s, base=2)! -)

Ja, Sie könnten Ihr Lambda verzerren, um Ihnen etwas davon zu geben - z. B. für das Überschreiben von Schlüsselwörtern,

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

aber ich hoffe sehr , dass selbst der leidenschaftlichste lambdaLiebhaber diesen Horror nicht lesbarer findet als den partialAnruf! -). Der Teil "Attributeinstellung" ist noch schwieriger, da die Einschränkung "Python ist ein einzelner Ausdruck" von Python lambda(plus die Tatsache, dass die Zuweisung niemals Teil eines Python-Ausdrucks sein kann) ... Sie am Ende "Zuweisungen innerhalb eines Ausdrucks vortäuschen". durch Ausdehnung des Listenverständnisses weit über seine Designgrenzen hinaus ...:

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

Kombinieren Sie nun die Überschreibbarkeit der benannten Argumente sowie die Einstellung von drei Attributen in einem einzigen Ausdruck und sagen Sie mir, wie lesbar das sein wird ...!

Alex Martelli
quelle
2
Ja, ich würde sagen, dass die zusätzliche Funktionalität functools.partial, die Sie erwähnt haben, Lambda überlegen macht. Vielleicht ist dies das Thema eines anderen Beitrags, aber was stört Sie auf Designebene so sehr lambda?
Nick Heiner
11
@Rosarch, wie gesagt: Erstens sind es Einschränkungen (Python unterscheidet Ausdrücke und Aussagen scharf - es gibt viel, was Sie in einem einzelnen Ausdruck nicht oder nicht sinnvoll tun können, und genau das ist der Körper eines Lambdas ); zweitens seine absolut seltsame Syntax Zucker. Wenn ich in der Zeit zurückgehen und eine Sache in Python ändern könnte, wäre es das Absurde, Bedeutungslose, Schandfleckige defund lambdaSchlüsselwort: Machen Sie beide function(eine Namenswahl, die Javascript wirklich richtig gemacht hat), und mindestens 1/3 meiner Einwände würde verschwinden ! -). Wie gesagt, ich habe keine Einwände gegen Lambda in Lisp ...! -)
Alex Martelli
1
@ Alex Martelli, warum hat Guido eine solche Einschränkung für Lambda festgelegt: "Körper ist ein einziger Ausdruck"? Der Lambda-Körper von C # kann alles sein, was im Körper einer Funktion gültig ist. Warum hebt Guido nicht einfach die Beschränkung für Python Lambda auf?
Peter Long
3
@ PeterLong Hoffentlich kann Guido Ihre Frage beantworten. Der Kern davon ist, dass es zu komplex wäre und dass Sie defsowieso eine verwenden können. Unser gütiger Führer hat gesprochen!
new123456
5
@AlexMartelli DropBox hat einen interessanten Einfluss auf Guido gehabt - twitter.com/gvanrossum/status/391769557758521345
David
82

Hier ist ein Beispiel, das einen Unterschied zeigt:

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

Diese Beiträge von Ivan Moore erweitern die "Einschränkungen von Lambda" und Verschlüsse in Python:

ars
quelle
1
Gutes Beispiel. Für mich scheint dies eigentlich eher ein "Bug" mit Lambda zu sein, aber ich verstehe, dass andere anderer Meinung sein können. (Ähnliches passiert mit Schließungen, die innerhalb einer Schleife definiert sind und in mehreren Programmiersprachen implementiert sind.)
ShreevatsaR
28
Die Lösung für dieses "frühe oder späte Bindungsdilemma" besteht darin, die frühe Bindung explizit zu verwenden, wenn Sie dies möchten lambda y, n=n: .... Eine späte Bindung (von Namen, die nur im Körper einer Funktion erscheinen, nicht in ihrem defoder einem gleichwertigen lambda) ist alles andere als ein Fehler, wie ich in langen SO-Antworten in der Vergangenheit ausführlich gezeigt habe: Sie binden explizit früh, wenn Sie dies wünschen. verwenden Sie die späte Bindung standardmäßig , wenn das ist , was Sie wollen, und das ist genau die richtige Design Wahl des Kontext des Restes von Python-Design gegeben.
Alex Martelli
1
@ Alex Martelli: Ja, tut mir leid. Ich gewöhne mich einfach nicht richtig an das späte Binden, vielleicht weil ich beim Definieren von Funktionen denke, dass ich tatsächlich etwas für immer definiere, und die unerwarteten Überraschungen verursachen mir nur Kopfschmerzen. (Mehr wenn ich versuche , als in Python funktionale Dinge in Javascript zu tun, though.) Ich verstehe , dass viele Menschen sind bequem mit dem späten Bindung, und dass es im Einklang mit dem Rest des Pythons-Designs. Ich würde trotzdem gerne Ihre anderen langen SO-Antworten lesen - Links? :-)
ShreevatsaR
3
Alex hat recht, es ist kein Fehler. Aber es ist ein "Gotcha", das viele Lambda-Enthusiasten fängt. Für die "Bug" -Seite des Arguments von einem Haskel / Funktionstyp siehe Andrej Bauers Beitrag: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ah ja, danke für den Link zu Andrej Bauers Beitrag. Ja, die Auswirkungen der späten Bindung sind sicherlich etwas, das wir Mathematiktypen (schlimmer noch, mit einem Haskell-Hintergrund) immer wieder als grob unerwartet und schockierend empfinden. :-) Ich bin mir nicht sicher, ob ich bis zu Prof. Bauer gehen und es als Designfehler bezeichnen würde, aber es ist für menschliche Programmierer schwierig, vollständig zwischen einer Denkweise und einer anderen zu wechseln. (Oder vielleicht ist dies nur meine unzureichende Python-Erfahrung.)
ShreevatsaR
26

In den neuesten Versionen von Python (> = 2.7) können Sie picklea partial, aber nicht 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>
Fred Foo
quelle
1
Leider können Teilfunktionen nicht berücksichtigt werden multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting
3
@wting Dieser Beitrag stammt aus dem Jahr 2010. partialEr kann in Python 2.7 ausgewählt werden.
Fred Foo
22

Ist functools irgendwie effizienter ..?

Als teilweise Antwort darauf habe ich beschlossen, die Leistung zu testen. Hier ist mein Beispiel:

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

unter Python 3.3 gibt es:

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

Dies bedeutet, dass Partial etwas mehr Zeit für die Erstellung benötigt, aber erheblich weniger Zeit für die Ausführung. Dies kann durchaus der Effekt der frühen und späten Bindung sein, die in der Antwort von ars diskutiert werden .

Trilarion
quelle
3
Noch wichtiger partialist, dass es in C geschrieben ist und nicht in reinem Python, was bedeutet, dass es einen effizienteren Aufruf erzeugen kann, als einfach eine Funktion zu erstellen, die eine andere Funktion aufruft.
Chepner
12

Neben der von Alex erwähnten zusätzlichen Funktionalität ist ein weiterer Vorteil von functools.partial die Geschwindigkeit. Mit partiell können Sie vermeiden, einen anderen Stapelrahmen zu konstruieren (und zu zerstören).

Weder die von partiellen noch von Lambdas generierte Funktion verfügt standardmäßig über Dokumentzeichenfolgen (obwohl Sie die Dokumentzeichenfolge für alle Objekte über festlegen können __doc__).

Weitere Details finden Sie in diesem Blog: Partial Function Application in Python

Leonardo.Z
quelle
Wenn Sie den Geschwindigkeitsvorteil getestet haben, welche Geschwindigkeitsverbesserung von Partial gegenüber Lambda ist zu erwarten?
Trilarion
1
Wenn Sie sagen, dass der Docstring vererbt wird, auf welche Python-Version beziehen Sie sich? In Python 2.7.15 und Python 3.7.2 werden sie nicht vererbt. Das ist eine gute Sache, da die ursprüngliche Dokumentzeichenfolge für die Funktion mit teilweise angewendeten Argumenten nicht unbedingt korrekt ist.
Januar
Für Python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): "Der Name und die Dokumentattribute werden nicht automatisch erstellt". Gleiches gilt für 3. [5-7].
Jaroslaw Nikitenko
Ihr Link enthält einen Fehler: log_info = partiell (log_template, level = "info") - Dies ist nicht möglich, da level im Beispiel kein Schlüsselwortargument ist. Sowohl Python 2 als auch Python 3 sagen: "TypeError: log_template () hat mehrere Werte für das Argument 'level' erhalten".
Jaroslaw Nikitenko
Tatsächlich habe ich ein Teil (f) von Hand erstellt und es gibt das Dokumentfeld als 'Teil (func, * args, ** Schlüsselwörter) - neue Funktion mit teilweiser Anwendung \ n der angegebenen Argumente und Schlüsselwörter. \ N' (beide) für Python 2 und 3).
Jaroslaw Nikitenko
1

Ich verstehe die Absicht im dritten Beispiel am schnellsten.

Wenn ich Lambdas analysiere, erwarte ich mehr Komplexität / Seltsamkeit als von der Standardbibliothek direkt angeboten.

Außerdem werden Sie feststellen, dass das dritte Beispiel das einzige ist, das nicht von der vollständigen Signatur von abhängt sum2. Dadurch wird es etwas lockerer gekoppelt.

Jon-Eric
quelle
1
Hm, ich bin eigentlich der gegenteiligen Überzeugung, ich habe viel länger gebraucht, um den functools.partialAnruf zu analysieren , während die Lambdas selbstverständlich sind.
David Z