Introduction
Lorsque l'on écrit des classes, il est courant de tenter d'encapsuler les détails internes. Cette section présente quelques idiomes de programmation Python pour ce faire, y compris les variables privées et les propriétés.
This tutorial is from open-source community. Access the source code
💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici
Lorsque l'on écrit des classes, il est courant de tenter d'encapsuler les détails internes. Cette section présente quelques idiomes de programmation Python pour ce faire, y compris les variables privées et les propriétés.
L'un des rôles principaux d'une classe est d'encapsuler les données et les détails d'implémentation interne d'un objet. Cependant, une classe définit également une interface publique que le monde extérieur est censé utiliser pour manipuler l'objet. Cette distinction entre les détails d'implémentation et l'interface publique est importante.
En Python, presque tout concernant les classes et les objets est ouvert.
Cela pose un problème lorsque vous essayez d'isoler les détails de l'implémentation interne.
Python repose sur des conventions de programmation pour indiquer l'utilisation prévue de quelque chose. Ces conventions sont basées sur la nomenclature. Il existe une attitude générale selon laquelle il incombe au programmeur d'observer les règles plutôt que de les faire imposer par le langage.
Tout nom d'attribut commençant par _
est considéré comme privé.
class Person(object):
def __init__(self, name):
self._name = 0
Comme mentionné précédemment, il s'agit seulement d'un style de programmation. Vous pouvez toujours y accéder et le modifier.
>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>
En règle générale, tout nom commençant par _
est considéré comme une implémentation interne, que ce soit une variable, une fonction ou un nom de module. Si vous vous trouvez utiliser directement de tels noms, vous faites probablement quelque chose de mal. Recherchez une fonctionnalité de niveau supérieur.
Considérez la classe suivante.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Une caractéristique surprenante est que vous pouvez attribuer à ces attributs n'importe quelle valeur :
>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>
Vous pourriez regarder cela et penser que vous voulez des vérifications supplémentaires.
s.shares = '50' ## Levée d'une TypeError, il s'agit d'une chaîne de caractères
Comment le feriez-vous?
Une approche : introduire des méthodes d'accès.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.set_shares(shares)
self.price = price
## Fonction qui couche l'opération "get"
def get_shares(self):
return self._shares
## Fonction qui couche l'opération "set"
def set_shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
self._shares = value
Dommage que cela brise tout notre code existant. s.shares = 50
devient s.set_shares(50)
Il existe une approche alternative au modèle précédent.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
L'accès aux attributs normaux déclenche désormais les méthodes getter et setter situées sous @property
et @shares.setter
.
>>> s = Stock('IBM', 50, 91.1)
>>> s.shares ## Décenche @property
50
>>> s.shares = 75 ## Décenche @shares.setter
>>>
Avec ce modèle, il n'est pas nécessaire d'apporter de modifications au code source. Le nouveau setter est également appelé lorsqu'il y a une affectation à l'intérieur de la classe, y compris à l'intérieur de la méthode __init__()
.
class Stock:
def __init__(self, name, shares, price):
...
## Cette affectation appelle le setter ci-dessous
self.shares = shares
...
...
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
Il existe souvent une confusion entre une propriété et l'utilisation de noms privés. Bien qu'une propriété utilise internement un nom privé comme _shares
, le reste de la classe (et non la propriété) peut continuer à utiliser un nom comme shares
.
Les propriétés sont également utiles pour les attributs de données calculés.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def cost(self):
return self.shares * self.price
...
Cela vous permet d'omettre les parenthèses supplémentaires, cachant le fait qu'il s'agit en réalité d'une méthode :
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## Variable d'instance
100
>>> s.cost ## Valeur calculée
49010.0
>>>
Le dernier exemple montre comment mettre une interface plus uniforme sur un objet. Si vous ne le faites pas, un objet peut être difficile à utiliser :
>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## Méthode
49010.0
>>> b = s.shares ## Attribut de données
100
>>>
Pourquoi est-il nécessaire d'utiliser ()
pour cost
, mais pas pour shares
? Une propriété peut résoudre ce problème.
La syntaxe @
est connue sous le nom de "décoration". Elle spécifie un modificateur qui est appliqué à la définition de fonction qui suit immédiatement.
...
@property
def cost(self):
return self.shares * self.price
Plus de détails sont donnés dans la Section 7.
__slots__
Vous pouvez restreindre l'ensemble des noms d'attributs.
class Stock:
__slots__ = ('name','_shares','price')
def __init__(self, name, shares, price):
self.name = name
...
Il générera une erreur pour les autres attributs.
>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in?
AttributeError: 'Stock' object has no attribute 'prices'
Bien que cela empêche les erreurs et restreigne l'utilisation des objets, il est en fait utilisé pour les performances et permet à Python d'utiliser la mémoire de manière plus efficace.
Ne poussez pas trop loin l'utilisation d'attributs privés, de propriétés, de slots, etc. Ils ont un but spécifique et vous les rencontrerez peut-être en lisant d'autres codes Python. Cependant, ils ne sont pas nécessaires pour la plupart des codages quotidiens.
Les propriétés sont un moyen utile d'ajouter des "attributs calculés" à un objet. Dans stock.py
, vous avez créé un objet Stock
. Remarquez qu'il y a une légère incohérence dans la manière dont différents types de données sont extraits de votre objet :
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>
Plus précisément, remarquez comment vous devez ajouter les parenthèses supplémentaires à cost
car il s'agit d'une méthode.
Vous pouvez éliminer les parenthèses supplémentaires de cost()
si vous le convertissez en propriété. Prenez votre classe Stock
et modifiez-la de sorte que le calcul du coût fonctionne comme suit :
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>
Essayez d'appeler s.cost()
en tant que fonction et observez qu'elle ne fonctionne plus maintenant que cost
a été défini comme une propriété.
>>> s.cost()
... échoue...
>>>
Apporter ces modifications peut probablement casser votre programme pcost.py
antérieur. Vous devrez peut-être revenir en arrière et éliminer les parenthèses de la méthode cost()
.
Modifiez l'attribut shares
de sorte que la valeur soit stockée dans un attribut privé et qu'une paire de fonctions de propriété soit utilisée pour s'assurer qu'elle est toujours définie sur une valeur entière. Voici un exemple du comportement attendu :
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'un grand nombre'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: valeur entière attendue
>>>
Modifiez la classe Stock
de sorte qu'elle ait un attribut __slots__
. Ensuite, vérifiez qu'il n'est pas possible d'ajouter de nouveaux attributs :
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... voir ce qui se passe...
>>>
Lorsque vous utilisez __slots__
, Python utilise une représentation interne plus efficace des objets. Que se passe-t-il si vous essayez d'inspecter le dictionnaire sous-jacent de s
ci-dessus?
>>> s.__dict__
... voir ce qui se passe...
>>>
Il est important de noter que __slots__
est le plus souvent utilisé comme une optimisation pour les classes servant de structures de données. L'utilisation de slots fera en sorte que de tels programmes utilisent beaucoup moins de mémoire et fonctionnent un peu plus rapidement. Cependant, vous devriez probablement éviter d'utiliser __slots__
pour la plupart des autres classes.
Félicitations! Vous avez terminé le laboratoire sur les classes et l'encapsulation. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.