Em Python, atributos são variáveis associadas a objetos. No contexto de programação orientada a objetos, existem dois tipos principais: atributos de classe e atributos de instância. Compreender a diferença entre eles é fundamental para escrever código correto e eficiente, especialmente quando lidamos com dados compartilhados ou específicos de cada objeto.

Atributos de classe pertencem à classe em si, sendo compartilhados por todas as instâncias. Já os atributos de instância são exclusivos de cada objeto. Nesta aula, veremos como definir cada um, como acessá-los e uma armadilha comum envolvendo variáveis mutáveis.

Diferença

A principal diferença está no escopo e no momento de criação. Atributos de classe são definidos diretamente no corpo da classe, fora de qualquer método. Eles são criados quando a classe é definida e existem uma única vez, independentemente do número de instâncias. Atributos de instância, por outro lado, são normalmente definidos dentro do método __init__ usando self, e são criados a cada nova instância.

Para acessar um atributo de classe, podemos usar o nome da classe (ex.: MinhaClasse.atributo) ou uma instância (ex.: obj.atributo). No entanto, ao modificar um atributo via self, Python cria um atributo de instância que sombreia o de classe. Já os atributos de instância só podem ser acessados através de uma instância.

Atributos de classe

Atributos de classe são úteis para armazenar dados que são comuns a todas as instâncias, como constantes, contadores ou configurações padrão. Por exemplo, uma classe Cachorro pode ter um atributo de classe especie = 'Canis familiaris' que é o mesmo para todos os cães.

Exemplo:

class Cachorro:
    especie = 'Canis familiaris'  # atributo de classe

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

c1 = Cachorro('Rex')
c2 = Cachorro('Bolt')

print(c1.especie)  # 'Canis familiaris'
print(c2.especie)  # 'Canis familiaris'
print(Cachorro.especie)  # 'Canis familiaris'

Note que o atributo especie é o mesmo objeto para todas as instâncias. Se modificarmos Cachorro.especie, a mudança reflete em todas as instâncias. Porém, se tentarmos modificar via c1.especie = 'Outra', criamos um novo atributo de instância em c1, sem afetar a classe ou outras instâncias.

Variáveis mutáveis compartilhadas (armadilha)

Uma armadilha comum é usar atributos de classe para valores mutáveis, como listas ou dicionários. Como o atributo é compartilhado, qualquer modificação feita por uma instância afeta todas as outras. Isso geralmente não é o desejado.

Exemplo problemático:

class Turma:
    alunos = []  # atributo de classe mutável

    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('João')
print(t1.alunos)  # ['João']
print(t2.alunos)  # ['João'] - efeito colateral!
print(Turma.alunos)  # ['João']

Para evitar isso, o atributo deve ser de instância, definido dentro de __init__:

class Turma:
    def __init__(self, nome):
        self.nome = nome
        self.alunos = []  # 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('João')
print(t1.alunos)  # ['João']
print(t2.alunos)  # []

Exemplos

Vamos ver mais exemplos práticos para consolidar os conceitos.

Exemplo 1: Contador de instâncias usando atributo de classe.

class Pessoa:
    contador = 0

    def __init__(self, nome):
        self.nome = nome
        Pessoa.contador += 1  # incrementa o contador da classe

p1 = Pessoa('Ana')
p2 = Pessoa('Carlos')
print(Pessoa.contador)  # 2

Exemplo 2: Constantes compartilhadas.

class Circulo:
    PI = 3.14159

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

    def area(self):
        return Circulo.PI * self.raio ** 2

c = Circulo(5)
print(c.area())  # 78.53975

Exemplo 3: Cuidado com mutáveis – lista de tarefas compartilhada indevidamente.

class ListaTarefas:
    tarefas = []  # errado!

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

    def adicionar(self, tarefa):
        self.tarefas.append(tarefa)

l1 = ListaTarefas('Casa')
l2 = ListaTarefas('Trabalho')
l1.adicionar('Limpar')
print(l2.tarefas)  # ['Limpar'] - indesejado

Correção:

class ListaTarefas:
    def __init__(self, nome):
        self.nome = nome
        self.tarefas = []

    def adicionar(self, tarefa):
        self.tarefas.append(tarefa)

Boas práticas

Use atributos de classe para dados que são verdadeiramente compartilhados e imutáveis (números, strings, tuplas) ou para implementar comportamentos como contadores. Evite atributos de classe mutáveis, pois podem causar bugs sutis. Prefira sempre inicializar atributos mutáveis em __init__ como atributos de instância.

Referências

Exercícios

  1. Crie uma classe Carro com um atributo de classe rodas = 4 e um atributo de instância cor. Instancie dois carros com cores diferentes e imprima o número de rodas para cada um.

    ✓ 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
  2. Na classe Carro do exercício 1, tente modificar o atributo rodas de uma instância para 3. Verifique se o atributo de classe foi alterado. Explique o resultado.

    ✓ Resposta:
    c1.rodas = 3
    print(c1.rodas)  # 3
    print(c2.rodas)  # 4 (atributo de classe permanece)
    print(Carro.rodas)  # 4
    # Isso ocorre porque a atribuição cria um novo atributo de instância, que sombreia o de classe para c1.
  3. Crie uma classe ContaBancaria com um atributo de classe taxa_juros (float) e um atributo de instância saldo. Adicione um método aplicar_juros que aumenta o saldo com base na taxa de juros da classe. Teste com duas contas.

    ✓ Resposta:
    class ContaBancaria:
        taxa_juros = 0.05
    
        def __init__(self, saldo):
            self.saldo = saldo
    
        def aplicar_juros(self):
            self.saldo += self.saldo * ContaBancaria.taxa_juros
    
    c1 = ContaBancaria(1000)
    c2 = ContaBancaria(500)
    c1.aplicar_juros()
    c2.aplicar_juros()
    print(c1.saldo)  # 1050.0
    print(c2.saldo)  # 525.0
  4. Explique o que acontece no código abaixo e corrija-o para que cada Pedido tenha sua própria lista de itens.

    class Pedido:
        itens = []
    
        def __init__(self, id):
            self.id = id
    
        def adicionar_item(self, item):
            self.itens.append(item)
    
    p1 = Pedido(1)
    p2 = Pedido(2)
    p1.adicionar_item('camisa')
    p2.adicionar_item('calça')
    print(p1.itens)
    print(p2.itens)

    ✓ Resposta:O código imprime ['camisa', 'calça'] para ambos, pois itens é um atributo de classe compartilhado. A correção é mover itens para o __init__:
    class Pedido:
        def __init__(self, id):
            self.id = id
            self.itens = []
    
        def adicionar_item(self, item):
            self.itens.append(item)
  5. Crie uma classe Config que armazene configurações globais como atributos de classe (ex.: versao = '1.0', debug = False). Adicione um método de classe mostrar_config que imprime todas as configurações.

    ✓ Resposta:
    class Config:
        versao = '1.0'
        debug = False
    
        @classmethod
        def mostrar_config(cls):
            print(f'Versão: {cls.versao}, Debug: {cls.debug}')
    
    Config.mostrar_config()  # Versão: 1.0, Debug: False