.. _tut-classes:
***********
Les classes
***********
Une vidéo de présentation des classes...
.. raw:: html
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é :keyword:`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 :class:`User`::
>>> class User:
... pass # classe vide
...
>>> User # la classe
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 :func:`__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 :func:`__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 :class:`User`::
>>> a
<__main__.User object at 0x01508750>
>>> type(a), id(a)
(, 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)
(, 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 :meth:`is_logged`:
.. code-block:: python
:linenos:
class User:
def __init__(self, name, logged):
self.name = name
self.logged = logged
def islogged(self):
return self.logged
a = User('Joe', True)
print("*** a *** type :", type(a), "id : ", id(a))
print(a.islogged())
Le résultat de l'exécution du code précédent montre que la méthode :meth:`islogged` retourne bien l'état de l'objet::
*** a *** type : id : 21977360
True
Comme évoqué précédemment, en utilisant des paramètres par défaut dans le constructeur :meth:`__init__`, on dispose de plusieurs façons d'instancier un objet:
.. code-block:: python
:linenos:
:emphasize-lines: 2
class User:
def __init__(self, name, logged=False):
self.name = name
self.logged = logged
def islogged(self):
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 :class:`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 :keyword:`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 :class:`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:
.. code-block:: python
:linenos:
:emphasize-lines: 2,6
class User:
num_of_users = 0
def __init__(self, name, logged=False):
self.name = name
self.logged = logged
User.num_of_users += 1
def islogged(self):
return self.logged
print("Creating Joe...")
joe = User('Joe', True)
print("there is now", User.num_of_users, "users")
print("Creating Jack...")
jack = User('Jack')
print("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`` :
.. code-block:: python
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 :func:`classmethod`. Ajoutons la méthode de classe :meth:`get_num_of_users` qui retourne le nombre d'utilisateurs:
.. code-block:: python
:linenos:
:emphasize-lines: 3-5
class User:
num_of_users = 0
@classmethod
def get_num_of_users(cls):
return cls.num_of_users
def __init__(self, name, logged=False):
self.name = name
self.logged = logged
User.num_of_users += 1
def islogged(self):
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:
.. code-block:: python
:linenos:
:lineno-start: 23
:emphasize-lines: 1,3
class SocialNetworkUser(User):
def __init__(self, name, nickname):
super().__init__(name)
self.nickname = nickname
socrate = SocialNetworkUser('Socrate', 'The philosopher')
print("Instance of SocialNetworkUser ? :", isinstance(socrate, SocialNetworkUser))
print("Instance of User ? :", isinstance(socrate, User))
print("Type : ", type(socrate))
print("'User' attribute name : ", socrate.name)
print("'User' attribute logged : ", socrate.logged)
print("'SocialNetworkUser' attribute nickname : ", socrate.nickname)
print("'User' method islogged() ? : ", socrate.islogged())
On définit ici une classe :class:`SocialNetworkUser` qui hérite de la classe :class:`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 :func:`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 :
'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 :ref:`tut-chaines`::
>>> '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
=====================
.. quiz:: quizz-12
:title: Les classes
- :quiz:`{"type":"TF","answer":"F"}` Une classe est un objet
- :quiz:`{"type":"TF","answer":"T"}` Une classe est une fabrique d'objets
- :quiz:`{"type":"TF","answer":"T"}` Une instance de classe est un objet
- :quiz:`{"type":"TF","answer":"T"}` Un objet peut disposer d'un ou plusieurs attributs
- :quiz:`{"type":"TF","answer":"F"}` Un objet doit disposer d'un ou plusieurs attributs
- :quiz:`{"type":"TF","answer":"T"}` Un objet peut disposer d'une ou plusieurs méthodes
- :quiz:`{"type":"TF","answer":"F"}` Un objet doit disposer d'une ou plusieurs méthodes
- :quiz:`{"type":"TF","answer":"F"}` Le constructeur est un attribut
- :quiz:`{"type":"TF","answer":"T"}` Le constructeur est une méthode
- :quiz:`{"type":"TF","answer":"T"}` Le constructeur possède au moins un paramètre
- :quiz:`{"type":"TF","answer":"F"}` Un objet possède toujours un constructeur
- :quiz:`{"type":"TF","answer":"F"}` Un deuxième appel identique au constructeur ne crée pas un objet différent
- :quiz:`{"type":"TF","answer":"F"}` Le constructeur porte le même nom que la classe
- :quiz:`{"type":"TF","answer":"T"}` Dans le code d'une classe, ``self`` désigne l'instance
- :quiz:`{"type":"TF","answer":"F"}` Dans le code d'une classe, ``self`` désigne la classe
- :quiz:`{"type":"TF","answer":"F"}` Une méthode d'instance est marquée par un décorateur
- :quiz:`{"type":"TF","answer":"T"}` Une méthode de classe est marquée par un décorateur
- :quiz:`{"type":"TF","answer":"F"}` C'est une bonne pratique de créer des attributs dynamiquement
- :quiz:`{"type":"TF","answer":"F"}` La classe a accès aux méthodes d'instances
- :quiz:`{"type":"TF","answer":"T"}` L'instance a accès aux méthodes de classe
- :quiz:`{"type":"TF","answer":"T"}` Python autorise le mécanisme d'héritage
- :quiz:`{"type":"TF","answer":"T"}` La :class:`str` met en oeuvre la redéfinition d'opérateur
- :quiz:`{"type":"TF","answer":"T"}` La :class:`list` met en oeuvre la redéfinition d'opérateur