Comment fonctools partial fait-il ce qu'il fait?


181

Je ne suis pas en mesure de comprendre comment fonctionne le partiel dans functools. J'ai le code suivant d' ici :

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

Maintenant dans la ligne

incr = lambda y : sum(1, y)

Je reçois que tout argument que je passe à incril sera passé comme yà lambdaqui retournera à sum(1, y)savoir 1 + y.

Je comprends que. Mais je n'ai pas compris cela incr2(4).

Comment le 4passe-t-il comme xune fonction partielle? Pour moi, 4devrait remplacer le sum2. Quelle est la relation entre xet 4?

Réponses:


218

En gros, partialfaites quelque chose comme ça (à part le support des arguments de mot-clé, etc.):

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*args)

    return wrapper

Ainsi, en appelant, partial(sum2, 4)vous créez une nouvelle fonction (un appelable, pour être précis) qui se comporte comme sum2, mais qui a un argument de position en moins. Cet argument manquant est toujours remplacé par 4, de sorte quepartial(sum2, 4)(2) == sum2(4, 2)

Quant à savoir pourquoi il est nécessaire, il existe une variété de cas. Juste pour un, supposons que vous deviez passer une fonction quelque part où elle devrait avoir 2 arguments:

class EventNotifier(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        ''' callback should accept two positional arguments, event and params '''
        self._listeners.append(callback)
        # ...

    def notify(self, event, *params):
        for f in self._listeners:
            f(event, params)

Mais une fonction que vous avez déjà a besoin d'accéder à un troisième contextobjet pour faire son travail:

def log_event(context, event, params):
    context.log_event("Something happened %s, %s", event, params)

Donc, il existe plusieurs solutions:

Un objet personnalisé:

class Listener(object):
   def __init__(self, context):
       self._context = context

   def __call__(self, event, params):
       self._context.log_event("Something happened %s, %s", event, params)


 notifier.add_listener(Listener(context))

Lambda:

log_listener = lambda event, params: log_event(context, event, params)
notifier.add_listener(log_listener)

Avec partiels:

context = get_context()  # whatever
notifier.add_listener(partial(log_event, context))

De ces trois, partialc'est le plus court et le plus rapide. (Pour un cas plus complexe, vous voudrez peut-être un objet personnalisé).


1
d'où avez-vous obtenu la extra_argsvariable
user1865341

2
extra_argsest quelque chose qui est passé par l'appelant partiel, dans l'exemple avec p = partial(func, 1); f(2, 3, 4)lui (2, 3, 4).
bereal

1
mais pourquoi nous ferions cela, tout cas d'utilisation spécial où quelque chose doit être fait par partie seulement et ne peut pas être fait avec autre chose
user1865341

@ user1865341 J'ai ajouté un exemple à la réponse.
bereal

avec votre exemple, quelle est la relation entre callbacketmy_callback
user1865341

92

partiels sont incroyablement utiles.

Par exemple, dans une séquence d'appels de fonction en "pipe-line" (dans laquelle la valeur retournée d'une fonction est l'argument passé à la suivante).

Parfois, une fonction dans un tel pipeline nécessite un seul argument , mais la fonction immédiatement en amont de celle-ci renvoie deux valeurs .

Dans ce scénario, functools.partial peut vous permettre de conserver ce pipeline de fonctions intact.

Voici un exemple spécifique et isolé: supposons que vous souhaitiez trier certaines données en fonction de la distance de chaque point de données par rapport à une cible:

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

Pour trier ces données en fonction de la distance de la cible, ce que vous voudriez bien sûr faire est ceci:

data.sort(key=euclid_dist)

mais vous ne pouvez pas - le paramètre clé de la méthode de tri n'accepte que les fonctions qui prennent un seul argument.

alors réécrivez euclid_distcomme une fonction prenant un seul paramètre:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist accepte désormais un seul argument,

>>> p_euclid_dist((3, 3))
  1.4142135623730951

vous pouvez maintenant trier vos données en passant la fonction partielle pour l'argument clé de la méthode de tri:

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

Ou par exemple, l'un des arguments de la fonction change dans une boucle externe mais est fixé lors de l'itération dans la boucle interne. En utilisant un partiel, vous n'avez pas à passer le paramètre supplémentaire lors de l'itération de la boucle interne, car la fonction modifiée (partielle) n'en a pas besoin.

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

créer une fonction partielle (en utilisant le mot-clé arg)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

vous pouvez également créer une fonction partielle avec un argument positionnel

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

mais cela lancera (par exemple, créer un partiel avec un argument mot-clé puis appeler en utilisant des arguments positionnels)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

un autre cas d'utilisation: écrire du code distribué en utilisant la multiprocessingbibliothèque de python . Un pool de processus est créé à l'aide de la méthode Pool:

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Pool a une méthode de carte, mais cela ne prend qu'un seul itérable, donc si vous avez besoin de passer une fonction avec une liste de paramètres plus longue, redéfinissez la fonction comme partielle, pour corriger tout sauf un:

>>> ppool.map(pfnx, [4, 6, 7, 8])

1
y a-t-il une utilisation pratique de cette fonction quelque part
user1865341

3
@ user1865341 a ajouté deux cas d'utilisation exemplaires à ma réponse
doug

À mon humble avis, c'est une meilleure réponse car elle évite les concepts non liés tels que les objets et les classes et se concentre sur les fonctions, c'est ce dont il s'agit.
akhan le

35

réponse courte, partialdonne des valeurs par défaut aux paramètres d'une fonction qui autrement n'auraient pas de valeurs par défaut.

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10

5
c'est à moitié vrai parce que nous pouvons remplacer les valeurs par défaut, nous pouvons même remplacer les paramètres de remplacement par les paramètres suivants partialet ainsi de suite
Azat Ibrakov

33

Les partiels peuvent être utilisés pour créer de nouvelles fonctions dérivées qui ont certains paramètres d'entrée pré-assignés

Pour voir une utilisation réelle des partiels, reportez-vous à ce très bon article de blog:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Un simple , mais l'exemple de débutant propre du blog, couvre la façon dont on peut utiliser partialsur re.searchpour rendre le code plus lisible. re.searchla signature de la méthode est:

search(pattern, string, flags=0) 

En appliquant, partialnous pouvons créer plusieurs versions de l'expression régulière searchen fonction de nos besoins, par exemple:

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

Maintenant is_spaced_apart et il is_grouped_togethery a deux nouvelles fonctions dérivées de re.searchqui ont l' patternargument appliqué (puisque patternc'est le premier argument dans la re.searchsignature de la méthode).

La signature de ces deux nouvelles fonctions (appelables) est:

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

Voici comment vous pouvez ensuite utiliser ces fonctions partielles sur du texte:

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

Vous pouvez consulter le lien ci - dessus pour obtenir une compréhension plus approfondie du sujet, car il couvre cet exemple spécifique et bien plus encore.


1
N'est-ce pas équivalent à is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Si tel est le cas, y a-t-il une garantie que l' partialidiome compile l'expression régulière pour une réutilisation plus rapide?
Aristide

10

À mon avis, c'est un moyen d'implémenter le curry en python.

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __name__ == "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

Le résultat est 3 et 4.


1

Il convient également de mentionner que lorsque la fonction partielle a passé une autre fonction où nous voulons "coder en dur" certains paramètres, cela devrait être le paramètre le plus à droite

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

mais si nous faisons de même, mais en changeant un paramètre à la place

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

il lancera une erreur, "TypeError: func () a plusieurs valeurs pour l'argument 'a'"


Hein? Vous faites le paramètre le plus à gauche comme ceci:prt=partial(func, 7)
DylanYoung

0

Cette réponse est plus un exemple de code. Toutes les réponses ci-dessus donnent de bonnes explications sur les raisons pour lesquelles on devrait utiliser partial. Je vais donner mes observations et cas d'utilisation sur partial.

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

La sortie du code ci-dessus doit être:

a:1,b:2,c:3
6

Notez que dans l'exemple ci-dessus, un nouvel appelable a été renvoyé qui prendra le paramètre (c) comme argument. Notez que c'est également le dernier argument de la fonction.

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

La sortie du code ci-dessus est également:

a:1,b:2,c:3
6

Notez que * a été utilisé pour décompresser les arguments non-mot-clé et l'appelable renvoyé en termes d'argument qu'il peut prendre est le même que ci-dessus.

Une autre observation est la suivante: L' exemple ci - dessous démontre que partial renvoie un appelable qui prendra le paramètre non déclaré (a) comme argument.

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

La sortie du code ci-dessus doit être:

a:20,b:10,c:2,d:3,e:4
39

De même,

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

Au-dessus des impressions de code

a:20,b:10,c:2,d:3,e:4
39

J'ai dû l'utiliser lorsque j'utilisais la Pool.map_asyncméthode du multiprocessingmodule. Vous ne pouvez passer qu'un seul argument à la fonction de travail, donc j'ai dû l'utiliser partialpour que ma fonction de travail ressemble à une fonction appelable avec un seul argument d'entrée, mais en réalité ma fonction de travail avait plusieurs arguments d'entrée.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.