Curso de Python / Distinguir atributos de instância e de classe

Atributos de instância guardam o estado de cada objeto, enquanto atributos de classe podem fornecer valores comuns pela própria classe. Quando cada objeto precisa de uma lista independente, crie essa lista durante a inicialização; uma lista definida no corpo da classe pode ser compartilhada por várias instâncias.

Localizar o atributo para entender a leitura e a atribuição

Nas classes simples deste exercício, self.items = [] dentro de __init__() associa uma lista nova à instância que está sendo inicializada. Já items = [] escrito diretamente no corpo da classe cria um atributo da classe. Se a instância não tiver seu próprio items, a consulta pode encontrar esse atributo comum.

Uma chamada como a.items.append(1) modifica a lista encontrada; ela não cria automaticamente uma cópia por ter sido escrita através de a. Por outro lado, a.items = [] associa uma lista à instância e pode ocultar, para essa instância, a lista da classe. A distinção é a mesma entre alterar um objeto compartilhado e reassociar um nome.

Um rótulo padrão imutável, como a moeda KRW, pode ser um atributo da classe. Atribuir b.currency = "USD" neste exemplo cria um valor próprio em b; não modifica Ledger.currency. A documentação de variáveis de classe e de instância descreve esses padrões de compartilhamento.

Executar listas independentes e um valor que oculta o padrão

Salve o programa completo em attributes.py, com UTF-8, numa pasta de prática. Execute python attributes.py no terminal com o ambiente virtual selecionado se houver. Em macOS ou Ubuntu sem ambiente virtual, use python3 attributes.py quando necessário. Não há dependências externas.

class Ledger:
    currency = "KRW"

    def __init__(self, owner):
        self.owner = owner
        self.items = []

    def add(self, amount):
        self.items.append(amount)

a = Ledger("A")
b = Ledger("B")
a.add(3500)
print(a.items, b.items)
b.currency = "USD"
print(a.currency, b.currency, Ledger.currency)

A saída esperada é:

[3500] []
KRW USD KRW

a e b recebem listas separadas durante duas chamadas de __init__(). Acrescentar 3500 a a mantém a lista de b vazia. Na segunda linha, a.currency ainda consulta o padrão da classe, b.currency consulta o valor próprio USD e Ledger.currency continua KRW.

Os textos de moeda são rótulos: o programa não faz conversão nem verifica se os valores da lista correspondem a essa unidade. O método add() também não valida tipos ou intervalos; ele apenas acrescenta o argumento recebido. As regras necessárias para uma aplicação real precisam ser definidas além da organização dos atributos.

Comparar compartilhamento acidental e estado por instância

Salve o segundo programa completo como shared_attributes.py, para preservar o primeiro arquivo. Execute python shared_attributes.py, ou python3 shared_attributes.py quando esse for o nome do interpretador.

class Shared:
    items = []
class Separate:
    def __init__(self):
        self.items = []
a, b = Shared(), Shared()
a.items.append(1)
print(a.items, b.items)
x, y = Separate(), Separate()
x.items.append(1)
print(x.items, y.items)

A saída esperada é:

[1] [1]
[1] []

Em Shared, nem a nem b tem seu próprio items. As duas consultas encontram a mesma lista da classe, e o acréscimo de 1 aparece pelos dois caminhos. Em Separate, cada inicialização cria uma lista; modificar x.items não modifica y.items.

Se uma classe receber uma lista externa e armazená-la diretamente, ela compartilhará esse objeto com quem a passou. Fazer uma cópia pode ser apropriado quando o contrato exigir independência, mas uma cópia rasa ainda compartilha objetos internos mutáveis. A simples presença de self no acesso não garante que a estrutura inteira seja exclusiva da instância.

Mudar o atributo da classe também não garante mudar o valor observado por todos os objetos. Depois de b.currency = "USD", uma atribuição a Ledger.currency altera o padrão consultado por a, mas b continua usando seu atributo próprio. Se esse comportamento dificultar a política de unidades, receber a moeda explicitamente na inicialização pode tornar o contrato mais claro.

Reproduzir o compartilhamento e restaurar a independência

  1. No primeiro arquivo, acrescente items = [] no corpo de Ledger, com a mesma indentação de currency, e retire self.items = [] de __init__(). Não deixe as duas atribuições ao mesmo tempo, pois a da instância continuaria ocultando a da classe.
  2. Execute num processo novo. A primeira linha passa a [3500] [3500]: a.add(3500) altera a lista comum que b também consulta. A segunda linha continua KRW USD KRW, pois a alteração da lista não muda a regra de moeda.
  3. Retire a lista do corpo da classe e restaure self.items = [] dentro de __init__(). Execute novamente e confirme [3500] [].
  4. Para testar somente a moeda, acrescente Ledger.currency = "EUR" depois de b.currency = "USD" e antes da última impressão. A linha passa a EUR USD EUR. Remova a atribuição e execute o programa num processo novo para restaurar o padrão KRW.

Uma lista compartilhada pode ser intencional, mas sua duração, seus responsáveis e os acessos concorrentes precisam fazer parte do desenho do programa. Não use um atributo mutável de classe como armazenamento global apenas porque ele parece funcionar numa instância isolada.

Ao testar uma classe, crie pelo menos duas instâncias e observe tanto o estado alterado como o que deve permanecer independente. A prática de classes, instâncias e métodos mostra o vínculo entre objeto e atributos; estes testes acrescentam a pergunta sobre onde o valor está guardado e quem o compartilha.