Python: ¿Por qué es necesario functools.partial?

193

La aplicación parcial es genial. ¿Qué funcionalidad functools.partialofrece que no puedes obtener a través de 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

¿Es de functoolsalguna manera más eficiente o legible?

Nick Heiner
fuente

Respuestas:

266

¿Qué funcionalidad functools.partialofrece que no puedes obtener a través de lambdas?

No mucho en términos de funcionalidad adicional (pero, ver más adelante), y la legibilidad está en el ojo del espectador.
Parece que a la mayoría de las personas que están familiarizadas con los lenguajes de programación funcionales (en particular las de las familias Lisp / Scheme) les gusta lambdabien: digo "la mayoría", definitivamente no todas, porque Guido y yo seguramente estamos entre los "familiarizados" (etc. ) Sin embargo, piensa que lambdaes una anomalía monstruosa en Python ...
Se arrepintió de haberlo aceptado en Python, mientras que planeaba eliminarlo de Python 3, como uno de los "problemas técnicos de Python".
Lo apoyé completamente en eso. (Me encanta lambda en Scheme ... mientras que sus limitaciones en Python , y la forma extraña en que simplemente no con el resto del lenguaje, haz que mi piel se arrastre).

Sin embargo, no es así para las hordas de lambdaamantes, que organizaron una de las cosas más cercanas a una rebelión jamás vista en la historia de Python, hasta que Guido retrocedió y decidió irse lambda.
Varias adiciones posibles a functools(para hacer que las funciones regresen constantes, identidad, etc) no sucedió (para evitar duplicar explícitamente más lambdafuncionalidades), aunque partialpor supuesto permaneció (no es una duplicación total , ni es una monstruosidad).

Recuerde que lambdael cuerpo está limitado a ser una expresión , por lo que tiene limitaciones. Por ejemplo...:

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

functools.partialLa función devuelta está decorada con atributos útiles para la introspección: la función que está ajustando y qué argumentos posicionales y nombrados fija allí. Además, los argumentos nombrados pueden anularse de inmediato (la "fijación" es, en cierto sentido, la configuración de los valores predeterminados):

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

Entonces, como ves, ¡ definitivamente no es tan simplista como lambda s: int(s, base=2)! -)

Sí, podría contorsionar su lambda para darle algo de esto, por ejemplo, para la anulación de palabras clave,

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

¡Pero espero que incluso el lambdaamante más ardiente no considere este horror más legible que la partialllamada! -). La parte de "configuración de atributos" es aún más difícil, debido a la limitación de "cuerpo de una sola expresión" de Python lambda(más el hecho de que la asignación nunca puede ser parte de una expresión de Python) ... terminas "falsificando tareas dentro de una expresión" estirando la comprensión de la lista mucho más allá de sus límites de diseño ...:

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

¡Ahora combine la sobreescritura de argumentos nombrados, más la configuración de tres atributos, en una sola expresión, y dígame cuán legible será eso ...!

Alex Martelli
fuente
2
Sí, diría que la funcionalidad adicional de lo functools.partialque mencionaste lo hace superior a lambda. Tal vez este es el tema de otra publicación, pero ¿qué es lo que te molesta tanto a nivel de diseño lambda?
Nick Heiner
11
@Rosarch, como ya he dicho: en primer lugar, las limitaciones (Python distingue claramente las expresiones y declaraciones - no hay mucho que no se puede hacer, o no puede hacer con sensatez , dentro de una sola expresión, y eso es lo que el cuerpo de una lambda es ); segundo, su sintaxis de azúcar absolutamente extraña. Si pudiera retroceder en el tiempo y cambiar una cosa dentro de Python, sería absurdo, sin sentido, monstruoso defy lambdapalabras clave: hacerlos a ambos function(una opción de nombre Javascript realmente correcto), y al menos 1/3 de mis objeciones desaparecerían ! -). Como dije, no tengo ninguna objeción a lambda en Lisp ...! -)
Alex Martelli
1
@Alex Martelli, ¿por qué Guido estableció tal limitación para lambda: "el cuerpo es una sola expresión"? El cuerpo lambda de C # podría ser cualquier cosa válida en el cuerpo de una función. ¿Por qué Guido no solo elimina la limitación para python lambda?
Peter Long
3
@PeterLong Esperemos que Guido pueda responder tu pregunta. La esencia de esto es que sería demasiado complejo y que puedes usar un de deftodos modos. ¡Nuestro benevolente líder ha hablado!
nuevo123456
55
@AlexMartelli DropBox ha tenido una influencia interesante en Guido - twitter.com/gvanrossum/status/391769557758521345
David
82

Bueno, aquí hay un ejemplo que muestra una diferencia:

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

Estas publicaciones de Ivan Moore amplían las "limitaciones de lambda" y los cierres en python:

ars
fuente
1
Buen ejemplo. Para mí, esto parece más un "error" con lambda, en realidad, pero entiendo que otros pueden estar en desacuerdo. (Algo similar sucede con los cierres definidos dentro de un bucle, tal como se implementa en varios lenguajes de programación.)
ShreevatsaR
28
La solución a este "dilema de unión temprana versus tardía" es usar explícitamente la unión temprana, cuando lo desee, por lambda y, n=n: .... La vinculación tardía (de nombres que aparecen solo en el cuerpo de una función, no en su defequivalente o equivalente lambda) no es más que un error, como he mostrado extensamente en respuestas SO largas en el pasado: se vincula explícitamente cuando eso es lo que quiere, use el valor predeterminado de enlace tardío cuando eso es lo que desea, y esa es exactamente la elección de diseño correcta dado el contexto del resto del diseño de Python.
Alex Martelli
1
@Alex Martelli: Sí, lo siento. Simplemente no me acostumbro al enlace tardío correctamente, tal vez porque creo que al definir funciones realmente estoy definiendo algo para siempre, y las sorpresas inesperadas solo me causan dolores de cabeza. (Sin embargo, más cuando trato de hacer cosas funcionales en Javascript que en Python). Entiendo que muchas personas se sienten cómodas con el enlace tardío, y que es consistente con el resto del diseño de Python. Sin embargo, todavía me gustaría leer sus otras respuestas largas de SO: ¿enlaces? :-)
ShreevatsaR
3
Alex tiene razón, no es un error. Pero es una "trampa" que atrapa a muchos entusiastas de la lambda. Para el lado del "error" del argumento de un tipo haskel / funcional, vea la publicación de Andrej Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ah sí, gracias por el enlace a la publicación de Andrej Bauer. Sí, los efectos de la unión tardía son ciertamente algo que nosotros, los matemáticos (peor, con antecedentes de Haskell) seguimos encontrando extremadamente inesperados e impactantes. :-) No estoy seguro de ir tan lejos como el Prof. Bauer y llamarlo un error de diseño, pero es difícil para los programadores humanos cambiar completamente entre una forma de pensar y otra. (O tal vez esta sea solo mi experiencia insuficiente en Python.)
ShreevatsaR
26

En las últimas versiones de Python (> = 2.7), puede pickleun partial, pero no un 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
fuente
1
Desafortunadamente, las funciones parciales no se pueden encurtir multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting
3
@wting Esa publicación es de 2010. partialse puede elegir en Python 2.7.
Fred Foo
22

¿Functools es de alguna manera más eficiente ...?

Como respuesta parcial a esto, decidí probar el rendimiento. Aquí está mi ejemplo:

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

en Python 3.3 da:

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

Lo que significa que parcial necesita un poco más de tiempo para la creación pero considerablemente menos tiempo para la ejecución. Este bien puede ser el efecto de la unión temprana y tardía que se discute en la respuesta de ars .

Trilarion
fuente
3
Más importante aún, partialestá escrito en C, en lugar de Python puro, lo que significa que puede producir un llamado más eficiente que simplemente crear una función que llame a otra función.
chepner
12

Además de la funcionalidad adicional que Alex mencionó, otra ventaja de functools.partial es la velocidad. Con parcial, puede evitar construir (y destruir) otro marco de pila.

Ni la función generada por parcial ni lambdas tienen cadenas de documentos por defecto (aunque puede establecer la cadena de documentos para cualquier objeto a través de __doc__).

Puede encontrar más detalles en este blog: Aplicación de función parcial en Python

Leonardo.Z
fuente
Si ha probado la ventaja de velocidad, ¿qué mejora de velocidad de parcial sobre lambda se puede esperar?
Trilarion
1
Cuando dice que la cadena de documentación es heredada, ¿a qué versión de Python se refiere? En Python 2.7.15 y Python 3.7.2 no se heredan. Lo cual es bueno, porque la cadena de documentación original no es necesariamente correcta para la función con argumentos parcialmente aplicados.
enero
Para python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): "el nombre y los atributos del documento no se crean automáticamente". Lo mismo para 3. [5-7].
Yaroslav Nikitenko
Hay un error en su enlace: log_info = partial (log_template, level = "info"): no es posible porque el nivel no es un argumento de palabra clave en el ejemplo. Python 2 y 3 dicen: "TypeError: log_template () obtuvo múltiples valores para el argumento 'nivel'".
Yaroslav Nikitenko
De hecho, creé un parcial (f) a mano y da el campo doc como 'parcial (func, * args, ** palabras clave): nueva función con aplicación parcial \ n de los argumentos y palabras clave dados. \ N' (ambos para python 2 y 3).
Yaroslav Nikitenko
1

Entiendo la intención más rápida en el tercer ejemplo.

Cuando analizo lambdas, espero más complejidad / rareza que la que ofrece la biblioteca estándar directamente.

Además, notará que el tercer ejemplo es el único que no depende de la firma completa de sum2; haciéndolo un poco más flojo.

Jon-Eric
fuente
1
Hm, en realidad soy de la persuasión opuesta, me tomó mucho más tiempo analizar la functools.partialllamada, mientras que las lambdas son evidentes.
David Z