Trilha de aprendizado · Nível 9 · Tutorial 6

Criar decoradores com functools.wraps

Ao concluir, você será capaz de acrescentar comportamento a funções por meio de decoradores, preservando o encaminhamento das chamadas e os principais metadados da função original.

  • Nível: Intermediário
  • Duração: 25 min
  • 7 passos
Criar decoradores com functools.wraps

O que você vai percorrer

  1. Acrescentar comportamento com uma função envoltória Crie um decorador simples que adiciona uma mensagem antes de chamar uma função, mantendo a responsabilidade do cálculo separada. 3 min
  2. Aplicar @ e separar definição de chamada Entenda quando o decorador é aplicado e quando o wrapper é executado nas chamadas posteriores. 3 min
  3. Encaminhar argumentos e devolver o resultado Generalize o wrapper para encaminhar qualquer combinação de argumentos e preservar o valor devolvido pela função original. 3 min
  4. Preservar as exceções da função original Acrescente ações a um wrapper sem esconder falhas que fazem parte do contrato da função original. 3 min
  5. Preservar metadados com functools.wraps Use functools.wraps para que uma função decorada continue apresentando os metadados da função que ela envolve. 5 min
  6. Entender dois decoradores empilhados Veja como duas camadas de decoradores são aplicadas e executadas, mantendo os metadados em cada wrapper. 4 min
  7. Aplicar e revisar um decorador completo Pratique em um único script um decorador que encaminha chamadas, preserva resultados, exceções e metadados, combinado com uma segunda camada. 5 min

O que você vai aprender

  • Implementar um decorador que receba uma função e retorne uma função envoltória.
  • Aplicar a sintaxe @ e distinguir a aplicação do decorador da execução da função decorada.
  • Encaminhar argumentos, devolver o resultado e preservar a propagação de exceções quando esse for o comportamento esperado.
  • Usar functools.wraps para preservar metadados e manter o acesso à função envolvida.
  • Determinar a ordem de aplicação e de execução de dois decoradores empilhados.

Antes de começar

  • Criar closures com estado e nonlocal
  • Receber argumentos variáveis com *args e **kwargs
  • Documentar funções com docstrings e tipos simples
  • Tratar exceções com try, except, else e finally
  • Separar código em módulos e usar importações explícitas

Passo 1 de 7

Acrescentar comportamento com uma função envoltória

Crie um decorador simples que adiciona uma mensagem antes de chamar uma função, mantendo a responsabilidade do cálculo separada.

O decorador devolve uma nova função

Uma camada em volta da chamada

Um decorador recebe uma função e devolve o objeto que ficará associado ao nome usado pelo programa. Neste primeiro modelo, ele devolve outra função: o wrapper.

Como em uma closure, o wrapper interno continua acessando a função recebida, mesmo depois que o decorador termina. Assim, ele pode executar uma ação extra e então chamar a função original.

Referências após decorar

O nome público passa a apontar para o wrapper; o wrapper mantém uma referência privada para a função de cálculo original.

Diagrama de referências: o nome calcular_total aponta para a função wrapper, e wrapper aponta para a função original calcular_total.

A função original continua existindo; o nome público foi reassociado ao wrapper.

Construa a função envoltória

Assinatura fixa neste exemplo

Comece com uma função de cálculo que recebe dois valores. O decorador recebe essa função em funcao e cria wrapper, também com dois parâmetros. Antes de encaminhar a chamada, o wrapper exibe uma mensagem.

Decorador simples

Observe que a última linha do decorador devolve a função wrapper, sem parênteses.

python
def avisar_antes(funcao):
    def wrapper(preco, quantidade):
        print("Calculando o total...")
        return funcao(preco, quantidade)

    return wrapper


def calcular_total(preco, quantidade):
    return preco * quantidade

Dica

Função ou resultado?

return wrapper entrega a função para uso posterior. Já return wrapper(...) a executaria naquele momento e devolveria seu resultado. Um decorador precisa devolver o wrapper, não o resultado de uma chamada dele.

Aplique explicitamente e observe

Reassocie o nome

A aplicação explícita chama o decorador com a função original e guarda o wrapper no mesmo nome público. Depois disso, chamar calcular_total(12, 3) entra primeiro no wrapper; ele mostra a mensagem e chama a função de cálculo.

Aplicação e chamada

Acrescente este trecho abaixo do código anterior e execute o script.

python
calcular_total = avisar_antes(calcular_total)

resultado = calcular_total(12, 3)
print(resultado)

# Saída:
# Calculando o total...
# 36

Exemplo

Responsabilidades separadas

calcular_total sabe multiplicar preço por quantidade. avisar_antes sabe acrescentar um aviso antes de uma chamada. O wrapper combina esses comportamentos sem colocar o print dentro da função de cálculo.

Organize o fluxo do decorador

Qual é a sequência correta?

Organize os eventos da aplicação explícita até a obtenção do resultado.

  1. Devolver `wrapper` sem executá-lo e reassociar o nome `calcular_total`.
  2. O wrapper mostra a mensagem, chama a função original e devolve 36.
  3. Criar o `wrapper` que mantém acesso à função recebida.
  4. Chamar `calcular_total(12, 3)`, que agora executa o wrapper.
  5. Passar a função `calcular_total` para `avisar_antes`.

Passo 2 de 7

Aplicar @ e separar definição de chamada

Entenda quando o decorador é aplicado e quando o wrapper é executado nas chamadas posteriores.

@ é uma aplicação durante a definição

Açúcar sintático para uma aplicação explícita

A sintaxe @anunciar acima de uma função é uma forma compacta de aplicar o decorador assim que a instrução def é executada.

Estas duas formas têm o mesmo efeito operacional:

<code>@anunciar
def calcular_total():
...</code>

<code>def calcular_total():
...

calcular_total = anunciar(calcular_total)</code>

Depois dessa aplicação, o nome calcular_total passa a se referir ao wrapper retornado por anunciar.

Decorador aplicado com @

Execute este script para observar os momentos distintos.

python
def anunciar(funcao):
    print("DECORAR")

    def wrapper():
        print("ANTES")
        return funcao()

    return wrapper

@anunciar
def cumprimentar():
    print("FUNÇÃO ORIGINAL")

print("--- primeira chamada ---")
cumprimentar()

print("--- segunda chamada ---")
cumprimentar()

Dois momentos no script

Aplicar não é chamar

Ao chegar à definição decorada, Python cria a função cumprimentar e executa anunciar(cumprimentar). Por isso, DECORAR aparece uma única vez.

Nesse momento, o corpo de cumprimentar ainda não é executado. Ele só será chamado pelo wrapper quando você executar cumprimentar().

Linha do tempo da definição e das chamadas

A aplicação troca a referência do nome; cada chamada posterior percorre o wrapper e então alcança a função original.

Diagrama em linha do tempo mostrando uma definição de função decorada, uma aplicação única do decorador que retorna o wrapper e duas chamadas separadas que passam pelo wrapper antes de chegar à função original.

A decoração ocorre uma vez na definição; o wrapper é executado uma vez por chamada.

Preveja a saída

Sequência de marcadores

Qual é a sequência de linhas impressas pelo script apresentado?

Passo 3 de 7

Encaminhar argumentos e devolver o resultado

Generalize o wrapper para encaminhar qualquer combinação de argumentos e preservar o valor devolvido pela função original.

O caminho completo da chamada

Encaminhe e devolva

Um wrapper útil não conhece antecipadamente a assinatura de cada função que poderá envolver. Por isso, ele recebe *args e **kwargs, repassa esses valores para a função capturada e devolve seu resultado.

O contrato aqui é simples: para cada chamada ao wrapper, a função original é chamada uma vez com os mesmos argumentos. O valor produzido volta ao chamador.

Ida dos argumentos, volta do resultado

Observe que o wrapper fica no meio do percurso: ele recebe a chamada, pode acrescentar uma ação e não deve interromper o fluxo de argumentos nem do resultado.

Diagrama mostrando chamador enviando argumentos posicionais e nomeados para um wrapper; o wrapper encaminha os mesmos argumentos para a função original; o resultado retorna pela mesma cadeia até o chamador.

Os argumentos seguem para dentro; o resultado retorna para fora.

Um wrapper genérico

Encaminhamento com retorno

A ação antes da chamada é adicionada pelo wrapper. A função calcular_total continua responsável apenas pelo cálculo.

python
def registrar_chamada(funcao):
    def wrapper(*args, **kwargs):
        print("Iniciando cálculo")
        resultado = funcao(*args, **kwargs)
        print("Cálculo concluído")
        return resultado
    return wrapper

@registrar_chamada
def calcular_total(preco, quantidade=1, desconto=0):
    return preco * quantidade - desconto

valor = calcular_total(50, quantidade=2, desconto=10)
print(valor)  # 90

Exemplo

O que acontece nessa chamada

Na chamada calcular_total(50, quantidade=2, desconto=10):

  1. O wrapper recebe args = (50,) e kwargs = {"quantidade": 2, "desconto": 10}.
  2. Ele chama a função original uma única vez com esses mesmos valores.
  3. A função original produz 90.
  4. O wrapper exibe a mensagem final e devolve 90.

*args e **kwargs não tornam argumentos inválidos válidos: se a função original rejeitaria uma chamada, ela continuará rejeitando-a.

Não perca o valor de retorno

Chamar não é devolver

Se o wrapper apenas executar funcao(*args, **kwargs), a função original pode calcular corretamente, mas quem chamou a função decorada receberá None. Toda função que termina sem return devolve None.

Guarde o resultado em uma variável quando precisar fazer algo depois da chamada; então, devolva essa variável.

Versão que perde o resultado

A função original retorna 14, mas o wrapper não devolve esse valor.

python
def anunciar(funcao):
    def wrapper(*args, **kwargs):
        print("Chamando função")
        funcao(*args, **kwargs)
    return wrapper

@anunciar
def dobrar(numero):
    return numero * 2

resultado = dobrar(7)
print(resultado)  # None

Dica

Regra prática

Use return funcao(*args, **kwargs) quando nada precisar acontecer após a chamada. Se houver uma ação posterior, use resultado = ..., execute a ação e termine com return resultado.

Complete o wrapper

Encaminhar e retornar

Complete o trecho para que medir receba os argumentos originais e devolva o resultado da função:

def medir(funcao):
    def wrapper(*args, **kwargs):
        print("Antes")
        resultado = funcao(____)
        print("Depois")
        ____ resultado
    return wrapper

Passo 4 de 7

Preservar as exceções da função original

Acrescente ações a um wrapper sem esconder falhas que fazem parte do contrato da função original.

A exceção pode atravessar o wrapper

Não capturar também é uma escolha

Se o wrapper chama a função original e não usa try/except, uma exceção lançada pela função original atravessa o wrapper naturalmente até quem fez a chamada. Assim, o comportamento acrescentado antes da chamada ocorre, mas a falha continua visível para o chamador.

Dois caminhos da mesma chamada

O wrapper pode iniciar uma ação e depois encaminhar a chamada. O resultado volta apenas no caminho de sucesso; uma exceção segue para fora no caminho de erro.

Diagrama mostrando chamador, wrapper e função original. Uma chamada passa pelo wrapper até a função; um caminho verde retorna um resultado ao chamador e um caminho vermelho leva uma exceção da função original ao chamador.

Sem captura no wrapper, a exceção não é convertida nem interrompida por ele.

Wrapper que preserva a falha

A mensagem anterior é exibida tanto para chamadas que dão certo quanto para chamadas que falham.

python
def registrar(funcao):
    def wrapper(*args, **kwargs):
        print("Iniciando chamada")
        return funcao(*args, **kwargs)
    return wrapper

@registrar
def dividir(total, quantidade):
    return total / quantidade

print(dividir(10, 2))  # 5.0
print(dividir(10, 0))  # ZeroDivisionError continua chegando ao chamador

Após sucesso ou sempre ao encerrar

A posição da ação define quando ela ocorre

Uma instrução logo depois de funcao(*args, **kwargs) só é alcançada se a chamada terminar normalmente. Já um bloco finally executa ao encerrar a tentativa, tanto após um resultado quanto durante a propagação de uma exceção.

Registrar encerramento sem alterar o desfecho

Guarde o resultado, faça a ação de encerramento e devolva o mesmo valor. Se houver erro, o finally registra o encerramento e a exceção continua sendo propagada.

python
def registrar_encerramento(funcao):
    def wrapper(*args, **kwargs):
        print("Antes da chamada")
        try:
            resultado = funcao(*args, **kwargs)
            print("Chamada concluída com sucesso")
            return resultado
        finally:
            print("Chamada encerrada")
    return wrapper

Não substitua o contrato sem querer

Atenção

Cuidado com retornos no except e no finally

Capturar ZeroDivisionError e devolver 0 faz a função decorada deixar de sinalizar divisão por zero: isso altera seu contrato. Do mesmo modo, um return ou uma nova exceção no finally pode substituir o resultado ou ocultar a falha original.

Capturar para registrar e relançar

Se você precisa agir especificamente no caminho de erro, registre a informação e use raise sem argumento para relançar a mesma exceção.

python
def registrar_falha(funcao):
    def wrapper(*args, **kwargs):
        try:
            return funcao(*args, **kwargs)
        except Exception:
            print("A chamada falhou")
            raise
    return wrapper

Escolha a estrutura adequada

Qual wrapper registra o encerramento, devolve o resultado em caso de sucesso e preserva uma exceção da função original?

Passo 5 de 7

Preservar metadados com functools.wraps

Use functools.wraps para que uma função decorada continue apresentando os metadados da função que ela envolve.

O que muda quando o nome aponta para o wrapper

Metadados não desapareceram

Quando você decora uma função, o nome público passa a apontar para o wrapper. Sem um cuidado adicional, consultas como __name__ e __doc__ descrevem o wrapper, não a função original.

Isso é uma perda aparente: a função original continua existindo, capturada pelo closure. O que mudou foi o objeto associado ao nome decorado.

Referências após a decoração

Sem wraps, o nome somar referencia o wrapper; o wrapper mantém uma referência interna à função original.

Diagrama mostrando o nome público somar apontando para wrapper, e wrapper apontando para a função original somar.

O nome público passa a acessar o wrapper; a função original continua acessível dentro dele.

Exemplo

Wrapper sem preservação

def registrar(funcao):
    def wrapper(*args, **kwargs):
        print("chamada registrada")
        return funcao(*args, **kwargs)
    return wrapper

@registrar
def somar(a: int, b: int) -> int:
    """Devolve a soma de dois inteiros."""
    return a + b

print(somar.__name__)  # wrapper
print(somar.__doc__)   # None

A docstring e as anotações continuam na função original, mas somar agora é o objeto wrapper.

Copie os metadados com wraps

Aplique wraps ao wrapper

Importe wraps explicitamente e use @wraps(funcao) imediatamente acima da definição do wrapper. Assim, wraps atualiza os principais metadados do wrapper a partir da função recebida.

Decorador com metadados preservados

O encaminhamento de argumentos e o retorno continuam sendo responsabilidade do wrapper.

python
from functools import wraps


def registrar(funcao):
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print("chamada registrada")
        return funcao(*args, **kwargs)

    return wrapper


@registrar
def somar(a: int, b: int) -> int:
    """Devolve a soma de dois inteiros."""
    return a + b


print(somar.__name__)  # somar
print(somar.__doc__)   # Devolve a soma de dois inteiros.
print(somar.__annotations__)  # {'a': int, 'b': int, 'return': int}
print(somar(2, 3))     # chamada registrada\n5

Dica

Responsabilidades separadas

wraps preserva metadados, mas não conserta a lógica do wrapper. Ainda é necessário encaminhar *args e **kwargs, devolver o resultado e não ocultar exceções quando elas devem se propagar.

A referência __wrapped__

Acesso à função envolvida

Além de atualizar metadados como nome, documentação e anotações, @wraps(funcao) cria __wrapped__. Esse atributo referencia a função imediatamente envolvida pela camada atual.

Por isso, somar.__wrapped__(2, 3) chama a função original diretamente e não executa a mensagem registrada pelo wrapper. É útil para acessar a camada envolvida, mas não substitui a interface decorada.

Duas formas de chamar

A chamada pública percorre o wrapper. A chamada por __wrapped__ pula somente o comportamento acrescentado por essa camada.

Diagrama comparando uma chamada pública que passa pelo wrapper antes de alcançar a função original, com uma chamada usando __wrapped__ que vai diretamente à função original.

__wrapped__ desvia da camada atual; a interface decorada continua sendo o nome público.

Associe cada atributo ao seu papel

Relacione o atributo ao efeito esperado após aplicar @wraps(funcao).

Toque em um item e depois no par correspondente.

Verifique o que wraps resolve

Explique a diferença observada

Compare mentalmente um decorador que usa @wraps(funcao) com outro que não usa. Por que __name__ e __doc__ podem mudar? Cite também uma responsabilidade que continua sendo do código do wrapper.

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 7

Entender dois decoradores empilhados

Veja como duas camadas de decoradores são aplicadas e executadas, mantendo os metadados em cada wrapper.

Duas camadas, uma composição

A aplicação acontece de baixo para cima

Quando há dois decoradores, o que está mais perto da definição recebe primeiro a função original. Assim:

@a
@b
def funcao():
    ...

é equivalente a:

funcao = a(b(funcao))

Primeiro, b recebe a função original e devolve seu wrapper. Depois, a recebe esse wrapper e devolve a camada externa associada ao nome funcao.

Camadas após a decoração

O nome público aponta para o wrapper criado por a; ele envolve o wrapper de b, que por sua vez envolve a função original.

Diagrama de três camadas aninhadas: nome público aponta para wrapper A, que aponta para wrapper B, que aponta para a função original.

Aplicação: função original → b → a. O resultado final é a camada externa de a.

Código e sequência de uma chamada

Dois decoradores com marcadores fixos

Execute este exemplo em um arquivo Python para observar a sequência.

python
from functools import wraps


def a(funcao):
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print("a: antes")
        resultado = funcao(*args, **kwargs)
        print("a: depois")
        return resultado

    return wrapper


def b(funcao):
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print("b: antes")
        resultado = funcao(*args, **kwargs)
        print("b: depois")
        return resultado

    return wrapper


@a
@b
def calcular(valor):
    """Devolve o dobro do valor."""
    print("função original")
    return valor * 2


print(calcular(4))

Entrada e saída da chamada

Na chamada calcular(4), entra-se pela camada externa e avança-se até a função original. Depois que ela retorna com sucesso, as ações posteriores acontecem no caminho de volta:

  1. a: antes
  2. b: antes
  3. função original
  4. b: depois
  5. a: depois
  6. 8

Os return dos dois wrappers encaminham o resultado até o chamador.

Confira a ordem observável

Ordene os eventos

Para o código anterior, coloque os cinco eventos internos na ordem em que são impressos por calcular(4).

  1. a: depois
  2. função original
  3. a: antes
  4. b: antes
  5. b: depois

Inverter e atravessar as camadas

A ordem escrita muda as camadas

Se você inverter os decoradores para @b sobre @a, a composição passa a ser b(a(funcao)). A entrada será por b e a saída terminará em b.

Como ambos usam @wraps(funcao), cada wrapper preserva os metadados da função que recebeu. Além disso, __wrapped__ aponta somente para a camada imediatamente envolvida.

O caminho de __wrapped__

Com @a sobre @b, uma remoção de camada não chega diretamente à função original.

Diagrama em camadas mostrando o nome calcular apontando para wrapper A; o atributo wrapped de A aponta para wrapper B; o atributo wrapped de B aponta para a função original.

calcular.__wrapped__ acessa o wrapper de b; um segundo .__wrapped__ alcança a função original.

Explique a inversão

Se o código fosse alterado para @b acima de @a, explique a ordem dos marcadores em uma chamada bem-sucedida. Depois, diga para qual objeto calcular.__wrapped__ apontaria nesse arranjo.

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 7

Aplicar e revisar um decorador completo

Pratique em um único script um decorador que encaminha chamadas, preserva resultados, exceções e metadados, combinado com uma segunda camada.

O contrato completo de uma camada

O que seu decorador deve preservar

Nesta prática, a função decorada continuará aceitando os mesmos argumentos, devolverá o mesmo resultado e deixará exceções chegarem ao chamador. A camada extra apenas registra eventos.

Use @wraps(funcao) no wrapper: assim, o nome público conserva os metadados da função de cálculo e aponta para ela por meio de __wrapped__.

Fluxo de uma chamada decorada

A chamada atravessa as camadas, chega à função original e retorna pelo caminho inverso. Se a função original falhar, a exceção também atravessa as camadas que não a capturam.

Diagrama com chamador à esquerda, camada marcar externa, camada registrar interna e função dividir à direita. Setas de argumentos seguem para dentro; seta de resultado volta para fora. Um caminho alternativo de exceção sai da função dividir e volta ao chamador.

Argumentos entram pela camada externa; resultado ou exceção retorna ao chamador.

Prática local: complete o decorador

Crie e execute um script

No seu editor, crie um arquivo chamado pratica_decorador.py. Copie o código abaixo e complete somente o corpo de registrar no local indicado. Depois, execute python pratica_decorador.py no terminal da pasta do arquivo.

O decorador marcar já está pronto e permite observar a composição das duas camadas.

Código para completar

python
from functools import wraps


def registrar(funcao):
    # COMPLETE AQUI
    # Crie um wrapper com @wraps(funcao).
    # Ele deve imprimir antes, chamar funcao com *args e **kwargs,
    # imprimir depois e devolver o resultado.
    pass


def marcar(funcao):
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print("marca: entrada")
        try:
            return funcao(*args, **kwargs)
        finally:
            print("marca: saída")

    return wrapper


@marcar
@registrar
def dividir(total: float, quantidade: float = 1) -> float:
    """Divide total pela quantidade."""
    return total / quantidade


print("resultado:", dividir(10, quantidade=2))

try:
    dividir(10, quantidade=0)
except ZeroDivisionError as erro:
    print("erro chegou ao chamador:", type(erro).__name__)

print("nome:", dividir.__name__)
print("doc:", dividir.__doc__)
print("camada interna:", dividir.__wrapped__.__name__)
print("original:", dividir.__wrapped__.__wrapped__.__name__)

Compare com a referência

Solução de referência para registrar

Compare a sua implementação com esta versão. O print posterior só ocorre após uma chamada bem-sucedida; se funcao lançar uma exceção, não há valor para retornar e a exceção continua seu percurso.

python
def registrar(funcao):
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print("registro: antes")
        resultado = funcao(*args, **kwargs)
        print("registro: depois")
        return resultado

    return wrapper

Exemplo

Saída esperada e leitura

Na chamada válida, a sequência será:

marca: entrada
registro: antes
registro: depois
marca: saída
resultado: 5.0

Na chamada com quantidade=0, você verá marca: entrada, registro: antes, marca: saída e então erro chegou ao chamador: ZeroDivisionError. Não aparece registro: depois, pois a divisão não terminou.

Os últimos quatro prints confirmam dividir como nome e a docstring original. A primeira referência __wrapped__ alcança o wrapper de registrar; a segunda alcança a função original. A ordem observada confirma @marcar como camada externa e @registrar como camada interna.

Revisão final

Registre sua conferência

Em poucas linhas, registre: como seu wrapper encaminha argumentos e resultado; o que ocorreu na divisão por zero; como __name__, __doc__ e __wrapped__ se comportaram; e qual foi a ordem das duas camadas.

Escreva pelo menos 180 caracteres (0/180).

Resumo

Lista de verificação

Você integrou os elementos de um decorador não parametrizado.

  • Recebi uma função e devolvi um wrapper.
  • Encaminhei *args e **kwargs à função envolvida.
  • Devolvi o resultado original após acrescentar comportamento.
  • Não escondi a ZeroDivisionError.
  • Usei @wraps(funcao) para manter nome, documentação e __wrapped__.
  • Relacionei a saída à entrada na camada externa e à saída no retorno das camadas internas.

Tutorial concluído

Parabéns! Você concluiu: Criar decoradores com functools.wraps

Muito bem! Agora você consegue criar decoradores simples que preservam o contrato da função e seus metadados. No próximo tutorial, você verá como configurar decoradores parametrizados.

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