Un attribut en Python : la donnée que porte un objet

Un attribut est une donnée accrochée à un objet et atteinte après le point. Il dit ce que l'objet porte, là où la méthode dit ce qu'il sait faire.
5 min de lecture
Believemy logo

Définition

Un programme manipule rarement une valeur isolée. Un compte bancaire, ce n'est pas un nombre : c'est un titulaire, un solde, une devise. Rangées dans des variables séparées, elles doivent voyager ensemble dans chaque fonction qui touche au compte, et rien n'empêche d'y mélanger le solde d'un client et le nom d'un autre.

L'attribut règle cet ennui en accrochant la donnée à l'objet. C'est un nom que l'on atteint en écrivant un point suivi de ce nom, et qui appartient à un objet précis : là où une variable vit seule dans son espace de noms, un attribut suit son objet partout.

PYTHON
class Compte:
    def __init__(self, titulaire):
        self.titulaire = titulaire   # deux attributs posés
        self.solde = 0               # à la création de l'objet

compte = Compte("Ada")
print(compte.titulaire)   # Ada : lecture d'un attribut
compte.solde = 250        # écriture dans l'attribut

Le point déclenche une recherche, pas une simple lecture. Python cherche le nom sur l'objet, puis sur sa classe, puis sur les classes dont elle hérite. C'est cette remontée qui rend les méthodes, écrites une seule fois sur la classe, disponibles sur chaque objet.

Quand le nom ne se trouve nulle part, Python lève une AttributeError qui indique l'attribut manquant et le type réel de l'objet. Ce dernier point est souvent le plus utile : il révèle qu'on ne tenait pas l'objet qu'on croyait.


Un attribut, une méthode, et ce qui les sépare

Les deux s'écrivent après un point, ce qui brouille la frontière plus souvent qu'on ne le croit. Il n'existe pourtant qu'une catégorie : une méthode est un attribut dont la valeur se laisse appeler. Tout ce qui suit un point est un attribut, les fonctions comprises.

Le tableau ci-dessous met côte à côte quatre écritures presque identiques à l'oeil : ce qui les sépare n'est pas le point, ce sont les parenthèses.

ÉcritureCe que Python en fait
compte.soldeRenvoie la valeur rangée sous ce nom
compte.crediterRenvoie la fonction elle-même, sans l'exécuter
compte.crediter(50)Exécute la fonction, l'objet en premier paramètre
compte.solde()Tente d'appeler un nombre, et lève une TypeError

Oublier les parenthèses ne plante pas sur le coup, et c'est ce qui rend l'erreur coûteuse : une fonction est une valeur ordinaire, que Python range sans protester. Le programme continue avec une fonction là où il attendait un résultat, et la panne se déclare bien plus loin, en comparaison jamais vraie ou en affichage commençant par <bound method.


Attribut d'instance et attribut de classe

Reste à savoir où écrire un attribut : les deux emplacements possibles ne donnent pas le même résultat.

Un attribut posé dans __init__ à travers self appartient à une instance et à elle seule : chaque objet reçoit son exemplaire, et le modifier ne regarde que lui.

Un attribut écrit dans le corps de la classe appartient à la classe, donc à toutes ses instances à la fois : il n'en existe qu'un seul exemplaire.

PYTHON
class Compte:
    devise = "EUR"          # attribut de classe, partagé : voulu
    operations = []         # attribut de classe, partagé : piège

    def __init__(self, titulaire):
        self.titulaire = titulaire   # attribut d'instance, un par objet

Sur devise, le partage est recherché. Sur operations, il est accidentel : la liste étant mutable, tous les comptes y ajoutent leurs opérations et les historiques se mélangent, avec un symptôme reconnaissable, un objet tout neuf qui arrive déjà rempli.

Attention

Un attribut de classe mutable, liste ou dictionnaire, ne se voit pas tant qu'un seul objet existe : le défaut apparaît quand plusieurs coexistent, donc en production. La correction tient en une ligne, créer la structure dans __init__.

L'écriture, elle, ne remonte jamais vers la classe : compte.devise = "USD" crée un attribut d'instance qui masque celui de la classe, sans rien changer pour les autres comptes. Retenez l'asymétrie, la lecture peut venir de la classe, l'écriture atterrit toujours sur l'instance.


Les manipuler quand le nom varie

Le point exige un nom écrit en dur, décidé au moment où vous tapez le code. Or il arrive parfois d'un formulaire ou d'un fichier de configuration, et n'est connu qu'à l'exécution. Trois fonctions prennent alors le relais : getattr pour lire, setattr pour écrire, hasattr pour vérifier la présence.

PYTHON
champ = "solde"
getattr(compte, champ)            # 250, comme compte.solde
getattr(compte, "taux", 0)        # 0 : valeur de repli, aucune erreur
setattr(compte, champ, 300)       # équivaut à compte.solde = 300
hasattr(compte, "titulaire")      # True

Le troisième argument de getattr mérite une mention : il fournit une valeur de repli et évite l'exception au lieu de la rattraper, ce qui rend le code lisible sur les champs facultatifs.

Pour inspecter un objet dont on ignore la forme, vars(compte) renvoie le dictionnaire de ses attributs d'instance, et dir(compte) la liste complète des noms accessibles, héritage compris. Dans le REPL, ils vont plus vite qu'une recherche dans la documentation.


Questions fréquentes

Question

Peut-on ajouter un attribut après la création de l'objet ?

Oui, à tout moment, et c'est ce qui rend la faute de frappe silencieuse : compte.slode = 250 ne signale rien, il crée un attribut de plus pendant que le vrai solde reste à zéro. Déclarer les champs attendus avec dataclass, ou fixer __slots__, referme cette porte.

Question

Comment rendre un attribut privé ?

Python n'offre aucune protection véritable, et c'est un choix assumé. Un tiret bas en tête de nom signale une convention que le reste du code est prié de respecter, sans rien interdire. Deux tirets bas compliquent l'accès depuis l'extérieur sans le fermer. Pour contrôler vraiment la lecture et l'écriture, passez par une property.

Question

Pourquoi mon attribut vaut-il autre chose que ce que j'y ai mis ?

Dans la grande majorité des cas, deux objets partagent la même valeur mutable, venue d'un attribut de classe ou d'une valeur par défaut de fonction. Comparez id() sur l'attribut des deux objets : un nombre identique signale une seule valeur, modifiée depuis deux endroits.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.