Programe mirando uma interface, não uma implementação.
Gamma, Helm, Johnson, Vlissides, First Principle of Object-Oriented Design Design Patterns: Elements of Reusable Object-Oriented Software, "Introduction," p. 18.
A programação orientada a objetos tem tudo a ver com interfaces. A melhor abordagem para entender um tipo em Python é conhecer os métodos que aquele tipo oferece—sua interface—como discutimos na [types_defined_by_ops_sec] do ([type_hints_in_def_ch]).
Dependendo da linguagem de programação, temos uma ou mais maneiras de definir e usar interfaces. Desde o Python 3.8, temos quatro maneiras. Elas estão ilustradas no Mapa de Sistemas de Tipagem (Figura 1). Podemos resumi-las assim:
- Duck typing (tipagem pato)
-
A abordagem default do Python para tipagem desde o início. Estamos estudando duck typing desde [data_model].
- Goose typing (tipagem ganso)
-
A abordagem suportada pelas classes base abstratas (ABCs, sigla em inglês para Abstract Base Classes) desde o Python 2.6, que depende de verificações dos objetos como as ABCs durante a execução. A tipagem ganso é um dos principais temas desse capítulo.
- Tipagem estática
-
A abordagem tradicional das linguagens de tipos estáticos como C e Java; suportada desde o Python 3.5 pelo módulo
typing
, e aplicada por verificadores de tipo externos compatíveis com a PEP 484—Type Hints. Este não é o foco desse capítulo. A maior parte do [type_hints_in_def_ch] e do [more_types_ch] mais adiante são sobre tipagem estática. - Duck typing estática
-
Uma abordagem popularizada pela linguagem Go; suportada por subclasses de
typing.Protocol
—lançada no Python 3.8 e também aplicada com o suporte de verificadores de tipo externos. Tratamos desse tema pela primeira vez em [protocols_in_fn] ([type_hints_in_def_ch]), e continuamos nesse capítulo.
As quatro abordagens retratadas na Figura 1 são complementares: elas tem diferentes prós e contras. Não faz sentido descartar qualquer uma delas.
Cada uma dessas quatro abordagens dependem de interfaces para funcionarem, mas a tipagem estática pode ser implementada de forma limitada usando apenas tipos concretos em vez de abstrações de interfaces como protocolos e classes base abstratas. Este capítulo é sobre duck typing, goose typing (tipagem ganso), e duck typing estática - disciplinas de tipagem com foco em interfaces.
O capítulo está dividido em quatro seções principais, tratando de três dos quatro quadrantes no Mapa de Sistemas de Tipagem. (Figura 1):
-
Dois tipos de protocolos compara duas formas de tipagem estrutural com protocolos - isto é, o lado esquerdo do Mapa.
-
Programando patos se aprofunda no já familiar duck typing do Python, incluindo como fazê-lo mais seguro e ao mesmo tempo preservar sua melhor qualidade: a flexibilidade.
-
Goose typing explica o uso de ABCs para um checagem de tipo mais estrita durante a execução do código. É a seção mais longa, não por ser a mais importante, mas porque há mais seções sobre duck typing, duck typing estático e tipagem estática em outras partes do livro.
-
Protocolos estáticos cobre o uso, a implementação e o design de subclasses de
typing.Protocol
— úteis para checagem de tipo estática e durante a execução.
Este capítulo foi bastante modificado, e é cerca de 24% mais longo que o capítulo correspondente (o capítulo 11) na primeira edição de Python Fluente. Apesar de algumas seções e muitos parágrafos serem idênticos, há muito conteúdo novo. Estes são os principais acréscimos e modificações:
-
A introdução do capítulo e o Mapa de Sistemas de Tipagem (Figura 1) são novos. Essa é a chave da maior parte do conteúdo novo - e de todos os outros capítulos relacionados à tipagem em Python ≥ 3.8.
-
Dois tipos de protocolos explica as semelhanças e diferenças entre protocolos dinâmicos e estáticos.
-
Programação defensiva e "falhe rápido" praticamente reproduz o conteúdo da primeira edição, mas foi atualizada e agora tem um título de seção que enfatiza sua importância.
-
Protocolos estáticos é toda nova. Ela se apoia na apresentação inicial em [protocols_in_fn] ([type_hints_in_def_ch]).
-
Os diagramas de classe de
collections.abc
nas Figuras #sequence_uml_repeat, #mutablesequence_uml, and #collections_uml foram atualizados para incluir aCollection
ABC, do Python 3.6.
A primeira edição de Python Fluente tinha uma seção encorajando o uso das ABCs numbers
para goose typing.
Na As ABCs em numbers e os novod protocolos numéricos eu explico porque, em vez disso, você deve usar protocolos numéricos estáticos do módulo typing
se você planeja usar verificadores de tipo estáticos, ou checagem durante a execução no estilo da goose typing.
A palavra protocolo tem significados diferentes na ciência da computação, dependendo do contexto.
Um protocolo de rede como o HTTP especifica comandos que um cliente pode enviar para um servidor, tais como GET
, PUT
e HEAD
.
Vimos na [protocol_duck_section] que um objeto protocolo especifica métodos que um objeto precisa oferecer para cumprir um papel.
O exemplo FrenchDeck
no [data_model] demonstra um objeto protocolo, o protocolo de sequência: os métodos que permitem a um objeto Python se comportar como uma sequência.
Implementar um protocolo completo pode exigir muitos métodos, mas muitas vezes não há problema em implementar apenas parte dele.
Considere a classe Vowels
no Exemplo 1.
__getitem__
>>> class Vowels:
... def __getitem__(self, i):
... return 'AEIOU'[i]
...
>>> v = Vowels()
>>> v[0]
'A'
>>> v[-1]
'U'
>>> for c in v: print(c)
...
A
E
I
O
U
>>> 'E' in v
True
>>> 'Z' in v
False
Implementar __getitem__
é o suficiente para obter itens pelo índice, e também para permitir iteração e o operador in
.
O método especial __getitem__
é de fato o ponto central do protocolo de sequência.
Veja essa parte do Manual de referência da API Python/C, "Seção Protocolo de Sequência":
int PySequence_Check(PyObject *o)
-
Retorna
1
se o objeto oferecer o protocolo de sequência, caso contrário retorna0
. Observe que ela retorna1
para classes Python com um método__getitem__
, a menos que sejam subclasses dedict
[…]
Esperamos que uma sequência também suporte len()
, através da implementação de __len__
.
Vowels
não tem um método __len__
, mas ainda assim se comporta como uma sequência em alguns contextos.
E isso pode ser o suficiente para nossos propósitos.
Por isso que gosto de dizer que um protocolo é uma "interface informal."
Também é assim que protocolos são entendidos em Smalltalk, o primeiro ambiente de programação orientado a objetos a usar esse termo.
Exceto em páginas sobre programação de redes, a maioria dos usos da palavra "protocolo" na documentação do Python se refere a essas interfaces informais.
Agora, com a adoção da PEP 544—Protocols: Structural subtyping (static duck typing) (EN) no Python 3.8, a palavra "protocolo" ganhou um novo sentido em Python - um sentido próximo, mas diferente.
Como vimos na [protocols_in_fn] ([type_hints_in_def_ch]), a PEP 544 nos permite criar subclasses de typing.Protocol
para definir um ou mais métodos que uma classe deve implementar (ou herdar) para satisfazer um verificador de tipo estático.
Quando precisar ser específico, vou adotar os seguintes termos:
- Protocolo dinâmico
-
Os protocolos informais que o Python sempre teve. Protocolos dinâmicos são implícitos, definidos por convenção e descritos na documentação. Os protocolos dinâmicos mais importantes do Python são mantidos pelo próprio interpretador, e documentados no capítulo "Modelo de Dados" em A Referência da Linguagem Python.
- Protocolo estático
-
Um protocolo como definido pela PEP 544—Protocols: Structural subtyping (static duck typing), a partir do Python 3.8. Um protocolo estático tem um definição explícita: uma subclasse de
typing.Protocol
.
Há duas diferenças fundamentais entre eles:
-
Um objeto pode implementar apenas parte de um protocolo dinâmico e ainda assim ser útil; mas para satisfazer um protocolo estático, o objeto precisa oferecer todos os métodos declarados na classe do protocolo, mesmo se seu programa não precise de todos eles.
-
Protocolos estáticos podem ser inspecionados por verificadores de tipo estáticos, protocolos dinâmicos não.
Os dois tipos de protocolo compartilham um característica essencial, uma classe nunca precisa declarar que suporta um protocolo pelo nome, isto é, por herança.
Além de protocolos estáticos, o Python também oferece outra forma de definir uma interface explícita no código: uma classe base abstrata (ABC).
O restante deste capítulo trata de protocolos dinâmicos e estáticos, bem como das ABCs.
Vamos começar nossa discussão de protocolos dinâmicos com os dois mais importantes em Python: o protocolo de sequência e o iterável. O interpretador faz grandes esforços para lidar com objetos que fornecem mesmo uma implementação mínima desses protocolos, como explicado na próxima seção.
A filosofia do Modelo de Dados do Python é cooperar o máximo possível com os protocolos dinâmicos essenciais. Quando se trata de sequências, o Python faz de tudo para lidar mesmo com as mais simples implementações.
A Figura 2 mostra como a interface Sequence
está formalizada como uma ABC.
O interpretador Python e as sequências embutidas como list
, str
, etc., não dependem de forma alguma daquela ABC.
Só estou usando a figura para descrever o que uma Sequence
completa deve oferecer.
Sequence
e classes abstratas relacionadas de collections.abc
. As setas de herança apontam de uma subclasse para suas superclasses. Nomes em itálico são métodos abstratos. Antes do Python 3.6, não existia uma ABC Collection
- Sequence
era uma subclasse direta de Container
, Iterable
e Sized
.
Tip
|
A maior parte das ABCs no módulo |
Estudando a Figura 2, vemos que uma subclasse correta de Sequence
deve implementar __getitem__
e __len__
(de Sized
).
Todos os outros métodos Sequence
são concretos, então as subclasses podem herdar suas implementações - ou fornecer versões melhores.
Agora, lembre-se da classe Vowels
no Exemplo 1.
Ela não herda de abc.Sequence
e implementa apenas __getitem__
.
Não há um método __iter__
, mas as instâncias de Vowels
são iteráveis porque - como alternativa - se o Python encontra um método __getitem__
, tenta iterar sobre o object chamando aquele método com índices inteiros começando de 0
.
Da mesma forma que o Python é esperto o suficiente para iterar sobre instâncias de Vowels
, ele também consegue fazer o operador in
funcionar mesmo quando o método __contains__
não existe: ele faz uma busca sequencial para verificar se o item está presente.
Em resumo, dada a importância de estruturas como a sequência, o Python consegue fazer a iteração e o operador in
funcionarem invocando __getitem__
quando __iter__
e __contains__
não estão presentes.
O FrenchDeck
original de [data_model] também não é subclasse de abc.Sequence
,
mas ele implementa os dois métodos do protocolo de sequência: __getitem__
e __len__
.
Veja o Exemplo 2.
link:code/01-data-model/frenchdeck.py[role=include]
Muitos dos exemplos no [data_model] funcionam por causa do tratamento especial que o Python dá a qualquer estrutura vagamente semelhante a uma sequência. O protocolo iterável em Python representa uma forma extrema de duck typing: o interpretador tenta dois métodos diferentes para iterar sobre objetos.
Para deixar mais claro, os comportamentos que que descrevi nessa seção estão implementados no próprio interpretador, na maioria dos casos em C.
Eles não dependem dos métodos da ABC Sequence
.
Por exemplo, os métodos concretos __iter__
e __contains__
na classe Sequence
emulam comportamentos internos do interpretador Python.
Se tiver curiosidade, veja o código-fonte destes métodos em Lib/_collections_abc.py.
Agora vamos estudar outro exemplo que enfatiza a natureza dinâmica dos protocolos - e mostra porque verificadores de tipo estáticos não tem como lidar com eles.
Monkey patching é a ação de modificar dinamicamente um módulo, uma classe ou uma função durante a execução do código, para acrescentar funcionalidade ou corrigir bugs.
Por exemplo, a biblioteca de rede gevent faz um "monkey patch" em partes da biblioteca padrão do Python, para permitir concorrência com baixo impacto, sem threads ou async
/await
.[1]
A classe FrenchDeck
do Exemplo 2 não tem uma funcionalidade essencial: ela não pode ser embaralhada.
Anos atrás, quando escrevi pela primeira vez o exemplo FrenchDeck
, implementei um método shuffle
.
Depois tive um insight pythônico: se um FrenchDeck
age como uma sequência, então ele não precisa de seu próprio método shuffle
, pois já existe um random.shuffle
,
documentado como "Embaralha a sequência x internamente."
A função random.shuffle
padrão é usada assim:
>>> from random import shuffle
>>> l = list(range(10))
>>> shuffle(l)
>>> l
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]
Tip
|
Quando você segue protocolos estabelecidos, você melhora suas chances de aproveitar o código já existente na biblioteca padrão e em bibliotecas de terceiros, graças ao duck typing. |
Entretanto, se tentamos usar shuffle com uma instância de FrenchDeck
ocorre uma exceção, como visto no Exemplo 3.
random.shuffle
cannot handle FrenchDeck
>>> from random import shuffle
>>> from frenchdeck import FrenchDeck
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../random.py", line 265, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment
A mensagem de erro é clara: O objeto 'FrenchDeck' não suporta a atribuição de itens
.
O problema é que shuffle opera internamente, trocando os itens de lugar dentro da coleção, e FrenchDeck
só implementa o protocolo de sequência imutável.
Sequências mutáveis precisam também oferecer um método __setitem__
.
Como o Python é dinâmico, podemos consertar isso durante a execução, até mesmo no console interativo. O Exemplo 4 mostra como fazer isso.
FrenchDeck
para torná-lo mutável e compatível com random.shuffle
(continuação do Exemplo 3)>>> def set_card(deck, position, card): (1)
... deck._cards[position] = card
...
>>> FrenchDeck.__setitem__ = set_card (2)
>>> shuffle(deck) (3)
>>> deck[:5]
[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4',
suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]
-
Cria uma função que recebe
deck
,position
, ecard
como argumentos. -
Atribui aquela função a um atributo chamado
__setitem__
na classeFrenchDeck
. -
deck
agora pode ser embaralhado, pois acrescentei o método necessário do protocolo de sequência mutável.
A assinatura do método especial __setitem__
está definida na A Referência da Linguagem Python em "3.3.6. Emulando de tipos contêineres".
Aqui nomeei os argumentos deck, position, card
—e não self, key, value
como na referência da linguagem - para mostrar que todo método Python começa sua vida como uma função comum, e nomear o primeiro argumento self
é só uma convenção.
Isso está bom para uma sessão no console, mas em um arquivo de código-fonte de Python é muito melhor usar self
, key
, e value
, seguindo a documentação.
O truque é que set_card
sabe que o deck
tem um atributo chamado cards
,
e _cards
tem que ser uma sequência mutável.
A função set_cards
é então anexada à classe FrenchDeck
class como o método especial __setitem__
.
Isso é um exemplo de _monkey patching:
modificar uma classe ou módulo durante a execução, sem tocar no código finte.
O "monkey patching" é poderoso, mas o código que efetivamente executa a modificação está muito intimamente ligado ao programa sendo modificado, muitas vezes trabalhando com atributos privados e não-documentados.
Além de ser um exemplo de "monkey patching", o
Exemplo 4 enfatiza a natureza dinâmica dos protocolos no duck typing dinâmico:
random.shuffle
não se importa com a classe do argumento, ela só precisa que o objeto implemente métodos do protocolo de sequência mutável.
Não importa sequer se o objeto "nasceu" com os métodos necessários ou se eles foram de alguma forma adquiridos depois.
O duck typing não precisa ser loucamente inseguro ou difícil de depurar e manter. A próxima seção mostra alguns padrões de programação úteis para detectar protocolos dinâmicos sem recorrer a verificações explícitas.
Programação defensiva é como direção defensiva: um conjunto de práticas para melhorar a segurança, mesmo quando defrontando programadores (ou motoristas) negligentes.
Muitos bugs não podem ser encontrados exceto durante a execução - mesmo nas principais linguagens de tipagem estática.[2] Em uma linguagem de tipagem dinâmica, "falhe rápido" é um conselho excelente para gerar programas mais seguros e mais fáceis de manter. Falhar rápido significa provocar erros de tempo de execução o mais cedo possível. Por exemplo, rejeitando argumentos inválidos no início do corpo de uma função.
Aqui está um exemplo:
quando você escreve código que aceita uma sequência de itens para processar internamente como uma list
, não imponha um argumento list
através de checagem de tipo.
Em vez disso, receba o argumento e construa imediatamente uma list
a partir dele.
Um exemplo desse padrão de programação é o método __init__
no Exemplo 10,
visto mais à frente nesse capítulo:
def __init__(self, iterable):
self._balls = list(iterable)
Dessa forma você torna seu código mais flexível, pois o construtor de list()
processa qualquer iterável que caiba na memória.
Se o argumento não for iterável, a chamada vai falhar rapidamente com uma exceção de TypeError
bastante clara, no exato momento em que o objeto for inicializado.
Se você quiser ser mais explícito, pode envelopar a chamada a list()
em um try/except
, para adequar a mensagem de erro - mas eu usaria aquele código extra apenas em uma API externa, pois o problema ficaria mais visível para os mantenedores da base de código.
De toda forma, a chamada errônea vai aparecer perto do final do traceback, tornando-a fácil de corrigir.
Se você não barrar o argumento inválido no construtor da classe, o programa vai quebrar mais tarde, quando algum outro método da classe precisar usar a variável self.balls
e ela não for uma list
.
Então a causa primeira do problema será mais difícil de encontrar.
Naturalmente, seria ruim passar o argumento para list()
se os dados não devem ser copiados, ou por seu tamanho ou porque quem chama a função, por projeto, espera que os itens sejam modificados internamente, como no caso de random.shuffle
.
Neste caso, uma verificação durante a execução como isinstance(x, abc.MutableSequence)
seria a melhor opção,
Se você estiver com receio de produzir um gerador infinito - algo que não é um problema muito comum - pode começar chamando len()
com o argumento.
Isso rejeitaria iteradores, mas lidaria de forma segura com tuplas, arrays e outras classes existentes ou futuras que implementem a interface Sequence
completa.
Chamar len()
normalmente não custa muito, e um argumento inválido gerará imediatamente um erro.
Por outro lado, se um iterável for aceitável, chame iter(x)
assim que possível, para obter um iterador, como veremos na [iter_func_sec].
E novamente, se x
não for iterável, isso falhará rapidamente com um exceção fácil de depurar.
Nos casos que acabei de descrever, uma dica de tipo poderia apontar alguns problemas mais cedo, mas não todos os problemas.
Lembre-se que o tipo Any
é consistente-com qualquer outro tipo.
Inferência de tipo pode fazer com que uma variável seja marcada com o tipo Any
.
Quando isso acontece, o verificador de tipo se torna inútil.
Além disso, dicas de tipo não são aplicadas durante a execução.
Falhar rápido é a última linha de defesa.
Código defensivo usando tipos "duck" também podem incluir lógica para lidar com tipos diferentes sem usar testes com isinstance()
e hasattr()
.
Um exemplo é como poderíamos emular o modo como collections.namedtuple
lida com o argumento field_names
:
field_names
aceita um única string, com identificadores separados por espaços ou vírgulas, ou uma sequência de identificadores.
O Exemplo 5 mostra como eu faria isso usando duck typing.
try: (1)
field_names = field_names.replace(',', ' ').split() (2)
except AttributeError: (3)
pass (4)
field_names = tuple(field_names) (5)
if not all(s.isidentifier() for s in field_names): (6)
raise ValueError('field_names must all be valid identifiers')
-
Supõe que é uma string (MFPP - mais fácil pedir perdão que permissão).
-
Converte vírgulas em espaços e divide o resultado em uma lista de nomes.
-
Desculpe,
field_names
não grasna como umastr
: não tem.replace
, ou retorna algo que não conseguimos passar para.split
-
Se um
AttributeError
aconteceu, entãofield_names
não é umastr
. Supomos que já é um iterável de nomes. -
Para ter certeza que é um iterável e para manter nossas própria cópia, criamos uma tupla com o que temos. Uma
tuple
é mais compacta que umalist
, e também impede que meu código troque os nomes por engano. -
Usamos
str.isidentifier
para se assegurar que todos os nomes são válidos.
O Exemplo 5 mostra uma situação onde o duck typing é mais expressivo que dicas de tipo estáticas.
Não há como escrever uma dica de tipo que diga "`field_names` deve ser uma string de identificadores separados por espaços ou vírgulas."
Essa é a parte relevante da assinatura de namedtuple
no typeshed
(veja o código-fonte completo em stdlib/3/collections/__init__.pyi):
def namedtuple(
typename: str,
field_names: Union[str, Iterable[str]],
*,
# rest of signature omitted
Como se vê, field_names
está anotado como Union[str, Iterable[str]]
, que serve para seus propósitos, mas não é suficiente para evitar todos os problemas possíveis.
Após revisar protocolos dinâmicos, passamos para uma forma mais explícita de checagem de tipo durante a execução: goose typing.
Uma classe abstrata representa uma interface.
Bjarne Stroustrup, criador do C++. Bjarne Stroustrup, The Design and Evolution of C++, p. 278 (Addison-Wesley).
O Python não tem uma palavra-chave interface
. Usamos classes base abstratas (ABCs) para definir interfaces passíveis de checagem explícita de tipo durante a execução - também suportado por verificadores de tipo estáticos.
O verbete para classe base abstrata no Glossário da Documentação do Python tem uma boa explicação do valor dessas estruturas para linguagens que usam duck typing:
Classes bases abstratas complementam [a] tipagem pato, fornecendo uma maneira de definir interfaces quando outras técnicas, como
hasattr()
, seriam desajeitadas ou sutilmente erradas (por exemplo, com métodos mágicos). CBAs introduzem subclasses virtuais, classes que não herdam de uma classe mas ainda são reconhecidas porisinstance()
eissubclass()
; veja a documentação do móduloabc
.[3]
A goose typing é uma abordagem à checagem de tipo durante a execução que se apoia nas ABCs. Vou deixar que Alex Martelli explique, no Pássaros aquáticos e as ABCs.
Note
|
Eu sou muito agradecido a meus amigos Alex MArtekli e Anna Ravenscroft. Mostrei a eles o primeiro rescunho do Python Fluente na OSCON 2013, e eles me encorajaram a submeter à O’Reilly para publicação. Mais tarde os dois contribuíram com revisões técnicas minuciosas. Alex já era a pessoa mais citada nesse livro, e então se ofereceu para escrever esse ensaio. Segue daí, Alex! |
By Alex Martelli
Eu recebi créditos na Wikipedia por ter ajudado a popularizar o útil meme e a frase de efeito "duck typing" (isto é, ignorar o tipo efetivo de um objeto, e em vez disso se dedicar a assegurar que o objeto implementa os nomes, assinaturas e semântica dos métodos necessários para o uso pretendido).
Em Python, isso essencialmente significa evitar o uso de isinstance
para verificar o tipo do objeto (sem nem mencionar a abordagem ainda pior de verificar, por exemplo, se type(foo) is bar
—que é corretamente considerado um anátema, pois inibe até as formas mais simples de herança!).
No geral, a abordagem da duck typing continua muito útil em inúmeros contextos - mas em muitos outros, um nova abordagem muitas vezes preferível evoluiu ao longo do tempo. E aqui começa nossa história…
Em gerações recentes, a taxinomia de gênero e espécies (incluindo, mas não limitada à família de pássaros aquáticos conhecida como Anatidae) foi guiada principalmente pela fenética - uma abordagem focalizada nas similaridades de morfologia e comportamento… principalmente traços observáveis. A analogia com o "duck typing" era patente.
Entretanto, a evolução paralela muitas vezes pode produzir características similares, tanto morfológicas quanto comportamentais, em espécies sem qualquer relação de parentesco, que apenas calharam de evoluir em nichos ecológicos similares, porém separados. "Similaridades acidentais" parecidas acontecem também em programação - por exemplo, considere o [seguinte] exemplo clássico de programação orientada a objetos:
class Artist:
def draw(self): ...
class Gunslinger:
def draw(self): ...
class Lottery:
def draw(self): ...
Obviamente, a mera existência de um método chamado draw
, chamado sem argumentos, está longe de ser suficiente para garantir que dois objetos x
e y
, da forma como x.draw()
e y.draw()
podem ser chamados, são de qualquer forma intercambiáveis ou abstratamente equivalentes — nada sobre a similaridade da semântica resultante de tais chamadas pode ser inferido. Na verdade, é necessário um programador inteligente para, de alguma forma, assegurar positivamente que tal equivalência é verdadeira em algum nível.
Em biologia (e outras disciplinas), este problema levou à emergência (e, em muitas facetas, à dominância) de uma abordagem alternativa à fenética, conhecida como cladística — que baseia as escolhas taxinômicas em características herdadas de ancestrais comuns em vez daquelas que evoluíram de forma independente (o sequenciamento de DNA cada vez mais barato e rápido vem tornando a cladística bastante prática em mais casos).
Por exemplo, os Chloephaga, gênero de gansos sul-americanos (antes classificados como próximos a outros gansos) e as tadornas (gênero de patos sul-americanos) estão agora agrupados juntos na subfamília Tadornidae (sugerindo que eles são mais próximos entre si que de qualquer outro Anatidae, pois compartilham um ancestral comum mais próximo). Além disso, a análise de DNA mostrou que o Asarcornis (pato da floresta ou pato de asas brancas) não é tão próximo do Cairina moschata (pato-do-mato), esse último uma tadorna, como as similaridades corporais e comportamentais sugeriram por tanto tempo - então o pato da floresta foi reclassificado em um gênero próprio, inteiramente fora da subfamília!
Isso importa? Depende do contexto! Para o propósito de decidir como cozinhar uma ave depois de caçá-la, por exemplo, características observáveis específicas (mas nem todas - a plumagem, por exemplo, é de mínima importância nesse contexto), especialmente textura e sabor (a boa e velha fenética), podem ser muito mais relevantes que a cladística. Mas para outros problemas, tal como a suscetibilidade a diferentes patógenos (se você estiver tentando criar aves aquáticas em cativeiro, ou preservá-las na natureza), a proximidade do DNA por ser muito mais crucial.
Então, a partir dessa analogia bem frouxa com as revoluções taxonômicas no mundo das aves aquáticas, estou recomendando suplementar (não substitui inteiramente - em determinados contexto ela ainda servirá) a boa e velha duck typing com… a goose typing (tipagem ganso)!
A goose typing significa o seguinte: isinstance(obj, cls)
agora é plenamente aceitável… desde que cls
seja uma classe base abstrata - em outras palavras, a metaclasse de cls é abc.ABCMeta
.
Você vai encontrar muitas classes abstratas prontas em collections.abc
(e outras no módulo numbers
da Biblioteca Padrão do Python)[4]
Dentre as muitas vantagens conceituais das ABCs sobre classes concretas
(e.g., Scott Meyer’s “toda classe não-final ("não-folha") deveria ser abstrata”; veja o Item 33 de seu livro, More Effective C++, Addison-Wesley), as ABCs do Python acrescentam uma grande vantagem prática: o método de classe register
, que permite ao código do usuário final "declarar" que determinada classe é uma subclasse "virtual" de uma ABC (para este propósito, a classe registrada precisa cumprir os requerimentos de nome de métodos e assinatura da ABC e, mais importante, o contrato semântico subjacente - mas não precisa ter sido desenvolvida com qualquer conhecimento da ABC, e especificamente não precisa herdar dela!).
Isso é um longo caminho andado na direção de quebrar a rigidez e o acoplamento forte que torna herança algo para ser usado com muito mais cautela que aquela tipicamente praticada pela maioria do programadores orientados a .objetos.
Em algumas ocasiões você sequer precisa registrar uma classe para que uma ABC a reconheça como uma subclasse!
Esse é o caso para as ABCs cuja essência se resume em alguns métodos especiais. Por exemplo:
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
Como se vê, abc.Sized
reconhece Struggle
como uma subclasse
, sem necessidade de registro, já que implementar o método especial chamado __len__
é o suficiente (o método deve ser implementado com a sintaxe e semântica corretas - deve poder ser chamado sem argumentos e retornar um inteiro não-negativo indicando o "comprimento" do objeto; mas qualquer código que implemente um método com nome especial, como __len__
, com uma sintaxe e uma semântica arbitrárias e incompatíveis tem problemas muitos maiores que esses).
Então, aqui está minha mensagem de despedida: sempre que você estiver implementando uma classe que incorpore qualquer dos conceitos representados nas ABCs de number
, collections.abc
ou em outro framework que estiver usando, se assegure (caso necessário) de ser uma subclasse ou de registrar sua classe com a ABC correspondente. No início de seu programa usando uma biblioteca ou framework que definam classes que omitiram esse passo, registre você mesmo as classes. Daí, quando precisar verificar se (tipicamente) um argumento é, por exemplo, "uma sequência", verifique se:
isinstance(the_arg, collections.abc.Sequence)
E não defina ABCs personalizadas (ou metaclasses) em código de produção. Se você sentir uma forte necessidade de fazer isso, aposto que é um caso da síndrome de "todos os problemas se parecem com um prego" em alguém que acabou de ganhar um novo martelo brilhante - você ( e os futuros mantenedores de seu código) serão muito mais felizes se limitando a código simples e direto, e evitando tais profundezas. Valē!
Em resumo, goose typing implica:
-
Criar subclasses de ABCs, para tornar explícito que você está implementando uma interface previamente definida.
-
Checagem de tipo durante a execução usando as ABCs em vez de classes concretas como segundo argumento para
isinstance
eissubclass
.
Alex também aponta que herdar de uma ABC é mais que implementar os métodos necessários: é também uma declaração de intenções clara da parte do desenvolvedor. A intenção também pode ficar explícita através do registro de uma subclasse virtual.
Note
|
Detalhes sobre o uso de from collections.abc import Sequence
Sequence.register(FrenchDeck) |
O uso de isinstance
e issubclass
se torna mais aceitável se você está verificando ABCs em vez de classes concretas.
Se usadas com classes concretas, verificações de tipo limitam o polimorfismo - um recurso essencial da programação orientada a objetos.
Mas com ABCs esses testes são mais flexíveis.
Afinal, se um componente não implementa uma ABC sendo uma subclasse - mas implementa os métodos necessários - ele sempre pode ser registrado posteriormente e passar naquelas verificações de tipo explícitas.
Entretanto, mesmo com ABCs, você deve se precaver contra o uso excessivo de verificações com isinstance
, pois isso poder ser um code smell— sintoma de um design ruim.
Normalmente não é bom ter uma série de if/elif/elif
com verificações de isinstance
executando ações diferentes, dependendo do tipo de objeto: nesse caso você deveria estar usando polimorfismo - isto é, projetando suas classes para permitir ao interpretador enviar chamadas para os métodos corretos, em vez de codificar diretamente a lógica de envio em blocos if/elif/elif
.
Por outro lado, não há problema em executar uma verificação com isinstance
contra uma ABC se você quer garantir um contrato de API:
"Cara, você tem que implementar isso se quiser me chamar," como costuma dizer o revisor técnico Lennart Regebro.
Isso é especialmente útil em sistemas com arquitetura plug-in.
Fora dos frameworks, duck typing é muitas vezes mais simples e flexível que verificações de tipo.
Por fim, em seu ensaio Alex reforça mais de uma vez a necessidade de coibir a criação de ABCs. Uso excessivo de ABCs imporia cerimônia a uma linguagem que se tornou popular por ser prática e pragmática. Durante o processo de revisão do Python Fluente, Alex me enviou uma email:
ABCs servem para encapsular conceitos muito genéricos, abstrações introduzidos por um framework - coisa como "uma sequência" e "um número exato". [Os leitores] quase certamente não precisam escrever alguma nova ABC, apenas usar as já existentes de forma correta, para obter 99% dos benefícios sem qualquer risco sério de design mal-feito.
Agora vamos ver a goose typing na prática.
Seguindo o conselho de Martelli, vamos aproveitar uma ABC existente, collections.MutableSequence
, antes de ousar inventar uma nova.
No Exemplo 6, FrenchDeck2
é explicitamente declarada como subclasse de collections.MutableSequence
.
FrenchDeck2
, uma subclasse de collections.MutableSequence
link:code/13-protocol-abc/frenchdeck2.py[role=include]
-
__setitem__
é tudo que precisamos para possibilitar o embaralhamento… -
…mas uma subclasse de
MutableSequence
é forçada a implementar__delitem__
, um método abstrato daquela ABC. -
Também precisamos implementar
insert
, o terceiro método abstrato deMutableSequence
.
O Python não verifica a implementação de métodos abstratos durante a importação
(quando o módulo frenchdeck2.py é carregado na memória e compilado),
mas apenas durante a execução, quando nós tentamos de fato instanciar FrenchDeck2
.
Ali, se deixamos de implementar qualquer dos métodos abstratos,
recebemos uma exceção de TypeError
com uma mensagem como
"Can't instantiate
abstract class FrenchDeck2 with abstract methods __delitem__, insert"
("Impossível instanciar a classe abstrata FrenchDeck2 com os métodos abstratos __delitem__
, insert
").
Por isso precisamos implementar __delitem__
e insert
,
mesmo se nossos exemplos usando FrenchDeck2
não precisem desses comportamentos: a ABC MutableSequence
os exige.
Como Figura 3 mostra, nem todos os métodos das ABCs Sequence
e MutableSequence
ABCs são abstratos.
MutableSequence
e suas superclasses em collections.abc
(as setas de herança apontam das subclasses para as ancestrais; nomes em itálico são classes e métodos abstratos).Para escrever FrenchDeck2
como uma subclasse de MutableSequence
, tive que pagar o preço de implementar __delitem__
e insert
, desnecessários em meus exemplos.
Em troca, FrenchDeck2
herda cinco métodos concretos de Sequence
:
__contains__
, __iter__
, __reversed__
, index
, e count
.
De MutableSequence
, ela recebe outros seis métodos: append
, reverse
, extend
, pop
, remove
, e __iadd__
— que suporta o operador +=
para concatenação direta.
Os métodos concretos em cada ABC de collections.abc
são implementados nos termos da interface pública da classe, então funcionam sem qualquer conhecimento da estrutura interna das instâncias.
Tip
|
Como programador de uma subclasse concreta, é possível sobrepor os métodos herdados das ABCs com implementações mais eficientes.
Por exemplo, Veja "Managing Ordered Sequences with Bisect" (EN) em fluentpython.com para conhecer mais sobre esse método. |
Para usar bem as ABCs, você precisa saber o que está disponível. Vamos então revisar as ABCs de collections
a seguir.
Desde o Python 2.6, a biblioteca padrão oferece várias ABCs. A maioria está definida no módulo collections.abc
, mas há outras.
Você pode encontrar ABCs nos pacotes io
e numbers
, por exemplo.
Mas a maioria das mais usadas estão em collections.abc
.
Tip
|
Há dois módulos chamados |
A Figura 4 é um diagrama de classe resumido (sem os nomes dos atributos) das 17 ABCs definidas em collections.abc
.
A documentação de collections.abc
inclui uma ótima tabela resumindo as ABCs, suas relações e seus métodos abstratos e concretos (chamados "métodos mixin").
Há muita herança múltipla acontecendo na Figura 4.
Vamos dedicar a maior parte de [herança] à herança múltipla,
mas por hora é suficiente dizer que isso normalmente não causa problemas no caso das ABCs.[5]
Vamos revisar os grupos em Figura 4:
Iterable
,Container
,Sized
-
Toda coleção deveria ou herdar dessas ABCs ou implementar protocolos compatíveis.
Iterable
oferece iteração com__iter__
,Container
oferece o operadorin
com__contains__
, eSized
oferecelen()
with__len__
. Collection
-
Essa ABC não tem nenhum método próprio, mas foi acrescentada no Python 3.6 para facilitar a criação de subclasses de
Iterable
,Container
, eSized
. Sequence
,Mapping
,Set
-
Esses são os principais tipos de coleções imutáveis, e cada um tem uma subclasse mutável. Um diagrama detalhado de
MutableSequence
é apresentado em Figura 3; paraMutableMapping
eMutableSet
, veja as Figuras #mapping_uml e #set_uml em [dicts-a-to-z]. MappingView
-
No Python 3, os objetos retornados pelos métodos de mapeamentos
.items()
,.keys()
, e.values()
implementam as interfaces definidas emItemsView
,KeysView
, eValuesView
, respectivamente. Os dois primeiros também implementam a rica interface deSet
, com todos os operadores que vimos na [set_op_section]. Iterator
-
Observe que iterator é subclasse de
Iterable
. Discutimos melhor isso adiante, em [iterables2generators]. Callable
,Hashable
-
Essas não são coleções, mas
collections.abc
foi o primeiro pacote a definir ABCs na biblioteca padrão, e essas duas foram consideradas importante o suficiente para serem incluídas. Elas suportam a verificação de tipo de objetos que precisam ser "chamáveis" ou hashable.
Para a detecção de 'callable', a função nativa callable(obj)
é muito mais conveniente que insinstance(obj, Callable)
.
Se insinstance(obj, Hashable)
retornar False
, você pode ter certeza que obj
não é hashable. Mas se ela retornar True
, pode ser um falso positivo.
Isso é explicado no box seguinte.
É fácil interpretar errado os resultados de testes usando isinstance
e issubclass
com as ABCs Hashable
and Iterable
.
Se isinstance(obj, Hashable)
retorna True
, is significa apenas que a classe de obj
implementa ou herda __hash__
.
Mas se obj
é uma tupla contendo itens unhashable, então obj
não é hashable, apesar do resultado positivo da verificação com isinstance
.
O revisor técnico Jürgen Gmach esclareceu que o duck typing fornece a forma mais precisa de determinar se uma instância é hashable: chamar hash(obj)
.
Essa chamada vai levantar um TypeError
se obj
não for hashable.
Por outro lado, mesmo quando isinstance(obj, Iterable)
retorna False
,
o Python ainda pode ser capaz de iterar sobre obj
usando __getitem__
com índices baseados em 0,
como vimos em [data_model] e na O Python curte sequências.
A documentação de
collections.abc.Iterable
afirma:
A única maneira confiável de determinar se um objeto é iterável é chamar iter(obj).
Após vermos algumas das ABCs existentes, vamos praticar goose typing implementando uma ABC do zero, e a colocando em uso. O objetivo aqui não é encorajar todo mundo a ficar criando ABCs a torto e a direito, mas aprender como ler o código-fonte das ABCs encontradas na biblioteca padrão e em outros pacotes.
Essa advertência estava no capítulo "Interfaces" da primeira edição de Python Fluente:
ABCs, como os descritores e as metaclasses, são ferramentas para criar frameworks, Assim, apenas uma pequena minoria dos desenvolvedores Python podem criar ABCs sem impor limitações pouco razoáveis e trabalho desnecessário a seus colegas programadores.
Agora ABCs tem mais casos de uso potenciais, em dicas de tipo para permitir tipagem estática. Como discutido na [type_hint_abc_sec], usar ABCs em vez de tipo concretos em dicas de tipos de argumentos de função dá mais flexibilidade a quem chama a função.
Para justificar a criação de uma ABC, precisamos pensar em um contexto para usá-la como um ponto de extensão em um framework.
Então aqui está nosso contexto: imagine que você precisa exibir publicidade em um site ou em uma app de celular, em ordem aleatória, mas sem repetir um anúncio antes que o inventário completo de anúncios tenha sido exibido.
Agora vamos presumir que estamos desenvolvendo um gerenciador de publicidade chamado ADAM
.
Um dos requerimentos é permitir o uso de classes de escolha aleatória não repetida fornecidas pelo usuário.[6]
Para deixar claro aos usuário do ADAM
o que se espera de um componente de "escolha aleatória não repetida", vamos definir uma ABC.
Na bibliografia sobre estruturas de dados, "stack" e "queue" descrevem interfaces abstratas em termos dos arranjos físicos dos objetos.
Vamos seguir o mesmo caminho e usar uma metáfora do mundo real para batizar nossa ABC:
gaiolas de bingo e sorteadores de loteria são máquinas projetadas para escolher aleatoriamente itens de um conjunto, finito sem repetições, até o conjunto ser exaurido. Vamos chamar a ABC de Tombola
, seguindo o nome italiano do bingo, e do recipiente giratório que mistura os números.
A ABC Tombola
tem quatro métodos. Os dois métodos abstratos são:
.load(…)
-
Coloca itens no container.
.pick()
-
Remove e retorna um item aleatório do container.
Os métodos concretos são:
.loaded()
-
Retorna
True
se existir pelo menos um item no container. .inspect()
-
Retorna uma
tuple
construída a partir dos itens atualmente no container, sem modificar o conteúdo (a ordem interna não é preservada).
A Figura 5 mostra a ABC Tombola
e três implementações concretas.
Tombola
e de seus métodos abstratos estão escritos em itálico, segundo as convenções da UML. A seta tracejada é usada para implementações de interface - as estou usando aqui para mostrar que TomboList
implementa não apenas a interface Tombola
, mas também está registrada como uma subclasse virtual de Tombola
- como veremos mais tarde nesse capítulo.«registrada» and «subclasse virtual» não são termos da UML padrão. Estão sendo usados para representar uma relação de classe específica do Python.O Exemplo 7 mostra a definição da ABC Tombola
.
Tombola
é uma ABC com dois métodos abstratos e dois métodos concretos.link:code/13-protocol-abc/tombola.py[role=include]
-
Para definir uma ABC, crie uma subclasse de
abc.ABC
. -
Um método abstrato é marcado com o decorador
@abstractmethod
, e muitas vezes seu corpo é vazio, exceto por uma docstring.[7] -
A docstring instrui os implementadores a levantarem
LookupError
se não existirem itens para escolher. -
Uma ABC pode incluir métodos concretos.
-
Métodos concretos em uma ABC devem depender apenas da interface definida pela ABC (isto é, outros métodos concretos ou abstratos ou propriedades da ABC).
-
Não sabemos como as subclasses concretas vão armazenar os itens, mas podemos escrever o resultado de
inspect
esvaziando aTombola
com chamadas sucessivas a.pick()
… -
…e então usando
.load(…)
para colocar tudo de volta.
Tip
|
Um método abstrato na verdade pode ter uma implementação.
Mas mesmo que tenha, as subclasses ainda são obrigadas a sobrepô-lo, mas poderão invocar o método abstrato com |
O código para o método .inspect()
é simplório,
mas mostra que podemos confiar em .pick()
e .load(…)
para inspecionar o que está dentro de Tombola
, puxando e devolvendo os itens - sem saber como eles são efetivamente armazenados.
O objetivo desse exemplo é ressaltar que não há problema em oferecer métodos concretos em ABCs, desde que eles dependam apenas de outros métodos na interface.
Conhecendo suas estruturas de dados internas, as subclasses concretas de Tombola
podem sempre sobrepor .inspect()
com uma implementação mais adequada, mas não são obrigadas a fazer isso.
O método .loaded()
no Exemplo 7 tem uma linha, mas é custoso:
ele chama .inspect()
para criar a tuple
apenas para aplicar bool()
nela.
Funciona, mas subclasses concretas podem fazer bem melhor, como veremos.
Observe que nossa implementação tortuosa de .inspect()
exige a captura de um LookupError
lançado por self.pick()
.
O fato de self.pick()
poder disparar um LookupError
também é parte de sua interface, mas não há como tornar isso explícito em Python, exceto na documentação (veja a docstring para o método abstrato pick
no Exemplo 7).
Eu escolhi a exceção LookupError
por sua posição na hierarquia de exceções em relação a IndexError
e KeyError
,
as exceções mais comuns de ocorrerem nas estruturas de dados usadas para implementar uma Tombola
concreta.
Dessa forma, as implementações podem lançar LookupError
, IndexError
, KeyError
, ou uma subclasse personalizada de LookupError
para atender à interface.
Veja a Figura 6.
Exception
.[8]➊ LookupError
é a exceção que tratamos em Tombola.inspect
.
➋ IndexError
é a subclasse de LookupError
gerada quando tentamos acessar um item em uma sequência usando um índice além da última posição.
➌ KeyError
ocorre quando usamos uma chave inexistente para acessar um item em um mapeamento (dict
etc.).
Agora temos nossa própria ABC Tombola
. Para observar a checagem da interface feita por uma ABC, vamos tentar enganar Tombola
com uma implementação defeituosa no Exemplo 8.
Tombola
falsa não passa desapercebida>>> from tombola import Tombola
>>> class Fake(Tombola): # (1)
... def pick(self):
... return 13
...
>>> Fake # (2)
<class '__main__.Fake'>
>>> f = Fake() # (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract method load
-
Declara
Fake
como subclasse deTombola
. -
A classe é criada, nenhum erro até agora.
-
Um
TypeError
é sinalizado quando tentamos instanciarFake
. A mensagem é bastante clara:Fake
é considerada abstrata porque deixou de implementarload
, um dos métodos abstratos declarados na ABCTombola
.
Então definimos nossa primeira ABC, e a usamos para validar uma classe.
Logo vamos criar uma subclasse de Tombola
, mas primeiro temos que falar sobre algumas regras para a programação de ABCs.
A forma padrão de declarar uma ABC é criar uma subclasse de abc.ABC
ou de alguma outra ABC.
Além da classe base ABC e do decorador @abstractmethod
, o módulo abc
define
os decoradores @abstractclassmethod
, @abstractstaticmethod
, and @abstractproperty
.
Entretanto, os três últimos foram descontinuados no Python 3.3,
quando se tornou possível empilhar decoradores sobre @abstractmethod
, tornando os outros redundantes.
Por exemplo, a maneira preferível de declarar um método de classe abstrato é:
class MyABC(abc.ABC):
@classmethod
@abc.abstractmethod
def an_abstract_classmethod(cls, ...):
pass
Warning
|
A ordem dos decoradores de função empilhados importa, e no caso de
Em outras palavras, nenhum outro decorador pode aparecer entre |
Agora que abordamos essas questões de sintaxe das ABCs, vamos colocar Tombola
em uso, implementando dois descendentes concretos dessa classe.
Dada a ABC Tombola
, vamos agora desenvolver duas subclasses concretas que satisfazem a interface.
Essas classes estão ilustradas na Figura 5, junto com a subclasse virtual que será discutida na seção seguinte.
A classe BingoCage
no Exemplo 9 é uma variação da [ex_bingo_callable] usando um randomizador melhor.
BingoCage
implementa os métodos abstratos obrigatórios load
e pick
.
BingoCage
é uma subclasse concreta de Tombola
link:code/13-protocol-abc/bingo.py[role=include]
-
Essa classe
BingoCage
estendeTombola
explicitamente. -
Finja que vamos usar isso para um jogo online.
random.SystemRandom
implementa a APIrandom
sobre a funçãoos.urandom(…)
, que fornece bytes aleatórios "adequados para uso em criptografia", segundo a documentação do móduloos
. -
Delega o carregamento inicial para o método
.load()
-
Em vez da função
random.shuffle()
normal, usamos o método.shuffle()
de nossa instância deSystemRandom
. -
pick
é implementado como em [ex_bingo_callable]. -
__call__
também é de [ex_bingo_callable]. Ele não é necessário para satisfazer a interface deTombola
, mas não há nenhum problema em adicionar métodos extra.
BingoCage
herda o custoso método loaded
e o tolo inspect
de Tombola
.
Ambos poderiam ser sobrepostos com métodos de uma linha muito mais rápidos, como no Exemplo 10. A questão é: podemos ser preguiçosos e escolher apenas herdar os método concretos menos que ideais de uma ABC. Os métodos herdados de Tombola
não são tão rápidos quanto poderia ser em BingoCage
, mas fornecem os resultados esperados para qualquer subclasse de Tombola
que implemente pick
e load
corretamente.
O Exemplo 10 mostra uma implementação muito diferente mas igualmente válida da interface de Tombola
.
Em vez de misturar as "bolas" e tirar a última, LottoBlower
tira um item de uma posição aleatória..
LottoBlower
é uma subclasse concreta que sobrecarrega os métodos inspect
e loaded
de Tombola
link:code/13-protocol-abc/lotto.py[role=include]
-
O construtor aceita qualquer iterável: o argumento é usado para construir uma lista.
-
a função
random.randrange(…)
levanta umValueError
se a faixa de valores estiver vazia, então capturamos esse erro e trocamos porLookupError
, para ser compatível comTombola
. -
Caso contrário, o item selecionado aleatoriamente é retirado de
self._balls
. -
Sobrepõe
loaded
para evitar a chamada ainspect
(comoTombola.loaded
faz no Exemplo 7). Podemos fazer isso mais rápido rápida trabalhando diretamente comself._balls
— não há necessidade de criar toda uma novatuple
. -
Sobrepõe
inspect
com uma linha de código.
O Exemplo 10 ilustra um idioma que vale a pena mencionar:
em __init__
, self._balls
armazena list(iterable)
, e não apenas uma referência para iterable
(isto é, nós não meramente atribuímos self._balls = iterable
, apelidando o argumento).
Como mencionado na Programação defensiva e "falhe rápido", isso torna nossa LottoBlower
flexível, pois o argumento iterable
pode ser de qualquer tipo iterável.
Ao mesmo tempo, garantimos que os itens serão armazenados em uma list
, da onde podemos pop
os itens.
E mesmo se nós sempre recebêssemos listas no argumento iterable
,
list(iterable)
produz uma cópia do argumento, o que é uma boa prática, considerando que vamos remover itens dali, e o cliente pode não estar esperando que a lista passada seja modificada.[10]
Chegamos agora à característica dinâmica crucial da goose typing:
declarar subclasses virtuais com o método register
Uma característica essencial da goose typing - e uma razão pela qual ela merece um nome de ave aquática - é a habilidade de registrar uma classe como uma subclasse virtual de uma ABC, mesmo se a classe não herde da ABC. Ao fazer isso, prometemos que a classe implementa fielmente a interface definida na ABC - e o Python vai acreditar em nós sem checar. Se mentirmos, vamos ser capturados pelas exceções de tempo de execução conhecidas.
Isso é feito chamando um método de classe register
da ABC, e será reconhecido assim por issubclass
, mas não implica na herança de qualquer método ou atributo da ABC.
Warning
|
Subclasses virtuais não herdam da ABC na qual se registram, e sua conformidade com a interface da ABC nunca é checada, nem quando são instanciadas. E mais, neste momento verificadores de tipo estáticos não conseguem tratar subclasses virtuais. Mais detalhes em Mypy issue 2922—ABCMeta.register support. |
O método register
normalmente é invocado como uma função comum (veja O Uso de register na Prática),
mas também pode ser usado como decorador. No Exemplo 11, usamos a sintaxe de decorador e implementamos TomboList
, uma subclasse virtual de Tombola
, ilustrada em Figura 7.
TomboList
, subclasse real de list
e subclassse virtual de Tombola
.TomboList
é uma subclasse virtual de Tombola
link:code/13-protocol-abc/tombolist.py[role=include]
-
TomboList
é registrada como subclasse virtual deTombola
. -
TomboList
estendelist
. -
TomboList
herda seu comportamento booleano delist
, e isso retornaTrue
se a lista não estiver vazia. -
Nosso
pick
chamaself.pop
, herdado delist
, passando um índice aleatório para um item. -
TomboList.load
é o mesmo quelist.extend
. -
loaded
delega parabool
.[11] -
É sempre possível chamar
register
dessa forma, e é útil fazer assim quando você precisa registrar uma classe que você não mantém, mas que implementa a interface.
Note que, por causa do registro,
as funções issubclass
e isinstance
agem como se TomboList
fosse uma subclasse de Tombola
:
>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True
Entretanto, a herança é guiada por um atributo de classe especial chamado __mro__
—a Ordem de Resolução do Método (mro é a sigla de Method Resolution Order).
Esse atributo basicamente lista a classe e suas superclasses na ordem que o Python usa para procurar métodos.[12]
Se você inspecionar o __mro__
de TomboList
,
verá que ele lista apenas as superclasses "reais" - list
e object
:
>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)
Tombola
não está em TomboList.__mro__
, então TomboList
não herda nenhum método de Tombola
.
Isso conclui nosso estudo de caso da ABC Tombola
.
Na próxima seção, vamos falar sobre como a função register
das ABCs é usada na vida real.
No Exemplo 11, usamos Tombola.register
como um decorador de classe.
Antes do Python 3.3, register
não podia ser usado dessa forma - ele tinha que ser chamado, como uma função normal, após a definição da classe, como sugerido pelo comentário no final do Exemplo 11.
Entretanto, ainda hoje ele mais usado como uma função para registrar classes definidas em outro lugar.
Por exemplo, no código-fonte do módulo collections.abc
,
os tipos nativos tuple
, str
, range
, e memoryview
são registrados como subclasses virtuais de Sequence
assim:
Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
Vários outros tipo nativos estão registrados com as ABCs em _collections_abc.py.
Esses registros ocorrem apenas quando aquele módulo é importado,
o que não causa problema, pois você terá mesmo que importar o módulo para obter as ABCs.
Por exemplo, você precisa importar MutableMapping
de collections.abc
para verificar algo como isinstance(my_dict, MutableMapping)
.
Criar uma subclasse de uma ABC ou se registrar com uma ABC são duas maneiras explícitas de fazer nossas classes passarem verificações com issubclass
e isinstance
(que também se apoia em issubclass
).
Mas algumas ABCs também suportam tipagem estrutural. A próxima seção explica isso.
As ABCs são usadas principalmente com tipagem nominal.
Quando uma classe Sub
herda explicitamente de AnABC
, ou está registrada com AnABC
, o nome de
AnABC
fica ligado ao da classe Sub
— e é assim que, durante a execução, issubclass(AnABC, Sub)
retorna True
.
Em contraste, a tipagem estrutural diz respeito a olhar para a estrutura da interface pública de um objeto para determinar seu tipo: um objeto é consistente-com um tipo se implementa os métodos definidos no tipo.[13] O duck typing estático e o dinâmico são duas abordagens à tipagem estrutural.
E ocorre que algumas ABCs também suportam tipagem estrutural,
Em seu ensaio, Pássaros aquáticos e as ABCs, Alex mostra que uma classe pode ser reconhecida como subclasse de uma ABC mesmo sem registro.
Aqui está novamente o exemplo dele, com um teste adicional usando issubclass
:
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
A classe Struggle
é considerada uma subclasse de abc.Sized
pela função issubclass
(e, consequentemente, também por isinstance
) porque abc.Sized
implementa um método de classe especial chamado __subclasshook__
.
O __subclasshook__
de Sized
verifica se o argumento classe tem um atributo chamado __len__
.
Se tiver, então a classe é considerada uma subclasse virtual de Sized
.
Veja Exemplo 12.
Sized
no código-fonte de Lib/_collections_abc.pyclass Sized(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
if any("__len__" in B.__dict__ for B in C.__mro__): # (1)
return True # (2)
return NotImplemented # (3)
-
Se há um atributo chamado
__len__
no__dict__
de qualquer classe listada emC.__mro__
(isto é,C
e suas superclasses)… -
…retorna
True
, sinalizando queC
é uma subclasse virtual deSized
. -
Caso contrário retorna
NotImplemented
, para permitir que a verificação de subclasse continue.
Note
|
Se você tiver interesse nos detalhes da verificação de subclasse,
estude o código-fonte do método |
É assim que __subclasshook__
permite às ABCs suportarem a tipagem estrutural.
Você pode formalizar uma interface com uma ABC, você pode fazer isinstance
verificar com a ABC,
e ainda ter um classe sem qualquer relação passando uma verificação de issubclass
porque ela implementa um certo método.
(ou porque ela faz o que quer que seja necessário para convencer um __subclasshook__
a dar a ela seu aval).
É uma boa ideia implementar __subclasshook__
em nossas próprias ABCs? Provavelmente não.
Todas as implementações de __subclasshook__
que eu vi no código-fonte do Python estão em ABCs como Sized
, que declara apenas um método especial, e elas simplesmente verificam a presença do nome daquele método especial.
Dado seu status "especial", é quase certeza que qualquer método chamado __len__
faz o que se espera.
Mas mesmo no reino dos métodos especiais e ABCs fundamentais,
pode ser arriscado fazer tais suposições.
Por exemplo, mapeamentos implementam __len__
, __getitem__
, e __iter__
, mas corretamente não são considerados subtipos de Sequence
,
pois você não pode recuperar itens usando deslocamentos inteiros ou faixas.
Por isso a classe abc.Sequence
não implementa __subclasshook__
.
Para ABCs que você ou eu podemos escrever, um __subclasshook__
seria ainda menos confiável.
Não estou preparado para acreditar que qualquer classe chamada Spam
que implemente ou herde
load
, pick
, inspect
, e loaded
vai necessariamente se comportar como uma Tombola
.
É melhor deixar o programador afirmar isso, fazendo de Spam
uma subclasse de Tombola
, ou registrando a classe com Tombola.register(Spam)
.
Claro, o seu __subclasshook__
poderia também verificar assinaturas de métodos e outras características, mas não creio que valha o esforço.
Note
|
Vimos algo sobre protocolos estáticos em [protocols_in_fn] ([type_hints_in_def_ch]). Até considerei deixar toda a discussão sobre protocolos para esse capítulo, mas decidi que a apresentação inicial de dicas de tipo em funções precisava incluir protocolos, pois o duck typing é uma parte essencial do Python, e verificação de tipo estática sem protocolos não consegue lidar muito bem com as APIs pythônicas. |
Vamos encerrar esse capítulo ilustrando os protocolos estáticos com dois exemplos simples, e uma discussão sobre as ABCs numéricas e protocolos.
Começaremos mostrando como um protocolo estático torna possível anotar e verificar tipos na função double()
, que vimos antes na [types_defined_by_ops_sec].
Quando eu apresento Python para programadores mais acostumados com uma linguagem de tipagem estática, um de meus exemplos favoritos é essa função double
simples:
>>> def double(x):
... return x * 2
...
>>> double(1.5)
3.0
>>> double('A')
'AA'
>>> double([10, 20, 30])
[10, 20, 30, 10, 20, 30]
>>> from fractions import Fraction
>>> double(Fraction(2, 5))
Fraction(4, 5)
Antes da introdução dos protocolos estáticos, não havia uma forma prática de acrescentar dicas de tipo a double
sem limitar seus usos possíveis.[14]
Graças ao duck typing, double
funciona mesmo com tipos do futuro, tal como a classe Vector
aprimorada que veremos no [overloading_mul] ([operator_overloading]):
>>> from vector_v7 import Vector
>>> double(Vector([11.0, 12.0, 13.0]))
Vector([22.0, 24.0, 26.0])
A implementação inicial de dicas de tipo no Python era um sistema de tipos nominal: o nome de um tipo em uma anotação tinha que corresponder ao nome do tipo do argumento real - ou com o nome de uma de suas superclasses. Como é impossível nomear todos os tipos que implementam um protocolo (suportando as operações requeridas), a duck typing não podia ser descrita por dicas de tipo antes do Python 3.8.
Agora, com typing.Protocol
, podemos informar ao Mypy que double
recebe um argumento x
que suporta x * 2
.
O Exemplo 13 mostra como.
double
usando um Protocol
.link:code/13-protocol-abc/double/double_protocol.py[role=include]
-
Vamos usar esse
T
na assinatura de__mul__
. -
__mul__
é a essência do protocoloRepeatable
. O parâmetroself
normalmente não é anotado - presume-se que seu tipo seja a classe. Aqui usamosT
para assegurar que o tipo do resultado é o mesmo tipo deself
. Além disso observe querepeat_count
está limitado nesse protocolo aint
. -
A variável de tipo
RT
é vinculada pelo protocoloRepeatable
: o verificador de tipo vai exigir que o tipo efetivo implementeRepeatable
. -
Agora o verificador de tipo pode verificar que o parâmetro
x
é um objeto que pode ser multiplicado por um inteiro, e que o valor retornado tem o mesmo tipo quex
.
Este exemplo mostra porque o título da PEP 544 é
"Protocols: Structural subtyping (static duck typing). (Protocolos: Subtipagem estrutural (duck typing estático))."
O tipo nominal de x
, argumento efetivamente passado a double
, é irrelevante, desde que grasne - ou seja, desde que implemente __mul__
.
No Mapa de Tipagem (Figura 1), typing.Protocol
aparece na área de verificação estática - a metade inferior do diagrama.
Entretanto, ao definir uma subclasse de typing.Protocol
, você pode usar o decorador @runtime_checkable
para fazer aquele protocolo aceitar verificações com isinstance/issubclass
durante a execução.
Isso funciona porque typing.Protocol
é uma ABC, assim suporta o __subclasshook__
que vimos na Tipagem estrutural com ABCs.
No Python 3.9, o módulo typing
inclui sete protocolos prontos para uso que são verificáveis durante a execução.
Aqui estão dois deles, citados diretamente da documentação de typing
:
class typing.SupportsComplex
-
An ABC with one abstract method,
__complex__
. ("Uma ABC com um método abstrato,__complex__
.") class typing.SupportsFloat
-
An ABC with one abstract method,
__float__
. ("Uma ABC com um método abstrato,__float__
.")
Esse protocolos foram projetados para verificar a "convertibilidade" de tipos numéricos:
se um objeto o
implementa __complex__
,
então deveria ser possível obter um complex
invocando complex(o)
— pois o método especial __complex__
existe para suportar a função embutida complex()
.
Exemplo 14 mostra o
código-fonte
do protocolo typing.SupportsComplex
.
typing.SupportsComplex
@runtime_checkable
class SupportsComplex(Protocol):
"""An ABC with one abstract method __complex__."""
__slots__ = ()
@abstractmethod
def __complex__(self) -> complex:
pass
A chave é o método abstrato __complex__
.[15]
Durante a checagem de tipo estática, um objeto será considerado consistente-com o protocolo SupportsComplex
se implementar um método __complex__
que recebe apenas self
e retorna um complex
.
Graças ao decorador de classe @runtime_checkable
, aplicado a SupportsComplex
,
aquele protocolo também pode ser utilizado em verificações com isinstance
no Exemplo 15.
SupportsComplex
durante a execução>>> from typing import SupportsComplex
>>> import numpy as np
>>> c64 = np.complex64(3+4j) # (1)
>>> isinstance(c64, complex) # (2)
False
>>> isinstance(c64, SupportsComplex) # (3)
True
>>> c = complex(c64) # (4)
>>> c
(3+4j)
>>> isinstance(c, SupportsComplex) # (5)
False
>>> complex(c)
(3+4j)
-
complex64
é um dos cinco tipos de números complexos fornecidos pelo NumPy. -
Nenhum dos tipos complexos do NumPy é subclasse do
complex
embutido. -
Mas os tipos complexos de NumPy implementam
__complex__
, então cumprem o protocoloSupportsComplex
. -
Portanto, você pode criar objetos
complex
a partir deles. -
Infelizmente, o tipo
complex
embutido não implementa__complex__
, apesar decomplex(c)
funcionar sem problemas sec
for umcomplex
.
Como consequência deste último ponto, se você quiser testar se um objeto c
é um complex
ou SupportsComplex
,
você pode passar uma tupla de tipos como segundo argumento para isinstance
, assim:
isinstance(c, (complex, SupportsComplex))
Uma outra alternativa seria usar a ABC Complex
, definida no módulo numbers
.
O tipo embutido complex
e os tipos complex64
e complex128
do NumPy são todos registrados como subclasses virtuais de numbers.Complex
, então isso aqui funciona:
>>> import numbers
>>> isinstance(c, numbers.Complex)
True
>>> isinstance(c64, numbers.Complex)
True
Na primeira edição de Python Fluente eu recomendava o uso das ABCs de numbers
, mas agora esse não é mais um bom conselho, pois aquelas ABCs não são reconhecidas pelos verificadores de tipo estáticos, como veremos na As ABCs em numbers e os novod protocolos numéricos.
Nessa seção eu queria demonstrar que um protocolo verificável durante a execução funciona com isinstance
, mas na verdade esse exemplo não é um caso de uso particularmente bom de isinstance
, como a barra lateral O Duck Typing É Seu Amigo explica.
Tip
|
Se você estiver usando um verificador de tipo externo, há uma vantagem nas verificações explícitas com |
Durante a execução, muitas vezes o duck typing é a melhor abordagem para verificação de tipo:
em vez de chamar isinstance
ou hasattr
, apenas tente realizar as operações que você precisa com o objeto, e trate as exceções conforme necessário. Aqui está um exemplo concreto:
Continuando a discussão anterior: dado um objeto o
que eu preciso usar como número complexo,
essa seria uma abordagem:
if isinstance(o, (complex, SupportsComplex)):
# do something that requires `o` to be convertible to complex
else:
raise TypeError('o must be convertible to complex')
A abordagem da goose typing seria usar a ABC numbers.Complex
:
if isinstance(o, numbers.Complex):
# do something with `o`, an instance of `Complex`
else:
raise TypeError('o must be an instance of Complex')
Eu, entretanto, prefiro aproveitar o duck typing e fazer isso usando o princípio do MFDP - mais fácil pedir desculpas que permissão:
try:
c = complex(o)
except TypeError as exc:
raise TypeError('o must be convertible to complex') from exc
E se de qualquer forma tudo que você vai fazer é levantar um TypeError
,
eu então omitiria o bloco try/except/raise
e escreveria apenas isso:
c = complex(o)
Nesse último caso, se o
não for de um tipo aceitável,
o Python vai levantar uma exceção com uma mensagem bem clara.
Por exemplo, se o
for uma tuple
, esse é o resultado:
TypeError: complex() first argument must be a string or a number, not 'tuple' ("O primeiro argumento de complex() deve ser uma string ou um número, não 'tuple'")
Acho a abordagem duck typing muito melhor nesse caso.
Agora que vimos como usar protocolos estáticos durante a execução com tipos pré-existentes como complex
e numpy.complex64
,
precisamos discutir as limitações de protocolos verificáveis durante a execução.
Vimos que dicas de tipo são geralmente ignoradas durante a execução,
e isso também afeta o uso de verificações com isinstance
or issubclass
com protocolos estáticos.
Por exemplo, qualquer classe com um método __float__
é considerada - durante a execução - uma subclasse virtual de SupportsFloat
,
mesmo se seu método __float__
não retorne um float
.
Veja essa sessão no console:
>>> import sys
>>> sys.version
'3.9.5 (v3.9.5:0a7dcbdb13, May 3 2021, 13:17:02) \n[Clang 6.0 (clang-600.0.57)]'
>>> c = 3+4j
>>> c.__float__
<method-wrapper '__float__' of complex object at 0x10a16c590>
>>> c.__float__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float
Em Python 3.9, o tipo complex
tem um método __float__
,
mas ele existe apenas para gerar TypeError
com uma mensagem de erro explícita.
Se aquele método __float__
tivesse anotações,
o tipo de retorno seria NoReturn
— que vimos na [noreturn_sec].
Mas incluir dicas de tipo em complex.__float__
no typeshed não resolveria esse problema,
porque o interpretador Python em geral ignora dicas de tipo—e também não acessa os arquivos stub do typeshed.
Continuando da sessão anterior de Python 3.9:
>>> from typing import SupportsFloat
>>> c = 3+4j
>>> isinstance(c, SupportsFloat)
True
>>> issubclass(complex, SupportsFloat)
True
Então temos resultados enganosos: as verificações durante a execução usando SupportsFloat
sugerem que você pode converter um complex
para float
, mas na verdade isso gera um erro de tipo.
Warning
|
O problema específico com o tipo Mas o problema geral persiste:
Verificações com |
Agora veremos como implementar um protocolo estático em uma classe definida pelo usuário.
Lembra da classe Vector2d
, que desenvolvemos em [pythonic_objects]?
Dado que tanto um número complex
quanto uma instância de Vector2d
consistem em um par de números de ponto flutuante, faz sentido suportar a conversão de Vector2d
para complex
.
O Exemplo 16 mostra a implementação do método __complex__
,
para melhorar a última versão de Vector2d
, vista no [ex_vector2d_v3_full].
Para deixar o serviço completo, podemos suportar a operação inversa, com um método de classe fromcomplex
, que constrói um Vector2d
a partir de um complex
.
complex
link:code/13-protocol-abc/typing/vector2d_v4.py[role=include]
-
Presume que
datum
tem atributos.real
e.imag
. Veremos uma implementação melhor no Exemplo 17.
Dado o código acima, e o método __abs__
que o Vector2d
já tinha em [ex_vector2d_v3_full], temos o seguinte:
>>> from typing import SupportsComplex, SupportsAbs
>>> from vector2d_v4 import Vector2d
>>> v = Vector2d(3, 4)
>>> isinstance(v, SupportsComplex)
True
>>> isinstance(v, SupportsAbs)
True
>>> complex(v)
(3+4j)
>>> abs(v)
5.0
>>> Vector2d.fromcomplex(3+4j)
Vector2d(3.0, 4.0)
Para verificação de tipo durante a execução, o Exemplo 16 serve bem,
mas para uma cobertura estática e relatório de erros melhores com o Mypy, os métodos
__abs__
, __complex__
, e fromcomplex
deveriam receber dicas de tipo, como mostrado no Exemplo 17.
link:code/13-protocol-abc/typing/vector2d_v5.py[role=include]
-
A anotação de retorno
float
é necessária, senão o Mypy infereAny
, e não verifica o corpo do método. -
Mesmo sem a anotação, o Mypy foi capaz de inferir que isso retorna um
complex
. A anotação evita um aviso, dependendo da sua configuração do Mypy. -
Aqui
SupportsComplex
garante quedatum
é conversível. -
Essa conversão explícita é necessária, pois o tipo
SupportsComplex
não declara os atributos.real
e.img
, usados na linha seguinte. Por exemplo,Vector2d
não tem esses atributos, mas implementa__complex__
.
O tipo de retorno de fromcomplex
pode ser Vector2d
se a linha from future import annotations
aparecer no início do módulo.
Aquela importação faz as dicas de tipo serem armazenadas como strings, sem serem processadas durante a importação, quando as definições de função são tratadas.
Sem o __future__
import of annotations
,
Vector2d
é uma referência inválida neste momento (a classe não está inteiramente definida ainda) e deveria ser escrita como uma string:
'Vector2d'
, como se fosse uma referência adiantada.
Essa importação de __future__
foi introduzida na
PEP 563—Postponed Evaluation of Annotations, implementada no Python 3.7.
Aquele comportamento estava marcado para se tornar default no 3.10, mas a mudança foi adiada para uma versão futura.[17]
Quando isso acontecer, a importação será redundante mas inofensiva.
Agora vamos criar - e depois estender - um novo protocolo estático.
Quando estudamos goose typing, vimos a ABC Tombola
em Definindo e usando uma ABC.
Aqui vamos ver como definir uma interface similar usando um protocolo estático.
A ABC Tombola
especifica dois métodos: pick
e load
.
Poderíamos também definir um protocolo estático com esses dois métodos,
mas aprendi com a comunidade Go que protocolos de apenas um método tornam o duck typing estático mais útil e flexível.
A biblioteca padrão do Go tem inúmeras interfaces,
como Reader
, uma interface para I/O que requer apenas um método read
.
Após algum tempo, se você entender que um protocolo mais complexo é necessário,
você pode combinar dois ou mais protocolos para definir um novo.
Usar um container que escolhe itens aleatoriamente pode ou não exigir o recarregamento do container, mas ele certamente precisa de um método para fazer a efetiva escolha do item, então o método pick
será o escolhido para o protocolo mínimo RandomPicker
.
O código do protocolo está no Exemplo 18, e
seu uso é demonstrado por testes no Exemplo 19.
RandomPicker
link:code/13-protocol-abc/typing/randompick.py[role=include]
Note
|
O método |
RandomPicker
em usolink:code/13-protocol-abc/typing/randompick_test.py[role=include]
-
Não é necessário importar um protocolo estático para definir uma classe que o implementa, Aqui eu importei
RandomPicker
apenas para usá-lo emtest_isinstance
mais tarde. -
SimplePicker
implementaRandomPicker
— mas não é uma subclasse dele. Isso é o duck typing estático em ação. -
Any
é o tipo de retorno default, então essa anotação não é estritamente necessária, mas deixa mais claro que estamos implementando o protocoloRandomPicker
, como definido em Exemplo 18. -
Não esqueça de acrescentar dicas
→ None
aos seus testes, se você quiser que o Mypy olhe para eles. -
Acrescentei uma dica de tipo para a variável
popper
, para mostrar que o Mypy entende que oSimplePicker
é consistente-com. -
Esse teste prova que uma instância de
SimplePicker
também é uma instância deRandomPicker
. Isso funciona por causa do decorador@runtime_checkable
aplicado aRandomPicker
, e porque oSimplePicker
tem um métodopick
, como exigido. -
Esse teste invoca o método
pick
deSimplePicker
, verifica que ele retorna um dos itens dados aSimplePicker
, e então realiza testes estáticos e de execução sobre o item obtido. -
Essa linha gera uma obervação no relatório do Mypy.
Como vimos no [top_protocol_test], reveal_type
é uma função "mágica" reconhecida pelo Mypy. Por isso ela não é importada e nós só conseguimos chamá-la de dentro de blocos if
protegidos por typing.TYPE_CHECKING
, que só é True
para os olhos de um verificador de tipo estático, mas é False
durante a execução.
Os dois testes em Exemplo 19 passam.
O Mypy também não vê nenhum erro naquele código,
e mostra o resultado de reveal_type
sobre o item
retornado por pick
:
$ mypy randompick_test.py
randompick_test.py:24: note: Revealed type is 'Any'
Tendo criado nosso primeiro protocolo, vamos estudar algumas recomendações sobre essa prática.
Após 10 anos de experiência com duck typing estático em Go, está claro que protocolos estreitos são mais úteis - muitas vezes tais protocolos tem um único método, raramente mais que um par de métodos. Martin Fowler descreve uma boa ideia para se ter em mente ao desenvolver protocolos: a Role Interface, (interface papel[18]). A ideia é que um protocolo deve ser definido em termos de um papel que um objeto pode desempenhar, e não em termos de uma classe específica.
Além disso, é comum ver um protocolo definido próximo a uma função que o usa-ou seja, definido em "código do cliente" em vez de ser definido em uma biblioteca separada. Isso torna mais fácil criar novos tipos para chamar aquela função, bom para a extensibilidade e para testes com simulações ou protótipos.
Ambas as práticas, protocolos estreitos e protocolos em código cliente, evitam um acoplamento muito firme, em acordo com o Princípio da Segregação de Interface, que podemos resumir como "Clientes não devem ser forçados a depender de interfaces que não usam."
A página "Contributing to typeshed" (EN) recomenda a seguinte convenção de nomenclatura para protocolos estáticos (os três pontos a seguir foram traduzidos o mais fielmente possível):
-
Use nomes simples para protocolos que representam um conceito claro (e.g.,
Iterator
,Container
). -
Use
SupportsX
para protocolos que oferecem métodos que podem ser chamados (e.g.,SupportsInt
,SupportsRead
,SupportsReadSeek
).[19] -
Use
HasX
para protocolos que tem atributos que podem ser lidos ou escritos, ou métodos getter/setter(e.g.,HasItems
,HasFileno
).
A biblioteca padrão do Go tem uma convenção de nomenclatura que gosto:
para protocolos de método único, se o nome do método é um verbo, acrescente o sufixo adequado (em inglês, "-er" ou "-or", em geral) para torná-lo um substantivo.
Por exemplo, em vez de SupportsRead
, temos Reader
.
Outros exemplos incluem Formatter
, Animator
, e Scanner
.
Para se inspirar, veja
"Go (Golang) Standard Library Interfaces (Selected)" (EN) de Asuka Kenji.
Uma boa razão para se criar protocolos minimalistas é a habilidade de estendê-los posteriormente, se necessário. Veremos a seguir que não é difícil criar um protocolo derivado com um método adicional
Como mencionei na seção anterior, os desenvolvedores Go defendem que, quando em dúvida, melhor escolher o minimalismo ao definir interfaces - o nome usado para protocolos estáticos naquela linguagem. Muitas das interfaces Go mais usadas tem um único método.
Quando a prática revela que um protocolo com mais métodos seria útil, em vezz de adicionar métodos ao protocolo original, é melhor derivar dali um novo protocolo. Estender um protocolo estático em Python tem algumas ressalvas, como mostra o Exemplo 20 shows.
RandomPicker
link:code/13-protocol-abc/typing/randompickload.py[role=include]
-
Se você quer que o protocolo derivado possa ser verificado durante a execução, você precisa aplicar o decorador novamente - seu comportamento não é herdado.[20]
-
Todo protocolo deve nomear explicitamente
typing.Protocol
como uma de suas classes base, além do protocolo que estamos estendendo. Isso é diferente da forma como herança funciona em Python.[21] -
De volta à programação orientada a objetos "normal": só precisamos declarar o método novo no protocolo derivado. A declaração do método
pick
é herdada deRandomPicker
.
Isso conclui o último exemplo sobre definir e usar um protocolo estático neste capítulo.
Para encerrar o capítulo, vamos olhar as ABCs numéricas e sua possível substituição por protocolos numéricos.
Como vimos em [numeric_tower_warning], as ABCs no pacote numbers
da biblioteca padrão funcionam bem para verificação de tipo durante a execução.
Se você precisa verificar um inteiro, pode usar isinstance(x, numbers.Integral)
para aceitar int
, bool
(que é subclasse de int
) ou outros tipos inteiros oferecidos por bibliotecas externas que registram seus tipos como subclasses virtuais das ABCs de numbers
.
Por exemplo, o NumPy tem 21 tipos inteiros — bem como várias variações de tipos de ponto flutuante registrados como numbers.Real
, e números complexos com várias amplitudes de bits, registrados como numbers.Complex
.
Tip
|
De forma algo surpreendente, |
Infelizmente, a torre numérica não foi projetada para checagem de tipo estática.
A ABC raiz - numbers.Number
- não tem métodos,
então se você declarar x: Number
, o Mypy não vai deixar você fazer operações aritméticas ou chamar qualquer método com X
.
Se as ABCs de numbers
não tem suporte, quais as opções?
Um bom lugar para procurar soluções de tipagem é no projeto typeshed.
Como parte da biblioteca padrão do Python, o módulo statistics
tem um arquivo stub correspondente no typeshed com dicas de tipo, o statistics.pyi,
Lá você encontrará as seguintes definições, que são usadas para anotar várias funções:
_Number = Union[float, Decimal, Fraction]
_NumberT = TypeVar('_NumberT', float, Decimal, Fraction)
Essa abordagem está correta, mas é limitada.
Ela não suporta tipos numéricos
fora da biblioteca padrão, que as ABCs de numbers
suportam durante a execução -
quando tipos numéricos são registrados como subclasses virtuais.
A tendência atual é recomendar os protocolos numéricos fornecidos pelo módulo typing
,
que discutimos na Protocolos estáticos checados durante a Execução.
Infelizmente, durante a execução os protocolos numéricos podem deixar você na mão.
Como mencionado em Limitações das verificações de protocolo durante a execução,
o tipo complex
no Python 3.9 implementa __float__
,
mas o método existe apenas para lançar uma TypeError
com uma mensagem explícita:
"can’t convert complex to float." ("não é possível converter complex para float")
Por alguma razão, ele também implementa __int__
.
A presença desses métodos faz isinstance
produzir resultados enganosos no Python 3.9. No Python 3.10, os métodos de complex
que geravam TypeError
incondicionalmente foram removidos.[22]
Por outro lado, os tipos complexos do NumPy implementam métodos __float__
e __int__
que funcionam,
emitindo apenas um aviso quando cada um deles é usado pela primeira vez:
>>> import numpy as np
>>> cd = np.cdouble(3+4j)
>>> cd
(3+4j)
>>> float(cd)
<stdin>:1: ComplexWarning: Casting complex values to real
discards the imaginary part
3.0
O problema oposto também acontece:
Os tipos embutidos complex
, float
, e int
, além numpy.float16
e numpy.uint8
, não
tem um método __complex__
, então isinstance(x, SupportsComplex)
retorna False
para eles.[23]
Os tipo complexos do NumPy, tal como np.complex64
, implementam __complex__
para conversão em um complex
embutido.
Entretanto, na prática, o construtor embutido complex()
trabalha com instâncias de todos esses tipos sem erros ou avisos.
>>> import numpy as np
>>> from typing import SupportsComplex
>>> sample = [1+0j, np.complex64(1+0j), 1.0, np.float16(1.0), 1, np.uint8(1)]
>>> [isinstance(x, SupportsComplex) for x in sample]
[False, True, False, False, False, False]
>>> [complex(x) for x in sample]
[(1+0j), (1+0j), (1+0j), (1+0j), (1+0j), (1+0j)]
Isso mostra que verificações de SupportsComplex
com isinstance
sugerem que todas aquelas conversões para complex
falhariam, mas ela são bem sucedidas.
Na mailing list typing-sig,
Guido van Rossum indicou que o complex
embutido aceita um único argumento,
e essa é a razão daquelas conversões funcionarem.
Por outro lado, o Mypy aceita argumentos de todos esses seis tipos em uma chamada à função to_complex()
, definida assim:
def to_complex(n: SupportsComplex) -> complex:
return complex(n)
No momento em que escrevo isso, o NumPy não tem dicas de tipo, então seus tipos numéricos são todos Any
.[24]
Por outro lado, o Mypy de alguma maneira "sabe" que o int
e o float
embutidos podem ser convertidos para complex
,
apesar de, no typeshed, apenas a classe embutida complex
ter o método __complex__
.[25]
Concluindo, apesar da impressão que a verificação de tipo para tipos numéricos não deveria ser difícil,
a situação atual é a seguinte:
as dicas de tipo da PEP 484
evitam (EN) a torre numérica
e recomendam implicitamente que os verificadores de tipo codifiquem explicitamente as relações de tipo entre os complex
, float
, e int
embutidos.
O Mypy faz isso, e também, pragmaticamente, aceita que int
e float
são consistente-com SupportsComplex
, apesar deles não implementarem __complex__
.
Tip
|
Eu só encontrei resultados inesperados usando verificações com |
As principais lições dessa seção são:
-
As ABCs de
numbers
são boas para verificação de tipo durante a execução, mas inadequadas para tipagem estática. -
Os protocolos numéricos estáticos
SupportsComplex
,SupportsFloat
, etc. funcionam bem para tipagem estática, mas são pouco confiáveis para verificação de tipo durante a execução se números complexos estiverem envolvidos.
Estamos agora prontos para uma rápida revisão do que vimos nesse capítulo.
O Mapa de Tipagem (Figura 1) é a chave para entender esse capítulo. Após uma breve introdução às quatro abordagens da tipagem, comparamos protocolos dinâmicos e estáticos, os quais suportam duck typing e duck typing estático, respectivamente. Os dois tipos de protocolo compartilham uma característica essencial, nunca é exigido de uma classe que ela declare explicitamente o suporte a qualquer protocolo específico. Uma classe suporta um protocolo simplesmente implementando os métodos necessários.
A próxima grande seção foi a Programando patos,
onde exploramos os esforços que interpretador Python faz para que os protocolos dinâmicos de sequência e iterável funcionem, incluindo a implementação parcial de ambos.
Então vimos como fazer uma classe implementar um protocolo durante a execução,
através da adição de métodos extra via monkey patching.
A seção sobre duck typing terminou com sugestões de programação defensiva,
incluindo a detecção de tipos estruturais sem verificações explícitas com isinstance
ou hasattr
, usando try/except
e falhando rápido.
Após Alex Martelli introduzir o goose typing em Pássaros aquáticos e as ABCs,
vimos como criar subclasses de ABCs existentes,
examinamos algumas ABCs importantes da biblioteca padrão,
e criamos uma ABC do zero,
que nós então implementamos da forma tradicional, criando subclasses, e por registro.
Finalizamos aquela seção vendo como o método especial __subclasshook__
permite às ABCs suportarem a tipagem estrutural, pelo reconhecimento de classes não-relacionadas, mas que fornecem os métodos que preenchem os requisitos da interface definida na ABC.
A última grande seção foi a Protocolos estáticos,
onde retomamos o estudo do duck typing estático, que havia começado no [type_hints_in_def_ch], em [protocols_in_fn].
Vimos como o decorador @runtime_checkable
também aproveita o __subclasshook__
para suportar tipagem estrutural durante a execução - mesmo que o melhor uso dos protocolos estáticos seja com verificadores de tipo estáticos,
que podem levar em consideração as dicas de tipo, tornando a tipagem estrutural mais confiável.
Então falamos sobre o projeto e a codificação de um protocolo estático e como estendê-lo.
O capítulo terminou com As ABCs em numbers e os novod protocolos numéricos,
que conta a triste história do abandono da torre numérica e das limitações da alternativa proposta:
os protocolos numéricos estáticos tal como SupportsFloat
e outros,
adicionados ao módulo typing
no Python 3.8.
A mensagem principal desse capítulo é que temos quatro maneiras complementares de programar com interfaces no Python moderno, cada uma com diferentes vantagens e deficiências. Você possivelmente encontrará casos de uso adequados para cada esquema de tipagem em qualquer base de código de Python moderno de tamanho significativo. Rejeitar qualquer dessas abordagens tornará seu trabalho como programador Python mais difícil que o necessário.
Dito isso, o Python ganhou sua enorme popularidade enquanto suportava apenas duck typing. Outras linguagens populares, como Javascript, PHP e Ruby, bem como Lisp, Smalltalk, Erlang e Clojure - essas últimas não muito populares mas extremamente influentes - são todas linguagens que tinham e ainda tem um impacto tremendo aproveitando o poder e a simplicidade do duck typing.
Para uma rápida revisão do prós e contras da tipagem, bem como da importância de typing.Protocol
para a saúde de bases de código verificadas estaticamente, eu recomendo fortemente o post de Glyph Lefkowitz
"I Want A New Duck: typing.Protocol
and the future of duck typing" (EN).("Eu Quero Um Novo Pato: typing.Protocol
e o futuro do duck typing`").
Eu também aprendi bastante em seu post
"Interfaces and Protocols" (EN) ("Interfaces e Protocolos"),
comparando typing.Protocol
com zope.interface
— um mecanismo mais antigo para definir interfaces em sistemas plug-in fracamente acoplados, usado no
Plone CMS,
na Pyramid web framework, e no framework de programação assíncrona
Twisted,
um projeto fundado por Glyph.[26]
Ótimos livros sobre Python tem - quase que por definição - uma ótima cobertura de duck typing. Dois de meus livros favoritos de Python tiveram atualizações lançadas após a primeira edição de Python Fluente: The Quick Python Book, 3rd ed., (Manning), de Naomi Ceder; e Python in a Nutshell, 3rd ed., de Alex Martelli, Anna Ravenscroft, e Steve Holden (O’Reilly).
Para uma discussão sobre os prós e contras da tipagem dinâmica, veja a entrevista de Guido van Rossum com Bill Venners em "Contracts in Python: A Conversation with Guido van Rossum, Part IV" (EN) ("Contratos em Python: Uma Conversa com Guido van Rossum, Parte IV"). O post "Dynamic Typing" (EN) ("Tipagem Dinâmica"), de Martin Fowler, traz uma avaliação perspicaz e equilibrada deste debate. Ele também escreveu "Role Interface" (EN) ("Interface Papel"), que mencionei na Melhores práticas no desenvolvimento de protocolos. Apesar de não ser sobre duck typing, aquele post é altamente relevante para o projeto de protocolos em Python, pois ele contrasta as estreitas interfaces papel com as interfaces públicas bem mais abrangentes de classes em geral.
A documentação do Mypy é, muitas vezes, a melhor fonte de informação sobre qualquer coisa relacionada a tipagem estática em Python, incluindo duck typing estático, tratado no capítulo "Protocols and structural subtyping" (EN) ("Protocolos e subtipagem estrutural").
As referências restantes são todas sobre goose typing.
Beazley and Jones’s Python Cookbook, 3rd ed. (O’Reilly)
tem uma seção sobre como definir uma ABC (Recipe 8.12).
O livro foi escrito antes do Python 3.4,
então eles não usam a atual sintaxe preferida para declarar ABCs, criar uma subclasse de abc.ABC
(em vez disso, eles usam a palavra-chave metaclass
, da qual nós só vamos precisar mesmo em[class_metaprog]).
Tirando esse pequeno detalhe, a receita cobre os principais recursos das ABCs muito bem.
The Python Standard Library by Example by Doug Hellmann (Addison-Wesley),
tem um capítulo sobre o módulo abc
.
Ele também esta disponível na web, no excelente site do Doug PyMOTW—Python Module of the Week (EN).
Hellmann também usa a declaração de ABC no estilo antigo:`PluginBase(metaclass=abc.ABCMeta)` em vez do mais simples PluginBase(abc.ABC)
, disponível desde o Python 3.4.
Quando usamos ABCs, herança múltipla não é apenas comum mas praticamente inevitável,
porque cada uma das ABCs fundamentais de coleções — Sequence
, Mapping
, e Set
— estendem Collection
, que por sua vez estende múltiplas ABCs
(veja Figura 4). Assim, [herança] é um importante tópico complementar a esse.
A PEP 3119—Introducing Abstract Base Classes (EN)
apresenta a justificativa para as ABCs. A PEP 3141—A Type Hierarchy for Numbers (EN)
apresenta as ABCs do módulo numbers
,
mas a discussão no Mypy issue #3186 "int is not a Number?"
inclui alguns argumentos sobre a razão da torre numérica ser inadequada para verificação estática de tipo.
Alex Waygood escreveu uma
resposta abrangente no StackOverflow, discutindo formas de anotar tipos numéricos.
Vou continuar monitorando o Mypy issue #3186 para os próximos capítulos dessa saga, na esperança de um final feliz que torne a tipagem estática e o goose typing compatíveis, como eles deveriam ser.
A Jornada PMV da tipagem estática em Python
Eu trabalho para a Thoughtworks, uma líder global em desenvolvimento de software ágil. Na Thoughtworks, muitas vezes recomendamos a nossos clientes que procurem criar e implanta PMVs: produtos mínimos viáveis, "uma versão simples de um produto, que é disponibilizada para os usuários com o objetivo de validar hipóteses centrais do negócio," como definido or meu colega Paulo Caroli in "Lean Inception", um post no Martin Fowler’s collective blog.
Guido van Rossum e os outros core developers que projetaram e implementaram a tipagem estática tem seguido a estratégia do PMV desde 2006. Primeiro, a PEP 3107—Function Annotations foi implementada no Python 3.0 com uma semântica bastante limitada: apenas sintaxe para anexar anotações a parâmetros e retornos de funções. Isso foi feito para explicitamente permitir experimentação e receber feedback - os principais benefícios de um PMV.
Oito anos depois, a PEP 484—Type Hints foi proposta e aprovada.
Sua implementação, no Python 3.5, não exigiu mudanças na linguagem ou na biblioteca padrão - exceto a adição do módulo typing
, do qual nenhuma outra parte da biblioteca padrão dependia.
A PEP 484 suportava apenas tipos nominais com genéricos - similar ao Java - mas com a verificação estática efetiva sendo executada por ferramentas externas.
Recursos importantes não existiam, como anotações de variáveis, tipos embutidos genéricos, e protocolos. Apesar dessas limitações, esse PMV de tipagem foi bem sucedida o suficiente para atrair investimento e adoção por parte de empresas com enormes bases de código em Python, como a Dropbox, o Google e o Facebook, bem como apoio de IDEs profissionais como o PyCharm,
o Wing, e o VS Code.
A PEP 526—Syntax for Variable Annotations foi o primeiro passo evolutivo que exigiu mudanças no interpretador, no Python 3.6. Mais mudanças no interpretador do Python 3.7 foram feitas para suportar a PEP 563—Postponed Evaluation of Annotations e a PEP 560—Core support for typing module and generic types, que permitiram que coleções embutidas e da biblioteca padrão aceitem dicas de tipo genéricas "de fábrica" no Python 3.9, graças à PEP 585—Type Hinting Generics In Standard Collections.
Durante todos esses anos, alguns usuários de Python - incluindo este autor - ficaram desapontados com o suporte à tipagem. Após aprender Go, a ausência de duck typing estático em Python era incompreensível, em uma linguagem onde o duck typing havia sempre sido uma força central.
Mas essa é a natureza dos PMVs: eles podem não satisfazer todos os usuários em potencial, mas exigem menos esforço de implementação, e guiam o desenvolvimento posterior com o feedback do uso em situações reais.
Se há uma coisa que todos aprendemos com o Python 3, é que progresso incremental é mais seguro que lançamentos estrondosos. Estou contente que não tivemos que esperar pelo Python 4 - se é que existirá - para tornar o Python mais atrativo par grandes empresas, onde os benefícios da tipagem estática superam a complexidade adicional.
Abordagens à tipagem em linguagens populares
A Figura 8 é uma variação do Mapa de Tipagem(Figura 1) com os nomes de algumas linguagem populares que suportam cada um dos modos de tipagem.
TypeScript e o Python ≥ 3.8 são as únicas linguagem em minha pequena e arbitrária amostra que suportam todas as quatro abordagens.
Go é claramente uma linguagem de tipo estáticos na tradição do Pascal, mas ela foi a pioneira do duck typing estático - pelo menos entre as linguagens mais usadas hoje. Eu também coloquei Go no quadrante do goose typing por causa de suas declarações (assertions) de tipo, que permitem a verificação e adaptação a diferentes tipos durante a execução.
Se eu tivesse que desenhar um diagrama similar no ano 2000, apenas os quadrantes do duck typing e da tipagem estática teriam linguagens. Não conheço nenhuma linguagem que suportava duck typing estático ou goose typing 20 anos atrás. O fato de cada um dos quatro quadrantes ter pelo menos três linguagens populares sugere que muita gente vê benefícios em cada uma das quatro abordagens à tipagem.
Monkey patching
Monkey patching tem uma reputação ruim. Se usado com exagero, pode gerar sistemas difíceis de entender e manter. A correção está normalmente intimamente ligada a seu alvo, tornando-se frágil. Outro problema é que duas bibliotecas que aplicam correções deste tipo durante a execução podem pisar nos pés uma da outra, com a segunda biblioteca a rodar destruindo as correções da primeira.
Mas o monkey patching pode também ser útil, por exemplo, para fazer uma classe implementar um protocolo durante a execução. O design pattern Adaptador resolve o mesmo problema através da implementação de uma nova classe inteira.
É fácil usar monkey patching em código Python, mas há limitações.
Ao contrário de Ruby e Javascript, o Python não permite modificações de tipos embutidos durante a execução. Eu na verdade considero isso uma vantagem, pois dá a certeza que um objeto str
vai sempre ter os mesmos métodos.
Essa limitação reduz a chance de bibliotecas externas aplicarem correções conflitantes.
Metáforas e idiomas em interfaces
Uma metáfora promove o entendimento tornando restrições e acessos visíveis. Esse é o valor das palavras "stack" (pilha) e "queue" (fila) para descrever estruturas de dados fundamentais: elas tornam claras aa operações permitidas, isto é, como os itens podem ser adicionados ou removidos. Por outro lado, Alan Cooper et al. escrevem em About Face, the Essentials of Interaction Design, 4th ed. (Wiley):
Fidelidade estrita a metáforas liga interfaces de forma desnecessariamente firme aos mecanismos do mundo físico.
Eles está falando de interface de usuário, mas a advertência se aplica também a APIs. Mas Cooper admite que quando uma metáfora "verdadeiramente apropriada" "cai no nosso colo," podemos usá-la (ele escreve "cai no nosso colo" porque é tão difícil encontrar metáforas adequadas que ninguém deveria perder tempo tentando encontrá-las ativamente). Acredito que a imagem da máquina de bingo que usei nesse capítulo é apropriada e eu a defenderei.
About Face é, de longe, o melhor livro sobre design de UI que eu já li - e eu li uns tantos. Abandonar as metáforas como paradigmas de design, as substituindo por "interfaces idiomáticas", foi a lição mais valiosa que aprendi com o trabalho de Cooper.
Em About Face, Cooper não lida com APIs, mas quanto mais penso em suas ideias, mais vejo como se aplicam ao Python. Os protocolos fundamentais da linguagem são o que Cooper chama de "idiomas." Uma vez que aprendemos o que é uma "sequência", podemos aplicar esse conhecimento em diferentes contextos. Esse é o tema principal de Python Fluente: ressaltar os idiomas fundamentais da linguagem, para que o seu código seja conciso, efetivo e legível - para um Pythonista fluente.
NotImplementedError
para sinalizar que as subclasses eram responsáveis por suas implementações. No Smalltalk-80, o corpo dos métodos abstratos invocaria subclassResponsibility
, um método herdado de object
que gerava um erro com a mensagem "Minha subclasse deveria ter sobreposto uma de minhas mensagens."
load()
não funciona com loaded()
, pois o tipo list
não implementa __bool__
, o método que eu teria de vincular a loaded
. O bool()
nativo não precisa de __bool__
para funcionar, porque pode também usar __len__
. Veja "4.1. Teste do Valor Verdade" no capítulo "Tipos Embutidos" da documentação do Python.
__mro__
na [mro_section]. Por agora, essas informações básicas são o suficiente.
max
? (Deveria o Mypy avisar sobre argumentos potencialmente inválidos passados a max
?)" aproveitava um protocolo _SupportsLessThan
, que usei para melhorar as anotações de max
, min
, sorted
, e list.sort
.
__slots__
é irrelevante para nossa discussão aqui - é uma otimização sobre a qual falamos na [slots_section].
x
é T
: é sobre determinar que o tipo de x
é consistente-com T
, o que pode ser caro. Não é de se espantar que o Mypy leve alguns segundos para fazer uma verificação de tipo, mesmo em scripts Python curtos.
@runtime_checkable
(EN) na PEP 544—Protocols: Structural subtyping (static duck typing).
numbers
, que o Mypy ignora.
complex
na verdade não tem mais um método __complex__
.