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

  1. No primeiro programa, logo depois de add_fee(alias, 500) e antes das impressões, acrescente add_fee(copy, 100). A lista copy passa a mostrar [3500, 8000, 100]; a lista original continua mostrando [3500, 8000, 500]. Remova essa linha antes do próximo teste.
  2. Dentro de add_fee(), troque somente items.append(fee) por items = items + [fee]. Essa expressão cria uma lista e associa o nome local items a ela. Como o novo valor não é retornado, o chamador não o recebe: a primeira linha agora mostra [3500, 8000]. A alteração de nested continua ocorrendo, pois é um trecho independente.
  3. Restaure append() e confirme as três linhas originais. Para comparar identidade, acrescente print(alias is original, copy is original, shallow is nested, shallow[0] is nested[0]) ao final. O resultado adicional deve ser True False False True. Retire a linha depois da comparação.
  4. No segundo programa, crie other = make_counter() após a impressão existente e acrescente print(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.