Aller au contenu

Portée des variables

Une variable présente dans le texte d'un programme n'est pas nécessairement accessible depuis tout point du texte de ce programme.

  • Une variable créée en-dehors de toute fonction aura une portée globale: elle est vue depuis toute fonction.
  • Tandis qu'une variable créée à l'intérieur d'une fonction n'est connue qu'à l'intérieur de cette fonction.

Nous détaillons ci-dessous ce que cela signifie avec Python.

Exemple 1

Avec le code suivant:

a = 2

def f(x):
    print("Variable a à l'intérieur de f:", a)
    return x+2

print("Variable a avant appel de f:", a)
f(a)
print("Variable a après appel de f:", a)

quels affichages obtient-on?

Réponse

On obtient 2 pour chaque affichage.

Le a de la ligne 1 est dans l'espace global: il est donc connu depuis tout point du programme.

  • Après la ligne 1, la situation peut être schématisée ainsi:
  • Les lignes 3 à 5 définissent f et ne sont pas exécutées tant que f n'est pas appelée.
  • La ligne 7 est exécutée et affiche la valeur de l'objet portant le nom a: cette ligne affiche donc 2.
  • En ligne 8, on fait un appel à f. L'appel f(a) a pour effet de créer une étiquette x locale à f avec l'affectation (d'étiquette) x ← a:
  • L'exécution de la ligne 4 (corps de f) affiche alors la valeur du seul objet étiqueté a. Remarque: l'étiquette est aussi connue de f car elle est dans l'espace global.
  • La ligne return x+2 crée un objet de type int et valeur 4 (mais sans étiquette).

    La fonction se termine: son espace de noms disparaît:
  • La dernière ligne (ligne 9) affiche la valeur du seul objet étiqueté a.

Exemple 2

a = 2

def f(x):
    a = x + 3
    return a

print("Variable a avant appel de f:", a)
f(a)
print("Variable a après appel de f:", a)

Quels affichages obtient-on?

Réponse

Dans les deux cas, la valeur affichée est 2.

Le a de la ligne 1 se situe dans l'espace global.

  • Ligne 1: on crée une étiquette a de portée globale sur un objet de type int:
  • Ligne 7: on affiche cette valeur.
  • Ligne 8: on fait un appel à f par f(a).

  • Ligne 4 (corps de f): on créé une étiquette a locale à f, que l'on pourrait noter a_f pour la distinguer de l'étiquette a de portée globale.

  • En ligne 5 (corps de f) return a: l'image de 2 par f est 5, mais aucune étiquette au niveau global n'est affectée à cette valeur, si bien qu'en sortie de f (les noms locaux à f disparaissant), on n'a en fait plus accès à cette image!

  • La ligne 9 est exécutée et affiche la valeur du seul objet étiqueté a.

A l'intérieur de la fonction, l'étiquette a désigne ainsi un autre objet qu'à l'extérieur de la fonction (elle masque la variable a de l'espace global). Les opérations faites alors sur ce a local n'affectent pas le a global.

Lorsqu'on sort de la fonction, le a local à la fonction disparaît (il n'existe que le temps d'exécution de la fonction) et c'est à nouveau le a global de la ligne 1 que l'on manipule.

En d'autres termes, le code de notre script pourrait être réécrit comme suit de façon tout à fait équivalente:

a = 2

def f(x):
    b = x + 3
    return b

print("Variable a avant appel de f:", a)
f(a)
print("Variable a après appel de f:", a)

Exemple 3

Avec le code:

a = 2

def f(a):
    a = a + 3
    return a

print("Variable a avant appel de f:", a)
f(a)
print("Variable a après appel de f:", a)

quels affichages?

Réponse

Dans les deux cas, la valeur affichée est 2.

Décomposons:

  • En ligne 1, on crée un objet de type int de valeur 2.

  • Les lignes 3 à 5 ne font que définir f, pour le moment elles ne sont pas exécutées.

  • La ligne suivante qui est exécutée est la ligne 7, elle affiche la valeur ciblée par a, c'est à dire 2.
  • La ligne 8 fait alors appel à f.

    • L'appel est f(a). Attention, à ce stade une nouvellle étiquette a est créée, mais qui ne sera connu que de f. On pourrait la nommer a_f pour le distinguer de l'étiquette a déjà créée.
      L'appel f(a) commence par une affectation a_f ← a, qui crée une nouvelle étiquette sur l'objet a.
    • On exécute ensuite la ligne 4 (dans le corps de la fonction). L'affectation a = a + 3 crée cette fois un nouvel objet qui recevra la valeur 2+3 et l'étiquette a. Attention, il s'agit là encore de l'étiquette a = a_f qui n'existe que dans l'espace local à f:
    • La ligne 5 return a signifie que f(a) vaut 5... (mais on ne s'en sert pas dans ce code en retournant dans la partie globale!) La fonction se termine: tous les noms (étiquettes) locaux à f disparaissent:
  • On reprend le déroulé du programme à la ligne 9 et on affiche la valeur du seul objet encore étiqueté a: il a pour valeur 2.

Le code ci-dessus peut donc être réécrit de façon parfaitement équivalente sous la forme:

a = 2

def f(b):
    b = b + 3
    return b

print("Variable a avant appel de f:", a)
f(a) # ici, appel équivalent à f(2)
print("Variable a après appel de f:", a)

Cette seconde écriture est certainement plus facile à lire car elle n'entraîne pas d'éventuelles confusions pour le lecteur entre des noms de variable de l'espace local à une fonction et des noms de variables de l'espace global.

Retenez cela: en général, évitez toujours de nommer les paramètres de fonction du même nom que des variables globales, cela ne ferait que compliquer la lecture de votre code.

Exemple 4

Avec le code:

a = 2

def f():
    a = a + 3
    return a

print("Variable a avant appel de f:", a)
f()
print("Variable a après appel de f:", a)

quels affichages?

Réponse

On obtient une erreur:

line 4, in f
a = a + 3
UnboundLocalError: local variable 'a' referenced before assignment

Décomposons:

  • En ligne 1, on crée un objet de type int de valeur 2.

  • La ligne 8 fait appel à f.

    • L'appel est f().
    • On exécute ensuite la ligne 4 (dans le corps de la fonction). L'affectation a = a + 3 est la ligne qui provoque une erreur.
      Pourquoi ?
      Comme nous avons vu que le membre de droite d'une affectation était exécuté en premier lieu, on pourrait s'attendre à ce que le a de droite désigne le a de l'espace global, puis qu'un a = a_f local à f soit créé avec la partie gauche de l'instruction d'affectation.
      Cela n'a pas lieu: la présence d'un a local et d'un a global dans un même espace de noms est interdit (si cela était permis, cela ne ferait que compliquer la lecture de code!). L'opération demandée ici est donc interdite. Même si l'on commence par la partie droite de l'instruction d'affectation, il faut donc considérer que l'on commence en fait par une lecture globale de l'instruction qui crée immédiatement un nom a local, ce qui fait que le a de droite ne peut que désigner également ce a local et n'a donc pas encore de valeur, d'où l'erreur affichée (referenced before assignment: variable référencée avant d'avoir été affectée).

    Mais nous avons vu dans les exemples précédents une façon qui pourrait contourner le problème. Le code suivant ne pose pas de problème (tout en étant inutile puisque, comme dans les exemples précédents, on n'utilise pas ici la valeur renvoyée par la fonction):

    a = 2
    
    def f():
        b = a + 3
        return b
    
    print("Variable a avant appel de f:", a)
    f()
    print("Variable a après appel de f:", a)
    

    La morale est toujours la même: évitez d'utiliser dans une fonction des noms qui peuvent prêter confusion avec les noms de l'espace global, votre code n'en sera que plus facile à lire!