Algo & Prog 1 Python : CM séance 06
6. Héritage, gel et modules
6.1. Héritage
6.1.1. Principe
L'héritage consiste à fabriquer une nouvelle classe, appelée classe fille ou classe dérivée, à partir d'une classe existante, appelée classe mère ou classe de base :
- la fille spécialise la mère ;
- la mère généralise la fille.
class Plante : # mère
pass
class Fleur(Plante) : # fille de Plante
pass
>>> issubclass (Fleur, Plante) # True
>>> f = Fleur()
>>> isinstance (f, Fleur) # True
>>> isinstance (f, Plante) # True : f est aussi une Plante
Les méthodes sont héritées par la classe fille :
class Oiseau :
def chanter(self) :
print("Cui cui !")
class Pigeon(Oiseau) :
pass
>>> o = Oiseau()
>>> o.chanter() # Cui cui !
>>> p = Pigeon()
>>> p.chanter() # Cui cui !
On peut surcharger (redéfinir) les méthodes de la classe fille :
class Oiseau :
def chanter(self) :
print("Cui cui !")
class Pigeon(Oiseau) :
def chanter(self) : # méthode surchargée
print("Rhou rhou !")
>>> o = Oiseau()
>>> o.chanter() # Cui cui !
>>> p = Pigeon()
>>> p.chanter() # Rhou rhou !
Dans une méthode héritée on peut déterminer la classe réelle avec __class__
:
class Véhicule :
def je_suis_quoi (self) :
print(self.__class__)
class Moto(Véhicule) :
pass
>>> v = Véhicule()
>>> v.je_suis_quoi() # <class '__main__.Véhicule'>
>>> m = Moto()
>>> m.je_suis_quoi() # <class '__main__.Moto'>
6.1.2. Constructeurs
Le constructeur est hérité par défaut (c'est une méthode) :
class Animal :
def __init__(self, nom) :
print("Je m'appelle", nom)
class Chien(Animal) :
pass
>>> a = Animal("Martin") # Je m'appelle Martin
>>> c = Chien("Raoul") # Je m'appelle Raoul
Si la classe fille a son propre constructeur, alors celui de la mère n'est plus appelé. Si besoin on peut l'appeler explicitement :
class Animal :
def __init__(self, nom) :
print("Je m'appelle", nom)
class Chien(Animal) :
def __init__(self, nom) :
print("Ouah ouah !")
Animal.__init__(self, nom) # appel explicite
>>> a = Animal("Martin") # Je m'appelle Martin
>>> c = Chien("Raoul") # Ouah ouah ! Je m'appelle Raoul
- Remarque : utilisation de
super
-
dans
__init__
, au lieu de :Animal.__init__(self, nom)
on peut écrire :
super(Chien, self).__init__(nom) super().__init__(nom) # Raccourci, sans self
Les constructeurs d'une classe mère et d'une classe fille peuvent avoir des paramètres différents et mémoriser d'autres attributs :
class Rectangle :
def __init__(self, longueur, largeur) :
self.long = longueur
self.larg = largeur
def aire(self) :
return self.long * self.larg
class Carré(Rectangle) :
def __init__(self, côté, couleur) :
self.couleur = couleur
super().__init__(côté, côté)
# ou : Rectangle.__init__(self, côté, côté)
>>> r = Rectangle(5,7)
>>> r.aire() # 35
>>> c = Carré(4, "rouge")
>>> c.couleur # 'rouge'
>>> c.long # 4
>>> c.larg # 4
>>> c.aire() # 16
6.1.3. Héritage multiple
On a vu le cas où la fille n'a qu'une seule mère, c'est l'héritage simple.
La classe fille peut avoir plusieurs classes mère, c'est l'héritage multiple. La syntaxe est :
class Fille (Mere1, Mere2, ...) :
...
La fille hérite alors des méthodes de chaque mère.
Il peut y avoir des conflits de méthodes dans l'héritage multiple, c'est-à-dire une même méthode qui apparait plusieurs fois (à éviter...) :
class Chat :
def crier(self) :
print("miaou !")
class Chien :
def crier(self) :
print("ouah !")
class MyPet(Chien, Chat) :
pass
>>> p = MyPet()
>>> p.crier()
Ouah ! # la première classe mère gagne
Comme l'héritage peut être simple ou multiple, éventuellement sur plusieurs niveaux, toute classe possède un Method Resolution Order (MRO), que l'on peut consulter :
>>> Pet.__mro__
(<class '__main__.MyPet'>, <class '__main__.Chien'>, <class '__main__.Chat'>,
<class 'object'>)
On voit ici que la dernière classe est <class 'object'>
:
en effet dans la mécanique interne de Python, toute classe dérive de la
classe racine object
.
6.2. Gel des attributs
Le mécanisme de surcharge d'instance donne une grande souplesse, mais est aussi une cause de bug dans les programmes :
class Tasse :
def __init__(self) :
self.café = 0
self.thé = 0
>>> tasse = Tasse()
>>> tasse.cafe = 1 # on a maintenant tasse.café et tasse.cafe ...
On peut geler une classe en déclarant les attributs possible dans __slots__
:
class Tasse :
__slots__ = ['café', 'thé'] # attribut de classe
def __init__(self) :
self.café = 0
self.thé = 0
>>> tasse = Tasse()
>>> tasse.café = 1 # ok
>>> tasse.cafe = 1 # AttributeError: 'Tasse' object has no attribute 'cafe'
>>> tasse.choco = 1 # AttributeError: 'Tasse' object has no attribute 'choco'
La protection est aussi faite à l'intérieur de la classe :
class Tasse :
__slots__ = ['café', 'thé']
def __init__(self) :
self.café = 0
self.thé = 0
self.lait = 0
tasse = Tasse() # AttributeError: 'Tasse' object has no attribute 'lait'
Comment faire si malgré tout on a besoin de rajouter un attribut ? → par héritage :
class Tasse :
__slots__ = ['café', 'thé']
def __init__(self) :
self.café = 0
self.thé = 0
class Bol(Tasse) :
def __init__(self) :
super().__init__()
self.lait = 0
>>> bol = Bol()
>>> bol.thé # 0
>>> bol.lait # 0
>>> bol.choco = 1
Mais du coup, la classe Bol
n'est pas gelée, donc pas protégée !
→ on peut la geler aussi :
class Tasse :
__slots__ = ['café', 'thé']
def __init__(self) :
self.café = 0
self.thé = 0
class Bol(Tasse) :
__slots__ = ['lait']
def __init__(self) :
super().__init__()
self.lait = 0
>>> bol = Bol()
>>> bol.thé # 0
>>> bol.lait # 0
>>> bol.choco = 1 # AttributeError: 'Bol' object has no attribute 'choco'
Comment voir tous les slots ?
dir(bol) # ... café thé lait
bol.__slots__ # ['lait']
bol.__class__ # <class '__main__.Bol'>
bol.__class__.__bases__ # (<class '__main__.Tasse'>,)
bol.__class__.__bases__[0] # <class '__main__.Tasse'>
bol.__class__.__bases__[0].__slots__ # ['café', 'thé']
Peut-on "forcer" des attributs via __dict__
?
bol.__dict__ # AttributeError: .. no attribute '__dict__'
bol.__dict__ = {'choco' : 1} # AttributeError: .. no attribute '__dict__'
Les valeurs des attributs sont directement mémorisés dans la mémoire de l'objet, pas dans un dictionnaire → code plus efficace.
6.3. Les modules
Dès que le code est un peu long, on a intérêt à découper un programme en modules, pour faciliter la lecture et la réutilisation.
Un module = un fichier python.
def dire_bonjour() :
print("Bonjour !")
# Programme principal
dire_bonjour()
Dans le terminal :
$ python3 hello.py
Bonjour !
En programmation orientée objet, on peut choisir de faire un module par classe
(comme en java), ou de regrouper dans un module des classes qui vont ensemble :
par exemple, un module geometrie.py
avec des classes Point
, Segment
,
Polygone
.
6.3.1. Import sans exécution
Tout fichier python peut être importé comme module, dans le terminal python comme dans un programme ; le but est d'importer ses déclarations (classes, fonctions, etc) afin de pouvoir les utiliser :
>>> import hello
Bonjour !
Problème : cela exécute tout le code en dehors des déclarations des fonctions et des classes, en particulier du programme principal !
Comment l'éviter ? en utilisant la variable __name__
:
def dire_bonjour() :
print("Bonjour !")
# Programme principal
print('__name__', __name__)
dire_bonjour()
On obtient alors une valeur différente lors de l'exécution dans le terminal :
$ python3 hello.py
__name__ __main__
Bonjour !
par rapport à celle obtenue lors de l'import :
>>> import hello
__name__ hello
Bonjour !
>>> __name__
'__main__'
→ La variable __name__
expose le nom du module qui contient le code exécuté.
On écrit donc toujours le programme principal avec une garde sous cette forme (en bleu) :
def dire_bonjour() :
print("Bonjour !")
# Programme principal
if __name__ == '__main__' :
dire_bonjour()
On teste dans le terminal :
$ python3 hello.py
Bonjour ! # programme principal exécuté
et avec un import :
>>> import hello # n'affiche rien, programme principal non exécuté
Remarque : après l'import on peut faire :
>>> help(hello)
>>> help(hello.dire_bonjour)
Et on peut tester en direct les méthodes :
>>> hello.dire_bonjour()
Bonjour !
- Remarque :
- Si on modifie un module, l'interpréteur
python3
n'en tiendra pas compte.
→ il faut fermer et réouvrirpython3
, puis réimporter le module.
6.3.2. Bytecode et cache
Sur certains systèmes tels que Linux,
l'import crée un répertoire __pycache__/
contenant
$ ls -l __pycache__/
-rw-rw-r-- 1 thiel thiel 302 sept. 10 14:21 hello.cpython-38.pyc
$ od -A x -t x1z -v __pycache__/hello.cpython-38.pyc
$ strings __pycache__/hello.cpython-38.pyc
Il s'agit du bytecode compilé du module.
On peut éviter la création des fichiers .pyc
avec : python3 -B
$ rm -rf __pycache__
$ python3 -B
>>> import hello
>>> quit()
$ ls
6.3.3. Autres façons d'importer
-
Importer en renommant :
>>> import hello as foo >>> foo.dire_bonjour() Bonjour ! >>> del foo
-
Importer dans l'espace de noms courant :
>>> from hello import * >>> dire_bonjour() Bonjour !
Inconvénient : conflit de noms, ambiguïté...
-
On peut aussi importer certaines classes ou fonctions :
>>> from hello import dire_bonjour, autres_fonctions, ...
Remarque :
Il ne doit pas y avoir de -
dans un nom de module car pour Python un nom
de module doit être un identificateur.
Solutions :
- renommer le fichier avec des
_
:nom-module.py
→nom_module.py
- ou encore remplacer :
import nom-module
par :nom_module = __import__('nom-module')
6.3.4. Dépendances circulaires
Python gère les dépendances circulaires :
import b
def exclamer(s) :
b.afficher(s+'!')
import a
def afficher(s) :
print(s)
def plop() :
a.exclamer('plop')
if __name__ == '__main__' :
plop()
Dans le terminal :
$ python3 b.py
plop!
6.3.5. Package Python
Un package Python est le nom d'un répertoire contenant un fichier __init__.py
éventuellement vide, et d'autres modules.
Pour utiliser le package, importer le répertoire. Cela provoque l'exécution
du fichier __init__.py
; son rôle peut être d'importer les modules.
→ Un package regroupe des modules et les masque (on ne voit que le package).
Exemple d'un package toto
:
def bingo() :
print('Bingo !')
from .truc import * # ou : from toto.truc import *
Test :
>>> import toto
>>> toto.bingo()
Bingo !
Les modules fournis avec python sont en réalité des packages :
-
math
(fonctions mathématiques réelles)>>> import math >>> math.sqrt(5) 2.23606797749979
-
cmath
(fonctions mathématiques complexes)>>> import cmath >>> cmath.sqrt(5+2j) (2.27872385417085+0.4388421169022545j)
-
re
(expressions régulières)>>> import re >>> s = "numéro 123" >>> p = re.search("^\D+ (?P<num>\d+)", s) >>> print(p.group('num')) 123
-
io
(entrées-sorties) os
(accès au système)sqlite3
(bases de données SQLite)numpy
,scipy
,matplotlib
tkinter
,gi
(pour PyGTK)- ...
Voir https://docs.python.org/3/library/index.html et https://pypi.org/