Python: Pourquoi functools.partial est-il nécessaire?

193

Une application partielle est cool. Quelle fonctionnalité functools.partialoffre que vous ne pouvez pas obtenir avec 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

Est en functoolsquelque sorte plus efficace ou lisible?

Nick Heiner
la source

Réponses:

266

Quelle fonctionnalité functools.partialoffre que vous ne pouvez pas obtenir avec lambdas?

Pas beaucoup en termes de fonctionnalités supplémentaires (mais, voir plus loin) - et la lisibilité est dans l'œil du spectateur.
La plupart des gens qui connaissent les langages de programmation fonctionnels (ceux des familles Lisp / Scheme en particulier) semblent aimer lambdatrès bien - je dis "la plupart", certainement pas tous, parce que Guido et moi sommes assurément parmi ceux qui "sont familiers" (etc.) ) mais pense lambdaà une anomalie horrible en Python ...
Il se repentait de ne jamais l'avoir acceptée en Python alors qu'il prévoyait de le supprimer de Python 3, comme l'un des "pépins de Python".
Je l'ai pleinement soutenu à cet égard. (J'adore lambda dans Scheme ... tandis que ses limitations en Python , et la façon étrange qu'il ne fait pas avec le reste du langage, font ramper ma peau).

Ce n'est pas le cas, cependant, pour les hordes d' lambdaamoureux - qui ont organisé l'une des choses les plus proches d'une rébellion jamais vue dans l'histoire de Python, jusqu'à ce que Guido revienne en arrière et décide de partir lambda.
Plusieurs ajouts possibles à functools(pour rendre les fonctions renvoyant des constantes, l'identité, etc) ne s'est pas produit (pour éviter de dupliquer explicitement plus de lambdafonctionnalités), bien partialqu'il soit bien sûr resté (ce n'est pas une duplication totale , ni une horreur).

N'oubliez pas que lambdale corps est limité à être une expression , il a donc des limites. Par exemple...:

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

functools.partialLa fonction renvoyée est décorée d'attributs utiles pour l'introspection - la fonction qu'elle encapsule et les arguments positionnels et nommés qu'elle y corrige. De plus, les arguments nommés peuvent être remplacés tout de suite (la «correction» est plutôt, dans un sens, la définition des valeurs par défaut):

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

Donc, comme vous le voyez, ce n'est certainement pas aussi simpliste que lambda s: int(s, base=2)! -)

Oui, vous pouvez déformer votre lambda pour vous en fournir une partie - par exemple, pour le mot clé prioritaire,

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

mais j'espèrelambda sincèrement que même l' amant le plus ardent ne considère pas cette horreur plus lisible que l' partialappel! -). La partie "définition d'attribut" est encore plus difficile, en raison de la limitation "corps une seule expression" de Python lambda(plus le fait que l'affectation ne peut jamais faire partie d'une expression Python) ... vous finissez par "simuler des affectations dans une expression" en étirant la compréhension de la liste bien au-delà de ses limites de conception ...:

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

Maintenant, combinez la substituabilité des arguments nommés, plus le réglage de trois attributs, en une seule expression, et dites-moi à quel point cela sera lisible ...!

Alex Martelli
la source
2
Oui, je dirais que la fonctionnalité supplémentaire functools.partialque vous avez mentionnée la rend supérieure à lambda. C'est peut-être le sujet d'un autre article, mais qu'est-ce qui vous dérange tant au niveau du design lambda?
Nick Heiner
11
@Rosarch, comme je l' ai dit: d' abord, il les limites (Python distingue nettement des expressions et des déclarations - il y a beaucoup que vous ne pouvez pas faire, ou ne peut pas sensiblement , dans une seule expression, et c'est ce que le corps d'un lambda est ); deuxièmement, son sucre de syntaxe absolument bizarre. Si je pouvais remonter dans le temps et changer une chose au sein de Python, ce serait l'absurde, le vide de sens, l'angoisse defet les lambdamots - clés: faites-les tous les deux function(un choix de nom Javascript a vraiment raison), et au moins 1/3 de mes objections disparaîtraient ! -). Comme je l'ai dit, je n'ai aucune objection à lambda à Lisp ...! -)
Alex Martelli
1
@Alex Martelli, Pourquoi Guido a-t-il fixé une telle limitation pour lambda: "body's a single expression"? Le corps lambda de C # pourrait être n'importe quoi de valide dans le corps d'une fonction. Pourquoi Guido ne supprime-t-il pas simplement la limitation pour python lambda?
Peter Long
3
@PeterLong J'espère que Guido pourra répondre à votre question. L'essentiel est que ce serait trop complexe et que vous pouvez en utiliser un de deftoute façon. Notre leader bienveillant s'est exprimé!
new123456
5
@AlexMartelli DropBox a eu une influence intéressante sur Guido - twitter.com/gvanrossum/status/391769557758521345
David
82

Eh bien, voici un exemple qui montre une différence:

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

Ces articles d'Ivan Moore développent les "limitations de lambda" et les fermetures en python:

ars
la source
1
Bon exemple. Pour moi, cela semble plus un "bug" avec lambda, en fait, mais je comprends que d'autres peuvent être en désaccord. (Quelque chose de similaire se produit avec les fermetures définies dans une boucle, comme implémentées dans plusieurs langages de programmation.)
ShreevatsaR
28
La solution à ce "dilemme de liaison précoce vs tardive" consiste à utiliser explicitement la liaison précoce, lorsque vous le souhaitez, par lambda y, n=n: .... La liaison tardive (des noms n'apparaissant que dans le corps d'une fonction, pas dans son corps defou équivalent lambda) est tout sauf un bogue, comme je l'ai montré longuement dans les réponses SO longues: vous vous liez tôt explicitement quand c'est ce que vous voulez, utilisez la valeur par défaut de liaison tardive lorsque c'est ce que vous voulez, et c'est exactement le bon choix de conception compte tenu du contexte du reste de la conception de Python.
Alex Martelli
1
@Alex Martelli: Oui, désolé. Je n'arrive pas à m'habituer correctement à la liaison tardive, peut-être parce que je pense qu'en définissant des fonctions que je définis réellement quelque chose pour de bon, et les surprises inattendues ne me causent que des maux de tête. (Plus quand j'essaye de faire des choses fonctionnelles en Javascript qu'en Python, cependant.) Je comprends que beaucoup de gens sont à l' aise avec la liaison tardive, et qu'elle est cohérente avec le reste de la conception de Python. Je voudrais quand même lire vos autres longues réponses SO - des liens? :-)
ShreevatsaR
3
Alex a raison, ce n'est pas un bug. Mais c'est un "gotcha" qui piège de nombreux amateurs de lambda. Pour le côté "bug" de l'argument d'un type haskel / fonctionnel, voir le message d'Andrej Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ah oui, merci pour le lien vers le post d'Andrej Bauer. Oui, les effets de la liaison tardive sont certainement quelque chose que nous, les types mathématiques (pire, avec un fond Haskell), continuons de trouver extrêmement inattendus et choquants. :-) Je ne suis pas sûr que j'irais jusqu'au professeur Bauer et j'appellerais cela une erreur de conception, mais il est difficile pour les programmeurs humains de basculer complètement entre une façon de penser et une autre. (Ou peut-être que c'est juste mon expérience Python insuffisante.)
ShreevatsaR
26

Dans les dernières versions de Python (> = 2.7), vous pouvez pickleun partial, mais pas 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
la source
1
Malheureusement, les fonctions partielles échouent multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting
3
@wting Ce message date de 2010. partialest pickleable en Python 2.7.
Fred Foo
22

Les functools sont-ils en quelque sorte plus efficaces ..?

En réponse à cela, j'ai décidé de tester les performances. Voici mon exemple:

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

sur Python 3.3, cela donne:

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

Ce qui signifie que le partiel a besoin d'un peu plus de temps pour la création mais considérablement moins de temps pour l'exécution. Cela peut bien être l'effet de la liaison précoce et tardive qui est discutée dans la réponse d' ars .

Trilarion
la source
3
Plus important encore, partialest écrit en C, plutôt qu'en Python pur, ce qui signifie qu'il peut produire un appelable plus efficace que de simplement créer une fonction qui appelle une autre fonction.
chepner
12

Outre les fonctionnalités supplémentaires mentionnées par Alex, un autre avantage de functools.partial est la vitesse. Avec partial, vous pouvez éviter de construire (et de détruire) un autre cadre de pile.

Ni la fonction générée par partial ni lambdas n'ont de docstrings par défaut (bien que vous puissiez définir la chaîne de doc pour tous les objets via __doc__ ).

Vous pouvez trouver plus de détails dans ce blog: Application de fonction partielle en Python

Leonardo.Z
la source
Si vous avez testé l'avantage de la vitesse, quelle amélioration de la vitesse partielle par rapport à lambda peut-on attendre?
Trilarion
1
Lorsque vous dites que la docstring est héritée, à quelle version de Python vous référez-vous? Dans Python 2.7.15 et Python 3.7.2, ils ne sont pas hérités. Ce qui est une bonne chose, car la docstring d'origine n'est pas nécessairement correcte pour la fonction avec des arguments partiellement appliqués.
jan
Pour python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): "le nom et les attributs doc ne sont pas créés automatiquement". Idem pour 3. [5-7].
Yaroslav Nikitenko
Il y a une erreur dans votre lien: log_info = partial (log_template, level = "info") - ce n'est pas possible car level n'est pas un argument mot-clé dans l'exemple. Les deux python 2 et 3 disent: "TypeError: log_template () a obtenu plusieurs valeurs pour l'argument 'level'".
Yaroslav Nikitenko
En fait, j'ai créé un partiel (f) à la main et il donne le champ doc comme 'partial (func, * args, ** mots-clés) - nouvelle fonction avec application partielle \ n des arguments et mots-clés donnés. \ N' (les deux pour python 2 et 3).
Yaroslav Nikitenko
1

Je comprends l'intention la plus rapide dans le troisième exemple.

Lorsque j'analyse des lambdas, je m'attends à plus de complexité / bizarrerie que celle offerte par la bibliothèque standard directement.

De plus, vous remarquerez que le troisième exemple est le seul qui ne dépend pas de la signature complète de sum2; le rendant ainsi légèrement plus lâche couplé.

Jon-Eric
la source
1
Hm, je suis en fait de la persuasion opposée, j'ai mis beaucoup plus de temps à analyser l' functools.partialappel, alors que les lambdas vont de soi.
David Z