Skip to content

Latest commit

 

History

History
1991 lines (1572 loc) · 127 KB

cap13.adoc

File metadata and controls

1991 lines (1572 loc) · 127 KB

Interfaces, protocolos, e ABCs

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.

O mapa de tipagem

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.

Quatro abordagens para verificação de tipo
Figura 1. A metade superior descreve abordagens de checagem de tipo durante a execução usando apenas o interpretador Python; a metade inferior requer um verificador de tipo estático externo, como o Mypy ou um IDE como o PyCharm. Os quadrantes da esquerda se referem a tipagem baseada na estrutura do objeto - isto é, dos métodos oferecidos pelo objeto, independente do nome de sua classe ou superclasses; os quadrantes da direita dependem dos objetos terem tipos explicitamente nomeados: o nome da classe do objeto, ou o nome de suas superclasses.

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.

Novidades nesse capítulo

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 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.

Dois tipos de protocolos

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.

Exemplo 1. Implementação parcial do protocolo de sequência usando __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.

int PySequence_Check(PyObject *o)

Retorna 1 se o objeto oferecer o protocolo de sequência, caso contrário retorna 0. Observe que ela retorna 1 para classes Python com um método __getitem__, a menos que sejam subclasses de dict […​]

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.

Programando patos

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.

O Python curte sequências

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.

UML class diagram for `Sequence`
Figura 2. Diagrama de classe UML para a ABC 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 collections.abc existem para formalizar interfaces que são implementadas por objetos nativos e são implicitamente suportadas pelo interpretador - objetos e suporte que existem desde antes das próprias ABCs. As ABCs são úteis como pontos de partida para novas classes, e para permitir checagem de tipo explícita durante a execução (também conhecida como goose typing), bem como para servirem de dicas de tipo para verificadores de tipo estáticos.

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.

Exemplo 2. Um deque como uma sequência de cartas (igual ao [ex_pythonic_deck])
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: Implementando um Protocolo durante a Execução

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.

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.

Exemplo 4. "Monkey patching" o 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')]
  1. Cria uma função que recebe deck, position, e card como argumentos.

  2. Atribui aquela função a um atributo chamado __setitem__ na classe FrenchDeck.

  3. 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 e "falhe rápido"

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.

Exemplo 5. Duck typing para lidar com uma string ou um iterável de strings
    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')
  1. Supõe que é uma string (MFPP - mais fácil pedir perdão que permissão).

  2. Converte vírgulas em espaços e divide o resultado em uma lista de nomes.

  3. Desculpe, field_names não grasna como uma str: não tem .replace, ou retorna algo que não conseguimos passar para .split

  4. Se um AttributeError aconteceu, então field_names não é uma str. Supomos que já é um iterável de nomes.

  5. 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 uma list, e também impede que meu código troque os nomes por engano.

  6. 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.

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 por isinstance() e issubclass(); veja a documentação do módulo abc.[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!

Pássaros aquáticos e as ABCs

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 e issubclass.

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 register são tratados em Uma subclasse virtual de uma ABC, mais adiante nesse mesmo capítulo. Por hora, aqui está um pequeno exemplo: dada a classe FrenchDeck, se eu quiser que ela passe em uma verificação como (FrenchDeck, Sequence), posso torná-la uma subclasse virtual da ABC Sequence com as seguintes linhas:

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.

Criando uma Subclasse de uma ABC

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.

Exemplo 6. frenchdeck2.py: FrenchDeck2, uma subclasse de collections.MutableSequence
link:code/13-protocol-abc/frenchdeck2.py[role=include]
  1. __setitem__ é tudo que precisamos para possibilitar o embaralhamento…​

  2. …​mas uma subclasse de MutableSequence é forçada a implementar __delitem__, um método abstrato daquela ABC.

  3. Também precisamos implementar insert, o terceiro método abstrato de MutableSequence.

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.

Diagrama de classe UML para `Sequence` e `MutableSequence`
Figura 3. Diagrama de classe UML para a ABC 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, __contains__ funciona executando uma busca sequencial, mas se a sua classe de sequência mantém os itens ordenados, você pode escrever um __contains__ que executa uma busca binária usando a função bisect da biblioteca padrão.

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.

ABCs na Biblioteca Padrão

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 abc na biblioteca padrão. Aqui nós estamos falando sobre o collections.abc. Para reduzir o tempo de carregamento, desde o Python 3.4 aquele módulo é implementado fora do pacote collections — em Lib/_collections_abc.py — então é importado separado de collections. O outro módulo abc é apenas abc (i.e., Lib/abc.py), onde a classe abc.ABC é definida. Toda ABC depende do módulo abc, mas não precisamos importá-lo nós mesmos, exceto para criar um nova ABC.

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]

UML for collections.abc
Figura 4. Diagrama de classes UML para as ABCs em collections.abc.

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 operador in com __contains__, e Sized oferece len() 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, e Sized.

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; para MutableMapping e MutableSet, 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 em ItemsView, KeysView, e ValuesView, respectivamente. Os dois primeiros também implementam a rica interface de Set, 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.

isinstance com Hashable e Iterable pode enganar você

É 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.

Definindo e usando uma ABC

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.

UML for Tombola
Figura 5. Diagrama UML para uma ABC e três subclasses. O nome da ABC 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.

Exemplo 7. tombola.py: Tombola é uma ABC com dois métodos abstratos e dois métodos concretos.
link:code/13-protocol-abc/tombola.py[role=include]
  1. Para definir uma ABC, crie uma subclasse de abc.ABC.

  2. Um método abstrato é marcado com o decorador @abstractmethod, e muitas vezes seu corpo é vazio, exceto por uma docstring.[7]

  3. A docstring instrui os implementadores a levantarem LookupError se não existirem itens para escolher.

  4. Uma ABC pode incluir métodos concretos.

  5. 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).

  6. Não sabemos como as subclasses concretas vão armazenar os itens, mas podemos escrever o resultado de inspect esvaziando a Tombola com chamadas sucessivas a .pick()…​

  7. …​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 super(), acrescentando funcionalidade em vez de implementar do zero. Veja a documentação do módulo abc para os detalhes do uso de @abstractmethod.

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.

Árvore de ponta cabeça, com BaseException no topo, e quatro ramos principais, incluindo Exception.
Figura 6. Parte da hierarquia da classe 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.

Exemplo 8. Uma 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
  1. Declara Fake como subclasse de Tombola.

  2. A classe é criada, nenhum erro até agora.

  3. Um TypeError é sinalizado quando tentamos instanciar Fake. A mensagem é bastante clara: Fake é considerada abstrata porque deixou de implementar load, um dos métodos abstratos declarados na ABC Tombola.

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.

Detalhes da Sintaxe das 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 @abstractmethod, a documentação é explícita:

Quando @abstractmethod é aplicado em combinação com outros descritores de método, ele deve ser aplicado como o decorador mais interno…​[9]

Em outras palavras, nenhum outro decorador pode aparecer entre @abstractmethod e o comando def.

Agora que abordamos essas questões de sintaxe das ABCs, vamos colocar Tombola em uso, implementando dois descendentes concretos dessa classe.

Criando uma subclasse de ABC

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.

Exemplo 9. bingo.py: BingoCage é uma subclasse concreta de Tombola
link:code/13-protocol-abc/bingo.py[role=include]
  1. Essa classe BingoCage estende Tombola explicitamente.

  2. Finja que vamos usar isso para um jogo online. random.SystemRandom implementa a API random sobre a função os.urandom(…), que fornece bytes aleatórios "adequados para uso em criptografia", segundo a documentação do módulo os.

  3. Delega o carregamento inicial para o método .load()

  4. Em vez da função random.shuffle() normal, usamos o método .shuffle() de nossa instância de SystemRandom.

  5. pick é implementado como em [ex_bingo_callable].

  6. __call__ também é de [ex_bingo_callable]. Ele não é necessário para satisfazer a interface de Tombola, 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..

Exemplo 10. lotto.py: LottoBlower é uma subclasse concreta que sobrecarrega os métodos inspect e loaded de Tombola
link:code/13-protocol-abc/lotto.py[role=include]
  1. O construtor aceita qualquer iterável: o argumento é usado para construir uma lista.

  2. a função random.randrange(…) levanta um ValueError se a faixa de valores estiver vazia, então capturamos esse erro e trocamos por LookupError, para ser compatível com Tombola.

  3. Caso contrário, o item selecionado aleatoriamente é retirado de self._balls.

  4. Sobrepõe loaded para evitar a chamada a inspect (como Tombola.loaded faz no Exemplo 7). Podemos fazer isso mais rápido rápida trabalhando diretamente com self._balls — não há necessidade de criar toda uma nova tuple.

  5. 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 subclasse virtual de uma ABC

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.

UML for TomboList
Figura 7. Diagrama de classe UML para TomboList, subclasse real de list e subclassse virtual de Tombola.
Exemplo 11. tombolist.py: a classe TomboList é uma subclasse virtual de Tombola
link:code/13-protocol-abc/tombolist.py[role=include]
  1. TomboList é registrada como subclasse virtual de Tombola.

  2. TomboList estende list.

  3. TomboList herda seu comportamento booleano de list, e isso retorna True se a lista não estiver vazia.

  4. Nosso pick chama self.pop, herdado de list, passando um índice aleatório para um item.

  5. TomboList.load é o mesmo que list.extend.

  6. loaded delega para bool.[11]

  7. É 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.

O Uso de register na Prática

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.

Tipagem estrutural com ABCs

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.

Exemplo 12. Definição de Sized no código-fonte de Lib/_collections_abc.py
class 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)
  1. Se há um atributo chamado __len__ no __dict__ de qualquer classe listada em C.__mro__ (isto é, C e suas superclasses)…​

  2. …​retorna True, sinalizando que C é uma subclasse virtual de Sized.

  3. 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 ABCMeta.__subclasscheck__ no Python 3.6: Lib/abc.py. Cuidado: ele tem muitos ifs e duas chamadas recursivas. No Python 3.7, Ivan Levkivskyi and Inada Naoki reescreveram em C a maior parte da lógica do módulo abc, para melhorar o desempenho. Veja Python issue #31333. A implementação atual de ABCMeta.__subclasscheck__ simplesmente chama abc_subclasscheck. O código-fonte em C relevante está em _cpython/Modules/_abc.c#L605.

É 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.

Protocolos estáticos

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].

A função double tipada

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.

Exemplo 13. double_protocol.py: a definição de double usando um Protocol.
link:code/13-protocol-abc/double/double_protocol.py[role=include]
  1. Vamos usar esse T na assinatura de __mul__.

  2. __mul__ é a essência do protocolo Repeatable. O parâmetro self normalmente não é anotado - presume-se que seu tipo seja a classe. Aqui usamos T para assegurar que o tipo do resultado é o mesmo tipo de self. Além disso observe que repeat_count está limitado nesse protocolo a int.

  3. A variável de tipo RT é vinculada pelo protocolo Repeatable: o verificador de tipo vai exigir que o tipo efetivo implemente Repeatable.

  4. 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 que x.

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__.

Protocolos estáticos checados durante a Execução

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.

Exemplo 14. código-fonte do protocolo 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.

Exemplo 15. Usando 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)
  1. complex64 é um dos cinco tipos de números complexos fornecidos pelo NumPy.

  2. Nenhum dos tipos complexos do NumPy é subclasse do complex embutido.

  3. Mas os tipos complexos de NumPy implementam __complex__, então cumprem o protocolo SupportsComplex.

  4. Portanto, você pode criar objetos complex a partir deles.

  5. Infelizmente, o tipo complex embutido não implementa __complex__, apesar de complex(c) funcionar sem problemas se c for um complex.

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 isinstance: quando você escreve um comando if onde a condição é isinstance(o, MyType), então o Mypy pode inferir que dentro do bloco if, o tipo do objeto o é consistente-com MyType.

O Duck Typing É Seu Amigo

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.

Limitações das verificações de protocolo 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 complex foi resolvido no Python 3.10.0b4, com a remoção do método complex.__float__.

Mas o problema geral persiste: Verificações com isinstance/issubclass só olham para a presença ou ausência de métodos, sem checar sequer suas assinaturas, muito menos suas anotações de tipo. E isso não vai mudar tão cedo, porque este tipo de verificação de tipo durante a execução traria um custo de processamento inaceitável.[16]

Agora veremos como implementar um protocolo estático em uma classe definida pelo usuário.

Suportando um protocolo estático

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.

Exemplo 16. vector2d_v4.py: métodos para conversão de e para complex
link:code/13-protocol-abc/typing/vector2d_v4.py[role=include]
  1. 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.

Exemplo 17. vector2d_v5.py: acrescentando anotações aos métodos mencionados
link:code/13-protocol-abc/typing/vector2d_v5.py[role=include]
  1. A anotação de retorno float é necessária, senão o Mypy infere Any, e não verifica o corpo do método.

  2. 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.

  3. Aqui SupportsComplex garante que datum é conversível.

  4. 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.

Projetando um 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.

Exemplo 18. randompick.py: definition of RandomPicker
link:code/13-protocol-abc/typing/randompick.py[role=include]
Note

O método pick retorna Any. Em [implementing_generic_static_proto_sec] veremos como tornar RandomPicker um tipo genérico, com um parâmetro que permite aos usuários do protocolo especificarem o tipo de retorno do método pick.

Exemplo 19. randompick_test.py: RandomPicker em uso
link:code/13-protocol-abc/typing/randompick_test.py[role=include]
  1. Não é necessário importar um protocolo estático para definir uma classe que o implementa, Aqui eu importei RandomPicker apenas para usá-lo em test_isinstance mais tarde.

  2. SimplePicker implementa RandomPicker — mas não é uma subclasse dele. Isso é o duck typing estático em ação.

  3. Any é o tipo de retorno default, então essa anotação não é estritamente necessária, mas deixa mais claro que estamos implementando o protocolo RandomPicker, como definido em Exemplo 18.

  4. Não esqueça de acrescentar dicas → None aos seus testes, se você quiser que o Mypy olhe para eles.

  5. Acrescentei uma dica de tipo para a variável popper, para mostrar que o Mypy entende que o SimplePicker é consistente-com.

  6. Esse teste prova que uma instância de SimplePicker também é uma instância de RandomPicker. Isso funciona por causa do decorador @runtime_checkable aplicado a RandomPicker, e porque o SimplePicker tem um método pick, como exigido.

  7. Esse teste invoca o método pick de SimplePicker, verifica que ele retorna um dos itens dados a SimplePicker, e então realiza testes estáticos e de execução sobre o item obtido.

  8. 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.

Melhores práticas no desenvolvimento de protocolos

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

Estendendo um Protocolo

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.

Exemplo 20. randompickload.py: estendendo RandomPicker
link:code/13-protocol-abc/typing/randompickload.py[role=include]
  1. 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]

  2. 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]

  3. 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 de RandomPicker.

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.

As ABCs em numbers e os novod 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, decimal.Decimal não é registrado como uma subclasse virtual de numbers.Real. A razão para isso é que, se você precisa da precisão de Decimal no seu programa, então você quer estar protegido da mistura acidental de números decimais e de números de ponto flutuante (que são menos precisos).

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 isinstance em conjunto com os protocolos numéricos Supports* quando fiz experiências de conversão de ou para complex. Se você não usa números complexos, pode confiar naqueles protocolos em vez das ABCs de numbers.

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.

Resumo do 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 saber mais

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.

Ponto de vista

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.

Quatro abordagens para verificação de tipo e algumas linguagens que as usam.
Figura 8. Quatro abordagens para verificação de tipo e algumas linguagens que as usam.

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.


1. O artigo "Monkey patch" (EN) na Wikipedia tem um exemplo engraçado em Python.
2. Por isso a necessidade de testes automatizados.
3. Consultada em 3 de março de 2023.
4. Você também pode, claro, definir suas próprias ABCs - mas eu não recomendaria esse caminho a ninguém, exceto aos mais avançados pythonistas, da mesma forma que eu os desencorajaria de definir suas próprias metaclasses personalizadas…​ e mesmo para os ditos "mais avançados pythonistas", aqueles de nós que exibem o domínio de todos os recantos por mais obscuros da linguagem, essas não são ferramentas de uso frequente. Este tipo de "metaprogramação profunda", se alguma vez for apropriada, o será no contexto dos autores de frameworks abrangentes, projetadas para serem estendidas de forma independente por inúmeras equipes de desenvolvimento diferentes…​ menos que 1% dos "mais avançados pythonistas" precisará disso alguma vez na vida! - A.M
5. Herança múltipla foi considerada nociva e excluída do Java, exceto para interfaces: Interfaces Java podem estender múltiplas interfaces, e classes Java podem implementar múltiplas interfaces.
6. Talvez o cliente precise auditar o randomizador ou a agência queira fornecer um randomizador "viciado". Nunca se sabe…​
7. Antes das ABCs existirem, métodos abstratos levantariam um 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."
8. A árvore completa está na seção "5.4. Exception hierarchy" da documentação da _Biblioteca Padrão do Python.
10. [defensive_argument] em [mutability_and_references] foi dedicado à questão de apelidamento que acabamos de evitar aqui.
11. O truque usado com 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.
12. Há toda uma explicação sobre o atributo de classe __mro__ na [mro_section]. Por agora, essas informações básicas são o suficiente.
13. O conceito de consistência de tipo é explicado na [consistent_with_sec].
14. Certo, double()` não é muito útil, exceto como um exemplo. Mas a biblioteca padrão do Python tem muitas funções que não poderiam ser anotadas de modo apropriado antes dos protocolos estáticos serem adicionados, no Python 3.8. Eu ajudei a corrigir alguns bugs no typeshed acrescentando dicas de tipo com o uso de protocolos. Por exemplo, o pull request (nome do processo de pedido de envio de modificações a um repositório de código) que consertou "Should Mypy warn about potential invalid arguments to 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.
15. O atributo __slots__ é irrelevante para nossa discussão aqui - é uma otimização sobre a qual falamos na [slots_section].
16. Agradeço a Ivan Levkivskyi, co-autor da PEP 544 (sobre Protocolos), por apontar que checagem de tipo não é apenas uma questão de verificar se o tipo de 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.
17. Leia a decisão (EN) do Python Steering Council no python-dev.
18. NT: "papel" aqui é usado no sentido de incorporação de um personagem
19. Qualquer método pode ser chamado, então essa recomendação não diz muito. Talvez "forneça um ou dois métodos"? De qualquer forma, é uma recomendação, não uma regra absoluta.
20. Para detalhes e justificativa, veja por favor a seção sobre @runtime_checkable (EN) na PEP 544—Protocols: Structural subtyping (static duck typing).
21. Novamente, leia por favor "Merging and extending protocols" (EN) na PEP 544 para os detalhes e justificativas.
23. Eu não testei todas as outras variantes de float e integer que o NumPy oferece.
24. Os tipos numéricos do NumPy são todos registrados com as ABCs apropriadas de numbers, que o Mypy ignora.
25. Isso é uma mentira bem intencionada da parte do typeshed: a partir do Python 3.9, o tipo embutido complex na verdade não tem mais um método __complex__.
26. Agradeço ao revisor técnico Jürgen Gmach por ter recomentado o post "Interfaces and Protocols".