Définitions fondamentales
Pour manipuler efficacement les données dans un programme orienté objet Python, il est essentiel de distinguer deux types de variables.
- Variables de classe : Déclarées directement dans le corps de la classe, en dehors de toute méthode. Elles sont partagées par toutes les instances créées à partir de cette classe. Leur utilisation est idéale pour les attributs constants ou les configurations communes, comme une espèce animale ou un nom d'antreprise.
- Variables d'instance : Généralement initialisées dans la méthode
__init__en utilisant la syntaxeself.nom_variable. Chaque objet possède sa propre copie de ces données, permettant de stocker des informations uniques comme un nom, un âge ou une référence.
Mécanisme de recherche des attributs
Lors de l'accès à un attribut via un objet ou une classe, Python suit une chaîne de recherche précise.
Accès via une instance (instance.attribut)
- Le programme consulte d'abord l'espace de noms propre à l'instance concernée, représenté par son dictionnaire
__dict__. - Si l'attribut n'est pas trouvé, la recherche se poursuit dans l'espace de noms de la classe à laquelle l'instance appartient.
- Ensuite, Python parcourt la chaîne d'héritage (les classes parentes) jusqu'à atteindre la classe racine
object. - Si l'attribut demeure introuvable dans toute la hiérarchie, une exception
AttributeErrorest levée.
Accès via une classe (Classe.attribut)
- La recherche s'effectue uniquement dans l'espace de noms de la classe spécifiée.
- Si nécessaire, elle remonte ensuite dans la chaîne d'héritage.
- L'attribut n'est jamais recherché dans les dicitonnaires des instances individuelles.
Démonstrations pratiques
Imaginons une classe Voiture pour illustrer ces concepts.
class Voiture:
# Attribut de classe : partagé par toutes les instances
categorie = "Véhicule terrestre"
options = [] # Liste mutable partagée
def __init__(self, marque, annee_modele):
# Attributs d'instance : spécifiques à chaque objet
self.marque = marque
self.annee_modele = annee_modele
def ajouter_option(self, option):
self.options.append(option) # Modification via l'instance
# Création de deux objets distincts
auto1 = Voiture("Citroën", 2020)
auto2 = Voiture("Peugeot", 2022)
Exemple 1 : Lecture d'un attribut de classe
# Par la classe directement
print(f"Categorie via la classe : {Voiture.categorie}") # Véhicule terrestre
# Par une instance (recherche étendue)
print(f"Categorie de auto1 : {auto1.categorie}") # Véhicule terrestre
print(f"Categorie de auto2 : {auto2.categorie}") # Véhicule terrestre
Analyse de la recherche pour auto1.categorie :
- Python inspecte
auto1.__dict__, qui ne contient que{'marque': 'Citroën', 'annee_modele': 2020}. - Il passe à la classe
Voiture, trouvecategoriedans son dictionnaire et renvoie la valeur.
Exemple 2 : Lecture d'un attribut d'instance
print(f"Marque de auto1 : {auto1.marque}") # Citroën
print(f"Marque de auto2 : {auto2.marque}") # Peugeot
# Tentative d'accès direct via la classe échoue
try:
print(Voiture.marque)
except AttributeError as e:
print(f"Erreur : {e}") # type object 'Voiture' has no attribute 'marque'
Analyse pour auto1.marque : L'atribut est trouvé immédiatement dans le dictionnaire de l'instance auto1. La recherche s'arrête là.
Exemple 3 : Modifications et pièges potentiels
3.1 Modification d'un objet mutable partagé
# État initial
print(f"Options initiales (classe) : {Voiture.options}") # []
# Ajout via une instance
auto1.ajouter_option("GPS")
# Conséquences
print(f"Options après ajout par auto1 : {auto1.options}") # ['GPS']
print(f"Options vues par auto2 : {auto2.options}") # ['GPS'] -- Modifié aussi !
print(f"Options de la classe : {Voiture.options}") # ['GPS']
Explication : L'appel self.options.append(...) dans la méthode ajouter_option modifie l'objet liste pointé par l'attribut de classe. Puisque toutes les instances et la classe partagent la même référence, la modification est globale.
3.2 Affectation créant une variable d'instance
# Affectation via une instance
auto1.categorie = "Véhicule utilitaire"
# Résultats
print(f"Catégorie de auto1 : {auto1.categorie}") # Véhicule utilitaire
print(f"Catégorie de auto2 : {auto2.categorie}") # Véhicule terrestre (inchangé)
print(f"Catégorie de la classe : {Voiture.categorie}") # Véhicule terrestre
# Le dictionnaire de auto1 est mis à jour
print(f"Dictionnaire de auto1 : {auto1.__dict__}")
# Contient maintenant {'marque': 'Citroën', 'annee_modele': 2020, 'categorie': 'Véhicule utilitaire'}
Explication : L'affectation auto1.categorie = ... crée une nouvelle entrée dans le dictionnaire de l'instance auto1. Cet attribut d'instance "masque" l'attribut de classe pour cet objet spécifique uniquement.
Synthèse des comportements
| Action | Via une instance (obj.attr) |
Via la classe (Classe.attr) |
|---|---|---|
| Lecture | 1. Recherche dans l'instance 2. Puis dans la classe/héritage | Recherche directe dans la classe/héritage |
| Modification d'objet mutable | Modifie l'objet partagé (si l'instance n'a pas son propre attribut) | Modifie l'objet partagé de la classe |
Affectation (=) |
Crée/met à jour un attribut d'instance, masquant l'attribut de classe | Met à jour l'attribut de la classe |
Recommandations
-
Pour modifier des données devant être cohérentes pour tous les objets, utilisez systématiquement la syntaxe
Classe.attributpour une lisibilité accrue. -
Préférez l'initialisation dans
__init__pour les attributs logiquement liés à l'instance, même avec une valeur par défaut. Cela garantit une isolation des données par objet. ```python# Approche recommandée pour des listes indépendantes class BonneVoiture: def __init__(self, marque): self.marque = marque self.equipements = [] # Liste propre à chaque instance -
Soyez vigilant avec les attributs de classe de type collection mutable (
list,dict), car toute modification via une instance affecte tous les objets partageant cet attribut.