cours/sources/Variables, scopes et closures en Python - Bibliothèque - Zeste de Savoir.md
Oscar Plaisant 602a41e7f8 update
2024-12-25 22:30:24 +01:00

721 lines
34 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
Cliped: 2024-05-13 10:07
Source: https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/
tags:
- s/informatique/langage/python
Comment:
---
Il est un sujet en Python qui concentre beaucoup dinterrogations : celui de la gestion des variables.
Les variables sont lun des premiers mécanismes que lon rencontre en Python, mais on peut facilement constater que leur fonctionnement est souvent mal compris. Malheureusement, cela est dû à des explications souvent trompeuses voire erronées dans les cours enseignant le Python. Les variables forment donc un point dincompréhension majeur du langage dont vont découler beaucoup dautres, notamment en raison de la gestion des *scopes* et des variables mutables.
Ce cours a pour but de vous expliquer le fonctionnement des variables en Python et de leur portée. Il devrait notamment vous permettre de répondre à des questions du type «Pourquoi cette variable est modifiée à mon insu ? » ou « À quoi sert le mot-clé `global` ? ».
Lidée de ce cours mest venue après des années à parcourir des forums dentraide spécialisés en Python, où je tombais régulièrement sur des problèmes similaires. Ce tutoriel est ladaptation [du billet que jai rédigé en février 2019](https://zestedesavoir.com/billets/2648/variables-scopes-et-closures-en-python-billet/).
- [Une histoire de variables](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#1-une-histoire-de-variables)
- [Référencement et déréférencement](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#2-referencement-et-dereferencement)
- [Portée des variables](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#3-portee-des-variables)
## [Une histoire de variables](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#1-une-histoire-de-variables)
### Représentation des variables[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#représentation-des-variables)
Lorsquon exécute des calculs, appelle des fonctions ou effectue divers traitements, il est souvent intéressant de pouvoir en conserver le résultat. Cest tout le principe des variables : garder la trace dune valeur.
Le concept de variables existe dans la majorité des langages de programmation, mais il peut prendre différentes formes. Les deux plus courantes vont être les variables sous forme de boîtes et les étiquettes.
Les boîtes sont la représentation que lon rencontre dans des langages tels que le C, où une variable correspond à une case mémoire. Il sagit donc dun nom associé à un emplacement mémoire, et assigner une valeur à la variable permet de stocker cette valeur à cet emplacement précis. Deux variables distinctes correspondent à deux emplacements différents.
Les étiquettes sont la représentation utilisée par Python, elles peuvent sembler similaires aux boîtes à lutilisation, mais diffèrent en bien des points. En Python une variable est une étiquettejuste un nomposée sur une valeur. Cest-à-dire que la valeur existe quelque part en mémoire et quon vient lui attacher une étiquette. On peut aisément placer plusieurs étiquettes sur une même valeur, mais aussi retirer une étiquette pour la placer sur une autre valeur.
La représentation des variables en Python est expliquée dans [cet article](http://foobarnbaz.com/2012/07/08/understanding-python-variables/) qui décrit très bien les choses.
### Déclaration et définition[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#déclaration-et-définition)
Certains langages distinguent la déclaration et la définition dune variable, ce nest pas le cas en Python où les deux sont confondues. Mais il sagit pourtant bien de deux concepts différents.
- Déclarer une variable, cest indiquer au compilateur que tel nom existe dans tel contexte pour lui permettre de résoudre les utilisations de ce nom.
- Définir une variable, cest lui assigner une valeur (soit en Python poser létiquette sur cette valeur).
`foo = 'bar'` en Python revient à déclarer une variable `foo` puis à la définir sur `'bar'`. Par la suite, un `foo = 'rab'` dans le même contexte revient à redéfinir une variable déjà déclarée. Cest-à-dire déplacer létiquette sur une autre valeur.
Pour être utilisée, une variable a besoin davoir été déclarée et définie, sans quoi vous rencontreriez une erreur de type `NameError` ou `UnboundLocalError` selon les cas.
```python
>>> def func():
... print(x)
...
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in func
NameError: name 'x' is not defined
```
```python
>>> def func():
... print(x)
... x = 0
...
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in func
UnboundLocalError: local variable 'x' referenced before assignment
```
Vous pourriez vous demander pourquoi je tiens à marquer cette distinction si Python la masque, mais cest parce que cela a de limportance sur dautres notions expliquées dans la suite du billet.
### Plusieurs étiquettes sur une même valeur (références multiples)[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#plusieurs-étiquettes-sur-une-même-valeur-références-multiples)
Comme je le disais, il est parfaitement envisageable davoir plusieurs étiquettes posées sur une même valeur, cest même quelque chose de très courant. Ça se produit même chaque fois que lon assigne une variable à une autre.
```python
a = []
b = a
```
Ici nous créons une nouvelle liste à laquelle nous ajoutons une première étiquette `a`. Puis nous ajoutons une seconde étiquette `b` sur cette même liste. `a` et `b` référencent la même valeur, la même liste.
Cela peut être mis en évidence à laide de lopérateur `is` :
```python
>>> a is b
True
```
Le résultat aurait été `False` si les deux listes avaient été distinctes :
```python
>>> c = []
>>> a is c
False
```
Notez que jutilise ici des listes pour ne pas être embêté dans mes exemples par certaines optimisations du compilateur. En effet, les listes étant des objets mutables (que lon peut modifier, par exemple en y ajoutant des éléments), deux instanciations différentes de listes donnent nécessairement des objets distincts. En revanche, les objets de type `int`, `str` ou encore `tuple` (et quelques autres) étant immutables, Python se permet dans certains cas de navoir quune seule instance pour plusieurs objets. Il sait que deux objets initialement égaux ne pourront être modifiés, et resteront donc toujours égaux, cela lui permet déviter doccuper inutilement de lespace avec plusieurs valeurs similaires.
Mais optimisations ou non, le fait que `b = a` ajoute une étiquette supplémentaire à la valeur existante est vrai pour tout type de valeur. En dautres termes, `a is b` sera toujours vrai après un `b = a`, quelle que soit la valeur initiale de `a`.
La conséquence de cela est que modifier `b` revient à modifier `a` (et inversement), puisque les deux étiquettes sont posées sur la même valeur :
```python
>>> b.append('value')
>>> a
['value']
>>> a += ['foobar']
>>> b
['value', 'foobar']
```
Dans ce dernier exemple, faites bien attention à lopérateur `+=` qui nest pas simplement un opérateur dassignation mais peut aussi modifier la valeur existante.
Des cas de références multiples, on en rencontre très régulièrement en Python, en voici deux un peu plus insidieux.
##### Multiplication de liste[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#multiplication-de-liste)
```python
>>> table = [[0] * 4] * 3
>>> table[0][0] = 1
>>> table
[[1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]]
```
`table` est une liste contenant 3 fois la même valeur, modifier lune delle revient à modifier les autres. En effet, la multiplication dune liste ne fait que multiplier les références que contient cette liste, elle ne procède pas à une copie des valeurs.
(Techniquement, `table[0]` est aussi une liste composée de 4 fois la même valeur, mais cette valeur étant immuable et donc redéfinie à chaque changement, il ny a pas deffet de bord.)
Pour pallier à ce problème, on utilisera plutôt une liste en intension avec un `range`.
```python
>>> table = [[0] * 4 for _ in range(3)]
>>> table[0][0] = 1
>>> table
[[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
```
##### Valeur par défaut dun paramètre de fonction[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#valeur-par-défaut-dun-paramètre-de-fonction)
```python
>>> def append_to_list(value, dest=[]):
... dest.append(value)
... return dest
...
>>> append_to_list(10)
[10]
>>> append_to_list(15)
[10, 15]
```
Le problème ici est que les valeurs par défaut des paramètres sont définies une bonne fois pour toutes lors de la définition de la fonction, et conservées pour tous les appels. Donc chaque appel à `append_to_list` utilisant la valeur par défaut référencera la même liste.
Pour contourner ce souci, il est conseillé déviter les valeurs par défaut mutables, ou dutiliser des sentinelles (`None` par exemple).
```python
>>> def append_to_list(value, dest=None):
... if dest is None:
... dest = []
... dest.append(value)
... return dest
...
>>> append_to_list(10)
[10]
>>> append_to_list(15)
[15]
```
## [Référencement et déréférencement](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#2-referencement-et-dereferencement)
### Étiquettes = références[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#étiquettes--références)
Plutôt que détiquettes, on parle plus couramment de références, mais lidée est exactement la même : une variable est une référence vers une valeur. Deux variables distinctes peuvent référencer la même valeur. Une variable peut être réassignée pour référencer une valeur différente.
Chaque définition dune variable en Python crée une nouvelle référence vers la valeur assignée. Cela est vrai pour toute variable, incluant au passage les paramètres dune fonction, qui deviennent lors de lappel de nouvelles références vers les valeurs passées en arguments. Il en est de même pour les valeurs insérées dans un conteneur (liste, *tuple*, dictionnaire, etc.) : cest une référence vers la valeur qui est stockée dans le conteneur.
Tant quil existe au moins une référence vers une valeur, on dit que cette valeur est référencée. Une valeur référencée ne peut jamais être supprimée de la mémoire (cela poserait des problèmes pour les utilisations futures de la valeur via dautres variables).
Comment alors supprimer une valeur ?
### Supprimer une valeur[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#supprimer-une-valeur)
Dans un premier temps il faut bien faire la distinction entre supprimer une variable et supprimer une valeur, les deux nétant pas du tout équivalents.
Supprimer une variable, cest faire en sorte que son nom nexiste plus. Ça se produit naturellement lorsque lon sort du contexte dans lequel la variable est déclarée, cest ce quil se passe pour les variables locales après lexécution dune fonction.
Cela peut aussi être déclenché manuellement à laide du mot-clé `del`. `del foo` a pour but de supprimer la variable `foo`, de faire en sorte que le nom `foo` ne corresponde plus à rien.
Quand une variable est supprimée, la référence vers la valeur est rompue. On dit que lon déréférence la valeur. Il y a dautres moyens que la suppression de variable pour déréférencer une valeur : la réassignation est aussi très courante, comme dans le code suivant.
```python
>>> l = ['foo']
>>> l = ['bar'] # L'ancienne valeur de l est déréférencée
```
Le modèle mémoire de Python fonctionne à laide dun compteur de références : chaque assignation dune valeur à une variable incrémente ce compteur, et chaque suppression le décrémente. Quand ce compteur atteint 0 (ce qui veut dire que la valeur nest plus référencée par aucune variable, et donc plus accessible de nulle part dans le code), la valeur peut alors être supprimée en toute sécurité, et cest le travail réalisé par le ramasse-miettes pour libérer la mémoire.
Ainsi, pour supprimer une valeur et libérer lespace mémoire quelle occupe, il est nécessaire de la déréférencer totalement, de supprimer toutes les références vers cette valeur. Cela concerne les variables aussi bien que les références stockées dans les conteneurs. Donc un `del` sur une variable ne suffit pas si la valeur est toujours référencée par une autre variable, il faut aussi soccuper des autres.
`del` peut dailleurs être utilisé pour supprimer tout type de référence et pas seulement les variables.
```python
>>> value = object() # value est une référence vers la valeur
>>> l = [value] # on crée une seconde référence depuis la liste
>>> d = {'key': value} # puis une troisième dans le dictionnaire
>>>
>>> del value # plus que deux références
>>> del l[0] # plus qu'une
>>> del d['key'] # plus du tout, la valeur est déréférencée
```
Quand Python supprime une valeur, il en appelle la méthode spéciale `__del__` (si elle en possède une). Cette méthode permet dintervenir juste avant la suppression de lobjet pour finaliser des traitements dessus. Pour reprendre lexemple précédent :
```python
>>> class Obj:
... def __del__(self):
... print('deleting', self)
...
>>> value = Obj()
>>> l = [value]
>>> d = {'key': value}
>>>
>>> del value
>>> del l[0]
>>> del d['key']
deleting <__main__.Obj object at 0x7f33ade3fba8>
```
On constate bien que ce nest pas lutilisation dun `del` qui déclenche lappel à `__del__`, mais bien le déréférencement total de notre valeur. La perte de toutes les références vers cette dernière.
Je nai utilisé ici que des déréférencements explicites, mais ils peuvent aussi être provoqués par des réassignations ou par un déréférencement parent (supprimer une liste revient à en déréférencer toutes les valeurs).
```python
>>> value = Obj()
>>> l = [value]
>>> d = {'key': value}
>>>
>>> value = None
>>> l[:] = []
>>> d['key'] = 20
deleting <__main__.Obj object at 0x7f33ade3fb38>
```
### Références cycliques
Dans tout cela, un cas qui peut être problématique est celui des références cycliques. Que faire par exemple si un objet `obj1` contient une référence vers un objet `obj2`, qui contient lui-même une référence vers `obj1` ?
```python
>>> obj1 = Obj()
>>> obj2 = Obj()
>>> obj1.ref = obj2
>>> obj2.ref = obj1
>>>
>>> del obj1
>>> del obj2
```
Comme vous le voyez, il ne se passe rien, les deux valeurs étant toujours référencées. Mais à la sortie du programme, les références cycliques seront résolues et les valeurs supprimées.
```python
>>> ^D
deleting <__main__.Obj object at 0x7f8fcf561b70>
deleting <__main__.Obj object at 0x7f8fcf561ba8>
```
Il peut néanmoins être gênant de devoir attendre la fin du programme pour collecter ces valeurs (elles occupent inutilement de lespace mémoire obligeant ainsi le programme à en allouer toujours plus), et dans ce genre de cas il est utile de pouvoir invoquer manuellement le ramasse-miettes. Cela se fait à laide la méthode `collect` du module `gc`, qui renvoie le nombre de valeurs non atteignables trouvées et supprimées.
```python
>>> obj1 = Obj()
>>> obj2 = Obj()
>>> obj1.ref = obj2
>>> obj2.ref = obj1
>>>
>>> del obj1
>>> del obj2
>>>
>>> import gc
>>> gc.collect()
deleting <__main__.Obj object at 0x7f4077157b70>
deleting <__main__.Obj object at 0x7f4077157be0>
4
```
Les références cycliques sont assez courantes lorsquon travaille sur des représentations arborescentes et que lon souhaite que les nœuds parents et enfants puissent se référencer. Cest aussi le cas pour la gestion de données sous forme de graphes.
### Références faibles[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#références-faibles)
Le problème des références cycliques provient du fait que le ramasse-miettes ne peut collecter les objets tant quils sont référencés. Une autre manière de le résoudre est alors dutiliser des références qui nempêchent pas ce ramasse-miettes de supprimer les valeurs. On les appelle «références faibles» et elles sont fournies en Python par le module [weakref](https://docs.python.org/3/library/weakref.html).
Une référence faible est similaire à un appel de fonction qui renvoie lobjet si celui-ci est toujours référencé, ou `None` sil a été supprimé.
```python
>>> import weakref
>>>
>>> obj1 = Obj()
>>> obj2 = Obj()
>>> obj1.ref = obj2
>>> obj2.ref = weakref.ref(obj1)
>>>
>>> obj2.ref
<weakref at 0x7f8de5d69408; to 'Obj' at 0x7f8de5d6b128>
>>> print(obj2.ref())
<__main__.Obj object at 0x7f8de5d6b128>
>>> obj2.ref() is obj1
True
>>>
>>> del obj1
deleting <__main__.Obj object at 0x7f8de5d6b128>
>>> print(obj2.ref())
None
```
---
Quelques liens pour aller plus loin sur le sujet :
- [https://rushter.com/blog/python-garbage-collector/](https://rushter.com/blog/python-garbage-collector/)
- [https://docs.python.org/3/library/gc.html](https://docs.python.org/3/library/gc.html)
## [Portée des variables](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#3-portee-des-variables)
### Scope[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#scope)
Une variable nest certes quun nom posé sur une valeur, mais plusieurs variables portant le même nom peuvent coexister au sein dun programme. Un nom de variable est pourtant unique, mais seulement dans le contexte où il est déclaré, dans son *scope*.
Une variable peut en effet être déclarée au niveau global dun module et ainsi être accessible depuis nimporte où dans ce module (on dit que sa portée est globale), mais elle peut aussi être déclarée dans le *scope* dune fonction et accessible depuis cette fonction uniquement (portée locale).
Il est donc tout à fait possible davoir une variable locale à une fonction portant le même nom quune variable du module, comme dans lexemple suivant pour la fonction `g`. La variable locale prendra la priorité sur la globale lors de la résolution du nom par le compilateur.
```python
>>> a = 0
>>> def f():
... print('f: a =', a)
...
>>> def g():
... a = 1
... print('g: a =', a)
...
>>> print('global: a =', a)
global: a = 0
>>> f()
f: a = 0
>>> g()
g: a = 1
>>> print('global: a =', a)
global: a = 0
```
Le *scope* de déclaration définit la portée dans laquelle est accessible une variable. Cette portée inclut le contexte courant, mais aussi tous les *scopes* enfants. Lexemple précédent ne présentait que deux niveaux, global et local, mais il y en a en réalité une multitude, les *scopes* de fonctions pouvant simbriquer.
```python
>>> def outer():
... def inner():
... print(var)
... var = 10
... inner()
...
>>> outer()
10
```
Ici, une variable déclarée dans le *scope* de la fonction `outer` est accessible dans `inner`. Et il en serait de même si `inner` définissait encore une sous-fonction, celle-ci aurait accès à `var` déclarée dans un *scope* parent.
Linverse nest pas vrai, une variable déclarée dans un *scope* enfant nest pas accessible depuis le parent.
```python
>>> def outer():
... def inner():
... var = 10
... print(var)
...
>>> outer()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in outer
NameError: name 'var' is not defined
```
Lorsquon quitte un *scope*, le *scope* ainsi que les variables quil contient sont détruit·e·s, ce qui permet de supprimer les valeurs si elles ne sont plus référencées.
```python
>>> class Obj:
... def __del__(self):
... print('deleting', self)
...
>>> def func():
... obj1 = Obj()
...
>>> func()
deleting <__main__.Obj object at 0x7f1f6eeedcc0>
```
Bien sûr, ça ne sapplique pas au cas où la fonction renverrait une valeur qui serait assignée à une variable, puisque lon en conserverait alors une référence.
```python
>>> def func():
... obj1 = Obj()
... obj2 = Obj()
... return obj1
...
>>> ret = func()
deleting <__main__.Obj object at 0x7f1f6eeedcc0>
>>> del ret
deleting <__main__.Obj object at 0x7f1f6eeedc88>
```
Ce que cela nous montre aussi, cest que bien quune variable soit associée à un *scope*, sa valeur peut, elle, transiter de *scope* en *scope* et ainsi remonter vers les parents à laide du `return`. La variable référençant la valeur change (on passe de `obj1` à `ret`), mais la valeur reste toujours la même (elle nest pas copiée).
### Variables locales et extérieures[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#variables-locales-et-extérieures)
Le corps dune fonction a donc accès aux variables définies dans le *scope* courant (les variables locales), mais aussi à celles des *scopes* parents (les variables extérieures). Ces variables extérieures ne se limitent pas aux variables globales : dans le premier exemple donné plus haut sur `outer` et `inner`, `inner` accède à une variable `var` qui est définie dans `outer`.
Il ne sagit donc pas dune variable globale (elle nexiste pas dans le *scope* global du module), mais dune variable locale d'`outer`, et donc une variable extérieure (non-locale) à `inner`.
Les variables extérieures peuvent être utilisées de la même manière que les variables locales. Et lors de la résolution du nom si elles sont plusieurs à porter le même nom, cest la variable du *scope* parent le plus proche qui sera utilisée en priorité.
```python
>>> var = 10
>>> def outer():
... var = 20
... def inner():
... print(var)
... inner()
...
>>> outer()
20
>>> var
10
```
Cet exemple nous amène à une petite subtilité des variables extérieures. Nous y voyons que nous pouvons définir dans une fonction une nouvelle variable portant le nom dune variable extérieure (sans que cela naffecte la variable extérieure). Comment alors redéfinir la valeur dune variable extérieure ?
Il est dans ce cas nécessaire dindiquer à Python quune variable de ce nom est déjà déclarée à lextérieur. Pour cela, deux mots-clés sont à notre disposition : `global` et `nonlocal`.
Le premier permet dindiquer quune variable est globale, et Python comprendra quelle devra être déclarée et définie dans le *scope* global. Il sutilise suivi dun ou plusieurs noms de variables : `global foo` ou `global foo, bar, baz`.
```python
>>> x = 5
>>> def define_globals():
... global x
... x = 10
...
>>> x
5
>>> define_globals()
>>> x
10
```
On notera que la variable na pas besoin davoir déjà été définie dans le *scope* global pour être utilisée dans notre fonction.
```python
>>> def define_globals():
... global y
... y = 2
...
>>> y
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined
>>> define_globals()
>>> y
2
```
Le mot-clé `nonlocal` est similaire mais pour indiquer quune variable existe dans un *scope* local parent. Au contraire de `global`, cette variable doit donc nécessairement avoir été définie dans un *scope* parent (autre que le *scope* global).
```python
>>> def outer():
... x = 0
... def inner():
... y = 0
... def inception():
... nonlocal y
... y = 10
... inception()
... nonlocal x
... x = y
... inner()
... return x
...
>>> outer()
10
```
Lomission de lun de ces deux `nonlocal` aurait pour effet de redéclarer une variable locale (`x` ou `y`), et donc ne permettrait pas la remontée de la valeur `10`.
Si plusieurs *scopes* parents déclarent une variable du même nom, cest la variable du *scope* le plus proche qui sera utilisée, comme cest le cas pour tout accès à une variable extérieure.
---
Par ailleurs, il est souvent dit que les mots-clés `global` et `nonlocal` sont nécessaires pour modifier une variable extérieure, pour y accéder en écriture.
Cela est faux, ils ne sont nécessaires que pour la redéfinir. Il est par exemple parfaitement possible dutiliser depuis une fonction la méthode `append` dune liste définie au niveau global, sans avoir à utiliser le mot-clé `global`.
```python
>>> values = []
>>> def append_value(value):
... values.append(value)
...
>>> append_value(0)
>>> append_value(1)
>>> append_value(2)
>>> values
[0, 1, 2]
```
Ainsi, les utilisations de `global` ou `nonlocal` sont en réalité plutôt rares, et il est généralement déconseillé dutiliser ces mots-clés (ils prêtent à confusion). Mais leur connaissance permet de résoudre certains cas problématiques.
### Closure (fermeture)[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#closure-fermeture)
Je disais quelques paragraphes plus haut que le contexte local à une fonction est détruit à la sortie de cette fonction. Je précisais ensuite que cela nentraînait pas nécessairement la destruction de toutes les valeurs de ce contexte si certaines étaient toujours référencées, comme ça peut être le cas de la valeur de retour de la fonction.
Un autre cas intéressant est celui des *closures* (fermetures). Celles-ci vont permettre de capturer des variables locales pour quelles soient toujours accessibles dans un contexte particulier.
Je sais que ces explications ne sont pas très claires et je préfère alors vous fournir tout de suite un exemple :
```python
>>> def cached_addition():
... cache = {}
... def addition(x, y):
... if (x, y) not in cache:
... print(f'Computing {x} + {y}')
... cache[x, y] = x + y
... return cache[x, y]
... return addition
...
>>> addition = cached_addition()
>>> addition(1, 2)
Computing 1 + 2
3
>>> addition(1, 2)
3
>>> addition(2, 3)
Computing 2 + 3
5
```
Ce nest pas lexemple le plus utile, mais on pourrait imaginer une fonction plus complexe à la place de laddition, dont la mise en cache du résultat serait nécessaire.
Lidée ici est que notre fonction `cached_addition` retourne à chaque appel une nouvelle fonction `addition` créée dynamiquement, utilisant un cache particulier. Ce cache est une variable définie localement dans `cached_addition` et donc accessible depuis `addition`.
Cependant, une fois lappel à `cached_addition` terminé, son *scope* local est détruit, ce qui doit impliquer la destruction des valeurs quil contient. Ici, on voit bien que `cache` lui survit puisquil continue à être utilisé sans problème par la fonction `addition`.
Ce quil se passe cest que la fonction `addition` crée une *closure* qui emprisonne les variables locales des *scopes* parents quelle utilise. Cela permet à ces valeurs dêtre toujours référencées. On peut dailleurs constater que notre fonction `addition` possède un attribut spécial `__closure__`.
```python
>>> addition.__closure__
(<cell at 0x7f3a700a5d98: dict object at 0x7f3a70174fc0>,)
>>> addition.__closure__[0].cell_contents
{(1, 2): 3, (2, 3): 5}
```
Lintérêt des *closures*, cest que plusieurs appels à `cached_addition` distincts renverront des fonctions utilisant un cache différent, parce quil sagira à chaque fois dune nouvelle variable locale.
```python
>>> other_addition = cached_addition()
>>> other_addition(1, 2)
Computing 1 + 2
3
```
Le mécanisme de *closures* est souvent utilisé au sein de décorateurs puisquil permet de facilement attacher des variables à une fonction créée dynamiquement (quelques exemples peuvent être trouvés [ici](https://zestedesavoir.com/tutoriels/954/notions-de-python-avancees/5-exercises/2-3-decorators/)).
Nous avons vu que la *closure* permettait la persistance en mémoire de certaines valeurs en les capturant. Mais cette *closure* nest pas éternelle et disparaît naturellement quand elle est elle-même déréférencée, cest à dire quand la fonction qui emprisonne ces valeurs disparaît.
```python
>>> class Obj:
... def __del__(self):
... print('deleting', self)
...
>>> def outer():
... obj = Obj()
... def inner():
... print(obj)
... return inner
...
>>> func1 = outer()
>>> func2 = outer()
>>> func1()
<__main__.Obj object at 0x7f58ba68dd68>
>>> func2()
<__main__.Obj object at 0x7f58ba68de10>
>>> del func1
deleting <__main__.Obj object at 0x7f58ba68dd68>
>>> del func2
deleting <__main__.Obj object at 0x7f58ba68de10>
```
Et pour terminer sur les *closures* voici un autre billet expliquant le concept avec des exemples en Python et en JS : [http://sametmax.com/closure-en-python-et-javascript/](http://sametmax.com/closure-en-python-et-javascript/).
### Délimitation dun scope[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#délimitation-dun-scope)
On pourrait croire, comme cest le cas dans dautres langages, quun nouveau *scope* est créé pour chaque bloc de code (identifié en Python par le niveau dindentation). Mais le code suivant, plutôt courant, serait alors invalide :
```python
>>> items = [1, 2, 3, ...]
>>> if items:
... value = items[0]
... else:
... value = None
...
>>> value
1
```
On remarque bien ici que notre variable `value` est définie dans le *scope* courant et non dans un *scope* fils créé spécialement pour le bloc conditionnel. Le niveau dindentation na pas dincidence sur le *scope*.
En fait, outre les modules, qui forment le *scope* global, seules les classes et les fonctions permettent de définir de nouveaux *scopes*. Ce nest donc pas le cas des autres blocs de code comme les conditions, les boucles ni même les blocs `with`.
Cela peut être particulièrement trompeur pour les boucles `for`, et notamment pour leur variable ditération qui nest donc pas propre à la boucle. Elle est définie et existe à lextérieur.
```python
>>> for i in range(10):
... pass
...
>>> i
9
```
Ce qui peut mener à des cas plus embêtants si lon imaginait que cette variable serait capturée dans une *closure* propre à la boucle.
```python
>>> functions = []
>>> for i in range(3):
... def add_func(x):
... return i + x
... functions.append(add_func)
...
>>> functions
[<function add_func at 0x7ff229851268>, <function add_func at 0x7ff2298512f0>, <function add_func at 0x7ff229851378>]
>>> [f(0) for f in functions]
[2, 2, 2]
```
Toutes les fonctions `f` renvoient la même valeur pour 0 car toutes accèdent à la même variable `i`, qui vaut 2 en sortie de boucle.
La solution dans ce genre de cas est donc de créer une fonction englobante et de lappeler afin de tirer profit du mécanisme des *closures*. Lexemple précédent pourrait être corrigé comme suit.
```python
>>> functions = []
>>> for i in range(3):
... def get_add_func(i):
... def add_func(x):
... return i + x
... return add_func
... functions.append(get_add_func(i))
...
>>> [f(0) for f in functions]
[0, 1, 2]
```
La fonction `get_add_func` (qui pourrait tout aussi bien être placée en dehors de la boucle, ce qui serait même préférable) possède un paramètre `i` qui sera capturé dans la *closure* de la sous-fonction `add_func`. Ainsi, à chaque tour de boucle `get_add_func` est appelée avec une valeur différente, et cest cette valeur qui est chaque fois emprisonnée.
### Variables non déclarées ou non définies[Link](https://zestedesavoir.com/tutoriels/3163/variables-scopes-et-closures-en-python/#variables-non-déclarées-ou-non-définies)
Au tout début de ce billet jévoquais les exceptions `NameError` et `UnboundLocalError` qui peuvent être levées lors de laccès à une variable.
La première (`NameError`) est levée si le nom dune variable nexiste pas, cest-à-dire quelle nest déclarée ni dans le *scope* courant, ni dans les parents.
```python
>>> def func():
... print(x)
...
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in func
NameError: name 'x' is not defined
```
La seconde (`UnboundLocalError`) est plus subtile et survient pour une variable déclarée mais non définie.
Pour rappel, linstruction `x = 10` a pour effet de déclarer la variable `x` puis de la définir avec pour valeur 10. Ces deux opérations ninterviennent pas au même moment : la définition se fait au moment où cette instruction est rencontrée, mais la déclaration est valable pour tout le *scope*. Cest donc comme si la variable avait été déclarée au tout début du *scope*.
Ainsi, prenons lexemple suivant :
```python
>>> def func():
... print(x)
... x = 0
...
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in func
UnboundLocalError: local variable 'x' referenced before assignment
```
Nous essayons ici dafficher la valeur de `x` mais celle-ci nest pas encore définie. En revanche, le nom de notre variable existe bel et bien, car elle est déclarée dans le *scope* courant, à la ligne suivante.
Cette erreur est très courante quand on souhaite redéfinir une variable globale après lavoir utilisée, mais en omettant le mot-clé `global`.
```python
>>> var = 0
>>>
>>> def func1():
... print(var)
... var = 10
...
>>> func1()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in func1
UnboundLocalError: local variable 'var' referenced before assignment
>>>
>>> def func2():
... global var
... print(var)
... var = 10
...
>>> func2()
0
>>> var
10
```
---
Derrière leur apparente simplicité, les variables sont en réalité un mécanisme bien plus complexe. Et lorsquelles sont mal comprises, elles peuvent vous amener à tomber dans de nombreux pièges. Jespère que ce tutoriel vous permettra de reconnaître ces pièges et surtout de les éviter.
Je tenais finalement à remercier les retours que jai reçus sur le billet qui mont amené à le transformer en tutoriel. Et si je devais finir sur une unique phrase : **Les variables sont des étiquettes, pas des boîtes !**