Cours de Python / Distinguer attributs d’instance et attributs de classe

Un attribut d’instance conserve l’état propre à un objet ; un attribut de classe fournit une valeur accessible à travers la classe. Pour donner à chaque objet sa propre liste modifiable, créez-la dans __init__ avec self.items = [] plutôt que dans le corps de la classe.

Identifier où une valeur est stockée

La classe Ledger représente ici un registre de montants. Son attribut currency est défini une fois dans le corps de la classe. À l’inverse, owner et items sont affectés à self pendant l’initialisation : chaque appel crée ici une nouvelle liste pour le nouvel objet.

Pour ces attributs ordinaires, lire a.currency utilise la valeur de classe si a n’a pas sa propre valeur de ce nom. Affecter b.currency = "USD" crée un attribut sur b et masque la valeur de classe pour cet objet ; cela ne remplace pas Ledger.currency. Cette règle décrit les classes simples de l’exercice ; des propriétés peuvent définir un comportement différent pour l’accès aux attributs.

Ne confondez pas affectation et modification d’un objet : a.items.append(3500) modifie la liste trouvée lors de la lecture de items. Si plusieurs instances trouvent la même liste de classe, elles voient donc la même modification. La documentation Python sur les variables de classe et d’instance explique cette différence de stockage.

Exécuter des registres indépendants

Enregistrez ce programme complet en UTF-8 dans attributes.py, dans un dossier d’exercice. Aucune dépendance externe n’est requise. Depuis ce dossier, lancez python attributes.py avec l’environnement virtuel du projet sélectionné ; utilisez python3 attributes.py si python3 est votre commande habituelle hors environnement virtuel sous macOS ou Ubuntu.

class Ledger:
    currency = "KRW"

    def __init__(self, owner):
        self.owner = owner
        self.items = []

    def add(self, amount):
        self.items.append(amount)

a = Ledger("A")
b = Ledger("B")
a.add(3500)
print(a.items, b.items)
b.currency = "USD"
print(a.currency, b.currency, Ledger.currency)

Résultat attendu :

[3500] []
KRW USD KRW

Seule la liste de a reçoit 3500. Les deux registres ont été initialisés séparément : a.items is b.items vaut False. La seconde ligne montre ensuite trois lectures différentes : a utilise encore le libellé de classe KRW, b possède USD et la classe conserve KRW. Ces libellés n’effectuent aucune conversion de montants.

Ajoutez temporairement Ledger.currency = "EUR" après b.currency = "USD" et avant le dernier print. La seconde ligne devient EUR USD EUR : b garde son propre attribut. Retirez la ligne pour rétablir le résultat initial. L’objet n’a pas reçu une copie automatique de tous les attributs de classe lors de sa création ; la lecture dépend des attributs présents au moment où elle a lieu.

Comparer une liste partagée et une liste par objet

Enregistrez ce deuxième programme complet dans un fichier distinct, sharing_demo.py, pour conserver attributes.py. Lancez-le depuis le même dossier avec python sharing_demo.py ou la commande python3 correspondante.

class Shared:
    items = []
class Separate:
    def __init__(self):
        self.items = []
a, b = Shared(), Shared()
a.items.append(1)
print(a.items, b.items)
x, y = Separate(), Separate()
x.items.append(1)
print(x.items, y.items)

Résultat attendu :

[1] [1]
[1] []

Dans Shared, a et b accèdent à la même liste : a.items is b.items vaut True. Dans Separate, les créations de x et y ont chacune exécuté self.items = [] ; leur identité de liste diffère. Les noms a et b de ce second fichier désignent ses propres objets Shared, indépendamment du premier exercice.

Pour voir la distinction entre masquer et modifier, ajoutez à la fin de sharing_demo.py a.items = [], puis print(a.items, b.items). Cette ligne supplémentaire affiche [] [1]. a possède maintenant une nouvelle liste d’instance ; b lit toujours la liste de classe. Retirez les deux lignes pour retrouver les deux sorties initiales.

Le problème peut aussi venir d’une liste fournie par l’appelant. Si un initialiseur stocke directement un argument avec self.items = items, il garde cette même liste ; il n’en crée pas une copie. Une copie superficielle telle que items.copy() sépare le conteneur extérieur mais conserve les références vers les objets imbriqués. Pour une liste de listes, modifier une liste intérieure peut donc rester visible des deux côtés. Choisissez la profondeur de copie selon le contrat de votre objet, comme l’explique la documentation des copies superficielles et profondes.

Reproduire un partage accidentel et le corriger

  1. Dans attributes.py, déplacez la création items = [] dans le corps de Ledger, au même niveau d’indentation que currency. Retirez la ligne self.items = [] de __init__, mais conservez self.owner et la méthode add.
  2. Relancez un nouveau processus. La première ligne devient [3500] [3500], car add modifie désormais la liste commune. La seconde ligne reste KRW USD KRW : le masquage de currency n’a pas changé.
  3. Supprimez items du corps de la classe et rétablissez self.items = [] dans __init__. Relancez pour vérifier [3500] [], puis les trois libellés initiaux.

Une affectation d’instance oubliée lors du déplacement empêcherait de reproduire le partage : elle masquerait la liste de classe. Vérifiez donc ensemble la définition, l’initialisation et l’opération effectuée sur la valeur. Les règles de références partagées et copies restent les mêmes en dehors des classes.

La méthode add de cet exercice ajoute le montant reçu sans le valider. Si un registre doit refuser valeurs négatives, chaînes ou booléens, définissez cette règle et le moment du contrôle ; l’indépendance des listes ne garantit pas la validité de leur contenu. Indiquez aussi clairement si une devise appartient à chaque registre ou constitue un réglage commun. Un argument d’initialisation explicite peut éviter un masquage difficile à suivre.

Une collection partagée peut être volontaire, mais sa durée de vie et les modifications concurrentes doivent alors être prévues. Pour des écritures propres à chaque registre, une liste créée par instance exprime directement l’intention et évite qu’un objet modifie accidentellement les données d’un autre.