La portée (Scope) : Local, Enclosing, Global et Built-in (LEGB)

Objectifs de la leçon
  • Expliquer ce qu’est la portée (Scope) et pourquoi une variable créée dans une fonction n’existe pas à l’extérieur
  • Connaître les quatre portées — Local, Enclosing, Global et Built-in — et l’ordre de recherche LEGB
  • Comprendre ce qui se passe quand on modifie une variable globale dans une fonction, et ce que fait global
  • Écrire des fonctions qui reçoivent des données par des paramètres et renvoient un résultat, au lieu de dépendre de variables globales

Qu’allons-nous apprendre ?

La portée (Scope) = la zone du code où un nom (une variable ou une fonction) est connu. Nous allons apprendre :

  • Local — les variables dans une fonction.
  • Enclosing — les variables d’une fonction extérieure, quand il y a une fonction dans une fonction.
  • Global — les variables au niveau supérieur du fichier.
  • Built-in — les noms intégrés de Python (print, len…).
  • LEGB — l’ordre dans lequel Python cherche un nom.

Pourquoi en avons-nous besoin ?

Si chaque variable était accessible de partout, une variable nommée total dans une fonction écraserait une variable total dans une autre fonction, et dans un programme de 10 000 lignes ce serait le chaos. La portée isole chaque fonction : elle peut utiliser ses propres noms sans craindre de collisions.

D’un autre côté, des erreurs comme NameError ou UnboundLocalError sont très déroutantes si l’on ne comprend pas la portée.

Explication à partir des bases

Local

Une variable créée dans une fonction (y compris ses paramètres) est locale :

  • Elle est créée à chaque appel de la fonction, et disparaît quand elle se termine.
  • Elle n’est pas accessible de l’extérieur.
  • Deux fonctions différentes peuvent avoir des variables locales du même nom, sans aucun lien entre elles.

Global

Une variable créée au niveau supérieur du fichier (pas dans une fonction) est globale :

  • Elle est accessible en lecture depuis toutes les fonctions du fichier.
  • Pour la modifier (lui affecter une nouvelle valeur) depuis une fonction, il faut une déclaration explicite : global name.

Enclosing

On peut définir une fonction dans une fonction. La fonction intérieure voit les variables de la fonction extérieure. La portée de la fonction extérieure s’appelle Enclosing (englobante) du point de vue de la fonction intérieure.

Built-in

Les noms que Python connaît toujours, sans import : print, len, sum, max, int, str, list, range…

L’ordre de recherche : LEGB

Quand le code utilise un nom, Python le cherche dans cet ordre, et s’arrête au premier endroit où il l’a trouvé :

L  Local       → inside the current function
E  Enclosing   → inside outer functions (if nested)
G  Global      → top level of the file
B  Built-in    → Python's built-in names
              → not found anywhere: NameError
Analogie : où sont les clés ?

Vous cherchez vos clés : d’abord dans votre poche (Local), puis dans le sac que vous portez (Enclosing), puis à la maison (Global), et enfin dans le hall de l’immeuble, où l’on laisse des choses pour tout le monde (Built-in). Dès que vous les trouvez, vous arrêtez de chercher. Et une clé dans la poche d’un ami, vous ne pouvez pas la voir du tout : c’est le Local d’une autre fonction.

Syntaxe

counter = 0                  # global

def outer():
    message = "hi"           # local to outer = enclosing for inner

    def inner():
        result = message + "!"   # local to inner; message found in Enclosing
        return result

    return inner()

def increase():
    global counter           # declare: assignments go to the GLOBAL counter
    counter = counter + 1

Premier exemple de code

week-2/scope_demo.py :

course = "Kodkod 6"

def welcome(name):
    message = "Welcome " + name + " to " + course
    return message

print(welcome("Dana"))
print(course)
print(message)

Sortie :

Welcome Dana to Kodkod 6
Kodkod 6
Traceback (most recent call last):
  File "scope_demo.py", line 11, in <module>
    print(message)
NameError: name 'message' is not defined

Analyse du code ligne par ligne

NomPortéeExplication
courseGlobalCréé au niveau supérieur. welcome le lit : L (pas trouvé) → E (aucun) → G (trouvé !).
nameLocal de welcomeUn paramètre = une variable locale. N’existe que pendant l’appel.
messageLocal de welcomeCréé dans la fonction et disparaît quand elle renvoie.
print, len…Built-inTrouvés à la fin de la recherche.
print(message) à l’extérieur—Au niveau supérieur, il n’y a pas de message. C’était un Local, et il a déjà disparu, d’où la NameError.

La bonne façon de sortir une valeur d’une fonction est return, et c’est exactement ce que fait welcome. L’appelant a reçu la valeur (et peut la stocker dans sa propre variable).

Un autre exemple

Le même nom dans plusieurs portées, et global. week-2/scope_levels.py :

total = 100

def local_total():
    total = 5
    print("inside local_total:", total)

def read_global():
    print("inside read_global:", total)

def change_global():
    global total
    total = total + 50
    print("inside change_global:", total)

def outer():
    label = "outer"

    def inner():
        print("inner sees enclosing label:", label)

    inner()

local_total()
print("after local_total:", total)
read_global()
change_global()
print("after change_global:", total)
outer()

Sortie :

inside local_total: 5
after local_total: 100
inside read_global: 100
inside change_global: 150
after change_global: 150
inner sees enclosing label: outer

Analyse :

  • local_total — l’affectation total = 5 crée une nouvelle variable locale nommée total, qui « cache » la globale. La globale n’a pas changé (100).
  • read_global — ne fait que lire total. Elle n’a pas de variable locale de ce nom, donc selon LEGB on arrive à la globale.
  • change_global — global total dit à Python : « dans cette fonction, total est la globale ». Maintenant l’affectation modifie la globale (150).
  • inner voit le label de outer. C’est le E de LEGB.

Erreur courante

Erreur 1 : UnboundLocalError — lire puis modifier une globale sans global.

count = 0

def add_one():
    count = count + 1

add_one()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Pourquoi ? Parce que la fonction contient une affectation à count, Python décide (à l’avance, avant l’exécution) que count est local dans toute la fonction. Et alors count + 1 essaie de lire une variable locale qui n’a pas encore reçu de valeur.

Erreur 2 (la grande) : utiliser global comme solution permanente. global « fonctionne », mais :

  • Chaque fonction peut modifier la variable, et quand il y a un bug, impossible de savoir qui l’a modifiée.
  • La fonction dépend de quelque chose « venu de l’extérieur », il est donc difficile de la tester et de la réutiliser.

La façon propre : un paramètre entre, return sort.

def add_one(count):
    return count + 1

count = 0
count = add_one(count)
print(count)   # 1

Maintenant, ce qui entre et ce qui sort est tout à fait clair, et la fonction ne touche à rien qu’elle n’a pas reçu. (Dans le thème POO, nous apprendrons une autre façon de gérer un « état » qui change : les objets.)

Erreur 3 : écraser un nom Built-in.

list = [1, 2, 3]
numbers = list(range(5))
TypeError: 'list' object is not callable

Notre list globale vient avant le Built-in dans LEGB, elle a donc « caché » la fonction intégrée. C’est la même chose avec sum = 0, max = 10, input = ..., str = .... Ne donnez jamais aux variables les noms des Built-ins.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: La variable qui a disparu

def welcome(name):
    message = "Welcome " + name
    return message

print(welcome("Dana"))
print(message)
  1. Que se passera-t-il à la dernière ligne, et pourquoi ?
  2. Corrigez pour que le message soit affiché deux fois.
Afficher la solution
  1. NameError: name 'message' is not defined. message est une variable Local de welcome — elle est créée à chaque appel et disparaît quand la fonction se termine.
  2. Enregistrez la valeur renvoyée dans notre propre variable :
def welcome(name):
    message = "Welcome " + name
    return message


result = welcome("Dana")
print(result)
print(result)
Welcome Dana
Welcome Dana

Exercice 2: Prédire la sortie

x = "global"

def first():
    x = "local in first"
    print("1:", x)

def second():
    print("2:", x)

first()
second()
print("3:", x)
Afficher la solution
x = "global"


def first():
    x = "local in first"
    print("1:", x)


def second():
    print("2:", x)


first()
second()
print("3:", x)
1: local in first
2: global
3: global
  • first crée son propre x local — la globale n’a pas été touchée.
  • second ne définit pas x, donc selon LEGB on arrive à la Global.

Exercice 3: UnboundLocalError

count = 0

def add_one():
    count = count + 1

add_one()
  1. Quelle erreur obtiendrez-vous, et pourquoi ?
  2. Corrigez sans global.
Afficher la solution
  1. UnboundLocalError. À cause de l’affectation count = ..., Python décide que count est une variable locale dans toute la fonction — et alors count + 1 essaie de lire une variable locale qui n’a pas encore de valeur.
  2. Un paramètre entre, return sort :
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
count = add_one(count)
print(count)
2

Exercice 4: Écraser un nom intégré

list = [1, 2, 3]
numbers = list(range(5))
  1. Quelle erreur obtiendrez-vous ?
  2. Expliquez avec LEGB pourquoi cela arrive, et corrigez.
Afficher la solution
  1. TypeError: 'list' object is not callable.
  2. Selon LEGB, Python cherche list dans Global avant Built-in. Notre variable nommée list a « caché » la fonction intégrée. La correction — ne pas donner aux variables les noms des Built-ins :
items = [1, 2, 3]
numbers = list(range(5))
print(items, numbers)
[1, 2, 3] [0, 1, 2, 3, 4]

Exercice 5: Sans global

cart = []
total = 0

def add(price):
    global total
    cart.append(price)
    total = total + price

add(100)
add(50)
print(cart, total)

Refactorisez pour qu’aucune fonction n’utilise global ni ne modifie de variables extérieures : chaque fonction reçoit ce dont elle a besoin par des paramètres et renvoie un résultat.

Résultat attendu :

[100, 50] 150
Afficher la solution
def add(cart, price):
    return cart + [price]


def cart_total(cart):
    return sum(cart)


cart = []
cart = add(cart, 100)
cart = add(cart, 50)
print(cart, cart_total(cart))
[100, 50] 150
  • total n’est plus un « état » à maintenir — il est calculé à partir du panier.
  • add renvoie une nouvelle liste (cart + [price]) et ne modifie pas l’existante.

Résumé

  • La portée = là où un nom est connu. Une variable créée dans une fonction (y compris les paramètres) est Local, et disparaît à la fin de l’appel.
  • LEGB : Local → Enclosing → Global → Built-in. On s’arrête au premier endroit où il est trouvé. S’il n’est pas trouvé, NameError.
  • Une affectation dans une fonction crée une variable locale, même s’il existe une globale du même nom.
  • global x permet de modifier une globale depuis une fonction, mais il vaut mieux l’éviter. Un paramètre entre et return sort.
  • Ne donnez pas aux variables les noms des Built-ins (list, sum, max, str…).

Documentation officielle