Aller au contenu

Algo & Prog 1 Python : CM séance 04

4. Programmation orientée objet

La Programmation orientée objet (POO) est un paradigme de programmation, qui consiste à manipuler des briques logicielles appelées objets.

Un objet possède à la fois :

  • des données internes, appelées attributs ;
  • des fonctions pour les manipuler, appelées méthodes.

Le type class permet de déclarer un objet en Python.

4.1. Déclaration et instanciation

Exemple : on déclare la classe Toto

class Toto :
    pass

Notez la majuscule (convention pour dire que c'est une classe définie par le programmeur).

C'est un nouveau type :

>>> type(Toto)
<class 'type'>

On peut ensuite fabriquer un objet de la classe Toto, mémorisé à l'aide d'une variable t :

>>> t = Toto()

>>> type(t)
<class '__main__.Toto'>
>>> isinstance (t, Toto)
True

On dit que l'objet désigné par t est une instance de la classe (= un exemplaire) ; en fabriquant t, on dit qu'on a instancié la classe.

Conventions de nommage

Définies dans la PEP 8 :

  • NomDeLaClasse : en CamelCase, première lettre en majuscule
  • instances, attributs et méthodes : en snake_case, première lettre minuscule
  • CONSTANTES : en TRUMP_CASE, tout en majuscule

4.2. Attributs d'instance

Chaque instance peut mémoriser des attributs, c'est-à-dire des variables internes :

class Toto :
    pass

>>> t = Toto()
>>> t.ga = "bu"
>>> t.zo = "meu"
>>> t.ga
'bu'

On dit que l'on a surchargé l'instance avec des attributs.

Ces attributs n'appartiennent qu'à cette instance :

>>> u = Toto()
>>> u.ga
AttributeError: 'Toto' object has no attribute 'ga'

Mais où sont stockés ces attributs ? Dans ce dictionnaire automatique :

>>> t.__dict__
{'ga': 'bu', 'zo': 'meu'}
>>> t.__dict__['zo']
'meu'
>>> for cle, val in t.__dict__.items() :
        print("t.", cle, " = ", val, sep="")
t.ga = bu
t.zo = meu

On peut donc rajouter aussi des attributs à t en faisant t.__dict__[clé] = valeur :

>>> t.__dict__['foo'] = 'bar'
>>> t.foo
'bar'

4.3. Méthodes

Une classe peut définir des méthodes, c'est-à-dire des fonctions attachées aux objets de la classe.

Exemple : création d'une méthode saluer

class Hello :
    def saluer(self, qui) :
        print("Hello", qui, "!")

>>> h = Hello()
>>> h.saluer("World")
Hello World !

Notez le paramètre self, obligatoire au début de chaque méthode ; il désigne l'instance courante quand une méthode est appelée.

Qui fournit le paramètre self lors d'un appel de la méthode ? Quand on écrit

>>> h.saluer("World")

c'est exactement comme si on avait appelé :

Hello.saluer(h, "World")

4.4. Constructeur et destructeur

La méthode __init__ est une méthode spéciale, appelée le constructeur de la classe (par abus de langage : c'est en fait l'initialisateur).

Elle est appelée automatiquement à chaque instanciation.

Exemple :

class Bingo :
    def __init__(self) :
        print("bingo !")

>>> b = Bingo()
bingo !
>>> c = Bingo()
bingo !

⚠  écrire __init__ avec 2 _ devant et derrière, et non _init_.

Il existe aussi la méthode __del__ appelée chaque fois qu'une instance est détruite, on l'appelle le destructeur.

Exemple :

class Bingo :
    def __init__(self) :
        print("bingo !")
    def __del__(self) :
        print("instance détruite")

>>> b = Bingo()
bingo !
>>> del b               # on détruit explicitement la variable
instance détruite
>>> c = Bingo()
bingo !
>>> c = 1             # on affecte une autre valeur -> l'objet n'est plus
instance détruite     # référencé -> il est automatiquement détruit
>>> d = Bingo()
bingo !
>>> e = d             # maintenant d et e désignent le même objet
>>> d = 1             # autre valeur -> objet non détruit car encore référencé
>>> e = 1             # autre valeur -> objet détruit car plus référencé
instance détruite

4.5. Initialisation avec paramètres

class Point :
    def __init__(self, px, py) :
        print("Initialisation d'un point")
        self.x = px
        self.y = py

>>> p = Point()
TypeError: __init__() missing 2 required positional arguments: 'px' and 'py'
>>> p = Point(3, 5)
Initialisation d'un point
>>> p.x
3

Lorsqu'on appelle p = Point(3,5), la méthode __init__ est appelée avec les paramètres p, 3, 5 (plus exactement, le futur p, sous le nom self).

La méthode __init__ surcharge ensuite l'instance courante self avec les attributs x et y, en y mémorisant les paramètres px et py.

Comme la méthode __init__ est appelée automatiquement à chaque instanciation, ceci garantit que chaque instance de la classe possédera des attributs x et y.

On peut maintenant utiliser ces attributs dans d'autres méthodes :

class Point :
    def __init__(self, px, py) :
        print("Initialisation d'un point")
        self.x = px
        self.y = py
    def afficher(self) :
        print (self.x, self.y)

>>> p = Point(3,5)
Initialisation d'un point
>>> p.afficher()
3 5
En résumé :

La syntaxe générale est :

class NomDeLaClasse :
    def __init__ (self, paramètres_création) :
        # corps du constructeur :
        # mémorisation dans des attributs : self.foo = ...

    def __del__ (self) :
        # corps du destructeur optionnel

    def autre_methode (self, autres_paramètres) :
        # corps méthode
        # utilisation des attributs : self.foo

Le paramètre self est passé en premier à chaque méthode, il désigne l'instance courante (this en java et en C++).

Instanciation = création d'une instance de la classe :

instance = NomDeLaClasse (paramètres_creation)

Appel d'une méthode :

instance.autre_methode (autres_paramètres)

4.6. Tout est objet

Tout est objet en python (sauf les opérateurs) :

  • les valeurs littérales :

    >>> type(1)                 # <class 'int'>
    >>> type("")                # <class 'str'>
    
  • les instances :

    >>> a = 1                   # ou a = int(1)
    >>> type(a)                 # <class 'int'>
    >>> isinstance (a, int)     # True
    
  • les types :

    >>> type(int)               # <class 'type'>
    >>> type(str)               # <class 'type'>
    
  • les fonctions :

    >>> def f() : pass
    >>> type(f)                 # <class 'function'>
    
  • les exceptions :

    >>> type(ValueError)        # <class 'type'>
    >>> type(Exception)         # <class 'type'>
    

Mais on ne peut pas rajouter des attributs à leurs instances : ces classes sont gelées.

>>> a = 1
>>> a.toto = 2
AttributeError: 'int' object has no attribute 'toto'

On peut examiner une classe ou une instance avec dir() :

>>> dir(Point)
>>> p = Point(3,5)
>>> dir(p)          # il y a en plus x, y

Les attributs entourés de doubles _ (comme __init__) sont réservés, et ont un rôle spécial. Exemple :

>>> p.__class__.__name__    # 'Point'

Les widgets de TkInter sont des classes :

>>> import tkinter
>>> type(tkinter.Tk)        # <class 'type'>
>>> type(tkinter.Canvas)    # <class 'type'>
>>> dir(tkinter.Canvas)     # méthodes nombreuses !

On peut surcharger les objets TkInter (servira en TP) :

zone_dessin = tkinter.Canvas(...)
zone_dessin.foo = bar

4.7. Attributs de classes

Ce sont des attributs partagés entre toutes les instances.

class Pouce :
    cm = 2.54   # attribut de classe

    def __init__ (self, x) :
        self.x = x

    def convertir (self) :
        print (self.x, "pouces =", self.x*self.cm, "centimètres")

>>> p = Pouce(3)
>>> p.convertir()       # 3 pouces = 7.62 centimètres
>>> q = Pouce(5)
>>> q.convertir()       # 5 pouces = 12.7 centimètres

>>> Pouce.cm            # 2.54
>>> Pouce.cm = 8
>>> p.cm ; q.cm         # 8 8

⚠ Attention :

>>> p.cm = 10
>>> p.cm ; q.cm         # 10 8
>>> Pouce.cm = 12
>>> p.cm ; q.cm         # 10 12

Explication :

>>> Pouce.__dict__      # .... 'cm': 12
>>> p.__dict__          # {'cm': 10, 'x': 3}
>>> q.__dict__          # {'x': 5}

→ affecter p.cm = 10 a surchargé l'instance, ce qui a masqué l'attribut de classe.

>>> del p.cm
>>> p.cm                # 12

>>> del Pouce.cm
>>> p.cm                # AttributeError

4.8. Représentations

Chaque objet a une valeur str par défaut :

class Angle :
    def __init__(self, degre) :
        self.degre = degre

>>> a = Angle(180)
>>> print(a)            # équivalent à : print(str(a))
<__main__.Angle object at 0x7f09c1d3e820>

On peut redéfinir la représentation de la classe = le résultat de str() :

class Angle :
    def __init__(self, degre) :
        self.degre = degre
    def __str__(self) :
        return str(self.degre)+'°'

>>> a = Angle(180)
>>> str(a)
'180°'
>>> print(a)
180°

Par contre,

>>> a                                        # équivalent à : repr(a)
<__main__.Angle object at 0x7f09c1d3e820>
>>> repr(a)
<__main__.Angle object at 0x7f09c1d3e820>

On peut également redéfinir la représentation d'une instance par repr() :

class Angle :
    def __init__(self, degre) :
        self.degre = degre
    def __str__(self) :
        return str(self.degre)+'°'
    def __repr__(self) :
        return "Instance de Angle valant "+str(self)

>>> a = Angle(120)
>>> print(a)
120°
>>> a
Instance de Angle valant 120°

4.9. Surcharge des opérateurs

Dans notre exemple précédent, on ne peut pas encore utiliser les opérateurs de calcul :

>>> a = Angle(180)
>>> b = Angle(30)
>>> a+b
TypeError: unsupported operand type(s) for +: 'Angle' and 'Angle'

Pour que cela soit possible il faut d'abord définir l'opérateur +.

class Angle :
    def __init__(self, degre) :
        self.degre = degre
    def __str__(self) :
        return str(self.degre)+'°'
    def __add__(self, other) :
        if not isinstance(other, Angle) :
            raise TypeError("type opérande non supporté par + : " +\
                "Angle et "+str(type(other)))
        res = self.degre + other.degre
        return Angle(res)

>>> a = Angle(180)
>>> b = Angle(30)
>>> c = a+b
>>> print(c)
210°

Test avec un mauvais type :

>>> d = a+50
TypeError: type opérande non supporté par + : Angle et <class 'int'>

Solution :

class Angle :
    ...
    def __add__(self, other) :
        if isinstance(other, Angle) :
            res = self.degre + other.degre
        elif isinstance(other, (int, float)) :
            res = self.degre + other
        else :
            raise TypeError("type opérande non supporté par + : " +\
                "Angle et "+str(type(other)))
        return Angle(res)

>>> a = Angle(180)
>>> b = Angle(30)
>>> c = a + b + 15 + 1.234
>>> print(c)
226.234°

Conclusion : toujours prévoir une garde, sous la forme d'un test de compatibilité de types, lorsqu'on surcharge des opérateurs.

On peut ainsi redéfinir tous les opérateurs de calcul.

Par exemple, on peut redéfinir l'opérateur += d'auto-addition avec __iadd__(self, other), en modifiant puis renvoyant self, de manière à conserver la même référence :

class Angle :
    ...
    def __iadd__(self, other) :
        res = self.__add__(other)   # ou encore : res = self + other
        self.degre = res.degre
        return self

>>> a = Angle(180)
>>> b = Angle(30)
>>> a += b
>>> print(a)
210°

Autres opérateurs de calculs :

+   __add__(self, other)         +=   __iadd__(self, other)
-   __sub__(self, other)         -=   __isub__(self, other)
*   __mul__(self, other)         *=   __imul__(self, other)
**  __pow__(self, other)         **=  __ipow__(self, other)
/   __truediv__(self, other)     /=   __itruediv__(self, other)
//  __floordiv__(self, other)    //=  __ifloordiv__(self, other)
%   __mod__(self, other)         %=   __imod__(self, other) 

Opérateurs unaires de signe : pas besoin de garde

-       __neg__(self)
+       __pos__(self)
abs()   __abs__(self)

Opérations logiques :

not()       __not__(self)
and         __and__(self, other)
or          __or__(self, other)

Opérateurs de comparaison :

==   __eq__(self, other)
!=   __ne__(self, other)
<    __lt__(self, other)
>    __gt__(self, other)
<=   __le__(self, other)
>=   __ge__(self, other)

Opérateurs de conteneur :

len(instance)                   __len__(self)
other in instance               __contains__(self, other)

instance[clé] ou 
instance.get(clé)               __getitem__(self, clé)
instance[clé] = valeur ou 
instance.set(clé, valeur)       __setitem__(self, clé, valeur) 
del instance[clé]               __delitem__(self, clé)

On peut presque tout surcharger... sauf l'affectation.

Voir : https://docs.python.org/3/reference/datamodel.html#special-method-names