Définition
Il arrive qu'une méthode n'ait rien à faire d'un objet précis : ce qu'elle veut, c'est la classe, pour y lire une valeur commune à tous les objets ou pour en fabriquer un à partir de données brutes. Avec une méthode ordinaire, le travail coince : il faut déjà tenir une instance pour l'appeler. La classmethod lève ce blocage.
C'est une méthode qui reçoit la classe elle-même en premier paramètre, là où une méthode ordinaire reçoit l'objet. On la déclare avec un décorateur posé juste au-dessus du def, et son premier paramètre se nomme cls par convention, plutôt que self.
cls n'est pas un mot-clé : Python accepterait n'importe quel nom ici, comme pour self. Mais tout le monde y lit « la classe arrive », et s'en écarter coûte au relecteur plus de temps que ça n'en fait gagner.
L'exemple le plus court tient en une classe, une valeur commune, et une méthode qui va la lire.
class Utilisateur:
domaine = "believemy.com" # valeur partagée par toute la classe
@classmethod
def adresse_type(cls): # cls, c'est Utilisateur
return f"prenom.nom@{cls.domaine}"
print(Utilisateur.adresse_type()) # appel direct sur la classe
# prenom.nom@believemy.comAucun objet n'a été créé ici, et c'est le premier apport de cette écriture : la méthode est disponible avant même qu'une instance existe.
Les trois formes de méthode
Cette écriture a deux voisines avec lesquelles on la confond souvent. Une classe contient trois sortes de fonctions, distinguées par ce qu'elles reçoivent en premier : dans le tableau, c'est la colonne du milieu qui commande.
| Écriture | Premier paramètre | Ce qu'elle sert à faire |
|---|---|---|
| Méthode d'instance | L'objet, nommé self | Travailler sur les données d'un objet précis |
@classmethod | La classe, nommée cls | Fabriquer un objet ou lire un attribut de classe |
@staticmethod | Rien du tout | Ranger une fonction utilitaire près de son sujet |
Le choix se règle avec une seule question : de quoi la méthode a-t-elle besoin ? Les données d'un objet précis, et elle prend self. La classe, et elle prend cls. Ni l'une ni l'autre, et elle ne doit rien recevoir. La property ne joue pas dans cette catégorie : elle change la façon d'appeler la méthode, pas ce qu'elle reçoit.
Le constructeur alternatif
C'est de très loin l'usage principal, et mieux vaut partir de l'ennui qu'il règle. L'__init__ décrit une seule porte d'entrée : celle où les données arrivent propres et séparées, un argument par information. Or elles arrivent rarement ainsi, puisqu'un fichier livre une ligne de texte et un formulaire une chaîne à découper.
On peut faire ce ménage dans le code appelant. Ça tient une fois, puis le même découpage se retrouve copié à trois endroits, et le jour où le format change il faut retrouver les trois. Une classmethod ajoute une seconde porte à la classe, sans toucher à la première.
class Client:
def __init__(self, prenom, nom): # la porte d'entrée officielle
self.prenom = prenom
self.nom = nom
@classmethod
def depuis_ligne(cls, ligne): # la seconde porte
prenom, nom = ligne.split(";")
return cls(prenom.strip(), nom.strip()) # puis on repasse par __init__
client = Client.depuis_ligne("Marie ; Dupont")Deux détails comptent. Le découpage par split reste enfermé dans la méthode, et le reste du programme n'a plus qu'un nom à retenir. La dernière ligne appelle cls(...), donc le constructeur normal : une classmethod ne remplace jamais __init__, elle lui prépare le terrain.
La bibliothèque standard en est remplie : dict.fromkeys, datetime.now, datetime.fromtimestamp. Le nom y annonce presque toujours la matière première, et nommer la vôtre ainsi rend son rôle lisible sans documentation.
Ce que cls change à l'héritage
Jusqu'ici, nommer Client en dur aurait donné le même résultat. Le vrai bénéfice de cls apparaît dès qu'une classe est dérivée : puisqu'il désigne la classe réellement appelée, et non celle où le code a été écrit, le constructeur alternatif fabrique le bon type dans les sous-classes.
class ClientPro(Client):
remise = 0.2 # la sous-classe n'ajoute qu'une valeur
c = ClientPro.depuis_ligne("Marie ; Dupont")
print(type(c))
# <class '__main__.ClientPro'>La sous-classe n'a rien redéfini, et hérite pourtant d'un constructeur qui lui rend des objets de son propre type. En dur, l'appel aurait rendu un Client ordinaire sans rien signaler, et le défaut serait resté invisible jusqu'au premier accès à remise. C'est pour cela que l'héritage et cette écriture vont si souvent ensemble.
Les erreurs qui reviennent
La plus fréquente est l'oubli de l'arobase. Sans son décorateur, une méthode qui déclare cls reste une méthode d'instance ordinaire : l'appel sur la classe lève une TypeError, mais l'appel sur un objet passe sans rien dire, avec un cls qui contient l'objet.
La deuxième touche les valeurs modifiées sur la classe, un compteur par exemple. cls.compteur += 1 semble incrémenter un total unique : vrai tant que l'appel vient de la classe d'origine, faux dès qu'il vient d'une sous-classe.
cls.compteur += 1 lit le compteur du parent, mais écrit le résultat sur la sous-classe, qui se retrouve avec son propre compteur masquant l'autre. Les deux totaux divergent en silence. Pour un compteur commun, nommez la classe d'origine : Client.compteur += 1.
La troisième tient au choix de l'outil : ce qui n'a besoin ni de l'objet ni de la classe n'a rien à faire ici. Une simple fonction au niveau du module se lit aussi bien et se teste mieux.
Questions fréquentes
Peut-on l'appeler sur une instance ?
Oui, et le résultat est identique : Python remonte à la classe de l'objet avant de la passer dans cls. L'appel sur la classe reste préférable, car il annonce qu'aucune donnée de l'objet n'entre en jeu.
Quelle différence avec une staticmethod ?
La première reçoit la classe, la seconde ne reçoit rien. Dès qu'une méthode doit fabriquer un objet, lire une valeur définie sur la classe ou rester juste en héritage, c'est cls qu'il faut. Sinon la forme statique suffit, et elle dit plus clairement que la méthode ne dépend de rien.
Peut-elle modifier un attribut de classe ?
Oui, et c'est son second usage courant : cls.parametre = valeur change la valeur pour toutes les instances qui n'ont pas la leur, ce qui sert aux réglages globaux et aux caches. Souvenez-vous que l'écriture atterrit sur la classe appelée.