Classes ======= Les classes permettent des créer des objets appelés instances qui partagent des caractéristiques communes. Une classe est en fait un gabarit qui nous permet de créer un certain type d’objets. .. admonition:: Références * `Cours Zeste de Savoir d'Antoine Rozo `__ * `Documentation Python 3 `__ Structure d’une classe ---------------------- Les objets d’une classe partagent des caractéristiques communes à la classe: #. des attributs, des variables propres à aux instances; #. des méthodes, des fonctions propres aux instances et qui agissent par exemple sur leurs attributs. Définition d’une classe et création d’objets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pour définir une classe, la syntaxe est la suivante: .. code:: python3 class MaClasse: ... Lorsqu’on lance l’exécution d’un module Python, l’interpréteur exécute le contenu du corps de toutes les classes qu’il rencontre pour la première fois. Ainsi si l’on exécute : .. code:: python3 class Foo: print('bar') La console affichera ``bar``. Pour créer une instance de classe, il faut appeler la classe. Appeler une classe revient à appeler son , composé d’un créateur d’objet et d’un initialiseur. En reprenant la classe ``Foo``: .. code:: pycon >>> Foo # évaluation de la classe Foo >>> Foo() # création d'un objet Foo <__main__.Foo object at 0x7f193dd99630> Attributs ~~~~~~~~~ Créons un objet : .. code:: pycon >>> foo = Foo() Cet objet est pour l’instant vide, on peut lui donner un attribut: .. code:: pycon >>> foo.attr = 1 >>> foo.attr 1 Les informations concernant les attributs d’une instance sont stockées dans le dictionnaire de l’instance, un attribut spécial nommé ``__dict__``. .. code:: pycon >>> foo.__dict__ {'attr': 1} Lorsqu’on accède un attribut avec la syntaxe ``foo.attr``, cette syntaxe est par défaut équivalente à: .. code:: pycon >>> foo.__dict__['attr'] 1 Pour modifier la valeur d’un attribut, on lui assigne tout simplement une nouvelle valeur: .. code:: pycon >>> foo.attr = 456 >>> foo.attr 456 >>> foo.__dict__['attr'] = 789 # équivalent >>> foo.attr 789 Une classe peut également définir des attributs de classe: .. code:: python3 class Foo: class_attr = "I'm a class attribute." Toutes les instances y ont accès: .. code:: pycon >>> Foo.class_attr "I'm a class attribute." >>> foo = Foo() >>> foo.class_attr "I'm a class attribute." Pourtant, cet attribut n’est pas dans ``foo.__dict__``. En effet, lorsqu’on accède à un attribut, Python va le rechercher dans le dictionnaire de l’instance, mais aussi de sa classe s’il ne l’a pas trouvé. Ainsi: - Modifier un attribut de classe pour une instance ajoutera une entrée dans son dictionnaire (on n’accède ainsi par la suite plus à l’attribut de classe mais au nouvel attribut d’instance, on peut dire qu’on l’a surchargé). - Modifier un attribut de classe via la classe le modifie pour toutes les instances qui ne l’ont pas surchargé, ainsi que pour toutes les instances qui seront créées ensuite. .. code:: pycon >>> foo.class_attr = 'New value' >>> objet.__dict__ {'class_attr': 'New value'} >>> bar = Foo() >>> bar.__dict__ {} >>> Foo.class_attr = "I just got a new value." >>> bar.class_attr "I just got a new value." >>> foo.class_attr 'New value' Méthodes ~~~~~~~~ Les méthodes se définissent comme des fonctions dans le corps de la classe, elles agissent en général sur les instances de la classe. Python leur passe *toujours* l’instance sur laquelle elles sont appliquées en premier paramètre. Par convention, il est noté ``self``. .. code:: python3 class MaClasse: def methode(self, arg1, arg2): print(locals()) Ensuite on les appelle de la manière suivante: .. code:: pycon >>> objet = MaClasse() >>> objet <__main__.MaClasse object at 0x7f13337e50f0> >>> objet.methode(arg1, arg2) {'self': <__main__.MaClasse object at 0x7f13337e50f0>, 'arg1': 123, 'arg2': 'ABC'} C’est grâce à cette variable ``self`` que l’on peut agir sur l’instance. Initialiseur ~~~~~~~~~~~~ L’initialiseur est une méthode spéciale appelée ``__init__()``, il est appelé lorsqu’une instance vient d’être créée et permet d’en initialiser les attributs. L’exemple suivant permet d’initialiser deux attributs: .. code:: python3 class MaClasse: def __init__(self, att1, att2): """Initialiseur""" self.attribut1 = att1 self.attribut2 = att2 Ces deux attributs sont initialisés lorsqu’on crée un nouvel objet. Les valeurs initiales des attributs sont automatiquement passés à ``__init__()`` lorsqu’on appelle la classe pour instancier: .. code:: pycon >>> objet = MaClasse(123, 'ABC') >>> objet.__dict__ {'attribut1': 123, 'attribut2': 'ABC'} Héritage -------- Principe ~~~~~~~~ L’héritage est un moyen de créer des classes dérivées (classes filles) d’une classe de base (classe mère). Une classe fille hérite de toutes les méthodes et attributs de sa classe mère. Pour indiquer les classes parentes d’une nouvelle classe, on les indique en paramètres lors de sa définition. L’héritage en action dans un exemple on ne peut plus simple: .. code:: pycon >>> class Mere: ... attr = 1 ... >>> class Fille(Mere): ... pass ... >>> Fille().attr 1 Il est possible de surcharger (d’écraser) une méthode héritée en la redéfinissant dans la classe fille. Si on veut accéder à une méthode héritée alors qu’on l’a redéfinie dans la classe fille, on utilise la fonction ``super()`` qui permet d’appeler la méthode de la classe mère de la classe présente (sans l’argument ``self``). **Exemple :** .. code:: python3 class Meuble: def __init__(self, couleur, materiau): self.couleur = couleur self.materiau = materiau class Bibliotheque(Meuble): def __init__(self, couleur, materiau, n): super().__init__(couleur, materiau) self.nb_livres = n On peut utiliser deux fonctions pour vérifier l’héritage: ``isinstance`` renvoie ``True`` si l’objet est une instance de la classe ou de ses classes filles ; ``issubclass`` permet de voir si une classe est fille d’une autre. .. code:: pycon >>> bibli = Bibliotheque('blanc', 'vert', 150) >>> bibli.__dict__ {'couleur': 'blanc', 'materiau': 'vert', 'nb_livres': 150} >>> isinstance(bibli, Meuble) True >>> isinstance(bibli, Bibliotheque) True >>> issubclass(Bibliotheque, Meuble) True >>> issubclass(Meuble, Bibliotheque) False >>> isinstance(bibli, int) False >>> isinstance(bibli, object) True .. admonition:: Plus d'informations :class: seealso * `OpenClassrooms `__ * `Documentation Python 3 `__ * `Programiz `__ Ordre de résolution de méthode ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Classe mère ``object`` ~~~~~~~~~~~~~~~~~~~~~~ On a dit précédemment que le constructeur était composé d’un initialiseur et d’un créateur d’instance ; cependant l’exemple ne définissait pas de créateur d’instance : c’est parce qu’il est défini dans une classe ``object``. Toutes les classes en Python 3 héritent implicitement de cette classe. Elle définit de nombreuses méthodes, notamment les méthodes dites spéciales, que l’on peut surcharger pour les personnaliser. .. code:: pycon >>> object >>> help(object) Help on class object in module builtins: class object | The most base type >>> class Foo: ... pass ... >>> issubclass(Foo, object) True .. _sec:proprietes: Propriétés ---------- Les propriétés représentent en Python le principe d’encapsulation. Elles sont utiles si on souhaite contrôler l’accès à un attribut ou si on veut que le changement d’une valeur d’un attribut engendre des modifications sur d’autres attributs. Du côté utilisateur, ce mécanisme est complètement transparent car il permet de garder la syntaxe classique ``inst.attr = valeur``. Les propriétés sont un cas particulier des descripteurs. On crée les propriétés en utilisant des décorateurs. Elles contiennent un accesseur, un mutateur, un destructeur et une aide (docstring de l’accesseur). Dans certains cas, il n’est pas nécessaire d’avoir un attribut associé, un simple calcul suffit: .. code:: python3 class Temperature: def __init__(self, celsius): self.celsius = celsius @property def fahrenheit(self): """Propriété 'fahrenheit'.""" return self.celsius * 1.8 + 32 @fahrenheit.setter def fahrenheit(self, value): self.celsius = (value - 32) / 1.8 Ainsi, on peut écrire: .. code:: pycon >>> temp = Temperature(20) >>> temp.fahrenheit 68.0 >>> temp.fahrenheit = 69 >>> temp.celsius 20.555555555555554 Parfois, il est nécessaire d’ajouter des attributs cachés , par exemple si l’on veut aussi contrôler le changement de température en degrés Celsius, pour éviter une récursivité infinie: .. code:: python3 class Temperature: def __init__(self, celsius): self.celsius = celsius @property def celsius(self): """Propriété 'celsius'. Vérifie si la température est supérieure à -273°C avant d'assigner.""" return self._celsius @celsius.setter def celsius(self, value): if value < -273: raise ValueError("Une température en degrés Celsius doit être supérieure à -273°C.") self._celsius = value @property def fahrenheit(self): """Propriété 'fahrenheit'.""" return self.celsius * 1.8 + 32 @fahrenheit.setter def fahrenheit(self, value): self.celsius = (value - 32) / 1.8 Dans d’autres cas, on peut stocker le résultat de calculs dans un attribut caché. Ici, le calcul des degrés Fahrenheit est rapide, mais il peut s’avérer utile de stocker le résultat pour ne pas avoir à recalculer à chaque fois. On utilise la propriété de la manière suivante: .. code:: pycon >>> help(Temperature.celsius) Help on property: Propriété 'celsius'. Vérifie si la température est supérieure à -273°C avant d'assigner. >>> temp.celsius = -300 Traceback (most recent call last): File "", line 1, in File ".../*.py", line 18, in celsius raise ValueError("Une température en degrés Celsius doit être supérieure à -273°C.") ValueError: Une température en degrés Celsius doit être supérieure à -273°C. >>> temp.fahrenheit = -462 # ça marche aussi car cette propriété fait appel à celle des celsius ! Traceback (most recent call last): File "", line 1, in File ".../*.py", line 18, in celsius raise ValueError("Une température en degrés Celsius doit être supérieure à -273°C.") ValueError: Une température en degrés Celsius doit être supérieure à -273°C. .. admonition:: Plus d'informations :class: seealso * `Documentation Python 3 `__ * `Priorités entre propriété et méthodes spéciales (Stack Overflow) `__ Méthodes statiques et méthodes de classes ----------------------------------------- Méthode statique ~~~~~~~~~~~~~~~~ .. admonition:: Référence `Documentation Python 3 `__ Les méthodes que l’on a vues jusqu’à maintenant agissent sur les instances des classes : elles prennent toujours en premier argument le mot clé ``self`` qui correspond à l’instance elle même. Lorsque l’on appelle une telle méthode sur une instance comme ceci : ``instance.methode(*args, **kwargs)`` Python exécute en fait ``type(instance).methode(instance, *args, **kwargs)``. En fait, ces deux objets sont différents. ``Classe.methode`` est une simple fonction, alors que ``instance.methode`` est une méthode partiellement évaluée sur l’instance (méthode liée, en anglais bound method), c’est-à-dire que l’instance est mise en premier argument. Parfois, on écrit des méthodes qui n’ont pas d’incidence sur les instances de la classe. Si l'on reprend la classe ``Temperature``, on peut envisager d'externaliser le calcul de la température (ici le calcul est simple, mais on peut extrapoler l'exemple à des choses plus complexes). .. code:: python3 class Temperature: def __init__(self, celsius): self.celsius = celsius @property def celsius(self): """Propriété 'celsius'. Vérifie si la température est supérieure à -273°C avant d'assigner.""" return self._celsius @celsius.setter def celsius(self, value): if value < -273: raise ValueError("Une température en degrés Celsius doit être supérieure à -273°C.") self._celsius = value @property def fahrenheit(self): """Propriété 'fahrenheit'.""" return self.celsius_to_fahrenheit(self.celsius) @fahrenheit.setter def fahrenheit(self, value): self.celsius = self.fahrenheit_to_celsius(value) def celsius_to_fahrenheit(celsius): return celsius * 1.8 + 32 def fahrenheit_to_celsius(fahrenheit): return (fahrenheit - 32) / 1.8 Si l'on esssaie cette nouvelle définition, on fait face à une erreur : .. code:: pycon >>> t = Temperature(21) >>> t.fahrenheit TypeError: celsius_to_fahrenheit() takes 1 positional argument but 2 were given Le problème étant que Python a en fait exécuté : ``Temperature.celsius_to_fahrenheit(t, 21)`` Pour remédier à cela, il existe le décorateur ``@staticmethod``. Il va permettre d'indiquer à Python que cette méthode ne prend pas l'instance en premier paramètre. .. code:: python3 class Temperature: # ... @staticmethod def celsius_to_fahrenheit(celsius): return celsius * 1.8 + 32 @staticmethod def fahrenheit_to_celsius(fahrenheit): return (fahrenheit - 32) / 1.8 On peut ainsi écrire sans crainte : .. code:: pycon >>> t = Temperature(21) >>> t.fahrenheit 69.80000000000001 Méthode de classe ~~~~~~~~~~~~~~~~~ .. admonition:: Référence `Documentation Python 3 `__ Parfois, on veut pouvoir agir sur la classe et non sur l’instance. Dans ce cas, la méthode de classe prend en premier paramètre ``cls`` (la classe) au lieu de ``self`` (l’instance). .. code:: pycon >>> class Foo: ... CONSTANT = "Bar" ... def print_constant(cls): ... print(cls.CONSTANT) ... >>> Foo().print_constant() Bar Deux problèmes surviennent. Tout d’abord, même si le premier paramètre s’appelle ``cls``, c’est encore l’instance qui est mise en paramètre. .. code:: pycon >>> foo = Foo() >>> foo.CONSTANT = "Baz" >>> foo.print_constant() Baz # On veut Bar ! Deuxième problème: on ne peut pas appeler la méthode de classe sur la classe (même problème que pour les méthodes statiques): .. code:: pycon >>> Foo.print_constant() Traceback (most recent call last): File "", line 1, in TypeError: print_constant() missing 1 required positional argument: 'cls' Pour remédier à cela, on utilise le décorateur ``@classmethod``. .. code:: pycon >>> class Foo: ... CONSTANT = "Bar" ... @classmethod ... def print_constant(cls): ... print(cls.CONSTANT) ... >>> foo = Foo() >>> foo.CONSTANT = "Baz" >>> foo.print_constant() Bar # Youpi ! >>> Foo.print_constant() Bar # Youyoupi ! Cas de l’héritage ~~~~~~~~~~~~~~~~~ En résumé: #. Les méthodes statiques sont des fonctions reliées à des classes, mais qui n’agissent pas sur celles-ci. #. Les méthodes de classe sont des fonctions qui prennent la classe en paramètre. Une classe qui hérite d’une classe mère hérite de toutes les méthodes de celle-ci. Les méthodes statiques restent donc inchangées, tandis que les méthodes de classe s’adaptent à la nouvelle classe, car elles la prennent en premier argument. **Exemple :** Un exemple d’utilisation de méthodes statiques et de classe sont la création de constructeurs alternatifs. On s’aperçoit de la différence des deux notions. .. code:: python3 class Personne: def __init__(self, nom, age): self.nom = nom self.age = age @staticmethod def par_date_de_naissance(nom, date): return Personne(nom, 2018-date) @classmethod def par_date_de_naissance2(cls, nom, date): return cls(nom, 2018-date) class Homme(Personne): sexe = 'homme' .. code:: pycon >>> homme1 = Homme.par_date_de_naissance('Jean', 1997) >>> homme2 = Homme.par_date_de_naissance2('Jean', 1997) >>> type(homme1) >>> type(homme2) Pour avoir ``homme1`` de type ``Homme``, il faut redéfinir la méthode statique dans la classe fille. .. admonition:: Plus d'informations :class: seealso * `Méthode statique sur Programiz `__ * `Méthode de classe sur Programiz `__ * `StackOverflow `__