Decorators Internamente: Como Funcionam e Como Criar os Seus

Se você escreve Python há algum tempo, já usou decorators sem perceber. O @app.route do Flask, o @pytest.mark.parametrize, o @dataclass da stdlib, o @property nativo da linguagem — todos são decorators. Eles aparecem em todo framework relevante do ecossistema, mas a maioria dos recursos disponíveis explica como usar sem explicar por que funciona.

Este artigo corrige isso.

A ideia aqui não é ensinar a sintaxe do @. É mostrar o mecanismo embaixo: o que Python faz quando encontra esse símbolo, como construir um decorator do zero com segurança e como evitar as armadilhas que só aparecem em produção.


1. Pré-requisito: Funções São Objetos de Primeira Classe

Antes de entender decorators, é preciso internalizar um conceito que Python respeita de forma consistente: funções são objetos como qualquer outro.

Isso significa que uma função pode ser atribuída a uma variável, passada como argumento para outra função, e retornada como resultado de uma chamada. Se isso parece óbvio, bem. Mas as consequências disso são o alicerce inteiro do decorator pattern.

Funções podem ser atribuídas a variáveis:

def saudacao() -> str:
    return "Olá!"

outra_referencia = saudacao   # sem parênteses — não estamos chamando, estamos referenciando
print(outra_referencia())     # "Olá!"

Funções podem ser passadas como argumento:

def executar(func) -> None:
    print("Antes")
    func()
    print("Depois")

executar(saudacao)
# Antes
# Olá!
# Depois

Funções podem ser retornadas por outras funções:

def criar_saudacao(nome: str):
    def mensagem() -> str:
        return f"Olá, {nome}!"   # captura 'nome' do escopo externo
    return mensagem  # retorna a função, não o resultado

ola_vicente = criar_saudacao("Vicente")
print(ola_vicente())   # "Olá, Vicente!" — mesmo após criar_saudacao() ter retornado

Esse último exemplo tem um nome técnico: closure. A função interna mensagem “fecha sobre” a variável nome do escopo externo e a mantém viva mesmo depois que criar_saudacao terminou de executar. O Python preserva esse contexto enquanto houver uma referência à função interna.

Closures são o mecanismo que permite decorators funcionarem. Quando você entende closures, a mecânica do @ deixa de ser mágica e passa a ser consequência natural.


2. A Mecânica do Decorator — Desvendando o @

Com closures claras, o decorator se torna trivial de entender: @decorator é açúcar sintático para uma atribuição.

@meu_decorator
def funcao() -> None:
    ...

O Python transforma isso exatamente em:

def funcao() -> None:
    ...
funcao = meu_decorator(funcao)

É tudo. Não existe nenhuma magia adicional. O símbolo @ instrui o interpretador a passar a função definida logo abaixo como argumento para meu_decorator e a reatribuir o resultado de volta ao mesmo nome.

Para deixar isso concreto, veja o primeiro decorator possível — sem usar @, para tornar o mecanismo explícito:

def logar(func):
    def wrapper():
        print(f"Chamando {func.__name__}...")
        func()
        print(f"{func.__name__} concluída.")
    return wrapper

def processar() -> None:
    print("Processando pedido.")

# Sem @: reatribuição explícita
processar = logar(processar)
processar()
# Chamando processar...
# Processando pedido.
# processar concluída.

Agora a mesma coisa com a sintaxe @ — o resultado é idêntico:

@logar
def processar() -> None:
    print("Processando pedido.")

processar()
# Chamando processar...
# Processando pedido.
# processar concluída.

O @ é apenas uma forma mais limpa de escrever processar = logar(processar). Reconhecer isso é o que permite raciocinar sobre qualquer decorator, não importa o quão complexo ele pareça.


3. Anatomia de um Decorator Bem Formado

O exemplo acima funciona, mas tem um problema: só aceita funções sem argumentos. Em produção, os decorators precisam ser transparentes — funcionar com qualquer assinatura de função, independentemente de quantos parâmetros ela receba.

Este é o template canônico:

import functools

def meu_decorator(func):
    @functools.wraps(func)  # (a) preserva a identidade da função original
    def wrapper(*args, **kwargs):  # (b) aceita qualquer assinatura
        # (c) lógica executada antes da função original
        resultado = func(*args, **kwargs)   # (d) chama a função original com seus argumentos
        # (e) lógica executada depois
        return resultado  # (f) retorna o valor original sem modificá-lo
    return wrapper  # (g) retorna o wrapper — não chama, retorna

Cada ponto merece atenção:

(a) @functools.wraps(func) preserva os metadados da função original no wrapper. O motivo completo merece uma seção própria — e vai ter uma logo adiante.

(b) *args, **kwargs garante que o wrapper aceita qualquer combinação de argumentos posicionais e nomeados, repassando-os intactos para a função original. Sem isso, o decorator só funciona com funções de assinatura idêntica à do wrapper.

© e (e) São os pontos onde a lógica do decorator vive: logging, validação, timing, cache — tudo entra aqui.

(d) func(*args, **kwargs) chama a função original com os mesmos argumentos recebidos. Note que a variável func vem do escopo externo — isso é a closure em ação.

(f) return resultado é crítico. Um decorator que não retorna o valor da função original “engole” o retorno silenciosamente. Se processar_pedido retorna uma lista de itens e o decorator não faz return resultado, o chamador recebe None.

(g) return wrapper está fora do corpo do wrapper. O decorator retorna a função wrapper — não a chama. É essa distinção que faz o mecanismo funcionar: ao escrever @meu_decorator, Python substitui o nome da função pelo wrapper retornado aqui.

Exemplo completo com timer:

import time
import functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = func(*args, **kwargs)
        fim = time.perf_counter()
        print(f"{func.__name__!r} executou em {fim - inicio:.6f}s")
        return resultado
    return wrapper

@timer
def processar_pedidos(n: int) -> list[int]:
    """Simula o processamento de uma lista de pedidos."""
    return list(range(n))

itens = processar_pedidos(100_000)
# 'processar_pedidos' executou em 0.004312s

time.perf_counter() é preferível a time.time() para medições de performance: tem resolução mais alta e não sofre ajustes de relógio do sistema.


4. O Problema da Identidade — Por que functools.wraps É Obrigatório

Há um detalhe sutil que cobra um preço alto quando ignorado. Observe:

def log(func):
    def wrapper(*args, **kwargs):  # sem @functools.wraps
        return func(*args, **kwargs)
    return wrapper

@log
def calcular_total(pedido: dict) -> float:
    """Calcula o valor total de um pedido com impostos."""
    ...

print(calcular_total.__name__)   # 'wrapper'  ← errado
print(calcular_total.__doc__)    # None        ← errado

Após a decoração, calcular_total aponta para o objeto wrapper. Sem nenhum cuidado adicional, __name__, __doc__, __annotations__ e outros atributos são os do wrapper — não os da função original. O nome que aparece em stack traces, em ferramentas de documentação automática como Sphinx, em pytest markers e no help() interativo é wrapper.

Em um projeto com dezenas de funções decoradas, todo stack trace em produção vai apontar para wrapper em vez de indicar a função real com problema. O custo de debugging aumenta desnecessariamente.

functools.wraps resolve isso. Ele é um decorator aplicado ao wrapper que copia os atributos relevantes da função original:

def log(func):
    @functools.wraps(func)    # copia os metadados de func para wrapper
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log
def calcular_total(pedido: dict) -> float:
    """Calcula o valor total de um pedido com impostos."""
    ...

print(calcular_total.__name__)   # 'calcular_total'  ← correto
print(calcular_total.__doc__)    # 'Calcula o valor...'  ← correto

Internamente, functools.wraps é um atalho para functools.update_wrapper(wrapper, func). Os atributos transferidos são:

Atributo O que representa
__name__ Nome da função — aparece em stack traces e repr
__qualname__ Nome qualificado — inclui classe e módulo, para contexto exato
__doc__ Docstring — essencial para help(), Sphinx e IDEs
__module__ Módulo de origem — identifica onde a função foi definida
__annotations__ Type hints — necessário para mypy e ferramentas de análise estática
__dict__ Atributos customizados — preserva metadados adicionados à função
__wrapped__ Referência direta à função original — adicionado pelo wraps

O atributo __wrapped__ merece destaque: ele permite “desembrulhar” a cadeia de decorators e acessar a função original diretamente, o que é útil em testes e introspecção.

print(calcular_total.__wrapped__)   # <function calcular_total at 0x...>

A regra é simples: todo decorator deve usar @functools.wraps(func) no wrapper interno, sem exceção. O custo é zero, o benefício é real.


5. Decorators com Argumentos — A Fábrica de Decorators

Até aqui, os decorators recebem apenas a função como argumento. Mas muitos dos decorators mais úteis precisam de configuração: @retry(max_tentativas=3), @cache(ttl=60), @permissao_requerida("admin").

Ao adicionar parênteses ao decorator, o comportamento muda completamente — e é aqui que a maioria dos tutoriais perde o leitor.

A confusão vem do seguinte: @repetir(vezes=3) não está chamando um decorator. Está chamando uma fábrica de decorator — uma função que, ao ser chamada com os argumentos de configuração, retorna o decorator de verdade.

A estrutura tem três camadas:

import functools

def repetir(vezes: int):  # ← camada 1: fábrica — recebe os argumentos
    def decorator(func):   # ← camada 2: decorator — recebe a função
        @functools.wraps(func)
        def wrapper(*args, **kwargs):  # ← camada 3: wrapper — executa em runtime
            for _ in range(vezes):
                resultado = func(*args, **kwargs)
            return resultado
        return wrapper
    return decorator  # fábrica retorna o decorator

@repetir(vezes=3)
def notificar(mensagem: str) -> None:
    print(f"[NOTIF] {mensagem}")

notificar("pedido aprovado")
# [NOTIF] pedido aprovado
# [NOTIF] pedido aprovado
# [NOTIF] pedido aprovado

Para entender o que acontece passo a passo, expanda a sintaxe @:

# @repetir(vezes=3) se desdobra em:

_decorator = repetir(vezes=3)  # step 1: chama a fábrica, obtém o decorator
notificar = _decorator(notificar)  # step 2: aplica o decorator à função

A variável vezes fica capturada pela closure do wrapper, que a usa em cada chamada de notificar.

A regra para identificar quantas camadas um decorator precisa é direta: decorator sem parênteses = uma função que recebe func; decorator com parênteses = uma função que recebe os argumentos e retorna uma função que recebe func.


6. Stacking — Empilhando Decorators e a Ordem de Execução

Python permite empilhar múltiplos decorators sobre uma mesma função. A ordem em que eles aparecem determina o comportamento — e errar essa ordem pode introduzir bugs silenciosos que só aparecem em produção.

@decorator_a  # aplicado por último, envolve o resultado de decorator_b
@decorator_b  # aplicado primeiro, envolve a função original
def minha_funcao():
    ...

# Equivalente exato:
minha_funcao = decorator_a(decorator_b(minha_funcao))

A regra de ouro: a aplicação é de baixo para cima (o decorator mais próximo da função é aplicado primeiro), mas a execução em runtime é de cima para baixo (o decorator mais externo executa primeiro).

Para tornar isso concreto, considere dois decorators em um endpoint de API:

import functools

def logar(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[LOG] Chamando {func.__name__!r}")
        resultado = func(*args, **kwargs)
        print(f"[LOG] {func.__name__!r} concluída")
        return resultado
    return wrapper

def autenticar(func):
    @functools.wraps(func)
    def wrapper(usuario: str, *args, **kwargs):
        if usuario != "admin":
            raise PermissionError(f"Acesso negado para '{usuario}'")
        return func(usuario, *args, **kwargs)
    return wrapper

Cenário A — @logar acima de @autenticar:

@logar
@autenticar
def obter_relatorio(usuario: str) -> dict:
    return {"relatorio": "dados sensíveis"}

obter_relatorio("convidado")
# [LOG] Chamando 'obter_relatorio'   ← loga antes de verificar permissão
# PermissionError: Acesso negado para 'convidado'

O log registra a tentativa de acesso mesmo quando o usuário não tem permissão. Em alguns sistemas, isso é o comportamento correto — registrar toda tentativa, incluindo as negadas.

Cenário B — @autenticar acima de @logar:

@autenticar
@logar
def obter_relatorio(usuario: str) -> dict:
    return {"relatorio": "dados sensíveis"}

obter_relatorio("convidado")
# PermissionError: Acesso negado para 'convidado'
# (sem log — a exceção ocorre antes do log ser atingido)

Aqui, a autenticação bloqueia antes do log registrar qualquer coisa. Apenas chamadas autenticadas chegam ao log.

Ambos os comportamentos podem ser desejados, dependendo do requisito. O ponto é que a ordem define o comportamento, e não há nada no código que sinalize a diferença visualmente além da posição do @. É uma decisão arquitetural que precisa ser documentada.


7. Decorators Baseados em Classe — Quando o Estado Importa

Até agora, todos os decorators foram funções. Mas Python permite usar classes como decorators também — e elas se tornam a escolha certa quando o decorator precisa manter estado entre chamadas.

Um contador de invocações é o exemplo mais direto:

import functools

class Contador:
    def __init__(self, func) -> None:
        functools.update_wrapper(self, func)
        self.func = func
        self.chamadas: int = 0

    def __call__(self, *args, **kwargs):
        self.chamadas += 1
        return self.func(*args, **kwargs)

@Contador
def buscar_usuario(user_id: int) -> dict:
    """Busca dados de um usuário pelo ID."""
    return {"id": user_id}

buscar_usuario(1)
buscar_usuario(2)
buscar_usuario(3)
print(f"Total de chamadas: {buscar_usuario.chamadas}")   # Total de chamadas: 3

O @Contador sobre buscar_usuario é equivalente a buscar_usuario = Contador(buscar_usuario). O construtor __init__ recebe a função, __call__ é executado cada vez que a função decorada é chamada, e o estado (self.chamadas) persiste no objeto.

O que update_wrapper realmente faz numa instância de classe

Aqui vale parar e ser preciso, porque há uma nuance importante que a maioria dos tutoriais ignora.

functools.update_wrapper(self, func) copia atributos como __name__, __qualname__, __doc__ e __annotations__ da função original para o objeto instância — não para a classe Contador. Isso significa que a introspecção programática funciona corretamente:

print(buscar_usuario.__name__)      # 'buscar_usuario'  ← correto
print(buscar_usuario.__doc__)       # 'Busca dados de um usuário pelo ID.'  ← correto
print(buscar_usuario.__wrapped__)   # <function buscar_usuario at 0x...>  ← correto

Porém, o __repr__ padrão de um objeto em Python é gerado pela classe, não pela instância. E a classe Contador não sabe nada sobre __name__ — ela simplesmente herda o __repr__ de object, que produz:

repr(buscar_usuario)
# <__main__.Contador object at 0x7f3a4c2b1d90>

Não <function buscar_usuario at 0x...>, como seria com um decorator de função. O update_wrapper não tem como alterar isso: atributos de instância não têm efeito sobre o __repr__ padrão da classe.

Para fins práticos do dia a dia — pytest, mypy, Sphinx, logging, stack traces — isso raramente é problema: todas essas ferramentas usam __name__ e __qualname__ diretamente, e esses atributos estão corretos. O __repr__ entra em cena principalmente no REPL interativo e em sessões de debug — exatamente onde um repr que “mente” pode confundir mais do que ajudar.

A solução correta: __repr__ que comunica a realidade

O caminho certo não é imitar o repr de uma função — é comunicar a natureza real do objeto, incluindo o estado que só um decorator de classe pode ter:

import functools

class Contador:
    def __init__(self, func) -> None:
        functools.update_wrapper(self, func)
        self.func = func
        self.chamadas: int = 0

    def __call__(self, *args, **kwargs):
        self.chamadas += 1
        return self.func(*args, **kwargs)

    def __repr__(self) -> str:
        return (
            f"<Contador decorator de {self.func.__qualname__!r} "
            f"— {self.chamadas} chamada(s)>"
        )

@Contador
def buscar_usuario(user_id: int) -> dict:
    """Busca dados de um usuário pelo ID."""
    return {"id": user_id}

buscar_usuario(1)
buscar_usuario(2)

repr(buscar_usuario)
# <Contador decorator de 'buscar_usuario' — 2 chamada(s)>

Isso honra os dois requisitos ao mesmo tempo: __name__ e __qualname__ continuam disponíveis para introspecção programática via update_wrapper, e o repr comunica o que o objeto realmente é — um decorator com estado — em vez de fingir ser uma função simples.

A distinção importa especialmente quando o decorator carrega estado observável. Um repr que oculta chamadas, cache, ou qualquer outro estado interno priva o desenvolvedor de informação útil no momento em que ele mais precisa dela: durante o debug.

Quando usar cada abordagem:

Situação Escolha
Comportamento puro sem estado (log, timer, validação) Decorator de função
Estado entre chamadas (contador, cache, rate limiter) Decorator de classe com __repr__ explícito
Lógica configurável via argumentos Fábrica de decorators

8. Padrões de Produção — Exemplos Prontos para Usar

Com a mecânica compreendida, esta seção apresenta três decorators que resolvem problemas reais e podem ser adaptados diretamente em projetos.

8.1 Retry Automático com Backoff

Chamadas a serviços externos falham. Redes instáveis, timeouts, rate limiting — são situações normais em produção. Um decorator de retry encapsula a lógica de re-tentativa sem poluir o código de negócio:

import time
import functools

def retry(max_tentativas: int = 3, delay: float = 1.0, excecoes: tuple = (Exception,)):
    """
    Tenta executar a função até max_tentativas vezes.
    Aguarda delay segundos entre cada tentativa.
    Levanta a exceção original após esgotar as tentativas.
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for tentativa in range(1, max_tentativas + 1):
                try:
                    return func(*args, **kwargs)
                except excecoes as e:
                    if tentativa == max_tentativas:
                        raise
                    print(
                        f"[RETRY] {func.__name__!r} — tentativa {tentativa}/{max_tentativas} "
                        f"falhou: {e}. Aguardando {delay}s..."
                    )
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_tentativas=3, delay=0.5, excecoes=(ConnectionError, TimeoutError))
def chamar_api_pagamentos(payload: dict) -> dict:
    """Envia um pagamento para o processador externo."""
    ...

O parâmetro excecoes permite especificar quais exceções devem acionar o retry. Erros de programação como ValueError ou TypeError não devem ser re-tentados — por isso o padrão não é Exception para tudo.

8.2 Cache por Memoização

Funções que recebem os mesmos argumentos e produzem sempre o mesmo resultado são candidatas à memoização. O decorator abaixo ilustra a lógica antes de introduzir a solução da stdlib:

import functools

def memoizar(func):
    """
    Armazena resultados anteriores indexados pelos argumentos.
    Evita recomputação para entradas já vistas.
    """
    cache: dict = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoizar
def fibonacci(n: int) -> int:
    """Calcula o n-ésimo número de Fibonacci."""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(40))   # instantâneo — sem memoização seria exponencial

Em projetos reais, use @functools.lru_cache(maxsize=128) ou @functools.cache (Python 3.9+) — são implementações da stdlib com controle de tamanho, thread safety e suporte a kwargs. O decorator manual acima serve para compreender o mecanismo antes de usar a versão pronta.

8.3 Validação de Argumentos

Validações de entrada que se repetem em múltiplas funções são candidatas a serem extraídas para um decorator. Isso reduz duplicação e, como consequência direta, reduz a complexidade ciclomática de cada função — o que já discutimos no artigo sobre Radon.

import functools

def validar_positivo(func):
    """
    Garante que o primeiro argumento posicional é um número positivo.
    Levanta ValueError com mensagem descritiva caso contrário.
    """
    @functools.wraps(func)
    def wrapper(valor: float, *args, **kwargs):
        if valor <= 0:
            raise ValueError(
                f"{func.__name__!r} exige um valor positivo. "
                f"Recebido: {valor!r}"
            )
        return func(valor, *args, **kwargs)
    return wrapper

@validar_positivo
def calcular_desconto(preco: float, percentual: float) -> float:
    """Calcula o valor após aplicar o desconto."""
    return preco * (1 - percentual / 100)

@validar_positivo
def calcular_frete(peso_kg: float) -> float:
    """Calcula o custo de frete baseado no peso."""
    return peso_kg * 3.5

calcular_desconto(-10.0, 5)
# ValueError: 'calcular_desconto' exige um valor positivo. Recebido: -10.0

Cada função de negócio ficou com uma única responsabilidade — o decorator cuidou da guarda de entrada.


9. Armadilhas Comuns — O que Costuma Dar Errado

Engolir o retorno da função original

# Errado — não retorna o resultado
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)   # ← sem return
    return wrapper

@log
def somar(a: int, b: int) -> int:
    return a + b

resultado = somar(2, 3)
print(resultado)   # None — o valor foi perdido silenciosamente

O Python não avisa sobre isso. A função executa normalmente, mas o valor retornado some. Sempre use return func(*args, **kwargs) ou armazene em variável antes de retornar.

Esquecer functools.wraps

Já detalhado na seção 4. O custo de depurar stack traces cheios de wrapper em produção é muito maior do que adicionar uma linha ao decorator.

Decorar métodos de instância sem considerar self

Decorators que inspecionam o primeiro argumento precisam de atenção ao ser aplicados a métodos:

def validar_id(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # args[0] aqui é 'self', não o primeiro argumento real do método
        user_id = args[1] if len(args) > 1 else kwargs.get("user_id")
        if not isinstance(user_id, int) or user_id <= 0:
            raise ValueError(f"user_id inválido: {user_id!r}")
        return func(*args, **kwargs)
    return wrapper

class UserService:
    @validar_id
    def buscar(self, user_id: int) -> dict:
        ...

O self entra como args[0], empurrando os argumentos reais para args[1] em diante. Decorators de função que assumem args[0] como primeiro argumento do usuário quebram silenciosamente ao serem aplicados a métodos.

Stacking na ordem errada

Como demonstrado na seção 6, inverter a posição de @autenticar e @logar produz comportamentos diferentes. Sem um comentário que documente a intenção, a ordem parece arbitrária para quem lê o código depois.


10. Checklist de Boas Práticas

# Prática Por quê
1 Sempre use @functools.wraps(func) no wrapper interno Preserva identidade da função em stack traces, docs e ferramentas
2 Use *args, **kwargs no wrapper Garante compatibilidade com qualquer assinatura de função
3 Sempre retorne resultado = func(...) / return resultado Evita engolir retornos silenciosamente
4 Prefira decorator de função para comportamento puro Mais simples, sem overhead de classe
5 Use decorator de classe quando precisar de estado entre chamadas self é o lugar natural para manter estado
6 Documente o decorator com docstring Descreva o que ele adiciona, não o que a função faz
7 Em stacking, coloque o decorator mais específico mais próximo da função Torna a cadeia de transformações previsível
8 Especifique as exceções no retry, não use Exception para tudo Evita re-tentativas em erros de programação
9 Em decorators de classe, use functools.update_wrapper(self, func) Equivalente ao @wraps para instâncias
10 Documente a ordem em stacking quando ela for semanticamente relevante Quem lê o código não deve ter que raciocinar sobre a ordem

11. Conclusão

Decorators não são mágica. São closures com açúcar sintático — e a sintaxe @ é apenas uma forma elegante de escrever funcao = decorator(funcao).

Entender isso abre um caminho direto para duas habilidades práticas: saber ler qualquer decorator existente em frameworks e bibliotecas, e saber construir os seus com a estrutura correta desde o início.

Há uma conexão direta com outros princípios já explorados aqui no blog. O functools.wraps é a materialização do princípio de nomear pelo propósito — sem ele, __name__ mente para toda ferramenta que depende do nome da função. E decorators que extraem lógica transversal — retry, log, validação, cache — reduzem a complexidade ciclomática das funções de negócio, exatamente o que o radon mediria como melhoria no artigo sobre CC.

Um decorator bem escrito é invisível: a função de negócio comunica sua intenção, e o comportamento adicional está encapsulado, testável e reutilizável. É código que se explica por si só — e isso é poder puro na engenharia de software.

Se este artigo te fez repensar como você aplica comportamento transversal no seu código, compartilhe o decorator mais criativo que já escreveu: @riverfount@bolha.us



Riverfount
Vicente Eduardo Ribeiro Marçal