Trilha de aprendizado · Nível 12 · Tutorial 7

Definir classes genéricas com parâmetros de tipo

Criar classes reutilizáveis que preservem o tipo dos dados armazenados e explicar por que algumas substituições entre classes parametrizadas são inseguras.

  • Nível: Avançado
  • Duração: 24 min
  • 6 passos
Definir classes genéricas com parâmetros de tipo

O que você vai percorrer

  1. Vincular atributos e métodos ao parâmetro de tipo Use um único parâmetro de tipo para manter relacionados o valor recebido, o valor armazenado e os valores lidos ou substituídos em uma instância. 4 min
  2. Especializar a classe na construção Crie instâncias de uma classe genérica com tipos explícitos ou inferidos e confira o contrato resultante com mypy. 4 min
  3. Explicar a invariância pela possibilidade de escrita Entenda por que um contêiner mutável de gatos não pode ser usado livremente onde se espera um contêiner de animais. 5 min
  4. Reconhecer covariância em contratos de consulta Use contratos de consulta para aceitar elementos de subtipos com segurança, sem prometer que a coleção concreta é imutável. 3 min
  5. Relacionar a interface à variância inferida Examine o que uma classe expõe para prever se suas especializações podem ser covariantes ou precisam ser invariantes. 5 min
  6. Aplicação final: definir e verificar classes genéricas Integre armazenamento mutável e consulta covariante em um exemplo completo, verificando quais substituições o mypy aceita ou rejeita. 5 min

O que você vai aprender

  • Declarar uma classe genérica e usar seu parâmetro de tipo em atributos e métodos.
  • Especializar a classe explicitamente ou conferir a especialização inferida pelo construtor.
  • Diagnosticar incompatibilidades causadas pela mutabilidade de contêineres genéricos.
  • Distinguir casos práticos de invariância e covariância.

Antes de começar

  • Preservar relações de tipos em funções genéricas
  • Escolher contratos de coleções e iteradores
  • Criar objetos com classes e métodos de instância
  • Controlar atributos com property

Passo 1 de 6

Vincular atributos e métodos ao parâmetro de tipo

Use um único parâmetro de tipo para manter relacionados o valor recebido, o valor armazenado e os valores lidos ou substituídos em uma instância.

Um tipo que acompanha a instância

Uma classe, um contrato de tipo

No Python 3.12 ou superior, declare o parâmetro entre colchetes no nome da classe: class Caixa[T].

Esse T pertence à classe. Portanto, todos os métodos da mesma Caixa reutilizam o mesmo tipo: o valor que entra na criação é armazenado, lido e eventualmente substituído por outro valor compatível com aquele contrato.

O percurso de T pela Caixa

As quatro ocorrências de T abaixo representam a mesma relação de tipos dentro de cada instância.

Diagrama de uma caixa genérica: uma seta do valor recebido no inicializador para um atributo interno, uma seta do atributo para o retorno do método obter, e uma seta do método substituir para o atributo. Todas as setas estão marcadas com o mesmo símbolo T.

O mesmo T conecta entrada, armazenamento, leitura e substituição.

Classe genérica com leitura e escrita

Definição de Caixa

Python 3.12 ou superior

python
class Caixa[T]:
    def __init__(self, valor: T) -> None:
        self.valor: T = valor

    def obter(self) -> T:
        return self.valor

    def substituir(self, novo_valor: T) -> None:
        self.valor = novo_valor

Leia T como uma promessa única

A assinatura de __init__ diz que valor tem tipo T. O atributo self.valor guarda esse mesmo T; por isso, obter() devolve T e substituir() aceita T.

Não declare um novo parâmetro de tipo em cada método: isso permitiria que cada chamada escolhesse um tipo independente e quebraria a relação com o conteúdo já armazenado.

Dica

Contrato estático, não conversão

As anotações orientam o verificador estático, como o mypy. Elas não convertem valores nem validam automaticamente dados recebidos em tempo de execução.

Complete o vínculo de tipos

Uma única relação de tipo

Complete cada lacuna com o parâmetro de tipo da classe:

class Caixa[T]:
    def __init__(self, valor: T) -> None:
        self.valor: ____ = valor

    def obter(self) -> ____:
        return self.valor

    def substituir(self, novo_valor: ____) -> None:
        self.valor = novo_valor

Passo 2 de 6

Especializar a classe na construção

Crie instâncias de uma classe genérica com tipos explícitos ou inferidos e confira o contrato resultante com mypy.

Escolha ou deixe o construtor inferir T

Uma instância, uma especialização

Depois de declarar class Caixa[T], cada construção fixa uma especialização para aquela instância. Você pode escrevê-la explicitamente, como Caixa[str]("relatório"), ou deixar o verificador deduzi-la pelo argumento, como Caixa(42).

A especialização não é uma conversão de valor: ela é o contrato estático que liga o valor inicial, as leituras e futuras substituições.

Construções explícita e inferida

python
class Caixa[T]:
    def __init__(self, valor: T) -> None:
        self.valor = valor

    def ler(self) -> T:
        return self.valor

    def substituir(self, valor: T) -> None:
        self.valor = valor

# Especialização escrita explicitamente.
titulo = Caixa[str]("Relatório mensal")

# Especialização inferida a partir do argumento do construtor.
quantidade = Caixa(42)

reveal_type(titulo)             # mypy: Caixa[builtins.str]
reveal_type(titulo.ler())       # mypy: builtins.str
reveal_type(quantidade)         # mypy: Caixa[builtins.int]
reveal_type(quantidade.ler())   # mypy: builtins.int

O tipo percorre a instância

O mesmo tipo escolhido na construção determina o que entra e o que sai pelos métodos.

Diagrama comparando uma Caixa especializada em str e outra especializada em int. Em cada uma, uma seta sai do argumento do construtor para o valor armazenado, outra vai para o retorno de ler, e outra indica que substituir aceita o mesmo tipo.

Caixa[str] preserva str em toda a interface; Caixa[int] preserva int.

O contrato passa a valer nos métodos

Anotação também comunica intenção

Uma anotação no nome da variável torna explícito o domínio que a instância deve aceitar. Em codigo: Caixa[str], o verificador exige uma caixa de textos; isso restringe tanto o argumento inicial quanto os próximos argumentos de substituir.

O contexto da anotação também participa da verificação. Ele não transforma 10 em texto: uma construção incompatível continua sendo um erro.

Leitura e escrita seguem a especialização

python
codigo: Caixa[str] = Caixa("A-17")

texto = codigo.ler()          # tipo: str
codigo.substituir("B-08")    # válido
codigo.substituir(8)          # erro do mypy: esperado str

incompativel: Caixa[str] = Caixa(10)
# erro do mypy: Caixa[int] não é compatível com Caixa[str]

Dica

Tipo estático não valida em execução

Mypy analisa as anotações sem executar o programa. Em Python, as anotações não fazem Caixa[str] rejeitar automaticamente um valor inadequado em tempo de execução. Quando dados vêm de fora do programa, a validação de valores é uma preocupação separada.

Confira e aplique a especialização

Relacione cada construção ao contrato

Associe cada expressão à especialização ou ao efeito correto.

Toque em um item e depois no par correspondente.

Prática no seu computador

Em um arquivo local com Python 3.12+, copie a classe e os exemplos deste step. Execute python -m mypy seu_arquivo.py para ver os resultados de reveal_type. Depois, remova as linhas intencionalmente incompatíveis antes de executar o arquivo normalmente, pois reveal_type é usado para análise estática.

Relate sua verificação

No seu arquivo local, qual tipo mypy revelou para uma instância construída com texto e para a chamada a ler()? Relate também o que ocorreu ao tentar substituir o valor por outro tipo.

Escreva pelo menos 30 caracteres (0/30).

Passo 3 de 6

Explicar a invariância pela possibilidade de escrita

Entenda por que um contêiner mutável de gatos não pode ser usado livremente onde se espera um contêiner de animais.

Subtipagem não se transfere automaticamente

Duas relações diferentes

Se Gato herda de Animal, um Gato pode ser usado onde um Animal é esperado. Isso fala sobre objetos individuais.

Já a variância pergunta outra coisa: uma especialização genérica pode ocupar o lugar de outra? Mesmo que Gato seja subtipo de Animal, não conclua que Caixa[Gato] pode ser usada como Caixa[Animal].

O tipo de uma instância não muda durante essa análise. O verificador avalia se a substituição entre os dois contratos seria segura.

Objeto individual versus contêiner

Compare a relação segura entre os objetos com a substituição insegura entre contêineres mutáveis.

Diagrama comparando um gato como subtipo de animal, relação indicada como segura, com uma caixa de gatos apontando para uma caixa de animais, relação indicada como não permitida devido à possibilidade de escrita.

Gato é compatível com Animal; isso não torna automaticamente Caixa[Gato] compatível com Caixa[Animal].

A escrita que quebra a promessa

Um contraexemplo concreto

Considere uma Caixa[T] que permite ler e substituir seu valor. Se uma Caixa[Gato] fosse aceita como Caixa[Animal], uma função poderia gravar nela qualquer subtipo de Animal, inclusive um Cachorro.

Mas outra referência ainda poderia acreditar que aquela mesma caixa contém somente Gato. A escrita respeita o contrato amplo da função, porém quebra o contrato específico da referência original.

A substituição que mypy deve rejeitar

Execute com Python 3.12+ e mypy para observar o diagnóstico na chamada final.

python
class Animal:
    pass


class Gato(Animal):
    def miar(self) -> None:
        print("Miau")


class Cachorro(Animal):
    pass


class Caixa[T]:
    def __init__(self, valor: T) -> None:
        self.valor = valor

    def obter(self) -> T:
        return self.valor

    def substituir(self, novo_valor: T) -> None:
        self.valor = novo_valor


def guardar_cachorro(caixa: Caixa[Animal]) -> None:
    caixa.substituir(Cachorro())


caixa_de_gatos = Caixa(Gato())
guardar_cachorro(caixa_de_gatos)  # mypy: argumento incompatível

gato = caixa_de_gatos.obter()
gato.miar()  # seria inseguro se a chamada acima fosse aceita

Duas referências, um único objeto

O caminho da incompatibilidade

O problema aparece porque as duas variáveis apontariam para a mesma caixa em memória.

Diagrama em quatro etapas mostrando caixa com gato, duas referências para a mesma caixa, uma função que a vê como caixa de animais e escreve um cachorro, e a referência original lendo o cachorro como se fosse gato.

Permitir a passagem criaria uma rota para inserir Cachorro em uma caixa prometida como Caixa[Gato].

Ordene o risco

Suponha, apenas para raciocinar, que Caixa[Gato] pudesse ser passada para uma função que espera Caixa[Animal]. Coloque os acontecimentos na ordem correta.

  1. Uma variável é criada como `Caixa[Gato]`.
  2. A mesma caixa é passada, hipoteticamente, para uma função que a trata como `Caixa[Animal]`.
  3. A referência original lê o valor esperando um `Gato`.
  4. A função grava um `Cachorro`, pois ele é um `Animal`.

A mesma regra vale para list

Listas também recebem e devolvem elementos

Uma list[Gato] permite leitura de Gato, mas também permite inserção de Gato. Se ela pudesse substituir uma list[Animal], quem recebesse a lista ampla poderia fazer append(Cachorro()).

Por isso, list é invariante: list[Gato] não é compatível com list[Animal], nem o contrário. A relação de subtipagem dos elementos não basta quando o contêiner pode ser alterado.

Transfira o raciocínio para uma lista

A chamada deve ser rejeitada pelo verificador pelo mesmo motivo da Caixa.

python
class Animal:
    pass


class Gato(Animal):
    def miar(self) -> None:
        print("Miau")


class Cachorro(Animal):
    pass


def adicionar_cachorro(animais: list[Animal]) -> None:
    animais.append(Cachorro())


gatos: list[Gato] = [Gato()]
adicionar_cachorro(gatos)  # mypy: argumento incompatível

gatos[0].miar()  # a lista poderia agora conter um Cachorro

Justifique sem citar o diagnóstico

Por que list[Gato] não deve ser aceita como argumento de uma função que recebe list[Animal] e pode chamar append? Explique usando Cachorro e o compartilhamento da mesma lista.

Escreva pelo menos 80 caracteres (0/80).

Passo 4 de 6

Reconhecer covariância em contratos de consulta

Use contratos de consulta para aceitar elementos de subtipos com segurança, sem prometer que a coleção concreta é imutável.

Da subtipagem à consulta

Covariância preserva a direção

Se todo Gato é um Animal, um contrato que apenas fornece elementos pode aceitar uma especialização mais específica:

Sequence[Gato] pode ser usada onde se espera Sequence[Animal].

Essa é a covariância: a direção da subtipagem é preservada entre as especializações. Quem recebe a sequência só pode consultar animais; portanto, receber gatos não quebra o contrato.

Consulta segura

O consumidor lê cada elemento como Animal, embora os objetos concretos sejam gatos.

Diagrama mostrando uma lista de gatos entregue a uma função de consulta que lê cada elemento como Animal, sem operações de escrita.

Como Gato é subtipo de Animal, valores produzidos como Gato servem para uma leitura que espera Animal.

Anote o que a função realmente faz

Consumidor somente de leitura

Python 3.12+

python
from collections.abc import Sequence

class Animal:
    def emitir_som(self) -> str:
        return "som"

class Gato(Animal):
    def emitir_som(self) -> str:
        return "miau"


def mostrar_sons(animais: Sequence[Animal]) -> None:
    for animal in animais:
        print(animal.emitir_som())


gatos: list[Gato] = [Gato(), Gato()]
mostrar_sons(gatos)  # Aceito: list[Gato] atende a Sequence[Animal]

Por que a chamada é aceita?

list continua sendo invariável: list[Gato] não é um list[Animal]. Porém, uma lista de gatos oferece a interface de consulta Sequence[Gato], e essa interface é compatível com Sequence[Animal].

A função não adiciona, remove nem substitui elementos. Por isso, anotar o parâmetro como Sequence[Animal] descreve exatamente as operações de que ela precisa.

Dica

Escolha o contrato, não uma conversão

Não é necessário copiar nem converter gatos para list[Animal]. Mantenha o tipo concreto verdadeiro, list[Gato], e use Sequence[Animal] no ponto em que o código só consulta a coleção.

Leitura não congela a lista

O contrato limita o consumidor

Sequence[Animal] impede que a função tipada trate o parâmetro como uma coleção alterável. Isso não torna o objeto original imutável: quem ainda possui a referência gatos pode alterá-la como uma list[Gato].

Interface de consulta ≠ objeto imutável

A mesma lista pode ser vista por uma função somente de leitura e pelo código que a criou, que conserva as operações de lista.

Diagrama de uma lista de gatos com duas referências: uma passa por uma interface somente de leitura para um consumidor e outra permanece como lista mutável para o proprietário.

A restrição está no contrato usado pela função, não em uma mudança no objeto concreto.

Contrato versus objeto

Ao receber gatos: list[Gato] em um parâmetro anotado como Sequence[Animal], a lista original se torna imutável em todo o programa.

Decida pelo uso necessário

Contrato adequado

Uma função percorre uma coleção, chama emitir_som() em cada elemento e não a altera. Ela deve aceitar tanto list[Gato] quanto outras sequências de animais. Qual anotação de parâmetro é a mais adequada?

Resumo

Regra prática

  • Covariância permite usar Sequence[Gato] onde se espera Sequence[Animal].
  • Uma list[Gato] pode ser passada a um parâmetro Sequence[Animal] quando a função só consulta.
  • list permanece invariável: não trate list[Gato] como list[Animal].
  • Uma interface de consulta não torna a lista concreta imutável.

Passo 5 de 6

Relacionar a interface à variância inferida

Examine o que uma classe expõe para prever se suas especializações podem ser covariantes ou precisam ser invariantes.

A interface determina a relação

Variância inferida na sintaxe moderna

Em Python 3.12+, declare uma classe genérica com class Nome[T]. O mypy pode inferir a variância de T a partir da interface exposta: como T é lido e como pode ser escrito.

Uma classe que apenas fornece um T pode ser covariante: onde se espera uma especialização mais ampla, uma especialização de subtipo é segura. Quando a interface também aceita um T para alteração, ela precisa ser invariante.

A relação já conhecida entre Gato e Animal continua a mesma; o que muda é a compatibilidade entre especializações da classe.

Ler não é o mesmo que escrever

Compare as três formas de expor um valor genérico.

Diagrama comparando três contêineres genéricos. O primeiro tem armazenamento interno e uma saída de leitura; o segundo tem leitura e entrada por setter; o terceiro tem um atributo público com leitura e escrita. O primeiro indica covariância, os outros dois invariância.

Somente consulta pode preservar a direção Gato → Animal; uma entrada pública de T exige invariância.

Consulta com armazenamento interno

Uma interface somente de leitura

Use Python 3.12 ou superior e verifique com mypy.

python
class Animal:
    pass


class Gato(Animal):
    pass


class Consulta[T]:
    def __init__(self, valor: T) -> None:
        self._valor = valor

    @property
    def valor(self) -> T:
        return self._valor


def observar(animal: Consulta[Animal]) -> Animal:
    return animal.valor


gatos = Consulta(Gato())
observado = observar(gatos)  # Aceito: Consulta[Gato] -> Consulta[Animal]

Por que esta substituição é segura?

T entra no __init__, mas o inicializador participa apenas da criação do objeto. Depois disso, a interface pública de Consulta só devolve T, pela propriedade sem setter.

Assim, observar consegue ler um Animal sem poder inserir outro animal na instância recebida. O mypy pode inferir T como covariante nessa classe.

O prefixo _ comunica armazenamento interno e é relevante para essa análise do mypy. Ele é uma convenção: em execução, Python ainda permite acessar gatos._valor.

Dica

Confira a inferência

Para inspecionar a especialização inferida, adicione temporariamente reveal_type(gatos). O mypy informa algo como Consulta[Gato]. Remova essa chamada antes de executar o programa normalmente.

Quando a escrita torna a classe invariante

Setter e atributo público expõem escrita

Estas duas interfaces recebem e devolvem T.

python
class CaixaComSetter[T]:
    def __init__(self, valor: T) -> None:
        self._valor = valor

    @property
    def valor(self) -> T:
        return self._valor

    @valor.setter
    def valor(self, novo_valor: T) -> None:
        self._valor = novo_valor


class CaixaPublica[T]:
    def __init__(self, valor: T) -> None:
        self.valor: T = valor


def guardar_animal(caixa: CaixaComSetter[Animal]) -> None:
    caixa.valor = Animal()


def trocar_animal(caixa: CaixaPublica[Animal]) -> None:
    caixa.valor = Animal()

A mesma regra em duas superfícies

Em CaixaComSetter, o setter aceita um novo T; em CaixaPublica, o atributo público pode receber um novo T. Ambas oferecem leitura e escrita, mesmo que não exista um método chamado trocar.

Portanto, o mypy infere invariância: CaixaComSetter[Gato] não substitui CaixaComSetter[Animal], e o mesmo vale para CaixaPublica. Se fosse permitido, uma função poderia gravar um Animal que não é Gato na mesma caixa.

Não tente tornar um atributo público “interno” apenas pelo nome: _valor não bloqueia acesso em tempo de execução. A decisão de interface continua sendo sua.

Classifique pela operação permitida

Interface e variância esperada

Associe cada interface à classificação que o mypy pode inferir para T.

Toque em um item e depois no par correspondente.

Preveja a mudança

Uma classe Consulta[T] possui armazenamento _valor e uma propriedade de leitura valor -> T. Ela é usada como Consulta[Gato] em uma função que espera Consulta[Animal]. Explique por que isso é seguro e o que muda se você adicionar um setter valor(novo: T).

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 6

Aplicação final: definir e verificar classes genéricas

Integre armazenamento mutável e consulta covariante em um exemplo completo, verificando quais substituições o mypy aceita ou rejeita.

Dois contratos, duas possibilidades

Escolha a interface pela operação permitida

Nesta aplicação final, use duas classes com o mesmo vínculo entre T, inicializador e valor armazenado:

  • Caixa[T] lê e substitui o valor. Por permitir escrita, sua especialização é invariante.
  • Consulta[T] expõe apenas uma propriedade de leitura. Com armazenamento interno _valor e sem setter, o mypy pode inferir covariância.

Assim, uma Consulta[Gato] pode atender um consumidor que só consulta Animal; uma Caixa[Gato] não pode atender um consumidor que poderia gravar qualquer Animal.

Leitura e escrita determinam a compatibilidade

Diagrama comparando duas referências: à esquerda, uma Caixa de Gato recebendo uma tentativa de escrita de Cachorro por meio de uma referência ampla; à direita, uma Consulta de Gato sendo lida como Animal sem alterar o valor.

Na caixa mutável, uma escrita ampla comprometeria a promessa de ler apenas gatos. Na consulta, só há saída de valores.

Implemente e rode a verificação local

Crie um arquivo autocontido

No seu computador, crie genericos.py com o código abaixo. As chamadas com reveal_type servem para o mypy; remova-as ou deixe-as apenas durante a análise. Há uma chamada insegura intencional, que deve produzir um diagnóstico.

genericos.py

python
class Animal:
    pass


class Gato(Animal):
    pass


class Cachorro(Animal):
    pass


class Caixa[T]:
    def __init__(self, valor: T) -> None:
        self._valor = valor

    def ler(self) -> T:
        return self._valor

    def trocar(self, novo_valor: T) -> None:
        self._valor = novo_valor


class Consulta[T]:
    def __init__(self, valor: T) -> None:
        self._valor = valor

    @property
    def valor(self) -> T:
        return self._valor


def colocar_cachorro(destino: Caixa[Animal]) -> None:
    destino.trocar(Cachorro())


def descrever(origem: Consulta[Animal]) -> Animal:
    return origem.valor


caixa_explicita = Caixa[Gato](Gato())
caixa_inferida = Caixa(Gato())
consulta_explicita = Consulta[Gato](Gato())
consulta_inferida = Consulta(Gato())
caixa_animais = Caixa[Animal](Gato())

reveal_type(caixa_explicita)      # Caixa[Gato]
reveal_type(caixa_inferida)       # Caixa[Gato]
reveal_type(consulta_explicita)   # Consulta[Gato]
reveal_type(consulta_inferida)    # Consulta[Gato]

colocar_cachorro(caixa_animais)       # Aceito: Caixa[Animal] pode receber Cachorro.
colocar_cachorro(caixa_explicita)     # Erro intencional: Caixa[Gato] não é Caixa[Animal].
animal = descrever(consulta_explicita)  # Aceito: Consulta[Gato] é compatível com Consulta[Animal].

Dica

Comando de análise

No diretório do arquivo, execute:

python -m mypy --strict --python-version 3.12 genericos.py

Espere os quatro tipos revelados e um erro na passagem de caixa_explicita para colocar_cachorro. Depois, comente somente essa chamada insegura e rode novamente: o resultado deve ser livre de erros.

Explique e corrija a substituição

Diagnóstico da solução

Após executar o mypy, responda:

  1. Quais tipos foram revelados para as construções explícitas e inferidas?
  2. Por que descrever(consulta_explicita) é aceita?
  3. Por que colocar_cachorro(caixa_explicita) é rejeitada?
  4. Corrija a situação sem Any, casts ou supressões. Explique a alternativa adequada se a função precisa gravar qualquer Animal e a alternativa adequada se ela deve trabalhar apenas com Gato.

Escreva pelo menos 280 caracteres (0/280).

Síntese da aplicação

Resumo

Checklist para classes genéricas

Use este roteiro ao projetar e revisar uma classe parametrizada.

  • Reutilize o mesmo parâmetro de tipo no inicializador, armazenamento, retornos e entradas que precisam preservar a mesma relação.
  • Confira a especialização explicitamente, como Caixa[Gato], ou com reveal_type quando o construtor a inferir.
  • Se uma interface recebe e devolve T, trate suas especializações como invariantes: Caixa[Gato] não substitui Caixa[Animal].
  • Uma interface de consulta pode ser covariante: Consulta[Gato] pode ser usada onde se consulta Consulta[Animal].
  • Decida a compatibilidade pelas operações permitidas ao consumidor, não apenas pela herança entre os valores armazenados.

Tutorial concluído

Parabéns! Você concluiu: Definir classes genéricas com parâmetros de tipo

Muito bem! Você já pode definir armazenamento genérico, conferir especializações inferidas e justificar invariância ou covariância com base no contrato exposto.

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