Qu'est-ce que la méthode __call__ ?
La méthode spéciale __call__ rend une instance « callable » (appelable). Vous pouvez alors appeler votre instance avec des parenthèses comme une fonction.
Imaginez une instance nommée mon_objet, si vous faites mon_objet(), en arrière-plan Python exécute mon_objet.__call__().
Attention
Par défaut, une instance n'est pas appelable.
class Utilisateur: def __init__(self, nom): self.nom = nom patrick = Utilisateur("Patrick") patrick() # TypeError: 'Utilisateur' object is not callable
Si vous appelez une instance, Python cherche alors la méthode __call__ de la classe. S'il ne la trouve pas, vous aurez une erreur TypeError.
Vous pouvez toutefois vous assurer qu'un objet est appelable avec la fonction callable() :
class Utilisateur: def __init__(self, nom): self.nom = nom patrick = Utilisateur("Patrick") def saluer(nom): return f"Bonjour {nom}" print(callable(saluer)) # True print(callable(patrick)) # False
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
Comment implémenter __call__ ?
C'est simple, il suffit de définir la méthode dans la classe 🤷, rien de plus.
Créons un validateur (c'est tout à fait le genre d'exemple que vous retrouverez), on définit des bornes à la création et on l'utilise comme une fonction :
class Validateur: def __init__(self, mini, maxi): self.mini = mini self.maxi = maxi def __call__(self, valeur): return self.mini <= valeur <= self.maxi age_valide = Validateur(18, 120) note_valide = Validateur(0, 20) print(age_valide(35)) # True print(age_valide(150)) # False print(note_valide(15)) # True
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
Les instances sont de la même classe, mais chacune a ses bornes. On se sert des instances comme deux fonctions configurées différemment, le tout sans écrire de fonction ! 😎
À noter
Vous pouvez passer les arguments que vous voulez à __call__, comme n'importe quelle fonction.
Pourquoi utiliser __call__ plutôt qu'une méthode ou une fonction ?
Ce que nous avons fait précédemment, nous aurions très bien pu le faire avec une fonction est_valide(valeur, mini, maxi) ou une méthode age_valide.verifier(valeur).
L'avantage est que le code qui reçoit l'objet n'a pas besoin de connaître le nom de la méthode. Créons une fonction d'import pour des valeurs, que nous filtrons avec un validateur.
def importer(valeurs, validateur): return [v for v in valeurs if validateur.verifier(v)] def est_majeur(age): return age >= 18 ages = [15, 35, 150, 42] importer(ages, est_majeur) # AttributeError: 'function' object has no attribute 'verifier'
La fonction importer est limitée au nom verifier : une simple fonction ou encore un validateur avec une méthode qui porte un nom différent la fait planter. __call__ permet ici à importer d'appeler ce qu'on lui donne sans rien connaître de sa structure.
class Validateur: def __init__(self, mini, maxi): self.mini = mini self.maxi = maxi def __call__(self, valeur): return self.mini <= valeur <= self.maxi age_valide = Validateur(18, 120) note_valide = Validateur(0, 20) def importer(valeurs, validateur): return [v for v in valeurs if validateur(v)] valeurs = [15, 35, 150, 42] print(importer(valeurs, age_valide)) # [35, 42] print(importer(valeurs, note_valide)) # [15]
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
Et un petit exemple avec la fonction filter :
print(list(filter(age_valide, valeurs))) # [35, 42]
On ne sait pas si age_valide est une fonction ou une instance de Validateur. Ça s'appelle comme une fonction, et pour filter et importer c'est tout ce qui compte 🦆.
Un autre avantage est que l'objet conserve un état entre deux appels sans variable globale, quand une fonction classique oublie tout, appel après appel. On pourrait ajouter à notre Validateur un compteur de rejets.
class Validateur: def __init__(self, mini, maxi): self.mini = mini self.maxi = maxi self.nb_rejets = 0 def __call__(self, valeur): if self.mini <= valeur <= self.maxi: return True self.nb_rejets += 1 return False age_valide = Validateur(18, 120) ages_valides = list(filter(age_valide, [15, 35, 150, 42])) print(ages_valides) # [35, 42] print(age_valide.nb_rejets) # 2
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
À noter
C'est ce que fait Django avec ses validateurs built-in : il s'agit de classes avec la méthode __call__.
Code source de Django
Comment créer un décorateur avec __call__ ?
Un cas d'usage courant que vous rencontrerez. Si un décorateur est une fonction qui reçoit une fonction et qui en renvoie une autre, rien n'empêche d'utiliser une classe.
Vous me voyez venir, je vais ressortir le cas du décorateur qui mesure le temps d'exécution d'une fonction :
import time class Chrono: def __init__(self, fonction): self.fonction = fonction self.appels = 0 self.duree_totale = 0 def __call__(self, *args, **kwargs): debut = time.perf_counter() resultat = self.fonction(*args, **kwargs) self.appels += 1 self.duree_totale += time.perf_counter() - debut return resultat @Chrono def generer_rapport(): time.sleep(0.5) return "Rapport prêt" generer_rapport() generer_rapport() print(generer_rapport.appels) # 2 print(f"{generer_rapport.duree_totale:.1f}s") # 1.0s
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
@Chrono au-dessus de la fonction generer_rapport() revient à faire generer_rapport = Chrono(generer_rapport). En effet, Python appelle la classe avec la fonction en argument.
generer_rapport n'est plus une fonction, mais une instance de Chrono. Elle garde un état entre chaque appel et possède les attributs de Chrono.