Je sais que la méthode __call__
d'une classe est déclenchée lorsque l'instance d'une classe est appelée. Cependant, je n'ai aucune idée du moment où 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 que dans la méthode __call__
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.
Le module de formulaires Django utilise la méthode __call__
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 quelques validateurs intégrés par défaut, tels que des validateurs de courrier électronique, des validateurs d'URL, etc. 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
Désormais, 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 constater, 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 le faire, mais à mon humble avis, il ne sera pas aussi lisible ni aussi facilement extensible que pour un gros framework comme Django.
Cet exemple utilise memoization , stockant essentiellement des valeurs dans une table (dictionnaire dans ce cas) afin que vous puissiez les rechercher plus tard au lieu de les recalculer.
Ici, nous utilisons une classe simple avec une méthode __call__
pour calculer des factorielles (via un objet appelable ) au lieu d'une fonction factorielle contenant une variable statique (comme ce 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 objet fact
qui est appelable, comme toutes les autres fonctions. 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 stateful.
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.
Voici le code que j'ai écrit hier et qui crée une version des méthodes hashlib.foo
qui hache 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 fonctions hashlib.foo
:
from filehash import sha1
print sha1('somefile.txt')
Bien sûr, j'aurais pu l'appliquer de manière différente, mais dans ce cas, cela semblait être une approche simple.
__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 __== "__main__":
hello()
Oui, lorsque vous savez que vous utilisez des objets, il est parfaitement possible (et souvent conseillé) d’utiliser un appel de méthode explicite. Cependant, vous traitez parfois avec du code qui attend des objets appelables - généralement des fonctions, mais grâce à __call__
, vous pouvez construire des objets plus complexes, avec des données d'instance et davantage de méthodes pour déléguer des tâches répétitives, etc.
De même, vous utilisez parfois à la fois des objets pour des tâches complexes (où il est judicieux 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 écrits en tant que fonctions). Pour avoir une interface commune, vous devez soit écrire de petites classes qui enveloppent ces fonctions avec l'interface attendue, soit conserver les fonctions de fonctions et rendre les objets plus complexes appelables. Prenons les discussions comme exemple. Les objets Thread
DU MODULE DE BIBLIOTH&EGRAVE;QUE STANDARD threading
veulent un appelable comme argument target
(c’est-à-dire comme une action à effectuer dans le nouveau thread). Avec un objet appelable, vous n'êtes pas limité aux fonctions, vous pouvez également transmettre d'autres objets, tels qu'un opérateur relativement complexe qui doit exécuter des tâches à partir d'autres threads et les exécuter de manière séquentielle:
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à suffisamment complexe pour justifier le cours. Faire cela uniquement avec des fonctions est difficile, du moins cela nécessite le renvoi de deux fonctions et cela devient lentement complexe. Un pourrait renommer __call__
en quelque chose d'autre et passer une méthode liée, mais cela rend le code créant le thread légèrement moins évident, et n'ajoute aucune valeur.
Les décorateurs basés sur les classes utilisent __call__
pour faire référence à 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)
Vous trouverez une bonne description des différentes options ici à Artima.com
La méthode IMHO __call__
et les fermetures nous offrent un moyen naturel de créer un modèle de conception STRATEGY en Python. Nous définissons une famille d'algorithmes, encapsulons chacun d'entre eux, les rendons interchangeables et nous pouvons finalement exécuter un ensemble d'étapes communes et, par exemple, calculer un hachage pour un fichier.
Je suis juste tombé sur une utilisation de __call__()
de concert avec __getattr__()
qui, à mon avis, est magnifique. Il vous permet de masquer plusieurs niveaux d'une API JSON/HTTP/(cependant_serialized) à l'intérieur d'un objet.
La partie __getattr__()
se charge de renvoyer de manière itérative une instance modifiée de la même classe, en remplissant un attribut supplémentaire à la fois. Ensuite, une fois que toutes les informations ont été épuisées, __call__()
reprend tous les arguments que vous avez passés.
En utilisant ce modèle, vous pouvez par exemple faire un appel comme api.v2.volumes.ssd.update(size=20)
, qui aboutit à une demande PUT à https://some.tld/api/v2/volumes/ssd/update
.
Le code particulier est un pilote de stockage de bloc pour un volume de volume donné 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.
Nous pouvons utiliser la méthode __call__
pour utiliser d'autres méthodes de classe en tant que 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")
Spécifiez un __metaclass__
, remplacez la méthode __call__
et faites en sorte que la méthode __new__
de la méta-classe spécifiée retourne une instance de la classe. Si vous ne respectez pas ces règles, vous disposez d'une "fonction" avec des méthodes.
Un exemple courant est le __call__
dans 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
Je trouve un bon endroit pour utiliser des objets appelables, ceux qui définissent __call__()
, lorsque vous utilisez les fonctionnalités de programmation fonctionnelle en Python, telles 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 utiliser d'autres informations qui ne sont pas transmises à la fonction __call__()
.
Voici un code qui filtre les noms de fichiers en fonction de leur extension en utilisant un objet appelable et filter()
.
Callable:
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
Sortie:
['me.jpg', 'friend1.jpg', 'friend2.bmp', 'you.jpeg']