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 attributobjet

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.methode

  • l’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:

_images/variable-attribut.svg

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 foo d’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 programme

  • par 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’objet foo soit un espace de nom, et disons, pour faire simple à ce stade, qu’il contienne l’attribut bar; bien entendu le processus est répété pour trouver tutu à 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

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

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 Vector a les attributs

    • __init__

    • length

  • l’objet vector a les attributs

    • x et y,

    • 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)