Em Python, atributos são variáveis associadas a objetos. Eles podem pertencer à classe (compartilhados por todas as instâncias) ou a cada instância individualmente. Entender essa diferença é fundamental para projetar classes corretas e evitar bugs sutis.

Atributos de classe são definidos diretamente no corpo da classe, fora de qualquer método, e são acessíveis tanto pela classe quanto por suas instâncias. Já atributos de instância são criados dentro do método __init__ (ou outros métodos) e pertencem exclusivamente a cada objeto.

Diferença

A principal diferença está no escopo e no momento de criação. Atributos de classe são criados quando a classe é definida e existem uma única vez, independentemente do número de instâncias. Atributos de instância são criados quando o objeto é instanciado e cada objeto tem sua própria cópia.

Além disso, ao modificar um atributo de classe através de uma instância, você na verdade cria um novo atributo de instância que sombreia o da classe. Isso pode gerar confusão se não for compreendido.

class Exemplo:
    atributo_classe = 0  # atributo de classe

    def __init__(self, valor):
        self.atributo_instancia = valor  # atributo de instância

obj1 = Exemplo(10)
obj2 = Exemplo(20)
print(obj1.atributo_classe)  # 0
print(obj2.atributo_classe)  # 0
print(obj1.atributo_instancia)  # 10
print(obj2.atributo_instancia)  # 20

# Modificando via classe
Exemplo.atributo_classe = 100
print(obj1.atributo_classe)  # 100
print(obj2.atributo_classe)  # 100

# Modificando via instância - cria atributo de instância
obj1.atributo_classe = 50
print(obj1.atributo_classe)  # 50 (instância)
print(obj2.atributo_classe)  # 100 (classe)
print(Exemplo.atributo_classe)  # 100

Atributos de classe

Atributos de classe são úteis para valores que devem ser compartilhados entre todas as instâncias, como constantes, contadores ou configurações globais da classe. Eles podem ser acessados diretamente pelo nome da classe ou por uma instância (desde que não haja sombreamento).

Exemplo comum: um contador de instâncias criadas.

class Pessoa:
    total_pessoas = 0  # atributo de classe

    def __init__(self, nome):
        self.nome = nome
        Pessoa.total_pessoas += 1

p1 = Pessoa("Ana")
p2 = Pessoa("João")
print(Pessoa.total_pessoas)  # 2

Outro uso é para definir valores padrão que podem ser sobrescritos em instâncias específicas, mas sem afetar as demais.

Variáveis mutáveis compartilhadas (armadilha)

Um erro comum é usar listas ou dicionários como atributos de classe, pois eles são mutáveis e compartilhados. Se uma instância modificar esse atributo, todas as outras instâncias serão afetadas.

class Turma:
    alunos = []  # lista mutável compartilhada

    def __init__(self, nome):
        self.nome = nome

    def adicionar_aluno(self, aluno):
        self.alunos.append(aluno)  # modifica a lista da classe!

t1 = Turma("A")
t2 = Turma("B")
t1.adicionar_aluno("Carlos")
print(t2.alunos)  # ['Carlos'] - efeito colateral!

Para evitar isso, o atributo deve ser definido como de instância, geralmente no __init__:

class Turma:
    def __init__(self, nome):
        self.nome = nome
        self.alunos = []  # agora cada turma tem sua própria lista

    def adicionar_aluno(self, aluno):
        self.alunos.append(aluno)

t1 = Turma("A")
t2 = Turma("B")
t1.adicionar_aluno("Carlos")
print(t2.alunos)  # [] - correto

Essa armadilha é uma das causas mais frequentes de bugs em programas orientados a objetos em Python.

Exemplos

Vamos ver um exemplo completo que combina atributos de classe e instância, além de demonstrar o comportamento de mutáveis.

class ContaBancaria:
    taxa_juros = 0.02  # atributo de classe
    total_contas = 0

    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo
        ContaBancaria.total_contas += 1

    def depositar(self, valor):
        self.saldo += valor

    def aplicar_juros(self):
        self.saldo += self.saldo * ContaBancaria.taxa_juros

    @classmethod
    def alterar_taxa(cls, nova_taxa):
        cls.taxa_juros = nova_taxa

# Uso
c1 = ContaBancaria("Alice", 1000)
c2 = ContaBancaria("Bob", 500)
print(ContaBancaria.total_contas)  # 2
print(c1.taxa_juros)  # 0.02
c1.aplicar_juros()
print(c1.saldo)  # 1020.0

# Alterando taxa via classe
ContaBancaria.alterar_taxa(0.03)
print(c2.taxa_juros)  # 0.03

# Tentativa de alterar via instância (cria atributo de instância)
c1.taxa_juros = 0.05
print(c1.taxa_juros)  # 0.05 (instância)
print(c2.taxa_juros)  # 0.03 (classe)

Esse exemplo mostra como usar métodos de classe para modificar atributos de classe de forma controlada e como o sombreamento pode ocorrer.

Boas práticas

  • Use atributos de classe para valores constantes ou compartilhados que não precisam ser copiados para cada instância.
  • Evite atributos de classe mutáveis, a menos que você realmente queira o compartilhamento (raro).
  • Para valores padrão que podem ser sobrescritos, prefira defini-los no __init__.
  • Use métodos de classe (@classmethod) para modificar atributos de classe de forma explícita.

Referências

Exercícios

  1. Crie uma classe Carro com um atributo de classe rodas igual a 4 e um atributo de instância cor. Instancie dois carros com cores diferentes e verifique se o atributo rodas é compartilhado.

    ✓ Resposta:
    class Carro:
        rodas = 4
    
        def __init__(self, cor):
            self.cor = cor
    
    c1 = Carro("vermelho")
    c2 = Carro("azul")
    print(c1.rodas)  # 4
    print(c2.rodas)  # 4
    print(Carro.rodas)  # 4
  2. Explique o que acontece se você fizer c1.rodas = 6 no código acima. Qual será o valor de c2.rodas e de Carro.rodas?

    ✓ Resposta: Ao fazer c1.rodas = 6, você cria um novo atributo de instância rodas em c1, que sombreia o atributo de classe. c2.rodas ainda acessa o atributo de classe (4) e Carro.rodas continua sendo 4. Portanto, c2.rodas = 4 e Carro.rodas = 4.
  3. Crie uma classe Configuracao com um atributo de classe parametros que é um dicionário vazio. Instancie dois objetos e, em um deles, adicione uma chave ao dicionário. O que acontece com o outro objeto? Como corrigir?

    ✓ Resposta: Como dicionários são mutáveis, adicionar uma chave em um objeto modifica o dicionário compartilhado da classe, afetando todos os objetos. Para corrigir, defina self.parametros = {} no __init__, criando um dicionário próprio para cada instância.
  4. Implemente um contador de instâncias usando um atributo de classe. A classe Animal deve incrementar um contador a cada nova instância e ter um método de classe que retorne o total de animais criados.

    ✓ Resposta:
    class Animal:
        total = 0
    
        def __init__(self, nome):
            self.nome = nome
            Animal.total += 1
    
        @classmethod
        def total_animais(cls):
            return cls.total
    
    a1 = Animal("Cachorro")
    a2 = Animal("Gato")
    print(Animal.total_animais())  # 2
  5. Qual é a diferença entre self.atributo e Classe.atributo dentro de um método de instância? Dê um exemplo onde o uso de self pode levar a um sombreamento indesejado.

    ✓ Resposta: self.atributo refere-se primeiro ao atributo de instância; se não existir, ao de classe. Classe.atributo sempre acessa o atributo de classe. Sombreamento indesejado ocorre quando você atribui a self.atributo pensando em modificar o atributo de classe, mas na verdade cria um de instância. Exemplo:
    class Exemplo:
        valor = 10
        def alterar(self):
            self.valor = 20  # cria atributo de instância, não altera o de classe
    obj = Exemplo()
    obj.alterar()
    print(Exemplo.valor)  # 10
    print(obj.valor)      # 20