Curso de Python / Escopo de variáveis, aliases e objetos mutáveis
O escopo determina onde Python procura um nome; a mutabilidade determina se um objeto pode ser alterado depois de criado. Atribuir um novo objeto a um nome local não troca automaticamente o objeto do chamador, mas modificar uma lista compartilhada torna a alteração visível por todos os nomes que apontam para ela.
Separar o alcance de um nome da identidade do objeto
Um nome associado dentro de uma função normalmente é local, salvo declarações como global ou nonlocal. A leitura de um nome externo não é igual à atribuição de um novo valor a esse nome. Em funções comuns, a procura de nomes é frequentemente resumida por LEGB: local, função envolvente (enclosing), módulo global e nomes embutidos (builtins). Esse resumo se refere ao contexto de funções deste exercício; classes e outros contextos têm regras específicas.
global direciona a associação de um nome para o nível do módulo. nonlocal permite reassociar um nome já existente numa função envolvente. Antes de recorrer a essas declarações, considere receber os dados por argumentos e devolver o resultado: isso deixa as dependências e os testes mais claros.
Objetos e nomes são questões relacionadas, mas distintas. Dois nomes locais ou globais podem apontar para a mesma lista. Uma cópia pode criar uma lista externa diferente e ainda compartilhar suas listas internas. O problema dos valores padrão mutáveis é outro exemplo em que o compartilhamento persiste além de uma única chamada.
Observar listas compartilhadas e cópias rasas
Salve o programa completo abaixo em scope.py, com codificação UTF-8, numa pasta de prática. No terminal com o ambiente virtual selecionado, execute python scope.py. Em macOS ou Ubuntu sem ambiente virtual, use python3 scope.py se esse for o nome do interpretador. Os dois programas deste artigo usam somente a biblioteca padrão.
def add_fee(items, fee):
items.append(fee)
original = [3500, 8000]
alias = original
copy = original.copy()
add_fee(alias, 500)
print(original)
print(copy)
nested = [[1], [2]]
shallow = nested.copy()
shallow[0].append(9)
print(nested)
A saída esperada é:
[3500, 8000, 500]
[3500, 8000]
[[1, 9], [2]]
alias = original cria outro nome para a mesma lista, sem copiá-la. Ao chamar add_fee(alias, 500), o parâmetro local items também passa a apontar para essa lista. items.append(fee) modifica o objeto, por isso original passa a mostrar 500 no fim. Não é necessário declarar global para essa alteração, pois o código não reassocia o nome global original.
original.copy() cria uma cópia rasa: uma nova lista externa cujos elementos ainda referenciam os mesmos objetos que os da origem. Como os elementos de original são inteiros imutáveis, acrescentar um elemento à lista original não acrescenta nada à lista chamada copy. No trecho com nested, os elementos são listas mutáveis: shallow[0] e nested[0] são a mesma lista interna, então o acréscimo de 9 aparece pelos dois caminhos.
Uma cópia profunda pode copiar recursivamente as listas internas desse exemplo. O módulo copy fornece deepcopy(), mas não duplica genericamente arquivos abertos, soquetes ou conexões. Copiar toda uma estrutura também pode copiar dados que deveriam permanecer compartilhados. Escolha a parte que precisa ser independente, conforme as diferenças entre cópia rasa e profunda. No programa acima, copy é o nome de uma variável; ao importar o módulo no mesmo arquivo, use um nome distinto, como import copy as copylib, para evitar conflito.
Examinar atribuições locais e estado de uma função envolvente
Salve o segundo programa completo em outro arquivo, scope_names.py, para manter o primeiro exemplo disponível. Execute python scope_names.py no mesmo terminal, ou python3 scope_names.py quando necessário.
value = 10
def local_read():
value = 20
return value
def broken():
value += 1
return value
print(local_read(), value)
try:
broken()
except UnboundLocalError:
print("local name not initialized")
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter())
A saída esperada é:
20 10
local name not initialized
1 2
local_read() cria um nome local value com valor 20 e o retorna. O value do módulo continua sendo 10, daí a primeira linha 20 10. Em broken(), value += 1 envolve leitura e atribuição; por haver uma atribuição no corpo sem outra declaração, esse nome é tratado como local. A leitura ocorre antes de ele receber um valor local e gera UnboundLocalError, capturado para imprimir “local name not initialized”, isto é, “nome local não inicializado”.
O Python considera as operações de associação de nomes no corpo inteiro para determinar os nomes locais; não passa a procurar automaticamente o valor global quando a leitura local falha. A referência sobre resolução de nomes explica essa regra e as declarações que a modificam.
Em make_counter(), a função interna increment() usa nonlocal count para atualizar o contador da função envolvente. O retorno de make_counter() é a própria função increment, ainda sem chamá-la. A variável counter conserva essa função e o acesso ao estado envolvente; duas chamadas sucessivas devolvem 1 e 2. Outra chamada a make_counter() criaria um contador com estado separado.
Testar efeitos no chamador e explicitar o contrato
- No primeiro programa, logo depois de
add_fee(alias, 500)e antes das impressões, acrescenteadd_fee(copy, 100). A listacopypassa a mostrar[3500, 8000, 100]; a listaoriginalcontinua mostrando[3500, 8000, 500]. Remova essa linha antes do próximo teste. - Dentro de
add_fee(), troque somenteitems.append(fee)poritems = items + [fee]. Essa expressão cria uma lista e associa o nome localitemsa ela. Como o novo valor não é retornado, o chamador não o recebe: a primeira linha agora mostra[3500, 8000]. A alteração denestedcontinua ocorrendo, pois é um trecho independente. - Restaure
append()e confirme as três linhas originais. Para comparar identidade, acrescenteprint(alias is original, copy is original, shallow is nested, shallow[0] is nested[0])ao final. O resultado adicional deve serTrue False False True. Retire a linha depois da comparação. - No segundo programa, crie
other = make_counter()após a impressão existente e acrescenteprint(other(), counter()). A linha adicional é1 3: o novo contador começa separado e o anterior continua seu próprio estado. Remova as duas linhas para restaurar o exemplo.
Uma função deve informar se altera os objetos recebidos. Para preservar uma entrada e fornecer uma nova lista, é preciso retornar a nova lista e usar esse retorno no chamador; reassociar apenas um nome local não basta. Teste tanto o valor devolvido quanto o conteúdo original depois da chamada. Um resultado aparentemente correto pode ocultar uma alteração não desejada no objeto compartilhado.




