Python Admin

Como Usar Metaclasses em Python: type, __new__ e Controle de Criação de Classes em Produção Já leu

O que são Metaclasses? Uma metaclasse é uma classe cujas instâncias são classes. Essa definição pode parecer abstrata à primeira vista, mas é fundamental entender que em Python, tudo é um objeto — inclusive as classes. Quando você define uma classe usando a palavra-chave , você está, na verdade, criando uma instância de uma metaclasse. Por padrão, essa metaclasse é . Para compreender melhor, considere que se um objeto é uma instância de uma classe, então uma classe é uma instância de uma metaclasse. Isso significa que você pode controlar como uma classe é criada, quais atributos ela tem e como ela se comporta, usando metaclasses. É como ter um "construtor de construtores" — você controla a criação de classes da mesma forma que construtores controlam a criação de objetos. Entendendo a Função type() type() como Inspecionador A função tem dois comportamentos distintos. Quando chamada com um argumento, ela retorna a metaclasse (o tipo) do objeto passado. Quando chamada com

O que são Metaclasses?

Uma metaclasse é uma classe cujas instâncias são classes. Essa definição pode parecer abstrata à primeira vista, mas é fundamental entender que em Python, tudo é um objeto — inclusive as classes. Quando você define uma classe usando a palavra-chave class, você está, na verdade, criando uma instância de uma metaclasse. Por padrão, essa metaclasse é type.

Para compreender melhor, considere que se um objeto é uma instância de uma classe, então uma classe é uma instância de uma metaclasse. Isso significa que você pode controlar como uma classe é criada, quais atributos ela tem e como ela se comporta, usando metaclasses. É como ter um "construtor de construtores" — você controla a criação de classes da mesma forma que construtores controlam a criação de objetos.

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

# Verificar a metaclasse de Pessoa
print(type(Pessoa))  # <class 'type'>
print(isinstance(Pessoa, type))  # True

# Pessoa é uma instância de type
joao = Pessoa("João")
print(type(joao))  # <class '__main__.Pessoa'>

Entendendo a Função type()

type() como Inspecionador

A função type() tem dois comportamentos distintos. Quando chamada com um argumento, ela retorna a metaclasse (o tipo) do objeto passado. Quando chamada com três argumentos, ela cria uma classe dinamicamente. Começaremos pelo primeiro uso, que é mais simples e didático.

class Animal:
    pass

# type() inspecionando um objeto
numero = 42
print(type(numero))  # <class 'int'>

lista = [1, 2, 3]
print(type(lista))  # <class 'list'>

# type() inspecionando uma classe
print(type(Animal))  # <class 'type'>
print(type(int))  # <class 'type'>
print(type(str))  # <class 'type'>

Observe que todas as classes têm type como sua metaclasse. Isso é o padrão em Python. Mas aqui está o ponto crucial: você pode criar suas próprias metaclasses que herdam de type para personalizar o comportamento da criação de classes.

type() como Construtor de Classes

O segundo comportamento de type() permite criar classes programaticamente. A sintaxe é type(nome, bases, dicionário), onde nome é uma string com o nome da classe, bases é uma tupla com as classes pai, e dicionário é um dicionário com os atributos e métodos da classe.

# Criar uma classe dinamicamente usando type()
Veiculo = type('Veiculo', (), {
    'velocidade_maxima': 200,
    'acelerar': lambda self: print("Acelerando!")
})

# Usar a classe criada
carro = Veiculo()
print(carro.velocidade_maxima)  # 200
carro.acelerar()  # Acelerando!

# Criar uma classe com herança
class Motor:
    def ligar(self):
        return "Motor ligado"

Carro = type('Carro', (Motor,), {
    'marca': 'Toyota',
    'modelo': 'Corolla',
    'velocidade_maxima': 220
})

meu_carro = Carro()
print(meu_carro.ligar())  # Motor ligado
print(meu_carro.marca)  # Toyota

Criando Metaclasses Personalizadas

O Método new em Metaclasses

Quando você cria uma metaclasse personalizada, herda de type e sobrescreve o método __new__. Este método é responsável pela criação da classe. A diferença entre __new__ e __init__ é que __new__ cria o objeto (neste caso, a classe), enquanto __init__ o inicializa após a criação. Em metaclasses, você geralmente trabalha com __new__ porque quer controlar a estrutura da classe antes dela existir completamente.

class MinhaMetaclasse(type):
    def __new__(cls, nome, bases, dicionario):
        print(f"Criando classe: {nome}")
        # Aqui você pode modificar o dicionário antes da classe ser criada
        dicionario['classe_criada_em'] = 'MinhaMetaclasse'

        # Chamar o __new__ da metaclasse pai (type)
        nova_classe = super().__new__(cls, nome, bases, dicionario)
        return nova_classe

# Usar a metaclasse personalizada
class Produto(metaclass=MinhaMetaclasse):
    def __init__(self, nome):
        self.nome = nome

# Ao executar, verá: "Criando classe: Produto"
print(Produto.classe_criada_em)  # MinhaMetaclasse

produto = Produto("Notebook")
print(produto.nome)  # Notebook

Controlando Atributos e Métodos

Uma aplicação prática de metaclasses é validar e controlar quais atributos e métodos uma classe pode ter. Por exemplo, você pode forçar que todos os métodos de uma classe tenham docstrings, ou que todos os atributos privados sigam uma convenção específica.

class ValidadorDocstring(type):
    def __new__(cls, nome, bases, dicionario):
        # Verificar se todos os métodos têm docstring
        for chave, valor in dicionario.items():
            if callable(valor) and not chave.startswith('_'):
                if not valor.__doc__:
                    raise TypeError(f"Método '{chave}' deve ter docstring")

        return super().__new__(cls, nome, bases, dicionario)

# Esta classe será criada com sucesso
class APIBemDocumentada(metaclass=ValidadorDocstring):
    def get_usuarios(self):
        """Retorna lista de usuários"""
        return []

    def criar_usuario(self, nome):
        """Cria um novo usuário"""
        return {'nome': nome}

# Esta classe vai gerar erro
try:
    class APIMalDocumentada(metaclass=ValidadorDocstring):
        def get_usuarios(self):
            return []  # Sem docstring!
except TypeError as e:
    print(f"Erro: {e}")  # Erro: Método 'get_usuarios' deve ter docstring

Transformando Métodos Automaticamente

Outro caso de uso real é transformar ou decorar todos os métodos de uma classe automaticamente. Por exemplo, adicionar logging a todos os métodos ou validação de argumentos.

import functools
import time

class LogMetodos(type):
    def __new__(cls, nome, bases, dicionario):
        for chave, valor in dicionario.items():
            if callable(valor) and not chave.startswith('_'):
                # Decorar cada método com logging
                dicionario[chave] = cls._adicionar_logging(valor)

        return super().__new__(cls, nome, bases, dicionario)

    @staticmethod
    def _adicionar_logging(funcao):
        @functools.wraps(funcao)
        def wrapper(*args, **kwargs):
            inicio = time.time()
            print(f"[LOG] Chamando {funcao.__name__}")
            resultado = funcao(*args, **kwargs)
            tempo = time.time() - inicio
            print(f"[LOG] {funcao.__name__} levou {tempo:.4f}s")
            return resultado
        return wrapper

class CalculadoraComLog(metaclass=LogMetodos):
    def somar(self, a, b):
        time.sleep(0.1)
        return a + b

    def multiplicar(self, a, b):
        time.sleep(0.05)
        return a * b

calc = CalculadoraComLog()
print(calc.somar(5, 3))
print(calc.multiplicar(4, 2))

# Saída:
# [LOG] Chamando somar
# [LOG] somar levou 0.1002s
# 8
# [LOG] Chamando multiplicar
# [LOG] multiplicar levou 0.0503s
# 8

Controle Avançado com init e call

Inicialização de Metaclasses com init

Além de __new__, você pode sobrescrever __init__ em uma metaclasse para fazer processamento após a classe ser criada. Enquanto __new__ controla a criação da classe, __init__ pode fazer validações ou configurações adicionais.

class ConfiguradorClasse(type):
    def __new__(cls, nome, bases, dicionario):
        dicionario['versao'] = '1.0'
        return super().__new__(cls, nome, bases, dicionario)

    def __init__(cls, nome, bases, dicionario):
        super().__init__(nome, bases, dicionario)
        # Registrar a classe criada
        print(f"Classe {nome} foi inicializada com versão {cls.versao}")

class MinhaClasse(metaclass=ConfiguradorClasse):
    pass

# Saída: Classe MinhaClasse foi inicializada com versão 1.0

Controlando a Instanciação com call

O método __call__ de uma metaclasse controla o que acontece quando você tenta criar uma instância da classe. Isso permite implementar padrões como Singleton ou Factory Methods no nível de metaclasse.

class Singleton(type):
    _instancias = {}

    def __call__(cls, *args, **kwargs):
        # Se a classe já foi instanciada, retornar a instância existente
        if cls not in cls._instancias:
            cls._instancias[cls] = super().__call__(*args, **kwargs)
        return cls._instancias[cls]

class ConexaoDB(metaclass=Singleton):
    def __init__(self):
        self.id = id(self)
        print(f"Conexão criada com ID: {self.id}")

# Criar duas "instâncias"
conn1 = ConexaoDB()
print(f"conn1 ID: {conn1.id}")

conn2 = ConexaoDB()
print(f"conn2 ID: {conn2.id}")

print(f"Mesma instância? {conn1 is conn2}")  # True

# Saída:
# Conexão criada com ID: 140234567890
# conn1 ID: 140234567890
# conn2 ID: 140234567890
# Mesma instância? True

Outro exemplo prático é validar os argumentos passados ao criar uma instância:

class ValidadorArgumentos(type):
    def __call__(cls, *args, **kwargs):
        # Validar antes de criar a instância
        if hasattr(cls, 'validar_argumentos'):
            cls.validar_argumentos(*args, **kwargs)

        # Chamar o __new__ e __init__ normalmente
        return super().__call__(*args, **kwargs)

class Usuario(metaclass=ValidadorArgumentos):
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    @staticmethod
    def validar_argumentos(nome, idade):
        if not isinstance(nome, str) or not nome:
            raise ValueError("Nome deve ser uma string não vazia")
        if not isinstance(idade, int) or idade < 0:
            raise ValueError("Idade deve ser um inteiro positivo")

# Funciona
usuario = Usuario("Alice", 30)
print(f"{usuario.nome} tem {usuario.idade} anos")

# Gera erro
try:
    usuario_invalido = Usuario("Bob", -5)
except ValueError as e:
    print(f"Erro: {e}")  # Erro: Idade deve ser um inteiro positivo

Casos de Uso Reais e Boas Práticas

Implementando um ORM Simplificado

Um caso real de metaclasses é na criação de ORMs (Object-Relational Mapping). Frameworks como Django usam metaclasses para transformar definições de classes em modelos de banco de dados.

class CampoDB:
    def __init__(self, tipo):
        self.tipo = tipo

    def __repr__(self):
        return f"CampoDB({self.tipo})"

class Modelo(type):
    def __new__(cls, nome, bases, dicionario):
        campos = {}

        # Encontrar todos os campos definidos
        for chave, valor in list(dicionario.items()):
            if isinstance(valor, CampoDB):
                campos[chave] = valor

        # Armazenar os campos na classe
        dicionario['_campos'] = campos

        # Remover os objetos CampoDB do dicionário
        for chave in campos:
            del dicionario[chave]

        return super().__new__(cls, nome, bases, dicionario)

class Usuario(metaclass=Modelo):
    nome = CampoDB('string')
    idade = CampoDB('inteiro')
    email = CampoDB('string')

print(Usuario._campos)
# {'nome': CampoDB(string), 'idade': CampoDB(inteiro), 'email': CampoDB(string)}

# Você poderia então usar isso para gerar SQL ou validar dados
for campo, tipo in Usuario._campos.items():
    print(f"CREATE COLUMN {campo} {tipo.tipo}")

Quando NÃO Usar Metaclasses

Uma consideração importante: metaclasses são poderosas, mas complexas. Use-as apenas quando realmente necessário. Alternativas como decoradores de classe e herança normal resolvem a maioria dos problemas de forma mais clara e manutenível.

# ❌ Evitar metaclasses complexas quando um decorador resolve:
def adicionar_logging(cls):
    """Decorador é mais legível que uma metaclasse"""
    for atributo in dir(cls):
        if callable(getattr(cls, atributo)) and not atributo.startswith('_'):
            setattr(cls, atributo, _log_wrapper(getattr(cls, atributo)))
    return cls

def _log_wrapper(func):
    def wrapper(*args, **kwargs):
        print(f"Chamando {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@adicionar_logging
class MinhaClasse:
    def metodo(self):
        return "resultado"

Conclusão

Três pontos fundamentais foram abordados neste artigo. Primeiro, compreender que metaclasses são classes que criam classes — um conceito diferente de herança normal — é essencial para dominar a programação avançada em Python. Segundo, o método __new__ em metaclasses oferece controle fino sobre como as classes são estruturadas antes de sua existência completa, permitindo validações, transformações e adição de atributos automaticamente. Terceiro, embora metaclasses sejam ferramentas poderosas usadas em frameworks profissionais, devem ser usadas com parcimônia — decoradores e composição frequentemente oferecem soluções mais legíveis e manuteníveis.

Dominar metaclasses o preparará para estudar frameworks como Django, compreender internamente como bibliotecas funcionam e, mais importante, saber quando aplicar esses conceitos avançados para resolver problemas reais de forma elegante.

Referências


Artigos relacionados