Skip to article frontmatterSkip to article content

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:

réutilisabilité & héritage

une autre approche consiste à écrire du code générique

dans les deux cas, pour bien comprendre les classes en Python, il faut comprendre deux mécanismes fondamentaux, qui sont

espaces de nom

et pour commencer parlons des espaces de nom:

espaces de nom - pourquoi

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:

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

obj.attribute = ...

i.e. à gauche d’une affectation

attributs en lecture

obj.attribute

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

résolution d’attribut pour la lecture

pour la lecture par contre, le mécanisme de résolution des attributs est plus élaboré

lecture: recherche de bas en haut

pour la lecture :
la règle pour chercher un attribut en partant d’un objet consiste à

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

%%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)
Loading...

résumé

donc dans ce cas simple de la classe Vector et de l’instance vector:

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

%%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)
Loading...

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 !

obj.liste += ['foo']

écriture

obj.liste.append('foo')

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

isinstance() et issubclass()

# 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()

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é

annexe: MRO & graphe d’héritage

(très avancé)

graphe d’héritage

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

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass