Functools.partial(): Rendre une fonction à n-argument en tant que object appelable avec moins d’arguments en Python

Vous avez un objet appelable (une fonction/méthode) que vous aimeriez utiliser dans un autre code Python, peut-être comme fonction de rappel ou gestionnaire, mais il prend trop d’arguments et provoque une exception quand il est appelé.

Si vous avez besoin de réduire le nombre d’arguments à une fonction, vous devriez utiliser la méthode functools.partial().

La fonction partial() vous permet d’assigner des valeurs fixes à un ou plusieurs arguments, réduisant ainsi le nombre d’arguments qui doivent être fournis aux appels suivants. Pour illustrer cela, supposons que vous ayez cette fonction:

def mafct(a, b, c, d):
    print(a, b, c, d)

Considérons maintenant l’utilisation de partial() pour fixer certaines valeurs d’arguments:

>>> from functools import partial
>>> s1 = partial(mafct, 1)       # a = 1
>>> s1(2, 3, 4)
1 2 3 4
>>> s1(4, 5, 6)
1 4 5 6
>>> s2 = partial(mafct, d=42)    # d = 42
>>> s2(1, 2, 3)
1 2 3 42
>>> s2(4, 5, 5)
4 5 5 42
>>> s3 = partial(mafct, 1, 2, d=42) # a = 1, b = 2, d = 42
>>> s3(3)
1 2 3 42
>>> s3(4)
1 2 4 42
>>> s3(5)
1 2 5 42
>>>

Observez que partial() fixe les valeurs de certains arguments et retourne un nouvel objet appelable en conséquence. Ce nouvel appelable accepte les arguments non encore assignés, les combine avec les arguments donnés à partial(), et passe le tout à la fonction originale.

Ce code est vraiment liée au problème de faire fonctionner ensemble des bits de code apparemment incompatibles. Une série d’exemples aidera à l’illustrer.

Comme premier exemple, supposons que vous ayez une liste de points représentés sous forme de tuples de coordonnées (x,y). Vous pouvez utiliser la fonction suivante pour calculer la distance entre deux points:

points = [ (1, 2), (3, 4), (5, 6), (7, 8) ]

import math
def distance(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return math.hypot(x2 - x1, y2 - y1)

Supposons maintenant que vous vouliez trier tous les points en fonction de leur distance par rapport à un autre point.

La méthode sort() des listes accepte un argument clé qui peut être utilisé pour personnaliser le tri, mais elle ne fonctionne qu’avec des fonctions qui prennent un seul argument (donc, distance() ne convient pas).

Voici comment vous pouvez utiliser partial() pour corriger ce problème:

>>> pt = (4, 3)
>>> points.sort(key=partial(distance,pt))
>>> points
[(3, 4), (1, 2), (5, 6), (7, 8)]
>>>

Dans le prolongement de cette idée, partial() peut souvent être utilisé pour modifier les signatures d’argument des fonctions de rappel utilisées dans d’autres bibliothèques.

Par exemple, voici un peu de code qui utilise le multitraitement pour calculer de manière asynchrone un résultat qui est transmis à une fonction de rappel qui accepte à la fois le résultat et un argument de journalisation facultatif:

def output_result(result, log=None):
    if log is not None:
        log.debug('On obient: %r', result)

# Une fonction d'exemple
def ajouter(x, y):
    return x + y

if __name__ == '__main__':
    import logging
    from multiprocessing import Pool
    from functools import partial

    logging.basicConfig(level=logging.DEBUG)
    log = logging.getLogger('test')

    p = Pool()
    p.apply_async(add, (3, 4), callback=partial(output_result, log=log))
    p.close()
    p.join()

Lorsque vous fournissez la fonction de rappel en utilisant apply_async(), l’argument de journalisation supplémentaire est donné en utilisant partial(). multiprocessing n’est pas le plus approprié dans tout cela – il appelle simplement la fonction de rappel avec une valeur simple.

A titre d’exemple similaire, considérons le problème de l’écriture des serveurs réseau. Le module socketserver le rend relativement facile. Par exemple, voici un simple serveur d’écho:

from socketserver import StreamRequestHandler, TCPServer

class EchoHandler(StreamRequestHandler):
    def gestionnaire(self):
        for ligne in self.rfichier:
            self.wfichier.write(b'obtenu:' + ligne)

serv = TCPServer(('', 15000), EchoHandler)
serv.serve_forever()

Cependant, supposons que vous vouliez donner à la classe EchoHandler une méthode __init__() qui accepte un argument de configuration supplémentaire. Par exemple:

class EchoHandler(StreamRequestHandler):

    #   ack est ajouté en argument mot-clé seulement. *args, **kwargs
    #   sont tous les paramètres normaux fournis (qui sont transmis)

    def __init__(self, *args, ack, **kwargs):
        self.ack = ack
        super().__init__(*args, **kwargs)
    def gestionnaire(self):
        for ligne in self.rfichier:
            self.wfichier.write(self.ack + ligne)




Si vous faites ce changement, vous constaterez qu’il n’y a plus de moyen évident de le brancher dans la classe TCPServer. En fait, vous constaterez que le code commence maintenant à générer des exceptions comme celle-ci:

Exception happened during processing of request from ('127.0.0.1', 59004)
Traceback (most recent call last):
 ...
TypeError: __init__() missing 1 required keyword-only argument: 'ack'

A première vue, il semble impossible de corriger ce code, à moins de modifier le code source en socketserver ou de trouver une sorte de solution bizarre.

Cependant, il est facile à résoudre en utilisant partial()-utilisez-le simplement pour fournir la valeur de l’argument ack, comme ceci:

from functools import partial
serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED:'))
serv.serve_forever()

Dans cet exemple, la spécification de l’argument ack dans la méthode __init__() peut sembler un peu bizarre, mais elle est spécifiée comme un argument uniquement par mot-clé. Cette question est abordée plus en détail dans l’article “Écrire des fonctions qui n’acceptent que des arguments de mots-clés en Python“.

La fonction partial() est parfois remplacée par une expression lambda. Par exemple, les codes précédents pourraient utiliser des instructions comme celles-ci:

points.sort(key=lambda p: distance(pt, p))

p.apply_async(add, (3, 4), callback=lambda result: output_result(result,log))

serv = TCPServer(('', 15080), lambda *args, **kwargs: EchoHandler(*args, ack=b'RECEIVED:', **kwargs))

Ce code fonctionne, mais il est plus verbeux et potentiellement beaucoup plus déroutant pour quelqu’un qui le lit. Utiliser partial() est un peu plus explicite sur vos intentions (en fournissant des valeurs pour certains arguments).

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here