web-dev-qa-db-fra.com

Erreur de portée de la variable Python

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 retire le commentaire de la ligne (B), je reçois un UnboundLocalError: 'c' not assigned à la ligne (A). Les valeurs de a et b sont imprimées correctement. Cela m'a complètement dérouté pour deux raisons:

  1. Pourquoi une erreur d'exécution est-elle générée à la ligne (A) à cause d'une instruction ultérieure sur la ligne (B)?

  2. Pourquoi les variables a et b sont-elles imprimées comme prévu, alors que c génère une erreur?

La seule explication que je puisse trouver est qu'une variable localc est créée par l'affectation c+=1, qui prime sur la variable "globale" c même avant la création de la variable locale. Bien sûr, il n’a pas de sens pour une variable de "voler" la portée avant qu’elle n’existe.

Quelqu'un pourrait-il s'il vous plaît expliquer ce comportement?

185
tba

Python traite les variables dans les fonctions différemment selon que vous leur attribuez ou non des valeurs depuis la fonction. Si une fonction contient des assignations à une variable, elle est traitée par défaut comme une variable locale. Par conséquent, lorsque vous supprimez la mise en commentaire de la ligne, vous essayez de référencer une variable locale avant qu'aucune valeur ne lui ait été affectée.

Si vous souhaitez que la variable c fasse référence à la variable globale c put

global c

comme la première ligne de la fonction.

Quant à Python 3, il y a maintenant

nonlocal c

que vous pouvez utiliser pour faire référence à la portée de la fonction englobante la plus proche qui a une variable c.

192
recursive

Python est un peu bizarre dans la mesure où il conserve tout dans un dictionnaire pour les différentes portées. Les originaux a, b, c sont dans la plus haute portée et donc dans le dictionnaire le plus élevé. La fonction a son propre dictionnaire. Lorsque vous atteignez les instructions print(a) et print(b), il n'y a rien qui porte ce nom dans le dictionnaire. Par conséquent, Python recherche la liste et la trouve dans le dictionnaire global.

Nous arrivons maintenant à c+=1, ce qui, bien sûr, équivaut à c=c+1. Quand Python analyse cette ligne, il dit "aha, il y a une variable nommée c, je vais la mettre dans mon dictionnaire de champs d'application local". Puis, lorsqu'il cherche une valeur pour c pour le c situé à droite de l'affectation, il trouve sa variable locale nommée c, qui n'a pas encore de valeur, et renvoie donc l'erreur.

L'instruction global c mentionnée ci-dessus indique simplement à l'analyseur qu'il utilise la variable c de la portée globale et qu'elle n'a donc pas besoin d'une nouvelle.

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

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

Mise à jour, voir les commentaires:

Il ne scanne pas le code deux fois mais scanne le code en deux phases, lexage et l'analyse.

Considérez comment l’analyse de cette ligne de code fonctionne. Le lexer lit le texte source et le divise en lexèmes, les "plus petits composants" de la grammaire. Alors quand il frappe la ligne

c+=1

il le divise en quelque chose comme

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

L’analyseur veut finalement en faire un arbre d’analyse syntaxique et l’exécuter, mais comme il s’agit d’une affectation, elle recherche le nom c dans le dictionnaire local, ne le voit pas et l’insère dans le dictionnaire, 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 second 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 que n'a pas encore commencé l'analyse de la ligne. Tout cela se passe en quelque sorte en préparation de l'analyse, donc le compteur de lignes n'a pas avancé à la ligne suivante. Ainsi, quand il signale l'erreur, il pense toujours à la ligne précédente.

Comme je l'ai dit, on pourrait dire que c'est un problème de facilité d'utilisation, mais c'est en fait une chose assez commune. Certains compilateurs sont plus honnêtes à ce sujet et disent "erreur sur la ligne XXX", mais celle-ci ne le fait pas.

67
Charlie Martin

Regarder 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 constater, le bytecode pour accéder à a est LOAD_FAST, et pour b, LOAD_GLOBAL. Cela est dû au fait que le compilateur a identifié a dans la fonction et l'a classé en tant que variable locale. Le mécanisme d'accès pour les sections locales est fondamentalement différent pour les globaux - ils se voient attribuer un décalage statique dans la table des variables du cadre, ce qui signifie que la recherche est un index rapide, plutôt que la recherche dictée plus coûteuse pour les globales. De ce fait, Python lit la ligne print a comme "récupère la valeur de la variable locale" a "conservée dans l'emplacement 0 et l'imprime"; une exception est détectée lorsqu'elle détecte que cette variable est toujours non initialisée.

42
Brian

Python a un comportement plutôt intéressant lorsque vous essayez la sémantique de variable globale traditionnelle. 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 souhaitez la modifier, vous devez utiliser le mot clé global. Essayez de changer test() en ceci:

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

En outre, si vous obtenez cette erreur, vous pouvez également déclarer une nouvelle variable de cette fonction portant le même nom que la variable "globale" et qui serait complètement séparée. L'interprète pense que vous essayez de créer une nouvelle variable dans cette étendue appelée c et de la modifier en une seule opération, ce qui n'est pas autorisé en Python car cette nouvelle c n'a pas été initialisée.

10
Mongoose

Voici deux liens qui peuvent 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 premier lien décrit l'erreur UnboundLocalError. Link Two peut vous aider à réécrire votre fonction de test. Sur la base du lien deux, le problème initial 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)
5
mcdon

Le meilleur exemple qui montre clairement est:

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

lors de l'appel de foo(), ceci aussi soulèveUnboundLocalError bien que nous n'allions jamais atteindre la ligne bar=0, de sorte qu'une variable locale logique ne devrait jamais être créée.

Le mystère réside dans "Python est un langage interprété" et la déclaration de la fonction foo est interprétée comme une instruction unique (c’est-à-dire une instruction composée). Donc, bar est reconnu dans la portée locale avant l'exécution.

Pour plus d'exemples comme celui-ci Lire 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 de Python des variables:

5
Sahil kalra

Il ne s'agit pas d'une réponse directe à votre question, mais elle est étroitement liée, car il s'agit d'un autre piège causé par la relation entre l'attribution augmentée et l'étendue des fonctions.

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 toutefois possible d’avoir des problèmes avec cela, dans un cas. Laisse-moi expliquer:

La façon dont fonctionne l'affectation simple de Python signifie que si a est passé à une fonction (comme func(a); notez que Python est toujours passé par référence), alors a = a + b ne modifiera pas le a transmis. pointeur local sur a

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

a = a + b

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

a.__iadd__(b)

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

Dans le second cas, a va réellement se modifier, ainsi toutes les références à a pointeront vers la version modifiée. Ceci 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

Le truc consiste donc à éviter l’affectation augmentée des arguments de la fonction (j’essaie de l’utiliser uniquement pour les variables locales/boucle). Utilisez une affectation simple, et vous serez à l’abri d’un comportement ambigu. 

3
alsuren

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

Comme vous le savez sans doute déjà, tout nom utilisé à gauche d'un '=' est implicitement une variable locale. Plus d'une fois, j'ai été surpris en changeant l'accès d'une variable à un = = et c'est soudain une variable différente.

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

2
James Hopkin

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

Utilisez les mots clés global ou nonlocal

nonlocal ne fonctionne que dans python 3, donc si vous utilisez python 2 et 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()
2
Colegram

Le meilleur moyen d’atteindre la variable de classe est d’y accéder directement par nom de classe

class Employee:
    counter=0

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

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 depuis une méthode, python pense que vous faites en réalité référence à une variable de la méthode elle-même qui n'est pas encore définie et lance donc error . globals () ['nom_variable'].

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

0
Santosh Kadam