UnboundLocalError na variável local quando reatribuído após o primeiro uso

209

O código a seguir funciona conforme o esperado no Python 2.5 e 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()

No entanto, quando descomente a linha (B) , recebo uma UnboundLocalError: 'c' not assignedna linha (A) . Os valores de ae bsão impressos corretamente. Isso me deixou completamente perplexo por dois motivos:

  1. Por que há um erro de tempo de execução lançado na linha (A) devido a uma instrução posterior na linha (B) ?

  2. Por que são variáveis ae bimpresso como esperado, enquanto cgera um erro?

A única explicação que posso encontrar é que uma variável localc é criada pela atribuição c+=1, que tem precedência sobre a variável "global" cmesmo antes de a variável local ser criada. Obviamente, não faz sentido que uma variável "roube" o escopo antes que ele exista.

Alguém poderia explicar esse comportamento?

tba
fonte

Respostas:

216

O Python trata variáveis ​​em funções de maneira diferente, dependendo se você atribui valores a elas de dentro ou de fora da função. Se uma variável é atribuída dentro de uma função, ela é tratada por padrão como uma variável local. Portanto, ao remover o comentário da linha, você está tentando fazer referência à variável local cantes que qualquer valor tenha sido atribuído a ela.

Se você deseja que a variável cse refira ao global c = 3atribuído antes da função, coloque

global c

como a primeira linha da função.

Quanto ao python 3, existe agora

nonlocal c

que você pode usar para se referir ao escopo da função de fechamento mais próximo que possui uma cvariável.

recursivo
fonte
3
Obrigado. Pergunta rápida. Isso implica que o Python decida o escopo de cada variável antes de executar um programa? Antes de executar uma função?
tba
7
A decisão de escopo variável é tomada pelo compilador, que normalmente é executado uma vez quando você inicia o programa. No entanto, vale lembrar que o compilador também poderá ser executado posteriormente se você tiver instruções "eval" ou "exec" em seu programa.
Greg Hewgill
2
Ok obrigado. Eu acho que "linguagem interpretada" não implica tanto quanto eu pensava.
tba
1
Ah, essa palavra-chave 'não-local' era exatamente o que eu estava procurando, parecia que o Python estava faltando. Presumivelmente, essas 'cascatas' em cada escopo anexo que importam a variável usando esta palavra-chave?
Brendan
6
@brainfsck: é mais fácil entender se você faz a distinção entre "procurar" e "atribuir" uma variável. A pesquisa volta para um escopo mais alto se o nome não for encontrado no escopo atual. A atribuição é sempre feita no escopo local (a menos que você use globalou nonlocalforce a atribuição global ou não local)
Steven
71

O Python é um pouco estranho, pois mantém tudo em um dicionário para os vários escopos. Os originais a, b, c estão no escopo mais alto e, portanto, no dicionário mais alto. A função possui seu próprio dicionário. Quando você alcança as instruções print(a)e print(b), não há nada com esse nome no dicionário; portanto, o Python pesquisa a lista e as encontra no dicionário global.

Agora chegamos a c+=1, que é, é claro, equivalente a c=c+1. Quando o Python verifica essa linha, ele diz "aha, há uma variável chamada c, eu a colocarei no meu dicionário de escopo local". Então, quando procura um valor para c para c no lado direito da atribuição, encontra sua variável local denominada c , que ainda não tem valor e, portanto, gera o erro.

A declaração global cmencionada acima simplesmente informa ao analisador que ele usa o cescopo global e, portanto, não precisa de um novo.

A razão pela qual diz que existe um problema na linha é que ela está procurando efetivamente os nomes antes de tentar gerar código e, de alguma forma, ainda não acha que realmente está fazendo essa linha. Eu diria que é um bug de usabilidade, mas geralmente é uma boa prática apenas aprender a não levar as mensagens de um compilador muito a sério.

Para facilitar, passei provavelmente um dia cavando e experimentando esse mesmo problema antes de encontrar algo que Guido havia escrito sobre os dicionários que explicavam tudo.

Atualização, veja os comentários:

Ele não verifica o código duas vezes, mas verifica o código em duas fases, lexing e análise.

Considere como a análise dessa linha de código funciona. O lexer lê o texto original e o divide em lexemas, os "menores componentes" da gramática. Então, quando atinge a linha

c+=1

divide em algo como

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

O analisador eventualmente quer transformar isso em uma árvore de análise e executá-lo, mas como é uma atribuição, antes disso, ele procura o nome c no dicionário local, não o vê e o insere no dicionário, marcando como não inicializado. Em uma linguagem totalmente compilada, ele simplesmente entra na tabela de símbolos e aguarda a análise, mas como não tem o luxo de um segundo passe, o lexer faz um trabalho extra para facilitar a vida mais tarde. Somente então ele vê o OPERADOR, vê que as regras dizem "se você tem um operador + = o lado esquerdo deve ter sido inicializado" e diz "gritos!"

O ponto aqui é que ele ainda não iniciou a análise da linha . Tudo isso está meio preparatório para a análise real, portanto o contador de linhas não avançou para a próxima linha. Assim, quando sinaliza o erro, ele ainda pensa que está na linha anterior.

Como eu disse, você pode argumentar que é um bug de usabilidade, mas na verdade é uma coisa bastante comum. Alguns compiladores são mais honestos e dizem "erro na linha XXX", mas esse não é o caso.

Charlie Martin
fonte
1
Ok, obrigado pela sua resposta; esclareceu algumas coisas para mim sobre escopos em python. No entanto, ainda não entendo por que o erro foi gerado na linha (A) e não na linha (B). O Python cria seu dicionário de escopo variável ANTES de executar o programa?
tba
1
Não, está no nível da expressão. Vou acrescentar à resposta, acho que não consigo encaixar isso em um comentário.
Charlie Martin
2
Nota sobre os detalhes da implementação: no CPython, o escopo local geralmente não é tratado como a dict, é internamente apenas uma matriz ( locals()preencherá um dictpara retornar, mas as alterações nele não criarão novo locals). A fase de análise está localizando cada atribuição para um local e convertendo de nome para posição nessa matriz e usando essa posição sempre que o nome é referenciado. Na entrada da função, os locais sem argumentos são inicializados em um espaço reservado UnboundLocalErrores acontecem quando uma variável é lida e seu índice associado ainda tem o valor do espaço reservado.
ShadowRanger 25/03
44

Examinar a desmontagem pode esclarecer o que está acontecendo:

>>> 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

Como você pode ver, o bytecode para acessar a é LOAD_FASTe para b LOAD_GLOBAL,. Isso ocorre porque o compilador identificou que a é atribuído à função e a classificou como uma variável local. O mecanismo de acesso para os locais é fundamentalmente diferente para os globais - eles recebem estaticamente um deslocamento na tabela de variáveis ​​do quadro, o que significa que a pesquisa é um índice rápido, em vez da pesquisa de ditado mais cara do que os globais. Por isso, o Python está lendo a print alinha como "obtenha o valor da variável local 'a' mantida no slot 0 e imprima-a" e, quando detecta que essa variável ainda não foi inicializada, gera uma exceção.

Brian
fonte
10

O Python possui um comportamento bastante interessante quando você tenta a semântica tradicional de variáveis ​​globais. Não me lembro dos detalhes, mas você pode ler muito bem o valor de uma variável declarada no escopo 'global', mas se quiser modificá-la, use a globalpalavra - chave. Tente mudar test()para isso:

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

Além disso, o motivo pelo qual você está recebendo esse erro é porque você também pode declarar uma nova variável dentro dessa função com o mesmo nome de uma 'global', e ela seria completamente separada. O intérprete acha que você está tentando criar uma nova variável nesse escopo chamada ce modificá-la em uma única operação, o que não é permitido no Python porque essa nova cnão foi inicializada.

Mangusto
fonte
Obrigado pela sua resposta, mas acho que não explica por que o erro é lançado na linha (A), onde estou apenas tentando imprimir uma variável. O programa nunca chega à linha (B) onde está tentando modificar uma variável não inicializada.
tba
1
O Python lerá, analisará e transformará toda a função em código de bytes interno antes de iniciar a execução do programa; portanto, o fato de a opção "transformar c em variável local" ocorrer textualmente após a impressão do valor não importa.
Vatine
6

O melhor exemplo que deixa claro é:

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

ao chamar foo(), isso também aumenta, UnboundLocalError embora nunca cheguemos à linha bar=0; portanto, a variável local logicamente nunca deve ser criada.

O mistério está em " Python é uma linguagem interpretada " e a declaração da função fooé interpretada como uma única declaração (isto é, uma declaração composta), apenas a interpreta de maneira tola e cria escopos locais e globais. Portanto, baré reconhecido no escopo local antes da execução.

Para mais exemplos como este Leia este post: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Este post fornece uma Descrição Completa e Análises do Escopo do Python de variáveis:

Sahil kalra
fonte
5

Aqui estão dois links que podem ajudar

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

o link um descreve o erro UnboundLocalError. O link dois pode ajudar a reescrever sua função de teste. Com base no link dois, o problema original pode ser reescrito como:

>>> 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
fonte
4

Essa não é uma resposta direta à sua pergunta, mas está intimamente relacionada, pois é outro problema causado pelo relacionamento entre atribuição aumentada e escopos de função.

Na maioria dos casos, você tende a pensar em atribuição aumentada ( a += b) como exatamente equivalente a tarefa simples ( a = a + b). É possível ter algum problema com isso, em um dos cantos. Deixe-me explicar:

A maneira como a atribuição simples do Python funciona significa que, se afor passado para uma função (como func(a); observe que o Python sempre é passado por referência), ele a = a + bnão modificará o aque é passado. Em vez disso, apenas modificará o ponteiro local para a.

Mas se você usar a += b, às vezes é implementado como:

a = a + b

ou algumas vezes (se o método existir) como:

a.__iadd__(b)

No primeiro caso (desde que anão seja declarado global), não há efeitos colaterais fora do escopo local, pois a atribuição a aé apenas uma atualização de ponteiro.

No segundo caso, aele realmente se modificará, portanto, todas as referências aapontarão para a versão modificada. Isso é demonstrado pelo seguinte código:

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

Portanto, o truque é evitar a atribuição aumentada de argumentos de função (tento usá-lo apenas para variáveis ​​locais / de loop). Use tarefas simples e estará protegido contra comportamentos ambíguos.

alsuren
fonte
2

O intérprete Python lerá uma função como uma unidade completa. Penso nisso como leitura em duas passagens, uma vez para reunir seu fechamento (as variáveis ​​locais) e depois novamente para transformá-lo em código de bytes.

Como tenho certeza de que você já sabia, qualquer nome usado à esquerda de um '=' é implicitamente uma variável local. Mais de uma vez fui pego alterando o acesso de uma variável para um + = e, de repente, ela é uma variável diferente.

Eu também queria ressaltar que não tem nada a ver com o escopo global especificamente. Você obtém o mesmo comportamento com funções aninhadas.

James Hopkin
fonte
2

c+=1atribui c, o python assume que as variáveis ​​atribuídas são locais, mas, neste caso, não foi declarado localmente.

Use as palavras-chave globalou nonlocal.

nonlocal funciona apenas no python 3, portanto, se você estiver usando o python 2 e não quiser tornar sua variável global, poderá usar um objeto mutável:

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

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

test()
Colegram
fonte
1

A melhor maneira de alcançar a variável de classe é acessando diretamente pelo nome da classe

class Employee:
    counter=0

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

Em python, temos uma declaração semelhante para todos os tipos de variáveis ​​local, variável de classe e variáveis ​​globais. Quando você se refere a variável global do método, o python acha que você está realmente referindo a variável do próprio método que ainda não está definida e, portanto, gera erro. Para se referir à variável global, precisamos usar globals () ['variableName'].

no seu caso, use globals () ['a], globals () [' b '] e globals () [' c '] em vez de a, bec, respectivamente.

Santosh Kadam
fonte
0

O mesmo problema me incomoda. Usando nonlocale globalpode resolver o problema.
No entanto, atenção necessária para o uso de nonlocal, ele funciona para funções aninhadas. No entanto, em um nível de módulo, ele não funciona. Veja exemplos aqui.

Qinsheng Zhang
fonte