La signification des traits de soulignement _ en Python
- 2021-07-06
- Publié par : Christophe DELEUZE
- Catégorie : Python
En python, le trait de soulignement (underscore) _ est omniprésent partout, souvent sous forme simple _ ou double __.
Les traits de soulignement simples et doubles ont une signification dans les noms de variables et de méthodes Python. Une partie de cette signification réside simplement dans la convention de nommage recommandée par Python au travers de la PEP 8. Cette convention a un rôle informatif important pour le programmeur. Une autre partie de la signification des traits de soulignement est utilisée par l’interpréteur Python.
Dans cet article, nous allons étudier en détail tout ce qui concerne les traits de soulignement et la manière dont ils informent et affectent le comportement de vos programmes Python.
Les cas d’usages sont les suivants :
- Un seul trait de soulignement en préfixe :
_variable; - Un seul trait de soulignement en suffixe :
variable_; - Deux traits de soulignement en préfixe :
__variable; - Deux traits de soulignement en préfixe et suffixe :
__variable__; - Un seul trait de soulignement unique :
_.
Un seul trait de soulignement en préfixe : _variable
En ce qui concerne les noms de variables et de méthodes, le préfixe de soulignement unique n’a de sens que par convention. C’est un indice pour le programmeur. Cela signifie que la communauté Python convient de ce que cela devrait signifier, mais cela n’affecte pas le comportement de vos programmes.
Le préfixe de soulignement est destiné à indiquer à un autre programmeur qu’une variable ou une méthode commençant par un seul trait de soulignement est destinée à un usage interne. Cette convention est définie dans le PEP 8.
Ce n’est pas imposé par Python. Python ne fait pas de distinctions fortes entre les variables “privées” et “publiques” comme le fait Java.
Prenons l’exemple suivant :
class Test:
def __init__(self):
self.foo = 11
self._bar = 23
Que va-t-il se passer si vous instanciez cette classe et essayez d’accéder aux attributs foo et _bar définis dans le constructeur __init__ de la classe Test ?
>>> t = Test()
>>> t.foo
11
>>> t._bar
23
Vous venez de voir que dans les deux cas, rien ne nous empêche d’accéder aux valeurs des attributs de la classe. C’est parce que le trait de soulignement utilisé en préfixe n’est qu’une convention convenue, du moins en ce qui concerne les noms de variables et de méthodes.
Cependant, ils ont un impact sur la façon dont les noms sont importés à partir des modules. Imaginez que vous ayez le code suivant dans un module appelé mon_module :
# mon_module.py
def fonction_externe():
return 23
def _fonction_interne():
return 42
Désormais, si vous utilisez une importation générique pour importer tous les noms du module, Python n’importera pas les noms commençant avec un trait de soulignement (à moins que le module ne définisse une liste __all__ qui remplace ce comportement) :
>>> from mon_module import *
>>> fonction_externe()
23
>>> _fonction_interne()
NameError: "name '_fonction_interne' is not defined"
Pour rappel, les importations génériques (*) doivent être évitées, car elles ne permettent pas de connaître quels noms sont présents dans l’espace de nommage. Il est donc préférable de s’en tenir aux importations régulières par souci de clarté.
Contrairement aux importations génériques, les importations régulières ne sont pas affectées par la convention de nommage du trait de soulignement utilisé en préfixe :
>>> import mon_module
>>> mon_module.fonction_externe()
23
>>> mon_module._fonction_interne()
42
Si vous vous en tenez à la recommandation PEP 8 selon laquelle les importations génériques doivent être évitées, alors tout ce que vous devez retenir est cela :
Les traits de soulignement simples sont une convention de nommage Python indiquant qu’un nom est destiné à un usage interne. Il n’est généralement pas appliqué par l’interpréteur Python et est uniquement destiné au programmeur.
Un seul trait de soulignement en suffixe : variable_
Parfois, le nom le plus approprié pour une variable est déjà pris par un mot-clé. Par exemple, prenons le cas du mot class qui ne peut pas être utilisé comme nom de variable en Python. Dans ce cas, vous pouvez ajouter un seul trait de soulignement pour résoudre le conflit de nom :
>>> def fonction(nom, class):
SyntaxError: "invalid syntax"
>>> def fonction(nom, class_):
... pass
En résumé, le trait de soulignement utilisé en suffixe sert par convention à éviter les conflits de nom avec les mots-clés Python. Cette convention est expliquée dans la PEP 8.
Deux traits de soulignement en préfixe : __variable
Les modèles de nommage que nous avons couverts jusqu’à présent ont reçu leur signification uniquement à partir de conventions convenues. Avec les attributs de classe Python (variables et méthodes) qui commencent par des doubles traits de soulignement (diminutif : dunders – pour double underscores), les choses sont un peu différentes. Un double préfixe de soulignement oblige l’interpréteur Python à réécrire le nom de l’attribut afin d’éviter les conflits de nom dans les sous-classes.
Il est également appelé le mutilateur de noms name mangling. Quand il est présent, l’interpréteur change le nom de la variable de manière à rendre plus difficile de créer des conflits lorsqu’une classe sera étendue plus tard.
Voici un petit exemple de code que nous pouvons utiliser pour l’expérimentation :
class Test:
def __init__(self):
self.foo = 11
self._bar = 23
self.__baz = 23
Regardons les attributs de cet objet à l’aide de la fonction native dir() :
>>> t = Test()
>>> dir(t)
['_Test__baz', '__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', '_bar', 'foo']
Cela nous donne une liste avec les attributs de l’objet. Prenons cette liste et recherchons nos noms de variables d’origine foo, _bar, et __baz.
La variable self.foo apparaît non modifiée dans la liste des attributs : foo.
La variable self._bar se comporte de la même manière : il apparaît dans la classe sous la forme _bar. Comme je l’ai déjà dit, le trait de soulignement seul utilisé en préfixe n’est qu’une convention.
Cependant, avec self.__baz, les choses semblent un peu différentes. Lorsque vous recherchez __baz dans cette liste, vous verrez qu’il n’y a pas de variable avec ce nom.
Mais si vous regardez attentivement, vous verrez qu’il y a un attribut appelé _Test__baz sur cet objet. C’est le nom mutilé que l’interpréteur Python a appliqué à self.__baz. Il l’a fait pour empêcher que la variable ne soit remplacée dans les sous-classes.
Créons une autre classe qui étend la classe Test et tente de remplacer ses attributs existants ajoutés dans le constructeur :
class TestEtendu(Test):
def __init__(self):
super().__init__()
self.foo = 'écrasé'
self._bar = 'écrasé'
self.__baz = 'écrasé'
Maintenant, vérifions les valeurs de foo, _bar, et __baz de cette classe :
>>> t2 = TestEtendu()
>>> t2.foo
'écrasé'
>>> t2._bar
'écrasé'
>>> t2.__baz
AttributeError: "'TestEtendu' object has no attribute '__baz'"
Vous remarquerez que la mutilation de nom a encore frappé ! Il s’avère que l’objet __baz n’a même pas d’attribut. Vérifions-le :
>>> dir(t2)
['_TestEtenduTest__baz', '_Test__baz', '__class__', '__delattr__',
'__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', '_bar', 'foo', 'get_vars']
Comme vous pouvez le voir, l’interpréteur à transformer self.__baz en _TestEtenduTest__baz pour éviter toute modification accidentelle :
>>> t2._TestEtendu__baz
'écrasé'
Mais l’attribut original _Test__baz est toujours là :
>>> t2._Test__baz
42
L’utilisation du double soulignement du nom est totalement transparent pour le programmeur. Constatez-le par vous-même avec l’exemple suivant qui vous le confirmera :
class Test:
def __init__(self):
self.__variable = 'salut'
def get_variable(self):
return self.__variable
>>> Test().get_variable()
'salut'
>>> Test().__variable
AttributeError: "'Test' object has no attribute '__variable'"
La modification des noms s’applique également aux noms de méthode. De manière générale, la modification des noms affecte tous les noms commençant par deux caractères de soulignement dans un contexte de classe :
class Test:
def __methode(self):
return 42
def appel_methode(self):
return self.__method()
>>> Test().__method()
AttributeError: "'Test' object has no attribute '__method'"
>>> Test().appel_methode()
42
Voici un autre exemple, peut-être surprenant, de mutilation de noms :
_TestGlobal__variable = 23
class TestGlobal:
def test(self):
return __variable
>>> TestGlobal().test()
23
Dans cet exemple, j’ai déclaré une variable globale appelée _TestGlobal__variable. Ensuite, j’ai accédé à la variable dans le contexte d’une classe nommée TestGlobal. En raison de la modification du nom, j’ai pu référencer la variable globale _TestGlobal__variable comme étant juste __variable à l’intérieur de la méthode de la classe test().
L’interpréteur Python a automatiquement transformé le nom __variable en _TestGlobal__variable car il commence par deux caractères de soulignement. Cela démontre que la modification des noms n’est pas spécifiquement liée aux attributs de classe. Elle peut s’appliquer à tout nom commençant par deux caractères de soulignement utilisés dans un contexte de classe.
Deux traits de soulignement en préfixe et suffixe : __variable__
Peut-être étonnamment, la modification des noms n’est pas appliquée si un nom commence et se termine par un double trait de soulignement. Les variables entourées d’un préfixe et d’un suffixe à double soulignement ne sont pas affectées par l’interpréteur Python :
class PrefixeSuffixeTest:
def __init__(self):
self.__univers__ = 42
>>> PrefixPostfixTest().__univers__
42
Cependant, les noms qui ont à la fois des traits de soulignement doubles de début et de fin sont réservés à un usage spécial dans le langage de programmation. Cette règle couvre des choses comme __init__ pour les constructeurs d’objets ou __call__ pour rendre un objet appelable.
Autant que possible, il est préférable d’éviter d’utiliser des noms qui commencent et se terminent par des doubles traits de soulignement (dunders) dans vos propres programmes pour éviter de futurs conflits avec les futures évolutions du langage Python.
Un seul trait de soulignement unique : _
Par convention, un seul trait de soulignement autonome est parfois utilisé comme nom pour indiquer qu’une variable est temporaire ou insignifiante.
Par exemple, dans la boucle suivante, nous n’avons pas besoin d’accéder à l’index courant et nous pouvons utiliser _ pour indiquer qu’il ne s’agit que d’une valeur temporaire :
>>> for _ in range(32):
... print('Salut !')
Vous pouvez également utiliser des traits de soulignement simples quand vous faites de l’unpacking (déballage) d’expressions en tant que variables indifférentes pour ignorer des valeurs particulières. Encore une fois, cette signification et cet usage n’existe uniquement que par convention et il n’y a pas de comportement spécial déclenché dans l’interpréteur Python. Le trait de soulignement unique est simplement un nom de variable valide qui est parfois utilisé à cette fin.
Dans l’exemple de code suivant, je décompresse (unpack) un cartuple en variables distinctes, mais je ne m’intéresse qu’aux valeurs de couleur et kilometrage. Cependant, pour que l’expression de décompression réussisse, je dois affecter toutes les valeurs contenues dans le tuple à des variables. C’est là que _ est utile comme variable d’espace réservé :
>>> voiture = ('rouge', 'automobile', 12, 3812.4)
>>> couleur, _, _, kilometrage = voiture
>>> couleur
'red'
>>> kilometrage
3812.4
>>> _
12
Outre son utilisation en tant que variable temporaire, _ est une variable spéciale dans la plupart des Interpréteurs interactifs Python. Elle représente le résultat de la dernière expression évaluée par l’interpréteur.
C’est très pratique si vous travaillez dans une session d’interprétation et que vous souhaitez accéder au résultat d’un calcul précédent. Ou si vous construisez des objets à la volée et que vous souhaitez interagir avec eux sans leur attribuer de nom au préalable :
>>> 20 + 3
23
>>> _
23
>>> print(_)
23
>>> list()
[]
>>> _.append(1)
>>> _.append(2)
>>> _.append(3)
>>> _
[1, 2, 3]
Le mot de la fin
Avant de vous laisser revenir à votre vie normale, voici un résumé rapide de ce que signifient les cinq modèles de soulignement qui ont été abordés dans cet article :
| Schéma | Exemple | Sens |
|---|---|---|
| Un seul trait de soulignement en préfixe | _variable |
La convention de nommage indiquant un nom est destinée à un usage interne. Généralement non appliqué par l’interpréteur Python (sauf dans les importations génériques) et destiné uniquement à servir d’indice au programmeur. |
| Un seul trait de soulignement en suffixe | variable_ |
Utilisé par convention pour éviter les conflits de nom avec les mots-clés Python. |
| Deux traits de soulignement en préfixe (dunder) | __variable |
Déclenche la modification du nom lorsqu’il est utilisé dans un contexte de classe. Renforcé par l’interpréteur Python. |
| Deux traits de soulignement en préfixe et suffixe (dunders) | __variable__ |
Indique des méthodes spéciales définies par le langage Python. Évitez ce schéma de nommage pour vos propres attributs. |
| Un seul trait de soulignement unique | _ |
Parfois utilisé comme nom pour des variables temporaires ou insignifiantes (« pas d’attention »). Également : le résultat de la dernière expression dans un REPL Python. |
Voilà, si j’ai raté quelque chose dans cette explication ou si vous avez des remarques à faire, les commentaires sont là pour vous.
Bonjour,
Il y a une erreur dans la section “Deux traits de soulignement en préfixe”.
Au lieu de “t2._ExtendedTest__baz” il faudrait “t2._TestEtendu__baz”.
Merci pour cette page très instructive.
Christian Le Corre
Merci beaucoup de ton retour. C’est corrigé. Si tu vois d’autres trucs, n’hésites pas 🙂 !