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 c
mencionada acima simplesmente informa ao analisador que ele usa o c
escopo 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.