Exemple pratique de méthode spéciale Python __call__

159

Je sais que cette __call__méthode dans une classe est déclenchée lorsque l'instance d'une classe est appelée. Cependant, je ne sais pas quand je peux utiliser cette méthode spéciale, car on peut simplement créer une nouvelle méthode et effectuer la même opération effectuée dans __call__method et au lieu d'appeler l'instance, vous pouvez appeler la méthode.

J'apprécierais vraiment que quelqu'un me donne une utilisation pratique de cette méthode spéciale.

mohi666
la source
8
la fonctionnalité de _call_ est exactement comme l'opérateur surchargé de () en C ++ . Si vous créez simplement une nouvelle méthode en dehors de la classe, vous ne pouvez pas accéder aux données internes d'une classe.
andy
2
L'utilisation la plus courante de __call__est cachée à la vue; c'est ainsi que vous instanciez une classe: x = Foo()est vraiment x = type(Foo).__call__(Foo), où __call__est défini par la métaclasse de Foo.
chepner

Réponses:

88

Le module Django Forms utilise __call__bien la méthode pour implémenter une API cohérente pour la validation de formulaire. Vous pouvez écrire votre propre validateur pour un formulaire dans Django en tant que fonction.

def custom_validator(value):
    #your validation logic

Django a des validateurs intégrés par défaut tels que des validateurs d'e-mails, des validateurs d'url, etc., qui relèvent généralement des validateurs RegEx. Pour les implémenter proprement, Django a recours à des classes appelables (au lieu de fonctions). Il implémente la logique de validation Regex par défaut dans un RegexValidator, puis étend ces classes pour d'autres validations.

class RegexValidator(object):
    def __call__(self, value):
        # validation logic

class URLValidator(RegexValidator):
    def __call__(self, value):
        super(URLValidator, self).__call__(value)
        #additional logic

class EmailValidator(RegexValidator):
    # some logic

Maintenant, votre fonction personnalisée et EmailValidator intégré peuvent être appelés avec la même syntaxe.

for v in [custom_validator, EmailValidator()]:
    v(value) # <-----

Comme vous pouvez le voir, cette implémentation dans Django est similaire à ce que d'autres ont expliqué dans leurs réponses ci-dessous. Cela peut-il être mis en œuvre d'une autre manière? Vous pourriez, mais à mon humble avis, ce ne sera pas aussi lisible ou aussi facilement extensible pour un gros framework comme Django.

Praveen Gollakota
la source
5
Donc, s'il est utilisé correctement, il peut rendre le code plus lisible. Je suppose que s'il est utilisé au mauvais endroit, cela rendrait également le code très illisible.
mohi666
15
Ceci est un exemple de la façon dont il peut être utilisé, mais pas un bon à mon avis. Il n'y a aucun avantage dans ce cas à avoir une instance appelable. Il serait préférable d'avoir une interface / classe abstraite avec une méthode, comme .validate (); c'est la même chose mais en plus explicite. La valeur réelle de __call__ est de pouvoir utiliser une instance dans un endroit où un appelable est attendu. J'utilise __call__ le plus souvent lors de la création de décorateurs, par exemple.
Daniel
120

Cet exemple utilise la mémorisation , le stockage essentiellement des valeurs dans un tableau (dictionnaire dans ce cas) afin que vous puissiez les consulter plus tard au lieu de les recalcul.

Ici, nous utilisons une classe simple avec une __call__méthode pour calculer les factorielles (via un objet appelable ) au lieu d'une fonction factorielle qui contient une variable statique (ce qui n'est pas possible en Python).

class Factorial:
    def __init__(self):
        self.cache = {}
    def __call__(self, n):
        if n not in self.cache:
            if n == 0:
                self.cache[n] = 1
            else:
                self.cache[n] = n * self.__call__(n-1)
        return self.cache[n]

fact = Factorial()

Vous avez maintenant un factobjet qui peut être appelé, comme toute autre fonction. Par exemple

for i in xrange(10):                                                             
    print("{}! = {}".format(i, fact(i)))

# output
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880

Et il est également avec état.

S.Lott
la source
2
Je préfère avoir un factobjet indexable puisque votre __call__fonction est essentiellement un index. Utiliserait également une liste au lieu d'un dict, mais ce n'est que moi.
Chris Lutz
4
@delnan - Presque tout peut être fait de différentes manières. Lequel est le plus lisible dépend du lecteur.
Chris Lutz
1
@Chris Lutz: Vous êtes libre d'envisager ce genre de changements. Pour la mémorisation en général , un dictionnaire fonctionne bien car vous ne pouvez pas garantir l'ordre dans lequel les choses remplissent votre liste. Dans ce cas, une liste peut fonctionner, mais elle ne sera ni plus rapide ni plus simple.
S.Lott
8
@delnan: Ce n'est pas censé être le plus court. Personne ne gagne au code golf. Il est destiné à montrer __call__, être simple et rien de plus.
S.Lott
3
Mais cela ruine un peu l'exemple lorsque la technique démontrée n'est pas idéale pour les tâches, n'est-ce pas? (Et je ne parlais pas du court-métrage "sauvegardons les lignes pour le diable", je parlais du "écrivez-le de cette manière tout aussi claire et enregistrez du code standard". Soyez assuré que je ne suis pas un de ces fous essayant d'écrire le code le plus court possible, je veux simplement éviter le code standard qui n'ajoute rien pour le lecteur.)
40

Je trouve cela utile car cela me permet de créer des API faciles à utiliser (vous avez un objet appelable qui nécessite des arguments spécifiques), et faciles à implémenter car vous pouvez utiliser des pratiques orientées objet.

Ce qui suit est le code que j'ai écrit hier qui crée une version des hashlib.foométhodes qui hachent des fichiers entiers plutôt que des chaînes:

# filehash.py
import hashlib


class Hasher(object):
    """
    A wrapper around the hashlib hash algorithms that allows an entire file to
    be hashed in a chunked manner.
    """
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def __call__(self, file):
        hash = self.algorithm()
        with open(file, 'rb') as f:
            for chunk in iter(lambda: f.read(4096), ''):
                hash.update(chunk)
        return hash.hexdigest()


md5    = Hasher(hashlib.md5)
sha1   = Hasher(hashlib.sha1)
sha224 = Hasher(hashlib.sha224)
sha256 = Hasher(hashlib.sha256)
sha384 = Hasher(hashlib.sha384)
sha512 = Hasher(hashlib.sha512)

Cette implémentation me permet d'utiliser les fonctions de la même manière que les hashlib.foofonctions:

from filehash import sha1
print sha1('somefile.txt')

Bien sûr, j'aurais pu l'implémenter d'une manière différente, mais dans ce cas, cela semblait être une approche simple.

bradley.ayers
la source
7
Encore une fois, les fermetures ruinent cet exemple. pastebin.com/961vU0ay est à 80% des lignes et tout aussi clair.
8
Je ne suis pas convaincu que ce serait toujours aussi clair pour quelqu'un (par exemple, quelqu'un qui n'a utilisé que Java). Les fonctions imbriquées et la recherche / la portée des variables peuvent prêter à confusion. Je suppose que mon point était que __call__vous donne un outil qui vous permet d'utiliser des techniques OO pour résoudre des problèmes.
bradley.ayers
4
Je pense que la question "pourquoi utiliser X sur Y" lorsque les deux fournissent des fonctionnalités équivalentes est terriblement subjective. Pour certaines personnes, l'approche OO est plus facile à comprendre, pour d'autres l'approche de fermeture l'est. Il n'y a pas argument convaincant d'utiliser l' un sur l'autre, à moins que vous avez eu une situation où vous deviez utiliser isinstanceou quelque chose de similaire.
bradley.ayers
2
@delnan Votre exemple de fermetures est moins de lignes de code, mais qu'il est tout aussi clair est plus difficile à argumenter.
Dennis
8
Un exemple où vous préférez utiliser une __call__méthode au lieu d'une fermeture est lorsque vous avez affaire au module de multitraitement, qui utilise le décapage pour transmettre des informations entre les processus. Vous ne pouvez pas sélectionner une clôture, mais vous pouvez sélectionner une instance d'une classe.
John Peter Thompson Garcés
21

__call__est également utilisé pour implémenter des classes de décorateur en python. Dans ce cas, l'instance de la classe est appelée lorsque la méthode avec le décorateur est appelée.

class EnterExitParam(object):

    def __init__(self, p1):
        self.p1 = p1

    def __call__(self, f):
        def new_f():
            print("Entering", f.__name__)
            print("p1=", self.p1)
            f()
            print("Leaving", f.__name__)
        return new_f


@EnterExitParam("foo bar")
def hello():
    print("Hello")


if __name__ == "__main__":
    hello()
Kris
la source
9

Oui, lorsque vous savez que vous avez affaire à des objets, il est parfaitement possible (et dans de nombreux cas conseillé) d'utiliser un appel de méthode explicite. Cependant, vous avez parfois affaire à du code qui attend des objets appelables - généralement des fonctions, mais grâce à__call__ vous, vous pouvez créer des objets plus complexes, avec des données d'instance et plus de méthodes pour déléguer des tâches répétitives, etc. qui sont toujours appelables.

De plus, vous utilisez parfois à la fois des objets pour des tâches complexes (où il est logique d'écrire une classe dédiée) et des objets pour des tâches simples (qui existent déjà dans des fonctions ou sont plus facilement écrites sous forme de fonctions). Pour avoir une interface commune, vous devez soit écrire de petites classes encapsulant ces fonctions avec l'interface attendue, soit conserver les fonctions des fonctions et rendre les objets plus complexes appelables. Prenons les threads comme exemple. Les Threadobjets du module standard de la bibliothèquethreading veulent un appelable comme targetargument (c'est-à-dire comme action à faire dans le nouveau thread). Avec un objet appelable, vous n'êtes pas limité aux fonctions, vous pouvez également transmettre d'autres objets, comme un worker relativement complexe qui obtient des tâches à faire à partir d'autres threads et les exécute séquentiellement:

class Worker(object):
    def __init__(self, *args, **kwargs):
        self.queue = queue.Queue()
        self.args = args
        self.kwargs = kwargs

    def add_task(self, task):
        self.queue.put(task)

    def __call__(self):
        while True:
            next_action = self.queue.get()
            success = next_action(*self.args, **self.kwargs)
            if not success:
               self.add_task(next_action)

Ce n'est qu'un exemple qui me vient à l'esprit, mais je pense que c'est déjà assez complexe pour justifier le cours. Faire cela uniquement avec des fonctions est difficile, au moins cela nécessite le retour de deux fonctions et cela devient lentement complexe. On pourrait renommer __call__quelque chose d'autre et passer une méthode liée, mais cela rend le code créant le thread un peu moins évident et n'ajoute aucune valeur.

yann.kmm
la source
3
Il est probablement utile d'utiliser ici l'expression "duck typing" ( en.wikipedia.org/wiki/Duck_typing#In_Python ) - vous pouvez imiter une fonction en utilisant un objet de classe plus compliqué de cette façon.
Andrew Jaffe
2
À titre d'exemple connexe, j'ai vu __call__utilisé des instances de classe (au lieu de fonctions) comme applications WSGI. Voici un exemple tiré de "The Definitive Guide to Pylons": Using Instances of Classes
Josh Rosen
5

Les décorateurs basés sur les classes utilisent __call__pour référencer la fonction encapsulée. Par exemple:

class Deco(object):
    def __init__(self,f):
        self.f = f
    def __call__(self, *args, **kwargs):
        print args
        print kwargs
        self.f(*args, **kwargs)

Il y a une bonne description des différentes options ici sur Artima.com

rorycl
la source
Cependant, je vois rarement des décorateurs de classe, car ils nécessitent un code standard non évident pour fonctionner avec des méthodes.
4

La __call__méthode et les fermetures IMHO nous donnent un moyen naturel de créer un modèle de conception STRATÉGIE en Python. Nous définissons une famille d'algorithmes, encapsulons chacun d'eux, les rendons interchangeables et à la fin nous pouvons exécuter un ensemble d'étapes communes et, par exemple, calculer un hachage pour un fichier.

ady
la source
4

Je viens de tomber sur une utilisation de __call__()concert avec __getattr__()laquelle je trouve que c'est beau. Il vous permet de masquer plusieurs niveaux d'une API JSON / HTTP / (cependant_sérialisée) à l'intérieur d'un objet.

La __getattr__()partie prend en charge le retour itératif d'une instance modifiée de la même classe, en remplissant un attribut supplémentaire à la fois. Ensuite, après que toutes les informations ont été épuisées, __call__()prend le relais avec les arguments que vous avez transmis.

En utilisant ce modèle, vous pouvez par exemple passer un appel comme api.v2.volumes.ssd.update(size=20), qui se termine par une requête PUT à https://some.tld/api/v2/volumes/ssd/update.

Le code particulier est un pilote de stockage en bloc pour un certain backend de volume dans OpenStack, vous pouvez le vérifier ici: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

EDIT: mise à jour du lien pour pointer vers la révision principale.

Peter slovaque
la source
C'est zonte. J'ai utilisé une fois le même mécanisme pour parcourir une arborescence XML arbitraire en utilisant l'accès aux attributs.
Petri
1

Spécifiez a __metaclass__et remplacez la __call__méthode, et demandez à la méthode des méta-classes spécifiée de __new__renvoyer une instance de la classe, alto vous avez une "fonction" avec des méthodes.

user2772852
la source
1

Nous pouvons utiliser __call__method pour utiliser d'autres méthodes de classe comme méthodes statiques.

    class _Callable:
        def __init__(self, anycallable):
            self.__call__ = anycallable

    class Model:

        def get_instance(conn, table_name):

            """ do something"""

        get_instance = _Callable(get_instance)

    provs_fac = Model.get_instance(connection, "users")             
Abhishek Jain
la source
0

Un exemple courant est le __call__in functools.partial, voici une version simplifiée (avec Python> = 3.5):

class partial:
    """New function with partial application of the given arguments and keywords."""

    def __new__(cls, func, *args, **kwargs):
        if not callable(func):
            raise TypeError("the first argument must be callable")
        self = super().__new__(cls)

        self.func = func
        self.args = args
        self.kwargs = kwargs
        return self

    def __call__(self, *args, **kwargs):
        return self.func(*self.args, *args, **self.kwargs, **kwargs)

Usage:

def add(x, y):
    return x + y

inc = partial(add, y=1)
print(inc(41))  # 42
interminable
la source
0

L'opérateur d'appel de fonction.

class Foo:
    def __call__(self, a, b, c):
        # do something

x = Foo()
x(1, 2, 3)

La méthode __call__ peut être utilisée pour redéfinir / réinitialiser le même objet. Il facilite également l'utilisation des instances / objets d'une classe en tant que fonctions en passant des arguments aux objets.


la source
Quand cela serait-il utile? Foo (1, 2, 3) semble plus clair.
Yaroslav Nikitenko
0

Je trouve un bon endroit pour utiliser des objets appelables, ceux qui définissent __call__(), est lors de l' utilisation des capacités de programmation fonctionnelle en Python, tels que map(), filter(), reduce().

Le meilleur moment pour utiliser un objet appelable sur une fonction simple ou une fonction lambda est lorsque la logique est complexe et doit conserver un état ou utilise d'autres informations qui ne sont pas transmises à la __call__()fonction.

Voici un code qui filtre les noms de fichiers en fonction de leur extension de nom de fichier à l'aide d'un objet appelable et filter().

Appelable:

import os

class FileAcceptor(object):
    def __init__(self, accepted_extensions):
        self.accepted_extensions = accepted_extensions

    def __call__(self, filename):
        base, ext = os.path.splitext(filename)
        return ext in self.accepted_extensions

class ImageFileAcceptor(FileAcceptor):
    def __init__(self):
        image_extensions = ('.jpg', '.jpeg', '.gif', '.bmp')
        super(ImageFileAcceptor, self).__init__(image_extensions)

Usage:

filenames = [
    'me.jpg',
    'me.txt',
    'friend1.jpg',
    'friend2.bmp',
    'you.jpeg',
    'you.xml']

acceptor = ImageFileAcceptor()
image_filenames = filter(acceptor, filenames)
print image_filenames

Production:

['me.jpg', 'friend1.jpg', 'friend2.bmp', 'you.jpeg']
cycgrog
la source
0

C'est trop tard mais je donne un exemple. Imaginez que vous ayez une Vectorclasse et une Pointclasse. Les deux prennent x, ycomme arguments positionnels. Imaginons que vous souhaitiez créer une fonction qui déplace le point à placer sur le vecteur.

4 solutions

  • put_point_on_vec(point, vec)

  • Faites-en une méthode sur la classe vectorielle. par exemple my_vec.put_point(point)

  • Faites-en une méthode sur la Pointclasse.my_point.put_on_vec(vec)
  • Vectorimplémente __call__, vous pouvez donc l'utiliser commemy_vec_instance(point)

Cela fait en fait partie de quelques exemples sur lesquels je travaille pour un guide des méthodes dunder expliquées avec Maths que je vais publier tôt ou tard.

J'ai quitté la logique du déplacement du point lui-même car ce n'est pas le sujet de cette question

Ahmed I. Elsayed
la source