Py: OOP 3 Attributs
Py: OOP 3 Attributs

Programmation orienté objet

Directory

serie

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!

Suite