web-dev-qa-db-fra.com

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

Je n'arrive pas à comprendre comment fonctionne le partiel dans functools. J'ai le code suivant de 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 file

incr = lambda y : sum(1, y)

Je comprends que, quel que soit l'argument que je passe à incr, il sera passé sous la forme y à lambda qui retournera sum(1, y) c'est-à-dire 1 + y.

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

Comment le 4 est-il passé sous la forme x dans une fonction partielle? Pour moi, 4 devrait remplacer le sum2. Quelle est la relation entre x et 4?

146
user1865341

En gros, partial fait quelque chose comme ceci (mis à part le support des mots clés, etc.):

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

    return wrapper

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

Pour ce qui est de la nécessité, 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à besoin d’accéder à un troisième objet context pour faire son travail:

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

Donc, il y a 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, partial est le plus court et le plus rapide. (Pour un cas plus complexe, vous voudrez peut-être un objet personnalisé).

177
bereal

partiels sont incroyablement utiles.

Par exemple, dans une séquence d'appels de fonctions "alignés sur les tuyaux" (dans laquelle la valeur renvoyée par une fonction est l'argument transmis à la suivante).

Parfois, une fonction dans un tel pipeline nécessite un argument unique, 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 par rapport à la cible, ce que vous voudriez bien sûr faire est la suivante:

data.sort(key=euclid_dist)

mais vous ne pouvez pas - le paramètre key de la méthode sort ​​accepte uniquement les fonctions qui prennent un argument single.

donc réécrivez euclid_dist comme une fonction prenant un paramètre single:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist accepte maintenant un seul argument,

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

alors maintenant, vous pouvez 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 fixe lors de l'itération dans la boucle interne. En utilisant un partiel, vous n'avez pas besoin de transmettre le paramètre supplémentaire lors de l'itération de la boucle interne, car la fonction modifiée (partielle) ne l'exige pas.

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

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

mais cela jettera (par exemple, créer un argument partiel avec 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 bibliothèque multiprocessing 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 map, mais il ne faut qu'une seule itérative, donc si vous devez transmettre une fonction avec une liste de paramètres plus longue, redéfinissez la fonction en tant que partielle, pour corriger tout le monde sauf un:

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

Les partiels peuvent être utilisés pour créer de nouvelles fonctions dérivées ayant certains paramètres d'entrée prédéfinis.

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

L'exemple du débutant, simple mais soigné, du blog, explique comment utiliser partial sur re.search pour rendre le code plus lisible. La signature de la méthode re.search est:

search(pattern, string, flags=0) 

En appliquant partial, nous pouvons créer plusieurs versions de l'expression régulière search pour répondre à nos besoins. Ainsi, 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 is_grouped_together sont deux nouvelles fonctions dérivées de re.search auxquelles l'argument pattern est appliqué (puisque pattern est le premier argument du re.search signature de la méthode).

La signature de ces deux nouvelles fonctions (appelable) 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 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 vous référer au lien ci-dessus pour obtenir une compréhension plus approfondie du sujet, car il couvre cet exemple spécifique et bien plus encore.

28
sisanared

réponse courte, partial donne les valeurs par défaut aux paramètres d'une fonction qui autrement n'aurait 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
23

À mon avis, c'est une façon de mettre en œuvre currying en python.

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

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

if __== "__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.

7
Hanzhou Tang

Il convient également de mentionner que, lorsque la fonction partielle passe une autre fonction pour laquelle nous voulons "coder en dur" certains paramètres, ce doit ê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 la même chose, mais en changeant un paramètre à la place

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

il va générer une erreur, "TypeError: func () a eu plusieurs valeurs pour l'argument 'a'"

1
MSK