UnboundLocalError sur la variable locale lors de la réaffectation après la première utilisation

209

Le code suivant fonctionne comme prévu dans Python 2.5 et 3.0:

a, b, c = (1, 2, 3)

print(a, b, c)

def test():
    print(a)
    print(b)
    print(c)    # (A)
    #c+=1       # (B)
test()

Cependant, lorsque je décommente la ligne (B) , j'obtiens une UnboundLocalError: 'c' not assignedligne (A) . Les valeurs de aet bsont imprimées correctement. Cela m'a complètement dérouté pour deux raisons:

  1. Pourquoi une erreur d'exécution est-elle renvoyée à la ligne (A) en raison d'une instruction ultérieure sur la ligne (B) ?

  2. Pourquoi les variables sont a-elles bimprimées comme prévu, alors que cgénère une erreur?

La seule explication que je peux trouver est qu'une variable localec est créée par l'affectation c+=1, qui a priorité sur la variable "globale" cavant même que la variable locale soit créée. Bien sûr, il n'est pas logique qu'une variable "vole" la portée avant qu'elle n'existe.

Quelqu'un pourrait-il expliquer ce comportement?

tba
la source
Est-ce que cela répond à votre question? Je ne comprends pas pourquoi UnboundLocalError se produit (fermeture)
norok2

Réponses:

216

Python traite les variables dans les fonctions différemment selon que vous leur attribuez des valeurs de l'intérieur ou de l'extérieur de la fonction. Si une variable est affectée dans une fonction, elle est traitée par défaut comme une variable locale. Par conséquent, lorsque vous décommentez la ligne, vous essayez de référencer la variable locale cavant qu'aucune valeur ne lui ait été affectée.

Si vous voulez que la variable cse réfère au global c = 3assigné avant la fonction, mettez

global c

comme première ligne de la fonction.

Quant à python 3, il y a maintenant

nonlocal c

que vous pouvez utiliser pour faire référence à l'étendue de la fonction englobante la plus proche qui a une cvariable.

récursif
la source
3
Merci. Question rapide. Est-ce à dire que Python décide de l'étendue de chaque variable avant d'exécuter un programme? Avant d'exécuter une fonction?
confirmer
7
La décision de portée variable est prise par le compilateur, qui s'exécute normalement une fois lorsque vous démarrez le programme pour la première fois. Cependant, il convient de garder à l'esprit que le compilateur peut également s'exécuter plus tard si vous avez des instructions "eval" ou "exec" dans votre programme.
Greg Hewgill
2
D'accord, merci. Je suppose que le «langage interprété» n'implique pas autant que je l'avais pensé.
confirmer
1
Ah ce mot-clé «non local» était exactement ce que je cherchais, il semblait que Python manquait cela. Vraisemblablement, cette «cascade» à travers chaque portée englobante qui importe la variable à l'aide de ce mot clé?
Brendan
6
@brainfsck: il est plus facile à comprendre si vous faites la distinction entre "rechercher" et "assigner" une variable. La recherche revient à une portée plus élevée si le nom n'est pas trouvé dans la portée actuelle. L'affectation est toujours effectuée dans la portée locale (sauf si vous utilisez globalou nonlocalpour forcer une affectation globale ou non locale)
Steven
71

Python est un peu bizarre en ce qu'il garde tout dans un dictionnaire pour les différentes étendues. Les originaux a, b, c sont dans la portée la plus élevée et donc dans ce dictionnaire le plus élevé. La fonction possède son propre dictionnaire. Lorsque vous atteignez les instructions print(a)et print(b), il n'y a rien de ce nom dans le dictionnaire, donc Python recherche la liste et les trouve dans le dictionnaire global.

Nous arrivons maintenant à c+=1, ce qui est, bien sûr, équivalent à c=c+1. Lorsque Python scanne cette ligne, il dit "aha, il y a une variable nommée c, je vais la mettre dans mon dictionnaire de portée locale." Puis quand il va chercher une valeur pour c pour le c sur le côté droit de l'affectation, il trouve sa variable locale nommée c , qui n'a pas encore de valeur, et lance donc l'erreur.

L'instruction global cmentionnée ci-dessus indique simplement à l'analyseur qu'il utilise le cde la portée globale et n'a donc pas besoin d'un nouveau.

La raison pour laquelle il dit qu'il y a un problème sur la ligne qu'il fait, c'est parce qu'il recherche effectivement les noms avant d'essayer de générer du code, et donc, dans un certain sens, ne pense pas qu'il fait vraiment encore cette ligne. Je dirais que c'est un bug d'utilisation, mais c'est généralement une bonne pratique d'apprendre simplement à ne pas prendre trop au sérieux les messages d'un compilateur .

Si cela vous rassure, j'ai probablement passé une journée à creuser et à expérimenter ce même problème avant de trouver quelque chose que Guido avait écrit sur les dictionnaires qui expliquaient tout.

Mettre à jour, voir les commentaires:

Il ne scanne pas le code deux fois, mais il scanne le code en deux phases, lexing et analyse.

Considérez comment l'analyse de cette ligne de code fonctionne. Le lexer lit le texte source et le décompose en lexèmes, les "plus petits composants" de la grammaire. Alors quand ça arrive

c+=1

il le décompose en quelque chose comme

SYMBOL(c) OPERATOR(+=) DIGIT(1)

L'analyseur souhaite finalement en faire un arbre d'analyse et l'exécuter, mais comme il s'agit d'une affectation, avant de le faire, il recherche le nom c dans le dictionnaire local, ne le voit pas et l'insère dans le dictionnaire, marquant comme non initialisé. Dans un langage entièrement compilé, il irait simplement dans la table des symboles et attendrait l'analyse, mais comme il N'AURA PAS le luxe d'un deuxième passage, le lexer fait un petit travail supplémentaire pour vous faciliter la vie plus tard. Seulement, alors il voit l'OPÉRATEUR, voit que les règles disent "si vous avez un opérateur + = le côté gauche doit avoir été initialisé" et dit "whoops!"

Le point ici est qu'il n'a pas encore vraiment commencé l'analyse de la ligne . Tout cela se passe en quelque sorte préparatoire à l'analyse réelle, donc le compteur de lignes n'a pas avancé à la ligne suivante. Ainsi, quand il signale l'erreur, il pense toujours que c'est sur la ligne précédente.

Comme je l'ai dit, vous pourriez dire que c'est un bug d'utilisation, mais c'est en fait une chose assez courante. Certains compilateurs sont plus honnêtes à ce sujet et disent "erreur sur ou autour de la ligne XXX", mais celui-ci ne le fait pas.

Charlie Martin
la source
1
D'accord merci pour votre réponse; cela m'a permis de clarifier certaines choses sur les portées en python. Cependant, je ne comprends toujours pas pourquoi l'erreur est déclenchée à la ligne (A) plutôt qu'à la ligne (B). Python crée-t-il son dictionnaire à portée variable AVANT d'exécuter le programme?
confirmer
1
Non, c'est au niveau de l'expression. J'ajouterai à la réponse, je ne pense pas pouvoir intégrer cela dans un commentaire.
Charlie Martin
2
Remarque sur les détails de l'implémentation: Dans CPython, la portée locale n'est généralement pas gérée comme un dict, c'est en interne juste un tableau ( locals()remplira a dictpour revenir, mais les modifications apportées ne créent pas de nouveau locals). La phase d'analyse consiste à trouver chaque affectation en local et à convertir le nom en position dans ce tableau, et à utiliser cette position chaque fois que le nom est référencé. À l'entrée de la fonction, les sections locales sans argument sont initialisées dans un espace réservé et UnboundLocalErrors se produisent lorsqu'une variable est lue et que son index associé a toujours la valeur d'espace réservé.
ShadowRanger
44

Un regard sur le démontage peut clarifier ce qui se passe:

>>> def f():
...    print a
...    print b
...    a = 1

>>> import dis
>>> dis.dis(f)

  2           0 LOAD_FAST                0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE

  3           5 LOAD_GLOBAL              0 (b)
              8 PRINT_ITEM
              9 PRINT_NEWLINE

  4          10 LOAD_CONST               1 (1)
             13 STORE_FAST               0 (a)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE

Comme vous pouvez le voir, le bytecode pour accéder à un est LOAD_FAST, et b, LOAD_GLOBAL. Cela est dû au fait que le compilateur a identifié qu'un a est affecté à l'intérieur de la fonction et l'a classé comme variable locale. Le mécanisme d'accès pour les locaux est fondamentalement différent pour les globaux - ils sont statiquement assignés un décalage dans la table des variables du cadre, ce qui signifie que la recherche est un index rapide, plutôt que la recherche de dict plus coûteuse que pour les globaux. Pour cette raison, Python lit la print aligne comme «obtenir la valeur de la variable locale« a »contenue dans l'emplacement 0 et l'imprimer», et lorsqu'il détecte que cette variable n'est toujours pas initialisée, déclenche une exception.

Brian
la source
10

Python a un comportement plutôt intéressant lorsque vous essayez la sémantique des variables globales traditionnelles. Je ne me souviens pas des détails, mais vous pouvez très bien lire la valeur d'une variable déclarée dans la portée «globale», mais si vous voulez la modifier, vous devez utiliser le globalmot - clé. Essayez de passer test()à ceci:

def test():
    global c
    print(a)
    print(b)
    print(c)    # (A)
    c+=1        # (B)

En outre, la raison pour laquelle vous obtenez cette erreur est que vous pouvez également déclarer une nouvelle variable à l'intérieur de cette fonction avec le même nom qu'une variable «globale», et elle serait complètement distincte. L'interprète pense que vous essayez de créer une nouvelle variable dans cette portée cet de la modifier en une seule opération, ce qui n'est pas autorisé en Python car cette nouvelle cn'a pas été initialisée.

Mangouste
la source
Merci pour votre réponse, mais je ne pense pas que cela explique pourquoi l'erreur est renvoyée à la ligne (A), où j'essaie simplement d'imprimer une variable. Le programme n'arrive jamais à la ligne (B) où il essaie de modifier une variable non initialisée.
confirmer
1
Python lira, analysera et transformera toute la fonction en bytecode interne avant de commencer à exécuter le programme, donc le fait que le "transformer c en variable locale" se produise textuellement après l'impression de la valeur n'a pas, pour ainsi dire, d'importance.
Vatine
6

Le meilleur exemple qui le montre clairement est:

bar = 42
def foo():
    print bar
    if False:
        bar = 0

lors de l'appel foo(), cela augmente également UnboundLocalErrorbien que nous n'atteindrons jamais la ligne bar=0, donc la variable logiquement locale ne doit jamais être créée.

Le mystère réside dans " Python est un langage interprété " et la déclaration de la fonction fooest interprétée comme une instruction unique (c'est-à-dire une instruction composée), elle l'interprète simplement de façon stupide et crée des étendues locales et globales. barEst donc reconnu dans la portée locale avant l'exécution.

Pour plus d'exemples comme celui-ci, lisez cet article: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Cet article fournit une description complète et des analyses de la portée Python des variables:

Sahil kalra
la source
5

Voici deux liens qui peuvent vous aider

1: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

2: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call-by-reference

le lien un décrit l'erreur UnboundLocalError. Le lien deux peut vous aider à réécrire votre fonction de test. Sur la base du lien deux, le problème d'origine pourrait être réécrit comme suit:

>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
...     print (a)
...     print (b)
...     print (c)
...     c += 1
...     return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)
mcdon
la source
4

Ce n'est pas une réponse directe à votre question, mais elle est étroitement liée, car c'est un autre problème causé par la relation entre l'affectation augmentée et les étendues de fonction.

Dans la plupart des cas, vous avez tendance à penser que l'affectation augmentée ( a += b) est exactement équivalente à l'affectation simple ( a = a + b). Il est possible d'avoir des problèmes avec cela, dans un cas d'angle. Laisse-moi expliquer:

La façon dont l'affectation simple de Python fonctionne signifie que si aest passé dans une fonction (comme func(a); notez que Python est toujours passe-par-référence), alors a = a + bne modifiera pas ce aqui est passé. Au lieu de cela, il modifiera simplement le pointeur local vers a.

Mais si vous utilisez a += b, alors il est parfois implémenté comme:

a = a + b

ou parfois (si la méthode existe) comme:

a.__iadd__(b)

Dans le premier cas (tant qu'il an'est pas déclaré global), il n'y a pas d'effets secondaires en dehors de la portée locale, car l'affectation à an'est qu'une mise à jour du pointeur.

Dans le second cas, ase modifiera réellement, donc toutes les références à apointeront vers la version modifiée. Cela est démontré par le code suivant:

def copy_on_write(a):
      a = a + a
def inplace_add(a):
      a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1

L'astuce consiste donc à éviter l'affectation augmentée sur les arguments de fonction (j'essaie de ne l'utiliser que pour les variables locales / de boucle). Utilisez une affectation simple et vous serez à l'abri d'un comportement ambigu.

alsuren
la source
2

L'interpréteur Python lira une fonction comme une unité complète. Je pense à cela comme le lisant en deux passes, une fois pour rassembler sa fermeture (les variables locales), puis encore pour la transformer en octet-code.

Comme je suis sûr que vous le saviez déjà, tout nom utilisé à gauche d'un '=' est implicitement une variable locale. Plus d'une fois, j'ai été rattrapé en changeant l'accès à une variable en + = et c'est soudain une variable différente.

Je voulais également souligner que cela n'a rien à voir avec la portée mondiale en particulier. Vous obtenez le même comportement avec les fonctions imbriquées.

James Hopkin
la source
2

c+=1assigne c, python suppose que les variables attribuées sont locales, mais dans ce cas, elles n'ont pas été déclarées localement.

Utilisez les mots global- nonlocalclés ou .

nonlocal ne fonctionne qu'en python 3, donc si vous utilisez python 2 et que vous ne voulez pas rendre votre variable globale, vous pouvez utiliser un objet mutable:

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()
Colegram
la source
1

La meilleure façon d'atteindre la variable de classe est d'accéder directement par nom de classe

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
Harun ERGUL
la source
0

En python, nous avons une déclaration similaire pour tous les types de variables locales, variables de classe et variables globales. lorsque vous faites référence à une variable globale à partir d'une méthode, python pense que vous faites réellement référence à une variable à partir de la méthode elle-même qui n'est pas encore définie et donc génère une erreur. Pour référencer une variable globale, nous devons utiliser globals () ['variableName'].

dans votre cas, utilisez globals () ['a], globals () [' b '] et globals () [' c '] au lieu de a, b et c respectivement.

Santosh Kadam
la source
0

Le même problème me dérange. Utiliser nonlocalet globalpeut résoudre le problème.
Cependant, attention requise pour l'utilisation de nonlocal, cela fonctionne pour les fonctions imbriquées. Cependant, au niveau d'un module, cela ne fonctionne pas. Voir des exemples ici.

Qinsheng Zhang
la source