Introspecção e Métodos Especiais em Python para Programação Orientada a Objetos

Python oferece uma rica coleção de ferramentas e métodos especiais para interagir com objetos e classes de forma dinâmica. Esta capacidade, frequentemente referida como introspecção ou reflexão, permite que os programas inspecionem e modifiquem suas próprias estruturas e comportamentos em tempo de execução. Exploraremos algumas dessas funcionalidades, incluindo funções integradas e métodos mágicos, que são essenciais para uma programação orientada a objetos (POO) avançada e flexível.

vars()

A função vars() é uma ferramenta de introspecção que, de certa forma, se assemelha a dir(), mas com uma distinção importante. Ela requer que o objeto fornecido possua um atributo __dict__.

  1. vars() retorna um dicionário contendo os atributos (chaves) e seus respectivos valores armazenados no __dict__ do objeto.
  2. Se o objeto fornecido não tiver um atributo __dict__, uma exceção TypeError será levantada.
  3. Quando vars() é chamada sem argumentos, ela retorna um dicionário com os atributos e valores do namespace local, funcionando de maneira similar a locals().

Veja um exemplo de uso com uma instância de classe:


class Dispositivo:
    def __init__(self, tipo_dispositivo):
        self.tipo = tipo_dispositivo

meu_dispositivo = Dispositivo("smartphone")
meu_dispositivo.fabricante = "Acme Corp"
meu_dispositivo.modelo = "XYZ-2000"

print(meu_dispositivo.__dict__)
print(vars(meu_dispositivo))

Saída:


{'tipo': 'smartphone', 'fabricante': 'Acme Corp', 'modelo': 'XYZ-2000'}
{'tipo': 'smartphone', 'fabricante': 'Acme Corp', 'modelo': 'XYZ-2000'}

dir()

A função dir() é uma utilidade poderosa para listar os atributos e métodos de um objeto. Sua funcionalidade varia ligeiramente dependendo do tipo de argumetno fornecido:

  1. Quando aplicada a um módulo, dir() exibe todos os atributos do módulo.
  2. Em uma instância de classe (seja ela clássica ou moderna), dir() lista as variáveis da instância, bem como os métodos e atributos de classe definidos na própria classe e em todas as suas classes base.
  3. Se usada em uma classe, dir() mostra o conteúdo de __dict__ da classe e de todas as suas classes base. Contudo, atributos de classe definidos em metaclasses não são exibidos.
  4. Chamada sem argumentos, dir() retorna uma lista dos nomes no escopo local do chamador.

Para objetos que sobrescrevem os atributos __dict__ ou __class__, dir() usa esses atributos. Para compatibilidade com versões anteriores, se __members__ e __methods__ forem definidos, eles também são considerados.

super()

A função super() é uma ferramenta essencial na programação orientada a objetos em Python, principalmente no contexto de herança. Seu objetivo principal é permitir a chamada de métodos de uma classe ancestral (ou "superclasse") em uma hierarquia de herança.

É importante notar que super() não se refere exclusivamente à classe pai direta, mas sim à próxima classe na Method Resolution Order (MRO), que é a sequência pela qual Python pesquisa métodos em uma hierarquia de herança. Isso é crucial para resolver problemas que surgem com herança múltipla, como a ordem de chamada de métodos e a evitação de chamadas duplicadas (o "problema do diamante").

Sintaxe básica de super():


super(tipo[, objeto_ou_tipo])

Uma diferença notável entre Python 3.x e Python 2.x é a simplificação da sintaxe. Em Python 3, é possível usar super().metodo() em vez de super(ClasseAtual, self).metodo():

Exemplo em Python 3.x:


class Entidade:
    def exibir_id(self, id_val):
        print(f"ID da Entidade: {id_val}")

class Usuario(Entidade):
    def exibir_id(self, id_val):
        print("Início da exibição do usuário.")
        super().exibir_id(id_val) # Chama o método exibir_id da próxima classe na MRO
        print("Fim da exibição do usuário.")

user = Usuario()
user.exibir_id(123)

Exemplo em Python 2.x (lembre-se de herdar de object para classes de "novo estilo"):


class Entidade(object):
    def exibir_id(self, id_val):
        print "ID da Entidade: %s" % id_val

class Usuario(Entidade):
    def exibir_id(self, id_val):
        print "Inicio da exibicao do usuario."
        super(Usuario, self).exibir_id(id_val) # Chama o método exibir_id da Entidade
        print "Fim da exibicao do usuario."

user = Usuario()
user.exibir_id(123)

Alguns pontos importantes sobre super():

  1. Em herança simples, super() e chamadas diretas ao construtor pai são similares.

    Ao lidar com uma única hierarquia de herança, o efeito de super().__init__() é similar ao de NomeDaClassePai.__init__(self).

    
    class ObjetoBase:
       def __init__(self):
           print("ObjetoBase inicializado.")
    
    class SubClasseA(ObjetoBase):
       def __init__(self):
           print("Inicializando SubClasseA.")
           ObjetoBase.__init__(self) # Chamada explícita
    
    class SubClasseB(ObjetoBase):
       def __init__(self):
           print("Inicializando SubClasseB.")
           super().__init__() # Usando super()
    
    obj_base = ObjetoBase()
    obj_a = SubClasseA()
    obj_b = SubClasseB()
    
    

    Saída:

    
    ObjetoBase inicializado.
    Inicializando SubClasseA.
    ObjetoBase inicializado.
    Inicializando SubClasseB.
    ObjetoBase inicializado.
    
    

    Note que super() elimina a necessidade de referenciar explicitamente a classe base pelo nome.

  2. super() funciona apenas com classes de "novo estilo".

    Se uma classe base não herdar de object (o que a tornaria uma classe de "estilo antigo" em Python 2.x), super() falhará com um TypeError. Em Python 3.x, todas as classes são de "novo estilo" por padrão.

  3. super() não representa a classe pai, mas sim a próxima classe na MRO.

    Este é um conceito fundamental. Em herança múltipla, super() segue a Method Resolution Order (MRO) para encontrar o próximo método a ser chamado. A MRO é uma lista que define a ordem de pesquisa de métodos. Você pode inspecioná-la usando SuaClasse.mro() ou SuaClasse.__mro__.

    Considere o seguinte exemplo de herança múltipla:

    
    class BaseComponent:
       def __init__(self):
           print("Inicializando BaseComponent.")
    
    class ComponenteA(BaseComponent):
       def __init__(self):
           print("Entrando em ComponenteA.")
           super().__init__()
           print("Saindo de ComponenteA.")
    
    class ComponenteB(BaseComponent):
       def __init__(self):
           print("Entrando em ComponenteB.")
           super().__init__()
           print("Saindo de ComponenteB.")
    
    class SistemaComplexo(ComponenteA, ComponenteB):
       pass
    
    sistema = SistemaComplexo()
    print(SistemaComplexo.__mro__)
    
    

    Saída:

    
    Entrando em ComponenteA.
    Entrando em ComponenteB.
    Inicializando BaseComponent.
    Saindo de ComponenteB.
    Saindo de ComponenteA.
    (<class>, <class>, <class>, <class>, <class>)
           </class></class></class></class></class>
    

    Observe que a ordem de execução do construtor segue a MRO: SistemaComplexo -> ComponenteA -> ComponenteB -> BaseComponent. Se tivéssemos chamado BaseComponent.__init__(self) diretamente em ComponenteA, ComponenteB teria sido ignorado.

  4. super() ajuda a evitar chamadas duplicadas em herança múltipla (problema do diamante).

    No cenário de herança em diamante, onde uma classe herda de duas classes que, por sua vez, herdam da mesma classe base, o uso de chamadas diretas ao construtor pode levar à execução duplicada do construtor da classe mais base. super() resolve isso garantindo que cada método seja chamado apenas uma vez na ordem correta da MRO.

    Exemplo com chamadas diretas (problema):

    
    class ServicoBase:
       def __init__(self):
           print("ServicoBase inicializado.")
    
    class ServicoLog(ServicoBase):
       def __init__(self):
           print("ServicoLog inicializado.")
           ServicoBase.__init__(self) # Chamada direta
    
    class ServicoAutenticacao(ServicoBase):
       def __init__(self):
           print("ServicoAutenticacao inicializado.")
           ServicoBase.__init__(self) # Chamada direta
    
    class SistemaGerenciador(ServicoLog, ServicoAutenticacao):
       def __init__(self):
           print("SistemaGerenciador inicializado.")
           ServicoLog.__init__(self)
           ServicoAutenticacao.__init__(self)
    
    print("--- Com chamadas diretas ---")
    gerenciador_direto = SistemaGerenciador()
    
    

    Saída (ServicoBase inicializado duas vezes):

    
    --- Com chamadas diretas ---
    SistemaGerenciador inicializado.
    ServicoLog inicializado.
    ServicoBase inicializado.
    ServicoAutenticacao inicializado.
    ServicoBase inicializado.
    
    

    Exemplo usando super() (solução):

    
    class ServicoBase:
       def __init__(self):
           print("ServicoBase inicializado.")
    
    class ServicoLog(ServicoBase):
       def __init__(self):
           print("ServicoLog inicializado.")
           super().__init__()
    
    class ServicoAutenticacao(ServicoBase):
       def __init__(self):
           print("ServicoAutenticacao inicializado.")
           super().__init__()
    
    class SistemaGerenciador(ServicoLog, ServicoAutenticacao):
       def __init__(self):
           print("SistemaGerenciador inicializado.")
           super().__init__()
    
    print("\n--- Com super() ---")
    gerenciador_super = SistemaGerenciador()
    print(SistemaGerenciador.__mro__)
    
    

    Saída (ServicoBase inicializado uma vez):

    
    --- Com super() ---
    SistemaGerenciador inicializado.
    ServicoLog inicializado.
    ServicoAutenticacao inicializado.
    ServicoBase inicializado.
    (<class>, <class>, <class>, <class>, <class>)
           </class></class></class></class></class>
    

isinstance() e issubclass()

isinstance(objeto, classe_ou_tupla)

Verifica se um objeto é uma instância de uma classe fornecida ou de qaulquer classe em uma tupla de classes. Retorna True ou False.


class Forma:
    pass

class Circulo(Forma):
    pass

minha_forma = Forma()
meu_circulo = Circulo()

print(isinstance(minha_forma, Forma))   # True
print(isinstance(meu_circulo, Circulo)) # True
print(isinstance(meu_circulo, Forma))   # True (Circulo é subclasse de Forma)
print(isinstance(minha_forma, Circulo)) # False

issubclass(subclasse, superclasse_ou_tupla)

Verifica se uma subclasse é de fato uma subclasse de uma superclasse fornecida ou de qualquer classe em uma tupla de superclasses. Retorna True ou False.


class Forma:
    pass

class Circulo(Forma):
    pass

class Retangulo(Forma):
    pass

class Quadrado(Retangulo):
    pass

print(issubclass(Circulo, Forma))      # True
print(issubclass(Quadrado, Retangulo)) # True
print(issubclass(Quadrado, Forma))     # True (Quadrado é subclasse de Retangulo, que é de Forma)
print(issubclass(Forma, Circulo))      # False

Reflexão (Introspecção)

O que é Reflexão?

Reflexão, ou introspecção, refere-se à capacidade de um programa de examinar, modificar e interagir com sua própria estrutura, tipos e comportamento em tempo de execução. Em Python, onde "tudo é um objeto", essa capacidade é inerente e pode ser aplicada a classes, objetos, funções e módulos.

Funções para Introspecção em Python

Python oferece quatro funções chave para trabalhar com reflexão, aplicáveis tanto a classes quanto a objetos:

hasattr(objeto, nome_atributo)

Verifica se um objeto possui um atributo (variável ou método) com o nome_atributo especificado como uma string. Retorna True se o atributo existir, False caso contrário.


class Funcionario:
    def __init__(self, nome, cargo):
        self.nome = nome
        self.cargo = cargo

    def apresentar(self):
        print(f"{self.nome} trabalha como {self.cargo}.")

colaborador = Funcionario("Ana", "Desenvolvedora")

print(hasattr(colaborador, 'nome'))
print(hasattr(colaborador, 'apresentar'))
print(hasattr(colaborador, 'salario')) # Atributo não existe

Saída:


True
True
False

getattr(objeto, nome_atributo, valor_padrao=None)

Retorna o valor de um atributo nomeado de um objeto. O nome_atributo deve ser uma string. Se o atributo não existir e um valor_padrao for fornecido, este será retornado. Caso contrário, se o atributo não existir e nenhum valor_padrao for fornecido, uma exceção AttributeError é levantada.


class Produto:
    def __init__(self, sku, nome, preco):
        self.sku = sku
        self.nome = nome
        self.preco = preco

    def calcular_desconto(self, percentual):
        return self.preco * (1 - percentual / 100)

item = Produto("P001", "Smartphone X", 1200)

print(getattr(item, 'nome'))
print(getattr(item, 'calcular_desconto')) # Retorna o objeto método
print(getattr(item, 'calcular_desconto')(10)) # Chama o método
print(getattr(item, 'descricao', 'Nenhuma descrição disponível.')) # Atributo não existe, retorna padrão

Saída:


Smartphone X
<bound method Produto.calcular_desconto of <__main__.Produto object at 0x...>>
1080.0
Nenhuma descrição disponível.

setattr(objeto, nome_atributo, valor)

Define o valor de um atributo de um objeto. Se o atributo já existir, seu valor é alterado. Se não existir, um novo atributo é criado no objeto.


class Configuracao:
    def __init__(self, tema):
        self.tema = tema

cfg = Configuracao("escuro")
print(cfg.__dict__)

setattr(cfg, 'fonte', 'Roboto') # Adiciona um novo atributo
print(cfg.__dict__)

setattr(cfg, 'tema', 'claro') # Modifica um atributo existente
print(cfg.__dict__)

Saída:


{'tema': 'escuro'}
{'tema': 'escuro', 'fonte': 'Roboto'}
{'tema': 'claro', 'fonte': 'Roboto'}

delattr(objeto, nome_atributo)

Deleta um atributo nomeado de um objeto. Se o atributo não existir, uma exceção AttributeError é levantada.


class DadosUsuario:
    def __init__(self, id_usuario, email):
        self.id_usuario = id_usuario
        self.email = email

usuario = DadosUsuario(101, 'user@example.com')
print(usuario.__dict__)

delattr(usuario, 'email')
print(usuario.__dict__)

# delattr(usuario, 'telefone') # Isso levantaria um AttributeError

Saída:


{'id_usuario': 101, 'email': 'user@example.com'}
{'id_usuario': 101}

Reflexão com Classes (Classes são Objetos)

As funções de reflexão também podem ser usadas diretamente com classes, pois em Python, classes são objetos de primeira classe.


class ConfiguracaoSistema:
    VERSAO = "1.0.0" # Atributo de classe

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

    def obter_status(self):
        return f"Sistema rodando em {self.ambiente} (v{self.VERSAO})"

    @staticmethod
    def log_evento(mensagem):
        print(f"LOG: {mensagem}")

print(getattr(ConfiguracaoSistema, 'VERSAO'))
print(getattr(ConfiguracaoSistema, 'obter_status'))
print(getattr(ConfiguracaoSistema, 'log_evento'))
print(getattr(ConfiguracaoSistema, 'log_evento')("Início da aplicação"))

Saída:


1.0.0
<function ConfiguracaoSistema.obter_status at 0x...>
<function ConfiguracaoSistema.log_evento at 0x...>
LOG: Início da aplicação

Reflexão de Membros do Módulo Atual

É possível usar reflexão para inspecionar funções e variáveis definidas no módulo atual, acessando-o através de sys.modules[__name__] ou sys.modules['__main__'].


import sys

def processar_dados():
    print("Processando dados...")

def gerar_relatorio():
    print("Gerando relatório...")

modulo_atual = sys.modules[__name__] # ou sys.modules['__main__'] se for o script principal

print(hasattr(modulo_atual, 'processar_dados'))
metodo_relatorio = getattr(modulo_atual, 'gerar_relatorio')
metodo_relatorio()

Saída:


True
Gerando relatório...

Importação Dinâmica de Módulos com __import__

A função __import__ permite importar módulos dinamicamente usando seu nome em string, e então utilizar reflexão para acessar seus membros.

Suponha um arquivo operacoes_uteis.py:


# operacoes_uteis.py
def somar(a, b):
    return a + b

def subtrair(a, b):
    return a - b

No script principle:


# script_principal.py
nome_modulo = 'operacoes_uteis'
modulo_util = __import__(nome_modulo)

print(hasattr(modulo_util, 'somar'))
func_somar = getattr(modulo_util, 'somar')
print(func_somar(5, 3))

Saída:


True
8

Benefícios da Reflexão

**1. Mecanismos "Plug-and-Play":**A reflexão permite a construção de sistemas onde componentes ou funcionalidades podem ser "conectados" ou "desconectados" dinamicamente. Por exemplo, uma aplicação pode definir interfaces (métodos esperados) e esperar que outros módulos forneçam implementações. Mesmo que uma implementação ainda não esteja disponível, a lógica principal pode ser escrita, utilizando reflexão para chamar as funcionalidades esperadas quando elas finalmente forem fornecidas. Isso é um tipo de "ligação tardia", onde a decisão sobre qual código executar é adiada para o tempo de execução.


# service_manager.py
class ServicoIndisponivel(Exception):
    pass

class GerenciadorDeServicos:
    def __init__(self):
        self._servicos = {}

    def registrar_servico(self, nome, obj_servico):
        self._servicos[nome] = obj_servico

    def executar_operacao(self, nome_servico, nome_operacao, *args, **kwargs):
        servico = self._servicos.get(nome_servico)
        if not servico:
            raise ServicoIndisponivel(f"Serviço '{nome_servico}' não registrado.")
        
        if hasattr(servico, nome_operacao):
            metodo = getattr(servico, nome_operacao)
            return metodo(*args, **kwargs)
        else:
            raise AttributeError(f"Serviço '{nome_servico}' não possui a operação '{nome_operacao}'.")

# logger_service.py
class LoggerService:
    def log_message(self, message):
        print(f"[LOG] {message}")

    def error_message(self, message):
        print(f"[ERROR] {message}")

# main_app.py
from service_manager import GerenciadorDeServicos
from logger_service import LoggerService

app_manager = GerenciadorDeServicos()
app_manager.registrar_servico("logger", LoggerService())

try:
    app_manager.executar_operacao("logger", "log_message", "Aplicação iniciada com sucesso.")
    app_manager.executar_operacao("logger", "error_message", "Falha na conexão com o banco de dados.")
    app_manager.executar_operacao("database", "connect") # Serviço não registrado
except (ServicoIndisponivel, AttributeError) as e:
    print(f"Erro: {e}")

**2. Importação Dinâmica de Módulos:**A reflexão, em conjunto com __import__ ou a função importlib.import_module (mais recomendada para Python 3), permite que uma aplicação decida quais módulos carregar em tempo de execução, com base em configurações, entradas do usuário ou outras condições. Isso é útil para arquiteturas de plugins, carregamento de drivers específicos ou configurações de ambiente.

__getattr__, __getattribute__, __setattr__, __delattr__

Estes são métodos especiais (também conhecidos como "métodos mágicos" ou "dunder methods") que interceptam o acesso a atributos de instâncias de classe. Eles oferecem um controle granular sobre como os atributos são criados, lidos, modificados e excluídos.

  • __setattr__(self, nome, valor): Chamado sempre que uma instância tem um atributo definido ou modificado.
  • __delattr__(self, nome): Invocado quando uma instância tem um atributo deletado.
  • __getattr__(self, nome): Chamado somente quando um atributo é acessado e NÃO é encontrado pelos meios normais (ou seja, não está em __dict__ da instância ou da classe, nem nas classes base).
  • __getattribute__(self, nome): Invocado para TODOS os acessos a atributos. É o primeiro método a ser chamado.

A ordem de chamada é crucial: __getattribute__ é sempre chamado primeiro. Se ele não encontrar o atributo ou levantar um AttributeError (especificamente), então __getattr__ será chamado (se existir).

Interação entre __getattribute__ e __getattr__

Considere uma classe de "novo estilo" (que herda de object):


class ObservadorAtributo(object):
    def __init__(self):
        print("ObservadorAtributo inicializado.")

    def __getattr__(self, nome):
        print(f"__getattr__ chamado para: {nome} (atributo não encontrado).")
        # Devemos levantar um AttributeError aqui se não pudermos fornecer o atributo
        raise AttributeError(f"Atributo '{nome}' não existe.")

    def __getattribute__(self, nome):
        print(f"__getattribute__ chamado para: {nome}.")
        # Para continuar a cadeia de pesquisa de atributos normal,
        # é NECESSÁRIO chamar o método da superclasse.
        # Caso contrário, o atributo nunca será encontrado e só veremos esta mensagem.
        return super().__getattribute__(nome)

    def __setattr__(self, nome, valor):
        print(f"__setattr__ chamado para: {nome} = {valor}.")
        super().__setattr__(nome, valor) # Importante para evitar recursão infinita

    def __delattr__(self, nome):
        print(f"__delattr__ chamado para: {nome}.")
        super().__delattr__(nome)

print("--- Criando instância ---")
monitor = ObservadorAtributo()
print("\n--- Acessando atributo existente ---")
monitor.novo_atributo = 42 # Isso chama __setattr__
print(monitor.novo_atributo) # Isso chama __getattribute__ e encontra o atributo

print("\n--- Acessando atributo inexistente ---")
try:
    print(monitor.atributo_fantasma) # Isso chama __getattribute__ e então __getattr__
except AttributeError as e:
    print(f"Erro capturado: {e}")

Saída:


--- Criando instância ---
ObservadorAtributo inicializado.

--- Acessando atributo existente ---
__setattr__ chamado para: novo_atributo = 42.
__getattribute__ chamado para: novo_atributo.
42

--- Acessando atributo inexistente ---
__getattribute__ chamado para: atributo_fantasma.
__getattr__ chamado para: atributo_fantasma (atributo não encontrado).
Erro capturado: Atributo 'atributo_fantasma' não existe.

Como visto no exemplo, __getattribute__ é chamado para todos os acessos. Se ele próprio não chamar super().__getattribute__(nome), o atributo não será encontrado, e __getattr__ (se presente) será chamado apenas se __getattribute__ levantar um AttributeError explicitamente (o que super().__getattribute__(nome) faz por padrão se o atributo não for encontrado).

Cuidado com a recursão em __setattr__

Ao implementar __setattr__, é crucial evitar chamadas recursivas infinitas. Se você atribuir um valor a self.algum_atributo dentro de __setattr__, isso chamará __setattr__ novamente, criando um loop infinito.


class ConfigManager(object):
    def __init__(self):
        # Correto: Usa super().__setattr__ para inicializar atributos
        # Caso contrário, self.log_level = 'INFO' chamaria __setattr__ recursivamente
        super().__setattr__('log_level', 'INFO') 

    def __setattr__(self, nome, valor):
        print(f"Interceptando configuração: {nome} = {valor}")
        # Errado: Isso causaria recursão infinita!
        # self.nome = valor 

        # Correto: Chama o setter da classe pai para definir o atributo
        super().__setattr__(nome, valor) 

config = ConfigManager()
config.log_level = 'DEBUG' # Chama __setattr__
print(config.log_level) # Chama __getattribute__ (não implementado, usa padrão)

Saída:


Interceptando configuração: log_level = DEBUG
DEBUG

Customização de Tipos Padrão (Empacotamento/Delegação)

Python oferece tipos de dados padrão e métodos embutidos. No entanto, muitas vezes é necessário estender ou modificar esses tipos para adicionar funcionalidade específica, validação ou controle de acesso. Isso pode ser feito por herança ou por um padrão conhecido como "empacotamento" ou "delegação".

Customização via Herança

Estender um tipo padrão via herança é direto. A nova classe herda todos os comportamentos do tipo base e pode sobrescrever ou adicionar novos métodos e atributos.


class ListaValidada(list):
    def adicionar(self, elemento):
        """Adiciona um elemento com verificação de tipo."""
        if not isinstance(elemento, int):
            raise TypeError("A ListaValidada aceita apenas inteiros.")
        super().append(elemento) # Chama o método append da classe 'list'

    @property
    def elemento_central(self):
        """Retorna o elemento do meio da lista."""
        if not self: # Lista vazia
            return None
        indice = len(self) // 2
        return self[indice]

minha_lista = ListaValidada([1, 2, 3, 4])
print(minha_lista)
minha_lista.adicionar(5)
print(minha_lista)
# minha_lista.adicionar("string") # Isso levantaria um TypeError

print(f"Elemento central: {minha_lista.elemento_central}")

# Outros métodos de lista ainda funcionam normalmente
minha_lista.insert(0, -10)
print(minha_lista)
minha_lista.clear() # Python 3.x tem clear()
print(minha_lista)

Saída:


[1, 2, 3, 4]
[1, 2, 3, 4, 5]
Elemento central: 3
[-10, 1, 2, 3, 4, 5]
[]

Customização via Delegação (Empacotamento com __getattr__)

A delegação é uma alternativa à herança. Em vez de herdar de um tipo padrão, a nova classe contém uma instância do tipo padrão e "delegação" chamadas de método ou acesso a atributos para essa instância interna. Isso oferece mais controle sobre quais métodos são expostos e como eles funcionam. O método mágico __getattr__ é a chave para implementar a delegação.


import time

class ArquivoComLog:
    def __init__(self, caminho_arquivo, modo='r', codificacao='utf-8'):
        self._arquivo = open(caminho_arquivo, modo, encoding=codificacao)
        self._caminho = caminho_arquivo
        self._modo = modo

    def escrever_com_timestamp(self, linha):
        """Sobrescreve o método write para adicionar um timestamp."""
        timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
        self._arquivo.write(f"{timestamp} {linha}\n")

    def __getattr__(self, nome_atributo):
        """Delega o acesso a outros atributos e métodos para o objeto de arquivo interno."""
        # Se o atributo não for encontrado na classe ArquivoComLog,
        # tenta encontrá-lo no objeto self._arquivo.
        return getattr(self._arquivo, nome_atributo)

    def __str__(self):
        return f"<ArquivoComLog para '{self._caminho}' em modo '{self._modo}'>"

# Exemplo de uso:
with open('log_eventos.txt', 'w', encoding='utf-8') as f:
    f.write("") # Garante que o arquivo esteja vazio para o teste

log_file = ArquivoComLog('log_eventos.txt', 'w+')
log_file.escrever_com_timestamp("Início do registro de eventos.")
log_file.escrever_com_timestamp("Um evento importante ocorreu.")

# Métodos como seek, read, close são delegados automaticamente
log_file.seek(0)
conteudo = log_file.read()
print(conteudo)
log_file.close()

# Exemplo de delegação para uma lista com controle de permissão
class ListaControlada:
    def __init__(self, itens_iniciais=None, permitir_limpeza=False):
        self._lista = list(itens_iniciais) if itens_iniciais else []
        self.permitir_limpeza = permitir_limpeza

    def limpar(self):
        """Método personalizado com controle de permissão."""
        if not self.permitir_limpeza:
            raise PermissionError("Operação de limpeza não permitida.")
        self._lista.clear()

    def __getattr__(self, nome_atributo):
        """Delega outros métodos de lista para a lista interna."""
        return getattr(self._lista, nome_atributo)

    def __str__(self):
        return str(self._lista)

minha_lista_controlada = ListaControlada([10, 20, 30], permitir_limpeza=False)
print(minha_lista_controlada)
minha_lista_controlada.append(40) # Delegado
print(minha_lista_controlada)

# Tenta limpar sem permissão
try:
    minha_lista_controlada.limpar()
except PermissionError as e:
    print(f"Erro: {e}")

minha_lista_controlada.permitir_limpeza = True
print("Permissão de limpeza concedida.")
minha_lista_controlada.limpar()
print(minha_lista_controlada)

Saída:


2023-10-27 10:00:00 Início do registro de eventos.
2023-10-27 10:00:01 Um evento importante ocorreu.

[10, 20, 30]
[10, 20, 30, 40]
Erro: Operação de limpeza não permitida.
Permissão de limpeza concedida.
[]

Tags: Python OOP metaprogramming reflection MagicMethods

Publicado em 6-18 17:11