Python @staticmethod et @classmethod
Apprenez comment fonctionnent @staticmethod et @classmethod en Python, quand les utiliser et comment écrire des méthodes factory avec des exemples.
Python attribue à chaque méthode d'une classe l'un des trois styles de liaison : elle peut être liée à une instance, à la classe elle-même, ou à aucun des deux. Les décorateurs @classmethod et @staticmethod contrôlent ces deux derniers styles.
Ce chapitre couvre :
- Les trois types de méthodes et ce qui les distingue
@staticmethod— une fonction ordinaire stockée dans un espace de noms de classe@classmethod— une méthode qui reçoit la classe comme premier argument- Les méthodes factory : l'utilisation réelle la plus courante de
@classmethod - Les constructeurs alternatifs et leur interaction avec l'héritage
- Quand choisir
@staticmethodvs@classmethodvs une fonction au niveau du module - Les pièges courants
Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python et l'héritage Python. Pour l'accès aux attributs calculés, consultez @property. Pour une exploration approfondie du fonctionnement des décorateurs en général, consultez Python Decorators.
Les trois types de méthodes
Avant d'examiner chaque décorateur, voici une comparaison côte à côte :
| Méthode d'instance | @classmethod | @staticmethod | |
|---|---|---|---|
| Premier paramètre | self (l'instance) | cls (la classe) | aucun |
| Reçoit l'instance ? | Oui | Non | Non |
| Reçoit la classe ? | Via type(self) | Oui (directement) | Non |
| Appelée sur une instance | Oui | Oui | Oui |
| Appelée sur la classe | Oui (mais self est absent) | Oui | Oui |
| Utilisation typique | Opérer sur les données d'instance | Méthodes factory, état de classe | Fonctions utilitaires/auxiliaires |
class Demo:
def instance_method(self):
return f"instance method — self is {self}"
@classmethod
def class_method(cls):
return f"class method — cls is {cls}"
@staticmethod
def static_method():
return "static method — no self, no cls"
d = Demo()
print(d.instance_method()) # instance method — self is <__main__.Demo object at 0x...>
print(d.class_method()) # class method — cls is <class '__main__.Demo'>
print(d.static_method()) # static method — no self, no cls
# All three can also be called directly on the class:
print(Demo.class_method()) # class method — cls is <class '__main__.Demo'>
print(Demo.static_method()) # static method — no self, no cls@staticmethod
Une méthode statique est la plus simple des trois. C'est simplement une fonction ordinaire qui se trouve dans un espace de noms de classe. Python ne passe pas automatiquement self ou cls.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def is_even(n):
return n % 2 == 0
print(MathUtils.add(3, 4)) # 7
print(MathUtils.is_even(10)) # TrueQuand utiliser @staticmethod
Utilisez @staticmethod quand une fonction auxiliaire appartient logiquement à une classe — pour la clarté, le regroupement ou l'espace de noms — mais n'a pas besoin de lire ni de modifier l'état de l'instance ou de la classe :
- Fonctions de validation appelées avant la construction d'un objet.
- Fonctions de conversion ou de calcul pures qui n'ont de sens que dans le contexte d'une classe.
- Fonctions utilitaires utilisées par plusieurs méthodes de la même classe mais nulle part ailleurs.
class Temperature:
def __init__(self, celsius):
if not Temperature._is_valid(celsius):
raise ValueError(f"Temperature {celsius} °C is below absolute zero")
self.celsius = celsius
@staticmethod
def _is_valid(celsius):
return celsius >= -273.15
@staticmethod
def celsius_to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
t = Temperature(100)
print(Temperature.celsius_to_fahrenheit(100)) # 212.0
print(Temperature._is_valid(-300)) # FalseRemarquez que _is_valid est préfixée par _ pour signaler qu'elle est interne à la classe. Les appelants qui n'ont besoin que d'objets Temperature ne la voient jamais — ils obtiennent simplement une ValueError s'ils passent une valeur impossible.
@staticmethod vs une fonction au niveau du module
Une fonction au niveau du module et une @staticmethod sont presque identiques en termes de comportement. La différence réside dans l'endroit où vit la fonction :
- Si la fonction n'est pertinente que pour
Temperature(ou n'est appelée qu'exclusivement depuisTemperature), placez-la dans la classe en tant que@staticmethod. - Si c'est un utilitaire général utilisé dans tout votre module, placez-la au niveau du module.
Il n'y a aucune différence de performance. Il s'agit purement d'un choix organisationnel.
@classmethod
Une méthode de classe reçoit la classe comme premier argument (nommé cls par convention — mais tout comme self, le nom est une convention, pas un mot-clé). Parce qu'elle a une référence à la classe, elle peut :
- Lire ou modifier les attributs au niveau de la classe.
- Créer et retourner de nouvelles instances de la classe (méthodes factory).
- Fonctionner correctement avec les sous-classes (factories polymorphiques).
class Counter:
_count = 0 # class-level attribute
def __init__(self):
Counter._count += 1
@classmethod
def get_count(cls):
return cls._count
@classmethod
def reset(cls):
cls._count = 0
Counter()
Counter()
Counter()
print(Counter.get_count()) # 3
Counter.reset()
print(Counter.get_count()) # 0Méthodes factory — le cas d'utilisation le plus important
L'utilisation la plus courante et la plus précieuse de @classmethod est en tant que méthode factory (également appelée constructeur alternatif). Une méthode factory crée des instances à partir de différents types d'entrée sans encombrer __init__ d'une logique conditionnelle.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
"""Create a Date from an ISO 8601 string, e.g. '2024-03-15'."""
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day)
@classmethod
def from_tuple(cls, date_tuple):
"""Create a Date from a (year, month, day) tuple."""
return cls(*date_tuple)
d1 = Date(2024, 3, 15)
d2 = Date.from_string("2024-03-15")
d3 = Date.from_tuple((2024, 3, 15))
print(d1) # Date(2024, 3, 15)
print(d2) # Date(2024, 3, 15)
print(d3) # Date(2024, 3, 15)__init__ reste simple — il se contente de stocker trois entiers. Les méthodes de classe gèrent la logique de conversion. C'est plus propre qu'un __init__ unique avec plusieurs paramètres optionnels et des branches if/elif.
Pourquoi cls est important pour l'héritage
Quand une méthode factory de classe appelle cls(...) au lieu de coder en dur le nom de la classe, elle crée une instance de la classe sur laquelle la méthode a été appelée — même une sous-classe. C'est pourquoi vous devriez toujours préférer cls(...) à ClassName(...) à l'intérieur d'un @classmethod.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"{type(self).__name__}({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day) # uses cls, not Date
class DateTime(Date):
pass # inherits from_string
dt = DateTime.from_string("2024-03-15")
print(dt) # DateTime(2024, 3, 15) — correct subclass
print(type(dt)) # <class '__main__.DateTime'>Si from_string avait codé en dur return Date(year, month, day), appeler DateTime.from_string(...) retournerait un Date, pas un DateTime — brisant silencieusement le contrat d'héritage.
Modification de l'état au niveau de la classe
Les méthodes de classe peuvent également agir comme des constructeurs nommés avec effets de bord, ou elles peuvent manipuler des variables de classe qui suivent un état partagé :
class Registry:
_instances = []
def __init__(self, name):
self.name = name
Registry._instances.append(self)
@classmethod
def all(cls):
return list(cls._instances)
@classmethod
def clear(cls):
cls._instances.clear()
Registry("alice")
Registry("bob")
Registry("carol")
print([r.name for r in Registry.all()]) # ['alice', 'bob', 'carol']
Registry.clear()
print(Registry.all()) # []Appel depuis une instance vs depuis la classe
@staticmethod et @classmethod peuvent être appelées sur une instance ou sur la classe. Python gère les deux formes :
class Circle:
PI = 3.14159265
def __init__(self, radius):
self.radius = radius
def area(self):
return Circle.PI * self.radius ** 2
@classmethod
def unit_circle(cls):
"""Return a circle with radius 1."""
return cls(1)
@staticmethod
def describe():
return "A circle is a round plane figure."
c = Circle(5)
# staticmethod — callable on instance or class
print(c.describe()) # A circle is a round plane figure.
print(Circle.describe()) # A circle is a round plane figure.
# classmethod — callable on instance or class
unit = c.unit_circle()
print(unit.radius) # 1
print(Circle.unit_circle().radius) # 1Les appeler sur la classe est généralement plus clair — cela signale au lecteur qu'aucune donnée d'instance n'est impliquée.
Combiner @classmethod et @staticmethod
Une méthode de classe peut déléguer le travail de validation à une méthode statique, car la méthode de classe a accès à cls pour l'appeler :
class PositiveNumber:
def __init__(self, value):
self.value = value
def __repr__(self):
return f"PositiveNumber({self.value})"
@staticmethod
def _validate(value):
if value <= 0:
raise ValueError(f"Expected a positive number, got {value!r}")
@classmethod
def create(cls, value):
cls._validate(value)
return cls(value)
n = PositiveNumber.create(42)
print(n) # PositiveNumber(42)
try:
PositiveNumber.create(-5)
except ValueError as e:
print(e) # Expected a positive number, got -5Référence rapide : quel décorateur utiliser ?
| Situation | Recommandation |
|---|---|
La méthode lit ou écrit self | Méthode d'instance ordinaire |
| La méthode crée une nouvelle instance | @classmethod (factory / constructeur alternatif) |
| La méthode lit ou écrit un attribut de classe | @classmethod |
| La méthode est un auxiliaire pur qui n'a besoin ni de la classe ni de données d'instance | @staticmethod (ou fonction au niveau du module) |
| La méthode valide les entrées avant la construction | @staticmethod |
| La méthode doit fonctionner correctement dans les sous-classes | @classmethod (utiliser cls, pas le nom de classe codé en dur) |
Pièges courants
Oublier cls dans @classmethod
Si vous codez en dur le nom de la classe au lieu d'utiliser cls, l'héritage se rompt silencieusement :
class Animal:
@classmethod
def create(cls):
return cls() # correct — returns an instance of the actual class
class Dog(Animal):
pass
print(type(Dog.create())) # <class '__main__.Dog'> — correctUtilisez toujours cls(...), jamais Animal(...), à l'intérieur d'une méthode de classe.
Accéder à self ou cls dans un @staticmethod
Un @staticmethod ne reçoit aucun premier argument implicite. Tenter de référencer self ou cls à l'intérieur est une erreur :
class Bad:
label = "bad"
@staticmethod
def show():
# print(cls.label) # NameError: name 'cls' is not defined
print("use @classmethod if you need cls")
Bad.show() # use @classmethod if you need clsSi vous constatez que vous avez besoin de cls dans ce que vous pensiez être une méthode statique, passez-la en @classmethod.
Confondre les décorateurs
Les méthodes @classmethod doivent avoir cls comme premier paramètre explicite, et les méthodes @staticmethod ne doivent en avoir aucun. Les échanger provoque une TypeError au moment de l'appel, pas à la définition — ce qui peut être surprenant :
class Broken:
@staticmethod
def forgot_cls(cls): # cls is just a regular positional argument here
return cls
# Broken.forgot_cls() # TypeError: forgot_cls() missing 1 required positional argument: 'cls'Redéfinition dans les sous-classes
Les deux décorateurs fonctionnent avec super() et peuvent être redéfinis :
class Base:
@classmethod
def who(cls):
return f"Base.who called with cls={cls.__name__}"
class Child(Base):
@classmethod
def who(cls):
parent = super().who()
return f"Child.who — parent said: {parent}"
print(Child.who())
# Child.who — parent said: Base.who called with cls=ChildRemarquez que cls dans Base.who est toujours Child — car la méthode a été dispatchée depuis Child.
Exemple concret : une classe User
Voici un exemple complet qui réunit les méthodes d'instance, une factory de méthode de classe et un validateur de méthode statique :
import re
class User:
_all_users = []
def __init__(self, name, email):
User._validate_email(email)
self.name = name
self.email = email
User._all_users.append(self)
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r})"
# --- instance method ---
def greet(self):
return f"Hello, my name is {self.name}."
# --- factory / alternative constructor ---
@classmethod
def from_dict(cls, data):
"""Create a User from a dict like {'name': 'Alice', 'email': '[email protected]'}."""
return cls(data["name"], data["email"])
# --- class-level query ---
@classmethod
def count(cls):
return len(cls._all_users)
# --- pure helper, no instance or class data needed ---
@staticmethod
def _validate_email(email):
pattern = r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, email):
raise ValueError(f"Invalid email address: {email!r}")
# Create via normal constructor
u1 = User("Alice", "[email protected]")
# Create via factory
u2 = User.from_dict({"name": "Bob", "email": "[email protected]"})
print(u1.greet()) # Hello, my name is Alice.
print(u2.greet()) # Hello, my name is Bob.
print(User.count()) # 2
try:
User("Carol", "not-an-email")
except ValueError as e:
print(e) # Invalid email address: 'not-an-email'Ce pattern — __init__ pour la construction normale, @classmethod pour les constructeurs alternatifs, @staticmethod pour les fonctions auxiliaires — apparaît tout au long de la bibliothèque standard Python (voir datetime.date.today(), datetime.date.fromisoformat(), int.from_bytes()).
Résumé
- Une méthode d'instance reçoit
selfet a un accès complet à l'état de l'objet. - Un
@classmethodreçoitcls— la classe elle-même — à la place d'une instance. Utilisez-le pour les méthodes factory et tout ce qui opère sur l'état au niveau de la classe. Utilisez toujourscls(...)à l'intérieur afin que les sous-classes fonctionnent correctement. - Un
@staticmethodne reçoit niselfnicls. Utilisez-le pour la logique utilitaire pure qui appartient à l'espace de noms de la classe mais n'a besoin d'aucune donnée d'objet ou de classe.
Pour les attributs calculés qui ressemblent à un accès d'attribut ordinaire, consultez @property. Pour le mécanisme complet des décorateurs qui fait fonctionner les trois, consultez Python Decorators.