Trilha de aprendizado · Nível 9 · Tutorial 7

Criar decoradores parametrizados

Ao concluir, você será capaz de configurar comportamentos adicionados por decoradores, separando os parâmetros da decoração dos argumentos usados para chamar a função decorada.

  • Nível: Intermediário
  • Duração: 18 min
  • 6 passos
Criar decoradores parametrizados

O que você vai percorrer

  1. Separar configuração e argumentos da chamada Entenda quais valores configuram o decorador e quais valores chegam à função decorada quando ela é chamada. 2 min
  2. Montar a fábrica, o decorador e o wrapper Organize um decorador parametrizado em três camadas: configurar, receber a função original e envolver cada chamada. 3 min
  3. Acompanhar a definição e as chamadas Entenda em que momento a fábrica, o decorador e o wrapper atuam em uma função decorada com configuração. 3 min
  4. Validar e conservar a configuração Valide o rótulo no momento em que a fábrica é chamada e use a versão normalizada nas chamadas posteriores. 3 min
  5. Isolar configuração e estado por função Escolha onde criar um contador para que ele tenha o tempo de vida desejado: por função decorada, compartilhado por um decorador reutilizado ou reiniciado a cada chamada. 4 min
  6. Aplicar um decorador parametrizado completo Integre validação, configuração capturada, preservação de metadados e contagem independente por função em um decorador parametrizado. 4 min

O que você vai aprender

  • Implementar uma fábrica que receba configurações e retorne um decorador.
  • Distinguir as responsabilidades da fábrica, do decorador e do wrapper.
  • Validar configurações no momento apropriado e preservá-las no ambiente capturado.
  • Aplicar configurações diferentes a funções distintas sem compartilhar estado acidentalmente.

Antes de começar

  • Criar decoradores com functools.wraps
  • Validar entradas e sinalizar falhas com raise

Passo 1 de 6

Separar configuração e argumentos da chamada

Entenda quais valores configuram o decorador e quais valores chegam à função decorada quando ela é chamada.

Uma camada para configurar

Do decorador simples ao parametrizado

Você já usou @decorador para adicionar um comportamento a uma função. Agora, queremos configurar esse comportamento.

No caso condutor, registrar_chamadas exibirá uma mensagem antes de executar a função. O valor rotulo define qual mensagem de registro será usada.

A expressão @registrar_chamadas(rotulo="CÁLCULO") chama uma função externa para produzir o decorador. Ela não chama a função que aparece logo abaixo.

Dois caminhos de valores

Observe que a configuração da decoração e os dados de uma chamada posterior percorrem caminhos diferentes.

Diagrama com um rótulo de configuração entrando em uma camada de decorador acima de uma função, enquanto argumentos de chamada entram diretamente na função decorada por outro caminho.

O rótulo configura o comportamento acrescentado; os argumentos pertencem à execução da função.

Configuração não é argumento da função

Exemplo

Definição e chamada em momentos diferentes

@registrar_chamadas(rotulo="CÁLCULO")
def somar(valor_a, valor_b, *, arredondar=False):
    total = valor_a + valor_b
    return round(total) if arredondar else total

resultado = somar(2.4, 3.2, arredondar=True)

Neste exemplo:

  • rotulo="CÁLCULO" configura o comportamento de registro.
  • 2.4, 3.2 e arredondar=True são argumentos da chamada de somar.
  • Os parênteses após registrar_chamadas pertencem à configuração do decorador. Eles não executam somar.

Dica

Pergunta-guia

Para classificar um valor, pergunte: ele aparece na expressão após @ ou na chamada feita pelo código, como somar(...)? No primeiro caso, configura a decoração; no segundo, é destinado à função decorada.

Confira a separação

Identifique os valores

Considere:

@registrar_chamadas(rotulo="PEDIDO")
def calcular_total(preco, quantidade, desconto=0):
    return preco * quantidade - desconto

calcular_total(25, 3, desconto=5)

Qual alternativa separa corretamente configuração e argumentos da chamada?

Passo 2 de 6

Montar a fábrica, o decorador e o wrapper

Organize um decorador parametrizado em três camadas: configurar, receber a função original e envolver cada chamada.

Três camadas, três responsabilidades

A estrutura do decorador parametrizado

Em um decorador parametrizado, uma função externa atua como fábrica: ela recebe a configuração, como rotulo. Dentro dela, o decorador recebe a função original. Por fim, o wrapper usa o rótulo capturado, adiciona o comportamento desejado e encaminha a chamada.

A fábrica retorna o decorador; o decorador retorna o wrapper. O wrapper é o objeto que ficará associado ao nome da função decorada.

Fluxo de referências entre as camadas

Diagrama de três caixas aninhadas: a fábrica recebe um rótulo e retorna o decorador; o decorador recebe a função original e retorna o wrapper; o wrapper consulta o rótulo, recebe argumentos e chama a função original.

O rótulo pertence ao ambiente da fábrica e pode ser consultado pelo wrapper interno.

Implementação completa

Leia de fora para dentro

O rotulo é recebido uma única vez pela fábrica e permanece disponível para o wrapper como variável capturada. @wraps(funcao) fica imediatamente antes do wrapper, usando a função original como referência.

registrar_chamadas parametrizado

python
from functools import wraps


def registrar_chamadas(rotulo):
    def decorador(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            print(f"[{rotulo}] chamando {funcao.__name__}")
            return funcao(*args, **kwargs)

        return wrapper

    return decorador


@registrar_chamadas(rotulo="RELATÓRIO")
def somar(a, b):
    """Devolve a soma de dois números."""
    return a + b


resultado = somar(2, 3)
print(resultado)

O que cada retorno entrega

Exemplo

Encadeamento sem antecipar a função original

registrar_chamadas("RELATÓRIO") produz um decorador configurado.

Esse decorador recebe somar e produz wrapper.

Assim, a decoração pode ser entendida como:

somar = registrar_chamadas("RELATÓRIO")(somar)

Os return da fábrica e do decorador devolvem funções. O return funcao(*args, **kwargs) do wrapper devolve o resultado da chamada encaminhada.

Dica

Preserve a interface

Mesmo que o wrapper acrescente um registro, ele deve encaminhar *args e **kwargs e devolver o resultado recebido. Com @wraps(funcao), metadados como nome e docstring também continuam associados à função decorada.

Complete as camadas

Parâmetro da fábrica

Complete o parâmetro da fábrica:

def registrar_chamadas(____):
    ...

Retorno do decorador

Complete o retorno do decorador:

def decorador(funcao):
    def wrapper(*args, **kwargs):
        return funcao(*args, **kwargs)

    return ____

Posição de wraps

Complete a linha que preserva os metadados:

def decorador(funcao):
    ____
    def wrapper(*args, **kwargs):
        return funcao(*args, **kwargs)

Passo 3 de 6

Acompanhar a definição e as chamadas

Entenda em que momento a fábrica, o decorador e o wrapper atuam em uma função decorada com configuração.

A sintaxe @ em forma explícita

O que acontece na definição

Em uma definição como @registrar_chamadas(rotulo="RELATÓRIO"), os parênteses chamam a fábrica, não a função que será decorada.

A fábrica devolve um decorador. Em seguida, esse decorador recebe a função recém-criada e devolve o wrapper, que passa a ficar associado ao nome gerar_relatorio.

Duas formas equivalentes

python
@registrar_chamadas(rotulo="RELATÓRIO")
def gerar_relatorio(mes):
    print(f"Relatório de {mes}")

# Equivale a:
def gerar_relatorio(mes):
    print(f"Relatório de {mes}")

decorador = registrar_chamadas(rotulo="RELATÓRIO")
gerar_relatorio = decorador(gerar_relatorio)

Dois momentos diferentes

Diagrama em duas faixas: na definição, a configuração entra na fábrica e o decorador transforma a função original em wrapper; nas chamadas posteriores, cada chamada passa pelo wrapper antes de chegar à função original.

A fábrica e o decorador atuam na definição; o wrapper atua nas chamadas posteriores.

Linha do tempo do exemplo

Mensagens para observar a ordem

python
def registrar_chamadas(rotulo):
    print("1. fábrica")

    def decorador(funcao):
        print("2. decorador")

        def wrapper(*args, **kwargs):
            print(f"3. wrapper: {rotulo}")
            return funcao(*args, **kwargs)

        return wrapper

    return decorador


@registrar_chamadas(rotulo="RELATÓRIO")
def gerar_relatorio(mes):
    print(f"4. função original: {mes}")


print("5. após a definição")
gerar_relatorio("janeiro")
gerar_relatorio("fevereiro")

Leia por fases

Ao executar a definição decorada, aparecem 1. fábrica e 2. decorador. O wrapper é criado e associado a gerar_relatorio, mas o corpo da função original ainda não roda.

Depois aparece 5. após a definição. Em cada chamada, o wrapper imprime a mensagem 3 e então chama a função original, que imprime a mensagem 4. A fábrica não é chamada outra vez automaticamente.

Dica

Regra de previsão

Separe mentalmente dois períodos: definição cria e aplica a decoração; chamada executa o wrapper e, neste caso, a função original. Duas chamadas produzem duas execuções do wrapper e da função original.

Organize os eventos

Uma definição e uma chamada

Considere o código anterior. Coloque estes eventos na ordem em que ocorrem até o término da primeira chamada gerar_relatorio("janeiro").

  1. A fábrica é executada com o rótulo "RELATÓRIO".
  2. O decorador recebe a função original e devolve o wrapper.
  3. O programa executa o print após a definição.
  4. A primeira chamada entra no wrapper e depois executa a função original.

Preveja a repetição nas chamadas

Duas chamadas posteriores

Depois que a definição decorada terminou, gerar_relatorio("janeiro") e gerar_relatorio("fevereiro") são executadas. Qual evento ocorre duas vezes?

Passo 4 de 6

Validar e conservar a configuração

Valide o rótulo no momento em que a fábrica é chamada e use a versão normalizada nas chamadas posteriores.

Contrato e momento da validação

Valide antes de decorar

Para este exemplo, rotulo deve ser uma string que continue não vazia após remover os espaços das extremidades.

A fábrica é o lugar certo para aplicar esse contrato: ela recebe a configuração antes de retornar o decorador. Assim, um tipo incompatível gera TypeError; uma string que vira vazia após strip() gera ValueError.

Com @registrar_chamadas(rotulo=" "), a falha ocorre enquanto a definição decorada está sendo executada. O wrapper e a função original ainda não foram chamados.

Caminho da configuração

A configuração atravessa a validação uma única vez, antes de a função decorada ficar disponível.

Diagrama mostrando um valor de configuração entrando em uma fábrica, passando por uma barreira de validação e seguindo como valor estabilizado para um decorador e um wrapper; uma função original permanece sem execução ao lado.

A validação pertence à fábrica; as chamadas posteriores consultam o rótulo que ela aprovou e normalizou.

Normalizar e capturar o rótulo

Fábrica com contrato explícito

A variável rotulo_normalizado é criada após as validações. O wrapper a consulta quando cada chamada acontece.

python
from functools import wraps


def registrar_chamadas(rotulo):
    if not isinstance(rotulo, str):
        raise TypeError("rotulo deve ser uma string")

    rotulo_normalizado = rotulo.strip()
    if not rotulo_normalizado:
        raise ValueError("rotulo não pode ficar vazio")

    def decorador(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            print(f"[{rotulo_normalizado}] chamando {funcao.__name__}")
            return funcao(*args, **kwargs)

        return wrapper

    return decorador


@registrar_chamadas("  cálculo  ")
def somar(a, b):
    return a + b

print(somar(2, 3))
# [cálculo] chamando somar
# 5

Atenção

Captura não é cópia automática

Neste caso, o rótulo normalizado é uma string e o wrapper apenas o consulta; isso conserva a configuração textual escolhida. Mas uma closure captura vínculos, não faz cópias automáticas de objetos mutáveis. Não conclua que qualquer configuração mutável ficaria isolada só por estar em uma closure.

Exemplo

Falha antes da chamada

Este código falha ao tentar criar a função decorada:

<code>@registrar_chamadas(" ")
def enviar():
print("enviando")</code>

Após strip(), o rótulo é vazio. A fábrica lança ValueError, portanto enviar não é disponibilizada e seu corpo não executa.

Verifique o local da falha

Associe cada configuração ao resultado

Relacione cada valor passado para rotulo ao resultado da fábrica.

Toque em um item e depois no par correspondente.

Explique a sequência

Por que @registrar_chamadas(" ") pode gerar um erro mesmo que ninguém chame a função decorada?

Escreva pelo menos 40 caracteres (0/40).

Passo 5 de 6

Isolar configuração e estado por função

Escolha onde criar um contador para que ele tenha o tempo de vida desejado: por função decorada, compartilhado por um decorador reutilizado ou reiniciado a cada chamada.

Um contador para cada função decorada

Configuração pode ser compartilhada; contador, não

Cada chamada a registrar_chamadas(...) cria um novo ambiente de configuração. Assim, chamadas distintas podem ter rótulos distintos.

Também é possível reutilizar o mesmo decorador retornado pela fábrica. Nesse caso, o rótulo capturado pode ser compartilhado intencionalmente. Para que cada função ainda tenha sua própria contagem, crie chamadas = 0 dentro de decorador, antes de definir o wrapper.

O wrapper consulta o rótulo e atualiza somente o contador associado à função que ele envolveu.

Escopos capturados e contadores

O diagrama mostra que um decorador reutilizado pode fornecer o mesmo rótulo a dois wrappers, sem obrigá-los a compartilhar o contador.

Diagrama de uma fábrica de decorador com rótulo compartilhado e dois wrappers separados, cada um com seu próprio contador de chamadas.

O rótulo pertence ao ambiente da fábrica; cada contador pertence à execução individual do decorador sobre uma função.

Decorador reutilizado, contagens independentes

Observe onde chamadas é inicializado: dentro de decorador.

python
from functools import wraps


def registrar_chamadas(rotulo):
    def decorador(funcao):
        chamadas = 0

        @wraps(funcao)
        def wrapper(*args, **kwargs):
            nonlocal chamadas
            chamadas += 1
            print(f"[{rotulo}] chamada {chamadas}: {funcao.__name__}")
            return funcao(*args, **kwargs)

        return wrapper

    return decorador


registrar_tarefa = registrar_chamadas("TAREFA")


@registrar_tarefa
def enviar(nome):
    return f"Envio de {nome} iniciado"


@registrar_tarefa
def cancelar(nome):
    return f"Envio de {nome} cancelado"


print(enviar("relatorio.pdf"))
print(cancelar("rascunho.txt"))
print(enviar("dados.csv"))
print(cancelar("imagem.png"))

# [TAREFA] chamada 1: enviar
# [TAREFA] chamada 1: cancelar
# [TAREFA] chamada 2: enviar
# [TAREFA] chamada 2: cancelar

O local da inicialização define o tempo de vida

Três locais, três comportamentos

O contador só persiste enquanto o ambiente que o contém continuar acessível:

  • Na fábrica: há um contador por chamada à fábrica. Se você reutilizar o decorador retornado para várias funções, elas compartilharão esse contador.
  • No decorador: há um contador por função decorada. É o local adequado para contagens independentes no exemplo.
  • No wrapper: um novo contador nasce a cada chamada. Ele será incrementado para 1 e descartado logo depois.

Duas posições que não atendem ao objetivo

Estes trechos destacam os efeitos de colocar o contador em outro escopo.

python
# 1. Contador na fábrica: compartilhado se o mesmo decorador for reutilizado.
def registrar_compartilhado(rotulo):
    chamadas = 0

    def decorador(funcao):
        def wrapper(*args, **kwargs):
            nonlocal chamadas
            chamadas += 1
            return funcao(*args, **kwargs)
        return wrapper
    return decorador


# 2. Contador no wrapper: reinicia em toda chamada.
def registrar_reiniciado(rotulo):
    def decorador(funcao):
        def wrapper(*args, **kwargs):
            chamadas = 0
            chamadas += 1  # sempre resulta em 1
            return funcao(*args, **kwargs)
        return wrapper
    return decorador

Relacione escopo e efeito

Onde o contador deve nascer?

Associe cada local de inicialização ao comportamento do contador quando um decorador retornado é reutilizado em duas funções.

Toque em um item e depois no par correspondente.

Justifique a escolha

Uma função, uma contagem

Você reutiliza registrar_tarefa para decorar enviar e cancelar, mas quer que ambas comecem em chamada 1 e avancem de forma independente. Em qual camada você inicializa chamadas? Justifique usando o momento em que essa camada é executada.

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 6

Aplicar um decorador parametrizado completo

Integre validação, configuração capturada, preservação de metadados e contagem independente por função em um decorador parametrizado.

Uma estrutura, duas funções independentes

Checklist da implementação

Neste exemplo, a fábrica recebe e valida o rótulo. O decorador recebe cada função. Dentro dele, o contador nasce uma vez para aquela função decorada; o wrapper o atualiza a cada chamada, registra a mensagem e devolve o resultado original.

Assim, somar e saudar terão rótulos e contagens próprios, mesmo quando suas chamadas forem intercaladas.

Onde cada valor fica

O diagrama mostra os ambientes criados para duas aplicações do mesmo padrão de decorador.

Diagrama de duas funções decoradas separadamente. Cada uma possui um rótulo capturado e um contador próprio, enquanto os argumentos chegam apenas ao wrapper durante cada chamada.

O rótulo é configurado na fábrica; o contador é criado no decorador para pertencer a uma única função decorada.

Local do contador

Para que cada função decorada tenha sua própria contagem persistente, crie quantidade = 0 dentro da função ____.

Script completo para executar

Pratique no seu computador

Crie um arquivo chamado decorador_completo.py, copie o código e execute python decorador_completo.py no terminal, na pasta do arquivo. Não há dependências externas.

Antes de executar, observe: um rótulo inválido falharia ao aplicar @registrar_chamadas(...), isto é, durante a definição decorada. Já 2 e 3, por exemplo, são argumentos que chegam a somar somente quando ela é chamada.

decorador_completo.py

Fábrica, decorador e wrapper integrados.

python
from functools import wraps


def registrar_chamadas(rotulo):
    if not isinstance(rotulo, str):
        raise TypeError("rotulo deve ser uma string")

    rotulo = rotulo.strip()
    if not rotulo:
        raise ValueError("rotulo não pode ficar vazio")

    def decorar(funcao):
        quantidade = 0

        @wraps(funcao)
        def wrapper(*args, **kwargs):
            nonlocal quantidade
            quantidade += 1
            print(f"[{rotulo}] chamada {quantidade}")
            return funcao(*args, **kwargs)

        return wrapper

    return decorar


@registrar_chamadas("Cálculo")
def somar(a, b):
    return a + b


@registrar_chamadas("Saudação")
def saudar(nome):
    return f"Olá, {nome}!"


print(somar(2, 3))
print(saudar("Lia"))
print(somar(10, 5))
print(saudar("Ravi"))
print(somar.__name__)
print(saudar.__name__)

Confira o comportamento observado

Exemplo

Saída esperada

[Cálculo] chamada 1
5
[Saudação] chamada 1
Olá, Lia!
[Cálculo] chamada 2
15
[Saudação] chamada 2
Olá, Ravi!
somar
saudar

As mensagens mostram contagens independentes. As duas últimas linhas confirmam que @wraps(funcao) preservou os nomes das funções.

Relate sua execução

Execute o script. Quais rótulos e números de chamada você observou nas chamadas intercaladas? Inclua também os resultados devolvidos por somar e saudar.

Escreva pelo menos 80 caracteres (0/80).

Síntese para o próximo decorador

Resumo

Decisões essenciais

Use esta sequência ao criar outro decorador parametrizado:

  • A fábrica recebe, valida e normaliza a configuração antes de retornar o decorador.
  • O decorador recebe uma função original e é o local para criar estado exclusivo daquela função, como quantidade = 0.
  • O wrapper recebe os argumentos de cada chamada, consulta a configuração capturada, atualiza o estado necessário e retorna o resultado da função original.
  • @wraps(funcao) fica no wrapper interno para preservar metadados da função original.
  • Argumentos como somar(2, 3) pertencem à chamada posterior; eles não reconfiguram o rótulo definido em @registrar_chamadas("Cálculo").

Aplicação final

Por que somar e saudar começam ambas na chamada 1, apesar de usarem o mesmo padrão registrar_chamadas?

Tutorial concluído

Parabéns! Você concluiu: Criar decoradores parametrizados

Muito bem! Você consegue separar configuração, decoração e chamada; validar no momento certo; preservar metadados; e escolher o escopo adequado para um estado independente por função.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso