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 classe User:

>>> 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’instance

  • Dans le code d’une classe, self désigne la classe

  • Une 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érateur

  • La list met en oeuvre la redéfinition d’opérateur