Construtores e destrutores: __init__ e __del__

1. Introdução aos métodos especiais em Python

Em Python, métodos especiais — também conhecidos como métodos "dunder" (abreviação de double underscore) — são identificados por nomes que começam e terminam com dois underscores, como __init__, __del__, __str__, __repr__, entre outros. Esses métodos permitem que objetos definidos pelo usuário se comportem de maneira semelhante aos tipos nativos da linguagem, integrando-se a operações como criação, destruição, representação textual, iteração e muito mais.

O ciclo de vida de um objeto em Python pode ser dividido em três fases principais:
1. Criação — alocação de memória e inicialização dos atributos.
2. Uso — chamadas a métodos e acesso a atributos durante a execução.
3. Destruição — liberação de recursos e desalocação da memória.

Os métodos __init__ e __del__ atuam como hooks nesse ciclo, permitindo que o programador defina comportamentos personalizados no momento da inicialização e da finalização de um objeto.

2. O construtor __init__: inicializando objetos

O método __init__ é o inicializador padrão de uma classe em Python. Sua sintaxe básica é:

class MinhaClasse:
    def __init__(self, parametro1, parametro2):
        self.atributo1 = parametro1
        self.atributo2 = parametro2

É importante destacar que, tecnicamente, __init__ não é um construtor, mas sim um inicializador. O verdadeiro construtor em Python é __new__, responsável por alocar a memória para o objeto. O __init__ é chamado automaticamente após __new__ e serve para configurar os atributos da instância recém-criada.

Parâmetros personalizados podem ser passados diretamente ao criar o objeto:

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

p1 = Pessoa("Alice")
p2 = Pessoa("Bob", 25)
print(p1.nome, p1.idade)  # Alice 30
print(p2.nome, p2.idade)  # Bob 25

3. Boas práticas com __init__

Ao implementar __init__, algumas boas práticas devem ser seguidas:

  • Inicialize todos os atributos de instância no __init__, mesmo que opcionais, para evitar erros de atributo não definido.
class Carro:
    def __init__(self, modelo, ano=None):
        self.modelo = modelo
        self.ano = ano if ano is not None else 2024
        self.km_rodados = 0  # valor padrão
  • Valide argumentos no momento da criação para garantir integridade dos dados.
class ContaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        if saldo_inicial < 0:
            raise ValueError("Saldo inicial não pode ser negativo")
        self.titular = titular
        self.saldo = saldo_inicial
  • Evite mutáveis como valores padrão. Essa é uma armadilha clássica em Python:
class ErroComum:
    def __init__(self, itens=[]):  # PROBLEMA: lista mutável compartilhada
        self.itens = itens

# Correto:
class Correto:
    def __init__(self, itens=None):
        self.itens = itens if itens is not None else []

4. Herança e o construtor __init__

Em hierarquias de classes, é comum que a classe filha precise estender ou modificar a inicialização da classe pai. Para isso, utiliza-se super().__init__():

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

class Cachorro(Animal):
    def __init__(self, nome, raca):
        super().__init__(nome)  # chama __init__ da classe pai
        self.raca = raca

rex = Cachorro("Rex", "Labrador")
print(rex.nome, rex.raca)  # Rex Labrador

A ordem de resolução de métodos (MRO — Method Resolution Order) garante que super() encontre o método correto em cenários de herança múltipla. O MRO pode ser consultado com Classe.__mro__.

5. O destrutor __del__: finalizando objetos

O método __del__ é chamado quando o objeto está prestes a ser destruído pelo garbage collector. Sua sintaxe é simples:

class Recurso:
    def __del__(self):
        print(f"Recurso {self.id} sendo destruído")

No entanto, é fundamental entender que não há garantia de que __del__ será chamado. Isso pode ocorrer em situações como:
- O programa termina abruptamente (exceção não tratada no topo).
- Existem referências circulares que o garbage collector padrão não consegue resolver imediatamente.
- O interpretador é encerrado sem executar a coleta de lixo.

Além disso, __del__ não deve ser utilizado como substituto para gerenciamento explícito de recursos.

6. Casos de uso práticos para __del__

Apesar das limitações, __del__ pode ser útil para liberar recursos externos, como arquivos abertos ou conexões de rede:

class ConexaoBanco:
    def __init__(self, host, usuario, senha):
        self.conexao = conectar_ao_banco(host, usuario, senha)
        print("Conexão estabelecida")

    def __del__(self):
        if self.conexao:
            self.conexao.fechar()
            print("Conexão fechada")

Porém, a abordagem moderna e mais segura em Python é utilizar gerenciadores de contexto com with e os métodos __enter__/__exit__:

class ConexaoSegura:
    def __enter__(self):
        self.conexao = conectar_ao_banco()
        return self.conexao

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conexao.fechar()

with ConexaoSegura() as conn:
    conn.executar_query("SELECT * FROM tabela")

Gerenciadores de contexto garantem que os recursos sejam liberados mesmo em caso de exceções.

7. Armadilhas e cuidados com __del__

Alguns cuidados importantes ao usar __del__:

  • Dependência de ordem de destruição: objetos podem ser destruídos em qualquer ordem, e um objeto pode tentar acessar outro que já foi finalizado.
  • Referências circulares: dois objetos que se referenciam mutuamente podem não ser coletados imediatamente, atrasando a chamada de __del__.
  • Lógica complexa: evite colocar operações críticas dentro de __del__, pois exceções lançadas ali são ignoradas pelo interpretador.
import gc

class A:
    def __init__(self, b):
        self.b = b

class B:
    def __init__(self, a):
        self.a = a

a = A(None)
b = B(a)
a.b = b

# Referência circular: a e b não serão coletados imediatamente
del a
del b
gc.collect()  # força a coleta

8. Conclusão e comparação com outras linguagens

Em resumo:
- __init__ é o inicializador padrão e deve ser usado para configurar atributos de instância. É seguro, previsível e amplamente utilizado.
- __del__ é um finalizador opcional com comportamento não determinístico. Deve ser evitado para gerenciamento de recursos, em favor de gerenciadores de contexto.

Comparado a linguagens como C++ ou Java:
- Em C++, destrutores são chamados de forma determinística quando o objeto sai de escopo (em objetos alocados na pilha).
- Em Java, o método finalize() (análogo a __del__) também é não determinístico e desaconselhado desde o Java 9.
- Python adota uma filosofia mais flexível, mas menos previsível para finalização, incentivando o uso de padrões mais seguros como with.

O estudo dos métodos especiais __init__ e __del__ abre portas para uma compreensão mais profunda do modelo de objetos em Python e de como a linguagem gerencia memória e recursos.

Referências