Atributos de instância e de classe

1. Introdução aos Atributos em Python

1.1. Definição de atributos e sua importância em Orientação a Objetos

Atributos são variáveis associadas a uma classe ou a uma instância de classe. Eles representam o estado de um objeto e são fundamentais na programação orientada a objetos, pois permitem encapsular dados e comportamentos dentro de estruturas coesas.

1.2. Diferença fundamental entre atributos de instância e de classe

A diferença essencial está no escopo e no ciclo de vida:
- Atributos de instância: pertencem a cada objeto individualmente. Cada instância tem sua própria cópia.
- Atributos de classe: pertencem à classe em si. São compartilhados por todas as instâncias.

1.3. Como o Python gerencia o escopo de atributos

Python mantém namespaces separados: o da classe (acessível via Classe.atributo) e o da instância (acessível via self.atributo). Quando você acessa um atributo em uma instância, Python primeiro procura no namespace da instância e, se não encontrar, busca no namespace da classe.

class Exemplo:
    atributo_classe = "compartilhado"

    def __init__(self, valor):
        self.atributo_instancia = valor

obj1 = Exemplo("A")
obj2 = Exemplo("B")

print(obj1.atributo_classe)      # "compartilhado"
print(obj1.atributo_instancia)   # "A"
print(obj2.atributo_instancia)   # "B"

2. Atributos de Instância

2.1. Declaração e inicialização dentro de __init__

Atributos de instância são tipicamente definidos no método __init__, que é o construtor da classe:

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

p = Pessoa("Alice", 30)
print(p.nome)   # Alice
print(p.idade)  # 30

2.2. Acesso e modificação dinâmica

Atributos de instância podem ser acessados e modificados dinamicamente:

p.nome = "Bob"
p.nova_propriedade = "valor"  # Adiciona atributo dinamicamente
print(p.nova_propriedade)     # "valor"

2.3. Atributos privados (convenção _ e name mangling com __)

Python não possui atributos privados de verdade, mas usa convenções:

class Conta:
    def __init__(self, saldo):
        self._saldo = saldo          # Convenção: "protegido"
        self.__senha = "1234"        # Name mangling: _Conta__senha

c = Conta(1000)
print(c._saldo)                      # 1000 (acessível, mas não recomendado)
# print(c.__senha)                   # AttributeError
print(c._Conta__senha)               # "1234" (name mangling)

3. Atributos de Classe

3.1. Declaração direta no corpo da classe

Atributos de classe são declarados diretamente no corpo da classe, fora de qualquer método:

class Animal:
    especie = "mamífero"  # Atributo de classe
    reino = "animalia"    # Atributo de classe

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

print(Animal.especie)  # "mamífero"

3.2. Comportamento compartilhado entre todas as instâncias

Todas as instâncias compartilham o mesmo atributo de classe:

gato = Animal("Felix")
cachorro = Animal("Rex")

print(gato.especie)     # "mamífero"
print(cachorro.especie) # "mamífero"

Animal.especie = "ave"
print(gato.especie)     # "ave" - mudou para todas

3.3. Modificação de atributos de classe via classe vs. via instância

Modificar via instância cria um atributo de instância que sombreia o de classe:

class Config:
    debug = False

config1 = Config()
config2 = Config()

config1.debug = True  # Cria atributo de instância
print(config1.debug)  # True
print(config2.debug)  # False (ainda usa o da classe)
print(Config.debug)   # False (não foi alterado)

4. Resolução de Atributos e Ordem de Busca (MRO)

4.1. Como o Python busca atributos: instância → classe → superclasse

A ordem de busca (Method Resolution Order - MRO) é: instância → classe → superclasse(s):

class A:
    x = "classe A"

class B(A):
    x = "classe B"

class C(B):
    pass

obj = C()
obj.x = "instância"
print(obj.x)  # "instância"

del obj.x
print(obj.x)  # "classe B" (busca na classe C, depois B)

del B.x
print(obj.x)  # "classe A" (busca na superclasse)

4.2. O papel do dicionário __dict__ da instância e da classe

Cada objeto e classe tem um dicionário __dict__ que armazena seus atributos:

class Demo:
    class_attr = 42

    def __init__(self):
        self.instance_attr = 10

d = Demo()
print(d.__dict__)           # {'instance_attr': 10}
print(Demo.__dict__)        # {'class_attr': 42, ...}

4.3. Exemplo prático de sombreamento (shadowing)

class Empresa:
    taxa_juros = 0.05

empresa1 = Empresa()
empresa2 = Empresa()

empresa1.taxa_juros = 0.07  # Sombramento
print(empresa1.taxa_juros)  # 0.07 (instância)
print(empresa2.taxa_juros)  # 0.05 (classe)
print(Empresa.taxa_juros)   # 0.05 (classe)

5. Mutabilidade e Cuidados com Atributos de Classe

5.1. Atributos de classe imutáveis vs. mutáveis

Atributos imutáveis (int, str, tuple) são seguros; mutáveis (list, dict, set) requerem cuidado:

class Turma:
    alunos = []  # Mutável - compartilhado!
    nome_escola = "Escola XYZ"  # Imutável - seguro

t1 = Turma()
t2 = Turma()

t1.alunos.append("João")
print(t2.alunos)  # ["João"] - compartilhou!

5.2. O perigo de modificar objetos mutáveis compartilhados

O exemplo acima mostra que modificar uma lista compartilhada afeta todas as instâncias. Isso é um bug comum.

5.3. Boas práticas: quando usar atributos de classe e quando evitar

  • Use atributos de classe para: constantes, configurações globais, contadores compartilhados
  • Evite atributos de classe para: listas, dicionários ou objetos mutáveis que podem ser modificados por instâncias
  • Prefira atributos de instância para dados que variam entre objetos

6. Métodos de Classe e @classmethod para Manipular Atributos de Classe

6.1. Acessando e alterando atributos de classe dentro de métodos de classe

class Contador:
    total_instancias = 0

    def __init__(self):
        Contador.total_instancias += 1

    @classmethod
    def resetar_contador(cls):
        cls.total_instancias = 0

    @classmethod
    def exibir_total(cls):
        return f"Total de instâncias: {cls.total_instancias}"

c1 = Contador()
c2 = Contador()
print(Contador.exibir_total())  # Total de instâncias: 2
Contador.resetar_contador()
print(Contador.exibir_total())  # Total de instâncias: 0

6.2. Exemplo: contador de instâncias usando atributo de classe

class Pedido:
    proximo_id = 1

    def __init__(self, descricao):
        self.id = Pedido.proximo_id
        self.descricao = descricao
        Pedido.proximo_id += 1

    @classmethod
    def ultimo_id_gerado(cls):
        return cls.proximo_id - 1

pedido1 = Pedido("Camiseta")
pedido2 = Pedido("Caneca")
print(pedido1.id)           # 1
print(pedido2.id)           # 2
print(Pedido.ultimo_id_gerado())  # 2

6.3. Diferença entre cls.atributo e self.atributo

class Exemplo:
    valor = 10

    def metodo_instancia(self):
        print(self.valor)  # Busca: instância → classe

    @classmethod
    def metodo_classe(cls):
        print(cls.valor)   # Busca diretamente na classe

e = Exemplo()
e.valor = 20
e.metodo_instancia()  # 20 (instância sombreou)
e.metodo_classe()     # 10 (classe)

7. Atributos Dinâmicos e __slots__

7.1. Adicionando atributos de instância fora da classe

class Dinamico:
    pass

obj = Dinamico()
obj.novo_atributo = "criado dinamicamente"
print(obj.novo_atributo)  # "criado dinamicamente"

7.2. Restringindo atributos com __slots__ para economia de memória

class Otimizado:
    __slots__ = ['nome', 'idade']

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

o = Otimizado("Maria", 25)
# o.email = "maria@email.com"  # AttributeError!

7.3. Limitações e impacto de __slots__ na herança

class Base:
    __slots__ = ['x']

class Derivada(Base):
    __slots__ = ['y']  # Precisa declarar slots novamente

    def __init__(self):
        self.x = 10
        self.y = 20

d = Derivada()
# d.z = 30  # AttributeError (a menos que Derivada declare 'z' em __slots__)

8. Casos Práticos e Boas Práticas

8.1. Exemplo completo: sistema de banco com saldo mínimo como atributo de classe

class ContaBancaria:
    taxa_juros = 0.02
    saldo_minimo = 100.00

    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self._saldo = max(saldo_inicial, ContaBancaria.saldo_minimo)
        self._transacoes = []

    def depositar(self, valor):
        self._saldo += valor
        self._transacoes.append(f"Depósito: R${valor:.2f}")

    def sacar(self, valor):
        if self._saldo - valor >= ContaBancaria.saldo_minimo:
            self._saldo -= valor
            self._transacoes.append(f"Saque: R${valor:.2f}")
        else:
            print("Saldo insuficiente para manter saldo mínimo")

    @classmethod
    def alterar_saldo_minimo(cls, novo_minimo):
        if novo_minimo >= 0:
            cls.saldo_minimo = novo_minimo
            print(f"Novo saldo mínimo: R${novo_minimo:.2f}")
        else:
            print("Saldo mínimo não pode ser negativo")

    @property
    def saldo(self):
        return self._saldo

    def __str__(self):
        return f"Conta de {self.titular} - Saldo: R${self._saldo:.2f}"

conta1 = ContaBancaria("Ana", 500)
conta2 = ContaBancaria("Carlos", 200)

conta1.depositar(300)
conta1.sacar(50)
print(conta1)  # Conta de Ana - Saldo: R$750.00

ContaBancaria.alterar_saldo_minimo(50)
print(f"Saldo mínimo atual: R${ContaBancaria.saldo_minimo:.2f}")

8.2. Quando optar por atributo de classe vs. atributo de instância

  • Atributo de classe: valores constantes, configurações globais, contadores, dados compartilhados (imutáveis)
  • Atributo de instância: dados específicos de cada objeto, estados mutáveis, informações únicas

8.3. Debugging: inspecionando atributos com vars(), dir() e getattr()

class Produto:
    imposto = 0.1

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

p = Produto("Notebook", 3500)

print(vars(p))           # {'nome': 'Notebook', 'preco': 3500}
print(dir(p))            # Lista todos os atributos e métodos
print(getattr(p, 'nome'))  # "Notebook"
print(getattr(p, 'preco', 0))  # 3500
print(getattr(p, 'desconto', 'Não definido'))  # "Não definido"

Referências