Classes et encapsulation

PythonPythonBeginner
Pratiquer maintenant

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

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.

Public vs Private.

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.

Un problème

En Python, presque tout concernant les classes et les objets est ouvert.

  • Vous pouvez facilement inspecter les détails internes de l'objet.
  • Vous pouvez modifier les choses à votre guise.
  • Il n'y a pas de notion forte de contrôle d'accès (c'est-à-dire de membres de classe privés)

Cela pose un problème lorsque vous essayez d'isoler les détails de l'implémentation interne.

Encapsulation en Python

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.

Attributs privés

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.

Attributs simples

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?

Attributs gérés

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)

Propriétés

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
>>>

Accès uniforme

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.

Syntaxe du décorateur

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.

Attribut __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.

Commentaires finaux sur l'encapsulation

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.

Exercice 5.6 : Propriétés simples

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().

✨ Vérifier la solution et pratiquer

Exercice 5.7 : Propriétés et mutateurs

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
>>>
✨ Vérifier la solution et pratiquer

Exercice 5.8 : Ajout de slots

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.

✨ Vérifier la solution et pratiquer

Sommaire

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.