Trilha de aprendizado · Nível 10 · Tutorial 2

Implementar iteráveis e iteradores em classes

Criar classes compatíveis com a iteração do Python, separando os dados da fonte do estado de cada percurso quando forem necessárias passagens independentes.

  • Nível: Intermediário
  • Duração: 20 min
  • 7 passos
Implementar iteráveis e iteradores em classes

O que você vai percorrer

  1. Conectar a classe ao protocolo de iteração Veja como uma classe passa a participar de iter, next e for ao oferecer os métodos especiais do protocolo de iteração. 2 min
  2. Implementar um iterador finito Construa um iterador que produz números inteiros de um início inclusivo até um fim exclusivo, mantendo sua própria posição de consumo. 4 min
  3. Preservar o esgotamento e tratar os limites Verifique os casos de limite de um iterador finito e preserve seu estado após o término. 3 min
  4. Separar a fonte de cada percurso Crie uma fonte reutilizável que guarda apenas seus limites e entrega um novo iterador para cada passagem. 3 min
  5. Comprovar a independência dos percursos Observe dois iteradores criados pela mesma fonte e confirme que cada um mantém sua própria posição de consumo. 2 min
  6. Testar o comportamento pela interface pública Use pytest para conferir valores, identidade e término das classes Faixa e IteradorFaixa sem acessar seus atributos internos. 3 min
  7. Aplicação final: corrigir e validar as duas classes Corrija uma implementação com estado compartilhado e reinicialização indevida, depois valide os contratos públicos de iteração. 4 min

O que você vai aprender

  • Implementar __iter__ e __next__ em uma classe iteradora finita.
  • Atualizar o estado de avanço e sinalizar o término com StopIteration.
  • Criar uma classe iterável cujo __iter__ forneça um novo iterador a cada chamada.
  • Testar sequência produzida, fonte vazia, esgotamento persistente e independência entre percursos.

Antes de começar

  • Consumir iteradores com iter e next
  • Criar objetos com classes e métodos de instância
  • Distinguir atributos de instância e de classe
  • Validar entradas e sinalizar falhas com raise
  • Testar mudanças de estado em objetos
  • Selecionar casos de teste e verificar exceções

Passo 1 de 7

Conectar a classe ao protocolo de iteração

Veja como uma classe passa a participar de iter, next e for ao oferecer os métodos especiais do protocolo de iteração.

Operações públicas, métodos especiais

A classe responde ao protocolo

Você já consumiu iteradores com iter, next e for. Agora a classe implementará o comportamento acionado por essas operações.

Quando Python executa iter(objeto), ele usa objeto.__iter__(). Quando executa next(iterador), usa iterador.__next__(). Em geral, escrevemos e consumimos pela interface pública — iter, next e for — sem chamar esses métodos especiais diretamente.

Fluxo do protocolo de iteração

A imagem mostra o caminho entre as operações públicas e os contratos que sua classe deve cumprir.

Diagrama mostrando um objeto passado para iter, que chama __iter__ e entrega um iterador; next chama __next__ repetidamente, produz valores e termina com StopIteration; for realiza esse mesmo ciclo automaticamente.

iter obtém o iterador; next pede um valor; for repete esse processo até o término.

Os dois contratos

O que cada método promete

__iter__ deve devolver um objeto iterador. Não basta devolver uma lista, uma tupla ou qualquer coleção de valores: o resultado precisa poder atender a chamadas de next(...).

__next__ deve devolver um elemento por chamada. Quando não existir outro elemento, deve lançar StopIteration. Esse sinal não é um valor comum: ele informa ao consumidor que o percurso acabou.

Exemplo

Leitura do contrato

fonte = MinhaClasse()
iterador = iter(fonte)      # aciona fonte.__iter__()
valor = next(iterador)      # aciona iterador.__next__()

for valor in fonte:
    print(valor)

No for, Python obtém um iterador e solicita valores sucessivos. Ao receber StopIteration, o laço termina normalmente.

Dica

Foco deste tutorial

Por enquanto, concentre-se nos contratos: quem fornece um iterador e quem fornece um próximo valor. A posição de avanço e a separação entre fonte e percurso aparecerão nos próximos passos.

Associe operação e responsabilidade

Mapa do protocolo

Relacione cada elemento à sua responsabilidade.

Toque em um item e depois no par correspondente.

Passo 2 de 7

Implementar um iterador finito

Construa um iterador que produz números inteiros de um início inclusivo até um fim exclusivo, mantendo sua própria posição de consumo.

Estado de um percurso

Uma faixa, um percurso

Vamos criar IteradorFaixa, uma classe que produz inteiros de inicio até antes de fim. Por exemplo, IteradorFaixa(3, 6) entrega 3, 4 e 5.

Cada instância guarda somente o que precisa para avançar: a posição atual e o limite. Os resultados não são montados previamente em uma lista.

Posição que avança

A posição começa em 3. A cada valor entregue, ela aumenta uma unidade. Quando chega a 6, não há mais valor válido para produzir.

Diagrama de uma linha numérica com os valores 3, 4, 5 e o limite 6. Um marcador de posição avança de 3 para 4 e depois 5; após isso, encontra uma barreira no 6.

Início inclusivo: 3. Fim exclusivo: 6. A posição é o estado que muda em cada avanço.

Dica

Estado por instância

Duas instâncias de IteradorFaixa podem ter posições diferentes. Nesta etapa, cada objeto representa um único percurso de consumo.

Implementação do iterador

O papel de cada método

No iterador, __iter__ devolve o próprio objeto: self. Isso não reinicia nem avança a posição.

Em __next__, teste primeiro se a posição chegou ao limite. Se chegou, lance StopIteration. Caso contrário, guarde a posição atual, avance-a e devolva o valor guardado.

Classe IteradorFaixa

Crie um arquivo Python com esta classe e execute os exemplos abaixo no seu computador.

python
class IteradorFaixa:
    def __init__(self, inicio, fim):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration

        valor = self.atual
        self.atual += 1
        return valor


it = IteradorFaixa(3, 6)
print(next(it))  # 3
print(next(it))  # 4
print(next(it))  # 5
print(next(it))  # StopIteration

Exemplo

Por que guardar antes de avançar?

Com atual = 3, __next__ guarda 3 em valor, altera atual para 4 e devolve 3. Assim, o primeiro número não é pulado.

Na próxima chamada, o mesmo processo devolve 4 e deixa a posição em 5.

Verifique o fluxo de avanço

Ordem dentro de __next__

Para uma posição ainda válida, organize as ações de __next__ na ordem correta.

  1. Guardar o valor de atual em uma variável local.
  2. Somar 1 a atual.
  3. Verificar se atual é maior ou igual a fim; se for, lançar StopIteration.
  4. Devolver o valor guardado.

Experimente no seu computador

Execute o código da tela anterior. Para IteradorFaixa(3, 6), relate a sequência devolvida pelas três primeiras chamadas a next, como a posição muda e o que ocorre na chamada seguinte.

Escreva pelo menos 80 caracteres (0/80).

Passo 3 de 7

Preservar o esgotamento e tratar os limites

Verifique os casos de limite de um iterador finito e preserve seu estado após o término.

O limite decide se ainda há valor

Vazio, último valor e término

Em IteradorFaixa(inicio, fim), o fim é exclusivo. Portanto, a condição self.atual >= self.fim significa que não existe próximo valor.

  • Se inicio >= fim, o percurso já nasce vazio.
  • Se atual ainda é menor que fim, esse valor é entregue — inclusive o último válido.
  • Depois que o último valor é entregue e atual avança, as próximas chamadas continuam encontrando a condição de término.

Posição e limite

Diagrama de duas faixas: uma começa em 2 e termina antes de 5, entregando 2, 3 e 4; a outra começa e termina em 5, sem valores disponíveis.

O valor no limite não é produzido. Com início igual ou maior que o fim, não há primeiro valor.

O estado não volta atrás

Implementação que preserva o término

A verificação ocorre antes de devolver um valor. __iter__ apenas confirma que este objeto é o iterador; ele não altera atual.

python
class IteradorFaixa:
    def __init__(self, inicio, fim):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration

        valor = self.atual
        self.atual += 1
        return valor

Exemplo

Último valor e esgotamento persistente

it = IteradorFaixa(2, 5)

print(next(it))  # 2
print(next(it))  # 3
print(next(it))  # 4: último valor válido

try:
    next(it)
except StopIteration:
    print("terminou")

print(iter(it) is it)  # True
next(it)  # ainda lança StopIteration

Após produzir 4, a posição passa a ser 5. Como 5 >= 5, o iterador permanece esgotado.

Atenção

Não reinicie em __iter__

Evite atribuir self.atual = ... dentro de __iter__. Uma chamada a iter(it) sobre o mesmo iterador não deve restaurar valores já consumidos. Além disso, __next__ deve usar raise StopIteration: retornar None ou retornar um objeto StopIteration faria esse objeto ser tratado como um valor normal.

Diagnóstico de contratos de término

Caso vazio

Considere it = IteradorFaixa(8, 8). Qual deve ser o resultado de next(it)?

Encontre a implementação correta

Qual versão respeita o protocolo?

Qual trecho mantém o esgotamento de um iterador e informa corretamente que não há próximo valor?

Passo 4 de 7

Separar a fonte de cada percurso

Crie uma fonte reutilizável que guarda apenas seus limites e entrega um novo iterador para cada passagem.

Fonte e percurso têm responsabilidades diferentes

Dois objetos, dois papéis

Até aqui, IteradorFaixa guarda os limites e a posição que avança. Para permitir que a mesma faixa seja percorrida novamente, crie uma fonte chamada Faixa.

  • Faixa conserva somente inicio e fim.
  • Cada IteradorFaixa conserva sua própria atual e a atualiza em __next__.

Assim, a fonte descreve os valores possíveis; o iterador representa um percurso em andamento.

Estrutura da iteração

A mesma fonte pode criar percursos separados com os mesmos limites.

Diagrama mostrando uma instância Faixa com início 2 e fim 5 apontando para duas instâncias independentes de IteradorFaixa, ambas com os mesmos limites e posições atuais próprias.

Faixa(2, 5) não guarda a posição. Cada iterador recebido começa com sua própria posição em 2.

Dica

Regra de responsabilidade

Se um atributo muda a cada next, ele pertence ao iterador do percurso — não à fonte reutilizável.

Implemente a fonte reutilizável

As duas classes

Defina IteradorFaixa e Faixa no mesmo arquivo.

python
class IteradorFaixa:
    def __init__(self, inicio, fim):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration

        valor = self.atual
        self.atual += 1
        return valor


class Faixa:
    def __init__(self, inicio, fim):
        self.inicio = inicio
        self.fim = fim

    def __iter__(self):
        return IteradorFaixa(self.inicio, self.fim)


faixa = Faixa(3, 6)
print(list(faixa))
print(list(faixa))

O que muda em cada `__iter__`?

Em IteradorFaixa, __iter__ devolve self: aquele objeto já é o percurso e não deve reiniciar.

Em Faixa, __iter__ cria IteradorFaixa(self.inicio, self.fim): cada solicitação recebe um percurso novo. Faixa não implementa __next__, pois ela não avança nem produz valores diretamente.

Exemplo

Resultado esperado

Ao executar o código localmente, as duas linhas exibem:

[3, 4, 5]
[3, 4, 5]

O primeiro list esgota apenas o iterador criado para aquela passagem. O segundo list pede outro iterador à mesma Faixa.

Classifique cada responsabilidade

Fonte ou iterador?

Associe cada elemento à classe que deve ser responsável por ele.

Toque em um item e depois no par correspondente.

Verifique uma nova passagem

Teste no seu computador

Execute o código da tela anterior. O que as duas chamadas a list(faixa) exibiram? Explique, em uma frase, por que a segunda passagem não ficou vazia.

Escreva pelo menos 30 caracteres (0/30).

Passo 5 de 7

Comprovar a independência dos percursos

Observe dois iteradores criados pela mesma fonte e confirme que cada um mantém sua própria posição de consumo.

Uma fonte, dois percursos

Cada chamada cria outro percurso

Uma instância de Faixa guarda apenas os limites. Ao chamar iter(faixa) duas vezes, você recebe dois objetos IteradorFaixa distintos, ambos começando no mesmo início.

Assim, avançar um deles não altera a posição do outro. Não é necessário criar ou duplicar uma lista de valores: cada iterador conserva somente sua própria posição.

Posições independentes

A mesma fonte fornece dois iteradores com marcadores de posição separados.

Diagrama de uma fonte Faixa com limites 2 e 5 apontando para dois iteradores distintos. Um marcador do primeiro iterador está adiantado em relação ao marcador do segundo, mostrando estados de consumo independentes.

A fonte compartilha os limites; cada percurso tem sua própria posição.

Intercale os avanços

Execute este trecho depois de definir as classes Faixa e IteradorFaixa criadas anteriormente.

python
faixa = Faixa(2, 5)

percurso_a = iter(faixa)
percurso_b = iter(faixa)

print(percurso_a is percurso_b)  # False
print(next(percurso_a))          # 2
print(next(percurso_a))          # 3
print(next(percurso_b))          # 2
print(list(percurso_a))          # [4]
print(list(percurso_b))          # [3, 4]

Preveja o restante de cada iterador

Consumo em um não consome no outro

No código anterior, percurso_a chega ao fim após entregar 4. Isso não esgota percurso_b: ele havia entregado apenas 2 e ainda tem valores próprios para fornecer.

Resultado do segundo percurso

Complete o resultado:

list(percurso_b) produz ___

Evite reutilizar o mesmo iterador

O defeito: guardar um percurso na fonte

A fonte deixa de ser reutilizável se ela criar um iterador uma única vez e devolvê-lo sempre. Nesse caso, duas chamadas a iter(faixa) retornam o mesmo objeto; os consumidores passam a compartilhar a mesma posição.

Compare a implementação incorreta e a correta

A versão incorreta reutiliza o percurso armazenado. A correta cria outro a cada chamada.

python
# Incorreta: todos recebem o mesmo iterador
class FaixaComDefeito:
    def __init__(self, inicio, fim):
        self.inicio = inicio
        self.fim = fim
        self._iterador = IteradorFaixa(inicio, fim)

    def __iter__(self):
        return self._iterador

# Correta: cada chamada inicia um percurso independente
class Faixa:
    def __init__(self, inicio, fim):
        self.inicio = inicio
        self.fim = fim

    def __iter__(self):
        return IteradorFaixa(self.inicio, self.fim)

Diagnostique o compartilhamento

Suponha que FaixaComDefeito.__iter__ seja usada. Explique o efeito de criar a = iter(faixa) e b = iter(faixa), avançar a até o fim e depois tentar consumir b. Indique a correção.

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 7

Testar o comportamento pela interface pública

Use pytest para conferir valores, identidade e término das classes Faixa e IteradorFaixa sem acessar seus atributos internos.

Teste o que o usuário do objeto observa

Contratos visíveis, não detalhes internos

Os testes devem consumir as classes como o código cliente faria: com iter(), next(), list() e for. Assim, eles continuam válidos mesmo que você altere os nomes ou a forma de guardar a posição internamente.

Verifique três tipos de resultado: valores produzidos, identidade do iterador e exceções no término.

A interface pública revela o contrato

Diagrama mostrando uma fonte Faixa gerando dois iteradores independentes; cada iterador avança por seus próprios valores e termina separadamente.

A fonte pode criar novos percursos; cada percurso é consumido apenas por operações públicas como iter e next.

Dica

Evite testes frágeis

Não faça asserções como assert iterador.atual == 3. A posição é um detalhe de implementação. Prefira pedir o próximo valor e conferir o resultado: assert next(iterador) == 3.

Código da fonte e do iterador

Crie o módulo local

No seu projeto, crie o arquivo faixa.py com as duas classes abaixo. A fonte Faixa guarda apenas os limites; cada chamada a iter(faixa) cria um novo IteradorFaixa.

faixa.py

python
class IteradorFaixa:
    def __init__(self, inicio, fim):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration

        valor = self.atual
        self.atual += 1
        return valor


class Faixa:
    def __init__(self, inicio, fim):
        self.inicio = inicio
        self.fim = fim

    def __iter__(self):
        return IteradorFaixa(self.inicio, self.fim)

Escreva uma suíte orientada ao comportamento

Casos essenciais

Crie test_faixa.py na mesma pasta. Os testes abaixo cobrem a sequência completa, as fontes vazias, o esgotamento persistente, a identidade do iterador e a independência entre percursos.

test_faixa.py

python
import pytest

from faixa import Faixa


def test_produz_sequencia_completa_na_ordem():
    assert list(Faixa(2, 5)) == [2, 3, 4]


@pytest.mark.parametrize(("inicio", "fim"), [(4, 4), (5, 2)])
def test_fonte_vazia_nao_produz_valores(inicio, fim):
    assert list(Faixa(inicio, fim)) == []


def test_iterador_mantem_identidade_e_posicao():
    iterador = iter(Faixa(2, 5))

    assert iter(iterador) is iterador
    assert next(iterador) == 2
    assert next(iter(iterador)) == 3


def test_esgotamento_permanece_apos_iter():
    iterador = iter(Faixa(7, 8))

    assert next(iterador) == 7

    with pytest.raises(StopIteration):
        next(iterador)

    with pytest.raises(StopIteration):
        next(iter(iterador))


def test_duas_passagens_completas_da_mesma_fonte():
    faixa = Faixa(1, 4)

    assert list(faixa) == [1, 2, 3]
    assert list(faixa) == [1, 2, 3]


def test_iteradores_da_mesma_fonte_sao_independentes():
    faixa = Faixa(10, 13)
    primeiro = iter(faixa)
    segundo = iter(faixa)

    assert primeiro is not segundo
    assert next(primeiro) == 10
    assert next(primeiro) == 11
    assert next(segundo) == 10
    assert list(primeiro) == [12]
    assert list(segundo) == [11, 12]

Exemplo

Execute no seu computador

No terminal, dentro da pasta que contém os dois arquivos, instale pytest caso ainda não esteja disponível:

python -m pip install pytest

Depois, execute a suíte:

python -m pytest -v

Com o código apresentado, o resultado esperado é a aprovação de todos os 7 testes.

Relacione garantias e testes

Que garantia cada teste verifica?

Associe o cenário à garantia principal verificada.

Toque em um item e depois no par correspondente.

Confira sua execução

Execute os testes no seu computador. Quantos testes passaram? Se houve falha, qual comportamento ela indicou?

Escreva pelo menos 60 caracteres (0/60).

Passo 7 de 7

Aplicação final: corrigir e validar as duas classes

Corrija uma implementação com estado compartilhado e reinicialização indevida, depois valide os contratos públicos de iteração.

Diagnóstico: onde está o estado errado?

Duas falhas, duas responsabilidades

A implementação abaixo parece produzir números, mas viola dois contratos: IteradorFaixa.__iter__ reinicia um percurso já iniciado, e Faixa.__iter__ devolve sempre o mesmo percurso. Leia o código e localize as duas linhas que causam esses comportamentos.

A correção deve ser localizada: a fonte guarda apenas inicio e fim; cada iterador guarda sua própria atual.

Fonte reutilizável versus percurso

Compare o desenho com o código defeituoso: dois chamados a iter(faixa) devem criar dois objetos de percurso, nunca compartilhar um marcador de posição.

Diagrama mostrando uma fonte Faixa com início e fim ligada a dois iteradores separados, cada um com uma posição atual própria; ao lado, um exemplo incorreto com ambos os consumidores apontando para um único iterador.

A fonte não consome valores; cada iterador controla somente a posição do seu próprio percurso.

Versão defeituosa para corrigir

Crie um arquivo faixa.py com este código. Não altere __next__: o avanço e o término já estão no lugar certo.

python
class IteradorFaixa:
    def __init__(self, inicio, fim):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        self.atual = 0  # falha: reinicia o percurso
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration

        valor = self.atual
        self.atual += 1
        return valor


class Faixa:
    def __init__(self, inicio, fim):
        self.inicio = inicio
        self.fim = fim
        self._iterador = IteradorFaixa(inicio, fim)

    def __iter__(self):
        return self._iterador  # falha: compartilha o percurso

Prática: corrija e execute os contratos

Roteiro de correção

No seu computador, corrija faixa.py para que:

  1. IteradorFaixa.__iter__ apenas devolva o próprio objeto, sem mudar atual.
  2. Faixa.__iter__ crie e devolva um novo IteradorFaixa usando os limites da fonte.
  3. A fonte não mantenha um iterador em atributo.

Em seguida, salve o teste abaixo como test_faixa.py na mesma pasta e execute pytest -q.

Testes pela interface pública

Os testes observam valores, identidade e exceções por iter, next e list, sem consultar atributos internos.

python
import pytest

from faixa import Faixa, IteradorFaixa


def test_produz_sequencia_completa_na_ordem():
    assert list(Faixa(2, 5)) == [2, 3, 4]


@pytest.mark.parametrize("inicio, fim", [(4, 4), (5, 2)])
def test_fonte_vazia(inicio, fim):
    assert list(Faixa(inicio, fim)) == []


def test_iterador_permanece_esgotado_e_iter_nao_reinicia():
    percurso = iter(Faixa(1, 3))

    assert iter(percurso) is percurso
    assert next(percurso) == 1
    assert iter(percurso) is percurso
    assert next(percurso) == 2

    with pytest.raises(StopIteration):
        next(percurso)
    with pytest.raises(StopIteration):
        next(percurso)


def test_passagens_da_mesma_fonte_sao_independentes():
    faixa = Faixa(10, 13)
    primeiro = iter(faixa)
    segundo = iter(faixa)

    assert primeiro is not segundo
    assert next(primeiro) == 10
    assert next(primeiro) == 11
    assert next(segundo) == 10
    assert list(primeiro) == [12]
    assert list(segundo) == [11, 12]


def test_iterador_vazio_termina_imediatamente():
    percurso = IteradorFaixa(3, 3)

    with pytest.raises(StopIteration):
        next(percurso)

Confira a implementação de referência

Uma solução completa

Compare com sua versão depois de tentar corrigir e executar os testes. Observe que cada __iter__ tem uma responsabilidade diferente.

python
class IteradorFaixa:
    def __init__(self, inicio, fim):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        return self

    def __next__(self):
        if self.atual >= self.fim:
            raise StopIteration

        valor = self.atual
        self.atual += 1
        return valor


class Faixa:
    def __init__(self, inicio, fim):
        self.inicio = inicio
        self.fim = fim

    def __iter__(self):
        return IteradorFaixa(self.inicio, self.fim)

Explique sua correção

Após executar os testes, explique quais mudanças você fez e por que elas garantem, ao mesmo tempo, percursos independentes e esgotamento persistente.

Escreva pelo menos 80 caracteres (0/80).

Síntese final

Resumo

Garantias da implementação

Você integrou uma fonte reutilizável a iteradores finitos e verificou o resultado somente pela interface pública.

  • Faixa conserva os parâmetros estáveis inicio e fim; ela não conserva uma posição de consumo.
  • Cada chamada a iter(faixa) cria um novo IteradorFaixa, permitindo passagens completas e simultâneas independentes.
  • IteradorFaixa.__iter__ devolve self sem avançar nem reiniciar o estado.
  • __next__ entrega o valor atual, avança uma unidade e lança StopIteration ao atingir o limite.
  • Depois de esgotado, o iterador continua lançando StopIteration; ele não devolve None como se fosse um valor.

Tutorial concluído

Parabéns! Você concluiu: Implementar iteráveis e iteradores em classes

Muito bem! Você consegue separar os parâmetros da fonte do estado de cada iterador, preservar o esgotamento e testar esses contratos com operações públicas do Python.

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