__call__

Qu'est-ce que la méthode spéciale __call__ en Python ?

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
PYTHON

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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'
PYTHON

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]
PYTHON
Un instant

Créez un compte pour exécuter ce code

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]
PYTHON

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.