Programmation orienté objet
Directory
serie
- partie 1: Introduction
- partie 2: En pratique
- partie 3: Attributs$ \Leftarrow $
- partie 4: Héritage
- partie 5: Héritage multiple et composition
- partie 6: Méthodes magiques
sources
Attributs de classe
Un attribut de classe est accessible par la classe (sans instance):
class Poule(object):
attribut_de_classe = 'valeur'
>>> print(Poule.attribut_de_classe)
valeur
Mais également par une instance:
>>> poupoule = Poule()
>>> print(poupoule.attribut_de_classe)
valeur
>>> print(Poule().attribut_de_classe)
valeur
Attributs mutables et non-mutables
En attendant une meilleur explication:
Il semble que Python a un comportement différent pour les attributs de classe mutables et non-mutables.
Mutables
Les attributs de classe mutables sont l’équivalent les variables statiques en C++. Une modification d’un attribut de classe dans une instance ou directement de la classe modifie l’attribut de tous les objets de la classe et de la classe elle-même:
L’attribut de classe est aussi conservé lors de l’héritage, et partagé avec les classes filles (sauf lorsque les classes filles redéfinissent l’attribut, de la même manière que pour les instances).
class Cl(object):
l = []
def display():
print("i1: {} @ {}".format(i1.l, hex(id(i1.l))))
print("i2: {} @ {}".format(i2.l, hex(id(i2.l))))
print("Cl: {} @ {}\n".format(Cl.l, hex(id(Cl.l))))
i1 = Cl()
i2 = Cl()
display()
i1.l.append(1)
display()
i2.l.append(7)
display()
Cl.l.append(4)
display()
i3 = Cl()
print("i3: {} @ {}".format(i3.l, hex(id(i1.l))))
output:
i1: [] @ 0x7f2f4a693988
i2: [] @ 0x7f2f4a693988
Cl: [] @ 0x7f2f4a693988
i1: [1] @ 0x7f2f4a693988
i2: [1] @ 0x7f2f4a693988
Cl: [1] @ 0x7f2f4a693988
i1: [1, 7] @ 0x7f2f4a693988
i2: [1, 7] @ 0x7f2f4a693988
Cl: [1, 7] @ 0x7f2f4a693988
i1: [1, 7, 4] @ 0x7f2f4a693988
i2: [1, 7, 4] @ 0x7f2f4a693988
Cl: [1, 7, 4] @ 0x7f2f4a693988
i3: [1, 7, 4] @ 0x7f2f4a693988
Non-mutables
Les attributs de classe non-mutable obéissent à une rêgle:
- Si nous modifions l’attribut au niveau de l’instance, seul l’instance voit les modifications.
- Si nous modifions l’attribut au niveau de la classe, la classe et toutes les instances créées après ont la nouvelle valeur.
class Cl(object):
s = "poule"
def display():
print("i1: {} @ {}".format(i1.s, hex(id(i1.s))))
print("i2: {} @ {}".format(i2.s, hex(id(i2.s))))
print("Cl: {} @ {}\n".format(Cl.s, hex(id(Cl.s))))
i1 = Cl()
i2 = Cl()
display()
i1.s = "cochon"
display()
i2.s = "vache"
display()
Cl.s = "poney"
display()
i3 = Cl()
print("i3: {} @ {}".format(i3.s, hex(id(i1.s))))
output:
i1: poule @ 0x7fc524f02928
i2: poule @ 0x7fc524f02928
Cl: poule @ 0x7fc524f02928
i1: cochon @ 0x7fc52396a6f8
i2: poule @ 0x7fc524f02928
Cl: poule @ 0x7fc524f02928
i1: cochon @ 0x7fc52396a6f8
i2: vache @ 0x7fc52396a6c0
Cl: poule @ 0x7fc524f02928
i1: cochon @ 0x7fc52396a6f8
i2: vache @ 0x7fc52396a6c0
Cl: poney @ 0x7fc52396a7a0
i3: poney @ 0x7fc52396a6f8
Utilisation
- Stocker les pseudo constantes: Il n’y a pas de vrai constantes en Python! On parle de constante quand une variable est écrite toute en majuscules (convention) et qu’elle est sensé ne jamais changer.
class Poule:
NOMBRE_DE_PATTES = 2
On met NOMBRE_DE_PATTES
comme attribut de classe et c’est essentiellement à titre informatif. Pour nous ça ne change rien mais pour quelqu’un qui va utiliser le code, il sait qu’il peut chercher toutes les constantes liées à Poule
en faisant Poule.VARIABLE_EN_MAJUSCULE
.
- Partager des données entre les instances:
Pour faire un compteur d’objets ou éviter des calcules innutiles comme dans l’exemple suivant qui est un terrible exemple par manque d’inspiration:
class Rectangle(object):
_cache = {}
def __init__(self, l, L):
self.id = str(l)+str(L)
self.l = l
self.L = L
self.calculer_aire()
def calculer_aire(self):
# si l'id n'est pas dans le cache,
# on fait le calcule et ajoute au cache.
if self.id not in self._cache:
self._cache[self.id] = self.l * self.L
>>> r1 = Rectangle(2,3)
calcule necessaire pour r1
>>> r2 = Rectangle(6,4)
calcule necessaire pour r2
>>> r3 = Rectangle(6,4)
>>> r4 = Rectangle(7,2)
calcule necessaire pour r4
>>> print(Rectangle._cache)
{'23': 6, '64': 24, '72': 14}
À noter qu’on appele la variable _cache
et non pas cache
. C’est encore une convention. Ça signale à celui qui utilise la classe que cette variable est utilisée en interne et qu’il n’a pas à savoir ce qu’elle fait (comparable a private en C++ en beaucoup moins bien) et qu’il ne faut pas la toucher.
Méthodes de classe
Pour rappel, les méthodes de classe sont comme les attributs de classe, des méthodes qui n’ont pas besoin d’instance pour être appelées:
class Rectangle(object):
_cache = {}
def __init__(self, nom, l, L):
self.id = str(l)+str(L)
self.nom = nom
self.l = l
self.L = L
self.calculer_aire()
def calculer_aire(self):
if self.id not in self._cache:
print("calcule necessaire pour", self.nom)
self._cache[self.id] = self.l * self.L
@classmethod
def cache(cls): # ajout d'une methode pour acceder "proprement"
return cls._cache # à l'attribut de classe "privé"
>>> r1 = Rectangle(2,3)
calcule necessaire pour r1
>>> r2 = Rectangle(6,4)
calcule necessaire pour r2
>>> r3 = Rectangle(6,4)
>>> r4 = Rectangle(7,2)
calcule necessaire pour r4
>>> print(Rectangle.cache())
{'23': 6, '64': 24, '72': 14}
Le premier paramètre n’est pas l’objet en cours mais la classe en cours et c’est tout ce dont nous avons besoin ici pour accéder à POULES
comme c’est un attribut de classe.
Méthodes statiques
Le décorateur @staticmethod
nous permet de rendre une méthode statique. C’est une méthode de classe sans aucun paramètre qui lui est passé automatiquement (self ou cls).
class Osef(object):
@staticmethod
def quoi(): # pas de cls, pas de self
print("rien")
>>> print(Osef.quoi())
rien
Avantage: Un peu plus rapide que la méthode de classe. On peut totalement négliger son utilisation.
N’a pas accès aux attributs de classe, méthodes de classe ou méthodes statiques!
En gros, c’est comme une fonction déclarée au sein d’une classe.
Retour sur les propriétés
Encore un rappel: Une property est simplement un déguisement qu’on met sur une méthode pour qu’elle ressemble à un attribut.
class Poule(object):
@property
def cri(self):
return "Cot cot"
>>> maggy = Poule()
>>> print(maggy.cri)
Cot cot
Mais on peut aller plus loin que la lecture. On peut décorer l’écriture et la suppression!
class Poule(object):
def __init__(self):
self._cri = "Cot cot" # privé ! underscore !
@property
def cri(self):
print('(get)cri')
return self._cri
@cri.setter # le décorateur a la meme nom que la méthode
def cri(self, value): # value est la valeur à droite du `=` quand on set (rValue)
print('set to {}'.format(value))
self._cri = value
@cri.deleter
def cri(self):
print('delete')
self._cri = None
>>> maggy = Poule()
>>> print(maggy.cri)
(get)cri
Cot cot
>>> maggy.cri = 'Coty Cota'
set to Coty Cota
>>> print(maggy.cri)
Coty Cota
>>> print(maggy._cri)
Coty Cota
>>> del maggy.cri
delete
>>> print(maggy.cri)
None
Autre exemple:
class Poule(object):
def __init__(self):
self._cri = 'COT COT'
@property
def cri(self):
return self._cri
@cri.setter
def cri(self, value):
self._cri = value.upper()
>>> maggy = Poule()
>>> print(maggy.cri)
COT COT
>>> maggy.cri = "coty cota"
>>> print(maggy.cri)
COTY COTA
On peut donc “intercepter” la récupération, la suppression et la modification d’un attribut facilement. Cela permet d’exposer une belle API à base d’attributs mais deière faire des traitements complexes.
Il suffit de transformer un attribut normal en méthode et de la décorer avec @property
C’est pour cette raison qu’on a jamais de getter et de setter en Python: on utilise les attributs tels quel et si le besoin se présente, on en fait des propriétés.
Sans le paramètre object
de la classe les setter et deleter ne fonctionnent pas!