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 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 = 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