Protocols e duck typing
Esta aula explora o conceito de duck typing e como o módulo typing.Protocol do Python permite definir protocolos para verificação estrutural de tipos. Você aprenderá a diferença entre tipagem nominal e estrutural, como usar Protocol para criar interfaces implícitas e como isso se compara com ABCs.
Em Python, o duck typing é um princípio que diz: "se anda como um pato e grasna como um pato, então é um pato". Ou seja, em vez de verificar o tipo exato de um objeto, verificamos se ele possui os métodos e atributos necessários para realizar uma determinada operação. Isso confere grande flexibilidade, mas também pode gerar erros em tempo de execução quando um objeto não possui o comportamento esperado.
Para trazer mais segurança sem abrir mão da flexibilidade, o Python introduziu o módulo typing com a classe Protocol, que permite definir interfaces estruturais. Com ela, podemos especificar quais métodos e atributos um objeto deve ter, e então usar essas definições para verificação de tipos estática (com mypy, por exemplo) e em tempo de execução com isinstance e issubclass (a partir do Python 3.12). Nesta aula, vamos mergulhar nesse tópico.
typing.Protocol
O Protocol é uma classe base especial do módulo typing que permite definir uma "interface" estrutural. Você cria uma subclasse de Protocol e declara os métodos e atributos que deseja exigir. Esses métodos podem ter apenas a assinatura (sem implementação), ou podem ter implementações padrão que serão usadas se a classe não as fornecer.
Para usar um Protocol, você o define como uma classe que herda de Protocol. Dentro dela, você declara os métodos e atributos que deseja que os objetos conformes possuam. Veja um exemplo simples:
from typing import Protocol
class Saudacao(Protocol):
def dizer_ola(self) -> str:
...
def cumprimentar(s: Saudacao) -> None:
print(s.dizer_ola())
class Brasileiro:
def dizer_ola(self) -> str:
return "Olá!"
class Americano:
def dizer_ola(self) -> str:
return "Hello!"
cumprimentar(Brasileiro())
cumprimentar(Americano())Neste exemplo, Saudacao é um Protocol que exige um método dizer_ola que retorna uma string. As classes Brasileiro e Americano não herdam de Saudacao, mas são estruturalmente compatíveis, então podem ser passadas para a função cumprimentar. Isso é duck typing com verificação de tipos: se você usar um type checker como o mypy, ele aceitará essas chamadas.
Protocols também podem ter atributos, propriedades e até métodos com implementação padrão. Por exemplo:
from typing import Protocol
class Animal(Protocol):
nome: str
def som(self) -> str:
...
def apresentar(self) -> str:
return f"{self.nome} faz {self.som()}"
class Cachorro:
def __init__(self, nome: str):
self.nome = nome
def som(self) -> str:
return "Au au"
class Gato:
def __init__(self, nome: str):
self.nome = nome
def som(self) -> str:
return "Miau"
c = Cachorro("Rex")
g = Gato("Mingau")
print(c.apresentar())
print(g.apresentar())Observe que apresentar é implementado no Protocol e, portanto, as classes que o implementam estruturalmente herdam esse método. Isso é útil para fornecer funcionalidade padrão.
Structural typing
Tipagem estrutural (structural typing) é um sistema de tipos onde a compatibilidade é determinada pela estrutura do objeto (métodos, atributos) e não pelo nome da classe. Em contraste, a tipagem nominal exige que o objeto seja de uma classe específica ou herde dela. Python tradicionalmente usa tipagem nominal, mas com Protocol e o suporte de type checkers, podemos usar tipagem estrutural de forma explícita.
O duck typing é uma forma implícita de tipagem estrutural: o Python não verifica nada em tempo de compilação (não existe), mas em tempo de execução as chamadas podem falhar se os métodos não existirem. A tipagem estrutural com Protocol permite que ferramentas como mypy detectem erros antes da execução, sem exigir herança.
Um exemplo clássico é a interface de "arquivo". Muitas funções esperam um objeto que tenha um método read(). Com Protocol, podemos definir:
from typing import Protocol
class Arquivo(Protocol):
def read(self) -> str:
...
class StringIO:
def __init__(self, texto: str):
self.texto = texto
def read(self) -> str:
return self.texto
def processar(arq: Arquivo) -> None:
print(arq.read())
processar(StringIO("conteúdo"))Isso permite que qualquer objeto que tenha read() seja usado, mesmo que não herde de uma classe base comum. É uma forma de programação orientada a interfaces sem herança.
vs ABC
ABCs (Abstract Base Classes) são a forma tradicional de definir interfaces em Python. Elas usam herança nominal: uma classe deve herdar da ABC e implementar os métodos abstratos. Por exemplo:
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circulo(Forma):
def __init__(self, raio: float):
self.raio = raio
def area(self) -> float:
return 3.14 * self.raio ** 2
# Circulo herda de Forma e implementa areaCom ABC, se uma classe não herdar de Forma, ela não é considerada uma Forma mesmo que tenha o método area. Isso pode ser rígido e impede o uso de classes existentes que não foram projetadas para herdar da ABC.
Protocol, por outro lado, permite a tipagem estrutural: qualquer classe que tenha os métodos e atributos exigidos é considerada compatível, sem necessidade de herança. Isso é mais flexível e alinhado com o duck typing.
No entanto, ABCs ainda têm seu lugar: eles podem fornecer implementações padrão, métodos concretos e também permitem o registro de subclasses virtuais via register. Além disso, ABCs podem ser usados com isinstance e issubclass em tempo de execução, enquanto Protocols (antes do Python 3.12) não eram suportados por isinstance a menos que você use @runtime_checkable.
Na prática, você pode combinar os dois: usar ABC para fornecer uma hierarquia nominal e Protocol para definir interfaces estruturais. Por exemplo, você pode ter uma ABC Animal e um Protocol Voador para objetos que podem voar.
Exemplos
Vamos ver alguns exemplos práticos de uso de Protocol.
Exemplo 1: Interface de coleção
from typing import Protocol, Iterable, Iterator, List
class Iteravel(Protocol):
def __iter__(self) -> Iterator:
...
def somar(iteravel: Iteravel) -> int:
total = 0
for item in iteravel:
total += item
return total
print(somar([1, 2, 3])) # listas são iteráveis
print(somar(range(5))) # ranges tambémExemplo 2: Protocol com atributos
from typing import Protocol
class Pessoa(Protocol):
nome: str
idade: int
def apresentar(p: Pessoa) -> str:
return f"{p.nome} tem {p.idade} anos"
class Cliente:
def __init__(self, nome: str, idade: int):
self.nome = nome
self.idade = idade
c = Cliente("João", 30)
print(apresentar(c))Exemplo 3: Protocol com métodos estáticos e de classe
from typing import Protocol
class Criador(Protocol):
@staticmethod
def criar() -> "Criador":
...
class Produto:
@staticmethod
def criar() -> "Produto":
return Produto()
def fabricar(c: Criador) -> None:
obj = c.criar()
print(obj)
fabricar(Produto)Exemplo 4: Protocol com runtime_checkable
from typing import Protocol, runtime_checkable
@runtime_checkable
class Voador(Protocol):
def voar(self) -> str:
...
class Passaro:
def voar(self) -> str:
return "Batendo asas"
class Aviao:
def voar(self) -> str:
return "Usando turbinas"
print(isinstance(Passaro(), Voador)) # True
print(isinstance(Aviao(), Voador)) # True
print(issubclass(Passaro, Voador)) # TrueNeste último exemplo, @runtime_checkable permite que isinstance verifique a estrutura do objeto. Isso é útil para validação em tempo de execução.
Boas práticas e observações finais
Ao usar Protocols, lembre-se de:
- Utilizar
...como corpo dos métodos para indicar que são apenas assinaturas. - Usar
@runtime_checkablesomente quando precisar de verificações em tempo de execução, pois isso tem custo de performance. - Preferir Protocol sobre ABC quando você não controla as classes que serão usadas, ou quando deseja uma interface mais flexível.
- Combinar Protocol com type hints para melhorar a documentação e a detecção de erros.
- Lembre-se de que Protocols são principalmente para type checkers; em tempo de execução, sem
@runtime_checkable, eles não são verificados.
Em resumo, Protocols e duck typing são ferramentas poderosas para escrever código flexível e seguro. Com eles, você define contratos estruturais que permitem que diferentes classes sejam usadas de forma intercambiável, mantendo a clareza e a segurança de tipos.
Referências
- Documentação oficial do typing.Protocol
- Mypy: Protocol types
- PEP 544 – Protocols: Structural subtyping
- Documentação oficial do módulo abc
- PEP 246 – Object Adaptation
- Glossário do Python: duck typing
Exercícios
Defina um Protocol chamado
ReprodutorMusicalque exija um métodotocarque receba uma stringmusicae retorne None, e um métodopausarque retorne None. Crie duas classes que implementem esse Protocol sem herdar dele:CDPlayereStreamingPlayer. Depois, escreva uma funçãoexecutarque receba um objeto do tipoReprodutorMusicale chame os dois métodos. Teste com as duas classes.✓ Resposta:from typing import Protocol class ReprodutorMusical(Protocol): def tocar(self, musica: str) -> None: ... def pausar(self) -> None: ... class CDPlayer: def tocar(self, musica: str) -> None: print(f"Tocando CD: {musica}") def pausar(self) -> None: print("Pausando CD") class StreamingPlayer: def tocar(self, musica: str) -> None: print(f"Tocando streaming: {musica}") def pausar(self) -> None: print("Pausando streaming") def executar(reprodutor: ReprodutorMusical) -> None: reprodutor.tocar("Imagine") reprodutor.pausar() executar(CDPlayer()) executar(StreamingPlayer())Qual é a diferença entre tipagem nominal e estrutural? Dê um exemplo em Python para cada uma.
✓ Resposta: A tipagem nominal verifica se o objeto é instância de uma classe específica ou subclasse. Exemplo:isinstance(obj, MinhaClasse). A tipagem estrutural verifica se o objeto possui determinados métodos/atributos, independentemente da classe. Exemplo: com Protocol,isinstance(obj, MeuProtocol)(com runtime_checkable).Explique por que Protocols são mais flexíveis que ABCs para definir interfaces. Dê um cenário onde Protocol é preferível.
✓ Resposta: ABCs exigem herança nominal, então classes externas que não herdam da ABC não são consideradas compatíveis, mesmo tendo os métodos. Protocol permite que qualquer classe com a estrutura correta seja aceita, sem modificar a classe. Preferível quando você não pode alterar as classes existentes, como em bibliotecas de terceiros.O que é
@runtime_checkablee quando você deve usá-lo? Dê um exemplo de uso.✓ Resposta:@runtime_checkableé um decorador que permite queisinstanceeissubclassfuncionem com Protocols, verificando a estrutura. Use quando precisar verificar em tempo de execução se um objeto atende ao Protocol, por exemplo, em validações. Sem ele, essas funções não funcionam com Protocols.Escreva um Protocol chamado
Imprimivelque tenha um métodoimprimir(self) -> str. Crie uma funçãomostrarque aceite umImprimivele imprima o resultado. Em seguida, crie duas classes diferentes que implementem esse Protocol e teste a função.✓ Resposta:from typing import Protocol class Imprimivel(Protocol): def imprimir(self) -> str: ... def mostrar(obj: Imprimivel) -> None: print(obj.imprimir()) class Documento: def imprimir(self) -> str: return "Conteúdo do documento" class Imagem: def imprimir(self) -> str: return "Imagem renderizada" mostrar(Documento()) mostrar(Imagem())