property et attribut privé

Introduction

Un étudiant à qui j'ai répondu avait des interrogations sur property, setter, attribut avec un simple/double underscore...

J'ai tenté de lui faire une réponse le plus claire possible, sans trop être dans le détail.

Une histoire de convention

Regardons ce code :

class Compte:
    def __init__(self, nom, numero, balance):
        self.nom = nom
        self._numero = numero
        self.balance = balance

    @property
    def numero(self):
        return self._numero

    @numero.setter
    def numero(self, numero):
        raise AttributeError("On ne change pas le numéro de compte !")

john = Compte(nom="John Smith", numero="123456", balance=20000)

L'underscore permet de signaler aux autres développeurs qu'il ne faut pas accéder directement à l'attribut depuis l'extérieur de la classe. On appelle ça rendre l'attribut privé.
Mais en réalité celui qui ne veut pas respecter cette convention peut y accéder quand même, car c'est juste une convention.

En effet, il est tout à fait possible d'afficher l'attribut privé :

print(john._numero)

Le double underscore c'est autre chose : on appelle ça le name mangling. Python rend l'accès à l'attribut plus difficile en gros. Mais en réalité, on peut y accéder quand même.

Si j'essaye de passer "normalement" par mon instance, Python lèvera un AttributeError :

print(john.__numero)

L'attribut est donc plus "difficile" d'accès, il faudrait en fait faire :

print(john._Compte__numero)

En résumé : que ce soit _ ou __, cela indique qu'il ne faut pas accéder à l'attribut en dehors de la classe.

Ce qui m'amène à dire qu'il ne faut pas modifier directement l'attribut, et donc passer par ce que l'on appelle un setter :

@numero.setter
def numero(self, numero):
    raise AttributeError("On ne change pas de numéro de compte")

Property et Setter

Ce qui est intéressant avec un setter c'est qu'on peut mettre de multiples conditions. Avec un autre exemple :

class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("L'âge ne peut pas être négatif")
        else:
            self._age = value

Il faut procéder ainsi pour passer par le setter :

# Création de l'instance
personne = Personne("Patrick", 30)
# Modification avec le setter
personne.age = -5
# Lève ValueError: L'âge ne peut pas être négatif

Le décorateur @property nous permet d'accéder à notre attribut privé _age comme s'il s'agissait d'un attribut normal, tout en gardant le contrôle sur sa lecture et sa modification.

Donc dans notre cas :

p = Personne("Alice", 25)
print(p.age)      # Utilise le getter 
p.age = 30        # Utilise le setter 
p.age = -5        # Lève une ValueError

Il est important de noter que pour créer un setter avec @property, la méthode doit porter le même nom que la property suivie de .setter. Par exemple, si notre property s'appelle age, notre méthode de modification s'appellera obligatoirement @age.setter. Cette convention de nommage est obligatoire pour que Python fasse le lien entre les deux méthodes.

Nous avons donc vu plusieurs façons de contrôler l'accès à nos attributs en Python :

  • Le simple underscore comme simple convention,
  • Le double underscore pour complexifier l'accès,
  • Les properties qui offrent une solution claire et élégante.

C'est un premier pas vers ce qu'on appelle l'encapsulation.


Retour