Licence CC BY-NC-ND, Thierry Parmentelat & Arnaud Legout
from IPython.display import HTML
HTML(filename="_static/style.html")
attributs & héritage#
pour réutiliser du code en python#
DRY = don’t repeat yourself : cut’n paste is evil
fonctions |
pas d’état après exécution |
modules |
garde l’état, une seule instance par programme |
classes |
instances multiples, chacune garde l’état, héritage |
programmation orientée objet#
pourquoi et comment ?
objectif |
réutilisabilité, donc |
comment |
modularité & héritage (a.k.a. espaces de nom et recherche d’attribut) |
réutilisabilité & modularité#
une façon d’écrire du code modulaire:
regrouper le code dans une classe et les données dans un objet
de cette façon, la classe constitue un tout cohérent (modularité)
l’encapsulation consiste à séparer l’interface (les méthodes) et l’implémentation
le code qu utilise la classe n’utilise que l’interface
ce qui permet de garantir certains invariants
et de cette façon on se réserve le droit de changer l’implémentation,
sans avoir avoir à modifier le code qui utilise la classe
remarques
plus on découpe en petits morceaux, plus on a de chances de pouvoir réutiliser
en Python, l’encapsulation est moins contraignante que des langages plus dogmatiques comme C++; ici pas de public/protected/private, on se base sur des conventions de nommage
réutilisabilité & héritage#
une autre approche consiste à écrire du code générique
par exemple, un moteur de jeu fait “avancer” une collection d’objets
et fournit quelques objets de base, facilement extensibles (grâce à l’héritage)
dès qu’un objet explique comment il avance, il peut faire partie du jeu
un exemple intéressant
un exemple de librairie qui utilise massivement ce trait est la librairie arcade
et par exemple ce petit programme de démo
pour écrire un jeu on n’a pas besoin d’écrire la boucle d’événements (mainloop), c’est du code générique
on se contente d’hériter des classes fournies par la librairie
dans les deux cas, pour bien comprendre les classes en Python, il faut comprendre deux mécanismes fondamentaux, qui sont
la notion d’espace de nom
et la recherche d’attributs
espaces de nom#
et pour commencer parlons des espaces de nom:
tous les objets qui sont un module, une classe ou une instance
constituent chacun un espace de nom, i.e. une association attribut → objet
enfin presque
ce n’est pas le cas pour les instances des types natifs, mais bon..
espaces de nom - pourquoi#
permet de lever l’ambigüité en cas d’homonymie
par ex. si 2 modules utilisent tous les 2 une globale
truc, elles peuvent coexister sans souci
les espaces de nom sont imbriqués (nested) - par ex.
package.module.classe.methodel’héritage rend cela dynamique, i.e. la résolution des attributs est faite à runtime
variables et attributs#
ce qui nous donne l’occasion d’insister sur ceci; c’est assez basique mais ça va mieux en le disant:
dans l’expression foo.bar.tutu(), il y a une différence fondamentale dans la nature de la variable et des attributs:
la variable est recherchée dans le code du programme; on parlera un peu plus tard de la notion de portée des variables, mais pour faire court on va chercher la variable
food’abord dans la fonction où se trouve ce code (y compris les paramètres), puis si on ne trouve pas dans la fonction englobante, etc.. on parle de liaison lexicale, un terme bien savant qui veut juste dire qu’on peut savoir avec certitude à quoi correspond la variable rien qu’en lisant le programmepar contre, et c’est le point qui nous importe, pour calculer
barà partir de (l’objet référencé par)foo, le calcul est fait à run-time, c’est-à-dire à l’exécution - on parle de liaison dynamique: il faut que l’objetfoosoit un espace de nom, et disons, pour faire simple à ce stade, qu’il contienne l’attributbar; bien entendu le processus est répété pour trouvertutuà partir de (l’objet référencé par)foo.bar
c’est simplifié
on va voir justement que la recherche d’attributs est plus compliquée que ça, mais l’important est de bien comprendre la différence entre les deux types de liaison:
variable |
liaison lexicale, en remontant dans le code |
attribut |
liaison dynamique, en remontant dans les espaces de nom |
lecture ou écriture des attributs#
nous allons voir cela en détail tout de suite, et pour cela il nous faut distinguer deux cas
attribut en écriture |
|
i.e. à gauche d’une affectation |
attributs en lecture |
|
les autres cas |
écriture d’attribut: pas de recherche#
quand on écrit un attribut dans un objet, le mécanisme est simple:
on écrit directement dans l’espace de nom de l’objet
exemple typique
en entrant dans le constructeur, l’objet self n’a pas encore l’attribut name
et quand on écrit self.name = name, on le crée
lecture ou écriture ?
on considère que c’est une écriture si le terme obj.attribute est à gauche d’une affectation
résolution d’attribut pour la lecture#
pour la lecture par contre, le mécanisme de résolution des attributs est plus élaboré
fil ournit la mécanique de base de la POO
et sous-tend notamment (mais pas que) la mécanique de l’héritage
lecture: recherche de bas en haut#
pour la lecture :
la règle pour chercher un attribut en partant d’un objet consiste à
le chercher dans l’espace de nom de l’objet lui-même
sinon dans l’espace de nom de sa classe
sinon dans les super-classes (on verra les détails plus loin)
cas particulier des dunders
on a vu que le langage peut faire une recherche implicite de dunder -
c’est le cas par exemple quand on recherche __iter__ parce que l’objet est itéré,
ou qu’on cherche __call__ parce que l’objet est appelé
dans ces cas-là on ne regarde pas dans l’objet lui-même
c’est une subtilité, qui ne va pas trop nous concerner dans ce chapitre, mais dont on verra l’impact dans la section sur les métaclasses
ex1. de résolution d’attribut#
# cas simple sans héritage
# appel d'une méthode
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def length(self):
return math.sqrt(self.x**2 + self.y**2)
# quand on cherche vector.length
# on cherche
# 1. dans vector - pas trouvé
# 2. dans Vector - bingo
vector = Vector(3, 4)
vector.length()
5.0
# on va voir ça en détail
# dans pythontutor
%load_ext ipythontutor
2 espaces de nom distincts#
la classe
Vectora les attributs__init__length
l’objet
vectora les attributsxety,mais pas
length!
%%ipythontutor width=1000 height=400 curInstr=7
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def length(self):
return math.sqrt(self.x**2 + self.y**2)
vector = Vector(2, 2)
les fonctions vars() et dirs()
ce n’est pas forcément à retenir, mais c’est utile si on essaie d’inspecter les espaces de nom sans ipythontutor:
avec la fonction builtin
vars(obj), on peut inspecter le contenu d’un espace de noms (et seulement lui)avec la builtin
dirs(obj)on peut cette fois accéder à l’ensemble des attributs qui sont disponibles surx, c’est donc la somme des attributs trouvés:dans l’espace de nom de
xdans l’espace de nom de sa classe
et de ses super-classes
enfin notez que, quand on se livre à ce genre d’introspection, on enlève souvent, pour clarifier, les attributs qui contiennent __
résumé#
donc dans ce cas simple de la classe Vector et de l’instance vector:
vector.xfait référence à l’attribut posé directement sur l’instancevector.lengthfait référence à la méthode qui est dans la classe
ex2. résolution d’attribut avec héritage#
jusqu’ici on n’a pas d’héritage puisque pour l’instant on n’a qu’une classe
mais l’héritage est une simple prolongation de cette logique
on verra un peu plus loin la syntaxe pour créer une sous-classe, mais voici déjà un premier exemple simplissime (et un peu bidon du coup)
# ici pour l'instant, une classe fille sans aucun contenu
class SubVector(Vector):
pass
subvector = SubVector(6, 8)
# grâce à l'héritage on peut tout à fait écrire ceci
subvector.length()
10.0
comment fait-on pour trouver subvector.length ? c’est exactement le même mécanisme qui est à l’oeuvre ! pour évaluer subvector.length(), on cherche l’attribut length
dans l’instance
subvector: nondans sa classe
SubVector: nondans la super-classe
Vector: ok, on prend ça
%%ipythontutor width=1000 height=400 curInstr=8
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def length(self):
return math.sqrt(self.x**2 + self.y**2)
class SubVector(Vector):
pass
subvector = SubVector(6, 8)
lecture vs écriture - cas limites#
(avancé)
il faut se méfier parfois: il y a écriture si et seulement si il y a affectation; du coup dans les deux phrases suivantes, qui semblent pourtant faire la même chose, en réalité la mécanique est totalement différente !
|
écriture |
|
lecture ! |
alors même que dans les deux cas il y a bien modification des données, évidemment
héritage#
syntaxe#
une classe peut hériter d’une (ou plusieurs) autre classes
# la syntaxe est
class Class(Super):
pass
# ou
class Class(Super1, Super2):
pass
si A hérite de B, ont dit que
A est une sous-classe de B
et B est la super-classe de A
de ce qui précède:
la sous-classe hérite (des attributs) de sa (ses) super-classe(s)
l’instance hérite de la classe qui la crée
isinstance() et issubclass()#
isinstance(x, class1)retourneTruesixest une instance declass1ou d’une super classeissubclass(class1, class2)retourneTruesiclass1est une sous-classe declass2
préférez isinstance() plutôt que type()
pour savoir si un objet est d’un certain type, on peut aussi penser à utiliser type(obj) is Class
toutefois il est de beaucoup préférable le plus souvent de faire isinstance(obj, Class) et cela pour deux raisons
d’abord parce
isinstance()vérifie aussi les super-classes deClassensuite, mais c’est de la commodité, on peut passer plusieurs types à isinstance, comme e.g.
isinstance(x, (int, float)
# A est la super-classe
class A:
pass
class B(A):
pass
a, b = A(), B()
isinstance(a, A), issubclass(B, A)
(True, True)
isinstance(b, A), isinstance(a, B)
(True, False)
# accepte plusieurs types/classes
isinstance(a, (A, B))
True
super()#
utile lorsque la spécialisation
consiste à ajouter ou modifier
par rapport à la classe mèrele cas typique est d’ailleurs le constructeur
dès qu’on ajoute un attribut de donnéepermet de ne pas mentionner explicitement
le nom de la classe mère (code + générique)
super() dans le constructeur#
# illustration de super()
# dans le constructeur
class C:
def __init__(self, x):
print("init x par superclasse")
self.x = x
class D(C):
def __init__(self, x, y):
# initialiser : la classe C
super().__init__(x)
print("init y par classe")
self.y = y
c = C(10)
init x par superclasse
d = D(100, 200)
init x par superclasse
init y par classe
super() dans une méthode standard#
# super() est souvent rencontrée
# dans __init__ mais s'applique
# partout
class C:
def f(self):
print('f dans C')
class D(C):
def f(self):
# remarquez l'absence
# de self !
super().f()
print('f dans D')
c = C(); c.f()
f dans C
d = D(); d.f()
f dans C
f dans D
résumé#
les instances et classes sont des objets mutables (sauf classes builtin)
chaque instance et chaque classe est un espace de nom
lorsqu’on écrit un attribut, on écrit directement dans l’espace de nom de cet objet
en lecture, on résoud la référence d’un attribut de bas en haut
on utilise
isinstance()pour tester le type d’un objetune méthode peut faire référence à la super-classe avec
super()en général
les classes ont des attributs de type méthode
les objets ont des attributs de type donnée
mais le modèle est flexible, dans le notebook suivant on va voir quelques exceptions notables
annexe: MRO & graphe d’héritage#
(très avancé)
graphe d’héritage#
on peut donc construire un graphe d’héritage
allant des super-classes aux instances
class C1:
pass
class C2:
pass
class C(C1, C2):
def func(self, x):
self.x = 10
o1 = C()
o2 = C()
MRO: method resolution order#
lors de la recherche, si on ne trouve pas dans l’objet ni dans sa classe, il faut décider dans quel ordre on recherche dans les super-classes - pour le cas pathologique où l’attribut serait présent dans plusieurs d’entre elles
on utilise pour cela le MRO : method resolution order; l’algorithme est le suivant
liste toutes les super-classes en utilisant un algorithme DFLR (depth first, left to right)
si classe dupliquée, ne garder que la dernière occurrence
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
parcours DFLR:
D,B,A,object,C,A,objectsuppressions :
D,B,,A,objectC,A,object