Les classes
Une vidéo de présentation des classes…
Dans Python, nous l’avons vu, tout est objet. Jusqu’à présent nous avons manipulé des objets faisant partie du langage lui même. Nous allons à présent aborder la conception d’objets définis par l’utilisateur.
Un objet sera défini par :
ses attributs, qui représentent ses caractéristiques ;
- et ses méthodes, qui permettent d’interagir avec lui :
en l’interrogeant sur son état ;
ou en modifiant son état.
Concevoir un objet, c’est donc écrire une classe, c’est à dire une fabrique d’objets, qui comprendra :
un constructeur, une méthode particulière qui n’est déclenchée qu’une seule fois à sa construction ;
et un ensemble de méthodes qui vont décrire son comportement et ses échanges avec le monde extérieur.
Classe et instance
En Python, une classe se définit avec le mot clé class
. La bonne pratique en Python est d’utiliser le CamelCase
pour définir les classes (le snake_case
étant utilisé pour les noms de variables et les fonctions) Prenons l’exemple de l’utilisateur d’un système informatique que l’on souhaite modéliser avec la classe User
:
>>> class User:
... pass # classe vide
...
>>> User # la classe
<class '__main__.User'>
Pour l’instant cette classe ne fait pas grand chose puisqu’elle est vide ! Mais rien n’empêche de l’utiliser.
L’instanciation, c’est à dire la création d’un objet de la classe, se fait par un appel au constructeur portant le même nom que la classe:
>>> a = User() # une instance de la classe
>>> a
<__main__.User object at 0x015888F0>
Un deuxième appel au constructeur crée un objet différent:
>>> b = User()
>>> b
<__main__.User object at 0x018A8750>
Et il en est ainsi de chaque nouvel appel au constructeur.
Le constructeur
En Python, l’objet est instancié grâce au constructeur __init__()
.
Note
En Python, il y a un constructeur unique, contrairement à Java, mais le système de paramètres nommés et paramètres par défaut permet d’instancier un objet de la classe de plusieurs façons différentes. Par rapport à Java, ce qui change c’est la façon de faire, pas la fonctionnalité.
La particularité du constructeur Python est de nécessiter le paramètre self
dans toutes les méthodes d’instance. self
référence l’objet courant et identifie la méthode comme une méthode d’instance et pas une méthode de classe. L’utilisateur de notre système informatique est identifié par un nom. Le constructeur ci dessous crée donc un attribut name
pour l’objet courant:
>>> class User:
... def __init__(self, name):
... self.name = name
...
self
est nécessaire dans la signature du constructeur __init__()
mais ignoré lors de l’appel où seul le paramètre name
est requis :
>>> a = User('Joe')
a
est maintenant un objet de la classeUser
:>>> a <__main__.User object at 0x01508750> >>> type(a), id(a) (<class '__main__.User'>, 22054736)
On peut instancier un autre objet avec le même paramètre. Cet objet est différent du premier, comme le montre son identité:
>>> b = User('Joe')
>>> type(b), id(b)
(<class '__main__.User'>, 22055184)
>>> a==b
False
Les attributs
Il n’y a pas en Python de notion public-protected-private
comme en Java. Les attributs de l’objet sont tous accessibles avec la syntaxe objet.attribut
:
>>> a.name
'Joe'
Note
Il existe une convention permettant d’indiquer que l’accès à l’attribut doit se faire avec une paire getter/setter. On préfixe l’attribut avec un
_
. L’indication est alors donnée que l’implémentation est susceptible de changer et que ce n’est pas une opération sûre d’accéder directement à l’attribut.Bien que ce ne soit pas recommandé, Python permet de modifier dynamiquement l’objet en ajoutant ou en retirant des attributs au cours de l’exécution. Ainsi la présence ou non d’un attribut peut être une information à part entière.
On peut tendre vers un modèle d’utilisateur plus réaliste en stockant l’information permettant de savoir si à un instant t, notre utilisateur est connecté ou non. L’attribut logged
est ajouté dans le constructeur:
>>> class User:
def __init__(self, name, logged):
self.name = name
self.logged = logged
>>> a = User('Joe', True)
>>> dir(a) # sortie tronquée pour l'affichage
[..., 'name', 'logged']
>>> a.name
'Joe'
>>> a.logged
True
Les méthodes
Un objet comporte des attributs qui représentent l’état de l’objet. Les méthodes sont les actions que peut effectuer l’objet sur lui même ou son environnement. On souhaite ici savoir si notre utilisateur est connecté ou non. Cette fonctionnalité est codée dans la méthode is_logged()
:
1class User: 2 def __init__(self, name, logged): 3 self.name = name 4 self.logged = logged 5 def islogged(self): 6 return self.logged 7 8a = User('Joe', True) 9print("*** a *** type :", type(a), "id : ", id(a)) 10print(a.islogged())
Le résultat de l’exécution du code précédent montre que la méthode islogged()
retourne bien l’état de l’objet:
*** a *** type : <class '__main__.User'> id : 21977360
True
Comme évoqué précédemment, en utilisant des paramètres par défaut dans le constructeur __init__()
, on dispose de plusieurs façons d’instancier un objet:
1class User: 2 def __init__(self, name, logged=False): 3 self.name = name 4 self.logged = logged 5 def islogged(self): 6 return self.logged
Ici Joe
est créé en étant connecté:
joe = User('Joe', True)
alors que Jack
non, puisque le paramètre logged
possède une valeur par défaut qui est utilisée si elle n’est pas précisée dans l’appel au constructeur:
jack = User('Jack')
On peut créer une collection, ici une list
, pour gérer un ensemble d’utilisateurs:
users = [joe, jack]
for user in users:
print(user.name, end=' ')
if user.logged:
print("is logged")
else:
print("is not logged")
Le résultat de l’exécution du code précédent:
Joe is logged
Jack is not logged
Attributs et méthodes de classe
Un attribut ou une méthode de classe est attaché à la class
et pas à l’instance.
La bonne pratique est de définir l’attribut de classe avant le constructeur. On y fait référence avec la syntaxe class.attribute
. En terme de conception, on utilisera un attribut de classe lorsqu’il n’est pas pertinent de stocker une information dans chaque instance des objets créés.
Dans notre exemple la classe User
doit garder la trace du nombre d’utilisateurs identifiés, qu’ils soient connectés ou non. Il n’y a aucune justification à héberger cette information au niveau de l’utilisateur, c’est la raison pour laquelle on utilise un attribut de classe:
1class User: 2 num_of_users = 0 3 def __init__(self, name, logged=False): 4 self.name = name 5 self.logged = logged 6 User.num_of_users += 1 7 def islogged(self): 8 return self.logged 9 10print("Creating Joe...") 11joe = User('Joe', True) 12print("there is now", User.num_of_users, "users") 13print("Creating Jack...") 14jack = User('Jack') 15print("there is now", User.num_of_users, "users")
L’attribut de classe User.num_of_users
garde la mémoire du nombre d’utilisateurs créés:
Creating Joe...
there is now 1 users
Creating Jack...
there is now 2 users
Les instances ont également accès à l’attribut de classe avec la syntaxe joe.num_of_users
:
print("there is now", joe.num_of_users, "users")
De la même façon que les méthodes d’instances prennent en paramètre une référence self
vers l’objet, les méthodes de classe prennent en paramètre une référence cls
vers la classe. On les tague avec le décorateur classmethod()
. Ajoutons la méthode de classe get_num_of_users()
qui retourne le nombre d’utilisateurs:
1class User:
2num_of_users = 0
3@classmethod
4def get_num_of_users(cls):
5 return cls.num_of_users
6def __init__(self, name, logged=False):
7 self.name = name
8 self.logged = logged
9 User.num_of_users += 1
10def islogged(self):
11 return self.logged
On crée à nouveau 2 utilisateurs Joe
et Jack
:
print("Creating Joe...")
joe = User('Joe', True)
print("Creating Jack...")
jack = User('Jack')
Comme on l’a vu juste au dessus, l’attribut de la classe est directement accessible via la classe ou l’instance.
Mais on peut maintenant y accéder également avec la méthode de classe, elle aussi accessible via la classe ou l’instance :
via la classe avec
User.get_num_of_users()
;et via l’instance avec
joe.get_num_of_users()
.
L’héritage
L’héritage est une caractéristique fondamentale des langages objet tels que Python. Cette propriété permet d’hériter, au sens génétique du terme, de toutes les caractéristiques (attributs et méthodes) de son ascendant. En Python, la syntaxe utilisée est la suivante:
23class SocialNetworkUser(User):
24 def __init__(self, name, nickname):
25 super().__init__(name)
26 self.nickname = nickname
27
28socrate = SocialNetworkUser('Socrate', 'The philosopher')
29print("Instance of SocialNetworkUser ? :", isinstance(socrate, SocialNetworkUser))
30print("Instance of User ? :", isinstance(socrate, User))
31print("Type : ", type(socrate))
32print("'User' attribute name : ", socrate.name)
33print("'User' attribute logged : ", socrate.logged)
34print("'SocialNetworkUser' attribute nickname : ", socrate.nickname)
35print("'User' method islogged() ? : ", socrate.islogged())
On définit ici une classe SocialNetworkUser
qui hérite de la classe User
. De façon classique dans la programmation orientée objet, l’instanciation d’une classe fille fait appel au constructeur de la classe mère par un appel à la fonction super.__init__()
. Une fois créé l’objet possède ses propres attributs/méthodes mais aussi des attributs/méthodes de la classe mère:
Instance of SocialNetworkUser ? : True
Instance of User ? : True
Type : <class '__main__.SocialNetworkUser'>
'User' attribute name : Socrate
'User' attribute logged : False
'SocialNetworkUser' attribute nickname : The philosopher
'User' method islogged() ? : False
Au delà de l’héritage, Python dispose de tous les mécanismes de la programmation orientée objet : polymorphisme, classes abstraites, etc… Il existe une littérature abondante sur le sujet.
La redéfinition d’opérateurs
La redéfinition d’opérateurs est un mécanisme qui permet de contextualiser le comportement des opérateurs du langage en fonction des objets sur lesquels ils s’appliquent. On a déjà abordé cette notion dans le chapître Les chaines de caractères:
>>> 'Hello' + " " + "World!"
'Hello World!'
>>> 'Hello ! '*3
'Hello ! Hello ! Hello ! '
Ce mécanisme est extrémement puissant et permet une écriture concise et élégante. On peut par exemple itérer sur des objets différents avec une même syntaxe:
>>> for i in 'abc':
... print(i)
...
a
b
c
>>> for i in range(3):
... print(i)
...
0
1
2
La liste des méthodes que l’on peut redéfinir est donnée dans le paragraphe Special method names.
Plus particulièrement, la redéfinition des opérateurs de conteneurs est décrite dans le paragraphe Emulating container types.
La redéfinition des opérateurs numériques est décrite dans le paragraphe Emulating numeric types.
Ce qu’il faut retenir
Une classe est un objet
Une classe est une fabrique d’objets
Une instance de classe est un objet
Un objet peut disposer d’un ou plusieurs attributs
Un objet doit disposer d’un ou plusieurs attributs
Un objet peut disposer d’une ou plusieurs méthodes
Un objet doit disposer d’une ou plusieurs méthodes
Le constructeur est un attribut
Le constructeur est une méthode
Le constructeur possède au moins un paramètre
Un objet possède toujours un constructeur
Un deuxième appel identique au constructeur ne crée pas un objet différent
Le constructeur porte le même nom que la classe
Dans le code d’une classe,
self
désigne l’instanceDans le code d’une classe,
self
désigne la classeUne méthode d’instance est marquée par un décorateur
Une méthode de classe est marquée par un décorateur
C’est une bonne pratique de créer des attributs dynamiquement
La classe a accès aux méthodes d’instances
L’instance a accès aux méthodes de classe
Python autorise le mécanisme d’héritage
La
str
met en oeuvre la redéfinition d’opérateurLa
list
met en oeuvre la redéfinition d’opérateur