Dans un récent shot data, on a abordé une différence fondamentale entre les boucles for et les list comprehensions en Python.
Voici le visuel de ce post pour un rappel rapide :

En résumé :
Après l'exécution d'une boucle for, la variable de boucle reste accessible.
Après l'exécution d'une list comprehension, la variable de boucle N'EST PAS accessible.
Comme je l'ai mentionné dans ce shot data, c'est étroitement lié à la notion de scope en Python.
Voyons ça aujourd'hui.
Qu'est-ce que le scope d'une variable et pourquoi en a-t-on besoin ?
En termes simples, le scope d'une variable désigne les zones de ton programme dans lesquelles une variable donnée est accessible.
Ce concept n'est pas propre à Python. Il s'applique à la programmation en général.
L'objectif est de limiter l'accès aux variables à des zones précises du code, afin d'éviter qu'elles soient modifiées depuis n'importe quel endroit du programme.

Ainsi, quand tu accèdes à N'IMPORTE QUELLE variable, c'est le scope qui détermine entièrement comment les noms seront recherchés dans ton code.
En Python, la résolution de scope se fait sur quatre niveaux.

Chaque fois que tu accèdes à un nom en Python (nom de variable, nom de fonction, nom de classe, etc.), l'interpréteur Python le recherche dans l'ordre suivant :
Local
Enclosing (englobant)
Global
Built-in
C'est ce qu'on appelle la règle LEGB.
Résolution de scope LEGB en Python
Voyons chacun de ces niveaux !
#1) Scope local
L'exemple le plus simple de scope local, ce sont des variables déclarées à l'intérieur d'une fonction, comme illustré ci-dessous :

La variable myVar a un scope local dans funcA, ce qui signifie qu'elle n'est accessible qu'à l'intérieur de la fonction.
Si tu essaies d'accéder à myVar en dehors de funcA, tu obtiens, sans surprise, une erreur car la variable est locale à funcA :

Ensuite, si tu as deux fonctions non imbriquées, tu remarqueras que les variables déclarées dans une fonction ne sont pas accessibles dans l'autre :

C'est le principe du scope local, et en Python, il a la priorité la plus haute lors de la résolution de scope.
Autrement dit, si le même nom de variable existe à plusieurs endroits dans un programme, Python utilisera la variable qui se trouve dans le scope local depuis lequel elle a été accédée.
Par exemple, dans le code ci-dessous, myVar est déclarée dans funcA et dans funcB. Quand on appelle funcA(), Python trouve myVar dans le scope local et l'affiche :

Si la variable est trouvée dans le scope local, parfait.
Sinon, Python passe au scope englobant (s'il existe) pour trouver la variable.
Avant de passer au scope englobant…
Un point à toujours retenir sur le scope local : il est créé à chaque appel de fonction, c'est-à-dire qu'il n'est pas créé au moment de la définition de la fonction.
Tu peux donc avoir autant de scopes locaux distincts que de fois où tu appelles une fonction, y compris lors d'appels récursifs.
#2) Scope englobant (Enclosing)
En Python, le scope englobant apparaît quand tu as des fonctions imbriquées, c'est-à-dire une ou plusieurs fonctions à l'intérieur d'une autre fonction.
Par exemple, reprends le dernier code ci-dessus. Déclarons funcB à l'intérieur de funcA.
Le scope local et le scope englobant par rapport à funcB sont illustrés ci-dessous :

Considère maintenant le code suivant, où myVar n'est pas disponible dans le scope local de funcB :

Comme le scope local ne contient pas de variable myVar, Python va d'abord vérifier s'il existe un scope englobant.
Oui, il y en a un : funcA !
Ensuite, il va vérifier si myVar est déclarée dans funcA.
Bien sûr que oui !
Python utilise la valeur déclarée dans le scope englobant pour myVar :

Dans le code ci-dessus, si myVar n'était pas déclarée dans funcA, Python serait passé au scope global, qui vient ensuite dans l'ordre de priorité.
#3) Scope global
Le scope global, comme tu l'as probablement deviné, est le scope le plus large.
Ici, les variables et fonctions sont déclarées en dehors de tout bloc, fonction ou classe spécifique.
Elles sont donc accessibles (mais pas directement modifiables) depuis n'importe quelle partie du programme, peu importe où elles sont définies.
Par exemple, dans le code ci-dessous, ni funcA ni funcB n'ont la variable myVar définie dans leur scope local respectif :

Il n'y a pas non plus de scope englobant.
Python vérifie donc si myVar est disponible dans le scope global, et elle l'est effectivement.
Ainsi, lors des print, on obtient la valeur globale de myVar, à savoir 10.
En guise d'exercice, peux-tu dire quel sera l'output dans ce cas :

#4) Scope built-in
Le dernier dans l'ordre de priorité de la résolution de scope est le scope built-in.
Ce sont des noms de variables qui sont automatiquement chargés dans le scope global de Python quand tu :
exécutes un script,
ou ouvres une session Python interactive.
Ces variables built-in sont accessibles via le nom __builtins__, et il y en a plus de 150 :

Parmi les plus courants : float, int, len, set, dict, range, str, zip, et bien d'autres.
Si une variable n'est pas trouvée dans le scope local, ni dans le scope englobant, ni même dans le scope global, Python tente de la trouver dans le scope built-in.
Et c'est ainsi que fonctionne la résolution de scope en Python.
Retour à l'exemple : list comprehension vs. boucle for 👇

La variable de boucle (loop_var) n'est pas accessible après l'exécution d'une list comprehension parce qu'elle est définie dans son scope local.
En revanche, la variable de boucle est définie dans le scope global dans la boucle for du code ci-dessus. Elle reste donc accessible après l'exécution de la boucle.
J'espère que tu as appris quelque chose de nouveau aujourd'hui.
Envie d'aller plus loin ? DataXp t'attend : formations et parcours pour progresser en data science, machine learning et IA.
👉 À toi de jouer : sais-tu comment modifier une variable globale depuis le scope local d'une fonction ?

