1766 votes

Signification de @classmethod et @staticmethod pour débutant

Que signifient @classmethod et @staticmethod en Python, et en quoi diffèrent-ils? Quand devrais-je les utiliser, pourquoi devrais-je les utiliser, et comment devrais-je les utiliser?

D'après ce que je comprends, @classmethod indique à une classe qu'il s'agit d'une méthode qui doit être héritée dans les sous-classes, ou... quelque chose comme ça. Cependant, quel est l'intérêt de cela? Pourquoi ne pas simplement définir la méthode de classe sans ajouter @classmethod ou @staticmethod ou toute autre définition @?

2776voto

Rostyslav Dzinko Points 10667

Bien que classmethod et staticmethod soient assez similaires, il y a une légère différence dans l'utilisation de ces deux entités : classmethod doit avoir une référence à un objet de classe comme premier paramètre, tandis que staticmethod peut ne pas avoir de paramètres du tout.

Exemple

class Date(object):

    def __init__(self, day=0, month=0, year=0):
        self.day = day
        self.month = month
        self.year = year

    @classmethod
    def from_string(cls, date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        date1 = cls(day, month, year)
        return date1

    @staticmethod
    def is_date_valid(date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        return day <= 31 and month <= 12 and year <= 3999

date2 = Date.from_string('11-09-2012')
is_date = Date.is_date_valid('11-09-2012')

Explication

Prenons un exemple d'une classe qui traite des informations de date (cela sera notre modèle de base) :

class Date(object):

    def __init__(self, day=0, month=0, year=0):
        self.day = day
        self.month = month
        self.year = year

Cette classe pourrait clairement être utilisée pour stocker des informations sur certaines dates (sans information de fuseau horaire ; supposons que toutes les dates sont présentées en UTC).

Ici, nous avons __init__, un initialiseur typique des instances de classe Python, qui reçoit des arguments comme une méthode d'instance typique, ayant le premier argument non optionnel (self) qui contient une référence à une instance nouvellement créée.

Méthode de classe

Nous avons certaines tâches qui peuvent être bien accomplies en utilisant des classmethod.

Supposons que nous voulons créer beaucoup d'instances de classe Date à partir d'informations de date provenant d'une source externe encodée sous forme de chaîne avec le format 'jj-mm-aaaa'. Supposons que nous devons le faire à différents endroits dans le code source de notre projet.

Nous devons donc faire ceci :

  1. Parser une chaîne pour recevoir le jour, le mois et l'année sous forme de trois variables entières ou un tuple à 3 éléments comprenant cette variable.
  2. Instancier Date en transmettant ces valeurs à l'appel d'initialisation.

Cela ressemblera à ceci :

jour, mois, annee = map(int, chaine_date.split('-'))
date1 = Date(jour, mois, annee)

À cette fin, C++ peut implémenter une telle fonctionnalité avec une surcharge, mais Python ne possède pas cette surcharge. À la place, nous pouvons utiliser classmethod. Créons un autre constructeur.

    @classmethod
    def from_string(cls, date_as_string):
        jour, mois, annee = map(int, date_as_string.split('-'))
        date1 = cls(jour, mois, annee)
        return date1

date2 = Date.from_string('11-09-2012')

Étudions de plus près la mise en œuvre ci-dessus, et examinons quels avantages nous avons ici :

  1. Nous avons implémenté l'analyse de chaîne de date en un seul endroit et elle est maintenant réutilisable.
  2. L'encapsulation fonctionne bien ici (si vous pensez que vous pourriez implémenter l'analyse de chaîne comme une seule fonction ailleurs, cette solution correspond beaucoup mieux au paradigme OOP).
  3. cls est la classe elle-même, pas une instance de la classe. C'est plutôt cool car si nous héritons de notre classe Date, tous les enfants auront également from_string défini.

Méthode statique

Et que dire de staticmethod? C'est assez similaire à classmethod, mais ne prend aucun paramètre obligatoire (comme le fait une méthode de classe ou une méthode d'instance).

Regardons le cas d'utilisation suivant.

Nous avons une chaîne de date que nous voulons valider d'une certaine manière. Cette tâche est également logiquement liée à la classe Date que nous avons utilisée jusqu'à présent, mais ne nécessite pas son instanciation.

C'est là que staticmethod peut être utile. Examinons le morceau de code suivant :

    @staticmethod
    def is_date_valid(date_as_string):
        jour, mois, annee = map(int, date_as_string.split('-'))
        return jour <= 31 and mois <= 12 and annee <= 3999

# utilisation :
is_date = Date.is_date_valid('11-09-2012')

Ainsi, comme nous pouvons le voir à partir de l'utilisation de staticmethod, nous n'avons pas accès à ce qu'est la classe --- c'est fondamentalement juste une fonction, appelée syntaxiquement comme une méthode, mais sans accès à l'objet et à ses éléments internes (champs et autres méthodes), auxquels classmethod a accès.

3 votes

La méthode "from_string" prend la classe "Date" (et non pas l'objet Date) en premier paramètre "cls" et renvoie le constructeur en appelant cls(day, month, year), ce qui équivaut à Date(day, month, year) et renvoie un objet Date.

0 votes

Peut-on considérer les méthodes de classe comme des "constructeurs alternatifs"? Je pense que c'est la meilleure explication jusqu'à présent!

4 votes

En ce qui concerne votre exemple de classmethod, ne seriez-vous pas en mesure de faire exactement ce que vous avez fait ci-dessus en définissant from_string comme un stringmethod au lieu de le définir comme classmethod, et alors, au lieu d'appeler cls(jour, mois, annee) vous appelleriez Date(jour, mois, annee)? Je suppose que le seul avantage d'utiliser un classmethod ici serait si vous souhaitez que cette méthode soit disponible pour des classes filles qui peuvent hériter et s'attendre à ce que from_string fonctionne pour la classe héritée, n'est-ce pas? Ou ai-je manqué quelque chose dans votre explication?

931voto

Yaw Points 2210

La réponse de Rostyslav Dzinko est très appropriée. Je pensais pouvoir souligner une autre raison pour laquelle vous devriez choisir @classmethod plutôt que @staticmethod lorsque vous créez un constructeur supplémentaire.

Dans l'exemple, Rostyslav a utilisé le @classmethod from_string en tant que Factory pour créer des objets Date à partir de paramètres autrement inacceptables. La même chose peut être faite avec @staticmethod comme le montre le code ci-dessous :

class Date:
  def __init__(self, month, day, year):
    self.month = month
    self.day   = day
    self.year  = year

  def display(self):
    return "{0}-{1}-{2}".format(self.month, self.day, self.year)

  @staticmethod
  def millenium(month, day):
    return Date(month, day, 2000)

new_year = Date(1, 1, 2013)               # Crée un nouvel objet Date
millenium_new_year = Date.millenium(1, 1) # crée également un objet Date

# Preuve :
new_year.display()           # "1-1-2013"
millenium_new_year.display() # "1-1-2000"

isinstance(new_year, Date) # True
isinstance(millenium_new_year, Date) # True

Ainsi, à la fois new_year et millenium_new_year sont des instances de la classe Date.

Cependant, si vous observez attentivement, le processus Factory est codé en dur pour créer des objets Date quoi qu'il arrive. Cela signifie que même si la classe Date est sous-classée, les sous-classes créeront quand même des objets Date simples (sans aucune propriété de la sous-classe). Voyez cela dans l'exemple ci-dessous :

class DateTime(Date):
  def display(self):
      return "{0}-{1}-{2} - 00:00:00PM".format(self.month, self.day, self.year)

datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10)

isinstance(datetime1, DateTime) # True
isinstance(datetime2, DateTime) # False

datetime1.display() # retourne "10-10-1990 - 00:00:00PM"
datetime2.display() # retourne "10-10-2000" car ce n'est pas un objet DateTime mais un objet Date. Vérifiez l'implémentation de la méthode millenium sur la classe Date pour plus de détails.

datetime2 n'est pas une instance de DateTime? WTF? Eh bien, c'est à cause du décorateur @staticmethod utilisé.

Dans la plupart des cas, c'est indésirable. Si ce que vous voulez est une méthode Factory consciente de la classe qui l'a appelée, alors @classmethod est ce dont vous avez besoin.

En réécrivant Date.millenium comme (c'est la seule partie du code ci-dessus qui change) :

@classmethod
def millenium(cls, month, day):
    return cls(month, day, 2000)

assure que la classe n'est pas codée en dur mais plutôt apprise. cls peut être n'importe quelle sous-classe. L'objet résultant sera à juste titre une instance de cls.
Testons ça :

datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10)

isinstance(datetime1, DateTime) # True
isinstance(datetime2, DateTime) # True

datetime1.display() # "10-10-1990 - 00:00:00PM"
datetime2.display() # "10-10-2000 - 00:00:00PM"

La raison est, comme vous le savez maintenant, que @classmethod a été utilisé au lieu de @staticmethod.

30 votes

Dire que c'est en fait une méthode de fabrique était la chose la plus utile pour expliquer à quoi sert @classmethod.

0 votes

Qu'est-ce que cls? Comme réponse ci-dessus - cls peut être n'importe quelle sous-classe. L'objet résultant sera alors une instance de cls. Est-ce que cls est un objet ou une méthode appelant Date ou Datetime? Veuillez expliquer.

0 votes

@rishijain Comme self se réfère à l'instance, cls se réfère à la classe - par ex. cls(mois, jour, 2000) == DateTime(mois, jour, 2000)

272voto

Simeon Visser Points 30697

@classmethod signifie : quand cette méthode est appelée, nous passons la classe en premier argument au lieu de l'instance de cette classe (comme nous le faisons normalement avec les méthodes). Cela signifie que vous pouvez utiliser la classe et ses propriétés à l'intérieur de cette méthode plutôt qu'une instance particulière.

@staticmethod signifie : quand cette méthode est appelée, nous ne lui passons pas une instance de la classe (comme nous le faisons normalement avec les méthodes). Cela signifie que vous pouvez mettre une fonction à l'intérieur d'une classe mais vous ne pouvez pas accéder à l'instance de cette classe (ceci est utile lorsque votre méthode n'utilise pas l'instance).

0 votes

Voici le tl;dr que je voulais

109voto

zangw Points 401

Quand utiliser chaque

@staticmethod fonction n'est rien de plus qu'une fonction définie à l'intérieur d'une classe. Il est appelable sans instancier la classe en premier. Sa définition est immuable via l'héritage.

  • Python n'a pas besoin d'instancier une méthode liée pour l'objet.
  • Cela facilite la lisibilité du code : en voyant @staticmethod, nous savons que la méthode ne dépend pas de l'état de l'objet lui-même ;

@classmethod fonction également appelable sans instancier la classe, mais sa définition suit la sous-classe, pas la classe parent, via l'héritage, peut être remplacée par une sous-classe. Cela est dû au fait que le premier argument pour la fonction @classmethod doit toujours être cls (classe).

  • Méthodes de fabrique, qui sont utilisées pour créer une instance pour une classe en utilisant par exemple un certain type de prétraitement.
  • Méthodes statiques appelant des méthodes statiques : si vous divisez des méthodes statiques en plusieurs méthodes statiques, vous ne devriez pas codifier en dur le nom de la classe mais utiliser des méthodes de classe.

ici est un bon lien sur ce sujet.

7 votes

Merci d'aller droit au but beaucouuup plus rapidement que la réponse acceptée.

58voto

Aaron Hall Points 7381

Sens de @classmethod et @staticmethod?

  • Une méthode est une fonction dans l'espace de noms d'un objet, accessible en tant qu'attribut.
  • Une méthode régulière (c'est-à-dire d'instance) reçoit l'instance (que nous appelons généralement self) en tant que premier argument implicite.
  • Une méthode de classe reçoit la classe (que nous appelons généralement cls) en tant que premier argument implicite.
  • Une méthode statique n'a pas d'argument implicite (comme une fonction régulière).

quand devrais-je les utiliser, pourquoi devrais-je les utiliser et comment devrais-je les utiliser?

Vous n'avez pas besoin de ces décorateurs. Mais selon le principe que vous devriez minimiser le nombre d'arguments aux fonctions (voir Clean Coder), ils sont utiles pour faire exactement cela.

class Example(object):

    def regular_instance_method(self):
        """Une fonction d'une instance a accès à chaque attribut de cette instance, y compris sa classe (et ses attributs).
        Ne pas accepter au moins un argument est une TypeError.
        Ne pas comprendre la sémantique de cet argument est une erreur de l'utilisateur.
        """
        return some_function_f(self)

    @classmethod
    def a_class_method(cls):
        """Une fonction d'une classe a accès à chaque attribut de la classe.
        Ne pas accepter au moins un argument est une TypeError.
        Ne pas comprendre la sémantique de cet argument est une erreur de l'utilisateur.
        """
        return some_function_g(cls)

    @staticmethod
    def a_static_method():
        """Une méthode statique n'a aucune information sur les instances ou les classes 
        à moins qu'elles ne soient explicitement données. Elle existe simplement dans l'espace de noms de la classe (et donc de ses instances).
        """
        return some_function_h()

Pour les méthodes d'instance et les méthodes de classe, ne pas accepter au moins un argument est une TypeError, mais ne pas comprendre la sémantique de cet argument est une erreur de l'utilisateur.

(Définissez les fonctions some_function, par exemple:

some_function_h = some_function_g = some_function_f = lambda x=None: x

et cela fonctionnera.)

consultations par points sur les instances et les classes :

Une consultation par point sur une instance se fait dans cet ordre - nous cherchons:

  1. un descripteur de données dans l'espace de noms de la classe (comme une propriété)
  2. des données dans l'instance __dict__
  3. un descripteur non-données dans l'espace de noms de la classe (méthodes)

Remarquez, une consultation par point sur une instance est invoquée comme suit :

instance = Example()
instance.regular_instance_method

et les méthodes sont des attributs appelables :

instance.regular_instance_method()

méthodes d'instance

L'argument self est implicitement donné via la consultation par point.

Vous devez accéder aux méthodes d'instance à partir des instances de la classe.

>>> instance = Example()
>>> instance.regular_instance_method()
<__main__.Example object at 0x00000000399524E0>

méthodes de classe

L'argument cls est implicitement donné via la consultation par point.

Vous pouvez accéder à cette méthode via une instance ou la classe (ou les sous-classes).

>>> instance.a_class_method()

>>> Example.a_class_method()

méthodes statiques

Aucun argument n'est implicitement donné. Cette méthode fonctionne comme n'importe quelle fonction définie (par exemple) dans l'espace de noms de modules, sauf qu'elle peut être consultée

>>> print(instance.a_static_method())
None

Encore une fois, quand devrais-je les utiliser, pourquoi devrais-je les utiliser?

Chacun de ces éléments transmet progressivement moins d'informations à la méthode par rapport aux méthodes d'instance.

Utilisez-les lorsque vous n'avez pas besoin de l'information.

Cela rend vos fonctions et méthodes plus faciles à comprendre et à tester unitairement.

Laquelle est plus facile à comprendre?

def fonction(x, y, z): ...

ou

def fonction(y, z): ...

ou

def fonction(z): ...

Les fonctions avec moins d'arguments sont plus faciles à comprendre. Elles sont également plus faciles à tester unitairement.

Cela ressemble aux méthodes d'instance, de classe et statiques. En gardant à l'esprit que lorsque nous avons une instance, nous avons également sa classe, interrogez-vous de nouveau, laquelle est plus facile à comprendre?:

def une_methode_d_instance(self, arg, kwarg=None):
    cls = type(self)             # A également la classe de l'instance!
    ...

@classmethod
def une_methode_de_classe(cls, arg, kwarg=None):
    ...

@staticmethod
def une_methode_statique(arg, kwarg=None):
    ...

Exemples intégrés

Voici quelques-uns de mes exemples intégrés préférés :

La méthode statique str.maketrans était une fonction dans le module string, mais il est beaucoup plus pratique qu'elle soit accessible depuis l'espace de noms de str.

>>> 'abc'.translate(str.maketrans({'a': 'b'}))
'bbc'

La méthode de classe dict.fromkeys retourne un nouveau dictionnaire instancié à partir d'un itérable de clés:

>>> dict.fromkeys('abc')
{'a': None, 'c': None, 'b': None}

En cas de sous-classe, nous voyons qu'elle obtient les informations de classe en tant que méthode de classe, ce qui est très utile :

>>> class MyDict(dict): pass
>>> type(MyDict.fromkeys('abc'))

Mon conseil - Conclusion

Utilisez les méthodes statiques lorsque vous n'avez pas besoin des arguments de classe ou d'instance, mais que la fonction est liée à l'utilisation de l'objet, et qu'il est pratique que la fonction soit dans l'espace de noms de l'objet.

Utilisez les méthodes de classe lorsque vous n'avez pas besoin d'informations sur l'instance, mais avez besoin des informations sur la classe peut-être pour ses autres méthodes de classe ou statiques, ou peut-être pour lui-même en tant que constructeur. (Vous ne voudriez pas coder en dur la classe pour que des sous-classes puissent être utilisées ici.)

3 votes

C'était incroyablement clair avec juste le bon équilibre d'explications succinctes et de micro-exemples.

7 votes

Utilisez les méthodes de classe lorsque vous n'avez pas besoin d'informations d'instance, mais avez besoin des informations de la classe peut-être pour ses autres méthodes de classe ou statiques, ou peut-être pour elle-même en tant que constructeur. (Vous ne coderiez pas la classe en dur pour que les sous-classes puissent être utilisées ici.) Précisément ce que je cherchais. La véritable réponse pourquoi je l'utiliserais.

0 votes

Ceci est la meilleure explication. Merci!

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X