9 La Programmation Orientée Objet en Python
9.1 Déclarer une classe
En Python, une classe est déclarée par la directive class. Elle est suivie du nom de la classe puis de deux points. Le corps de la classe doit évidemment être indenté.
Une classe est un callable. Un appel à cette classe retournera une instance de cette classe et pour maintenir une référence à cette instance, nous allons l’affecter a une variable.
Le code suivant est le code minimum permettant de déclarer et d’instancier une classe. La variable myInstance référence alors une donnée de type MyClass.
class MyClass:
pass
myInstance = MyClass()Les instances de cette classe sont des objets sans état et sans comportement. Ils ne servent pas à grand chose mais nous avons tout de même créé un nouveau type dans notre système.
9.2 Les méthodes
En Python, les méthodes sont des fonctions. Il s’agit donc des traitements. Lors de la conception d’un objet, les méthodes doivent être caractérisées par des verbes et représentent des actions.
Les méthodes peuvent recevoir des paramètres et retourner une donnée. Deux caractéristiques vont faire qu’une fonction est une méthodes :
- Les méthodes sont définies dans les classes.
- Vous devez déclarer un premier paramètre nommé par convention
self. Ce paramètre est destiné à recevoir l’instance en cours. Vous pouvez déclarer d’autres paramètres en plus de ce paramètreself.
Pour appeler une méthode, il faut la préfixer par le nom de l’objet à laquelle elle appartient en ignorant le paramètre attendu self.
- 1
-
Affiche
I did something - 2
-
Affiche
I will do something with a value
9.2.1 appels
- 1
- Instanciation de l’objet
- 2
- Appel d’une méthode sans arguments
- 3
- Appel d’une méthode avec arguments
9.2.2 Les attributs
Un attribut représente une donnée propre à un objet. En soi, c’est une variable qui référence donc une donnée. Lors de la conception d’un objet, les attributs sont caractérisés par des noms.
Pour distinguer les attributs des variables au sein d’un objet (dans le code des méthodes de la classe), un attribut est préfixé par le namespace self (ce n’est pas qu’une convention). Pour accéder à un attribut en dehors de l’objet, il faut le préfixer par la référence de son instance.
class Training:
def reset_students(self):
self.students = []
def get_available_seats(self):
return self.max_seats - len(self.students)
training = Training("Python")
print(training.students)Dans cet exemple, la liste des participants, students, est un attribut public accessible à l’extérieur de l’objet comme le montre la dernière ligne.
9.2.3 En Python, un objet n’a que des attributs
Si vous consultez la documentation de la fonction dir(), vous verrez qu’elle liste tous les attributs d’un objet. Or, dans cette liste, il y a des méthodes.
Rappelons qu’en Python, tout est objet. Une méthode peut donc être considérée comme un attribut qui a la particularité d’être un callable, donc de pouvoir être exécuté. Cette propriété permet la mise en œuvre de l’introspection.
9.2.4 Méthode particulière : le constructeur
Pour créer un objet, l’interpréteur va appeler une méthode particulière. Cette méthode porte habituellement le nom de constructeur. Le constructeur est appelée à l’instanciation de l’objet et permet de définir son état initial. Elle est disponible pour toutes les classes et par défaut ne définit aucun état.
Python a une particularité : lors de l’instanciation de l’objet, deux méthodes sont appelées successivement :
- La méthode
__new__(self)qui est le constructeur et s’assure de la création technique de l’objet - La méthode
__init__(self)qui est un initialiseur et qui définit l’état initial de l’objet.
En Python, l’état initial d’un objet et plus généralement tout son comportement fonctionnel à l’initialisation sont définis dans l’initialiseur. Par abus de langage, on parle de constructeur.
Le constructeur de base est le suivant.
class UselessClass:
def __init__(self):
passLe comportement de cet initialiseur est le comportement par défaut, il est inutile d’écrire la fonction __init__() de ce type. En son absence, un initialiseur équivalent par défaut est appelé.
Nous pouvons tester le comportement du constructeur par le code suivant.
Nous pouvons voir que le message est affiché à l’instanciation et donc que cette méthode est bien appelée à ce moment.
class DemoClass:
def __init__(self):
print('I have been initilized')
1obj = DemoClass()- 1
-
Cette expression va créer un objet de type
DemoClasset le retourner mais auparavant, elle va afficher la chaine de caractèresI have been initilized.
9.2.5 Méthode particulière : le destructeur
Il existe une autre méthode particulière communément appelée destructeur. En fait, c’est une méthode appelée lorsque l’objet perdra sa dernière référence soit juste avant sa destruction.
Le comportement peut être illustré par le code suivant.
- 1
-
Affichera
I have been initilizeden créant l’objet. - 2
-
Affichera
I will be destroyedcar la suppression de la variable entraine la suppression de la dernière référence à l’objet donc sa destruction.
L’usage est donc la libération de ressources (c’est le rôle des destructeurs dans les langages bas niveau) et faire le ménage.
9.2.6 Utilisation du constructeur
Le constructeur est indispensable pour initialiser un objet, c’est à dire définir son état initial. Évidemment, des paramètres doivent être passés à l’initialisation de l’objet. Ces paramètres sont ceux de l’initialiseur __init__(). Ainsi, le code suivant permet d’instancier une formation selon la spécification du diagramme UML.
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.subject = subject
self.duration = duration
self.available_seats = capacity
self.students = []
Vous remarquerez plusieurs points ici.
En premier lieu, les paramètres de l’initialiseur et les attributs n’ont pas à avoir le même nom. En effet, il s’agit de choses très différentes. Les paramètres ne sont que des informations pour construire l’état de l’objet. Dans notre cas, nous utilisons directement les données pour les affecter aux attributs, mais ce ne sera pas toujours le cas. Vous avez cependant une illustration du changement de concept entre l’attente d’information sur la capacité en terme de places de la formation devenant le nombre de places disponibles pour l’inscription.
L’initialiseur étant une fonction, nous pouvons utiliser les même propriétés à savoir l’argument par défaut.
Retenez également qu’il n’y a pas de symétrie entre les paramètres et les attributs. Nous venons de voir en sens, mais aussi en nombre. L’attribut self.students est une liste qui sera alimentée au fur et à mesure de l’inscription des stagiaires. Sa création n’attends pas d’information spécifique lors de l’instanciation, elle est donc créée dans dépendance à un paramètre.
l’initialiseur est une méthode appelée à l’instanciation de l’objet et dont le rôle est de s’assurer de l’état initial de l’objet. C’est au sein de cette méthode que vous devez vous assurer de l’intégrité de l’objet. Par principe, mettez-y le tout le code assurant l’intégrité de l’objet. Déclarez également tous les attributs qui font l’objet. Mais assurez-vous aussi de la cohérence de l’état de l’objet. Ainsi, nous pouvons par exemple ajouter la validation suivante (le code se limite au code d’intérêt).
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.available_seats = int(capacity)
if self.available_seats < 1:
raise ValueError(f"Capacity must be positive, got {self.available_seats}")
9.2.7 Méthodes et état de l’objet
Nous avons vu plus haut comment déclarer des méthodes. Une méthode voit les attribut de l’objet et peut donc les interroger ou les modifier.
C’est donc par les méthodes que l’état d’un objet doit changer. Ainsi, d’après notre modèle, l’objet de type Training doit avoir une méthode add_student(name). Cette méthode ajoutera un stagiaire, dont le nom est passé en paramètre, à la formation. Comme précédemment, je ne m’intéresse qu’au code d’intérêt qui est le suivant.
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.available_seats = int(capacity)
self.students = []
def add_student(self, name:str):
self.students.append(name)
L’intérêt de la méthode est d’avoir les plein contrôles sur le changement d’état de l’objet. Ainsi, il est de la responsabilité de cette méthode de s’assurer de l’absence de doublons (non illustré ici) et du respect de la contrainte de places disponibles. La méthode add_student(name) devrait donc aller dans le sens suivant :
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.available_seats = int(capacity)
self.students = []
def add_student(self, name:str):
if len(self.students) >= self.available_seats:
raise ValueError('Training full')
self.students.append(name)
9.3 Visibilité et encapsulation en Python
Jusque’ici, nous ne nous sommes pas soucié de la notion de visibilité. Aucun mot clef ne a déclare. Vous remarquerez que vous accédez librement à tous les attributs et méthodes.
Et pour cause, en Python, tous les attributs et les méthodes ont une visibilité publique.
C’est tout à fait voulu par Guido Van Rossum.
We are all grown up adults here
Il existe cependant une convention forte : lorsqu’un nom d’attribut ou de méthode commence par le caractère souligné, l’underscore, il doit être considéré comme privé. Ainsi :
mon_attributest un attribut publique_mon_attributest un attribut privé.
Pour être plus cohérent, on parlera souvent d’attribut non-public.
La visibilité est un concept à associer à celui de l’encapsulation.
9.3.1 Principe d’encapsulation
L’encapsulation est le principe de la programmation orientée objet consistant à masquer l’implémentation et à communiquer avec les interfaces publiques.
Contrairement à des idées reçues, l’objectif est avant tout de maitriser le comportement et l’évolution de l’implémentation. Et surtout de décolérer ce qui est exposé de l’implémentation.
Pour une formation, nous ne voulons pas nous contenter du nombre total de places mais aussi du nombre de places disponibles. La méthode ici peut laisser entendre l’existence d’un attribut mais calcul cette valeur :
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.available_seats = int(capacity)
self.students = []
def remaining_seats(self):
return self.available_seats - len(self._students) L’encapsulation permet de mettre en œuvre le principe Open/Closed : ouvert à l’extension, fermé à la modification.
9.3.2 Name Mangling
Le Name Mangling est une technique permettant de s’assurer qu’un attribut ne sera pas renommé dans une classe fille lors de la mise en œuvre de l’héritage.
En Python, des attributs dont le nom commence par au moins deux underscores et se termine par au plus un underscore basculent sur un mécanisme de résolution qui semble les faire apparaitre comme réellement privés. Ce n’est pas le cas…
Le code suivant semble pourtant montrer ce comportement à première vue.
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.available_seats = int(capacity)
self.__students_ = []
t = Training('Python', 5)
1t.__students_- 1
-
Provoque une
AttributeError
Oui mais…
T._Training__students_Retourne bien une liste.
Cet attribut est donc bien visible mais avec un nom modifié. Il ne s’agit pas de __attribut_ mais _classe__attribut_.
La méconnaissance de ce mécanisme conduit de nombreux dévelopeur Python à utiliser cette convention de nom avec l’illusion de rendre l’attribut invisible de l’extérieur. Ce n’est pas le cas (puisqu’il ne le fait pas, faites un dir(t)…), ce mécanisme sert à éviter qu’un attribut soit renommé par accident dans une classe spécialisée lors de la mise en œuvre de l’héritage.
9.3.3 Les properties
La visibilité des attributs d’une classe est absolue : elle définit tous les accès à l’attribut. Il n’est pas possible de définir un attribut qui sera accédé de manière publique en lecture mais privée en écriture. Dans notre exemple, nous avons besoin de limiter l’accès à l’attribut students. En effet, nous devons certes y accéder pour consulter la liste des inscrits, mais nous ne devrions pas y avoir accès en modification. L’objet doit être le seul maître de l’évolution de son état.
Pour cette raison, la visibilité est définie en privé. Pour y accéder, nous aurons une méthode.
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.subject = subject
self._students = []
def students(self):
return self._students.copy()
Mais cette implémentation entraine une violation du principe d’accès uniforme : certains attributs sont obtenus par appel à l’attribut et d’autres par appel à des méthodes, ce qui provoque une complexité : comment l’utilisateur saura comment accéder à un attribut donné ? Dans beaucoup de langages, l’uniformisation consiste à définir tous les attributs en privé et à définir des méthodes dites accesseurs.
En outre, une évolution d’un attribut publique vers un accès par méthode, même si le nom est conservé, entraine une modification du code utilisant cet objet. En effet, il faudra modifier tous les accès pour ajouter des parenthèses. Ceci est particulièrement dangereux en Python où accéder à un méthode retourne sa référence. Déclarer tous les attributs en privé et définir des accesseurs permet également, dans beaucoup de langages, d’éviter cette situation.
Python permet le respect du principe objet en proposant un descripteur de données, les property.
Les property s’utilisent de deux manières différentes : sous forme de décorateurs ou sous forme de classe interne. Ce document présentera l’usage du décorateur (dont le principe est détaillé plus loin). Pour plus d’informations, voir : https://docs.python.org/3/howto/descriptor.html#properties
L’usage du décorateur property est simple, il suffit de le rajouter avant la méthode accesseur. Le nom de la méthode devient donc un attribut.
Voici un extrait du de la déclaration de la fonction limité au code qui nous intéresse.
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.subject = subject
self._students = []
@property
def students(self):
return self._students.copy()L’usage sera le suivant.
In [2]: t_python = training.Training("Python", 5)
In [3]: print(t_python.students)
[]Vous voyez qu’à l’usage, il semble y avoir un attribut students qui retourne bien la valeur attendue. Mais cet attribut n’existe pas, c’est une property qui masque l’accès à une fonction. Cette fonction n’accepte pas de paramètre et retourne une donnée.
Avec cette implémentation, la liste des stagiaires peut être accédée comme un attribut mais cet attribut ne pourra pas être modifié par affectation (cela lèvera un AttributeError). Ainsi, cette liste est accédée par la property et évolue par appel aux méthodes add_student() et remove_student().
Il est possible grâce aux properties de masquer une fonction accesseur pour y accéder comme une affectation de valeur à un attribut ou pour supprimer un attribut. Ces usages sortent du périmètre de cette formation.
Notez que le descripteur property permet ainsi la mise en œuvre du principe de la Programmation Orientée Objet qu’est l’encapsulation. Vous allez masquer l’implémentation et ne communiquer que par les attributs et méthodes publiques.
Voici ci-dessous l’exemple à l’accès à la propriété places restantes. que bous avons exprimée plus haut sous forme de méthode :
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.subject = subject
self.available_seats = int(capacity)
self._students = []
@property
def remaining_seats(self):
return self.available_seats - len(self._students)
Cette propriété apparait comme un attribut d’un objet de type Training mais est en pratique déduit à partir d’attributs réels.
9.4 Les méthodes spéciales
Nous avons vu jusqu’ici une méthode spéciale : __init__(self). Les méthodes spéciales sont des méthodes qui sont encadrés de double-underscores. Leur particularité est qu’elle ne sont jamais (rarement) appelées directement. __init__ est appelée par l’interpréteur lors de l’instanciation d’un objet. Python propose un ensemble de méthodes spéciales :
__init__(self): initialiseur appelé juste après l’instanciation d’un objet__del__(self): destructeur, appelé juste avant la destruction de l’objet__str__(self) -> str: est appelé par la fonction de conversion de typestr()et par la fonction print(). Elle doit donc retourner une chaine de caractères représentant l’objet.__repr__(self) -> str: est appelé par la fonctionrepr()et doit retourner une chaine de caractères contenue entre des chevrons et contenant nom, type de l’objet et informations additionnelles.
Il est conseillé de redéfinir (surcharger, vois l’héritage) ces méthodes lorsque vous créez vos classes.
Python propose également des méthodes de comparaison entre objets. Celles-ci sont appelées par les opérations de comparaison correspondantes. Dans la colonne Opération du tableau suivant, x représente l’objet courant et y l’objet other avec qui il est comparé.
| Méthode | Opération |
__lt__(self, other) |
x < y |
__le__(self, other) |
x <= y |
__eq__(self, other) |
x == y |
__ne__(self, other) |
x != y |
__ge__(self, other) |
x >= y |
__gt__(self, other) |
x > y |
Ces méthodes ne doivent être redéfinies que si elles sont un sens. Si vos objets doivent être triés, vous devez redéfinir la méthode __gt__(self, other).
Python propose également des méthodes spéciales qui permettent des opérations mathématiques entre les objets.
| Méthode | Opération |
__neg__ |
-x |
__add__ |
x + y |
__sub__ |
x - y |
__mul__ |
x * y |
__div__ |
x / y |
Python propose d’autres méthodes spéciales, pour plus d’informations, vous pouvez consulter :
9.5 Attributs et méthodes de classes
Les attributs et les méthodes de classe ne sont pas liés à une instance mais à une classe.
9.5.1 Attributs de classe
Un attribut de classe est déclaré au niveau de la classe :
class MyClass:
class_attr = NoneD’usage, un tel attribut est accédé par MyClass.class_attr. Dans une instance de cette classe, vous pouvez y accéder par self.class_attr mais attention : une consultation retournera bien la valeur de l’attribut de classe mais une modification (l’affectation d’une valeur) créera un attribut d’instance qui masquera l’attribut de classe.
Les attributs de classe ont un usage technique en Python comme pour les descripteurs ou les properties.
9.5.2 Méthodes de classe
Une méthode de classe est déclarée avec un décorateur :
class MyClass:
@classmethod
def class_method(cls):
passCe sont des méthodes qui seront appelées préfixées par la classe, qui connaissent la classe (notez le premier paramètre cls et non self) mais pas d’instance.
En Python, elles sont utilisées pour proposer des constructeurs alternatifs. Ainsi, pour notre classe Training, nous pouvons proposer un constructeur qui accepte les informations sur les participants et crée une formation avec des participants. Pour l’exemple suivant, students_data est une liste d’informations extraite d’une base de données ou d’un CSV.
class Training:
@classmethod
def with_students(cls, subject: str, duration: int,
capacity: int = 12, students_data: list = None):
training = cls(subject, duration, capacity)
if students_data:
for student_data in students_data:
training.add_student(*student_data)
return trainingEt son usage :
Training.with_students('Python', 5, students_data=['Nicolas', 'Alceste',
'Agnan', 'Rufus'])9.6 Héritage et abstraction
9.6.1 L’héritage
L’héritage définit la relation entre classes comme « est un ». Le vocabulaire associé est la généralisation et la spécialisation qui représentent les sens opposés de la relation.
Au delà de la définition, il y a une conséquence technique : la spécialisation est et peut faire tout ce qu’est ou sait faire la définition plus générale.
Ainsi, la généralisation consiste à avoir une définition universelle, donc réutilisable. La spécialisation consiste à adapter les caractéristiques et les comportements pour une définition plus spécifique.
9.6.2 Base de l’héritage avec les exceptions
Le premier bénéfice de la spécialisation est d’avoir un nouveau type qui est également le type de la classe héritée (et ainsi en cascade). Une première application concerne les exceptions.
En effet, pour créer nos propres exceptions, nous devrons créer des classes qui héritent de Exception. Vous avez vu dans le chapitre concerné qu’il existe toute une arborescence, en fait, nous pouvons spécialiser un type d’exception plus spécifique que Exception.
Dans notre code d’illustration, nous levons une exception de type ValueError lorsque nous essayons d’ajouter un stagiaire à une formation qui ne peut en accepter d’avantage. Ce n’est évidemment pas le bon type de message.
Nous pouvons créer un nouveau type d’exception, TrainingFullException qui spécialise directement Exception.
class TraininfFullException(Exception):
...
Et c’est tout. Cette simple instruction permet de créer un nouveau type d’exception en se basant sur le type existant. TrainingFullException se comporte exactement comme Exception, mais c’est un type, donc un message, en plus.
Nous allons pouvoir l’utiliser de la manière suivante.
class Training:
def __init__(self, subject: str, duration: int, capacity: int = 12):
self.available_seats = int(capacity)
if self.available_seats < 1:
raise TraininfFullException(f"Capacity must be positive, "
f"got {self.available_seats}")
9.6.3 Base de conception de l’héritage
La relation d’héritage apparait en général lors de la conception. Si nous reprenons notre modèle, nous avons défini une classe Student pour représenter les stagiaires. Dans un soucis de simplification, je ne représente ci-dessous que les attributs de cette classe. Ces attributs ont été complétés par un identifiant et l’information de la société qui emploi le stagiaire.
Mais il nous manque une notion pour animer cette formation : l’intervenant…
Nous allons donc définir une nouvelle classe que nous allons appeler Coach. Cette notion nécessite une nouvelle classe pour plusieurs raisons.
D’une part, il s’agit d’un autre concept, je dois pouvoir identifier ce que je manipule.
D’autre part, l’état de l’objet est différent. L’identifiant n’est pas le même que pour les stagiaires, je n’ai pas besoin d’information de société et, indépendamment de la formation, l’intervenant a un salaire.
Définir un autre objet est tout à fait nécessaire. Mais bien qu’il s’agisse de deux notions différentes, elles partagent des similitudes.
En premier lieu, elles partagent la même notion : un stagiaire et un intervenant peuvent être, toutes deux, qualifiées de personnes. Nous pouvons perler d’elles de manière plus abstraite.
En conséquence, elles partagent des caractéristiques communes : le nom et la civilité.
Nous allons donc pouvoir généraliser le concept de personne.
La représentation de la relation de ces trois classes est représentée ci-dessous. Les classes Student et Coach héritent de Person. Student et Coach sont des spécialisations de Person, Person est une généralisation de Student ou de Coach.
Vous remarquerez que Person définit les attributs name et title. En même temps, ces attributs sont absents de la définition de Student et Coach. Mais comme Student ou Coach sont également de type Person, les objets posséderont évidemment ces attributs.
9.6.4 Notion d’implémentation de l’héritage
Commençons de manière simple. Nous avions une classe Student qui avait un constructeur que nous pouvons rappeler ici.
class Student:
def __init__(self, name:str, title:str, company:str=None):
if len(name) < 2:
raise ValueError("Name too short")
self.name = name
self.title = title
self.company = company
Ce constructeur possède une validation sur la taille du nom.
La classe Coach a un comportement similaire. Nous décidons de généraliser la notion de personne ce qui nous conduit à la définition suivante.
class Person:
def __init__(self, name:str, title:str):
if len(name) < 2:
raise ValueError("Name too short")
self.name = name
self.title = titleNous retrouvons évidemment le même code que pour la classe Student. Code qui est, pour cette partie, identique à la classe Coach (non représenté ici).
La première étape de la généralisation consiste donc à définir une classe reprenant les notions générales de nos implémentations.
Nous allons donc maintenant pouvoir considérer que Student et Coach sont des spécialisations de Person. Qu’elles héritent de Person.
En premier lieu, pour déclarer que Student hérite de Person, il faut modifier la déclaration de la classe et ajouter, entre parenthèses, le nom des classes héritées. Oui, pluriel car Python prends en charge l’héritage multiple ce qui sera détaillé plus loin.
La base sera la suivante.
class Student(Person):
def __init__(self, name:str, title:str, company:str=None):
self.company = company
Nous venons ici de définir une classe Student qui, en héritant de Person, est du même type et accède donc aux mêmes méthodes. Cependant, en déclarant la méthode __init__(), nous remplaçons son contenu pour la classe Student. Ainsi, si vous instanciez un objet de type Student, c’est ce constructeur qui sera exécuté et lui seul. Donc, les attributs name et title ne seront pas créés.
Pour que la classe Student délègue à la classe Person la gestion de ce qu’elle sait faire, il faut appeler sa méthode. Ceci est réalisé par une fonction spéciale, super().
Notre code devient :
class Student(Person):
def __init__(self, name:str, title:str, company:str=None):
super().__init__(name, title)
self.company = company
Lorsque le constructeur sera appelé, la fonction super() appellera le constructeur du parent puis continuera l’exécution. Cela permet donc de déléguer une partie du traitement à la classe parent avant de réaliser la partie spécifique.
9.6.5 Héritage simple et héritage multiple
Le schéma précédent présente un héritage simple : chaque classe spécialisée n’hérite que d’une seule classe générale. Certains langages permettent l’héritage multiple, c’est à dire qu’une classe peut hériter de plusieurs classes. Elle acquiert alors les caractéristiques de l’ensemble des classes héritées.
En général, l’héritage multiple est difficile à mettre en œuvre efficacement car il est source d’erreurs et est assez difficile à maintenir. Même si le langage le permet, évitez un héritage multiple.
9.6.6 Héritage et type des objets
Le type d’un objet est sa classe. Mais dans le mécanisme de l’héritage, un objet est également du type d’une classe parente. Ainsi, une instance de Student est aussi bien de type Student que Person.
Cette propriété est fondamentale dans les langages déclaratifs afin de manipuler plusieurs types spécialisés pour leur caractéristique commune. C’est le premier pas vers les notions d’abstraction.
9.6.7 Abstraction : classes abstraites et classes concrètes
Dans l’exemple précédent, la classe Person est une classe abstraite. Une classe abstraite est une classe qui ne peut être instanciée. Elle ne peut être que spécialisée.
Cette propriété permet de s’assurer qu’une classe qui n’a pas de sens dans notre système ne pourra exister en elle même. Mais qu’il sera possible de la spécialiser. ### Abstraction : les interfaces
Une interface peut être définie de la manière la plus simple comme un contrat.
Il s’agit d’un composant abstrait qui ne fait que définir des méthodes publiques que les classes qui implémentent l’interface doivent déclarer. Une interface garantit donc un comportement commun entre des objets de types distincts sans aucune filiation. Une interface est représentée par un trait vertical discontinu.
L’autre propriété d’une interfaces est que, même si elle n’est pas une classe, elle confère son type à la classe qui l’implémente. Dans l’exemple ci-dessous, la classe Account est également de type BankAccount.
La notion d’interface est apparue avec les langages déclaratifs interdisant l’héritage multiple. Elles permettent un comportement et un type commun entre objets sans filiation.
Python et les abstractions
Par rapport à ce qui a été présenté ci-dessus, Python
- permet l’héritage
- supporte l’héritage multiple
- ne possède pas (nativement) la notion de classes abstraites
- ne possède pas la notion d’interfaces
Python ne propose pas nativement la notion de classe abstraite. Cependant, la PEP-3119 propose la notion d’Abstract Base Class (ABC). Ce mécanisme n’est pas abordé dans cette formation.
9.6.8 L’héritage en Python
Pour définir qu’une classe est une spécialisation d’une autre en Python, on l’indique dans la déclaration de la class en faisant suivre le nom de la classe par des parenthèses contenant le ou les nom(s) de la ou des classe(s) héritée(s).
La déclaration de la classe Client ci-dessous suffit pour déclarer un nouveau type spécialisant la notion de Person. Ce nouveau type déclare tous les états et comportements du type Person.
class Client(Person):
pass
Spécialiser une classe signifie lui donner un comportement spécifique, mais cela peut également signifier lui attribuer des comportements complémentaires.
Dans ce dernier cas, une classe spécialisée peut déléguer à la classe générale ce qu’elle sait gérer. Lors de l’instanciation d’une classe Client, la classe Person sait gérer l’initialisation de nom et prénom. Nous pouvons lui déléguer cette gestion dans le constructeur.
class Client(Person):
def __init__(self, nom, prenom, nid):
super().__init__(nom, prenom)
self._identifiant = nid
self._accounts = []
La fonction super() permet d’appeler les parents de la classe Client et donc leur méthode. Dans le cas de l’héritage simple (Client est la spécialisation d’un seul parent), c’est donc la méthode du parent qui est appelée. Dans le cas de l’héritage multiple (Client est la spécialisation de plusieurs parents), la fonction super() gérera l’ordre d’appel des méthodes des parents.
Les attributs créés par l’initialiseur de la classe Person seront évidemment accessibles par l’objet Client.
9.7 Notions de polymorphisme
Le polymorphisme est la capacité d’un système à choisir l’implémentation réelle d’une méthode. Nous la mettons déjà en œuvre en surchargeant la méthode __init__(self).
Spécialiser une classe peut ainsi conduire à redéfinir le comportement de certaines méthodes. Si nous avons un nouveau type de compte DeferedAccount qui est compte à débit différé, l’opération de débit ne peut être la même que la classe Account dont il hérite. Nous allons donc implémenter cette classe de la manière suivante :
class DeferedAccount(Account):
def __init__(self, nid, balance=100):
Account.__init__(self, nid, balance)
self._operations = []
def withdraw(self, value):
self._operations.append(value)
Pour les objets de type DeferedAccount, l’interpréter ne fera pas appel à la méthode withdraw() de la classe parent mais bien à celle redéfinie (surchargée) dans la déclaration de DeferedAccount.
9.8 Polymorphisme et duck typing
En Python, la notion de polymorphisme repose sur la notion de duck typing.
Si je vois un animal qui vole comme un canard, cancane comme un canard, et nage comme un canard, alors j’appelle cet oiseau un canard
Ce qui nous intéresse en Python, ce n’est pas quel est le type réel de l’objet mais est-ce qu’il a la caractéristique nécessaire à l’action.
Ce principe peut être résumé par cette citation :
Don’t check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with. If the argument fails this speci c-ducklyhood-subset-test, then you can shrug, ask « why a duck? »
Alex Martelli
Ce qui nous conduit à un principe clef en Python :
It’s easier to ask for forgiveness than permission
9.8.1 Ne testez pas si c’est un canard…
Il existe une fonction qui permet de vérifier si un objet est une instance d’une classe : isinstance(objet, classe). En Python, nous allons rarement l’utiliser. Plutôt que de tester si l’objet est de cette instance, nous testerons si il a les propriétés qui nous intéressent. Pour cela, nous utiliserons la fonction hasattr(objet, nom) qui permet de vérifier si un objet a un certain attribut. Le paramètre nom est une chaine de caractère du nom de la propriété.
In [1]: hasattr(my_account, 'deposit')
Out[1]: True
In [2]: hasattr(my_account, 'upper')
Out[2]: False9.8.2 Ou ne testez pas du tout
Si vous tentez d’appeler une méthode qui n’existe pas, cet appel va provoquer une exception de type AttributeError.
In [3]: my_account.upper()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-7-436618faa434> in <module>()
----> 1 my_account.upper()
AttributeError: 'DebtAccount' object has no attribute 'upper'Vous pouvez donc directement capturer et gérer l’exception comme le décrit le chapitre suivant.
9.9 Les attributs
9.9.1 Affecter et accéder à une valeur dans l’objet
Un attribut dans la classe a un syntaxe self.nom_attribut.
On affecte une valeur à un attribut en déclarant celui-ci préfixé par self :
class MyClass:
def assign_value(self, value:int):
self.attribute = value
On y accède en le référant de la même manière.
class MyClass:
def access_value(self) -> int:
return self.attribute
9.9.2 Accéder à un attribut en dehors de l’objet
On peut accéder à un attribut en dehors de l’objet
>>> my_object = MyClass()
>>> my_object.assign_value(42)
>>> my_object.value
42On peut modifier la valeur par assignation avec la même syntaxe. Note que si l’attribut n’existe pas, il est créé.
>>> my_object = MyClass()
>>> my_object.value = 42
>>> my_object.value
429.10 Les méthodes spéciales
Documentation : https://docs.python.org/3/reference/datamodel.html
9.10.1 L’initialiseur
class MyClass:
def __init__(self, param_1, param_2):
self.attribute_1 = param_1
self.attribute_2 = param_2
9.10.2 La représentation sous forme de chaine de caractères
9.10.2.1 Représentation informelle avec __str__
class MyClass:
def __init__(self, param_1, param_2):
self.attribute_1 = param_1
self.attribute_2 = param_2
def __str__(self):
return f'Objet {self.attribute_1} {self.attribute_2}'
9.10.2.2 Représentation officielle avec __repr__
The target audience for the string representation returned by .__repr__() is the programmer developing and maintaining the program. In general, it provides detailed and unambiguous information about the object. Another important property of the official string representation is that a programmer can normally use it to re-create an object equal to the original one.
One way to implement the method is the following :.
class MyClass:
def __init__(self, param_1, param_2):
self.attribute_1 = param_1
self.attribute_2 = param_2
def __repr__(self):
class_name = type(self).__name__
return f"{class_name}(param_1={self.attribute_1!r},
param_2={self.attribute_2!r})"
Note : si __repr__est surchargé mais pas __str__, le retour de __repr__ sera utilisé lors des requêtes à __str__.
9.10.3 Autres méthodes spéciales
9.10.3.1 Opérateurs de comparaison
| Méthode spéciale | Opérateur |
|---|---|
__lt__(self, other) |
self < other |
__gt__(self, other) |
self > other |
__le__(self, other) |
self <= other |
__ge__(self, other) |
self >= other |
__eq__(self, other) |
self == other |
__ne__(self, other) |
self != other |
9.10.3.2 Opérateurs mathématiques
| Méthode spéciale | Opérateur |
|---|---|
__neg__(self) |
-self |
__add__(self, other) |
self + other |
__sub__(self, other) |
self - other |
__mul__(self, other) |
self * other |
__truediv__(self, other) |
self / other |
9.11 Visibilité des attributs
En Python, tout a une visibilité publique. Par convention, un attribut est considéré comme non-public si son nom commence par un underscore (_) :
class MyClass:
def __init__(self):
self._non_publi_attribute = 42
def _non_public_method(self):
pass