Syntaxe POO en Python
Temps de lecture10 minComme nous l’avons vu dans le cours d’introduction aux concepts de la POO, une classe est une sorte de modèle pour créer des objets. C’est dans la classe que nous définirons nos méthodes et attributs.
La POO est particulièrement utile lorsque vous devez représenter des données un peu plus complexes qu’un simple nombre ou une chaîne de caractères. Bien sûr, il existe des classes que chaque langage de programmation (y compris Python) définit pour nous : les nombres, les chaînes et les listes en font partie. Mais nous serions limités si nous ne pouvions pas créer nos propres classes.
Définition de classe
Pour introduire la syntaxe, créons une première classe très simple représentant une personne (classe Person
). Pour nous ici, une personne est caractérisée par le nom de famille, le prénom, l’âge et le lieu de résidence (ville). La classe aura donc 4 attributs et un constructeur.
La création de cette classe correspond au code ci-dessous.
class Person:
""" Represents a person with a name, firstname, age, and city.
"""
def __init__(self, name: str, firstname: str, age: int, city: str): # The constructor
""" Constructs a new Person object with the specified name, firstname, age, and city.
"""
self.name = name
self.firstname = firstname
self.age = age
self.city = city
Examinons la syntaxe en détail :
-
La classe est définie par le mot-clé
class
, suivi du nom de la classe et des deux-points rituels:
. Par convention, les noms de classe commencent par une majuscule. -
Une
docstring
commentant la classe. C’est optionnel, mais fortement recommandé. Elle est placée juste après le nom de la classe et entourée de trois guillemets"""
. -
La définition du constructeur. Le nom d’un constructeur est toujours
__init__
et le premier paramètreself
. Nous verrons plus tard ce que cela signifie. Les autres paramètres sont séparés par,
comme avec les fonctions en programmation procédurale. -
Le constructeur contient la déclaration et l’initialisation des attributs de la classe. Ici, une personne est créée avec un nom, un prénom, un âge et la ville où elle vit.
Créez un fichier person.py
et copiez le code ci-dessus. Si vous essayez de l’exécuter, rien ne se passe, le code Java lève même une exception… Vous venez de définir la classe, nous devons l’instancier avant de pouvoir l’utiliser.
Instanciation de classe : créer des objets
Nous voulons créer deux personnes.
if __name__ == '__main__':
# Create a person named Alice Weber from London and 33 years old
one_person = Person("Weber", "Alice", 33, "London")
print(one_person.to_string())
# Create a person named Bob Smith from Brighton and 25 years old
a_second_person = Person("Smith", "Bob", 25, "Brighton")
print(a_second_person)
Copiez/collez le code ci-dessus à la fin du fichier person.py
en prenant garde à l’indentation. La condition
if __name__ == '__main__':
doit se trouver le plus à gauche possible. Exécutez-le. Maintenant, vous pouvez voir quelque chose de peu compréhensible… Ici, le plus important est de voir la classe d’où vient l’objet. Nous pouvons donc vérifier que les deux objets proviennent bien de notre classe Person
. Vous pouvez ajouter ce genre de tests de fonctionnement dans chaque classe en attendant que l’on vous présente une méthode plus élégante pour tester chaque fonctionnalité de votre classe.
Nous allons maintenant ajouter une méthode qui retourne une chaîne représentant une personne, et nous l’appellerons après avoir instancié la classe.
class Person:
""" Represents a person with a name, firstname, age, and city.
"""
def __init__(self, name: str, firstname: str, age: int, city: str): # The constructor
""" Constructs a new Person object with the specified name, firstname, age, and city.
"""
self.name = name
self.firstname = firstname
self.age = age
self.city = city
def to_string(self) -> str:
""" Returns a string representation of the person.
"""
return f"{self.firstname} {self.name} ({self.age} old, from {self.city})"
if __name__ == '__main__':
one_person = Person("Weber", "Alice", 33, "London")
print(one_person.to_string())
a_second_person = Person("Smith", "Bob", 25, "Brighton")
print(a_second_person.to_string())
Comme vous pouvez le voir dans le code ci-dessus, la définition d’une méthode dans une classe est similaire à la définition d’une fonction classique, à part l’existence du mot-clé self
comme premier argument de la méthode. L’appel de méthode se fait en précédant le nom de la méthode par l’objet sur lequel la méthode doit être appelée. Dans notre exemple, one_person.to_string()
exécute le code de la méthode to_string()
de l’objet one_person
.
Python offre ce qu’on appelle des méthodes spéciales. Ce sont des méthodes que Python reconnaît et sait comment utiliser. Le nom d’une telle méthode prend une syntaxe spéciale : __methodspeciale__
. En fait, vous en connaissez déjà une, __init__
. Une autre méthode de ce type est __str__
(qui ne prend aucun argument à part self
comme toute autre méthode), qui est appelée par Python pour afficher un objet. Vous pouvez tester ceci en ajoutant la méthode __str__(self)
à notre classe Person
et en utilisant print(one_person)
pour voir le résultat.
De même, la méthode __repr__
est appelée par Python pour afficher un objet dans la console. Vous pouvez tester ceci en ajoutant la méthode __repr__(self)
à notre classe Person
et en utilisant one_person
pour voir le résultat.
__str__
est utilisée pour afficher l’objet à l’utilisateur de manière lisible.
__repr__
fournit une représentation complète sous forme de chaîne de l’objet, qui est utile pour le débogage, la journalisation et le développement. Ainsi, elle est plus pour les développeurs que pour les utilisateurs.
Maintenant nous pouvons parler du mot-clé self
. Quand vous créez un objet, dans ce cas une personne, les valeurs de ses attributs lui sont propres (la valeur de l’attribut name
de one_person
n’est pas la même que celle de a_second_person
). C’est logique, car chaque personne a son propre nom, etc. Donc quand vous appelez une méthode depuis un objet, il est important de savoir sur quel objet elle doit être exécutée et c’est le rôle du mot-clé self
.
Encapsulation
Enfin, examinons le concept d’encapsulation. Pour rappel, ce concept consiste à protéger ou cacher certains attributs d’un objet.
En Python, par défaut, la valeur de l’attribut d’un objet est directement accessible : il suffit d’écrire my_object.my_attribute
et c’est fait (essayez le code ci-dessous dans votre fichier person.py
). Il y a deux stratégies pour rechercher l’encapsulation des attributs :
-
utiliser des identifiants d’attributs sous la forme
_my_attribute
. Ce _single_leading_underscore est juste une convention utilisée en Python pour indiquer des attributs qui ne sont pas supposés être lus ou écrits depuis l’extérieur de la classe. Mais ce n’est qu’une convention ! rien ne les empêche d’être utilisés directement en dehors de la classe. Vous pouvez lire la documentation pour plus d’informations. -
utiliser des identifiants d’attributs sous la forme
__my_attribute
. Cela renforce l’encapsulation, car la valeur de tels attributs ne peut pas être directement accessible depuis l’extérieur de la classe. Cependant, l’encapsulation n’est pas vraiment implémentée car Python transforme ces noms avec le nom de la classe :__my_attribute
dans la classeClass
peut être accessible depuis l’objeto
en appelanto._Class__my_attribute
.
Voyons en pratique pour notre classe Person
.
class Person:
""" Represents a person with a name, firstname, age, and city.
"""
def __init__(self, name: str, firstname: str, age: int, city: str): # The constructor
""" Constructs a new Person object with the specified name, firstname, age, and city.
"""
self.name = name
self.firstname = firstname
self.__age = age
self._city = city
def to_string(self) -> str:
""" Returns a string representation of the person.
"""
return f"{self.firstname} {self.name} ({self.__age} old, from {self._city})"
if __name__ == '__main__':
one_person = Person("Weber", "Alice", 33, "London")
print(one_person.name) # No error. Expected output: "Weber"
print(one_person._city) # No error. Expected output: "London"
print(one_person.__age) # Raises an error: AttributeError: 'Person' object has no attribute '__age'
print(one_person._Person__age) # No error. Expected output: 33
Les formes avec un et deux traits de soulignement en début peuvent aussi être utilisées lors de la nomination de méthodes pour indiquer des méthodes non-publiques. Voir la documentation pour plus d’informations.
Héritage
L’héritage est une façon de structurer le code qui définit des relations hiérarchiques entre les classes. Pour illustrer comment créer des classes qui héritent d’une autre, supposons que nous voulions créer un programme qui manipule des chiens et des chats. Les deux ont un surnom et nous voulons suivre le nombre de repas qu’ils ont mangés depuis leur naissance. Cependant, les chiens aboient et les chats miaulent et nous voulons suivre le poids d’un chien.
Comme les chiens et les chats ont des caractéristiques communes (surnom et nombre de repas), nous décidons de créer une classe Animal
avec deux attributs (un pour stocker le nom de l’animal et l’autre pour compter le nombre de fois qu’il a mangé) et une méthode, exécutée chaque fois que l’animal mange quelque chose.
Ci-dessous vous trouverez le code de la classe Animal
.
# Here, we create a class "Animal"
"""
Represents an animal with a nickname and the number of meals it has eaten.
"""
class Animal ():
# Classes have a special method "__init__" called a constructor.
# This is the code that will be executed when you instantiate an animal later.
# All methods of a class should the keyword "self" as first argument.
# This keyword references the object itself when the class is instantiated.
def __init__ (self, name: str):
"""
Constructs a new Animal object with the specified nickname.
name: the animal's nickname
"""
self.nickname = name # Create an attribute to store the name
self.__nb_meals = 0 # Create an attribute to count how many meals the animal had
# Now, let's define methods that do custom stuff
def get_nickname (self) -> str:
"""
This method returns the nickname of the animal.
"""
return self.nickname
def eat_stuff (self, stuff_name: str):
"""
This method is called when the animal eats something.
stuff_name: the name of the stuff the animal is eating
"""
print("Yum, tasty", stuff_name)
self.__nb_meals += 1
def get_nb_meals(self) -> int:
"""
This method returns the number of meals the animal has eaten.
"""
return self.__nb_meals
Maintenant, comment les chiens et les chats sont-ils réifiés dans notre programme ? D’un côté, en tant qu’animal, un chien devrait avoir un surnom et le nombre de fois qu’il a mangé mais un chien a aussi un poids et peut aboyer. D’un autre côté, un chat est aussi un animal mais il miaule aussi. Par conséquent, nous allons implémenter les classes Dog
et Cat
comme enfants de la classe Animal
pour qu’elles aient automatiquement accès à ses attributs et méthodes. La classe Dog
aura un nouvel attribut (weight
) et une nouvelle méthode (bark
) et la classe Cat
fournira une méthode meow
. Le code pour ces classes est présenté ci-dessous.
# Here, we create a class "Dog"
"""
Represents a dog with a nickname, the number of meals it has eaten, and its weight.
A Dog inherits from an Animal.
"""
class Dog (Animal):
# It is good practice to also call the parent's constructor
# Then you can complement with additional codes if needed
# It is also good practice not to repeat arguments of the parent class
# Arguments *args and **kwargs are here for this purpose
def __init__ (self, size, *args, **kwargs):
"""
Constructs a new Dog object with the specified nickname and weight.
size: the dog's weight
"""
super().__init__(*args, **kwargs) # Call parent's constructor
self.weight = weight # Create an attribute to store the weight
# The weight of a dog can be accessed
def get_weight (self) -> float:
"""
This method returns the weight of the dog.
"""
return self.weight
# Now, let's define a new method
# A dog can bark
def bark (self):
"""
This method is called when the dog barks.
"""
print("Woof")
# Here, we create a class "Cat"
"""
Represents a cat with a nickname and the number of meals it has eaten.
A Cat inherits from an Animal.
"""
class Cat (Animal):
# It is good practice to also call the parent's constructor
# Then you can complement with additional codes if needed
# It is also good practice not to repeat arguments of the parent class
# Arguments *args and **kwargs are here for this purpose
def __init__ (self, *args, **kwargs):
"""
Constructs a new Cat object with the specified nickname.
"""
super().__init__(*args, **kwargs) # Call parent's constructor
# Now, let's define a new method
# A cat can meow
def meow (self):
"""
This method is called when the cat meows.
"""
print("Meow")
Essayons ce code :
# Instantiate an object of class Dog
a_dog = Dog(42, "Snoopy")
# Show its attributes "Snoopy", 42, 0
print(a_dog.get_nickname(), a_dog.get_weight(), a_dog.get_nb_meals())
# Call its methods
a_dog.eat_stuff("cookie") # Shows "Yum, tasty cookie"
a_dog.bark() # Shows "Woof"
# Show its attributes again
print(a_dog.get_nickname(), a_dog.get_weight(), a_dog.get_nb_meals()) # Shows "Snoopy", 42, 1
# Instantiate an object of class Cat
a_cat = Cat("Garfield")
# Show its attributes "Garfield", 0
print(a_cat.get_nickname(), a_cat.get_nb_meals())
# Call its methods
a_cat.eat_stuff("kittens") # Shows "Yum, tasty kittens"
a_cat.meow() # Shows "Meow"
a_cat.bark() # Raises an error
Pour aller plus loin
Certains langages comme Python permettent l’héritage multiple, c’est-à-dire qu’une classe hérite des propriétés et des méthodes de plusieurs classes parentes.
Exercice pratique : Héritage multiple
Pour expérimenter avec l’héritage multiple, essayez de créer une classe Human
qui a la capacité de parler, puis créez une classe Cheshire
(chat du Cheshire d’Alice au Pays des Merveilles) qui hérite à la fois de Cat
et de Human
. Cette classe devrait pouvoir :
- Miauler comme un chat (hérité de
Cat
) - Parler comme un humain (hérité de
Human
) - Avoir un surnom et compter ses repas (hérité de
Animal
viaCat
)
Voici un exemple de structure pour vous aider à démarrer :
class Human:
"""
Represents a human that can speak.
"""
def speak(self, message: str):
"""
This method is called when the human speaks.
message: what the human wants to say
"""
print(f"Human says: {message}")
class Cheshire(Cat, Human):
"""
Represents a Cheshire cat that can both meow and speak.
A Cheshire inherits from both Cat and Human.
"""
def __init__(self, *args, **kwargs):
"""
Constructs a new Cheshire object with the specified nickname.
"""
# TODO: Appelez le constructeur parent approprié
pass
def grin(self):
"""
This method is called when the Cheshire cat grins mysteriously.
"""
print("*grins mysteriously*")
Testez votre implémentation avec ce code :
# Instantiate a Cheshire cat
cheshire = Cheshire("Cheshire")
# Test inherited methods from different parents
cheshire.speak("We're all mad here!") # From Human
cheshire.meow() # From Cat
cheshire.eat_stuff("magic mushroom") # From Animal (via Cat)
cheshire.grin() # Own method
# Show that it's still an animal
print(f"Nickname: {cheshire.get_nickname()}, Meals: {cheshire.get_nb_meals()}")
En Python, l’ordre des classes parentes dans la déclaration d’héritage multiple (class Cheshire(Cat, Human)
) détermine l’ordre de résolution des méthodes (Method Resolution Order - MRO). Vous pouvez utiliser Cheshire.mro()
pour voir dans quel ordre Python recherche les méthodes. Faites des tests avec une méthode de même nom définie dans les deux classes parentes Cat
et Human
.
Pour aller au-delà
Python est un langage très permissif laissant toute liberté aux développeurs. Certains aspects de la POO, comme la protection de propriétés de classe par encapsulation, ne sont pas strictement définis et respectés. Par exemple, il est, par convention, conseillé de préfixer une propriété protégée par un _. Ceci n’est qu’informatif car la propriété reste accessible en dehors de la classe et de ses sous-classes.
Des mécanismes de décorateurs permettent cependant d’ajouter ce contrôle d’accès. Essayer de protéger l’accès à une propriété de classe à l’aide des décorateurs @property
, @setter
et @getter
. Exploitez notamment la documentation suivante :