Attributs privés en Python 8


Tout est accessible en Python. Il n’y a pas de variables privées.

Quand on veut une variable à usage interne, la convention est de la nommer avec un underscore devant :

class Monique:
    def __init__(self):
        self._private = "lessons"

Ça n’empêche rien, mais les devs savent qu’il faut éviter d’utiliser cette variable car elle ne fait pas partie de l’API publique de la classe, et l’implémentation pourrait changer plus tard.

Comme self, c’est une convention forte puisque la plupart des libs de completion du code la prennent en compte.

Il existe une fonctionalité moins connue, qui fait que quand on utilise deux underscores, accéder directement à la variable lève un AttributeError:

class Monique:
    def __init__(self):
        self.__private = "lessons"
 
m = Monique()
print(m.__private)
AttributeError: 'Monique' object has no attribute '__private'

Du coup, quelques rares personnes ont utilisé cette feature pour émuler des attributs privés, ignorant le fait qu’on vous dit partout sur la toile que tout est accessible en Python.

C’est une mauvaise idée.

C’est une mauvaise idée car si votre objectif est la sécurité, ça ne sert à rien puisque votre variable est accessible de tas de manières détournées différentes. Par exemple :

print(m.__dict__['_Monique__private'])
# lessons
m.__dict__['_Monique__private'] = None
print(m.__dict__['_Monique__private'])
# None

C’est une mauvaise idée car si votre objectif est de créer une API publique, un seul underscore suffit.

Enfin c’est une mauvaise idée car si vous fournissez une lib qui ne correspond pas à un besoin, quelqu’un peut toujours contourner le problème en monkey patchant votre code le temps que vous trouviez une solution plus propre. En rendant la variable “privée”, vous rendez ceci plus difficile, sans le rendre impossible. Tout le monde y perd.

8 thoughts on “Attributs privés en Python

  • Morgotth

    Je connaissais pas ce comportement d’AttributeError avec __ :o

    Par contre j’ai eu du mal à comprendre l’exemple avec __dict__ , le préfixe _Monique étant assez déroutant … Peut être que rajouter l’utilisation de vars aiderait :

    class Monique:
       def __init__(self):
           self.a = 1
           self._b = 2
           self.__c = 3
     
    m = Monique()
    print(vars(m))
    # {'_b': 2, 'a': 1, '_Monique__c': 3}
    print(m.__dict__['_Monique__c'])
    # 3

    Rappel : vars affiche les attributs d’un objet sous forme de dictionnaire.

    Et j’ai eu un doute : et si c’était pour cacher aux classes filles les attributs privés ??!

    class Monique:
        def __init__(self):
            self.a = 1
            self._b = 2
            self.__c = 3
     
    class SousMonique(Monique):
        def __init__(self):
            super().__init__()
            self.__d = 4
     
        def parle(self):
            try:
                # Ne marchera jamais
                return "%s et %s" % (self.__c, self.__d)
            except AttributeError:
                return "fix: %s et %s" % (self._Monique__c, self.__d)
     
     
    l = SousMonique()
     
    print(vars(l))
    # {'a': 1, '_Monique__c': 3, '_b': 2, '_SousMonique__d': 4}
     
    print(l.parle())
    # fix: 3 et 4

    L’objectif est sans doute d’imiter le comportement public/protected/private en Java. Mais on est en Python ici, heureusement que je n’utilise jamais de __ pour mes attributs :’)

  • Marien

    On peut noter aussi qu’un from module import * n’importe pas les objets commençant par “_” ou “__”. Il faut le faire explicitement si on veut pouvoir les manipuler.

  • yoshi_120

    Bonjour,

    J’ai testé avec python3 sur ubuntu.
    Acceder a __private provoque une exception.
    Mais si on change la valeur de __private
    un acces ultérieur ne lance plus d’exception

    Est ce normal ?

  • Fred

    Bonjour
    Ben je ne suis pas trop d’accord avec le “c’est une mauvaise idée”.
    Déjà d’une part parce que dans mes projets, toutes mes variables internes à mes classes ont deux underscores. En effet, moi aussi je trouvais ça bien de m’interdire à moi-même d’accèder de façon directe à ces variables “internes”. Bon je connaissais __dict__ donc je savais que ce n’est pas une protection absolue mais ça ne me dérange pas (je me vois mal me hacker moi-même). Et en plus ayant découvert il y a peu de temps les assesseurs (les getters/setters qu’on peut créer via le property()), il m’arrive même maintenant de créer des variables privées mais accessibles de l’extérieur (c’est aussi faisable sans ça, bien sûr, mais c’est plus lourd).
    Et donc tout changer pour ne mettre qu’un seul underscore serait assez fastidieux.

    Et d’autre part, un seul underscore n’empêche pas l’accès direct aux variables. Ce n’est qu’une convention qui dit aux progs de ne pas le faire mais rien ne les empêche de taper à loisir dans le tas. Deux underscores ont au-moins l’avantage de lever l’exception quand on le fait. De toute façon, dans les deux cas (un ou deux underscores) la convention “c’est mal de passer outre” existe et elle est connue. Dans les deux cas (un ou deux underscores), on peut quand-même passer outre si on le désire. Mais dans le second (deux underscores), il y a quand-même l’avantage d’au-moins lever une exception…

  • toub

    J’aime bien le principe de python de ne pas interdire l’accès aux attributs des classes, contrairement à C++, en laissant aux dev le choix d’accéder malgré tout à des attributs auxquels ils ne sont pas sensés accéder.
    Mais je sais aussi que quand tout est accessible, on a vite fait de se retrouver avec du code bien pourri, qu’on ne peut plus faire évoluer parce que les régressions seraient trop importantes

    Du coup j’ai opté pour la solution suivante : j’utilise le __ pour les attributs privés, et le _ pour les attributs protected

  • Remram
    print(m.__dict__['_Monique__private'])

    Pourquoi utiliser __dict__ ici ? Ca marche tout aussi bien sans :

    print(m._Monique__private)
    • Sam Post author

      @Remram : parce que je ne savais pas que ça marchait. Tu m’apprends un truc :)

Leave a comment

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.