Trilha de aprendizado · Nível 12 · Tutorial 5

Preservar relações de tipos em funções genéricas

Definir funções genéricas que preservem a relação entre os tipos recebidos e devolvidos, sem perder essa informação em object, Any ou uniões desnecessárias.

  • Nível: Avançado
  • Duração: 22 min
  • 8 passos
Preservar relações de tipos em funções genéricas

O que você vai percorrer

  1. Identificar a relação que a assinatura precisa preservar Compare assinaturas que aceitam os mesmos valores, mas oferecem níveis diferentes de informação estática sobre o retorno. 2 min
  2. Declarar parâmetros de tipo e acompanhar a inferência Use a sintaxe do Python 3.12 para declarar uma função genérica e acompanhe como o verificador preserva o tipo conhecido em cada chamada. 3 min
  3. Relacionar o elemento de uma coleção ao resultado Anote funções de seleção para que o tipo estático de cada elemento da lista seja preservado no valor retornado. 3 min
  4. Respeitar o contrato no corpo genérico Verifique se o corpo de uma função genérica usa apenas operações garantidas para qualquer tipo que possa ocupar T. 2 min
  5. Separar os tipos de entrada e saída de uma transformação Anote uma função que transforma cada elemento de uma lista, preservando a relação entre o tipo recebido pelo callback e o tipo produzido. 3 min
  6. Usar um limite superior sem perder o subtipo Use um limite superior para acessar operações de uma classe base sem apagar o subtipo conhecido em cada chamada. 3 min
  7. Decidir quando um genérico agrega informação Escolha a assinatura que comunica a relação real entre entradas e saída, sem introduzir parâmetros de tipo que não preservam informação útil. 2 min
  8. Aplicar e verificar as relações de ponta a ponta Integre seleção, transformação e preservação de subtipos em um único arquivo e confirme a inferência com mypy. 4 min

O que você vai aprender

  • Declarar parâmetros de tipo em funções com a sintaxe do Python 3.12.
  • Relacionar o tipo de um elemento recebido ao tipo do resultado.
  • Descrever transformações com tipos de entrada e saída distintos.
  • Restringir um parâmetro de tipo quando a implementação exigir operações de uma classe base.

Antes de começar

  • Anotar coleções e estruturas aninhadas
  • Tratar valores opcionais e uniões de tipos
  • Anotar funções recebidas e retornadas com Callable
  • Especializar classes com herança e super

Passo 1 de 8

Identificar a relação que a assinatura precisa preservar

Compare assinaturas que aceitam os mesmos valores, mas oferecem níveis diferentes de informação estática sobre o retorno.

Aceitar não é preservar

O contrato pode perder uma relação

Considere uma função cujo comportamento é simplesmente devolver o valor que recebeu. Ela aceita vários tipos, mas o ponto importante não é apenas quais tipos entram: é que o tipo que sai deve acompanhar o tipo que entrou em cada chamada.

Uma assinatura ampla pode aceitar corretamente str e int, mas ainda apagar essa ligação para quem chama a função.

Relação por chamada

Cada chamada tem uma correspondência própria entre entrada e retorno.

Diagrama comparando uma função identidade que devolve um valor do mesmo tipo recebido com uma assinatura ampla que transforma os retornos em um tipo desconhecido.

À esquerda, texto retorna texto e número retorna número. À direita, ambos os resultados viram um retorno amplo para o verificador.

Assinaturas amplas apagam detalhes

Mesmo comportamento, contratos diferentes

As três funções devolvem o argumento recebido, mas suas anotações informam coisas diferentes ao verificador.

python
def identidade_object(valor: object) -> object:
    return valor


def identidade_any(valor: object) -> Any:
    return valor


def identidade_uniao(valor: str | int) -> str | int:
    return valor

nome = identidade_object("Ana")  # tipo estático: object
codigo = identidade_any("A-17")  # tipo estático: Any
idade = identidade_uniao(42)      # tipo estático: str | int

O que se perde?

object aceita valores de qualquer tipo, mas o retorno fica conhecido apenas como object. Any vai além: desliga a verificação nas operações posteriores.

A união parece mais específica, porém str | int no retorno só diz que uma das alternativas será devolvida. Ela não registra a regra: “se entrou str, sai str; se entrou int, sai int”. Assim, mesmo ao passar 42, o resultado continua sendo tratado como str | int.

Dica

Pergunta de diagnóstico

Ao ler uma função, pergunte: o tipo do retorno depende do tipo de algum argumento nesta chamada? Se depende, listar alternativas aceitas pode não bastar; o contrato precisa representar a dependência.

Alternativas não são correlação

Qual informação está ausente?

Uma função tem a assinatura def eco(valor: str | int) -> str | int. Ela apenas retorna valor. Qual afirmação é correta?

A relação será definida no uso

O próximo passo

Para uma função de identidade, o contrato desejado não é “aceita texto ou número”. É “para qualquer tipo escolhido nesta chamada, devolve esse mesmo tipo”.

Um parâmetro de tipo representa essa variável de relação: ele será determinado conforme o tipo estático disponível em cada uso da função. Na próxima etapa, você vai declarar essa relação na sintaxe do Python 3.12.

Frente

Uma união repetida na entrada e no retorno garante que o mesmo membro da união será devolvido?

Toque para virar

Passo 2 de 8

Declarar parâmetros de tipo e acompanhar a inferência

Use a sintaxe do Python 3.12 para declarar uma função genérica e acompanhe como o verificador preserva o tipo conhecido em cada chamada.

Declare a relação na própria função

Um parâmetro de tipo local

Quando uma função recebe um valor e o devolve sem transformá-lo, declare um parâmetro de tipo entre colchetes após o nome da função. Em def identidade[T](valor: T) -> T:, o mesmo T liga o argumento ao retorno.

Esse T existe no escopo da função identidade: ele não é um tipo concreto fixado uma única vez no arquivo.

Função genérica no Python 3.12

python
def identidade[T](valor: T) -> T:
    return valor

O mesmo T nos dois lados

Diagrama mostrando uma função identidade com T na entrada e T na saída; três chamadas independentes conectam str a str, int a int e um objeto Pedido a Pedido.

Em cada chamada, o verificador infere um T compatível com o valor estático fornecido e o reutiliza no retorno.

T é inferido a cada chamada

Sem colchetes na chamada

Você chama a função normalmente. Não escreva identidade[str](...): o verificador infere T a partir do argumento disponível naquela chamada.

A inferência preserva o tipo estático que o código já conhece. Ela não inspeciona o objeto em execução para descobrir uma classe mais específica do que a anotação da variável.

Consulte os tipos inferidos

Execute este arquivo com Python 3.12 e verifique-o com python -m mypy nome_do_arquivo.py.

python
def identidade[T](valor: T) -> T:
    return valor

texto = identidade("ativo")
numero = identidade(42)

reveal_type(texto)   # mypy: builtins.str
reveal_type(numero)  # mypy: builtins.int

class Conta:
    pass

class ContaPremium(Conta):
    pass

conta: Conta = ContaPremium()
resultado = identidade(conta)
reveal_type(resultado)  # mypy: Conta, não ContaPremium

Dica

Leia o comentário como previsão

reveal_type é apenas uma consulta ao mypy. Remova essas chamadas antes de executar o arquivo normalmente, pois elas não fazem parte da lógica da aplicação.

Complete a assinatura

Uma relação, um T

Complete a anotação: def eco[T](mensagem: T) -> ___:

Verifique no seu computador

Prática curta

No seu editor, crie um arquivo com a função abaixo. Acrescente duas chamadas: uma com uma lista de inteiros e outra com uma tupla de textos. Depois, ponha reveal_type em cada resultado e execute o mypy. A função deve continuar usando apenas T.

Ponto de partida

python
def primeiro[T](valor: T) -> T:
    return valor

Relate sua observação

Quais tipos o mypy revelou para os dois resultados? Explique em uma frase por que eles podem ser diferentes embora chamem a mesma função.

Escreva pelo menos 30 caracteres (0/30).

Passo 3 de 8

Relacionar o elemento de uma coleção ao resultado

Anote funções de seleção para que o tipo estático de cada elemento da lista seja preservado no valor retornado.

Da lista ao elemento

Uma seleção muda o nível do valor

Uma função que escolhe um item de uma lista recebe uma coleção, mas devolve um elemento dela. Por isso, a relação correta é list[T] → T: o mesmo T representa o tipo dos elementos na entrada e o tipo do resultado.

Não use list[T] → list[T] quando a função retorna apenas um item. Isso prometeria uma coleção como resultado.

A relação preservada

Diagrama mostrando uma lista de elementos do tipo T entrando em uma função de seleção e um único elemento do tipo T saindo dela.

A lista contém valores de T; a seleção devolve um único valor do mesmo T.

Uma assinatura que não apaga o elemento

Selecionar o primeiro item

Python 3.12 ou superior

python
def primeiro_ou_erro[T](itens: list[T]) -> T:
    if not itens:
        raise ValueError("a lista não pode estar vazia")
    return itens[0]

nomes: list[str] = ["Ana", "Bia"]
primeiro_nome = primeiro_ou_erro(nomes)  # str

valores: list[int | str] = [10, "dez"]
primeiro_valor = primeiro_ou_erro(valores)  # int | str

Inferência no ponto de chamada

Em cada chamada, o verificador infere T a partir do tipo conhecido da lista. Com list[str], o retorno é str. Se os elementos já são int | str, o retorno continua sendo int | str; a função não escolhe uma alternativa mais específica por observar o objeto em execução.

Repetir T pede compatibilidade com o tipo inferido. Se houver mais de uma fonte de inferência na chamada, o verificador pode chegar a um tipo mais amplo que satisfaça todas elas.

Dica

Caso vazio sem inventar um T

A exceção encerra o caminho sem devolver um valor. Assim, todo caminho que retorna normalmente devolve um T real: itens[0].

Complete o contrato

Seleção genérica

Complete a anotação de retorno:

def ultimo_ou_erro[T](itens: list[T]) -> ____:

Todos os retornos devem cumprir a promessa

Lista vazia

Por que esta implementação viola o contrato?

def primeiro[T](itens: list[T]) -> T:
if not itens:
return False
return itens[0]

Passo 4 de 8

Respeitar o contrato no corpo genérico

Verifique se o corpo de uma função genérica usa apenas operações garantidas para qualquer tipo que possa ocupar T.

T vale para todos os tipos admitidos

O contrato também vale dentro da função

Em uma função declarada com def identidade[T](valor: T) -> T, T não significa “o tipo dos exemplos que testei”. Ele pode representar qualquer tipo inferido em uma chamada. Por isso, o corpo precisa ser válido para todos esses tipos possíveis.

É seguro receber, armazenar, encaminhar e devolver um valor de T. Já chamar um método exclusivo de str ou somar um número pressupõe um contrato que a assinatura ainda não forneceu.

O que T garante — e o que não garante

Compare operações que apenas transportam o valor com operações que dependem de uma característica específica dele.

Diagrama de uma função genérica recebendo valores de tipos diferentes e devolvendo-os sem alteração; ao lado, tentativas inválidas de chamar um método de texto e de somar um número a valores variados.

T preserva a identidade do tipo, mas não promete métodos ou operadores específicos.

Dica

Regra prática

Pergunte: “Esta operação funciona para qualquer valor que possa ser T?” Se a resposta for não, ajuste a implementação ou declare um contrato que realmente garanta a operação — sem usar Any para esconder o diagnóstico.

Contêiner e elemento têm garantias diferentes

list[T] não torna T uma lista

Uma operação pode ser válida na coleção e inválida em cada elemento. Por exemplo, valores.append(novo) é uma operação da lista; isso não autoriza usar .append() em um valor individual de tipo T.

Revise o corpo, não silencie o erro

Exemplos para salvar em um arquivo Python 3.12 e verificar com mypy.

python
def repetir[T](valor: T) -> tuple[T, T]:
    # Apenas recebe e devolve T: válido para qualquer T.
    return valor, valor


def primeiro[T](valores: list[T]) -> T:
    # A indexação é uma operação da lista; o resultado é T.
    return valores[0]


def normalizar_incorreto[T](valor: T) -> T:
    # Erro estático: T não garante o método lower().
    return valor.lower()


def adicionar_incorreto[T](valor: T) -> T:
    # Erro estático: T não garante suporte a + com int.
    return valor + 1


def incluir[T](valores: list[T], novo: T) -> None:
    # Válido: append pertence a list e novo tem o mesmo T dos elementos.
    valores.append(novo)

# Não faça isto para calar o verificador:
# def normalizar[T](valor: T) -> T:
#     return Any(valor).lower()

Atenção

Não use Any como curativo

Se o mypy aponta que uma operação não existe em T, o problema é a promessa da assinatura ou a lógica genérica. Trocar o valor por Any elimina a verificação justamente onde o contrato precisava proteger você.

Associe cada ação à garantia necessária

Qual garantia justifica a ação?

Relacione cada ação à garantia que torna seu uso válido no corpo de uma função.

Toque em um item e depois no par correspondente.

Explique o diagnóstico

Por que uma função def limpar[T](valor: T) -> T: return valor.lower() é genericamente inválida, mesmo que limpar(" oi ") rode sem erro?

Escreva pelo menos 60 caracteres (0/60).

Passo 5 de 8

Separar os tipos de entrada e saída de uma transformação

Anote uma função que transforma cada elemento de uma lista, preservando a relação entre o tipo recebido pelo callback e o tipo produzido.

Dois tipos, duas funções na relação

Transformar não é apenas repetir o mesmo tipo

Em uma seleção, o elemento de entrada e o resultado podiam compartilhar o mesmo parâmetro de tipo. Já uma transformação pode mudar o tipo: por exemplo, converter uma lista de int em uma lista de str.

Use dois parâmetros independentes: T é o tipo de cada elemento fornecido pela lista, e R é o tipo retornado pelo callback. A assinatura conecta os três pontos: list[T], Callable[[T], R] e list[R].

Fluxo dos tipos na transformação

Diagrama mostrando uma lista de números do tipo T, cada número passando por um callback que recebe T e retorna R, e uma lista final de rótulos do tipo R.

O callback consome um T por vez e produz um R; por isso, a lista resultante é list[R].

Implemente a transformação tipada

Uma versão genérica de mapear

Crie um arquivo transformar.py com este código e execute python -m mypy transformar.py no seu ambiente.

python
from collections.abc import Callable


def mapear[T, R](
    valores: list[T],
    transformar: Callable[[T], R],
) -> list[R]:
    resultado: list[R] = []
    for valor in valores:
        resultado.append(transformar(valor))
    return resultado


def rotulo(numero: int) -> str:
    return f"item-{numero}"

numeros: list[int] = [2, 4, 6]
rotulos = mapear(numeros, rotulo)

reveal_type(rotulos)  # mypy: builtins.list[builtins.str]

Como a inferência se combina

Na chamada mapear(numeros, rotulo), a lista informa T = int. A assinatura de rotulo informa que ela recebe int e devolve str, portanto R = str. Logo, rotulos é inferida como list[str].

Não especialize a função na chamada com colchetes: a inferência usa a coleção e o callback. Remova reveal_type antes de executar o arquivo normalmente.

Dica

Cheque a compatibilidade do callback

A função sempre fornecerá elementos T ao callback. Portanto, o callback precisa aceitar o tipo dos elementos da lista. Com list[int], uma função que exige str não serve, mesmo que ela também retorne str.

Complete o retorno da assinatura

Qual é a lista de saída?

Complete o retorno:

def mapear[T, R](valores: list[T], transformar: Callable[[T], R]) -> ____:
    ...

Diagnostique uma incompatibilidade

Uma chamada que o verificador deve rejeitar

python
from collections.abc import Callable


def mapear[T, R](
    valores: list[T],
    transformar: Callable[[T], R],
) -> list[R]:
    return [transformar(valor) for valor in valores]


def deixar_maiusculo(texto: str) -> str:
    return texto.upper()

numeros: list[int] = [1, 2, 3]
resultado = mapear(numeros, deixar_maiusculo)

Explique o erro de contrato

Execute o código no seu computador com python -m mypy transformar.py e explique por que deixar_maiusculo é incompatível com essa chamada de mapear.

Escreva pelo menos 40 caracteres (0/40).

Passo 6 de 8

Usar um limite superior sem perder o subtipo

Use um limite superior para acessar operações de uma classe base sem apagar o subtipo conhecido em cada chamada.

O limite torna operações justificadas

De T desconhecido para T com contrato

Um parâmetro de tipo irrestrito poderia representar qualquer tipo, então seu corpo não pode pressupor métodos específicos. Quando a função precisa usar uma operação garantida por uma classe base, declare um limite superior:

def normalizar[T: Documento](item: T) -> T:

Aqui, T pode ser Documento ou qualquer subtipo compatível. Dentro da função, o verificador sabe que todo T possui as operações declaradas em Documento.

Base comum, subtipo preservado

O limite permite acessar o contrato comum, mas T mantém a identidade estática do subtipo recebido.

Diagrama mostrando Documento como classe base, com Fatura e Relatorio como subclasses. Uma função genérica recebe e retorna Fatura como Fatura e recebe e retorna Relatorio como Relatorio, enquanto usa uma operação comum definida em Documento.

T: Documento permite usar membros de Documento; o retorno T conserva o subtipo inferido na chamada.

Implementação e retorno relacionado

Uma função que trabalha com Documento

Exemplo em Python 3.12: a operação usada está no contrato da classe base, e o objeto devolvido continua sendo T.

python
class Documento:
    def marcar_revisado(self) -> None:
        print("Revisado")


class Fatura(Documento):
    def calcular_imposto(self) -> float:
        return 42.0


class Relatorio(Documento):
    def exportar_pdf(self) -> None:
        print("PDF exportado")


def revisar[T: Documento](documento: T) -> T:
    documento.marcar_revisado()
    return documento


fatura = revisar(Fatura())
revelar_imposto = fatura.calcular_imposto()

relatorio = revisar(Relatorio())
relatorio.exportar_pdf()

Dica

O papel de cada anotação

O limite T: Documento justifica documento.marcar_revisado(). Repetir T no retorno diz que a função devolve o mesmo tipo estático que recebeu: Fatura continua sendo Fatura, e Relatorio continua sendo Relatorio.

Leia a assinatura

O que a assinatura abaixo comunica?

def revisar[T: Documento](documento: T) -> T:

Não fabrique uma base como se fosse T

Atenção

O limite não transforma Documento em qualquer subtipo

Esta implementação viola o contrato:

def criar_revisado[T: Documento](documento: T) -> T:
    return Documento()

Se a chamada receber uma Fatura, o retorno prometido é Fatura; uma instância nova de Documento não satisfaz essa promessa. O limite afirma que T é compatível com Documento, não que todo Documento seja um T específico.

Contrato estático, não validação em execução

A anotação é analisada pelo verificador de tipos. Ela não adiciona uma checagem automática quando o programa roda, nem impede que dados externos incorretos cheguem ao código. Validações necessárias em tempo de execução continuam sendo responsabilidade explícita do programa.

Justifique o limite e o retorno

Analise uma proposta de função

Considere:

def revisar[T: Documento](documento: T) -> T:
    documento.marcar_revisado()
    return documento

Por que T: Documento é justificável? Por que o retorno é T em vez de Documento?

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8

Decidir quando um genérico agrega informação

Escolha a assinatura que comunica a relação real entre entradas e saída, sem introduzir parâmetros de tipo que não preservam informação útil.

A pergunta decisiva: qual relação deve sobreviver?

Genérico não significa “aceita muitos tipos”

Um parâmetro de tipo é útil quando conecta posições da assinatura. Ele pode dizer, por exemplo: “o retorno tem o mesmo tipo da entrada” (T → T) ou “o callback transforma T em R”.

Se essa relação não existe, um genérico pode só tornar a assinatura mais abstrata, sem informar nada novo ao chamador.

Compare a informação preservada

As três assinaturas podem aceitar valores variados, mas prometem coisas diferentes sobre o resultado.

Diagrama comparando três funções: uma função concreta que sempre retorna str, uma função com união que aceita str ou int e retorna str, e uma função genérica que recebe T e retorna T, com setas indicando a relação preservada apenas na terceira.

A quantidade de tipos aceitos não define a utilidade do genérico; a relação entre as posições da assinatura define.

Exemplo

O retorno decide a escolha

# Resultado sempre é texto: não há relação a preservar.
def rotulo(valor: object) -> str:
    return str(valor)

# As alternativas são aceitas, mas o retorno é sempre int.
def tamanho(codigo: str | bytes) -> int:
    return len(codigo)

# O próprio valor volta: o tipo de entrada determina o retorno.
def identidade[T](valor: T) -> T:
    return valor

rotulo e tamanho não ganham precisão ao declarar [T]. Já identidade precisa de T nas duas posições para manter a informação.

Escolha pelo comportamento

Associe cada necessidade à assinatura

Relacione o comportamento da função à categoria de assinatura mais adequada.

Toque em um item e depois no par correspondente.

Evite um T decorativo

Quando rejeitar o genérico

Compare estas versões:

def registrar[T](valor: T) -> None:
    print(valor)

def registrar(valor: object) -> None:
    print(valor)

Como o retorno é None e não há outra posição ligada a T, o primeiro contrato não preserva uma relação útil. A segunda versão comunica diretamente que a função só recebe um valor para exibi-lo.

Da mesma forma, use uma união quando ela descreve alternativas reais sem correlação: def codigo(valor: int | str) -> str sempre produz str, seja qual for a alternativa recebida.

Justifique a assinatura

Uma função normalizar aceita str ou bytes e sempre devolve str. Qual assinatura você escolheria? Explique por que usar def normalizar[T](valor: T) -> str não melhora o contrato.

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8

Aplicar e verificar as relações de ponta a ponta

Integre seleção, transformação e preservação de subtipos em um único arquivo e confirme a inferência com mypy.

Veja as três relações no mesmo fluxo

Um contrato para cada relação

Nesta aplicação, cada função responde a uma pergunta diferente:

  • selecionar: de list[T], obter um T;
  • transformar: de list[T] e Callable[[T], R], obter list[R];
  • preservar subtipo: aceitar um T limitado por uma classe base, usar uma operação da base e devolver o mesmo T.

Ao compor as chamadas, acompanhe o tipo estático em cada etapa. O objetivo não é aceitar “qualquer coisa”, e sim manter a informação que já existe no ponto de chamada.

Rastreamento dos tipos

O diagrama mostra como cada assinatura transporta a informação de tipo até o resultado.

Diagrama de três fluxos: lista de textos produzindo um texto, lista de textos transformada em lista de inteiros e um relatório passando por uma operação de Documento e permanecendo relatório.

Cada seta preserva ou transforma tipos de acordo com a relação declarada.

Prática local: refatore as assinaturas

Prepare seu arquivo

Crie um arquivo chamado relacoes.py e copie o código abaixo. As implementações já estão completas; substitua apenas as três assinaturas amplas pelas assinaturas genéricas adequadas.

Use parâmetros de tipo declarados diretamente nas funções, na sintaxe do Python 3.12. Não introduza Any, casts nem supressões de diagnóstico.

Código inicial para refatorar

python
from collections.abc import Callable


class Documento:
    def __init__(self, titulo: str) -> None:
        self.titulo = titulo

    def descricao(self) -> str:
        return self.titulo.upper()


class Relatorio(Documento):
    def exportar_pdf(self) -> bytes:
        return b"PDF"


# Troque list[object] e object por uma relação list[T] -> T.
def primeiro(itens: list[object]) -> object:
    return itens[0]


# Declare tipos independentes para a entrada e a saída.
def transformar(
    itens: list[object], funcao: Callable[[object], object]
) -> list[object]:
    return [funcao(item) for item in itens]


# Limite o tipo pela classe Documento e preserve o subtipo no retorno.
def registrar(documento: Documento) -> Documento:
    print(documento.descricao())
    return documento


codigos = ["10", "25"]
numero_inicial = primeiro(codigos)
valores = transformar(codigos, int)
relatorio = Relatorio("Fechamento mensal")
relatorio_registrado = registrar(relatorio)

reveal_type(numero_inicial)
reveal_type(valores)
reveal_type(relatorio_registrado)

Dica

Roteiro de decisão

Para cada função, localize as posições que precisam ficar relacionadas:

  1. O elemento retornado vem da lista? Reutilize um mesmo parâmetro de tipo.
  2. O callback pode mudar o elemento? Separe o tipo recebido do tipo produzido.
  3. O corpo chama descricao()? Use um limite superior que garanta essa operação, mas retorne o próprio parâmetro de tipo limitado.

Confira a inferência e o caminho de retorno

Uma refatoração possível

Compare somente depois de tentar. Execute python -m mypy --strict relacoes.py e leia os três resultados de reveal_type.

python
from collections.abc import Callable


class Documento:
    def __init__(self, titulo: str) -> None:
        self.titulo = titulo

    def descricao(self) -> str:
        return self.titulo.upper()


class Relatorio(Documento):
    def exportar_pdf(self) -> bytes:
        return b"PDF"


def primeiro[T](itens: list[T]) -> T:
    return itens[0]


def transformar[T, R](
    itens: list[T], funcao: Callable[[T], R]
) -> list[R]:
    return [funcao(item) for item in itens]


def registrar[T: Documento](documento: T) -> T:
    print(documento.descricao())
    return documento


codigos = ["10", "25"]
numero_inicial = primeiro(codigos)
valores = transformar(codigos, int)
relatorio = Relatorio("Fechamento mensal")
relatorio_registrado = registrar(relatorio)

reveal_type(numero_inicial)             # builtins.str
reveal_type(valores)                     # builtins.list[builtins.int]
reveal_type(relatorio_registrado)        # relacoes.Relatorio

Atenção

O limite não permite fabricar T

Esta implementação viola o contrato, mesmo que Documento satisfaça o limite:

def registrar[T: Documento](documento: T) -> T:
    print(documento.descricao())
    return Documento("Registro novo")

T pode ser Relatorio; portanto, um Documento novo não é necessariamente um T. Retornar documento respeita todos os subtipos admitidos. Se o resultado realmente devesse ser uma nova instância da base, o retorno teria de ser Documento, e a preservação do subtipo deixaria de fazer parte do contrato.

Revise sem apagar informação

Se o verificador apontar um erro, revise primeiro a relação prometida e todos os caminhos de retorno. Evite “resolver” o diagnóstico com Any ou uma supressão: isso interrompe justamente a evidência estática que você quer acompanhar.

Depois da verificação, remova os reveal_type, pois eles servem apenas à inspeção do mypy.

Síntese e aplicação final

Resumo

Critérios para funções genéricas

  • Use T repetido quando a saída deve manter o tipo relacionado à entrada, como em list[T] -> T.
  • Use T e R quando uma transformação recebe um tipo e produz outro: list[T], Callable[[T], R] e list[R].
  • Use T: ClasseBase quando o corpo precisa de operações garantidas pela base, mas o retorno ainda deve preservar o subtipo recebido.
  • Confirme a relação nas chamadas e em todos os retornos; não use Any para ocultar incompatibilidades.

Relate sua verificação

Após executar o mypy, relate: quais foram os três tipos revelados? Explique também por que retornar um novo Documento em registrar[T: Documento] não cumpre necessariamente o retorno T.

Escreva pelo menos 120 caracteres (0/120).

Relações preservadas

Parabéns! Você concluiu: Preservar relações de tipos em funções genéricas

Concluído! Antes de declarar um genérico, identifique a relação que a assinatura precisa transportar. Se houver relação, expresse-a; se não houver, prefira o contrato mais simples que descreva o comportamento.

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