Python: ¿Por qué es necesario functools.partial?
functional-programming partial-application (6)
¿Qué funcionalidad ofrece functools.partial 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.
A la mayoría de las personas que están familiarizadas con los lenguajes de programación funcionales (las familias Lisp / Scheme en particular) parece que les gusta lambda
, digo "la mayoría", definitivamente no todas, porque Guido y yo ciertamente estamos entre los que están "familiarizados" (etc) aún piensa en lambda
como una anomalía de monstruosidad en Python ...
Él se arrepintió de haberlo aceptado en Python mientras que planeaba eliminarlo de Python 3, como uno de los "fallos técnicos de Python".
Lo apoyé completamente en eso. (Amo lambda
en Scheme ... mientras que sus limitaciones en Python , y la extraña manera en que no encaja con el resto del lenguaje, hacen que mi piel se arrastre).
No es así, sin embargo, para las hordas de amantes de lambda
, quienes protagonizaron una de las cosas más cercanas a una rebelión jamás vista en la historia de Python, hasta que Guido dio marcha atrás y decidió dejar la lambda
.
Varias posibles adiciones a functools
(para hacer funciones que devuelven constantes, identidad, etc.) no sucedieron (para evitar duplicar explícitamente más de la funcionalidad de lambda
), aunque partial
mantuvo, por supuesto (no es una duplicación total , ni es una monstruosidad )
Recuerde que lambda
cuerpo de lambda
se limita 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}
>>>
La función devuelta de functools.partial
está decorada con atributos útiles para la introspección: la función que está envolviendo y los argumentos posicionales y con nombre que fija. Además, los argumentos nombrados pueden anularse hacia atrás (la "fijación" es más bien, en cierto sentido, la configuración de los valores predeterminados):
>>> f(''23'', base=10)
23
Entonces, como ve, definitivamente no es tan simple como lambda s: int(s, base=2)
! -)
Sí, podría contorsionar su lambda para darle algo de esto, por ejemplo, para la palabra clave anulada,
>>> f = lambda s, **k: int(s, **dict({''base'': 2}, **k))
pero espero sinceramente que incluso la más ardiente lambda
-lover no considere este horror más legible que la llamada partial
! -). La parte de "ajuste de atributo" es aún más difícil, debido a la limitación de "cuerpo es una sola expresión" de la lambda
de Python (más el hecho de que la asignación nunca puede ser parte de una expresión de Python) ... terminas "falsificando asignaciones 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 combina la capacidad de overdability de los argumentos nombrados, más la configuración de tres atributos, en una sola expresión, ¡y dime qué tan legible será ...! -)
La aplicación parcial es genial. ¿Qué funcionalidad ofrece functools.partial
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
¿ functools
alguna manera más eficientes o legibles?
¿Son los functools de alguna manera más eficientes ...?
Como una 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 vinculación temprana y tardía que se discuten en la respuesta de ars .
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.
La función generada por parcial hereda la docstring de la función original mientras que lambdas no tiene docstrings por defecto (aunque puede establecer la cadena de documentación para cualquier objeto mediante __doc__
)
Puede encontrar más detalles en este blog: Aplicación de función parcial en Python
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:
En las últimas versiones de Python (> = 2.7), puede pickle
un 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>
Entiendo el intento más rápido en el tercer ejemplo.
Cuando analizo lambdas, estoy esperando más complejidad / rareza que la que ofrece la biblioteca estándar directamente.
Además, observará que el tercer ejemplo es el único que no depende de la firma completa de sum2
; lo que lo hace un poco más débilmente acoplado.