W3docs

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 @staticmethod vs @classmethod vs 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ètreself (l'instance)cls (la classe)aucun
Reçoit l'instance ?OuiNonNon
Reçoit la classe ?Via type(self)Oui (directement)Non
Appelée sur une instanceOuiOuiOui
Appelée sur la classeOui (mais self est absent)OuiOui
Utilisation typiqueOpérer sur les données d'instanceMéthodes factory, état de classeFonctions 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)) # True

Quand 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))             # False

Remarquez 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 depuis Temperature), 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())  # 0

Mé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)  # 1

Les 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 -5

Référence rapide : quel décorateur utiliser ?

SituationRecommandation
La méthode lit ou écrit selfMé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'>  — correct

Utilisez 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 cls

Si 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=Child

Remarquez 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 self et a un accès complet à l'état de l'objet.
  • Un @classmethod reçoit cls — 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 toujours cls(...) à l'intérieur afin que les sous-classes fonctionnent correctement.
  • Un @staticmethod ne reçoit ni self ni cls. 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.

Was this page helpful?