Você está na página 1de 724

Python Fluente, Segunda Edição (2023)

Luciano Ramalho
Sumário
Sobre esta tradução
Histórico das traduções
Parte I: Estruturas de dados
1. O modelo de dados do Python
1.1. Novidades nesse capítulo
1.2. Um baralho pythônico
1.3. Como os métodos especiais são utilizados
1.4. Visão geral dos, métodos especiais
1.5. Porque len não é um método?
1.6. Resumo do capítulo
1.7. Para saber mais
2. Uma coleção de sequências
2.1. Novidades neste capítulo
2.2. Uma visão geral das sequências embutidas
2.3. Compreensões de listas e expressões geradoras
2.4. Tuplas não são apenas listas imutáveis
2.5. Desempacotando sequências e iteráveis
2.6. Pattern matching com sequências
2.7. Fatiamento
2.8. Usando + e * com sequências
2.9. list.sort versus a função embutida sorted
2.10. Quando uma lista não é a resposta
2.11. Resumo do capítulo
2.12. Leitura complementar
3. Dicionários e conjuntos
3.1. Novidades nesse capítulo
3.2. A sintaxe moderna dos dicts
3.3. Pattern matching com mapeamentos
3.4. A API padrão dos tipos de mapeamentos
3.5. Tratamento automático de chaves ausentes
3.6. Variações de dict
3.7. Mapeamentos imutáveis
3.8. Views de dicionários
3.9. Consequências práticas da forma como dict funciona
3.10. Teoria dos conjuntos
3.11. Consequências práticas da forma de funcionamento dos conjuntos
3.12. Operações de conjuntos em views de dict
3.13. Resumo do capítulo
3.14. Leitura complementar
4. Texto em Unicode versus Bytes
4.1. Novidades nesse capítulo
4.2. Questões de caracteres
4.3. Os fundamentos do byte
4.4. Codificadores/Decodificadores básicos
4.5. Entendendo os problemas de codificação/decodificação
4.6. Processando arquivos de texto
4.7. Normalizando o Unicode para comparações confiáveis
4.8. Ordenando texto Unicode
4.9. O banco de dados do Unicode
4.10. APIs de modo dual para str e bytes
4.11. Resumo do capítulo
4.12. Leitura complementar
5. Fábricas de classes de dados
5.1. Novidades nesse capítulo
5.2. Visão geral das fábricas de classes de dados
5.3. Tuplas nomeadas clássicas
5.4. Tuplas nomeadas com tipo
5.5. Introdução às dicas de tipo
5.6. Mais detalhes sobre @dataclass
5.7. A classe de dados como cheiro no código
5.8. Pattern Matching com instâncias de classes
5.9. Resumo do Capítulo
5.10. Leitura complementar
6. Referências, mutabilidade, e memória
6.1. Novidades nesse capítulo
6.2. Variáveis não são caixas
6.3. Identidade, igualdade e apelidos
6.4. A princípio, cópias são rasas
6.5. Parâmetros de função como referências
6.6. del e coleta de lixo
6.7. Peças que Python prega com imutáveis
6.8. Resumo do capítulo
6.9. Para saber mais
Parte II: Funções como objetos
7. Funções como objetos de primeira classe
7.1. Novidades nesse capítulo
7.2. Tratando uma função como um objeto
7.3. Funções de ordem superior
7.4. Funções anônimas
7.5. Os nove sabores de objetos invocáveis
7.6. Tipos invocáveis definidos pelo usuário
7.7. De parâmetros posicionais a parâmetros somente nomeados
7.8. Pacotes para programação funcional
7.9. Resumo do capítulo
7.10. Leitura complementar
8. Dicas de tipo em funções
8.1. Novidades nesse capítulo
8.2. Sobre tipagem gradual
8.3. Tipagem gradual na prática
8.4. Tipos são definidos pelas operações possíveis
8.5. Tipos próprios para anotações
8.6. Anotando parâmetros apenas posicionais e variádicos
8.7. Tipos imperfeitos e testes poderosos
8.8. Resumo do capítulo
8.9. Para saber mais
9. Decoradores e Clausuras
9.1. Novidades nesse capítulo
9.2. Introdução aos decoradores
9.3. Quando o Python executa decoradores
9.4. Decoradores de registro
9.5. Regras de escopo de variáveis
9.6. Clausuras
9.7. A declaração nonlocal
9.8. Implementando um decorador simples
9.9. Decoradores na biblioteca padrão
9.10. Decoradores parametrizados
9.11. Resumo do capítulo
9.12. Leitura complementar
10. Padrões de projetos com funções de primeira classe
10.1. Novidades nesse capítulo
10.2. Estudo de caso: refatorando Estratégia
10.3. Padrão Estratégia aperfeiçoado com um decorador
10.4. O padrão Comando
10.5. Resumo do Capítulo
10.6. Leitura complementar
Parte III: Classes e protocolos
11. Um objeto pythônico
11.1. Novidades nesse capítulo
11.2. Representações de objetos
11.3. A volta da classe Vector
11.4. Um construtor alternativo
11.5. classmethod versus staticmethod
11.6. Exibição fomatada
11.7. Um Vector2d hashable
11.8. Suportando o pattern matching posicional
11.9. Listagem completa Vector2d, versão 3
11.10. Atributos privados e "protegidos" no Python
11.11. Economizando memória com __slots__
11.12. Sobrepondo atributos de classe
11.13. Resumo do capítulo
11.14. Leitura complementar
12. Métodos especiais para sequências
12.1. Novidades nesse capítulo
12.2. Vector: Um tipo sequência definido pelo usuário
12.3. Vector versão #1: compatível com Vector2d
12.4. Protocolos e o duck typing
12.5. Vector versão #2: Uma sequência fatiável
12.6. Vector versão #3: acesso dinâmico a atributos
12.7. Vector versão #4: o hash e um == mais rápido
12.8. Vector versão #5: Formatando
12.9. Resumo do capítulo
12.10. Leitura complementar
13. Interfaces, protocolos, e ABCs
13.1. O mapa de tipagem
13.2. Novidades nesse capítulo
13.3. Dois tipos de protocolos
13.4. Programando patos
13.5. Goose typing
13.6. Protocolos estáticos
13.7. Resumo do capítulo
13.8. Para saber mais
14. Herança: para o bem ou para o mal
14.1. Novidades nesse capítulo
14.2. A função super()
14.3. É complicado criar subclasses de tipos embutidos
14.4. Herança múltipla e a Ordem de Resolução de Métodos
14.5. Classes mixin
14.6. Herança múltipla no mundo real
14.7. Lidando com a herança
14.8. Resumo do capítulo
14.9. Leitura complementar
15. Mais dicas de tipo
15.1. Novidades nesse capítulo
15.2. Assinaturas sobrepostas
15.3. TypedDict
15.4. Coerção de Tipo
15.5. Lendo dicas de tipo durante a execução
15.6. Implementando uma classe genérica
15.7. Variância
15.8. Implementando um protocolo estático genérico
15.9. Resumo do capítulo
15.10. Leitura complementar
16. Sobrecarga de operadores
16.1. Novidades nesse capítulo
16.2. Introdução à sobrecarga de operadores
16.3. Operadores unários
16.4. Sobrecarregando + para adição de Vector
16.5. Sobrecarregando * para multiplicação escalar
16.6. Usando @ como operador infixo
16.7. Resumindo os operadores aritméticos
16.8. Operadores de comparação cheia
16.9. Operadores de atribuição aumentada
16.10. Resumo do capítulo
16.11. Leitura complementar
Parte IV: Controle de fluxo
17. Iteradores, geradores e corrotinas clássicas
17.1. Novidades nesse capítulo
17.2. Uma sequência de palavras
17.3. Porque sequências são iteráveis: a função iter
17.4. Iteráveis versus iteradores
17.5. Classes Sentence com __iter__
17.6. Sentenças preguiçosas
17.7. Quando usar expressões geradoras
17.8. Um gerador de progressão aritmética
17.9. Funções geradoras na biblioteca padrão
17.10. Funções de redução de iteráveis
17.11. Subgeradoras com yield from
17.12. Tipos iteráveis genéricos
17.13. Corrotinas clássicas
17.14. Resumo do capítulo
17.15. Leitura complementar
18. Instruções with, match, e blocos else
18.1. Novidades nesse capítulo
18.2. Gerenciadores de contexto e a instrução with
18.3. Pattern matching no lis.py: um estudo de caso
18.4. Faça isso, então aquilo: os blocos else além do if
18.5. Resumo do capítulo
18.6. Para saber mais
19. Modelos de concorrência em Python
19.1. Novidades nesse capítulo
19.2. A visão geral
19.3. Um pouco de jargão
19.4. Um "Olá mundo" concorrente
19.5. O real impacto da GIL
19.6. Um pool de processos caseiro
19.7. Python no mundo multi-núcleo.
19.8. Resumo do capítulo
19.9. Para saber mais
20. Executores concorrentes
20.1. Novidades nesse capítulo
20.2. Downloads concorrentes da web
20.3. Iniciando processos com concurrent.futures
20.4. Experimentando com Executor.map
20.5. Download com exibição do progresso e tratamento de erro
20.6. Resumo do capítulo
20.7. Para saber mais
21. Programação assíncrona
21.1. Novidades nesse capítulo
21.2. Algumas definições.
21.3. Um exemplo de asyncio: sondando domínios
21.4. Novo conceito: awaitable ou esperável
21.5. Downloads com asyncio e HTTPX
21.6. Gerenciadores de contexto assíncronos
21.7. Melhorando o download de bandeiras asyncio
21.8. Delegando tarefas a executores
21.9. Programando servidores asyncio
21.10. Iteração assíncrona e iteráveis assíncronos
21.11. Programação assíncrona além do asyncio: Curio
21.12. Dicas de tipo para objetos assíncronos
21.13. Como a programação assíncrona funciona e como não funciona
21.14. Resumo do capítulo
21.15. Para saber mais
Parte V: Metaprogramação
22. Atributos dinâmicos e propriedades
22.1. Novidades nesse capítulo
22.2. Processamento de dados com atributos dinâmicos
22.3. Propriedades computadas
22.4. Usando uma propriedade para validação de atributos
22.5. Considerando as propriedades de forma adequada
22.6. Criando uma fábrica de propriedades
22.7. Tratando a exclusão de atributos
22.8. Atributos e funções essenciais para tratamento de atributos
22.9. Resumo do capítulo
22.10. Leitura Complementar
🚧
23. Descritores de atributos
🚧
24. Metaprogramação com classes
Sobre esta tradução
Python Fluente, Segunda Edição é uma tradução direta de Fluent Python, Second Edition (O’Reilly, 2022). Não é uma obra
derivada de Python Fluente (Novatec, 2015).

A presente tradução foi autorizada pela O’Reilly Media para distribuição nos termos da licença CC BY-NC-ND
(https://creativecommons.org/licenses/by-nc-nd/4.0/deed.pt_BR). Os arquivos-fonte em formato Asciidoc estão no repositório
público https://github.com/pythonfluente/pythonfluente2e.

Esta tradução está em construção. No sumário, capítulos ainda não traduzidos estão marcados com o
🚧
símbolo (em obras).

Os links com colchetes [assim] não funcionam porque são referências cruzadas para capítulos ainda
não publicados. Eles passarão a funcionar automaticamente quando seus destinos estiverem no ar.

✒️ NOTA Priorizamos os capítulos 6, 8, 13, 19, 20, e 21. para apoiar o curso Python Engineer a ser lançado pela
LINUXtips (https://www.linuxtips.io/) em 2023. Depois do capítulo 21, os capítulos restantes serão
traduzidos em ordem do 1 ao 24.

No momento não precisamos de ajuda para traduzir, mas correções ou sugestões de melhorias são
muito bem vindas! Para contribuir, veja os issues (https://github.com/pythonfluente/pythonfluente2e/issues)
no repositório https://github.com/pythonfluente/pythonfluente2e.

Histórico das traduções


Escrevi a primeira e a segunda edições deste livro originalmente em inglês, para serem mais facilmente distribuídas no
mercado internacional.

Cedi os direitos exclusivos para a O’Reilly Media, nos termos usuais de contratos com editoras famosas: elas ficam com
a maior parte do lucro, o direito de publicar, e o direito de vender licenças para tradução em outros idiomas.

Até 2022, a primeira edição foi publicada nesses idiomas:

1. inglês,
2. português brasileiro,
3. chinês simplificado (China),
4. chinês tradicional (Taiwan),
5. japonês,
6. coreano,
7. russo,
8. francês,
9. polonês.

A ótima tradução PT-BR foi produzida e publicada no Brasil pela Editora Novatec em 2015, sob licença da O’Reilly.

Entre 2020 e 2022, atualizei e expandi bastante o livro para a segunda edição. Sou muito grato à liderança da
Thoughtworks Brasil (https://www.thoughtworks.com/pt-br) por todo o apoio que têm me dado nesse projeto. Quando
entreguei o manuscrito para a O’Reilly, negociei um adendo contratual para liberar a tradução da segunda edição em
PT-BR com uma licença livre, como uma contribuição para comunidade Python lusófona.
A O’Reilly autorizou que essa tradução fosse publicada sob a licença CC BY-NC-ND: Creative Commons — Atribuição-
NãoComercial-SemDerivações 4.0 Internacional (https://creativecommons.org/licenses/by-nc-nd/4.0/deed.pt_BR). Com essa
mudança contratual, a Editora Novatec não teve interesse em traduzir e publicar a segunda edição.

Felizmente encontrei meu querido amigo Paulo Candido de Oliveira Filho (PC). Fomos colegas do ensino fundamental
ao médio, e depois trabalhamos juntos como programadores em diferentes momentos e empresas. Hoje ele presta
serviços editoriais, inclusive faz traduções com a excelente qualidade desta aqui.

Contratei PC para traduzir. Estou fazendo a revisão técnica, gerando os arquivos HTML com Asciidoctor
(https://asciidoctor.org/) e publicando em https://PythonFluente.com. Estamos trabalhando diretamente a partir do Fluent
Python, Second Edition da O’Reilly, sem aproveitar a tradução da primeira edição, cujo copyright pertence à Novatec.

O copyright desta tradução pertence a mim.

Luciano Ramalho, São Paulo, 13 de março de 2023


Parte I: Estruturas de dados
1. O modelo de dados do Python
O senso estético de Guido para o design de linguagens é incrível. Conheci muitos projetistas capazes de criar
linguagens teoricamente lindas, que ninguém jamais usaria. Mas Guido é uma daquelas raras pessoas capaz criar
uma linguagem só um pouco menos teoricamente linda que, por isso mesmo, é uma delícia para programar.

Jim Hugunin, criador do Jython, co-criador do AspectJ, arquiteto do DLR (Dynamic Language Runtime) do .Net.
"Story of Jython" (_A História do Jython_) (https://fpy.li/1-1) (EN), escrito como prefácio ao Jython Essentials
(https://fpy.li/1-2) (EN), de Samuele Pedroni e Noel Rappin (O'Reilly).

Uma das melhores qualidades do Python é sua consistência. Após trabalhar com Python por algum tempo é possível
intuir, de uma maneira informada e correta, o funcionamento de recursos que você acabou de conhecer.

Entretanto, se você aprendeu outra linguagem orientada a objetos antes do Python, pode achar estranho usar
len(collection) em vez de collection.len() . Essa aparente esquisitice é a ponta de um iceberg que, quando
compreendido de forma apropriada, é a chave para tudo aquilo que chamamos de pythônico. O iceberg se chama o
Modelo de Dados do Python, e é a API que usamos para fazer nossos objetos lidarem bem com os aspectos mais
idiomáticos da linguagem.

É possível pensar no modelo de dados como uma descrição do Python na forma de uma framework. Ele formaliza as
interfaces dos elementos constituintes da própria linguagem, como sequências, funções, iteradores, corrotinas, classes,
gerenciadores de contexto e assim por diante.

Quando usamos uma framework, gastamos um bom tempo programando métodos que são chamados por ela. O
mesmo acontece quando nos valemos do Modelo de Dados do Python para criar novas classes. O interpretador do
Python invoca métodos especiais para realizar operações básicas sobre os objetos, muitas vezes acionados por uma
sintaxe especial. Os nomes dos métodos especiais são sempre precedidos e seguidos de dois sublinhados. Por exemplo,
a sintaxe obj[key] está amparada no método especial __getitem__ . Para resolver my_collection[key] , o
interpretador chama my_collection.__getitem__(key) .

Implementamos métodos especiais quando queremos que nossos objetos suportem e interajam com elementos
fundamentais da linguagem, tais como:

Coleções
Acesso a atributos
Iteração (incluindo iteração assíncrona com async for )

Sobrecarga (overloading) de operadores


Invocação de funções e métodos
Representação e formatação de strings
Programação assíncrona usando await

Criação e destruição de objetos


Contextos gerenciados usando as instruções with ou async with
Mágica e o "dunder"
O termo método mágico é uma gíria usada para se referir aos métodos especiais, mas como falamos
de um método específico, por exemplo __getitem__ ? Aprendi a dizer "dunder-getitem" com o
autor e professor Steve Holden. "Dunder" é uma contração da frase em inglês "double underscore
✒️ NOTA before and after" (sublinhado duplo antes e depois). Por isso os métodos especiais são também
conhecidos como métodos dunder. O capítulo "Análise Léxica"
(https://docs.python.org/pt-br/3/reference/lexical_analysis.html#reserved-classes-of-identifiers) de A Referência
da Linguagem Python adverte que "Qualquer uso de nomes no formato __*__ que não siga
explicitamente o uso documentado, em qualquer contexto, está sujeito a quebra sem aviso prévio."

1.1. Novidades nesse capítulo


Esse capítulo sofreu poucas alterações desde a primeira edição, pois é uma introdução ao Modelo de Dados do Python,
que é muito estável. As mudanças mais significativas foram:

Métodos especiais que suportam programação assíncrona e outras novas funcionalidades foram acrescentados às
tabelas em Seção 1.4.
A Figura 2, mostrando o uso de métodos especiais em Seção 1.3.4, incluindo a classe base abstrata
collections.abc.Collection , introduzida no Python 3.6.

Além disso, aqui e por toda essa segunda edição, adotei a sintaxe f-string, introduzida no Python 3.6, que é mais legível
e muitas vezes mais conveniente que as notações de formatação de strings mais antigas: o método str.format() e o
operador % .

Existe ainda uma razão para usar my_fmt.format() : quando a definição de my_fmt precisa vir de
um lugar diferente daquele onde a operação de formatação precisa acontecer no código. Por
👉 DICA exemplo, quando my_fmt tem múltiplas linhas e é melhor definida em uma constante, ou quando
tem de vir de um arquivo de configuração ou de um banco de dados. Essas são necessidades reais,
mas não acontecem com frequência.

1.2. Um baralho pythônico


O Exemplo 1 é simples, mas demonstra as possibilidades que se abrem com a implementação de apenas dois métodos
especiais, __getitem__ e __len__ .

Exemplo 1. Um baralho como uma sequência de cartas


PYTHON3
import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):


return self._cards[position]

A primeira coisa a se observar é o uso de collections.namedtuple para construir uma classe simples representando
cartas individuais. Usamos namedtuple para criar classes de objetos que são apenas um agrupamento de atributos,
sem métodos próprios, como um registro de banco de dados. Neste exemplo, a utilizamos para fornecer uma boa
representação para as cartas em um baralho, como mostra a sessão no console:

PYCON
>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')

Mas a parte central desse exemplo é a classe FrenchDeck . Ela é curta, mas poderosa. Primeiro, como qualquer coleção
padrão do Python, uma instância de FrenchDeck responde à função len() , devolvendo o número de cartas naquele
baralho:

PYCON
>>> deck = FrenchDeck()
>>> len(deck)
52

Ler cartas específicas do baralho é fácil, graças ao método __getitem__ . Por exemplo, a primeira e a última carta:

PYCON
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')

Deveríamos criar um método para obter uma carta aleatória? Não é necessário. O Python já tem uma função que
devolve um item aleatório de uma sequência: random.choice . Podemos usá-la em uma instância de FrenchDeck :

PYCON
>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>> choice(deck)
Card(rank='2', suit='clubs')

Acabamos de ver duas vantagens de usar os métodos especiais no contexto do Modelo de Dados do Python.
Os usuários de suas classes não precisam memorizar nomes arbitrários de métodos para operações comuns ("Como
descobrir o número de itens? Seria .size() , .length() ou alguma outra coisa?")
É mais fácil de aproveitar a rica biblioteca padrão do Python e evitar reinventar a roda, como no caso da função
random.choice .

Mas fica melhor.

Como nosso __getitem__ usa o operador [] de self._cards , nosso baralho suporta fatiamento automaticamente.
Podemos olhar as três primeiras cartas no topo de um novo baralho, e depois pegar apenas os ases, iniciando com o
índice 12 e pulando 13 cartas por vez:

PYCON
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

E como já temos o método especial __getitem__ , nosso baralho é um objeto iterável, ou seja, pode ser percorrido em
um laço for :

PYCON
>>> for card in deck: # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...

Também podemos iterar sobre o baralho na ordem inversa:

PYCON
>>> for card in reversed(deck): # doctest: +ELLIPSIS
... print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...

Reticências nos doctests


Sempre que possível, extraí as listagens do console do Python usadas neste livro com o doctest
(https://docs.python.org/pt-br/3/library/doctest.html), para garantir a precisão. Quando a saída era grande
demais, a parte omitida está marcada por reticências ( …​), como na última linha do trecho de código
✒️ NOTA anterior.

Nesse casos, usei a diretiva # doctest: +ELLIPSIS para fazer o doctest funcionar. Se você estiver
tentando rodar esses exemplos no console iterativo, pode simplesmente omitir todos os comentários
de doctest.

A iteração muitas vezes é implícita. Se uma coleção não fornecer um método __contains__ , o operador in realiza
uma busca sequencial. No nosso caso, in funciona com nossa classe FrenchDeck porque ela é iterável. Veja a seguir:
PYCON
>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False

E o ordenamento? Um sistema comum de ordenar cartas é por seu valor numérico (ases sendo os mais altos) e depois
por naipe, na ordem espadas (o mais alto), copas, ouros e paus (o mais baixo). Aqui está uma função que ordena as
cartas com essa regra, devolvendo 0 para o 2 de paus e 51 para o às de espadas.

PYTHON3
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]

Podemos agora listar nosso baralho em ordem crescente de usando spades_high como critério de ordenação:

PYCON
>>> for card in sorted(deck, key=spades_high): # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (46 cards omitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

Apesar da FrenchDeck herdar implicitamente da classe object , a maior parte de sua funcionalidade não é herdada,
vem do uso do modelo de dados e de composição. Ao implementar os métodos especiais __len__ e __getitem__ ,
nosso FrenchDeck se comporta como uma sequência Python padrão, podendo assim se beneficiar de recursos centrais
da linguagem (por exemplo, iteração e fatiamento), e da biblioteca padrão, como mostramos nos exemplos usando
random.choice , reversed , e sorted . Graças à composição, as implementações de __len__ e __getitem__ podem
delegar todo o trabalho para um objeto list , especificamente self._cards .

E como embaralhar as cartas?


Como foi implementado até aqui, um FrenchDeck não pode ser embaralhado, porque as cartas e
✒️ NOTA suas posições não podem ser alteradas, exceto violando o encapsulamento e manipulando o atributo
_cards diretamente. Em Capítulo 13 vamos corrigir isso acrescentando um método __setitem__
de uma linha. Você consegue imaginar como ele seria implementado?

1.3. Como os métodos especiais são utilizados


A primeira coisa para se saber sobre os métodos especiais é que eles foram feitos para serem chamados pelo
interpretador Python, e não por você. Você não escreve my_object.__len__() . Escreve len(my_object) e, se
my_object é uma instância de de uma classe definida pelo usuário, então o Python chama o método __len__ que
você implementou.

Mas o interpretador pega um atalho quando está lidando com um tipo embutido como list , str , bytearray , ou
extensões como os arrays do NumPy. As coleções de tamanho variável do Python escritas em C incluem uma struct[1]
chamada PyVarObject , com um campo ob_size que mantém o número de itens na coleção. Então, se my_object é
uma instância de algum daqueles tipos embutidos, len(my_object) lê o valor do campo ob_size , e isso é muito mais
rápido que chamar um método.
Na maior parte das vezes, a chamada a um método especial é implícita. Por exemplo, o comando for i in x: na
verdade gera uma invocação de iter(x) , que por sua vez pode chamar x.__iter__() se esse método estiver
disponível, ou usar x.__getitem__() , como no exemplo do FrenchDeck .

Em condições normais, seu código não deveria conter muitas chamadas diretas a métodos especiais. A menos que você
esteja fazendo muita metaprogramação, implementar métodos especiais deve ser muito mais frequente que invocá-los
explicitamente. O único método especial que é chamado frequentemente pelo seu código é __init__ , para invocar o
método de inicialização da superclasse na implementação do seu próprio __init__ .

Geralmente, se você precisa invocar um método especial, é melhor chamar a função embutida relacionada (por
exemplo, len , iter , str , etc.). Essas funções chamam o método especial correspondente, mas também fornecem
outros serviços e—para tipos embutidos—são mais rápidas que chamadas a métodos. Veja, por exemplo, Seção 17.3.1
no Capítulo 17.

Na próxima seção veremos alguns dos usos mais importantes dos métodos especiais:

Emular tipos numéricos


Representar objetos na forma de strings
Determinar o valor booleano de um objeto
Implementar de coleções

1.3.1. Emulando tipos numéricos


Vários métodos especiais permitem que objetos criados pelo usuário respondam a operadores como + . Vamos tratar
disso com mais detalhes no capítulo Capítulo 16. Aqui nosso objetivo é continuar ilustrando o uso dos métodos
especiais, através de outro exemplo simples.

Vamos implementar uma classe para representar vetores bi-dimensionais—isto é, vetores euclidianos como aqueles
usados em matemática e física (veja a Figura 1).

👉 DICA O tipo embutido complex pode ser usado para representar vetores bi-dimensionais, mas nossa
classe pode ser estendida para representar vetores n-dimensionais. Faremos isso em Capítulo 17.
Figura 1. Exemplo de adição de vetores bi-dimensionais; Vector(2, 4) + Vector(2, 1) resulta em Vector(4, 5).

Vamos começar a projetar a API para essa classe escrevendo em uma sessão de console simulada, que depois podemos
usar como um doctest. O trecho a seguir testa a adição de vetores ilustrada na Figura 1:

PYCON
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

Observe como o operador + produz um novo objeto Vector(4, 5) .

A função embutida abs devolve o valor absoluto de números inteiros e de ponto flutuante, e a magnitude de números
complex . Então, por consistência, nossa API também usa abs para calcular a magnitude de um vetor:

PYCON
>>> v = Vector(3, 4)
>>> abs(v)
5.0
Podemos também implementar o operador * , para realizar multiplicação escalar (isto é, multiplicar um vetor por um
número para obter um novo vetor de mesma direção e magnitude multiplicada):

PYCON
>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0

O Exemplo 2 é uma classe Vector que implementa as operações descritas acima, usando os métodos especiais
__repr__ , __abs__ , __add__ , e __mul__ .

Exemplo 2. A simple two-dimensional vector class


PYTHON3
"""
vector2d.py: a simplistic class demonstrating some special methods

It is simplistic for didactic reasons. It lacks proper error handling,


especially in the ``__add__`` and ``__mul__`` methods.

This example is greatly expanded later in the book.

Addition::

>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

Absolute value::

>>> v = Vector(3, 4)
>>> abs(v)
5.0

Scalar multiplication::

>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0

"""

import math

class Vector:

def __init__(self, x=0, y=0):


self.x = x
self.y = y

def __repr__(self):
return f'Vector({self.x!r}, {self.y!r})'

def __abs__(self):
return math.hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))

def __add__(self, other):


x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

def __mul__(self, scalar):


return Vector(self.x * scalar, self.y * scalar)

Implementamos cinco métodos especiais, além do costumeiro __init__ . Veja que nenhum deles é chamado
diretamente dentro da classe ou durante seu uso normal, ilustrado pelos doctests. Como mencionado antes, o
interpretador Python é o único usuário frequente da maioria dos métodos especiais.

O Exemplo 2 implementa dois operadores: + e * , para demonstrar o uso básico de __add__ e __mul__ . No dois
casos, os métodos criam e devolvem uma nova instância de Vector , e não modificam nenhum dos operandos: self e
other são apenas lidos. Esse é o comportamento esperado de operadores infixos: criar novos objetos e não tocar em
seus operandos. Vou falar muito mais sobre esse tópico no capítulo Capítulo 16.
Da forma como está implementado, o Exemplo 2 permite multiplicar um Vector por um número,
⚠️ AVISO mas não um número por um Vector , violando a propriedade comutativa da multiplicação escalar.
Vamos consertar isso com o método especial __rmul__ no capítulo Capítulo 16.

Nas seções seguintes vamos discutir os outros métodos especiais em Vector .

1.3.2. Representação de strings


O método especial __repr__ é chamado pelo repr embutido para obter a representação do objeto como string, para
inspeção. Sem um __repr__ personalizado, o console do Python mostraria uma instância de Vector como <Vector
object at 0x10e100070> .

O console iterativo e o depurador chamam repr para exibir o resultado das expressões. O repr também é usado:

Pelo marcador posicional %r na formatação clássica com o operador % . Ex.: '%r' % my_obj

Pelo sinalizador de conversão !r na nova sintaxe de strings de formato


(https://docs.python.org/pt-br/3.10/library/string.html#format-string-syntax) usada nas f-strings e no método str.format . Ex:
f'{my_obj!r}'

Note que a f-string no nosso __repr__ usa !r para obter a representação padrão dos atributos a serem exibidos. Isso
é uma boa prática, pois durante uma seção de depuração podemos ver a diferença entre Vector(1, 2) e
Vector('1', '2') . Este segundo objeto não funcionaria no contexto desse exemplo, porque nosso código espera que
os argumentos do construtor sejam números, não str .

A string devolvida por __repr__ não deve ser ambígua e, se possível, deve corresponder ao código-fonte necessário
para recriar o objeto representado. É por isso que nossa representação de Vector se parece com uma chamada ao
construtor da classe, por exemplo Vector(3, 4) .

Por outro lado, __str__ é chamado pelo método embutido str() e usado implicitamente pela função print . Ele
deve devolver uma string apropriada para ser exibida aos usuários finais.

Algumas vezes a própria string devolvida por __repr__ é adequada para exibir ao usuário, e você não precisa
programar __str__ , porque a implementação herdada da classe object chama __repr__ como alternativa. O
Exemplo 2 é um dos muitos exemplos neste livro com um __str__ personalizado.

Programadores com experiência anterior em linguagens que contém o método toString tendem a
implementar __str__ e não __repr__ . Se você for implementar apenas um desses métodos
especiais, escolha __repr__ .
👉 DICA
"What is the difference between __str__ and __repr__ in Python?" (Qual a diferença entre
__str__ e __repr__ em Python?) (https://fpy.li/1-5) (EN) é uma questão no Stack Overflow com
excelentes contribuições dos pythonistas Alex Martelli e Martijn Pieters.

1.3.3. O valor booleano de um tipo personalizado


Apesar do Python ter um tipo bool , ele aceita qualquer objeto em um contexto booleano, tal como as expressões
controlando uma instrução if ou while , ou como operandos de and , or e not . Para determinar se um valor x é
verdadeiro ou falso, o Python invoca bool(x) , que devolve True ou False .
Por default, instâncias de classes definidas pelo usuário são consideradas verdadeiras, a menos que __bool__ ou
__len__ estejam implementadas. Basicamente, bool(x) chama x.__bool__() e usa o resultado. Se __bool__ não
está implementado, o Python tenta invocar x.__len__() , e se esse último devolver zero, bool devolve False . Caso
contrário, bool devolve True .

Nossa implementação de __bool__ é conceitualmente simples: ela devolve False se a magnitude do vetor for zero,
caso contrário devolve True . Convertemos a magnitude para um valor booleano usando bool(abs(self)) , porque
espera-se que __bool__ devolva um booleano. Fora dos métodos __bool__ , raramente é necessário chamar bool()
explicitamente, porque qualquer objeto pode ser usado em um contexto booleano.

Observe que o método especial __bool__ permite que seus objetos sigam as regras de teste do valor verdade definidas
no capítulo "Tipos Embutidos" (https://docs.python.org/pt-br/3/library/stdtypes.html#truth) da documentação da Biblioteca
Padrão do Python.

Essa é uma implementação mais rápida de Vector.__bool__ :

PYTHON3
def __bool__(self):
return bool(self.x or self.y)

✒️ NOTA
Isso é mais difícil de ler, mas evita a jornada através de abs , __abs__ , os quadrados, e a raiz
quadrada. A conversão explícita para bool é necessária porque __bool__ deve devolver um
booleano, e or devolve um dos seus operandos no formato original: x or y resulta em x se x for
verdadeiro, caso contrário resulta em y , qualquer que seja o valor deste último.

1.3.4. A API de Collection


A Figura 2 documenta as interfaces dos tipos de coleções essenciais na linguagem. Todas as classes no diagrama são
ABCs—classes base abstratas (ABC é a sigla para a mesma expressão em inglês, Abstract Base Classes). As ABCs e o
módulo collections.abc são tratados no capítulo Capítulo 13. O objetivo dessa pequena seção é dar uma visão
panorâmica das interfaces das coleções mais importantes do Python, mostrando como elas são criadas a partir de
métodos especiais.
Figura 2. Diagrama de classes UML com os tipos fundamentais de coleções. Métodos como nome em itálico são
abstratos, então precisam ser implementados pelas subclasses concretas, tais como list e dict . O restante dos
métodos tem implementações concretas, então as subclasses podem herdá-los.

Cada uma das ABCs no topo da hierarquia tem um único método especial. A ABC Collection (introduzida no Python
3.6) unifica as três interfaces essenciais, que toda coleção deveria implementar:

Iterable , para suportar for , desempacotamento


(https://docs.python.org/pt-br/3/tutorial/controlflow.html#unpacking-argument-lists), e outras formas de iteração

Sized para suportar a função embutida len

Container para suportar o operador in

Na verdade, o Python não exige que classes concretas herdem de qualquer dessas ABCs. Qualquer classe que
implemente __len__ satisfaz a interface Sized .

Três especializações muito importantes de Collection são:

Sequence , formalizando a interface de tipos embutidos como list e str

Mapping , implementado por dict , collections.defaultdict , etc.

Set , a interface dos tipos embutidos set e frozenset


Apenas Sequence é Reversible , porque sequências suportam o ordenamento arbitrário de seu conteúdo, ao
contrário de mapeamentos(mappings) e conjuntos(sets).

Desde o Python 3.7, o tipo dict é oficialmente "ordenado", mas isso só que dizer que a ordem de
✒️ NOTA inserção das chaves é preservada. Você não pode rearranjar as chaves em um dict da forma que
quiser.

Todos os métodos especiais na ABC Set implementam operadores infixos. Por exemplo, a & b calcula a intersecção
entre os conjuntos a e b , e é implementada no método especial __and__ .

Os próximos dois capítulos vão tratar em detalhes das sequências, mapeamentos e conjuntos da biblioteca padrão.

Agora vamos considerar as duas principais categorias dos métodos especiais definidos no Modelo de Dados do Python.

1.4. Visão geral dos, métodos especiais


O capítulo "Modelo de Dados" (https://docs.python.org/pt-br/3/reference/datamodel.html) de A Referência da Linguagem Python
lista mais de 80 nomes de métodos especiais. Mais da metade deles implementa operadores aritméticos, bit a bit, ou de
comparação. Para ter uma visão geral do que está disponível, veja tabelas a seguir.

A Tabela 1 mostra nomes de métodos especiais, excluindo aqueles usados para implementar operadores infixos ou
funções matemáticas fundamentais como abs . A maioria desses métodos será tratado ao longo do livro, incluindo as
adições mais recentes: métodos especiais assíncronos como __anext__ (acrescentado no Python 3.5), e o método de
personalização de classes, __init_subclass__ (do Python 3.6).

Tabela 1. Nomes de métodos especiais (excluindo operadores)

Categoria Nomes dos métodos

Representação de string/bytes __repr__ __str__ __format__ __bytes__ __fspath__

Conversão para número __bool__ __complex__ __int__ __float__ __hash__


__index__

Emulação de coleções __len__ __getitem__ __setitem__ __delitem__


__contains__

Iteração __iter__ __aiter__ __next__ __anext__


__reversed__

Execução de chamável ou corrotina __call__ __await__

Gerenciamento de contexto __enter__ __exit__ __aexit__ __aenter__

Criação e destruição de instâncias __new__ __init__ __del__

Gerenciamento de atributos __getattr__ __getattribute__ __setattr__


__delattr__ __dir__

Descritores de atributos __get__ __set__ __delete__ __set_name__

Classes base abstratas __instancecheck__ __subclasscheck__


Categoria Nomes dos métodos

Metaprogramação de classes __prepare__ __init_subclass__ __class_getitem__


__mro_entries__

Operadores infixos e numéricos são suportados pelos métodos especiais listados na Tabela 2. Aqui os nomes mais
recentes são __matmul__ , __rmatmul__ , e __imatmul__ , adicionados no Python 3.5 para suportar o uso de @ como
um operador infixo de multiplicação de matrizes, como veremos no capítulo Capítulo 16.

Tabela 2. Nomes e símbolos de métodos especiais para operadores


Categoria do operador Símbolos Nomes de métodos

Unário numérico - + abs() __neg__ __pos__ __abs__

Comparação rica < <= == != > >= __lt__ __le__ __eq__ __ne__
__gt__ __ge__

Aritmético + - * / // % @ divmod() round() __add__ __sub__ __mul__


** pow() __truediv__ __floordiv__
__mod__ __matmul__ __divmod__
__round__ __pow__

Aritmética reversa operadores aritméticos com __radd__ __rsub__ __rmul__


operandos invertidos) __rtruediv__ __rfloordiv__
__rmod__ __rmatmul__
__rdivmod__ __rpow__

Atribuição aritmética aumentada += -= *= /= //= %= @= **= __iadd__ __isub__ __imul__


__itruediv__ __ifloordiv__
__imod__ __imatmul__ __ipow__

Bit a bit & | ^ << >> ~ __and__ __or__ __xor__


__lshift__ __rshift__
__invert__

Bit a bit reversa (operadores bit a bit com os __rand__ __ror__ __rxor__
operandos invertidos) __rlshift__ __rrshift__

Atribuição bit a bit aumentada &= |= ^= <⇐ >>= __iand__ __ior__ __ixor__
__ilshift__ __irshift__

O Python invoca um método especial de operador reverso no segundo argumento quando o método
especial correspondente não pode ser usado no primeiro operando. Atribuições aumentadas são
✒️ NOTA atalho combinando um operador infixo com uma atribuição de variável, por exemplo a += b .

O capítulo Capítulo 16 explica em detalhes os operadores reversos e a atribuição aumentada.


1.5. Porque len não é um método?
Em 2013, fiz essa pergunta a Raymond Hettinger, um dos desenvolvedores principais do Python, e o núcleo de sua
resposta era uma citação do "The Zen of Python" (O Zen do Python) (https://fpy.li/1-8) (EN): "a praticidade vence a pureza."
Em Seção 1.3, descrevi como len(x) roda muito rápido quando x é uma instância de um tipo embutido. Nenhum
método é chamado para os objetos embutidos do CPython: o tamanho é simplesmente lido de um campo em uma struct
C. Obter o número de itens em uma coleção é uma operação comum, e precisa funcionar de forma eficiente para tipos
tão básicos e diferentes como str , list , memoryview , e assim por diante.

Em outras palavras, len não é chamado como um método porque recebe um tratamento especial como parte do
Modelo de Dados do Python, da mesma forma que abs . Mas graças ao método especial __len__ , também é possível
fazer len funcionar com nossos objetos personalizados. Isso é um compromisso justo entre a necessidade de objetos
embutidos eficientes e a consistência da linguagem. Também de "O Zen do Python": "Casos especiais não são especiais o
bastante para quebrar as regras."

Pensar em abs e len como operadores unários nos deixa mais inclinados a perdoar seus aspectos
funcionais, contrários à sintaxe de chamada de método que esperaríamos em uma linguagem
✒️ NOTA orientada a objetos. De fato, a linguagem ABC—uma ancestral direta do Python, que antecipou
muitas das funcionalidades desta última—tinha o operador # , que era o equivalente de len (se
escrevia #s ). Quando usado como operador infixo, x#s contava as ocorrências de x em s , que em
Python obtemos com s.count(x) , para qualquer sequência s .

1.6. Resumo do capítulo


Ao implementar métodos especiais, seus objetos podem se comportar como tipos embutidos, permitindo o estilo de
programação expressivo que a comunidade considera pythônico.

Uma exigência básica para um objeto Python é fornecer strings representando a si mesmo que possam ser usadas, uma
para depuração e registro (log), outra para apresentar aos usuários finais. É para isso que os métodos especiais
__repr__ e __str__ existem no modelo de dados.

Emular sequências, como mostrado com o exemplo do FrenchDeck , é um dos usos mais comuns dos métodos
especiais. Por exemplo, bibliotecas de banco de dados frequentemente devolvem resultados de consultas na forma de
coleções similares a sequências. Tirar o máximo proveito dos tipos de sequências existentes é o assunto do capítulo
Capítulo 2. Como implementar suas próprias sequências será visto na seção Capítulo 12, onde criaremos uma extensão
multidimensional da classe Vector .

Graças à sobrecarga de operadores, o Python oferece uma rica seleção de tipos numéricos, desde os tipos embutidos até
decimal.Decimal e fractions.Fraction , todos eles suportando operadores aritméticos infixos. As bibliotecas de
ciência de dados NumPy suportam operadores infixos com matrizes e tensores. A implementação de operadores—
incluindo operadores reversos e atribuição aumentada—será vista no capítulo Capítulo 16, usando melhorias do
exemplo Vector .

Também veremos o uso e a implementação da maioria dos outros métodos especiais do Modelo de Dados do Python ao
longo deste livro.

1.7. Para saber mais


O capítulo "Modelo de Dados" (https://docs.python.org/pt-br/3/reference/datamodel.html) em A Referência da Linguagem Python
é a fonte canônica para o assunto desse capítulo e de uma boa parte deste livro.
Python in a Nutshell, 3rd ed. (https://fpy.li/pynut3) (EN), de Alex Martelli, Anna Ravenscroft, e Steve Holden (O’Reilly) tem
uma excelente cobertura do modelo de dados. Sua descrição da mecânica de acesso a atributos é a mais competente
que já vi, perdendo apenas para o próprio código-fonte em C do CPython. Martelli também é um contribuidor prolífico
do Stack Overflow, com mais de 6200 respostas publicadas. Veja seu perfil de usuário no Stack Overflow (https://fpy.li/1-9).

David Beazley tem dois livros tratando do modelo de dados em detalhes, no contexto do Python 3: Python Essential
Reference (https://dabeaz.com/per.html) (EN), 4th ed. (Addison-Wesley), e Python Cookbook, 3rd ed. (https://fpy.li/pycook3) (EN)
(O’Reilly), com a co-autoria de Brian K. Jones.

O The Art of the Metaobject Protocol (https://mitpress.mit.edu/books/art-metaobject-protocol) (EN) (MIT Press) de Gregor
Kiczales, Jim des Rivieres, e Daniel G. Bobrow explica o conceito de um protocolo de metaobjetos, do qual o Modelo de
Dados do Python é um exemplo.

Ponto de Vista
Modelo de dados ou modelo de objetos?

Aquilo que a documentação do Python chama de "Modelo de Dados do Python", a maioria dos autores diria que é
o "Modelo de objetos do Python"

O Python in a Nutshell, 3rd ed. de Martelli, Ravenscroft, e Holden, e o Python Essential Reference, 4th ed., de David
Beazley são os melhores livros sobre o Modelo de Dados do Python, mas se referem a ele como o "modelo de
objetos." Na Wikipedia, a primeira definição de "modelo de objetos" (https://fpy.li/1-10) (EN) é: "as propriedades dos
objetos em geral em uma linguagem de programação de computadores específica." É disso que o Modelo de
Dados do Python trata. Neste livro, usarei "modelo de dados" porque a documentação prefere este termo ao se
referir ao modelo de objetos do Python, e porque esse é o título do capítulo de A Referência da Linguagem Python
(https://docs.python.org/pt-br/3/reference/datamodel.html) mais relevante para nossas discussões.

Métodos de "trouxas"

O The Original Hacker’s Dictionary (Dicionário Hacker Original) (https://fpy.li/1-11) (EN) define mágica como "algo
ainda não explicado ou muito complicado para explicar" ou "uma funcionalidade, em geral não divulgada, que
permite fazer algo que de outra forma seria impossível."

A comunidade Ruby chama o equivalente dos métodos especiais naquela linguagem de métodos mágicos. Muitos
integrantes da comunidade Python também adotam esse termo. Eu acredito que os métodos especiais são o
contrário de mágica. O Python e o Ruby oferecem a seus usuários um rico protocolo de metaobjetos
integralmente documentado, permitindo que "trouxas" como você e eu possam emular muitas das
funcionalidades disponíveis para os desenvolvedores principais que escrevem os interpretadores daquelas
linguagens.

Por outro lado, pense no Go. Alguns objetos naquela linguagem tem funcionalidades que são mágicas, no sentido
de não poderem ser emuladas em nossos próprios objetos definidos pelo usuário. Por exemplo, os arrays, strings
e mapas do Go suportam o uso de colchetes para acesso a um item, na forma a[i] . Mas não há como fazer a
notação [] funcionar com um novo tipo de coleção definida por você. Pior ainda, o Go não tem o conceito de
uma interface iterável ou um objeto iterador ao nível do usuário, daí sua sintaxe para for/range estar limitada
a suportar cinco tipos "mágicos" embutidos, incluindo arrays, strings e mapas.

Talvez, no futuro, os projetistas do Go melhorem seu protocolo de metaobjetos. Em 2021, ele ainda é muito mais
limitado do que Python, Ruby, e JavaScript oferecem.

Metaobjetos
The Art of the Metaobject Protocol (AMOP) (A Arte do protocolo de metaobjetos) é meu título favorito entre livros
de computação. Mas o menciono aqui porque o termo protocolo de metaobjetos é útil para pensar sobre o Modelo
de Dados do Python, e sobre recursos similares em outras linguagens. A parte metaobjetos se refere aos objetos
que são os componentes essenciais da própria linguagem. Nesse contexto, protocolo é sinônimo de interface.
Assim, um protocolo de metaobjetos é um sinônimo chique para modelo de objetos: uma API para os elementos
fundamentais da linguagem.

Um protocolo de metaobjetos rico permite estender a linguagem para suportar novos paradigmas de
programação. Gregor Kiczales, o primeiro autor do AMOP, mais tarde se tornou um pioneiro da programação
orientada a aspecto, e o autor inicial do AspectJ, uma extensão de Java implementando aquele paradigma. A
programação orientada a aspecto é muito mais fácil de implementar em uma linguagem dinâmica como Python,
e algumas frameworks fazem exatamente isso. O exemplo mais importante é a zope.interface (https://fpy.li/1-12)
(EN), parte da framework sobre a qual o sistema de gerenciamento de conteúdo Plone (https://plone.org.br/) é
construído.
2. Uma coleção de sequências
Como vocês podem ter notado, várias das operações mencionadas funcionam da mesma forma com textos, listas e
tabelas. Coletivamente, textos, listas e tabelas são chamados de 'trens' (trains). [...] O comando `FOR` também
funciona, de forma geral, em trens.

Leo Geurts, Lambert Meertens, e Steven Pembertonm, ABC Programmer's Handbook, p. 8. (Bosko Books)

Antes de criar o Python, Guido foi um dos desenvolvedores da linguagem ABC—um projeto de pesquisa de 10 anos
para criar um ambiente de programação para iniciantes. A ABC introduziu várias ideias que hoje consideramos
"pithônicas": operações genéricas com diferentes tipos de sequências, tipos tupla e mapeamento embutidos, estrutura
[do código] por indentação, tipagem forte sem declaração de variáveis, entre outras. O Python não é assim tão amigável
por acidente.

O Python herdou da ABC o tratamento uniforme de sequências. Strings, listas, sequências de bytes, arrays, elementos
XML e resultados vindos de bancos de dados compartilham um rico conjunto de operações comuns, incluindo iteração,
fatiamento, ordenação e concatenação.

Entender a variedade de sequências disponíveis no Python evita que reinventemos a roda, e sua interface comum nos
inspira a criar APIs que suportem e se aproveitem de forma apropriada dos tipos de sequências existentes e futuras.

A maior parte da discussão deste capítulo se aplica às sequências em geral, desde a conhecida list até os tipos str e
bytes , adicionados no Python 3. Tópicos específicos sobre listas, tuplas, arrays e filas também foram incluídos, mas os
detalhes sobre strings Unicode e sequências de bytes são tratados no Capítulo 4. Além disso, a ideia aqui é falar sobre os
tipos de sequências prontas para usar. A criação de novos tipos de sequência é o tema do Capítulo 12.

Os principais tópicos cobertos neste capítulo são:

Compreensão de listas e os fundamentos das expressões geradoras.


O uso de tuplas como registros versus o uso de tuplas como listas imutáveis
Desempacotamento de sequências e padrões de sequências.
Lendo de fatias e escrevendo em fatias
Tipos especializados de sequências, tais como arrays e filas

2.1. Novidades neste capítulo


A atualização mais importante desse capítulo é a seção Seção 2.6, primeira abordagem das instruções match/case
introduzidas no Python 3.10.

As outras mudanças não são atualizações e sim aperfeiçoamentos da primeira edição:

Um novo diagrama e uma nova descrição do funcionamento interno das sequências, contrastando contêineres e
sequências planas.
Uma comparação entre list e tuple quanto ao desempenho e ao armazenamento.
Ressalvas sobre tuplas com elementos mutáveis, e como detectá-los se necessário.

Movi a discussão sobre tuplas nomeadas para a seção Seção 5.3 no Capítulo 5, onde elas são comparadas com
typing.NamedTuple e @dataclass .
Para abrir espaço para conteúdo novo mantendo o número de páginas dentro do razoável, a seção

✒️ NOTA "Managing Ordered Sequences with Bisect" ("Gerenciando sequências ordenadas com bisect") da
primeira edição agora é um artigo (https://fpy.li/bisect) (EN) no site que complementa o livro,
fluentpython.com (http://fluentpython.com).

2.2. Uma visão geral das sequências embutidas


A biblioteca padrão oferece uma boa seleção de tipos de sequências, implementadas em C:

Sequências contêiner
Podem armazenar itens de tipos diferentes, incluindo contêineres aninhados e objetos de qualquer tipo. Alguns
exemplos: list , tuple , e collections.deque .

Sequências planas
Armazenam itens de algum tipo simples, mas não outras coleções ou referências a objetos. Alguns exemplos: str ,
bytes , e array.array .

Uma sequência contêiner mantém referências para os objetos que contém, que podem ser de qualquer tipo, enquanto
uma sequência plana armazena o valor de seu conteúdo em seu próprio espaço de memória, e não como objetos Python
distintos. Veja a Figura 1.

Figura 1. Diagramas de memória simplificados mostrando uma tuple e um array , cada uma com três itens. As
células em cinza representam o cabeçalho de cada objeto Python na memória. A tuple tem um array de referências
para seus itens. Cada item é um objeto Python separado, possivelmente contendo também referências aninhadas a
outros objetos Python, como aquela lista de dois itens. Por outro lado, um array Python é um único objeto, contendo
um array da linguagem C com três números de ponto flutuante`.

Dessa forma, sequências planas são mais compactas, mas estão limitadas a manter valores primitivos como bytes e
números inteiros e de ponto flutuante.
Todo objeto Python na memória tem um cabeçalho com metadados. O objeto Python mais simples,
um float , tem um campo de valor e dois campos de metadados:

ob_refcnt : a contagem de referências ao objeto

ob_type : um ponteiro para o tipo do objeto


✒️ NOTA ob_fval : um double de C mantendo o valor do float

No Python 64-bits, cada um desses campos ocupa 8 bytes. Por isso um array de números de ponto
flutuante é muito mais compacto que uma tupla de números de ponto flutuante: o array é um único
objeto contendo apenas o valor dos números, enquanto a tupla consiste de vários objetos—a própria
tupla e cada objeto float que ela contém.

Outra forma de agrupar as sequências é por mutabilidade:

Sequências mutáveis
Por exemplo, list , bytearray , array.array e collections.deque .

Sequências imutáveis
Por exemplo, tuple , str , e bytes .

A Figura 2 ajuda a visualizar como as sequências mutáveis herdam todos os métodos das sequências imutáveis e
implementam vários métodos adicionais. Os tipos embutidos concretos de sequências na verdade não são subclasses
das classes base abstratas (ABCs) Sequence e MutableSequence , mas sim subclasses virtuais registradas com aquelas
ABCs—como veremos no Capítulo 13. Por serem subclasses virtuais, tuple e list passam nesses testes:

PYCON
>>> from collections import abc
>>> issubclass(tuple, abc.Sequence)
True
>>> issubclass(list, abc.MutableSequence)
True

Figura 2. Diagrama de classe UML simplificado para algumas classes de collections.abc (as superclasses estão à
esquerda; as setas de herança apontam das subclasses para as superclasses; nomes em itálico indicam classes e
métodos abstratos).

Lembre-se dessas características básicas: mutável versus imutável; contêiner versus plana. Elas ajudam a extrapolar o
que se sabe sobre um tipo de sequência para outros tipos.
O tipo mais fundamental de sequência é a lista: um contêiner mutável. Espero que você já esteja muito familiarizada
com listas, então vamos passar diretamente para a compreensão de listas, uma forma potente de criar listas que
algumas vezes é subutilizada por sua sintaxe parecer, a princípio, estranha. Dominar as compreensões de listas abre as
portas para expressões geradoras que—entre outros usos—podem produzir elementos para preencher sequências de
qualquer tipo. Ambas são temas da próxima seção.

2.3. Compreensões de listas e expressões geradoras


Um jeito rápido de criar uma sequência é usando uma compreensão de lista (se o alvo é uma list ) ou uma expressão
geradora (para outros tipos de sequências). Se você não usa essas formas sintáticas diariamente, aposto que está
perdendo oportunidades de escrever código mais legível e, muitas vezes, mais rápido também.

Se você duvida de minha alegação, sobre essas formas serem "mais legíveis", continue lendo. Vou tentar convencer
você.

👉 DICA Por comodidade, muitos programadores Python se referem a compreensões de listas como
listcomps, e a expressões geradoras como genexps. Usarei também esses dois termos.

2.3.1. Compreensões de lista e legibilidade


Aqui está um teste: qual dos dois você acha mais fácil de ler, o Exemplo 1 ou o Exemplo 2?

Exemplo 1. Cria uma lista de pontos de código Unicode a partir de uma string

PYCON
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
... codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]

Exemplo 2. Cria uma lista de pontos de código Unicode a partir de uma string, usando uma listcomp

PYCON
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]

Qualquer um que saiba um pouco de Python consegue ler o Exemplo 1. Entretanto, após aprender sobre as listcomps,
acho o Exemplo 2 mais legível, porque deixa sua intenção explícita.

Um loop for pode ser usado para muitas coisas diferentes: percorrer uma sequência para contar ou encontrar itens,
computar valores agregados (somas, médias), ou inúmeras outras tarefas. O código no Exemplo 1 está criando uma
lista. Uma listcomp, por outro lado, é mais clara. Seu objetivo é sempre criar uma nova lista.

Naturalmente, é possível abusar das compreensões de lista para escrever código verdadeiramente incompreensível. Já
vi código Python usando listcomps apenas para repetir um bloco de código por seus efeitos colaterais. Se você não vai
fazer alguma coisa com a lista criada, não deveria usar essa sintaxe. Além disso, tente manter o código curto. Se uma
compreensão ocupa mais de duas linhas, provavelmente seria melhor quebrá-la ou reescrevê-la como um bom e velho
loop for . Avalie qual o melhor caminho: em Python, como em português, não existem regras absolutas para se
escrever bem.
Dica de sintaxe
No código Python, quebras de linha são ignoradas dentro de pares de [] , {} , ou () . Então você
pode usar múltiplas linhas para criar listas, listcomps, tuplas, dicionários, etc., sem necessidade de
usar o marcador de continuação de linha \ , que não funciona se após o \ você acidentalmente
👉 DICA digitar um espaço. Outro detalhe, quando aqueles pares de delimitadores são usados para definir
um literal com uma série de itens separados por vírgulas, uma vírgula solta no final será ignorada.
Daí, por exemplo, quando se codifica uma lista a partir de um literal com múltiplas linhas, é de bom
tom deixar uma vírgula após o último item. Isso torna um pouco mais fácil ao próximo programador
acrescentar mais um item àquela lista, e reduz o ruído quando se lê os diffs.

Escopo local dentro de compreensões e expressões geradoras


No Python 3, compreensões de lista, expressões geradoras, e suas irmãs, as compreensões de set e de dict , tem
um escopo local para manter as variáveis criadas na condição for . Entretanto, variáveis atribuídas com o
"operador morsa" ("Walrus operator"), := , continuam acessíveis após aquelas compreensões ou expressões
retornarem—diferente das variáveis locais em uma função. A PEP 572—Assignment Expressions
(https://fpy.li/pep572) (EN) define o escopo do alvo de um := como a função à qual ele pertence, exceto se houver
uma declaração global ou nonlocal para aquele alvo.[2]

PYCON
>>> x = 'ABC'
>>> codes = [ord(x) for x in x]
>>> x (1)
'ABC'
>>> codes
[65, 66, 67]
>>> codes = [last := ord(c) for c in x]
>>> last (2)
67
>>> c (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

1. x não foi sobrescrito: continua vinculado a 'ABC' .

2. last permanece.
3. c desapareceu; ele só existiu dentro da listcomp.

Compreensões de lista criam listas a partir de sequências ou de qualquer outro tipo iterável, filtrando e transformando
os itens. As funções embutidas filter e map podem fazer o mesmo, mas perde-se alguma legibilidade, como veremos
a seguir.

2.3.2. Listcomps versus map e filter


Listcomps fazem tudo que as funções map e filter fazem, sem os malabarismos exigidos pela funcionalidade
limitada do lambda do Python.

Considere o Exemplo 3.

Exemplo 3. A mesma lista, criada por uma listcomp e por uma composição de map/filter
PYCON
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]

Eu acreditava que map e filter eram mais rápidas que as listcomps equivalentes, mas Alex Martelli assinalou que
não é o caso—pelo menos não nos exemplos acima. O script listcomp_speed.py (https://fpy.li/2-1) no repositório de código
do Python Fluente (https://fpy.li/code) é um teste de velocidade simples, comparando listcomp com filter/map .

Vou falar mais sobre map e filter no Capítulo 7. Vamos agora ver o uso de listcomps para computar produtos
cartesianos: uma lista contendo tuplas criadas a partir de todos os itens de duas ou mais listas.

2.3.3. Produtos cartesianos


Listcomps podem criar listas a partir do produto cartesiano de dois ou mais iteráveis. Os itens resultantes de um
produto cartesiano são tuplas criadas com os itens de cada iterável na entrada, e a lista resultante tem o tamanho igual
ao produto da multiplicação dos tamanhos dos iteráveis usados. Veja a Figura 3.

Figura 3. O produto cartesiano de 3 valores de cartas e 4 naipes é uma sequência de 12 parâmetros.

Por exemplo, imagine que você precisa produzir uma lista de camisetas disponíveis em duas cores e três tamanhos. O
Exemplo 4 mostra como produzir tal lista usando uma listcomp. O resultado tem seis itens.

Exemplo 4. Produto cartesiano usando uma compreensão de lista


PYCON
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes] (1)
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
('white', 'M'), ('white', 'L')]
>>> for color in colors: (2)
... for size in sizes:
... print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes (3)
... for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
('black', 'L'), ('white', 'L')]

1. Isso gera uma lista de tuplas ordenadas por cor, depois por tamanho.
2. Observe que a lista resultante é ordenada como se os loops for estivessem aninhados na mesma ordem que eles
aparecem na listcomp.
3. Para ter os itens ordenados por tamanho e então por cor, apenas rearranje as cláusulas for ; adicionar uma quebra
de linha listcomp torna mais fácil ver como o resultado será ordenado.

No Exemplo 1 (em Capítulo 1), usei a seguinte expressão para inicializar um baralho de cartas com uma lista contendo
52 cartas de todos os 13 valores possíveis para cada um dos quatro naipes, ordenada por naipe e então por valor:

PYTHON3
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

Listcomps são mágicos de um só truque: elas criam listas. Para gerar dados para outros tipos de sequências, uma
genexp é o caminho. A próxima seção é uma pequena incursão às genexps, no contexto de criação de sequências que
não são listas.

2.3.4. Expressões geradoras


Para inicializar tuplas, arrays e outros tipos de sequências, você também poderia começar de uma listcomp, mas uma
genexp (expressão geradora) economiza memória, pois ela produz itens um de cada vez usando o protocolo iterador,
em vez de criar uma lista inteira apenas para alimentar outro construtor.

As genexps usam a mesma sintaxe das listcomps, mas são delimitadas por parênteses em vez de colchetes.

O Exemplo 5 demonstra o uso básico de genexps para criar uma tupla e um array.

Exemplo 5. Inicializando uma tupla e um array a partir de uma expressão geradora


PYCON
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) (1)
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) (2)
array('I', [36, 162, 163, 165, 8364, 164])

1. Se a expressão geradora é o único argumento em uma chamada de função, não há necessidade de duplicar os
parênteses circundantes.
2. O construtor de array espera dois argumentos, então os parênteses em torno da expressão geradora são
obrigatórios. O primeiro argumento do construtor de array define o tipo de armazenamento usado para os
números no array, como veremos na seção Seção 2.10.1.

O Exemplo 6 usa uma genexp com um produto cartesiano para gerar uma relação de camisetas de duas cores em três
tamanhos. Diferente do Exemplo 4, aquela lista de camisetas com seis itens nunca é criada na memória: a expressão
geradora alimenta o loop for produzindo um item por vez. Se as duas listas usadas no produto cartesiano tivessem
mil itens cada uma, usar uma função geradora evitaria o custo de construir uma lista com um milhão de itens apenas
para passar ao loop for .

Exemplo 6. Produto cartesiano em uma expressão geradora

PYCON
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in (f'{c} {s}' for c in colors for s in sizes): (1)
... print(tshirt)
...
black S
black M
black L
white S
white M
white L

1. A expressão geradora produz um item por vez; uma lista com todas as seis variações de camisetas nunca aparece
neste exemplo.

O Capítulo 17 explica em detalhes o funcionamento de geradoras. A ideia aqui é apenas mostrar o


✒️ NOTA uso de expressões geradores para inicializar sequências diferentes de listas, ou produzir uma saída
que não precise ser mantida na memória.

Vamos agora estudar outra sequência fundamental do Python: a tupla.

2.4. Tuplas não são apenas listas imutáveis


Alguns textos introdutórios de Python apresentam as tuplas como "listas imutáveis", mas isso é subestimá-las. Tuplas
tem duas funções: elas podem ser usada como listas imutáveis e também como registros sem nomes de campos. Esse
uso algumas vezes é negligenciado, então vamos começar por ele.

2.4.1. Tuplas como registros


Tuplas podem conter registros: cada item na tupla contém os dados de um campo, e a posição do item indica seu
significado.
Se você pensar em uma tupla apenas como uma lista imutável, a quantidade e a ordem dos elementos pode ou não ter
alguma importância, dependendo do contexto. Mas quando usamos uma tupla como uma coleção de campos, o
número de itens em geral é fixo e sua ordem é sempre importante.

O Exemplo 7 mostras tuplas usadas como registros. Observe que, em todas as expressões, ordenar a tupla destruiria a
informação, pois o significado de cada campo é dado por sua posição na tupla.

Exemplo 7. Tuplas usadas como registros

PYCON
>>> lax_coordinates = (33.9425, -118.408056) (1)
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014) (2)
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), (3)
... ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids): (4)
... print('%s/%s' % passport) (5)
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids: (6)
... print(country)
...
USA
BRA
ESP

1. Latitude e longitude do Aeroporto Internacional de Los Angeles.


2. Dados sobre Tóquio: nome, ano, população (em milhares), crescimento populacional (%) e área (km²).
3. Uma lista de tuplas no formato (código_de_país, número_do_passaporte).
4. Iterando sobre a lista, passport é vinculado a cada tupla.
5. O operador de formatação % entende as tuplas e trata cada item como um campo separado.
6. O loop for sabe como recuperar separadamente os itens de uma tupla—isso é chamado "desempacotamento"
("unpacking"). Aqui não estamos interessados no segundo item, então o atribuímos a _ , uma variável descartável,
usada apenas para coletar valores que não serão usados.

Em geral, usar _ como variável descartável (dummy variable) é só uma convenção. É apenas um
nome de variável estranho mas válido. Entretanto, em uma instrução match/case , o _ é um
👉 DICA coringa que corresponde a qualquer valor, mas não está vinculado a um valor. Veja a seção Seção
2.6. E no console do Python, o resultado do comando anterior é atribuído a _ —a menos que o
resultado seja None .

Muitas vezes pensamos em registros como estruturas de dados com campos nomeados. O Capítulo 5 apresenta duas
formas de criar tuplas com campos nomeados.

Mas muitas vezes não é preciso se dar ao trabalho de criar uma classe apenas para nomear os campos, especialmente
se você aproveitar o desempacotamento e evitar o uso de índices para acessar os campos. No Exemplo 7, atribuímos
('Tokyo', 2003, 32_450, 0.66, 8014) a city, year, pop, chg, area em um único comando. E daí o operador
% atribuiu cada item da tupla passport para a posição correspondente da string de formato no argumento print .
Esses foram dois exemplos de desempacotamento de tuplas.
O termo "desempacotamento de tuplas" (tuple unpacking) é muito usado entre os pythonistas, mas
desempacotamento de iteráveis é mais preciso e está ganhando popularidade, como no título da PEP
✒️ NOTA 3132 — Extended Iterable Unpacking (Desempacotamento Estendido de Iteráveis) (https://fpy.li/2-2).

A seção Seção 2.5 fala muito mais sobre desempacotamento, não apenas de tuplas, mas também de
sequências e iteráveis em geral.

Agora vamos considerar o uso da classe tuple como uma variante imutável da classe list .

2.4.2. Tuplas como listas imutáveis


O interpretador Python e a biblioteca padrão fazem uso extensivo das tuplas como listas imutáveis, e você deveria
seguir o exemplo. Isso traz dois benefícios importantes:

Clareza
Quando você vê uma tuple no código, sabe que seu tamanho nunca mudará.

Desempenho
Uma tuple usa menos memória que uma list de mesmo tamanho, e permite ao Python realizar algumas
otimizações.

Entretanto, lembre-se que a imutabilidade de uma tuple só se aplica às referências ali contidas. Referências em um
tupla não podem ser apagadas ou substituídas. Mas se uma daquelas referências apontar para um objeto mutável, e
aquele objeto mudar, então o valor da tuple muda. O próximo trecho de código ilustra esse fato criando duas tuplas—
a e b — que inicialmente são iguais. A Figura 4 representa a disposição inicial da tupla b na memória.

Figura 4. O conteúdo em si da tupla é imutável, mas isso significa apenas que as referências mantidas pela tupla vão
sempre apontar para os mesmos objetos. Entretanto, se um dos objetos referenciados for mutável—uma lista, por
exemplo—seu conteúdo pode mudar.

Quando o último item em b muda, b e a se tornam diferentes:


PYCON
>>> a = (10, 'alpha', [1, 2])
>>> b = (10, 'alpha', [1, 2])
>>> a == b
True
>>> b[-1].append(99)
>>> a == b
False
>>> b
(10, 'alpha', [1, 2, 99])

Tuplas com itens mutáveis podem ser uma fonte de bugs. Se uma tupla contém qualquer item mutável, ela não pode
ser usada como chave em um dict ou como elemento em um set . O motivo será explicado em Seção 3.4.1.

Se você quiser determinar explicitamente se uma tupla (ou qualquer outro objeto) tem um valor fixo, pode usar a
função embutida hash para criar uma função fixed , assim:

PYCON
>>> def fixed(o):
... try:
... hash(o)
... except TypeError:
... return False
... return True
...
>>> tf = (10, 'alpha', (1, 2))
>>> tm = (10, 'alpha', [1, 2])
>>> fixed(tf)
True
>>> fixed(tm)
False

Vamos aprofundar essa questão em Seção 6.3.2.

Apesar dessa ressalva, as tuplas são frequentemente usadas como listas imutáveis. Elas oferecem algumas vantagens
de desempenho, explicadas por uma dos desenvolvedores principais do Python, Raymond Hettinger, em uma resposta
à questão "Are tuples more efficient than lists in Python?" (As tuplas são mais eficientes que as listas no Python?)
(https://fpy.li/2-3) no StackOverflow. Em resumo, Hettinger escreveu:

Para avaliar uma tupla literal, o compilador Python gera bytecode para uma constante tupla em uma operação; mas
para um literal lista, o bytecode gerado insere cada elemento como uma constante separada no stack de dados, e
então cria a lista.
Dada a tupla t , tuple(t) simplesmente devolve uma referência para a mesma t . Não há necessidade de cópia.
Por outro lado, dada uma lista l , o construtor list(l) precisa criar uma nova cópia de l .
Devido a seu tamanho fixo, uma instância de tuple tem alocado para si o espaço exato de memória que precisa.
Em contrapartida, instâncias de list tem alocadas para si memória adicional, para amortizar o custo de
acréscimos futuros.
As referências para os itens em uma tupla são armazenadas em um array na struct da tupla, enquanto uma lista
mantém um ponteiro para um array de referências armazenada em outro lugar. Essa indireção é necessária porque,
quando a lista cresce além do espaço alocado naquele momento, o Python precisa realocar o array de referências
para criar espaço. A indireção adicional torna o cache da CPU menos eficiente.

2.4.3. Comparando os métodos de tuplas e listas


Quando usamos uma tupla como uma variante imutável de list , é bom saber o quão similares são suas APIs. Como se
pode ver na Tabela 3, tuple suporta todos os métodos de list que não envolvem adicionar ou remover itens, com
uma exceção— tuple não possui o método __reversed__ . Entretanto, isso é só uma otimização;
reversed(my_tuple) funciona sem esse método.
Tabela 3. Métodos e atributos encontrados em list ou tuple (os métodos implementados por object foram
omitidos para economizar espaço)
list tuple

s.__add__(s2) ● ● s + s2—concatenação

s.__iadd__(s2) ● s += s2—concatenação no
mesmo lugar

s.append(e) ● Acrescenta um elemento


após o último

s.clear() ● Apaga todos os itens

s.__contains__(e) ● ● e in s

s.copy() ● Cópia rasa da lista

s.count(e) ● ● Conta as ocorrências de


um elemento

s.__delitem__(p) ● Remove o item na posição


p

s.extend(it) ● Acrescenta itens do


iterável it

s.__getitem__(p) ● ● s[p]—obtém o item na


posição p

s.__getnewargs__() ● Suporte a serialização


otimizada com pickle

s.index(e) ● ● Encontra a posição da


primeira ocorrência de e

s.insert(p, e) ● Insere elemento e antes


do item na posição p

s.__iter__() ● ● Obtém o iterador

s.__len__() ● ● len(s)—número de itens

s.__mul__(n) ● ● s * n—concatenação
repetida

s.__imul__(n) ● s *= n—concatenação
repetida no mesmo lugar

s.__rmul__(n) ● ● n * s—concatenação
repetida inversa[3]
list tuple

s.pop([p]) ● Remove e devolve o


último item ou o item na
posição opcional p

s.remove(e) ● Remove a primeira


ocorrência do elemento e,
por valor

s.reverse() ● Reverte, no lugar, a ordem


dos itens

s.__reversed__() ● Obtém iterador para


examinar itens, do último
para o primeiro

s.__setitem__(p, e) ● s[p] = e—coloca e na


posição p ,
sobrescrevendo o item
existente[4]

s.sort([key], ● Ordena os itens no lugar,


[reverse]) com os argumentos
nomeados opcionais key
e reverse

Vamos agora examinar um tópico importante para a programação Python idiomática: tuplas, listas e
desempacotamento iterável.

2.5. Desempacotando sequências e iteráveis


O desempacotamento é importante porque evita o uso de índices para extrair elementos de sequências, um processo
desnecessário e vulnerável a erros. Além disso, o desempacotamento funciona tendo qualquer objeto iterável como
fonte de dados—incluindo iteradores, que não suportam a notação de índice ( [] ). O único requisito é que o iterável
produza exatamente um item por variável na ponta de recebimento, a menos que você use um asterisco ( * ) para
capturar os itens em excesso, como explicado na seção Seção 2.5.1.

A forma mais visível de desempacotamento é a atribuição paralela; isto é, atribuir itens de um iterável a uma tupla de
variáveis, como vemos nesse exemplo:

PYCON
>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # unpacking
>>> latitude
33.9425
>>> longitude
-118.408056

Uma aplicação elegante de desempacotamento é permutar os valores de variáveis sem usar uma variável temporária:

PYCON
>>> b, a = a, b
Outro exemplo de desempacotamento é prefixar um argumento com * ao chamar uma função:

PYCON
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4)

O código acima mostra outro uso do desempacotamento: permitir que funções devolvam múltiplos valores de forma
conveniente para quem as chama. Em ainda outro exemplo, a função os.path.split() cria uma tupla (path,
last_part) a partir de um caminho do sistema de arquivos:

PYCON
>>> import os
>>> _, filename = os.path.split('/home/luciano/.ssh/id_rsa.pub')
>>> filename
'id_rsa.pub'

Outra forma de usar apenas alguns itens quando desempacotando é com a sintaxe * , que veremos a seguir.

2.5.1. Usando * para recolher itens em excesso


Definir parâmetros de função com *args para capturar argumentos arbitrários em excesso é um recurso clássico do
Python.

No Python 3, essa ideia foi estendida para se aplicar também à atribuição paralela:

PYCON
>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])

No contexto da atribuição paralela, o prefixo * pode ser aplicado a exatamente uma variável, mas pode aparecer em
qualquer posição:

PYCON
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)

2.5.2. Desempacotando com * em chamadas de função e sequências literais


A PEP 448—Additional Unpacking Generalizations (Generalizações adicionais de desempacotamento) (https://fpy.li/pep448)
(EN) introduziu uma sintaxe mais flexível para desempacotamento iterável, melhor resumida em "O que há de novo no
Python 3.5" (https://docs.python.org/pt-br/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations) (EN).

Em chamadas de função, podemos usar * múltiplas vezes:


PYCON
>>> def fun(a, b, c, d, *rest):
... return a, b, c, d, rest
...
>>> fun(*[1, 2], 3, *range(4, 7))
(1, 2, 3, 4, (5, 6))

O * pode também ser usado na definição de literais list , tuple , ou set , como visto nesses exemplos de "O que há
de novo no Python 3.5" (https://docs.python.org/pt-br/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations) (EN):

PYCON
>>> *range(4), 4
(0, 1, 2, 3, 4)
>>> [*range(4), 4]
[0, 1, 2, 3, 4]
>>> {*range(4), 4, *(5, 6, 7)}
{0, 1, 2, 3, 4, 5, 6, 7}

A PEP 448 introduziu uma nova sintaxe similar para ** , que veremos na seção Seção 3.2.2.

Por fim, outro importante aspecto do desempacotamento de tuplas: ele funciona com estruturas aninhadas.

2.5.3. Desempacotamento aninhado


O alvo de um desempacotamento pode usar aninhamento, por exemplo (a, b, (c, d)) . O Python fará a coisa certa
se o valor tiver a mesma estrutura aninhada. O Exemplo 8 mostra o desempacotamento aninhado em ação.

Exemplo 8. Desempacotando tuplas aninhadas para acessar a longitude

PYTHON3
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # (1)
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for name, _, _, (lat, lon) in metro_areas: # (2)
if lon <= 0: # (3)
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')

if __name__ == '__main__':
main()

1. Cada tupla contém um registro com quatro campos, o último deles um par de coordenadas.
2. Ao atribuir o último campo a uma tupla aninhada, desempacotamos as coordenadas.
3. O teste lon ⇐ 0: seleciona apenas cidades no hemisfério ocidental.

A saída do Exemplo 8 é:

TEXT
| latitude | longitude
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
São Paulo | -23.5478 | -46.6358
O alvo da atribuição de um desempacotamento pode também ser uma lista, mas bons casos de uso aqui são raros. Aqui
está o único que conheço: se você tem uma consulta de banco de dados que devolve um único registro (por exemplo, se
o código SQL tem a instrução LIMIT 1 ), daí é possível desempacotar e ao mesmo tempo se assegurar que há apenas
um resultado com o seguinte código:

PYCON
>>> [record] = query_returning_single_row()

Se o registro contiver apenas um campo, é possível obtê-lo diretamente, assim:

PYCON
>>> [[field]] = query_returning_single_row_with_single_field()

Ambos os exemplos acima podem ser escritos com tuplas, mas não esqueça da peculiaridade sintática, tuplas com um
único item devem ser escritas com uma vírgula final. Então o primeiro alvo seria (record,) e o segundo
((field,),) . Nos dois casos, esquecer aquela vírgula causa um bug silencioso.[5]

Agora vamos estudar pattern matching, que suporta maneiras ainda mais poderosas para desempacotar sequências.

2.6. Pattern matching com sequências


O novo recurso mais visível do Python 3.10 é o pattern matching (casamento de padrões) com a instrução match/case ,
proposta na PEP 634—Structural Pattern Matching: Specification (Casamento Estrutural de Padrões: Especificação)
(https://fpy.li/pep634) (EN).

Carol Willing, uma das desenvolvedoras principais do Python, escreveu uma excelente introdução
ao pattern matching na seção "Correspondência de padrão estrutural"
(https://docs.python.org/pt-br/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching)[6] em "O que há
✒️ NOTA de novo no Python 3.10" (https://docs.python.org/pt-br/3.10/whatsnew/3.10.html). Você pode querer ler
aquela revisão rápida. Neste livro, optei por dividir o tratamento da correspondência de padrões em
diferentes capítulos, dependendo dos tipos de padrão: Na seção Seção 3.3 e na Seção 5.8. E há um
exemplo mais longo na seção Seção 18.3.

Vamos ao primeiro exemplo do tratamento de sequências com match/case .

Imagine que você está construindo um robô que aceita comandos, enviados como sequências de palavras e números,
como BEEPER 440 3 . Após separar o comando em partes e analisar os números, você teria uma mensagem como
['BEEPER', 440, 3] . Então, você poderia usar um método assim para interpretar mensagens naquele formato:

Exemplo 9. Método de uma classe Robot imaginária

PYTHON3
def handle_command(self, message):
match message: # (1)
case ['BEEPER', frequency, times]: # (2)
self.beep(times, frequency)
case ['NECK', angle]: # (3)
self.rotate_neck(angle)
case ['LED', ident, intensity]: # (4)
self.leds[ident].set_brightness(ident, intensity)
case ['LED', ident, red, green, blue]: # (5)
self.leds[ident].set_color(ident, red, green, blue)
case _: # (6)
raise InvalidCommand(message)
1. A expressão após a palavra-chave match é o sujeito (subject). O sujeito contém os dados que o Python vai comparar
aos padrões em cada instrução case .
2. Esse padrão casa com qualquer sujeito que seja uma sequência de três itens. O primeiro item deve ser a string
BEEPER . O segundo e o terceiro itens podem ser qualquer coisa, e serão vinculados às variáveis frequency e
times , nessa ordem.

3. Isso casa com qualquer sujeito com dois itens, se o primeiro for 'NECK' .

4. Isso vai casar com uma sujeito de três itens começando com LED . Se o número de itens não for correspondente, o
Python segue para o próximo case .
5. Outro padrão de sequência começando com 'LED' , agora com cinco itens—incluindo a constante 'LED' .

6. Esse é o case default. Vai casar com qualquer sujeito que não tenha sido capturado por um dos padrões
precedentes. A variável _ é especial, como logo veremos.

Olhando superficialmente, match/case se parece instrução switch/case da linguagem C—mas isso é só uma
pequena parte da sua funcionalidade.[7] Uma melhoria fundamental do match sobre o switch é a desestruturação—
uma forma mais avançada de desempacotamento. Desestruturação é uma palavra nova no vocabulário do Python, mas
é usada com frequência na documentação de linguagens que suportam o pattern matching—como Scala e Elixir.

Como um primeiro exemplo de desestruturação, o Exemplo 10 mostra parte do Exemplo 8 reescrito com match/case .

Exemplo 10. Desestruturando tuplas aninhadas—requer Python ≥ 3.10

PYTHON3
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for record in metro_areas:
match record: # (1)
case [name, _, _, (lat, lon)] if lon <= 0: # (2)
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')

1. O sujeito desse match é record —isto é, cada uma das tuplas em metro_areas .

2. Uma instrução case tem duas partes: um padrão e uma guarda opcional, com a palavra-chave if .

Em geral, um padrão de sequência casa com o sujeito se estas três condições forem verdadeiras:

1. O sujeito é uma sequência, e


2. O sujeito e o padrão tem o mesmo número de itens, e
3. Cada item correspondente casa, incluindo os itens aninhados.

Por exemplo, o padrão [name, _, _, (lat, lon)] no Exemplo 10 casa com uma sequência de quatro itens, e o último
item tem que ser uma sequência de dois itens.
Padrões de sequência podem ser escritos como tuplas e listas, mas a sintaxe usada não faz diferença: em um padrão de
sequência, colchetes e parênteses tem o mesmo significado. Escrevi o padrão como uma lista com uma tupla aninhada
de dois itens para evitar a repetição de colchetes ou parênteses no Exemplo 10.

Um padrão de sequência pode casar com instâncias da maioria das subclasses reais ou virtuais de
collections.abc.Sequence , com a exceção de str , bytes , e bytearray .

Instâncias de str , bytes , e bytearray não são tratadas como sequências no contexto de um
match/case . Um sujeito de match de um desses tipos é tratado como um valor "atômico"—assim
como o inteiro 987 é tratado como um único valor, e não como uma sequência de dígitos. Tratar
aqueles três tipos como sequências poderia causar bugs devido a casamentos não intencionais. Se
você quer usar um objeto daqueles tipos como um sujeito sequência, converta-o na instrução
match . Por exemplo, veja tuple(phone) no trecho abaixo, que poderia ser usado para separar
números de telefone por regiões do mundo com base no prefixo DDI:
⚠️ AVISO PY
match tuple(phone):
case ['1', *rest]: # North America and Caribbean
...
case ['2', *rest]: # Africa and some territories
...
case ['3' | '4', *rest]: # Europe
...

Na biblioteca padrão, os seguintes tipos são compatíveis com padrões de sequência:

list memoryview array.array


tuple range collections.deque

Ao contrário do desempacotamento, padrões não desestruturam iteráveis que não sejam sequências (tal como os
iteradores).

O símbolo _ é especial nos padrões: ele casa com qualquer item naquela posição, mas nunca é vinculado ao valor
daquele item. O valor é descartado. Além disso, o _ é a única variável que pode aparecer mais de uma vez em um
padrão.

Você pode vincular qualquer parte de um padrão a uma variável usando a palavra-chave as :

PYTHON3
case [name, _, _, (lat, lon) as coord]:

Dado o sujeito ['Shanghai', 'CN', 24.9, (31.1, 121.3)] , o padrão anterior vai casar e atribuir valores às
seguintes variáveis:

Variável Valor atribuído

name 'Shanghai'

lat 31.1

lon 121.3
Variável Valor atribuído

coord (31.1, 121.3)

Podemos tornar os padrões mais específicos, incluindo informação de tipo. Por exemplo, o seguinte padrão casa com a
mesma estrutura de sequência aninhada do exemplo anterior, mas o primeiro item deve ser uma instância de str , e
ambos os itens da tupla devem ser instâncias de float :

PYTHON3
case [str(name), _, _, (float(lat), float(lon))]:

As expressões str(name) e float(lat) se parecem com chamadas a construtores, que usaríamos


para converter name e lat para str e float . Mas no contexto de um padrão, aquela sintaxe faz
uma verificação de tipo durante a execução do programa: o padrão acima vai casar com uma

👉 DICA sequência de quatro itens, na qual o item 0 deve ser uma str e o item 3 deve ser um par de
números de ponto flutuante. Além disso, a str no item 0 será vinculada à variável name e os
números no item 3 serão vinculados a lat e lon , respectivamente. Assim, apesar de imitar a
sintaxe de uma chamada de construtor, o significado de str(name) é totalmente diferente no
contexto de um padrão. O uso de classes arbitrárias em padrões será tratado na seção Seção 5.8.

Por outro lado, se queremos casar qualquer sujeito sequência começando com uma str e terminando com uma
sequência aninhada com dois números de ponto flutuante, podemos escrever:

PYTHON3
case [str(name), *_, (float(lat), float(lon))]:

O *_ casa com qualquer número de itens, sem vinculá-los a uma variável. Usar *extra em vez de *_ vincularia os
itens a extra como uma list com 0 ou mais itens.

A instrução de guarda opcional começando com if só é avaliada se o padrão casar, e pode se referir a variáveis
vinculadas no padrão, como no Exemplo 10:

PYTHON3
match record:
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')

O bloco aninhado com o comando print só será executado se o padrão casar e a expressão guarda for verdadeira.

A desestruturação com padrões é tão expressiva que, algumas vezes, um match com um único

👉 DICA case pode tornar o código mais simples. Guido van Rossum tem uma coleção de exemplos de
case/match , incluindo um que ele chamou de "A very deep iterable and type match with
extraction" (Um match de iterável e tipo muito profundo, com extração) (https://fpy.li/2-10) (EN).

O Exemplo 10 não melhora o Exemplo 8. É apenas um exemplo para contrastar duas formas de fazer a mesma coisa. O
próximo exemplo mostra como o pattern matching contribui para a criação de código claro, conciso e eficaz.

2.6.1. Casando padrões de sequência em um interpretador


Peter Norvig, da Universidade de Stanford, escreveu o lis.py (https://fpy.li/2-11): um interpretador de um subconjunto do
dialeto Scheme da linguagem de programação Lisp, em 132 belas linhas de código Python legível. Peguei o código fonte
de Norvig (publicado sob a licença MIT) e o atualizei para o Python 3.10, para exemplificar o pattern matching. Nessa
seção, vamos comparar uma parte fundamental do código de Norvig—que usa if/elif e desempacotamento—com
uma nova versão usando match/case .
As duas funções principais do lis.py são parse e evaluate .[8] O parser (analisador sintático) recebe as expressões
entre parênteses do Scheme e devolve listas Python. Aqui estão dois exemplos:

PYTHON
>>> parse('(gcd 18 45)')
['gcd', 18, 45]
>>> parse('''
... (define double
... (lambda (n)
... (* n 2)))
... ''')
['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]]

O avaliador recebe listas como essas e as executa. O primeiro exemplo está chamando uma função gcd com 18 e 45
como argumentos. Quando executada, ela computa o maior divisor comum (gcd são as iniciais do termo em inglês,
_greatest common divisor) dos argumentos (que é 9). O segundo exemplo está definindo uma função chamada double
com um parâmetro n . O corpo da função é a expressão (* n 2) . O resultado da chamada a uma função em Scheme é
o valor da última expressão no corpo da função chamada.

Nosso foco aqui é a desestruturação de sequências, então não vou explicar as ações do avaliador. Veja a seção Seção
18.3 para aprender mais sobre o funcionamento do lis.py.

O Exemplo 11 mostra o avaliador de Norvig com algumas pequenas modificações, e abreviado para mostrar apenas os
padrões de sequência.

Exemplo 11. Casando padrões sem match/case

PYTHON
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
if isinstance(exp, Symbol): # variable reference
return env[exp]
# ... lines omitted
elif exp[0] == 'quote': # (quote exp)
(_, x) = exp
return x
elif exp[0] == 'if': # (if test conseq alt)
(_, test, consequence, alternative) = exp
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
elif exp[0] == 'lambda': # (lambda (parm…) body…)
(_, parms, *body) = exp
return Procedure(parms, body, env)
elif exp[0] == 'define':
(_, name, value_exp) = exp
env[name] = evaluate(value_exp, env)
# ... more lines omitted

Observe como cada instrução elif verifica o primeiro item da lista, e então desempacota a lista, ignorando o primeiro
item. O uso extensivo do desempacotamento sugere que Norvig é um fã do pattern matching, mas ele originalmente
escreveu aquele código em Python 2 (apesar de agora ele funcionar com qualquer Python 3)

Usando match/case em Python ≥ 3.10, podemos refatorar evaluate , como mostrado no Exemplo 12.
Exemplo 12. Pattern matching com match/case —requer Python ≥ 3.10

PYTHON3
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
match exp:
# ... lines omitted
case ['quote', x]: # (1)
return x
case ['if', test, consequence, alternative]: # (2)
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
case ['lambda', [*parms], *body] if body: # (3)
return Procedure(parms, body, env)
case ['define', Symbol() as name, value_exp]: # (4)
env[name] = evaluate(value_exp, env)
# ... more lines omitted
case _: # (5)
raise SyntaxError(lispstr(exp))

1. Casa se o sujeito for uma sequência de dois itens começando com 'quote' .

2. Casa se o sujeito for uma sequência de quatro itens começando com 'if' .

3. Casa se o sujeito for uma sequência com três ou mais itens começando com 'lambda' . A guarda assegura que
body não esteja vazio.

4. Casa de o sujeito for uma sequência de três itens começando com 'define' , seguido de uma instância de Symbol .

5. é uma boa prática ter um case para capturar todo o resto. Neste exemplo, se exp não casar com nenhum dos
padrões, a expressão está mal-formada, então gera um SyntaxError .

Sem o último case , para pegar tudo que tiver passado pelos anteriores, todo o bloco match não faz nada quando o
sujeito não casa com algum case —e isso pode ser uma falha silenciosa.

Norvig deliberadamente evitou a checagem e o tratamento de erros em lis.py, para manter o código fácil de entender.
Com pattern matching, podemos acrescentar mais verificações e ainda manter o programa legível. Por exemplo, no
padrão 'define' , o código original não se assegura que name é uma instância de Symbol —isso exigiria um bloco
if , uma chamada a isinstance , e mais código. O Exemplo 12 é mais curto e mais seguro que o Exemplo 11.

Padrões alternativos para lambda


Essa é a sintaxe de lambda no Scheme, usando a convenção sintática onde o sufixo … significa que o elemento pode
aparecer zero ou mais vezes:

LISP
(lambda (parms…) body1 body2…)

Um padrão simples para o case de 'lambda' seria esse:

PYTHON3
case ['lambda', parms, *body] if body:

Entretanto, isso casa com qualquer valor na posição parms , incluindo o primeiro x nesse sujeito inválido:

PYTHON3
['lambda', 'x', ['*', 'x', 2]]
A lista aninhada após a palavra-chave lambda do Scheme contém os nomes do parâmetros formais da função, e deve
ser uma lista mesmo que contenha apenas um elemento. Ela pode também ser uma lista vazia, se função não receber
parâmetros—como a random.random() do Python.

No Exemplo 12, tornei o padrão de 'lambda' mais seguro usando um padrão de sequência aninhado:

PYTHON3
case ['lambda', [*parms], *body] if body:
return Procedure(parms, body, env)

Em um padrão de sequência, o * pode aparecer apenas uma vez por sequência. Aqui temos duas sequências: a
externa e a interna.

Acrescentando os caracteres [*] em torno de parms fez o padrão mais parecido com a sintaxe do Scheme da qual ele
trata, e nos deu uma verificação estrutural adicional.

Sintaxe abreviada para definição de função


O Scheme tem uma sintaxe alternativa de define , para criar uma função nomeada sem usar um lambda aninhado.
Tal sintaxe funciona assim:

LISP
(define (name parm…) body1 body2…)

A palavra-chave define é seguida por uma lista com o name da nova função e zero ou mais nomes de parâmetros.
Após a lista vem o corpo da função, com uma ou mais expressões.

Acrescentar essas duas linhas ao match cuida da implementação:

PY
case ['define', [Symbol() as name, *parms], *body] if body:
env[name] = Procedure(parms, body, env)

Eu colocaria esse case após o case da outra forma de define no Exemplo 12. A ordem desses cases de define é
irrelevante nesse exemplo, pois nenhum sujeito pode casar com esses dois padrões: o segundo elemento deve ser um
Symbol na forma original de define , mas deve ser uma sequência começando com um Symbol no atalho de define
para definição de função.

Agora pense em quanto trabalho teríamos para adicionar o suporte a essa segunda sintaxe de define sem a ajuda do
pattern matching no Exemplo 11. A instrução match faz muito mais que o switch das linguagens similares ao C.

O pattern matching é um exemplo de programação declarativa: o código descreve "o que" você quer casar, em vez de
"como" casar. A forma do código segue a forma dos dados, como ilustra a Tabela 4.

Tabela 4. Algumas formas sintáticas do Scheme e os padrões de case para tratá-las

Sintaxe do Scheme Padrão de sequência

(quote exp) ['quote', exp]

(if test conseq alt) ['if', test, conseq, alt]

(lambda (parms…) body1 body2…) ['lambda', [*parms], *body] if body

(define name exp) ['define', Symbol() as name, exp]


Sintaxe do Scheme Padrão de sequência

(define (name parms…) body1 body2…) ['define', [Symbol() as name, *parms], *body] if
body

Espero que a refatoração do evaluate de Norvig com pattern matching tenha convencido você que match/case pode
tornar seu código mais legível e mais seguro.

Veremos mais do lis.py na seção Seção 18.3, quando vamos revisar o exemplo completo de

✒️ NOTA match/case em evaluate . Se você quiser aprender mais sobre o lys.py de Norvig, leia seu
maravilhoso post "(How to Write a (Lisp) Interpreter (in Python))" (Como Escrever um Interpretador
(Lisp) em (Python)) (https://fpy.li/2-12).

Isso conclui nossa primeira passagem por desempacotamento, desestruturação e pattern matching com sequências.
Vamos tratar de outros tipos de padrões mais adiante, em outros capítulos.

Todo programador Python sabe que sequências podem ser fatiadas usando a sintaxe s[a:b] . Vamos agora examinar
alguns fatos menos conhecidos sobre fatiamento.

2.7. Fatiamento
Um recurso comum a list , tuple , str , e a todos os tipos de sequência em Python, é o suporte a operações de
fatiamento, que são mais potentes do que a maioria das pessoas percebe.

Nesta seção descrevemos o uso dessas formas avançadas de fatiamento. Sua implementação em uma classe definida
pelo usuário será tratada no Capítulo 12, mantendo nossa filosofia de tratar de classes prontas para usar nessa parte do
livro, e da criação de novas classes na [classes_protocols_part].

2.7.1. Por que fatias e faixas excluem o último item?


A convenção pythônica de excluir o último item em fatias e faixas funciona bem com a indexação iniciada no zero
usada no Python, no C e em muitas outras linguagens. Algumas características convenientes da convenção são:

É fácil ver o tamanho da fatia ou da faixa quando apenas a posição final é dada: tanto range(3) quanto
my_list[:3] produzem três itens.

É fácil calcular o tamanho de uma fatia ou de uma faixa quando o início e o fim são dados: basta subtrair fim-
início .

É fácil cortar uma sequência em duas partes em qualquer índice x , sem sobreposição: simplesmente escreva
my_list[:x] e my_list[x:] . Por exemplo:

PYCON
>>> l = [10, 20, 30, 40, 50, 60]
>>> l[:2] # split at 2
[10, 20]
>>> l[2:]
[30, 40, 50, 60]
>>> l[:3] # split at 3
[10, 20, 30]
>>> l[3:]
[40, 50, 60]

Os melhores argumentos a favor desta convenção foram escritos pelo cientista da computação holandês Edsger W.
Dijkstra (veja a última referência na seção Seção 2.12).
Agora vamos olhar mais de perto a forma como o Python interpreta a notação de fatiamento.

2.7.2. Objetos fatia


Isso não é segredo, mas vale a pena repetir, só para ter certeza: s[a:b:c] pode ser usado para especificar um passo ou
salto c , fazendo com que a fatia resultante pule itens. O passo pode ser também negativo, devolvendo os itens em
ordem inversa. Três exemplos esclarecem a questão:

PYCON
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

Vimos outro exemplo no capítulo Capítulo 1, quando usamos deck[12::13] para obter todos os ases de uma baralho
não embaralhado:

PYCON
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

A notação a:b:c só é válida entre [] quando usada como operador de indexação ou de subscrição (subscript), e
produz um objeto fatia (slice object): slice(a, b, c) . Como veremos na seção Seção 12.5.1, para avaliar a expressão
seq[start:stop:step] , o Python chama seq.__getitem__(slice(start, stop, step)) . Mesmo se você não for
implementar seus próprios tipos de sequência, saber dos objetos fatia é útil, porque eles permitem que você atribua
nomes às fatias, da mesma forma que planilhas permitem dar nomes a faixas de células.

Suponha que você precise analisar um arquivo de dados como a fatura mostrada na Exemplo 13. Em vez de encher seu
código de fatias explícitas fixas, você pode nomeá-las. Veja como isso torna legível o loop for no final do exemplo.

Exemplo 13. Itens de um arquivo tabular de fatura

PYCON
>>> invoice = """
... 0.....6.................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... """
>>> SKU = slice(0, 6)
>>> DESCRIPTION = slice(6, 40)
>>> UNIT_PRICE = slice(40, 52)
>>> QUANTITY = slice(52, 55)
>>> ITEM_TOTAL = slice(55, None)
>>> line_items = invoice.split('\n')[2:]
>>> for item in line_items:
... print(item[UNIT_PRICE], item[DESCRIPTION])
...
$17.50 Pimoroni PiBrella
$4.95 6mm Tactile Switch x20
$28.00 Panavise Jr. - PV-201
$34.95 PiTFT Mini Kit 320x240
Voltaremos aos objetos slice quando formos discutir a criação de suas próprias coleções, na seção Seção 12.5. Enquanto
isso, do ponto de vista do usuário, o fatiamento tem recursos adicionais, tais como fatias multidimensionais e a notação
de reticências ( ... ). Siga comigo.

2.7.3. Fatiamento multidimensional e reticências


O operador []pode também receber múltiplos índices ou fatias separadas por vírgulas. Os métodos especiais
__getitem__ e __setitem__ , que tratam o operador [] , apenas recebem os índices em a[i, j] como uma tupla.
Em outras palavras, para avaliar a[i, j] , o Python chama a.__getitem__((i, j)) .

Isso é usado, por exemplo, no pacote externo NumPy, onde itens de uma numpy.ndarray bi-dimensional podem ser
recuperados usando a sintaxe a[i, j] , e uma fatia bi-dimensional é obtida com uma expressão como a[m:n, k:l] .
O Exemplo 22, abaixo nesse mesmo capítulo, mostra o uso dessa notação.

Exceto por memoryview , os tipos embutidos de sequência do Python são uni-dimensionais, então aceitam apenas um
índice ou fatia, e não uma tupla de índices ou fatias.[9]

As reticências—escritas como três pontos finais ( ... ) e não como … (Unicode U+2026)—são reconhecidas como um
símbolo pelo parser do Python. Esse símbolo é um apelido para o objeto Ellipsis , a única instância da classe
ellipsis .[10] Dessa forma, ele pode ser passado como argumento para funções e como parte da especificação de uma
fatia, como em f(a, ..., z) ou a[i:...] . O NumPy usa ... como atalho ao fatiar arrays com muitas dimensões;
por exemplo, se x é um array com quatro dimensões, x[i, ...] é um atalho para x[i, :, :, :,] . Veja "NumPy
quickstart" (https://fpy.li/2-13) (EN) para saber mais sobre isso.

No momento em que escrevo isso, desconheço usos de Ellipsis ou de índices multidimensionais na biblioteca
padrão do Python. Se você souber de algum, me avise. Esses recursos sintáticos existem para suportar tipos definidos
pelo usuário ou extensões como o NumPy.

Fatias não são úteis apenas para extrair informações de sequências; elas podem também ser usadas para modificar
sequências mutáveis no lugar—isto é, sem precisar reconstruí-las do zero.

2.7.4. Atribuindo a fatias


Sequências mutáveis podem ser transplantadas, extirpadas e, de forma geral, modificadas no lugar com o uso da
notação de fatias no lado esquerdo de um comando de atribuição ou como alvo de um comando del . Os próximos
exemplos dão uma ideia do poder dessa notação:

PYCON
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100 (1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]
1. Quando o alvo de uma atribuição é uma fatia, o lado direito deve ser um objeto iterável, mesmo que tenha apenas
um item.

Todo programador sabe que a concatenação é uma operação frequente com sequências. Tutoriais introdutórios de
Python explicam o uso de + e * para tal propósito, mas há detalhes sutis em seu funcionamento, como veremos a
seguir.

2.8. Usando + e * com sequências


Programadores Python esperam que sequências suportem + e * . Em geral, os dois operandos de + devem ser
sequências do mesmo tipo, e nenhum deles é modificado, uma nova sequência daquele mesmo tipo é criada como
resultado da concatenação.

Para concatenar múltiplas cópias da mesma sequência basta multiplicá-la por um inteiro. E da mesma forma, uma
nova sequência é criada:

PYCON
>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'

Tanto + quanto * sempre criam um novo objetos, e nunca modificam seus operandos.

Tenha cuidado com expressões como a * n quando a é uma sequência contendo itens mutáveis,

⚠️ AVISO pois o resultado pode ser surpreendente. Por exemplo, tentar inicializar uma lista de listas como
my_list = [[]] * 3 vai resultar em uma lista com três referências para a mesma lista interna, que
provavelmente não é o quê você quer.

A próxima seção fala das armadilhas ao se tentar usar * para inicializar uma lista de listas.

2.8.1. Criando uma lista de listas


Algumas vezes precisamos inicializar uma lista com um certo número de listas aninhadas—para, por exemplo,
distribuir estudantes em uma lista de equipes, ou para representar casas no tabuleiro de um jogo. A melhor forma de
fazer isso é com uma compreensão de lista, como no Exemplo 14.

Exemplo 14. Uma lista com três listas de tamanho 3 pode representar um tabuleiro de jogo da velha

PYCON
>>> board = [['_'] * 3 for i in range(3)] (1)
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' (2)
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

1. Cria uma lista de três listas, cada uma com três itens. Inspeciona a estrutura criada.
2. Coloca um "X" na linha 1, coluna 2, e verifica o resultado.

Um atalho tentador mas errado seria fazer algo como o Exemplo 15.

Exemplo 15. Uma lista com três referências para a mesma lista é inútil
PYCON
>>> weird_board = [['_'] * 3] * 3 (1)
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' (2)
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

1. A lista externa é feita de três referências para a mesma lista interna. Enquanto ela não é modificada, tudo parece
correr bem.
2. Colocar um "O" na linha 1, coluna 2, revela que todas as linhas são apelidos do mesmo objeto.

O problema com o Exemplo 15 é que ele se comporta, essencialmente, como o código abaixo:

PYTHON3
row = ['_'] * 3
board = []
for i in range(3):
board.append(row) (1)

1. A mesma row é anexada três vezes ao board .

Por outro lado, a compreensão de lista no Exemplo 14 equivale ao seguinte código:

PYTHON3
>>> board = []
>>> for i in range(3):
... row = ['_'] * 3 # (1)
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board # (2)
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]

1. Cada iteração cria uma nova row e a acrescenta ao board .

2. Como esperado, apenas a linha 2 é modificada.

👉 DICA Se o problema ou a solução mostrados nessa seção não estão claros para você, não se preocupe. O
Capítulo 6 foi escrito para esclarecer a mecânica e os perigos das referências e dos objetos mutáveis.

Até aqui discutimos o uso dos operadores simples + e * com sequências, mas existem também os operadores += e
*= , que produzem resultados muito diferentes, dependendo da mutabilidade da sequência alvo. A próxima seção
explica como eles funcionam.

2.8.2. Atribuição aumentada com sequências


Os operadores de atribuição aumentada += e *=` se comportam de formas muito diferentes, dependendo do
primeiro operando. Para simplificar a discussão, vamos primeiro nos concentrar na adição aumentada ( += ), mas os
conceitos se aplicam a *= e a outros operadores de atribuição aumentada.

O método especial que faz += funcionar é __iadd__ (significando "in-place addition", adição no mesmo lugar).

Entretanto, se __iadd__ não estiver implementado, o Python chama __add__ como fallback. Considere essa
expressão simples:
PYCON
>>> a += b

Se implementar __iadd__ , esse método será chamado. No caso de sequências mutáveis (por exemplo, list ,
a
bytearray , array.array ), a será modificada no lugar (isto é, o efeito ser similar a a.extend(b) ). Porém, quando
a não implementa __iadd__ , a expressão a += b tem o mesmo efeito de a = a + b : a expressão a + b é avaliada
antes, produzindo um novo objeto, que então é vinculado a a . Em outras palavras, a identidade do objeto vinculado a
a pode ou não mudar, dependendo da disponibilidade de __iadd__ .

Em geral, para sequências mutáveis, é razoável supor que __iadd__ está implementado e que += acontece no mesmo
lugar. Para sequências imutáveis, obviamente não há forma disso acontecer.

Isso que acabei de escrever sobre += também se aplica a *= , que é implementado via __imul__ . Os métodos
especiais __iadd__ e __imul__ são tratados no Capítulo 16. Aqui está uma demonstração de *= com uma sequência
mutável e depois com uma sequência imutável:

PYCON
>>> l = [1, 2, 3]
>>> id(l)
4311953800 (1)
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800 (2)
>>> t = (1, 2, 3)
>>> id(t)
4312681568 (3)
>>> t *= 2
>>> id(t)
4301348296 (4)

1. O ID da lista inicial.
2. Após a multiplicação, a lista é o mesmo objeto, com novos itens anexados.
3. O ID da tupla inicial.
4. Após a multiplicação, uma nova tupla foi criada.

A concatenação repetida de sequências imutáveis é ineficiente, pois ao invés de apenas acrescentar novos itens, o
interpretador tem que copiar toda a sequência alvo para criar um novo objeto com os novos itens concatenados.[11]

Vimos casos de uso comuns para += . A próxima seção mostra um caso lateral intrigante, que realça o real significado
de "imutável" no contexto das tuplas.

2.8.3. Um quebra-cabeça com a atribuição +=


Tente responder sem usar o console: qual o resultado da avaliação das duas expressões no Exemplo 16?[12]

Exemplo 16. Um enigma

PYCON
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

O que acontece a seguir? Escolha a melhor alternativa:

A. t se torna (1, 2, [30, 40, 50, 60]) .


B. É gerado um TypeError com a mensagem 'tuple' object does not support item assignment (o objeto tupla
não suporta atribuição de itens).
C. Nenhuma das alternativas acima..
D. Ambas as alternativas, A e B.
Quando vi isso, tinha certeza que a resposta era B, mas, na verdade é D, "Ambas as alternativas, A e B"! O Exemplo 17 é
a saída real em um console rodando Python 3.10.[13]

Exemplo 17. O resultado inesperado: o item t2 é modificado e uma exceção é gerada

PYCON
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

O Online Python Tutor (https://fpy.li/2-14) (EN) é uma ferramenta online fantástica para visualizar em detalhes o
funcionamento do Python. A Figura 5 é uma composição de duas capturas de tela, mostrando os estados inicial e final
da tupla t do Exemplo 17.

Figura 5. Estados inicial e final do enigma da atribuição de tuplas (diagrama gerado pelo Online Python Tutor).

Se olharmos o bytecode gerado pelo Python para a expressão s[a] += b (Exemplo 18), fica claro como isso acontece.

Exemplo 18. Bytecode para a expressão s[a] += b


PYCON
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
3 LOAD_NAME 1 (a)
6 DUP_TOP_TWO
7 BINARY_SUBSCR (1)
8 LOAD_NAME 2 (b)
11 INPLACE_ADD (2)
12 ROT_THREE
13 STORE_SUBSCR (3)
14 LOAD_CONST 0 (None)
17 RETURN_VALUE

1. Coloca o valor de s[a] no TOS (Top Of Stack, topo da pilha de execução_).


2. Executa TOS += b . Isso é bem sucedido se TOS se refere a um objeto mutável (no Exemplo 17 é uma lista).
3. Atribui s[a] = TOS . Isso falha se s é imutável (a tupla t no Exemplo 17).

Esse exemplo é um caso raro—em meus 20 anos usando Python, nunca vi esse comportamento estranho estragar o dia
de alguém.

Há três lições para tirar daqui:

Evite colocar objetos mutáveis em tuplas.


A atribuição aumentada não é uma operação atômica—acabamos de vê-la gerar uma exceção após executar parte
de seu trabalho.
Inspecionar o bytecode do Python não é muito difícil, e pode ajudar a ver o que está acontecendo por debaixo dos
panos.

Após testemunharmos as sutilezas do uso de + e * para concatenação, podemos mudar de assunto e tratar de outra
operação essencial com sequências: ordenação.

2.9. list.sort versus a função embutida sorted


O método list.sort ordena uma lista no mesmo lugar—isto é, sem criar uma cópia. Ele devolve None para nos
lembrar que muda a própria instância e não cria uma nova lista. Essa é uma convenção importante da API do Python:
funções e métodos que mudam um objeto no mesmo lugar deve devolver None , para deixar claro a quem chamou que
o receptor[14] foi modificado, e que nenhum objeto novo foi criado. Um comportamento similar pode ser observado,
por exemplo, na função random.shuffle(s) , que devolve None após embaralhar os itens de uma sequência mutável
in-place (no lugar), isto é, mudando a posição dos itens dentro da própria sequência.

A convenção de devolver None para sinalizar mudanças no mesmo lugar tem uma desvantagem:
não podemos cascatear chamadas a esses métodos. Em contraste, métodos que devolvem novos
✒️ NOTA objetos (todos os métodos de str , por exemplo) podem ser cascateados no estilo de uma interface
fluente. Veja o artigo "Fluent interface" (https://fpy.li/2-15) (EN) da Wikipedia em inglês para uma
descrição mais detalhada deste tópico.

A função embutida sorted , por outro lado, cria e devolve uma nova lista. Ela aceita qualquer objeto iterável como um
argumento, incluindo sequências imutáveis e geradores (veja o Capítulo 17). Independente do tipo do iterável passado
a sorted , ela sempre cria e devolve uma nova lista.

Tanto list.sort quanto sorted podem receber dois argumentos de palavra-chave opcionais:
reverse
Se True , os itens são devolvidos em ordem decrescente (isto é, invertendo a comparação dos itens). O default é
False .

key

Uma função com um argumento que será aplicada a cada item, para produzir sua chave de ordenação. Por exemplo,
ao ordenar uma lista de strings, key=str.lower pode ser usada para realizar uma ordenação sem levar em conta
maiúsculas e minúsculas, e key=len irá ordenar as strings pela quantidade de caracteres. O default é a função
identidade (isto é, os itens propriamente ditos são comparados).

Também se pode usar o parâmetro de palavra-chave opcional key com as funções embutidas
👉 DICA min() e max() , e com outras funções da biblioteca padrão (por exemplo, itertools.groupby() e
heapq.nlargest() ).

Aqui estão alguns exemplos para esclarecer o uso dessas funções e dos argumentos de palavra-chave. Os exemplos
também demonstram que o algoritmo de ordenação do Python é estável (isto é, ele preserva a ordem relativa de itens
que resultam iguais na comparação):[15]

PYCON
>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry'] (1)
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] (2)
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple'] (3)
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry'] (4)
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple'] (5)
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] (6)
>>> fruits.sort() (7)
>>> fruits
['apple', 'banana', 'grape', 'raspberry'] (8)

1. Isso produz uma lista de strings ordenadas alfabeticamente.[16]


2. Inspecionando a lista original, vemos que ela não mudou.
3. Isso é a ordenação "alfabética" anterior, invertida.
4. Uma nova lista de strings, agora ordenada por tamanho. Como o algoritmo de ordenação é estável, "grape" e
"apple," ambas com tamanho 5, estão em sua ordem original.
5. Essas são strings ordenadas por tamanho em ordem descendente. Não é o inverso do resultado anterior porque a
ordenação é estável e então, novamente, "grape" aparece antes de "apple."
6. Até aqui, a ordenação da lista fruits original não mudou.
7. Isso ordena a lista no mesmo lugar, devolvendo None (que o console omite).
8. Agora fruits está ordenada.

Por default, o Python ordena as strings lexicograficamente por código de caractere. Isso quer dizer

⚠️ AVISO que as letras maiúsculas ASCII virão antes das minúsculas, e que os caracteres não-ASCII
dificilmente serão ordenados de forma razoável. A seção Seção 4.8 trata de maneiras corretas de
ordenar texto da forma esperada por seres humanos.
Uma vez ordenadas, podemos realizar em nossas sequências de forma muito eficiente. Um algoritmo de busca binária
já é fornecido no módulo bisect da biblioteca padrão do Python. Aquele módulo também inclui a função
bisect.insort , que você pode usar para assegurar que suas sequências ordenadas permaneçam ordenadas. Há uma
introdução ilustrada ao módulo bisect no post "Managing Ordered Sequences with Bisect" (Gerenciando Sequências
Ordenadas com Bisect) (https://fpy.li/bisect) (EN) em fluentpython.com (http://fluentpython.com), o website que complementa
este livro.

Muito do que vimos até aqui neste capítulo se aplica a sequências em geral, não apenas a listas ou tuplas.
Programadores Python às vezes usam excessivamente o tipo list , por ele ser tão conveniente—eu mesmo já fiz isso.
Por exemplo, se você está processando grandes listas de números, deveria considerar usar arrays em vez de listas. O
restante do capítulo é dedicado a alternativas a listas e tuplas.

2.10. Quando uma lista não é a resposta


O tipo list é flexível e fácil de usar mas, dependendo dos requerimentos específicos, há opções melhores. Por
exemplo, um array economiza muita memória se você precisa manipular milhões de valores de ponto flutuante. Por
outro lado, se você está constantemente acrescentando e removendo itens das pontas opostas de uma lista, é bom saber
que um deque (uma fila com duas pontas) é uma estrutura de dados FIFO[17] mais eficiente.

Se seu código frequentemente verifica se um item está presente em uma coleção (por exemplo, item
in my_collection ), considere usar um set para my_collection , especialmente se ela contiver
👉 DICA um número grande de itens. Um set é otimizado para verificação rápida de presença de itens. Eles
também são iteráveis, mas não são coleções, porque a ordenação de itens de sets não é especificada.
Vamos falar deles no Capítulo 3.

O restante desse capítulo discute tipos mutáveis de sequências que, em muitos casos, podem substituir as listas.
Começamos pelos arrays.

2.10.1. Arrays
Se uma lista contém apenas números, uma array.array é um substituto mais eficiente. Arrays suportam todas as
operações das sequências mutáveis (incluindo .pop , .insert , e .extend ), bem como métodos adicionais para
carregamento e armazenamento rápidos, tais como .frombytes e .tofile .

Um array do Python quase tão enxuto quanto um array do C. Como mostrado na Figura 1, um array de valores float
não mantém instâncias completas de float , mas apenas pacotes de bytes representando seus valores em código de
máquina—de forma similar a um array de double na linguagem C. Ao criar um array , você fornece um código de
tipo (typecode), uma letra que determina o tipo C subjacente usado para armazenar cada item no array. Por exemplo, b
é o código de tipo para o que o C chama de signed char , um inteiro variando de -128 a 127. Se você criar uma
array('b') , então cada item será armazenado em um único byte e será interpretado como um inteiro. Para grandes
sequências de números, isso economiza muita memória. E o Python não permite que você insira qualquer número que
não corresponda ao tipo do array.

O Exemplo 19 mostra a criação, o armazenamento e o carregamento de um array de 10 milhões de números de ponto


flutuante aleatórios.

Exemplo 19. Criando, armazenando e carregando uma grande array de números de ponto flutuante.
PYCON
>>> from array import array (1)
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7))) (2)
>>> floats[-1] (3)
0.07802343889111107
>>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp) (4)
>>> fp.close()
>>> floats2 = array('d') (5)
>>> fp = open('floats.bin', 'rb')
>>> floats2.fromfile(fp, 10**7) (6)
>>> fp.close()
>>> floats2[-1] (7)
0.07802343889111107
>>> floats2 == floats (8)
True

1. Importa o tipo array .

2. Cria um array de números de ponto flutuante de dupla precisão (código de tipo 'd' ) a partir de qualquer objeto
iterável—nesse caso, uma expressão geradora.
3. Inspeciona o último número no array.
4. Salva o array em um arquivo binário.
5. Cria um array vazio de números de ponto flutuante de dupla precisão
6. Lê 10 milhões de números do arquivo binário.
7. Inspeciona o último número no array.
8. Verifica a igualdade do conteúdo dos arrays

Como você pode ver, array.tofile e array.fromfile são fáceis de usar. Se você rodar o exemplo, verá que são
também muito rápidos. Um pequeno experimento mostra que array.fromfile demora aproximadamente 0,1
segundos para carregar 10 milhões de números de ponto flutuante de dupla precisão de um arquivo binário criado
com array.tofile . Isso é quase 60 vezes mais rápido que ler os números de um arquivo de texto, algo que também
exige passar cada linha para a função embutida float . Salvar o arquivo com array.tofile é umas sete vezes mais
rápido que escrever um número de ponto flutuante por vez em um arquivo de texto. Além disso, o tamanho do arquivo
binário com 10 milhões de números de dupla precisão é de 80.000.000 bytes (8 bytes por número, zero excesso),
enquanto o arquivo de texto ocupa 181.515.739 bytes para os mesmos dados.

Para o caso específico de arrays numéricas representando dados binários, tal como bitmaps de imagens, o Python tem
os tipos bytes e bytearray , discutidos na seção Capítulo 4.

Vamos encerrar essa seção sobre arrays com a Tabela 5, comparando as características de list e array.array .

Tabela 5. Métodos e atributos encontrados em list ou array (os métodos descontinuados de array e aqueles
implementados também pir object foram omitidos para preservar espaço)
list array

s.__add__(s2) ● ● s + s2—concatenação

s.__iadd__(s2) ● ● s += s2—concatenação no
mesmo lugar
list array

s.append(e) ● ● Acrescenta um elemento


após o último

s.byteswap() ● Permuta os bytes de todos


os itens do array para
conversão de endianness
(ordem de interpretação
bytes)

s.clear() ● Apaga todos os itens

s.__contains__(e) ● ● e in s

s.copy() ● Cópia rasa da lista

s.__copy__() ● Suporte a copy.copy

s.count(e) ● ● Conta as ocorrências de


um elemento

s.__deepcopy__() ● Suporte otimizado a


copy.deepcopy

s.__delitem__(p) ● ● Remove item na posição p

s.extend(it) ● ● Acrescenta itens a partir


do iterável it

s.frombytes(b) ● Acrescenta itens de uma


sequência de bytes,
interpretada como valores
em código de máquina
empacotados

s.fromfile(f, n) ● Acrescenta n itens de um


arquivo binário f ,
interpretado como valores
em código de máquina
empacotados

s.fromlist(l) ● Acrescenta itens de lista;


se um deles causar um
TypeError, nenhum item
é acrescentado

s.__getitem__(p) ● ● s[p]—obtém o item ou


fatia na posição

s.index(e) ● ● Encontra a posição da


primeira ocorrência de e
list array

s.insert(p, e) ● ● Insere elemento e antes


do item na posição p

s.itemsize ● Tamanho em bytes de


cada item do array

s.__iter__() ● ● Obtém iterador

s.__len__() ● ● len(s)—número de itens

s.__mul__(n) ● ● s * n—concatenação
repetida

s.__imul__(n) ● ● s *= n—concatenação
repetida no mesmo lugar

s.__rmul__(n) ● ● n * s—concatenação
repetida invertida[18]

s.pop([p]) ● ● Remove e devolve o item


na posição p (default: o
último)

s.remove(e) ● ● Remove a primeira


ocorrência do elemento e
por valor

s.reverse() ● ● Reverte a ordem dos itens


no mesmo lugar

s.__reversed__() ● Obtém iterador para


percorrer itens do último
até o primeiro

s.__setitem__(p, e) ● ● s[p] = e—coloca e na


posição p ,
sobrescrevendo item ou
fatia existente

s.sort([key], ● Ordena itens no mesmo


[reverse]) lugar, com os argumentos
de palavra-chave
opcionais key e reverse

s.tobytes() ● Devolve itens como


pacotes de valores em
código de máquina em um
objeto bytes
list array

s.tofile(f) ● Grava itens como pacotes


de valores em código de
máquina no arquivo
binário f

s.tolist() ● Devolve os itens como


objetos numéricos em
uma list

s.typecode ● String de um caractere


identificando o tipo em C
dos itens

Até o Python 3.10, o tipo array ainda não tem um método sort equivalente a list.sort() , que
reordena os elementos na própria estrutura de dados, sem copiá-la. Se você precisa ordenar um
array, use a função embutida sorted para reconstruir o array:

👉 DICA a = array.array(a.typecode, sorted(a))


PYTHON3

Para manter a ordem de um array ordenad ao acrescentar novos itens, use a função
bisect.insort (https://docs.python.org/pt-br/3/library/bisect.html#bisect.insort).

Se você trabalha muito com arrays e não conhece memoryview , está perdendo oportunidades. Veja o próximo tópico.

2.10.2. Views de memória


A classe embutida memoryview é um tipo sequência de memória compartilhada, que permite manipular fatias de
arrays sem copiar bytes. Ela foi inspirada pela biblioteca NumPy (que discutiremos brevemente, na seção Seção 2.10.3).
Travis Oliphant, autor principal da NumPy, responde assim à questão "When should a memoryview be used?" Quando
se deve usar uma memoryview? (https://fpy.li/2-17):

“ Uma memoryview é essencialmente uma estrutura de array Numpy generalizada dentro do


próprio Python (sem a matemática). Ela permite compartilhar memória entre estruturas de
dados (coisas como imagens PIL, bancos de dados SQLite, arrays da NumPy, etc.) sem copiar
antes. Isso é muito importante para conjuntos grandes de dados.

Usando uma notação similar ao módulo array , o método memoryview.cast permite mudar a forma como múltiplos
bytes são lidos ou escritos como unidades, sem a necessidade de mover os bits. memoryview.cast devolve ainda outro
objeto memoryview , sempre compartilhando a mesma memória.

O Exemplo 20 mostra como criar views alternativas da mesmo array de 6 bytes, para operar com ele como uma matriz
de 2x3 ou de 3x2.

Exemplo 20. Manipular 6 bytes de memória como views de 1×6, 2×3, e 3×2
PYCON
>>> from array import array
>>> octets = array('B', range(6)) # (1)
>>> m1 = memoryview(octets) # (2)
>>> m1.tolist()
[0, 1, 2, 3, 4, 5]
>>> m2 = m1.cast('B', [2, 3]) # (3)
>>> m2.tolist()
[[0, 1, 2], [3, 4, 5]]
>>> m3 = m1.cast('B', [3, 2]) # (4)
>>> m3.tolist()
[[0, 1], [2, 3], [4, 5]]
>>> m2[1,1] = 22 # (5)
>>> m3[1,1] = 33 # (6)
>>> octets # (7)
array('B', [0, 1, 2, 33, 22, 5])

1. Cria um array de 6 bytes (código de tipo 'B' ).

2. Cria uma memoryview a partir daquele array, e a exporta como uma lista.
3. Cria uma nova memoryview a partir da anterior, mas com 2 linhas e 3 colunas.
4. Ainda outra memoryview , agora com 3 linhas e 2 colunas.
5. Sobrescreve o byte em m2 , na linha 1 , coluna 1 com 22 .

6. Sobrescreve o byte em m3 , na linha 1 , coluna 1 com 33 .

7. Mostra o array original, provando que a memória era compartilhada entre octets , m1 , m2 , e m3 .

O fantástico poder de memoryview também pode ser usado para o mal. O Exemplo 21 mostra como mudar um único
byte de um item em um array de inteiros de 16 bits.

Exemplo 21. Mudando o valor de um item em um array de inteiros de 16 bits trocando apenas o valor de um de seus
bytes

PYCON
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) (1)
>>> len(memv)
5
>>> memv[0] (2)
-2
>>> memv_oct = memv.cast('B') (3)
>>> memv_oct.tolist() (4)
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 (5)
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) (6)

1. Cria uma memoryview a partir de um array de 5 inteiros com sinal de 16 bits (código de tipo 'h' ).

2. memv vê os mesmos 5 itens no array.


3. Cria memv_oct , transformando os elementos de memv em bytes (código de tipo 'B' ).

4. Exporta os elementos de memv_oct como uma lista de 10 bytes, para inspeção.


5. Atribui o valor 4 ao byte com offset 5.

6. Observe a mudança em numbers : um 4 no byte mais significativo de um inteiro de 2 bytes sem sinal é 1024 .
Você pode ver um exemplo de inspeção de uma memoryview com o pacote struct em
✒️ NOTA fluentpython.com (http://fluentpython.com): "Parsing binary records with struct" Analisando registros
binários com struct (https://fpy.li/2-18) (EN).

Enquanto isso, se você está fazendo processamento numérico avançado com arrays, deveria estar usando as bibliotecas
NumPy. Vamos agora fazer um breve passeio por elas.

2.10.3. NumPy
Por todo esse livro, procuro destacar o que já existe na biblioteca padrão do Python, para que você a aproveite ao
máximo. Mas a NumPy é tão maravilhosa que exige um desvio.

Por suas operações avançadas de arrays e matrizes, o Numpy é a razão pela qual o Python se tornou uma das
principais linguagens para aplicações de computação científica. A Numpy implementa tipos multidimensionais e
homogêneos de arrays e matrizes, que podem conter não apenas números, mas também registros definidos pelo
usuário. E fornece operações eficientes ao nível desses elementos.

A SciPy é uma biblioteca criada usando a NumPy, e oferece inúmeros algoritmos de computação científica, incluindo
álgebra linear, cálculo numérico e estatística. A SciPy é rápida e confiável porque usa a popular base de código C e
Fortran do Repositório Netlib (https://fpy.li/2-19). Em outras palavras, a SciPy dá a cientistas o melhor de dois mundos: um
prompt iterativo e as APIs de alto nível do Python, junto com funções estáveis e de eficiência comprovada para
processamento de números, otimizadas em C e Fortran

O Exemplo 22, uma amostra muito rápida da Numpy, demonstra algumas operações básicas com arrays bi-
dimensionais.

Exemplo 22. Operações básicas com linhas e colunas em uma numpy.ndarray

PYCON
>>> import numpy as np (1)
>>> a = np.arange(12) (2)
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape (3)
(12,)
>>> a.shape = 3, 4 (4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[2] (5)
array([ 8, 9, 10, 11])
>>> a[2, 1] (6)
9
>>> a[:, 1] (7)
array([1, 5, 9])
>>> a.transpose() (8)
array([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])

1. Importa a NumPy, que precisa ser instalada previamente (ela não faz parte da biblioteca padrão do Python). Por
convenção, numpy é importada como np .
2. Cria e inspeciona uma numpy.ndarray com inteiros de 0 a 11 .
3. Inspeciona as dimensões do array: essa é um array com uma dimensão e 12 elementos.
4. Muda o formato do array, acrescentando uma dimensão e depois inspecionando o resultado.
5. Obtém a linha no índice 2

6. Obtém elemento na posição 2, 1 .

7. Obtém a coluna no índice 1

8. Cria um novo array por transposição (permutando as colunas com as linhas)


A NumPy também suporta operações de alto nível para carregar, salvar e operar sobre todos os elementos de uma
numpy.ndarray :

PYCON
>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt') (1)
>>> floats[-3:] (2)
array([ 3016362.69195522, 535281.10514262, 4566560.44373946])
>>> floats *= .5 (3)
>>> floats[-3:]
array([ 1508181.34597761, 267640.55257131, 2283280.22186973])
>>> from time import perf_counter as pc (4)
>>> t0 = pc(); floats /= 3; pc() - t0 (5)
0.03690556302899495
>>> numpy.save('floats-10M', floats) (6)
>>> floats2 = numpy.load('floats-10M.npy', 'r+') (7)
>>> floats2 *= 6
>>> floats2[-3:] (8)
memmap([ 3016362.69195522, 535281.10514262, 4566560.44373946])

1. Carrega 10 milhões de números de ponto flutuante de um arquivo de texto.


2. Usa a notação de fatiamento de sequência para inspecionar os três últimos números.
3. Multiplica cada elemento no array floats por .5 e inspeciona novamente os três últimos elementos.
4. Importa o cronômetro de medida de tempo em alta resolução (disponível desde o Python 3.3).
5. Divide cada elemento por 3 ; o tempo decorrido para dividir os 10 milhões de números de ponto flutuante é menos
de 40 milissegundos.
6. Salva o array em um arquivo binário .npy.
7. Carrega os dados como um arquivo mapeado na memória em outro array; isso permite o processamento eficiente
de fatias do array, mesmo que ele não caiba inteiro na memória.
8. Inspeciona os três últimos elementos após multiplicar cada elemento por 6.

Mas isso foi apenas um aperitivo.

A NumPy e a SciPy são bibliotecas formidáveis, e estão na base de outras ferramentas fantásticas, como a Pandas
(https://fpy.li/2-20) (EN)—que implementa tipos eficientes de arrays capazes de manter dados não-numéricos, e fornece
funções de importação/exportação em vários formatos diferentes, como .csv, .xls, dumps SQL, HDF5, etc.—e a scikit-
learn (https://fpy.li/2-21) (EN), o conjunto de ferramentas para Aprendizagem de Máquina mais usado atualmente. A
maior parte das funções da NumPy e da SciPy são implementadas em C ou C++, e conseguem aproveitar todos os
núcleos de CPU disponíveis, pois podem liberar a GIL (Global Interpreter Lock, Trava Global do Interpretador) do
Python. O projeto Dask (https://fpy.li/dask) suporta a paralelização do processamento da NumPy, da Pandas e da scikit-
learn para grupos (clusters) de máquinas. Esses pacotes merecem que livros inteiros sejam escritos sobre eles. Este não
um desses livros. Mas nenhuma revisão das sequências do Python estaria completa sem pelo menos uma breve
passagem pelos arrays da NumPy.
Tendo olhado as sequências planas—arrays padrão e arrays da NumPy—vamos agora nos voltar para um grupo
completamente diferentes de substitutos para a boa e velha list : filas (queues).

2.10.4. Deques e outras filas


Os métodos .append e .pop tornam uma list usável como uma pilha (stack) ou uma fila (queue) (usando .append
e .pop(0) , se obtém um comportamento FIFO). Mas inserir e remover da cabeça de uma lista (a posição com índice 0)
é caro, pois a lista toda precisa ser deslocada na memória.

A classe collections.deque é uma fila de duas pontas e segura para usar com threads, projetada para inserção e
remoção rápida nas duas pontas. É também a estrutura preferencial se você precisa manter uma lista de "últimos itens
vistos" ou coisa semelhante, pois um deque pode ser delimitado—isto é, criado com um tamanho máximo fixo. Se um
deque delimitado está cheio, quando se adiciona um novo item, o item na ponta oposta é descartado. O Exemplo 23
mostra algumas das operações típicas com um deque .

Exemplo 23. Usando um deque

PYCON
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10) (1)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3) (2)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1) (3)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33]) (4)
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40]) (5)
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

1. O argumento opcional maxlen determina o número máximo de itens permitidos nessa instância de deque ; isso
estabelece o valor de um atributo de instância maxlen , somente de leitura.
2. Rotacionar com n > 0 retira itens da direita e os recoloca pela esquerda; quando n < 0 , os itens são retirados pela
esquerda e anexados pela direita.
3. Acrescentar itens a um deque cheio ( len(d) == d.maxlen ) elimina itens da ponta oposta. Observe, na linha
seguinte, que o 0 foi descartado.
4. Acrescentar três itens à direita derruba -1 , 1 , e 2 da extremidade esquerda.
5. Observe que extendleft(iter) acrescenta cada item sucessivo do argumento iter do lado esquerdo do deque ,
então a posição final dos itens é invertida.

A Tabela 6 compara os métodos específicos de list e deque (omitindo aqueles que também aparecem em object ).

Veja que dequeimplementa a maioria dos métodos de list , acrescentando alguns específicos ao seu modelo, como
popleft e rotate . Mas há um custo oculto: remover itens do meio de um deque não é rápido. A estrutura é
realmente otimizada para acréscimos e remoções pelas pontas.
As operações append e popleft são atômicas, então deque pode ser usado de forma segura como uma fila FIFO em
aplicações multithread sem a necessidade de travas.

Tabela 6. Métodos implementados em list ou deque (aqueles também implementados por object foram omitidos
para preservar espaço)

list deque

s.__add__(s2) ● s + s2—concatenação

s.__iadd__(s2) ● ● s += s2—concatenação no
mesmo lugar

s.append(e) ● ● Acrescenta um elemento à


direita (após o último)

s.appendleft(e) ● Acrescenta um elemento à


esquerda (antes do
primeiro)

s.clear() ● ● Apaga todos os itens

s.__contains__(e) ● e in s

s.copy() ● Cópia rasa da lista

s.__copy__() ● Suporte a copy.copy


(cópia rasa)

s.count(e) ● ● Conta ocorrências de um


elemento

s.__delitem__(p) ● ● Remove item na posição p

s.extend(i) ● ● Acrescenta item do


iterável i pela direita

s.extendleft(i) ● Acrescenta item do


iterável i pela esquerda

s.__getitem__(p) ● ● s[p]—obtém item ou fatia


na posição

s.index(e) ● Encontra a primeira


ocorrência de e

s.insert(p, e) ● Insere elemento e antes


do item na posição p

s.__iter__() ● ● Obtém iterador

s.__len__() ● ● len(s)—número de itens

s.__mul__(n) ● s * n—concatenação
repetida
list deque

s.__imul__(n) ● s *= n—concatenação
repetida no mesmo lugar

s.__rmul__(n) ● n * s—concatenação
repetida invertida[19]

s.pop() ● ● Remove e devolve último


item[20]

s.popleft() ● Remove e devolve


primeiro item

s.remove(e) ● ● Remove primeira


ocorrência do elemento e
por valor

s.reverse() ● ● Inverte a ordem do itens


no mesmo lugar

s.__reversed__() ● ● Obtém iterador para


percorrer itens, do último
para o primeiro

s.rotate(n) ● Move n itens de um lado


para o outro

s.__setitem__(p, e) ● ● s[p] = e—coloca e na


posição p ,
sobrescrevendo item ou
fatia existentes

s.sort([key], ● Ordena os itens no mesmo


[reverse]) lugar, com os argumentos
de palavra-chave
opcionais key e reverse

Além de deque , outros pacotes da biblioteca padrão do Python implementam filas:

queue

Fornece as classes sincronizadas (isto é, seguras para se usar com múltiplas threads) SimpleQueue , Queue ,
LifoQueue , e PriorityQueue . Essas classes podem ser usadas para comunicação segura entre threads. Todas,
exceto SimpleQueue , podem ser delimitadas passando um argumento maxsize maior que 0 ao construtor.
Entretanto, elas não descartam um item para abrir espaço, como faz deque . Em vez disso, quando a fila está lotada,
a inserção de um novo item bloqueia quem tentou inserir—isto é, ela espera até alguma outra thread criar espaço
retirando um item da fila, algo útil para limitar o número de threads ativas.

multiprocessing

Implementa sua própria SimpleQueue , não-delimitada, e Queue , delimitada, muito similares àquelas no pacote
queue , mas projetadas para comunicação entre processos. Uma fila especializada,
multiprocessing.JoinableQueue , é disponibilizada para gerenciamento de tarefas.
asyncio
Fornece Queue , LifoQueue , PriorityQueue , e JoinableQueue com APIs inspiradas pelas classes nos módulos
queue e multiprocessing , mas adaptadas para gerenciar tarefas em programação assíncrona.

heapq

Diferente do últimos três módulos, heapq não implementa a classe queue, mas oferece funções como heappush e
heappop , que permitem o uso de uma sequência mutável como uma fila do tipo heap ou como uma fila de
prioridade.
Aqui termina nossa revisão das alternativas ao tipo list , e também nossa exploração dos tipos sequência em geral—
exceto pelas especificidades de str e das sequências binárias, que tem seu próprio capítulo (Capítulo 4).

2.11. Resumo do capítulo


Dominar o uso dos tipos sequência da biblioteca padrão é um pré-requisito para escrever código Python conciso,
eficiente e idiomático.

As sequências do Python são geralmente categorizadas como mutáveis ou imutáveis, mas também é útil considerar um
outro eixo: sequências planas e sequências contêiner. As primeiras são mais compactas, mais rápidas e mais fáceis de
usar, mas estão limitadas a armazenar dados atômicos como números, caracteres e bytes. As sequências contêiner são
mais flexíveis, mas podem surpreender quando contêm objetos mutáveis. Então, quando armazenando estruturas de
dados aninhadas, é preciso ter cuidado para usar tais sequências da forma correta.

Infelizmente o Python não tem um tipo de sequência contêiner imutável infalível: mesmo as tuplas "imutáveis" podem
ter seus valores modificados quando contêm itens mutáveis como listas ou objetos definidos pelo usuário.

Compreensões de lista e expressões geradoras são notações poderosas para criar e inicializar sequências. Se você ainda
não se sente confortável com essas técnicas, gaste o tempo necessário para aprender seu uso básico. Não é difícil, e
você logo vai estar gostando delas.

As tuplas no Python tem dois papéis: como registros de campos sem nome e como listas imutáveis. Ao usar uma tupla
como uma lista imutável, lembre-se que só é garantido que o valor de uma tupla será fixo se todos os seus itens
também forem imutáveis. Chamar hash(t) com a tupla como argumento é uma forma rápida de se assegurar que seu
valor é fixo. Se t contiver itens mutáveis, um TypeError é gerado.

Quando uma tupla é usada como registro, o desempacotamento de tuplas é a forma mais segura e legível de extrair
seus campos. Além das tuplas, * funciona com listas e iteráveis em vários contextos, e alguns de seus casos de uso
apareceram no Python 3.5 com a PEP 448—Additional Unpacking Generalizations (Generalizações de
Desempacotamento Adicionais) (https://fpy.li/pep448) (EN). O Python 3.10 introduziu o pattern matching com match/case ,
suportando um tipo de desempacotamento mais poderoso, conhecido como desestruturação.

Fatiamento de sequências é um dos recursos de sintaxe preferidos do Python, e é ainda mais poderoso do que muita
gente pensa. Fatiamento multidimensional e a notação de reticências ( ... ), como usados no NumPy, podem também
ser suportados por sequências definidas pelo usuário. Atribuir a fatias é uma forma muito expressiva de editar
sequências mutáveis.

Concatenação repetida, como em seq * n , é conveniente e, tomando cuidado, pode ser usada para inicializar listas de
listas contendo itens imutáveis. Atribuição aumentada com += e *= se comporta de forma diferente com sequências
mutáveis e imutáveis. No último caso, esses operadores necessariamente criam novas sequências. Mas se a sequência
alvo é mutável, ela em geral é modificada no lugar—mas nem sempre, depende de como a sequência é implementada.
O método sort e a função embutida sorted são fáceis de usar e flexíveis, graças ao argumento opcional key : uma
função para calcular o critério de ordenação. E aliás, key também pode ser usado com as funções embutidas min e
max .

Além de listas e tuplas, a biblioteca padrão do Python oferece array.array . Apesar da NumPy e da SciPy não serem
parte da biblioteca padrão, se você faz qualquer tipo de processamento numérico em grandes conjuntos de dados,
estudar mesmo uma pequena parte dessas bibliotecas pode levar você muito longe.

Terminamos com uma visita à versátil collections.deque , também segura para usar com threads. Comparamos sua
API com a de list na Tabela 6 e mencionamos as outras implementações de filas na biblioteca padrão.

2.12. Leitura complementar


O capítulo 1, "Data Structures" (Estruturas de Dados) do Python Cookbook, 3rd ed. (https://fpy.li/pycook3) (EN) (O’Reilly), de
David Beazley e Brian K. Jones, traz muitas receitas usando sequências, incluindo a "Recipe 1.11. Naming a Slice"
(Receita 1.11. Nomeando uma Fatia), onde aprendi o truque de atribuir fatias a variáveis para melhorar a legibilidade,
como ilustrado no nosso Exemplo 13.

A segunda edição do Python Cookbook foi escrita para Python 2.4, mas a maior parte de seu código funciona com
Python 3, e muitas das receitas dos capítulos 5 e 6 lidam com sequências. O livro foi editado por Alex Martelli, Anna
Ravenscroft, e David Ascher, e inclui contribuições de dúzias de pythonistas. A terceira edição foi reescrita do zero, e se
concentra mais na semântica da linguagem—especialmente no que mudou no Python 3—enquanto o volume mais
antigo enfatiza a pragmática (isto é, como aplicar a linguagem a problemas da vida real). Apesar de algumas das
soluções da segunda edição não serem mais a melhor abordagem, eu honestamente acho que vale a pena ter à mão as
duas edições do Python Cookbook.

O "HowTo - Ordenação" (https://docs.python.org/pt-br/3/howto/sorting.html) oficial do Python tem vários exemplos de técnicas


avançadas de uso de sorted e list.sort .

A PEP 3132—​Extended Iterable Unpacking (Desempacotamento Iterável Estendido) (https://fpy.li/2-2) (EN) é a fonte
canônica para ler sobre o novo uso da sintaxe *extra no lado esquerdo de atribuições paralelas. Se você quiser dar
uma olhada no processo de evolução do Python, "Missing *-unpacking generalizations" (As generalizações esquecidas de
* no desempacotamento) (https://fpy.li/2-24) (EN) é um tópico do bug tracker propondo melhorias na notação de
desempacotamento iterável. PEP 448—​Additional Unpacking Generalizations (Generalizações de Desempacotamento
Adicionais) (https://fpy.li/pep448) (EN) foi o resultado de discussões ocorridas naquele tópico.

Como mencionei na seção Seção 2.6, o texto introdutório "Correspondência de padrão estrutural"
(https://docs.python.org/pt-br/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching), de Carol Willing, no "O que há de
novo no Python 3.10" (https://docs.python.org/pt-br/3.10/whatsnew/3.10.html), é uma ótima introdução a esse novo grande
recurso, em mais ou menos 1.400 palavras (isso é menos de 5 páginas quando o Firefox converte o HTML em PDF). A
PEP 636—Structural Pattern Matching: Tutorial (Casamento de Padrões Estrutural: Tutorial) (https://fpy.li/pep636) (EN)
também é boa, mas mais longa. A mesma PEP 636 inclui o "Appendix A—Quick Intro" (Apêndice A-Introdução Rápida)
(https://fpy.li/2-27) (EN). Ele é menor que a introdução de Willing, porque omite as considerações gerais sobre os motivos
pelos quais o pattern matching é bom para você. SE você precisar de mais argumentos para se convencer ou convencer
outros que o pattern matching é bom para o Python, leia as 22 páginas de PEP 635—Structural Pattern Matching:
Motivation and Rationale (_Casamento de Padrões Estrutural: Motivação e Justificativa) (https://fpy.li/pep635) (EN).

O post de Eli Bendersky em seu blog, "Less copies in Python with the buffer protocol and memoryviews" (Menos cópias
em Python, com o protocolo de buffer e mamoryviews) (https://fpy.li/2-28) inclui um pequeno tutorial sobre memoryview .
Há muitos livros tratando da NumPy no mercado, e muitos não mencionam "NumPy" no título. Dois exemplos são o
Python Data Science Handbook (https://fpy.li/2-29), escrito por Jake VanderPlas e de acesso aberto, e a segunda edição do
Python for Data Analysis (https://fpy.li/2-30), de Wes McKinney.

"A Numpy é toda sobre vetorização". Essa é a frase de abertura do livro de acesso aberto From Python to NumPy
(https://fpy.li/2-31), de Nicolas P. Rougier. Operações vetorizadas aplicam funções matemáticas a todos os elementos de um
array sem um loop explícito escrito em Python. Elas podem operar em paralelo, usando instruções especiais de vetor
presentes em CPUs modernas, tirando proveito de múltiplos núcleos ou delegando para a GPU, dependendo da
biblioteca. O primeiro exemplo no livro de Rougier mostra um aumento de velocidade de 500 vezes, após a refatoração
de uma bela classe pythônica, usando um método gerador, em uma pequena e feroz função que chama um par de
funções de vetor da NumPy.

Para aprender a usar deque (e outras coleções), veja os exemplos e as receitas práticas em "Tipos de dados de
contêineres" (https://docs.python.org/pt-br/3/library/collections.html), na documentação do Python.

A melhor defesa da convenção do Python de excluir o último item em faixas e fatias foi escrita pelo próprio Edsger W.
Dijkstra, em uma nota curta intitulada "Why Numbering Should Start at Zero" (Porque a Numeração Deve Começar em
Zero) (https://fpy.li/2-32). O assunto da nota é notação matemática, mas ela é relevante para o Python porque Dijkstra
explica, com humor e rigor, porque uma sequência como 2, 3, …​, 12 deveria sempre ser expressa como 2 ≤ i < 13. Todas
as outras convenções razoáveis são refutadas, bem como a ideia de deixar cada usuário escolher uma convenção. O
título se refere à indexação baseada em zero, mas a nota na verdade é sobre porque é desejável que 'ABCDE'[1:3]
signifique 'BC' e não 'BCD' , e porque faz todo sentido escrever range(2, 13) para produzir 2, 3, 4, …​, 12. E, por
sinal, a nota foi escrita à mão, mas é linda e totalmente legível. A letra de Dijkstra é tão cristalina que alguém criou uma
fonte (https://fpy.li/2-33) a partir de suas anotações.

Ponto de Vista
A natureza das tuplas

Em 2012, apresentei um poster sobre a linguagem ABC na PyCon US. Antes de criar o Python, Guido van Rossum
tinha trabalhado no interpretador ABC, então ele veio ver meu pôster. Entre outras coisas, falamos sobre como os
compounds (compostos) da ABC, que são claramente os predecessores das tuplas do Python. Compostos também
suportam atribuição paralela e são usados como chaves compostas em dicionários (ou tabelas, no jargão da ABC).
Entretanto, compostos não são sequências, Eles não são iteráveis, e não é possível obter um campo por índice,
muitos menos fatiá-los. Ou você manuseia o composto inteiro ou extrai os campos individuais usando atribuição
paralela, e é isso.

Disse a Guido que essas limitações tornavam muito claro o principal propósito dos compostos: ele são apenas
registros sem campos nomeados. Sua resposta: "Fazer as tuplas se comportarem como sequências foi uma
gambiarra."

Isso ilustra a abordagem pragmática que tornou o Python mais prático e mais bem sucedido que a ABC. Da
perspectiva de um implementador de linguagens, fazer as tuplas se comportarem como sequências custa pouco.
Como resultado, o principal caso de uso de tuplas como registros não é tão óbvio, mas ganhamos listas imutáveis
—mesmo que seu tipo não seja tão claramente nomeado como o de frozenlist .

Sequências planas versus sequências contêineres

Para realçar os diferentes modelos de memória dos tipos de sequências usei os termos sequência contêiner e
sequência plana. A palavra "contêiner" vem da própria documentação do "Modelo de Dados"
(https://docs.python.org/pt-br/3/reference/datamodel.html#objects-values-and-types):


“ Alguns objetos contêm referências a outros objetos; eles são chamados de contêineres.
Usei o termo "sequência contêiner" para ser específico, porque existem contêineres em Python que não são
sequências, como dict e set . Sequências contêineres podem ser aninhadas porque elas podem conter objetos
de qualquer tipo, incluindo seu próprio tipo.

Por outro lado, sequências planas são tipos de sequências que não podem ser aninhadas, pois só podem conter
valores atômicos como inteiros, números de ponto flutuante ou caracteres.

Adotei o termo sequência plana porque precisava de algo para contrastar com "sequência contêiner."

Apesar dos uso anterior da palavra "containers" na documentação oficial, há uma classe abstrata em
collections.abc chamada Container . Aquela ABC tem apenas um método, __contains__ —o método
especial por trás do operador in . Isso significa que arrays e strings, que não são contêineres no sentido
tradicional, são subclasses virtuais de Container , porque implementam __contains__ . Isso é só mais um
exemplo de humanos usando uma mesma palavra para significar coisas diferentes. Nesse livro, vou escrever
"contêiner" com minúscula e em português para "um objeto que contém referências para outros objetos" e
Container com a inicial maiúscula em fonte mono espaçada para me referir a collections.abc.Container .

Listas bagunçadas

Textos introdutórios de Python costumam enfatizar que listas podem conter objetos de diferentes tipos, mas na
prática esse recurso não é muito útil: colocamos itens em uma lista para processá-los mais tarde, o que implica o
suporte, da parte de todos os itens, a pelo menos alguma operação em comum (isto é, eles devem todos "grasnar",
independente de serem ou não 100% patos, geneticamente falando), Por exemplo, não é possível ordenar uma
lista em Python 3 a menos que os itens ali contidos sejam comparáveis:

PYCON
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
>>> sorted(l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() < int()

Diferente das listas, as tuplas muitas vezes mantêm itens de tipos diferentes. Isso é natural: se cada item em uma
tupla é um campo, então cada campo pode ter um tipo diferente.

'key' é brilhante

O argumento opcional key de list.sort , sorted , max , e min é uma grande ideia. Outras linguagens forçam
você a fornecer uma função de comparação com dois argumentos, como a função descontinuada do Python 2
cmp(a, b) . Usar key é mais simples e mais eficiente. É mais simples porque basta definir uma função de um
único argumento que recupera ou calcula o critério a ser usado para ordenar seus objetos; isso é mais fácil que
escrever uma função de dois argumentos para devolver –1, 0, 1. Também é mais eficiente, porque a função key é
invocada apenas uma vez por item, enquanto a comparação de dois argumentos é chamada a cada vez que o
algoritmo de ordenação precisa comparar dois itens. Claro, o Python também precisa comparar as chaves ao
ordenar, mas aquela comparação é feita em código C otimizado, não em uma função Python escrita por você.

Por sinal, usando key podemos ordenar uma lista bagunçada de números e strings "parecidas com números". Só
precisamos decidir se queremos tratar todos os itens como inteiros ou como strings:
PYCON
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
>>> sorted(l, key=int)
[0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
>>> sorted(l, key=str)
[0, '1', 14, 19, '23', 28, '28', 5, 6, '9']

A Oracle, o Google, e a Conspiração Timbot

O algoritmo de ordenação usado em sorted e list.sort é o Timsort, um algoritmo adaptativo que troca de
estratégia de ordenação ( entre merge sort e insertion sort), dependendo de quão ordenados os dados já estão. Isso
é eficiente porque dados reais tendem a ter séries de itens ordenados. Há um artigo da Wikipedia
(https://pt.wikipedia.org/wiki/Timsort) sobre ele.

O Timsort foi usado no CPython pela primeira vez em 2002. Desde 2009, o Timsort também é usado para ordenar
arrays tanto no Java padrão quanto no Android, um fato que ficou muito conhecido quando a Oracle usou parte
do código relacionado ao Timsort como evidência da violação da propriedade intelectual da Sun pelo Google. Por
exemplo, veja essa ordem do Juiz William Alsup (https://fpy.li/2-36) (EN) de 2012. Em 2021, a Suprema Corte dos
Estados Unidos decidiu que o uso do código do Java pelo Google é "fair use"[21]

O Timsort foi inventado por Tim Peters, um dos desenvolvedores principais do Python, e tão produtivo que se
acredita que ele seja uma inteligência artificial, o Timbot. Você pode ler mais sobre essa teoria da conspiração em
"Python Humor" (https://fpy.li/2-37) (EN). Tim também escreveu "The Zen of Python": import this .
3. Dicionários e conjuntos
“ O Python é feito basicamente de dicionários cobertos por muitas camadas de açucar sintático — Lalo Martins
pioneiro do nomadismo digital e pythonista

Usamos dicionários em todos os nossos programas Python. Se não diretamente em nosso código, então indiretamente,
pois o tipo dict é um elemento fundamental da implementação do Python. Atributos de classes e de instâncias,
espaços de nomes de módulos e argumentos nomeados de funções são alguns dos elementos fundamentais do Python
representados na memória por dicionários. O __builtins__.__dict__ armazena todos os tipos, funções e objetos
embutidos.

Por seu papel crucial, os dicts do Python são extremamente otimizados—e continuam recebendo melhorias. As Tabelas
de hash são o motor por trás do alto desempenho dos dicts do Python.

Outros tipos embutidos baseados em tabelas de hash são set e frozenset . Eles oferecem uma API mais completa e
operadores mais robustos que os conjuntos que você pode ter encontrado em outras linguagens populares. Em
especial, os conjuntos do Python implementam todas as operações fundamentais da teoria dos conjuntos, como união,
intersecção, testes de subconjuntos, etc. Com eles, podemos expressar algoritmos de forma mais declarativa, evitando o
excesso de loops e condicionais aninhados.

Aqui está um breve esquema do capítulo:

A sintaxe moderna para criar e manipular dicts e mapeamentos, incluindo desempacotamento aumentado e
pattern matching (casamento de padrões)
Métodos comuns dos tipos de mapeamentos
Tratamento especial para chaves ausentes
Variantes de dict na biblioteca padrão
Os tipos set e frozenset

As implicações das tabelas de hash no comportamento de conjuntos e dicionários

3.1. Novidades nesse capítulo


A maior parte das mudanças nessa segunda edição se concentra em novos recursos relacionados a tipos de
mapeamento:

A seção Seção 3.2 fala da sintaxe aperfeiçoada de desempacotamento e de diferentes maneiras de mesclar
mapeamentos—incluindo os operadores | e |= , suportados pelos dicts desde o Python 3.9.
A seção Seção 3.3 ilustra o manuseio de mapeamentos com match/case , recurso que surgiu no Python 3.10.

A seção Seção 3.6.1 agora se concentra nas pequenas mas ainda relevantes diferenças entre dict e OrderedDict
—levando em conta que, desde o Python 3.6, dict passou a manter a ordem de inserção das chaves.
Novas seções sobre os objetos view devolvidos por dict.keys , dict.items , e dict.values : a Seção 3.8 e a Seção
3.12.

A implementação interna de dict e set ainda está alicerçada em tabelas de hash, mas o código de dict teve duas
otimizações importantes, que economizam memória e preservam o ordem de inserção das chaves. As seções Seção 3.9
e Seção 3.11 resumem o que você precisa saber sobre isso para usar bem as estruturas efetadas.
Após acrescentar mais de 200 páginas a essa segunda edição, transferi a seção opcional "Internals of
sets and dicts" (As entranhas dos sets e dos dicts) (https://fpy.li/hashint) (EN) para o fluentpython.com
(http://fluentpython.com), o site que complementa o livro. O post de 18 páginas (https://fpy.li/hashint) (EN)
foi atualizado e expandido, e inclui explicações e diagramas sobre:

✒️ NOTA O algoritmo de tabela de hash e as estruturas de dados, começando por seu uso em
mais simples de entender.
set , que é

A otimização de memória que preserva a ordem de inserção de chaves em instâncias de dict


(desde o Python 3.6) .
O layout do compartilhamento de chaves em dicionários que mantêm atributos de instância—o
__dict__ de objetos definidos pelo usuário (otimização implementada no Python 3.3).

3.2. A sintaxe moderna dos dicts


As próximas seções descrevem os recursos avançados de sintaxe para criação, desempacotamento e processamento de
mapeamentos. Alguns desses recursos não são novos na linguagem, mas podem ser novidade para você. Outros
requerem Python 3.9 (como o operador | ) ou Python 3.10 (como match/case ). Vamos começar por um dos melhores
e mais antigos desses recursos.

3.2.1. Compreensões de dict


Desde o Python 2.7, a sintaxe das listcomps e genexps foi adaptada para compreensões de dict (e também
compreensões de set , que veremos em breve). Uma dictcomp (compreensão de dict) cria uma instância de dict ,
recebendo pares key:value de qualquer iterável. O Exemplo 1 mostra o uso de compreensões de dict para criar
dois dicionários a partir de uma mesma lista de tuplas.

Exemplo 1. Exemplos de compreensões de dict

PYCON
>>> dial_codes = [ # (1)
... (880, 'Bangladesh'),
... (55, 'Brazil'),
... (86, 'China'),
... (91, 'India'),
... (62, 'Indonesia'),
... (81, 'Japan'),
... (234, 'Nigeria'),
... (92, 'Pakistan'),
... (7, 'Russia'),
... (1, 'United States'),
... ]
>>> country_dial = {country: code for code, country in dial_codes} # (2)
>>> country_dial
{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62,
'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
>>> {code: country.upper() # (3)
... for country, code in sorted(country_dial.items())
... if code < 70}
{55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA', 1: 'UNITED STATES'}

1. Um iterável de pares chave-valor como dial_codes pode ser passado diretamente para o construtor de dict ,
mas…​
2. …​aqui permutamos os pares: country é a chave, e code é o valor.
3. Ordenando country_dial por nome, revertendo novamente os pares, colocando os valores em maiúsculas e
filtrando os itens com code < 70 .
Se você já está acostumada com as listcomps, as dictcomps são um próximo passo natural. Caso contrário, a propagação
da sintaxe de compreensão mostra que agora é mais valioso que nunca se tornar fluente nessa técnica.

3.2.2. Desempacotando mapeamentos


A PEP 448—Additional Unpacking Generalizations (Generalizações de Desempacotamento Adicionais) (https://fpy.li/pep448)
melhorou o suporte ao desempacotamento de mapeamentos de duas formas, desde o Python 3.5.

Primeiro, podemos aplicar ** a mais de um argumento em uma chamada de função. Isso funciona quando todas as
chaves são strings e únicas, para todos os argumentos (porque argumentos nomeados duplicados são proibidos):

PYCON
>>> def dump(**kwargs):
... return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}

Em segundo lugar, ** pode ser usado dentro de um literal dict —também múltiplas vezes:

PYCON
>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}

Nesse caso, chaves duplicadas são permitidas. Cada ocorrência sobrescreve ocorrências anteriores—observe o valor
mapeado para x no exemplo.

Essa sintaxe também pode ser usada para mesclar mapas, mas isso pode ser feito de outras formas. Siga comigo.

3.2.3. Fundindo mapeamentos com |


Desde a versão 3.9, Python suporta o uso de | e |= para mesclar mapeamentos. Isso faz todo sentido, já que estes são
também os operadores de união de conjuntos.

O operador | cria um novo mapeamento:

PYCON
>>> d1 = {'a': 1, 'b': 3}
>>> d2 = {'a': 2, 'b': 4, 'c': 6}
>>> d1 | d2
{'a': 2, 'b': 4, 'c': 6}

O tipo do novo mapeamento normalmente será o mesmo do operando da esquerda—no exemplo, d1 —mas ele pode
ser do tipo do segundo operando se tipos definidos pelo usuário estiverem envolvidos na operação, dependendo das
regras de sobrecarga de operadores, que exploraremos no Capítulo 16.

Para atualizar mapeamentos existentes no mesmo lugar, use |= . Retomando o exemplo anterior, ali d1 não foi
modificado. Mas aqui sim:

PYCON
>>> d1
{'a': 1, 'b': 3}
>>> d1 |= d2
>>> d1
{'a': 2, 'b': 4, 'c': 6}
Se você precisa manter código rodando no Python 3.8 ou anterior, a seção "Motivation" (Motivação)
👉 DICA (https://fpy.li/3-1) (EN) da PEP 584—Add Union Operators To dict (Acrescentar Operadores de União a
dict) (https://fpy.li/pep584) (EN) inclui um bom resumo das outras formas de mesclar mapeamentos.

Agora vamos ver como o pattern matching se aplica aos mapeamentos.

3.3. Pattern matching com mapeamentos


A instrução match/case suporta sujeitos que sejam objetos mapeamento. Padrões para mapeamentos se parecem com
literais dict , mas podem casar com instâncias de qualquer subclasse real ou virtual de collections.abc.Mapping .
[22]

No Capítulo 2 nos concentramos apenas nos padrões de sequência, mas tipos diferentes de padrões podem ser
combinados e aninhados. Graças à desestruturação, o pattern matching é uma ferramenta poderosa para processar
registros estruturados como sequências e mapeamentos aninhados, que frequentemente precisamos ler de APIs JSON
ou bancos de dados com schemas semi-estruturados, como o MongoDB, o EdgeDB, ou o PostgreSQL. O Exemplo 2
demonstra isso.

As dicas de tipo simples em get_creators tornam claro que ela recebe um dict e devolve uma list .

Exemplo 2. creator.py: get_creators() extrai o nome dos criadores em registros de mídia

PY
def get_creators(record: dict) -> list:
match record:
case {'type': 'book', 'api': 2, 'authors': [*names]}: # (1)
return names
case {'type': 'book', 'api': 1, 'author': name}: # (2)
return [name]
case {'type': 'book'}: # (3)
raise ValueError(f"Invalid 'book' record: {record!r}")
case {'type': 'movie', 'director': name}: # (4)
return [name]
case _: # (5)
raise ValueError(f'Invalid record: {record!r}')

1. Casa com qualquer mapeamento na forma 'type': 'book', 'api' :2 , e uma chave 'authors' mapeada para
uma sequência. Devolve os itens da sequência, como uma nova list .
2. Casa com qualquer mapeamento na forma 'type': 'book', 'api' :1 , e uma chave 'author' mapeada para
qualquer objeto. Devolve aquele objeto dentro de uma list .
3. Qualquer outro mapeamento na forma 'type': 'book' é inválido e gera um ValueError .

4. Casa qualquer mapeamento na forma 'type': 'movie' e uma chave 'director' mapeada para um único
objeto. Devolve o objeto dentro de uma list .
5. Qualquer outro sujeito é inválido e gera um ValueError .

O Exemplo 2 mostra algumas práticas úteis para lidar com dados semi-estruturados, tais como registros JSON:

Incluir um campo descrevendo o tipo de registro (por exemplo, 'type': 'movie' )

Incluir um campo identificando a versão do schema (por exemplo, 'api': 2' ), para permitir evoluções futuras das
APIs públicas.
Ter cláusulas case para processar registros inválidos de um tipo específico (por exemplo, 'book' ), bem como um
case final para capturar tudo que tenha passado pelas condições anteriores.
Agora vamos ver como get_creators se comporta com alguns doctests concretos:

PYCON
>>> b1 = dict(api=1, author='Douglas Hofstadter',
... type='book', title='Gödel, Escher, Bach')
>>> get_creators(b1)
['Douglas Hofstadter']
>>> from collections import OrderedDict
>>> b2 = OrderedDict(api=2, type='book',
... title='Python in a Nutshell',
... authors='Martelli Ravenscroft Holden'.split())
>>> get_creators(b2)
['Martelli', 'Ravenscroft', 'Holden']
>>> get_creators({'type': 'book', 'pages': 770})
Traceback (most recent call last):
...
ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}
>>> get_creators('Spam, spam, spam')
Traceback (most recent call last):
...
ValueError: Invalid record: 'Spam, spam, spam'

Observe que a ordem das chaves nos padrões é irrelevante, mesmo se o sujeito for um OrderedDict como b2 .

Diferente de patterns de sequência, patterns de mapeamento funcionam com matches parciais. Nos doctests, os sujeitos
b1 e b2 incluem uma chave 'title' , que não aparece em nenhum padrão 'book' , mas mesmo assim casam.

Não há necessidade de usar **extra para casar pares chave-valor adicionais, mas se você quiser capturá-los como um
dict , pode prefixar uma variável com ** . Ela precisa ser a última do padrão, e **_ é proibido, pois seria
redundante. Um exemplo simples:

PYCON
>>> food = dict(category='ice cream', flavor='vanilla', cost=199)
>>> match food:
... case {'category': 'ice cream', **details}:
... print(f'Ice cream details: {details}')
...
Ice cream details: {'flavor': 'vanilla', 'cost': 199}

Na seção Seção 3.5, vamos estudar o defaultdict e outros mapeamentos onde buscas com chaves via __getitem__
(isto é, d[chave] ) funcionam porque itens ausentes são criados na hora. No contexto do pattern matching, um match é
bem sucedido apenas se o sujeito já possui as chaves necessárias no início do bloco match .

O tratamento automático de chaves ausentes não é acionado porque o pattern matching sempre usa
👉 DICA o método d.get(key, sentinel) —onde o sentinel default é um marcador com valor especial,
que não pode aparecer nos dados do usuário.

Vistas a sintaxe e a estrutura, vamos estudar a API dos mapeamentos.

3.4. A API padrão dos tipos de mapeamentos


O módulo collections.abc contém as ABCs Mapping e MutableMapping , descrevendo as interfaces de dict e de
tipos similares. Veja a Figura 1.

A maior utilidade dessas ABCs é documentar e formalizar as interfaces padrão para os mapeamentos, e servir e critério
para testes com isinstance em código que precise suportar mapeamentos de forma geral:
PYCON
>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
>>> isinstance(my_dict, abc.MutableMapping)
True

Usar isinstance com uma ABC é muitas vezes melhor que verificar se um argumento de função é
👉 DICA do tipo concreto dict , porque daí tipos alternativos de mapeamentos podem ser usados. Vamos
discutir isso em detalhes no Capítulo 13.

Figura 1. Diagrama de classe simplificado para MutableMapping e suas superclasses de collections.abc (as setas
de herança apontam das subclasses para as superclasses; nomes em itálico indicam classes e métodos abstratos

Para implementar uma mapeamento personalizado, é mais fácil estender collections.UserDict , ou envolver um
dict por composição, ao invés de criar uma subclasse dessas ABCs. A classe collections.UserDict e todas as
classes concretas de mapeamentos da biblioteca padrão encapsulam o dict básico em suas implementações, que por
sua vez é criado sobre uma tabela de hash. Assim, todas elas compartilham a mesma limitação, as chaves precisam ser
hashable (os valores não precisam ser hashable, só as chaves). Se você precisa de uma recapitulação, a próxima seção
explica isso.

3.4.1. O que é hashable?


Aqui está parte da definição de hashable, adaptado do Glossário do Python
(https://docs.python.org/pt-br/3/glossary.html#term-hashable):

“ Um objeto é hashable se tem um código de hash que nunca muda durante seu ciclo de vida
(precisa ter um método hash()) e pode ser comparado com outros objetos (precisa ter um
método eq()). Objetos hashable que são comparados como iguais devem ter o mesmo código de
hash.[23]

Tipos numéricos e os tipos planos imutáveis str e bytes são todos hashable. Tipos contêineres são hashable se forem
imutáveis e se todos os objetos por eles contidos forem também hashable. Um frozenset é sempre hashable, pois
todos os elementos que ele contém devem ser, por definição, hashable. Uma tuple é hashable apenas se todos os seus
itens também forem. Observe as tuplas tt , tl , and tf :
PYCON
>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110

O código de hash de um objeto pode ser diferente dependendo da versão do Python, da arquitetura da máquina, e pelo
sal acrescentado ao cálculo do hash por razões de segurança.[24] O código de hash de um objeto corretamente
implementado tem a garantia de ser constante apenas dentro de um processo Python.

Tipos definidos pelo usuário são hashble por default, pois seu código de hash é seu id() , e o método __eq__()
herdado da classe objetct apenas compara os IDs dos objetos. Se um objeto implementar seu próprio __eq__() , que
leve em consideração seu estado interno, ele será hashable apenas se seu __hash__() sempre devolver o mesmo
código de hash. Na prática, isso exige que __eq__() e __hash__() levem em conta apenas atributos de instância que
nunca mudem durante a vida do objeto.

Vamos agora revisar a API dos tipos de mapeamento mais comumente usado no Python: dict , defaultdict , e
OrderedDict .

3.4.2. Revisão dos métodos mais comuns dos mapeamentos


A API básica para mapeamentos é muito variada. A Tabela 7 mostra os métodos implementados por dict e por duas
variantes populares: defaultdict e OrderedDict , ambas classes definidas no módulo collections .

Tabela 7. Métodos do tipos de mapeamento dict , collections.defaultdict , e collections.OrderedDict


(métodos comuns de object omitidos por concisão); argumentos opcionais então entre […]
dict defaultdict OrderedDict

d.clear() ● ● ● Remove todos os


itens.

d.__contains__(k) ● ● ● k in d.

d.copy() ● ● ● Cópia rasa.

d.__copy__() ● Suporte a
copy.copy(d).

d.default_factory ● Chamável invocado


por __missing__
para definir valores
ausentes.[25]

d.__delitem__(k) ● ● ● del d[k] —remove


item com chave k
dict defaultdict OrderedDict

d.fromkeys(it, ● ● ● Novo mapeamento


[initial]) com chaves no
iterável it , com um
valor inicial
opcional (o default é
None ).

d.get(k, ● ● ● Obtém item com


[default]) chave k , devolve
default ou None
se k não existir.

d.__getitem__(k) ● ● ● d[k]—obtém item


com chave k.

d.items() ● ● ● Obtém uma view dos


itens—pares
(chave, valor) .

d.__iter__() ● ● ● Obtém iterador das


chaves.

d.keys() ● ● ● Obtém view das


chaves.

d.__len__() ● ● ● len(d)—número de
itens.

d.__missing__(k) ● Chamado quando


__getitem__ não
consegue encontrar
a chave.

d.move_to_end(k, ● Move k para


[last]) primeira ou última
posição ( last é
True por default).

d.__or__(other) ● ● ● Suporte a d1 | d2
para criar um novo
`dict`, fundindo d1 e
d2 (Python ≥ 3.9).

d.__ior__(other) ● ● ● Suporte a d1 |= d2
para atualizar d1
com d2 (Python ≥
3.9).
dict defaultdict OrderedDict

d.pop(k, ● ● ● Remove e devolve


[default]) valor em k , ou
default ou None ,
se k não existir.

d.popitem() ● ● ● Remove e devolve,


na forma (chave,
valor) , o último
item inserido.[26]

d.__reversed__() ● ● ● Suporte a
reverse(d)—
devolve um iterador
de chaves, da última
para a primeira a
serem inseridas.

d.__ror__(other) ● ● ● Suporte a other |


dd —operador de
união invertido
(Python ≥ 3.9)[27]

d.setdefault(k, ● ● ● Se k in d , devolve
[default]) d[k] ; senão, atribui
d[k] = default e
devolve isso.

d.__setitem__(k, ● ● ● d[k] = v —coloca


v) v em k

d.update(m, ● ● ● Atualiza d com


[**kwargs]) itens de um
mapeamento ou
iterável de pares
(chave, valor) .

d.values() ● ● ● Obtém uma view dos


valores.

A forma como d.update(m) lida com seu primeiro argumento, m , é um excelente exemplo de duck typing (tipagem
pato): ele primeiro verifica se m possui um método keys e, em caso afirmativo, assume que m é um mapeamento.
Caso contrário, update() reverte para uma iteração sobre m , presumindo que seus item são pares (chave, valor) .
O construtor da maioria dos mapeamentos do Python usa internamente a lógica de update() , o que quer dizer que
eles podem ser inicializados por outros mapeamentos ou a partir de qualquer objeto iterável que produza pares
(chave, valor) .

Um método sutil dos mapeamentos é setdefault() . Ele evita buscas redundantes de chaves quando precisamos
atualizar o valor em um item no mesmo lugar. A próxima seção mostra como ele pode ser usado.

3.4.3. Inserindo ou atualizando valores mutáveis


Alinhada à filosofia de falhar rápido do Python, a consulta a um dict com d[k] gera um erro quando k não é uma
chave existente. Pythonistas sabem que d.get(k, default) é uma alternativa a d[k] sempre que receber um valor
default é mais conveniente que tratar um KeyError . Entretanto, se você está buscando um valor mutável e quer
atualizá-lo, há um jeito melhor.

Considere um script para indexar texto, produzindo um mapeamento no qual cada chave é uma palavra, e o valor é
uma lista das posições onde aquela palavra ocorre, como mostrado no Exemplo 3.

Exemplo 3. Saída parcial do Exemplo 4 processando o texto "Zen of Python"; cada linha mostra uma palavra e uma lista
de ocorrências na forma de pares ( line_number , column_number ) (número da linha, _número da coluna)

BASH
$ python3 index0.py zen.txt
a [(19, 48), (20, 53)]
Although [(11, 1), (16, 1), (18, 1)]
ambiguity [(14, 16)]
and [(15, 23)]
are [(21, 12)]
aren [(10, 15)]
at [(16, 38)]
bad [(19, 50)]
be [(15, 14), (16, 27), (20, 50)]
beats [(11, 23)]
Beautiful [(3, 1)]
better [(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8), (18, 25)]
...

O Exemplo 4 é um script aquém do ideal, para mostrar um caso onde dict.get não é a melhor maneira de lidar com
uma chave ausente. Ele foi adaptado de um exemplo de Alex Martelli.[28]

Exemplo 4. index0.py usa dict.get para obter e atualizar uma lista de ocorrências de palavras de um índice (uma
solução melhor é apresentada no Exemplo 5)

PY
"""Build an index mapping word -> list of occurrences"""

import re
import sys

WORD_RE = re.compile(r'\w+')

index = {}
with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start() + 1
location = (line_no, column_no)
# this is ugly; coded like this to make a point
occurrences = index.get(word, []) # (1)
occurrences.append(location) # (2)
index[word] = occurrences # (3)

# display in alphabetical order


for word in sorted(index, key=str.upper): # (4)
print(word, index[word])

1. Obtém a lista de ocorrências de word , ou [] se a palavra não for encontrada.


2. Acrescenta uma nova localização a occurrences .
3. Coloca a occurrences modificada no dict index ; isso exige uma segunda busca em index .

4. Não estou chamando str.upper no argumento key= de sorted , apenas passando uma referência àquele
método, para que a função sorted possa usá-lo para normalizar as palavras antes de ordená-las.[29]
As três linhas tratando de occurrences no Exemplo 4 podem ser substituídas por uma única linha usando
dict.setdefault . O Exemplo 5 fica mais próximo do código apresentado por Alex Martelli.

Exemplo 5. index.py usa dict.setdefault para obter e atualizar uma lista de ocorrências de uma palavra em uma
única linha de código; compare com o Exemplo 4

PY
"""Build an index mapping word -> list of occurrences"""

import re
import sys

WORD_RE = re.compile(r'\w+')

index = {}
with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start() + 1
location = (line_no, column_no)
index.setdefault(word, []).append(location) # (1)

# display in alphabetical order


for word in sorted(index, key=str.upper):
print(word, index[word])

1. Obtém a lista de ocorrências de word , ou a define como [] , se não for encontrada; setdefault devolve o valor,
então ele pode ser atualizado sem uma segunda busca.

Em outras palavras, o resultado final desta linha…​

PYTHON3
my_dict.setdefault(key, []).append(new_value)

…​é o mesmo que executar…​

PYTHON3
if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)

…​exceto que este último trecho de código executa pelo menos duas buscas por key —três se a chave não for
encontrada—enquanto setdefault faz tudo isso com uma única busca.

Uma questão relacionada, o tratamento de chaves ausentes em qualquer busca (e não apenas para inserção de
valores), é o assunto da próxima seção.

3.5. Tratamento automático de chaves ausentes


Algumas vezes é conveniente que os mapeamentos devolvam algum valor padronizado quando se busca por uma
chave ausente. Há duas abordagem principais para esse fim: uma é usar um defaultdict em vez de um dict
simples. A outra é criar uma subclasse de dict ou de qualquer outro tipo de mapeamento e acrescentar um método
__missing__ . Vamos ver as duas soluções a seguir.
3.5.1. defaultdict: outra perspectiva sobre as chaves ausentes
Uma instância de collections.defaultdict cria itens com um valor default sob demanda, sempre que uma chave
ausente é buscada usando a sintaxe d[k] . O Exemplo 6 usa defaultdict para fornecer outra solução elegante para o
índice de palavras do Exemplo 5.

Funciona assim: ao instanciar um defaultdict , você fornece um chamável que produz um valor default sempre que
__getitem__ recebe uma chave inexistente como argumento.

Por exemplo, dado um defaultdict criado por dd = defaultdict(list) , se 'new-key' não estiver em dd , a
expressão dd['new-key'] segue os seguintes passos:

1. Chama list() para criar uma nova lista.


2. Insere a lista em dd usando 'new-key' como chave.
3. Devolve uma referência para aquela lista.

O chamável que produz os valores default é mantido em um atributo de instância chamado default_factory .

Exemplo 6. index_default.py: usando um defaultdict em vez do método setdefault

PY
"""Build an index mapping word -> list of occurrences"""

import collections
import re
import sys

WORD_RE = re.compile(r'\w+')

index = collections.defaultdict(list) # (1)


with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start() + 1
location = (line_no, column_no)
index[word].append(location) # (2)

# display in alphabetical order


for word in sorted(index, key=str.upper):
print(word, index[word])

1. Cria um defaultdict com o construtor de list como default_factory .

2. Se word não está inicialmente no index , o default_factory é chamado para produzir o valor ausente, que
neste caso é uma list vazia, que então é atribuída a index[word] e devolvida, de forma que a operação
.append(location) é sempre bem sucedida.

Se nenhum default_factory é fornecido, o KeyError usual é gerado para chaves ausente.

O default_factory de um defaultdict só é invocado para fornecer valores default para

⚠️ AVISO chamadas a __getitem__ , não para outros métodos. Por exemplo, se dd é um defaultdict e
uma chave ausente, dd[k] chamará default_factory para criar um valor default, mas
k

dd.get(k) vai devolver None e k in dd é False .


O mecanismo que faz defaultdict funcionar, chamando default_factory , é o método especial __missing__ , um
recurso discutido a seguir.

3.5.2. O método __missing__


Por trás da forma como os mapeamentos lidam com chaves ausentes está o método muito apropriadamente chamado
__missing__ .[30]. Esse método não é definido na classe base dict , mas dict está ciente de sua possibilidade: se você
criar uma subclasse de dict e incluir um método __missing__ , o dict.__getitem__ padrão vai chamar seu
método sempre que uma chave não for encontrada, em vez de gerar um KeyError .

Suponha que você queira um mapeamento onde as chaves são convertidas para str quando são procuradas. Um caso
de uso concreto seria uma biblioteca para dispositivos IoT (Internet of Things, Internet das Coisas)[31], onde uma placa
programável com portas genéricas programáveis (por exemplo, uma Raspberry Pi ou uma Arduino) é representada por
uma classe "Placa" com um atributo minha_placa.portas , que é uma mapeamento dos identificadores das portas
físicas para objetos de software portas. O identificador da porta física pode ser um número ou uma string como "A0"
ou "P9_12" . Por consistência, é desejável que todas as chaves em placa.portas seja strings, mas também é
conveniente buscar uma porta por número, como em meu-arduino.porta[13] , para evitar que iniciantes tropecem
quando quiserem fazer piscar o LED na porta 13 de seus Arduinos. O Exemplo 7 mostra como tal mapeamento
funcionaria.

Exemplo 7. Ao buscar por uma chave não-string, StrKeyDict0 a converte para str quando ela não é encontrada

PY
Tests for item retrieval using `d[key]` notation::

>>> d = StrKeyDict0([('2', 'two'), ('4', 'four')])


>>> d['2']
'two'
>>> d[4]
'four'
>>> d[1]
Traceback (most recent call last):
...
KeyError: '1'

Tests for item retrieval using `d.get(key)` notation::

>>> d.get('2')
'two'
>>> d.get(4)
'four'
>>> d.get(1, 'N/A')
'N/A'

Tests for the `in` operator::

>>> 2 in d
True
>>> 1 in d
False

O Exemplo 8 implementa a classe StrKeyDict0 , que passa nos doctests acima.

Uma forma melhor de criar uma mapeamento definido pelo usuário é criar uma subclasse de

👉 DICA collections.UserDict em vez de dict (como faremos no Exemplo 9). Aqui criamos uma
sibclasse de dict apenas para mostrar que __missing__ é suportado pelo método embutido
dict.__getitem__ .
Exemplo 8. StrKeyDict0 converte chaves não-string para string no momento da consulta (vejas os testes no Exemplo
7)

PY
class StrKeyDict0(dict): # (1)

def __missing__(self, key):


if isinstance(key, str): # (2)
raise KeyError(key)
return self[str(key)] # (3)

def get(self, key, default=None):


try:
return self[key] # (4)
except KeyError:
return default # (5)

def __contains__(self, key):


return key in self.keys() or str(key) in self.keys() # (6)

1. StrKeyDict0 herda de dict .

2. Verifica se key já é uma str . Se é, e está ausente, gera um KeyError .

3. Cria uma str de key e a procura.


4. O método get delega para __getitem__ usando a notação self[key] ; isso dá oportunidade para nosso
__missing__ agir.
5. Se um KeyError foi gerado, __missing__ já falhou, então devolvemos o default .

6. Procura pela chave não-modificada (a instância pode conter chaves não- str ), depois por uma str criada a partir
da chave.

Considere por um momento o motivo do teste isinstance(key, str) ser necessário na implementação de
__missing__ .

Sem aquele teste, nosso método __missing__ funcionaria bem com qualquer chave k — str ou não—sempre que
str(k) produzisse uma chave existente. Mas se str(k) não for uma chave existente, teríamos uma recursão infinita.
Na última linha de __missing__ , self[str(key)] chamaria __getitem__ , passando aquela chave str , e
__getitem__ , por sua vez, chamaria __missing__ novamente.

O método __contains__ também é necessário para que o comportamento nesse exemplo seja consistente, pois a
operação k in d o chama, mas o método herdado de dict não invoca __missing__ com chaves ausentes. Há um
detalhe sutil em nossa implementação de __contains__ : não verificamos a existência da chave da forma pythônica
normal— k in d —porque str(key) in self chamaria __contains__ recursivamente. Evitamos isso procurando a
chave explicitamente em self.keys() .

Uma busca como k in my_dict.keys() é eficiente em Python 3 mesmo para mapeamentos muito grandes, porque
dict.keys() devolve uma view, que é similar a um set, como veremos na seção Seção 3.12. Entretanto, lembre-se que
k in my_dict faz o mesmo trabalho, e é mais rápido porque evita a busca nos atributos para encontrar o método
.keys .

Eu tinha uma razão específica para usar self.keys() no método __contains__ do Exemplo 8. A verificação da
chave não-modificada key in self.keys() é necessária por correção, pois StrKeyDict0 não obriga todas as chaves
no dicionário a serem do tipo str . Nosso único objetivo com esse exemplo simples foi fazer a busca "mais amigável", e
não forçar tipos.
Classes definidas pelo usuário derivadas de mapeamentos da biblioteca padrão podem ou não usar
⚠️ AVISO __missing__ como alternativa em sua implementação de __getitem__ , get , ou __contains__ ,
como explicado na próxima seção.

3.5.3. O uso inconsistente de __missing__ na biblioteca padrão


Considere os seguintes cenários, e como eles afetam a busca de chaves ausentes:

subclasse de dict
Uma subclasse de dict que implemente apenas __missing__ e nenhum outro método. Nesse caso, __missing__
pode ser chamado apenas em d[k] , que usará o __getitem__ herdado de dict .

subclasse de collections.UserDict
Da mesma forma, uma subclasse de UserDict que implemente apenas __missing__ e nenhum outro método. O
método get herdado de UserDict chama __getitem__ . Isso significa que __missing__ pode ser chamado para
tratar de consultas com d[k] e com d.get(k) .

subclasse de abc.Mapping com o __getitem__ mais simples possível


Uma subclasse mínima de abc.Mapping , implementando __missing__ e os métodos abstratos obrigatórios,
incluindo uma implementação de __getitem__ que não chama __missing__ . O método __missing__ nunca é
acionado nessa classe.

subclasse de abc.Mapping com __getitem__ chamando __missing__


Uma subclasse mínima de abc.Mapping , implementando __missing__ e os métodos abstratos obrigatórios,
incluindo uma implementação de __getitem__ que chama __missing__ . O método __missing__ é acionado
nessa classe para consultas por chaves ausentes feitas com d[k] , d.get(k) , e k in d .

Veja missing.py (https://fpy.li/3-7) no repositório de exemplos de código para demonstrações dos cenários descritos acima.

Os quatro cenários que acabo de descrever supõe implementações mínimas. Se a sua subclasse implementa
__getitem__ , get , e __contains__ , então você pode ou não fazer tais métodos usarem __missing__ , dependendo
de suas necessidades. O ponto aqui é mostrar que é preciso ter cuidado ao criar subclasses dos mapeamentos da
biblioteca padrão para usar __missing__ , porque as classes base suportam comportamentos default diferentes. Não
se esqueça que o comportamento de setdefault e update também é afetado pela consulta de chaves. E por fim,
dependendo da lógica de seu __missing__ , pode ser necessário implementar uma lógica especial em __setitem__ ,
para evitar inconsistências ou comportamentos surpreeendentes. Veremos um exemplo disso na seção Seção 3.6.5.

Até aqui tratamos dos tipos de mapeamentos dict e defaultdict , mas a biblioteca padrão traz outras
implementações de mapeamentos, que discutiremos a seguir.

3.6. Variações de dict


Nessa seção falaremos brevemente sobre os tipos de mapeamentos incluídos na biblioteca padrão diferentes de
defaultdict , já visto na seção Seção 3.5.1.

3.6.1. collections.OrderedDict
Agora que o dict embutido também mantém as chaves ordenadas (desde o Python 3.6), o motivo mais comum para
usar OrderedDict é escrever código compatível com versões anteriores do Python. Dito isso, a documentação lista
algumas diferenças entre dict e OrderedDict que ainda persistem e que cito aqui—apenas reordenando os itens
conforme sua relevância no uso diário:

A operação de igualdade para OrderedDict verifica a igualdade da ordenação.


O método popitem() de OrderedDict tem uma assinatura diferente, que aceita um argumento opcional
especificando qual item será devolvido.
OrderedDict tem um método move_to_end() , que reposiciona de um elemento para uma ponta do dicionário de
forma eficiente.
O dict comum foi projetado para ser muito bom nas operações de mapeamento. Monitorar a ordem de inserção
era uma preocupação secundária.
OrderedDict foi projetado para ser bom em operações de reordenamento. Eficiência espacial, velocidade de
iteração e o desempenho de operações de atualização eram preocupações secundárias.
Em termos do algoritmo, um OrderedDict lida melhor que um dict com operações frequentes de reordenamento.
Isso o torna adequado para monitorar acessos recentes (em um cache LRU[32], por exemplo).

3.6.2. collections.ChainMap
Uma instância de ChainMap mantém uma lista de mapeamentos que podem ser consultados como se fossem um
mapeamento único. A busca é realizada em cada mapa incluído, na ordem em que eles aparecem na chamada ao
construtor, e é bem sucedida assim que a chave é encontrada em um daqueles mapeamentos. Por exemplo:

PYCON
>>> d1 = dict(a=1, b=3)
>>> d2 = dict(a=2, b=4, c=6)
>>> from collections import ChainMap
>>> chain = ChainMap(d1, d2)
>>> chain['a']
1
>>> chain['c']
6

A instância de ChainMap não cria cópias dos mapeamentos, mantém referências para eles. Atualizações ou inserções a
um ChainMap afetam apenas o primeiro mapeamento passado. Continuando do exemplo anterior:

PYCON
>>> chain['c'] = -1
>>> d1
{'a': 1, 'b': 3, 'c': -1}
>>> d2
{'a': 2, 'b': 4, 'c': 6}

Um ChainMap é útil na implementação de linguagens com escopos aninhados, onde cada mapeamento representa um
contexto de escopo, desde o escopo aninhado mais interno até o mais externo. A seção "Objetos ChainMap"
(https://docs.python.org/pt-br/3/library/collections.html#collections.ChainMap), na documentação de collections , apresenta
vários exemplos do uso de Chainmap , incluindo esse trecho inspirado nas regras básicas de consulta de variáveis no
Python:

PYTHON3
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))

O Exemplo 14 mostra uma subclasse de ChainMap usada para implementar um interpretador parcial da linguagem de
programação Scheme.

3.6.3. collections.Counter
Um mapeamento que mantém uma contagem inteira para cada chave. Atualizar uma chave existente adiciona à sua
contagem. Isso pode ser usado para contar instâncias de objetos hashable ou como um multiset ("conjunto múltiplo"),
discutido adiante nessa seção. Counter implementa os operadores + e - para combinar contagens, e outros métodos
úteis tal como o most_common([n]) , que devolve uma lista ordenada de tuplas com os n itens mais comuns e suas
contagens; veja a documentação (https://docs.python.org/pt-br/3/library/collections.html#collections.Counter).
Aqui temos um Counter usado para contar as letras em palavras:

PYCON
>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.update('aaaaazzz')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(3)
[('a', 10), ('z', 3), ('b', 2)]

Observe que as chaves 'b' e 'r' estão empatadas em terceiro lugar, mas ct.most_common(3) mostra apenas três
contagens.

Para usar collections.Counter como um conjunto múltiplo, trate cada chave como um elemento de um conjunto, e
a contagem será o número de ocorrências daquele elemento no conjunto.

3.6.4. shelve.Shelf
O módulo shelve na biblioteca padrão fornece armazenamento persistente a um mapeamento de chaves em formato
string para objetos Python serializados no formato binário pickle . O nome curioso, shelve , faz sentido quando você
percebe que potes de pickle são armazenadas em prateleiras.[33]

A função de módulo shelve.open devolve uma instância de shelve.Shelf —um banco de dados DBM simples de
chave-valor, baseado no módulo dbm , com as seguintes características:

shelve.Shelfé uma subclasse de abc.MutableMapping , então fornece os métodos essenciais esperados de um


tipo mapeamento.
Além disso, shelve.Shelf fornece alguns outros métodos de gerenciamento de E/S, como sync e close .

Uma instância de Shelf é um gerenciador de contexto, então é possível usar um bloco with para garantir que ela
seja fechada após o uso.
Chaves e valores são salvos sempre que um novo valor é atribuído a uma chave.
As chaves devem ser strings.
Os valores devem ser objetos que o módulo pickle possa serializar.

A documentação para os módulos shelve (https://docs.python.org/pt-br/3/library/shelve.html), dbm


(https://docs.python.org/pt-br/3/library/dbm.html) (EN), e pickle (https://docs.python.org/pt-br/3/library/pickle.html) traz mais
detalhes e também algumas ressalvas.

O pickle do Python é fácil de usar nos caso mais simples, mas tem vários inconvenientes. Leia o

⚠️ AVISO "Pickle’s nine flaws" (https://fpy.li/3-13), de Ned Batchelder, antes de adotar qualquer solução
envolvendo pickle . Em seu post, Ned menciona outros formatos de serialização que podem ser
considerados como alternativas.

As classes OrderedDict , ChainMap , Counter , e Shelf podem ser usadas diretamente, mas também podem ser
personalizadas por subclasses. UserDict , por outro lado, foi planejada apenas como uma classe base a ser estendida.

3.6.5. Criando subclasses de UserDict em vez de dict


É melhor criar um novo tipo de mapeamento estendendo collections.UserDict em vez de dict . Percebemos isso
quando tentamos estender nosso StrKeyDict0 do Exemplo 8 para assegurar que qualquer chave adicionada ao
mapeamento seja armazenada como str .

A principal razão pela qual é melhor criar uma subclasse de UserDict em vez de dict é que o tipo embutido tem
alguns atalhos de implementação, que acabam nos obrigando a sobrepor métodos que poderíamos apenas herdar de
UserDict sem maiores problemas.[34]

Observe que UserDict não herda de dict , mas usa uma composição: a classe tem uma instância interna de dict ,
chamada data , que mantém os itens propriamente ditos. Isso evita recursão indesejada quando escrevemos métodos
especiais, como __setitem__ , e simplifica a programação de __contains__ , quando comparado com o Exemplo 8.

Graças a UserDict , o StrKeyDict (Exemplo 9) é mais conciso que o StrKeyDict0 (Exemplo 8), mais ainda faz
melhor: ele armazena todas as chaves como str , evitando surpresas desagradáveis se a instância for criada ou
atualizada com dados contendo chaves de outros tipos (que não string).

Exemplo 9. StrKeyDict sempre converte chaves que não sejam strings para str na inserção, atualização e busca

PY
import collections

class StrKeyDict(collections.UserDict): # (1)

def __missing__(self, key): # (2)


if isinstance(key, str):
raise KeyError(key)
return self[str(key)]

def __contains__(self, key):


return str(key) in self.data # (3)

def __setitem__(self, key, item):


self.data[str(key)] = item # (4)

1. StrKeyDict estende UserDict .

2. __missing__ é exatamente igual ao do Exemplo 8.


3. __contains__ é mais simples: podemos assumir que todas as chaves armazenadas são str , e podemos operar
sobre self.data em vez de invocar self.keys() , como fizemos em StrKeyDict0 .

4. __setitem__ converte qualquer key para uma str . Esse método é mais fácil de sobrepor quando podemos
delegar para o atributo self.data .

Como UserDict estende abc.MutableMapping , o restante dos métodos que fazem de StrKeyDict uma mapeamento
completo são herdados de UserDict , MutableMapping , ou Mapping . Estes últimos contém vários métodos concretos
úteis, apesar de serem classes base abstratas (ABCs). Os seguinte métodos são dignos de nota:

MutableMapping.update

Esse método poderoso pode ser chamado diretamente, mas também é usado por __init__ para criar a instância a
partir de outros mapeamentos, de iteráveis de pares (chave, valor) , e de argumentos nomeados. Como usa
self[chave] = valor para adicionar itens, ele termina por invocar nossa implementação de __setitem__ .

Mapping.get
No StrKeyDict0(Exemplo 8), precisamos codificar nosso próprio get para devolver os mesmos resultados de
__getitem__ , mas no Exemplo 9 herdamos Mapping.get , que é implementado exatamente como
StrKeyDict0.get (consulte o código-fonte do Python (https://fpy.li/3-14)).

Antoine Pitrou escreveu a PEP 455—​Adding a key-transforming dictionary to collections


(Acrescentando um dicionário com transformação de chaves a collections) (https://fpy.li/pep455) (EN) e
um patch para aperfeiçoar o módulo collections com uma classe TransformDict , que é mais
genérico que StrKeyDict e preserva as chaves como fornecidas antes de aplicar a transformação.
👉 DICA A PEP 455 foi rejeitada em maio de 2015—veja a mensagem de rejeição (https://fpy.li/3-15) (EN) de
Raymond Hettinger. Para experimentar com a TransformDict , extraí o patch de Pitrou do
issue18986 (https://fpy.li/3-16) (EN) para um módulo independente (03-dict-set/transformdict.py
(https://fpy.li/3-17) disponível no repositório de código da segunda edição do Fluent Python
(https://fpy.li/code)).

Sabemos que existem tipos de sequências imutáveis, mas e mapeamentos imutáveis? Bem, não há um tipo real desses
na biblioteca padrão, mas um substituto está disponível. É o que vem a seguir.

3.7. Mapeamentos imutáveis


Os tipos de mapeamentos disponíveis na biblioteca padrão são todos mutáveis, mas pode ser necessário impedir que os
usuários mudem um mapeamento por acidente. Um caso de uso concreto pode ser encontrado, novamente, em uma
biblioteca de programação de hardware como a Pingo, mencionada na seção Seção 3.5.2: o mapeamento board.pins
representa as portas de GPIO (General Purpose Input/Output, Entrada/Saída Genérica) em um dispositivo. Dessa forma,
seria útil evitar atualizações descuidadas de board.pins , pois o hardware não pode ser modificado via software:
qualquer mudança no mapeamento o tornaria inconsistente com a realidade física do dispositivo.

O módulo types oferece uma classe invólucro (wrapper) chamada MappingProxyType que, dado um mapeamento,
devolve uma instância de mappingproxy , que é um proxy somente para leitura (mas dinâmico) do mapeamento
original. Isso significa que atualizações ao mapeamento original são refletidas no mappingproxy , mas nenhuma
mudança pode ser feita através desse último. Veja uma breve demonstração no Exemplo 10.

Exemplo 10. MappingProxyType cria uma instância somente de leitura de mappingproxy a partir de um dict

PYCON
>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1] (1)
'A'
>>> d_proxy[2] = 'x' (2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d[2] = 'B'
>>> d_proxy (3)
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>

1. Os items em d podem ser vistos através de d_proxy .

2. Não é possível fazer modificações através de d_proxy .


3. d_proxy é dinâmica: qualquer mudança em d é refletida ali.
Isso pode ser usado assim na prática, no cenário da programação de hardware: o construtor em uma subcalsse
concreta Board preencheria um mapeamento privado com os objetos porta, e o exporia aos clientes da API via um
atributo público .portas , implementado como um mappingproxy . Dessa forma os clientes não poderiam acrescentar,
remover ou modificar as portas por acidente.

A seguir veremos views—que permitem operações de alto desempenho em um dict , sem cópias desnecessárias dos
dados.

3.8. Views de dicionários


Os métodos de instância de dict .keys() , .values() , e .items() devolvem instâncias de classes chamadas
dict_keys , dict_values , e dict_items , respectivamente. Essas views de dicionário são projeções somente para
leitura de estruturas de dados internas usadas na implemetação de dict . Elas evitam o uso de memória adicional dos
métodos equivalentes no Python 2, que devolviam listas, duplicando dados já presentes no dict alvo. E também
substituem os métodos antigos que devolviam iteradores.

O Exemplo 11 mostra algumas operações básicas suportadas por todas as views de dicionários.

Exemplo 11. O método .values() devolve uma view dos valores em um dict

PYCON
>>> d = dict(a=10, b=20, c=30)
>>> values = d.values()
>>> values
dict_values([10, 20, 30]) (1)
>>> len(values) (2)
3
>>> list(values) (3)
[10, 20, 30]
>>> reversed(values) (4)
<dict_reversevalueiterator object at 0x10e9e7310>
>>> values[0] (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'dict_values' object is not subscriptable

1. O repr de um objeto view mostra seu conteúdo.


2. Podemos consultar a len de uma view.
3. Views são iteráveis, então é fácil criar listas a partir delas.
4. Views implementam __reversed__ , devolvendo um iterador personalizado.

5. Não é possível usar [] para obter itens individuais de uma view.

Um objeto view é um proxy dinâmico. Se o dict fonte é atualizado, as mudanças podem ser vistas imediatamente
através de uma view existente. Continuando do Exemplo 11:

PYCON
>>> d['z'] = 99
>>> d
{'a': 10, 'b': 20, 'c': 30, 'z': 99}
>>> values
dict_values([10, 20, 30, 99])
As classes dict_keys , dict_values , e dict_items são internas: elas não estão disponíveis via __builtins__ ou
qualquer módulo da biblioteca padrão, e mesmo que você obtenha uma referência para uma delas, não pode usar essa
referência para criar uma view do zero no seu código Python:

PYCON
>>> values_class = type({}.values())
>>> v = values_class()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create 'dict_values' instances

A classe dict_values é a view de dicionário mais simples—ela implementa apenas os métodos especiais __len__ ,
__iter__ , e __reversed__ . Além desses métodos, dict_keys e dict_items implementam vários métodos dos sets,
quase tantos quanto a classe frozenset . Após vermos os conjuntos (sets), teremos mais a dizer sobre dict_keys e
dict_items , na seção Seção 3.12.

Agora vamos ver algumas regras e dicas baseadas na forma como dict é implementado debaixo dos panos.

3.9. Consequências práticas da forma como dict funciona


A implementação da tabela de hash do dict do Python é muito eficiente, mas é importante entender os efeitos
práticos desse design:

Chaves devem ser objetos hashable. Eles devem implementar métodos __hash__ e __eq__ apropriados, como
descrito na seção Seção 3.4.1.
O acesso aos itens através da chave é muito rápido. Mesmo que um dict tenha milhões de chaves, o Python pode
localizar uma chave diretamente, computando o código hash da chave e derivando um deslocamento do índice na
tabela de hash, com um possível ônus de um pequeno número de tentativas até encontrar a entrada
correspondente.
A ordenação das chaves é preservada, como efeito colateral de um layout de memória mais compacto para dict no
CPython 3.6, que se tornou um recurso oficial da linguagem no 3.7.
Apesar de seu novo layout compacto, os dicts apresentam, inevitavelmente, um uso adicional significativo de
memória. A estrutura de dados interna mais compacta para um contêiner seria um array de ponteiros para os itens.
[35] Comparado a isso, uma tabela de hash precisa armazenar mais dados para cada entrada e, para manter a
eficiência, o Python precisa manter pelo menos um terço das linhas da tabela de hash vazias.
Para economizar memória, evite criar atributos de instância fora do método __init__ .

Essa última dica, sobre atributos de instância, é consequência do comportamento default do Python, de armazenar
atributos de instância em um atributo __dict__ especial, que é um dict vinculado a cada instância.[36] Desde a
implementação da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves) (https://fpy.li/pep412)
(EN), no Python 3.3, instâncias de uma classe podem compartilhar uma tabela de hash comum, armazenada com a
classe. Essa tabela de hash comum é compartilhada pelo __dict__ de cada nova instância que, quando __init__
retorna, tenha os mesmos nomes de atributos que a primeira instância daquela classe a ser criada. O __dict__ de
cada instância então pode manter apenas seus próprios valores de atributos como uma simples array de ponteiros.
Acrescentar um atributo de instância após o __init__ obriga o Python a criar uma nova tabela de hash só para o
__dict__ daquela instância (que era o comportamento default antes do Python 3.3). De acordo com a PEP 412, essa
otimização reduz o uso da memória entre 10% e 20% em programas orientados as objetos. Os detalhes das otimizações
do layout compacto e do compartilhamento de chaves são bastante complexos. Para saber mais, por favor leio o texto
"Internals of sets and dicts" (https://fpy.li/hashint) (EN) em fluentpython.com (http://fluentpython.com).

Agora vamos estudar conjuntos(sets).


3.10. Teoria dos conjuntos
Conjuntos não são novidade no Python, mais ainda são um tanto subutilizados. O tipo set e seu irmão imutável,
frozenset , surgiram inicialmente como módulos, na biblioteca padrão do Python 2.3, e foram promovidos a tipos
embutidos no Python 2.6.

✒️ NOTA Nesse livro, uso a palavra "conjunto" para me referir tanto a set quanto a frozenset . Quando falo
especificamente sobre a classe set , uso a fonte de espaçamento constante: set .

Um conjunto é uma coleção de objetos únicos. Um caso de uso básico é a remoção de itens duplicados:

PYCON
>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
>>> set(l)
{'eggs', 'spam', 'bacon'}
>>> list(set(l))
['eggs', 'spam', 'bacon']

Para remover elementos duplicados preservando a ordem da primeira ocorrência de cada item,
você pode fazer isso com um dict simples, assim:

👉 DICA >>> dict.fromkeys(l).keys()


dict_keys(['spam', 'eggs', 'bacon'])
PYCON

>>> list(dict.fromkeys(l).keys())
['spam', 'eggs', 'bacon']

Elementos de um conjunto devem ser hashable. O tipo set não é hashable, então não é possível criar um set com
instâncias aninhadas de set . Mas frozenset é hashable, então você pode ter elementos frozenset dentro de um
set .

Além de impor a unicidade de cada elemento, os tipos conjunto implementam muitas operações entre conjuntos como
operadores infixos. Assim, dados dois conjuntos a e b , a | b devolve sua união, a & b calcula a intersecção, a - b
a diferença, e a ^ b a diferença simétrica. Usadas com sabedoria, as operações de conjuntos podem reduzir tanto a
contagem de linhas quanto o tempo de execução de programas Python, ao mesmo tempo em que tornam o código mais
legível e mais fácil de entender e debater—pela remoção de loops e da lógica condicional.

Por exemplo, imagine que você tem um grande conjunto de endereços de email (o palheiro —haystack) e um
conjunto menor de endereços (as agulhas —needles), e precisa contar quantas agulhas existem no palheiro .
Graças à interseção de set (o operador & ), é possível programar isso em uma única linha (veja o Exemplo 12).

Exemplo 12. Conta as ocorrências de agulhas (needles) em um palheiro (haystack), ambos do tipo set

PYTHON3
found = len(needles & haystack)

Sem o operador de intersecção, seria necessário escrever o Exemplo 13 para realizar a mesma tarefa executa pelo
Exemplo 12.

Exemplo 13. Conta as ocorrências de agulhas (needles) em um palheiro (haystack); mesmo resultado final do Exemplo
12
PYTHON3
found = 0
for n in needles:
if n in haystack:
found += 1

O Exemplo 12 é um pouco mais rápido que o Exemplo 13. Por outros lado, o Exemplo 13 funciona para quaisquer
objetos iteráveis needles e haystack , enquanto o Exemplo 12 exige que ambos sejam conjuntos. Mas se você não
tem conjuntos à mão, pode sempre criá-los na hora, como mostra o Exemplo 14.

Exemplo 14. Conta as ocorrências de agulhas (needles) em um palheiro (haystack); essas linhas funcionam para
qualquer tipo iterável

PYTHON3
found = len(set(needles) & set(haystack))

# another way:
found = len(set(needles).intersection(haystack))

Claro, há o custo extra envolvido na criação dos conjuntos no Exemplo 14, mas se ou as needles ou o haystack já
forem um set , a alternativa no Exemplo 14 pode ser mais barata que o Exemplo 13.

Qualquer dos exemplos acima é capaz de buscar 1000 elementos em um haystack de 10,000,000 de itens em cerca de
0,3 milisegundos—isso é próximo de 0,3 microsegundos por elemento.

Além do teste de existência extremamente rápido (graças à tabela de hash subjacente), os tipos embutidos set e
frozenset oferecem uma rica API para criar novos conjuntos ou, no caso de set , para modificar conjuntos
existentes. Vamos discutir essas operações em breve, após uma observação sobre sintaxe.

3.10.1. Sets literais


A sintaxe de literais set — {1} , {1, 2} , etc.—parece exatamente igual à notação matemática, mas tem uma
importante exceção: não notação literal para o set vazio, então precisamos nos lembrar de escrever set() .

Peculiaridade sintática
⚠️ AVISO Não esqueça que, para criar um set vazio, é preciso usar o construtor sem argumentos: set() . Se
você escrever {} , vai criar um dict vazio—isso não mudou no Python 3.

No Python 3, a representação padrão dos sets como strings sempre usa a notação {…} , exceto para o conjunto vazio:

PYCON
>>> s = {1}
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s.pop()
1
>>> s
set()

A sintaxe do setliteral, como {1, 2, 3} , é mais rápida e mais legível que uma chamada ao construtor (por exemplo,
set([1, 2, 3]) ). Essa última forma é mais lenta porque, para avaliá-la, o Python precisa buscar o nome set para
obter seu construtor, daí criar uma lista e, finalmente, passá-la para o construtor. Por outro lado, para processar um
literal como {1, 2, 3} , o Python roda um bytecode especializado, BUILD_SET .[37]
Não há sintaxe especial para representar literais frozenset —eles devem ser criados chamando seu construtor. Sua
representação padrão como string no Python 3 se parece com uma chamada ao construtor de frozenset . Observe a
saída na sessão de console a seguir:

PYCON
>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

E por falar em sintaxe, a ideia das listcomps foi adaptada para criar conjuntos também.

3.10.2. Compreensões de conjuntos


Compreensões de conjuntos (setcomps) apareceram há bastante tempo, no Python 2.7, junto com as dictcomps que
vimos na seção Seção 3.2.1. O Exemplo 15 mostra procedimento.

Exemplo 15. Cria um conjunto de caracteres Latin-1 que tenham a palavra "SIGN" em seus nomes Unicode

PYCON
>>> from unicodedata import name (1)
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} (2)
{'§', '=', '¢', '#', '¤', '<', '¥', 'µ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}

1. Importa a função name de unicodedata para obter os nomes dos caracteres.


2. Cria um conjunto de caracteres com códigos entre 32 e 255 que contenham a palavra 'SIGN' em seus nomes.

A ordem da saída muda a cada processo Python, devido ao hash "salgado", mencionado na seção Seção 3.4.1.

Questões de sintaxe à parte, vamos considerar agora o comportamento dos conjuntos.

3.11. Consequências práticas da forma de funcionamento dos conjuntos


Os tipos set e frozenset são ambos implementados com um tabela de hash. Isso tem os seguintes efeitos:

Elementos de conjuntos tem que ser objetos hashable. Eles precisam implementar métodos __hash__ e __eq__
adequados, como descrido na seção Seção 3.4.1.
O teste de existência de um elemento é muito eficiente. Um conjunto pode ter milhões de elementos, mas um
elemento pode ser localizado diretamente, computando o código hash da chave e derivando um deslocamento do
índice, com o possível ônus de um pequeno número de tentativas até encontrar a entrada correspondente ou
exaurir a busca.
Conjuntos usam mais memória se comparados aos simples ponteiros de um array para seus elementos—que é uma
estrutura mais compacta, mas também muito mais lenta para buscas se seu tamanho cresce além de uns poucos
elementos.
A ordem dos elementos depende da ordem de inserção, mas não de forma útil ou confiável. Se dois elementos são
diferentes mas tem o mesmo código hash, sua posição depende de qual elemento foi inserido primeiro.
Acrescentar elementos a um conjunto muda a ordem dos elementos existentes. Isso ocorre porque o algoritmo se
torna menos eficiente se a tabela de hash estiver com mais de dois terços de ocupação, então o Python pode ter que
mover e redimensionar a tabela conforme ela cresce. Quando isso acontece, os elementos são reinseridos e sua
ordem relativa pode mudar.

Veja o post "Internals of sets and dicts" (https://fpy.li/hashint) (EN) no fluentpython.com (http://fluentpython.com) para maiores
detalhes.
Agora vamos revisar a vasta seleção de operações oferecidas pelos conjuntos.

3.11.1. Operações de conjuntos


A Figura 2 dá uma visão geral dos métodos disponíveis em conjuntos mutáveis e imutáveis. Muitos deles são métodos
especiais que sobrecarregam operadores, tais como & and >= . A Tabela 8 mostra os operadores matemáticos de
conjuntos que tem operadores ou métodos correspondentes no Python. Note que alguns operadores e métodos
realizam mudanças no mesmo lugar sobre o conjunto alvo (por exemplo, &= , difference_update , etc.). Tais
operações não fazem sentido no mundo ideal dos conjuntos matemáticos, e também não são implementadas em
frozenset .

Os operadores infixos na Tabela 8 exigem que os dois operandos sejam conjuntos, mas todos os
outros métodos recebem um ou mais argumentos iteráveis. Por exemplo, para produzir a união de
quatro coleções, a , b , c , e d , você pode chamar a.union(b, c, d) , onde a precisa ser um set ,
👉 DICA mas b , c , e d podem ser iteráveis de qualquer tipo que produza itens hashable. Para criar um
novo conjunto com a união de quatro iteráveis, desde o Python 3.5 você pode escrever {*a, *b,
*c, *d} ao invés de atualizar um conjunto existente, graças à PEP 448—Additional Unpacking
Generalizations (Generalizações de Desempacotamento Adicionais) (https://fpy.li/pep448).

Figura 2. Diagrama de classes UML simplificado para MutableSet e suas superclasses em collections.abc (nomes
em itálico são classes e métodos abstratos; métodos de operadores reversos foram omitidos por concisão).

Tabela 8. Operações matemáticas com conjuntos: esses métodos ou produzem um novo conjunto ou atualizam o
conjunto alvo no mesmo lugar, se ele for mutável
Math symbol Python operator Method Description

S∩Z s & z s.__and__(z) Intersecção de s e z

z & s s.__rand__(z) Operador & invertido

s.intersection(it, …) Intersecção de s e
todos os conjuntos
construídos a partir de
iteráveis it , etc.

s &= z s.__iand__(z) s atualizado com a


intersecção de s e z
Math symbol Python operator Method Description

s.intersection_update(it, …) s atualizado com a


intersecção de s e
todos os conjuntos
construídos a partir de
iteráveis it , etc.

S∪Z s | z s.__or__(z) União de s e z

z | s s.__ror__(z) | invertido

s.union(it, …) União de s e todos os


conjuntos construídos
a partir de iteráveis
it , etc.

s |= z s.__ior__(z) s atualizado com a


união de s e z

s.update(it, …) s atualizado com a


união de s e todos os
conjuntos construídos
a partir de iteráveis
it , etc.

S\Z s - z s.__sub__(z) Complemeto relativo


ou diferença entre s e
z

z - s s.__rsub__(z) Operador - invertido

s.difference(it, …) Diferença entre s e


todos os conjuntos
construídos a partir de
iteráveis it , etc.

s -= z s.__isub__(z) s atualizado com a


diferença entre s e z

s.difference_update(it, …) s atualizado com a


diferença entre s e
todos os conjuntos
construídos a partir de
iteráveis it , etc.

S∆Z s ^ z s.__xor__(z) Diferença simétrica (o


complemento da
intersecção s & z)
Math symbol Python operator Method Description

z ^ s s.__rxor__(z) Operador ^ invertido

s.symmetric_difference(it) Complemento de s &


set(it)

s ^= z s.__ixor__(z) s atualizado com a


diferença simétrica de
s e z

s.symmetric_difference_update(it, s atualizado com a


…) diferença simétrica de
s e todos os conjuntos
construídos a partir de
iteráveis it , etc.

A Tabela 9 lista predicados de conjuntos: operadores e métodos que devolvem True ou False .

Tabela 9. Operadores e métodos de comparação de conjuntos que devolvem um booleano


Math symbol Python operator Method Description

S∩Z=∅ s.isdisjoint(z) s e z são disjuntos (não


tem elementos em
comum)

e∈S e in s s.__contains__(e) Elemento e é membro de


s

S⊆Z s <= z s.__le__(z) s é um subconjunto do


conjunto z

s.issubset(it) s é um subconjunto do
conjunto criado a partir do
iterável it

S⊂Z s < z s.__lt__(z) s é um subconjunto


próprio[38] do conjunto z

S⊇Z s >= z s.__ge__(z) s é um superconjunto do


conjunto z

s.issuperset(it) s é um superconjunto do
conjunto criado a partir do
iterável it

S⊃Z s > z s.__gt__(z) s é um superconjunto


próprio do conjunto z
Math symbol Python operator Method Description

Além de operadores e métodos derivados da teoria matemática dos conjuntos, os tipos conjunto implementam outros
métodos para tornar seu uso prático, resumidos na Tabela 10.

Tabela 10. Métodos adicionais de conjuntos


set frozenset

s.add(e) ● Adiciona elemento e a s

s.clear() ● Remove todos os


elementos de s

s.copy() ● ● Cópia rasa de s

s.discard(e) ● Remove elemento e de


s , se existir

s.__iter__() ● ● Obtém iterador de s

s.__len__() ● ● len(s)

s.pop() ● Remove e devolve um


elemento de s , gerando
um KeyError se s
estiver vazio

s.remove(e) ● Remove elemento e de


s , gerando um KeyError
se e não existir em s

Isso encerra nossa visão geral dos recursos dos conjuntos. Como prometido na seção Seção 3.8, vamos agora ver como
dois dos tipos de views de dicionários se comportam de forma muito similar a um frozenset .

3.12. Operações de conjuntos em views de dict


A Tabela 11 mostra como os objetos view devolvidos pelos métodos .keys() e .items() de dict são notavelmente
similares a um frozenset .

Tabela 11. Métodos implementados por frozenset , dict_keys , e dict_items

frozenset dict_keys dict_items Description

s.__and__(z) ● ● ● s & z (interseção


de s e z)

s.__rand__(z) ● ● ● operador &


invertido

s.__contains__() ● ● ● e in s

s.copy() ● Cópia rasa de s


frozenset dict_keys dict_items Description

s.difference(it, …) ● Diferença entre s


e os iteráveis it ,
etc.

s.intersection(it, …) ● Intersecção de s e
dos iteráveis it ,
etc.

s.isdisjoint(z) ● ● ● s e z são
disjuntos (não tem
elementos em
comum)

s.issubset(it) ● s é um
subconjunto do
iterável it

s.issuperset(it) ● s é um
superconjunto do
iterável it

s.__iter__() ● ● ● obtém iterador


para s

s.__len__() ● ● ● len(s)

s.__or__(z) ● ● ● s | z (união de
s e z)

s.__ror__() ● ● ● Operador |
invertido

s.__reversed__() ● ● Obtém iterador


para s com a
ordem invertida

s.__rsub__(z) ● ● ● Operador -
invertido

s.__sub__(z) ● ● ● s - z (diferença
entre s e z)

s.symmetric_difference(it) ● Complemento de s
& set(it)

s.union(it, …) ● União de s e dos


iteráveis`it`, etc.

s.__xor__() ● ● ● s ^ z (diferença
simétrica de s e
z)
frozenset dict_keys dict_items Description

s.__rxor__() ● ● ● Operador ^
invertido

Especificamente, dict_keys e dict_items implementam os métodos especiais para suportar as poderosas operações
de conjuntos & (intersecção), | (união), - (diferença), and ^ (diferença simétrica).

Por exemplo, usando & é fácil obter as chaves que aparecem em dois dicionários:

PYCON
>>> d1 = dict(a=1, b=2, c=3, d=4)
>>> d2 = dict(b=20, d=40, e=50)
>>> d1.keys() & d2.keys()
{'b', 'd'}

Observe que o valor devolvido por &é um set . Melhor ainda: os operadores de conjuntos em views de dicionários
são compatíveis com instâncias de set . Veja isso:

PYCON
>>> s = {'a', 'e', 'i'}
>>> d1.keys() & s
{'a'}
>>> d1.keys() | s
{'a', 'c', 'b', 'd', 'i', 'e'}

Uma view obtida de dict_items só funciona como um conjunto se todos os valores naquele dict
são hashable. Tentar executar operações de conjuntos sobre uma view devolvida por dict_items
que contenha valores não-hashable gera um TypeError: unhashable type 'T' , sendo T o tipo do
⚠️ AVISO valor incorreto.

Por outro lado, uma view devolvida por dict_keys sempre pode ser usada como um conjunto, pois
todas as chaves são hashable—por definição.

Usar operações de conjunto com views pode evitar a necessidade de muitos loops e ifs quando seu código precisa
inspecionar o conteúdo de dicionários. Deixe a eficiente implemtação do Python em C trabalhar para você!

Com isso, encerramos esse capítulo.

3.13. Resumo do capítulo


Dicionários são a pedra fundamental do Python. Ao longo dos anos, a sintaxe literal familiar, {k1: v1, k2: v2} , foi
aperfeiçoada para suportar desempacotamento com ** e pattern matching, bem como com compreensões de dict .

Além do dict básico, a biblioteca padrão oferece mapeamentos práticos prontos para serem usados, como o
defaultdict , o ChainMap , e o Counter , todos definidos no módulo collections . Com a nova implementação de
dict , o OrderedDict não é mais tão útil quanto antes, mas deve permanecer na bibliotece padrão para manter a
compatibilidade retroativa—e por suas características específicas ausentes em dict , tal como a capacidade de levar
em consideração o ordenamento das chaves em uma comparação == . Também no módulo collections está o
UserDict , uma classe base fácil de usar na criação de mapeamentos personalizados.

Dois métodos poderosos disponíveis na maioria dos mapeamentos são setdefault e update . O método setdefault
pode atualizar itens que mantenham valores mutáveis—por exemplo, em um dict de valores list —evitando uma
segunda busca pela mesma chave. O método update permite inserir ou sobrescrever itens em massa a partir de
qualquer outro mapeamento, desde iteráveis que forneçam pares (chave, valor) até argumentos nomeados. Os
construtores de mapeamentos também usam update internamente, permitindo que instâncias sejam inicializadas a
partir de outros mapeamentos, de iteráveis e de argumentos nomeados. Desde o Python 3.9 também podemos usar o
operador |= para atualizar uma mapeamento e o operador | para criar um novo mapeamento a partir a união de
dois mapeamentos.
Um gancho elegante na API de mapeamento é o método __missing__ , que permite personalizar o que acontece
quando uma chave não é encontrada ao se usar a sintaxe d[k] syntax, que invoca __getitem__ .

O módulo collections.abc oferece as classes base abstratas Mapping e MutableMapping como interfaces padrão,
muito úteis para checagem de tipo durante a execução. O MappingProxyType , do módulo types , cria uma fachada
imutável para um mapeamento que você precise proteger de modificações acidentais. Existem também ABCs para Set
e MutableSet .

Views de dicionários foram uma grande novidade no Python 3, eliminando o uso desnecessário de memória dos
métodos .keys() , .values() , e .items() do Python 2, que criavam listas duplicando os dados na instância alvo de
dict . Além disso, as classes dict_keys e dict_items suportam os operadores e métodos mais úteis de frozenset .

3.14. Leitura complementar


Na documentação da Biblioteca Padrão do Python, a seção "collections—Tipos de dados de contêineres"
(https://docs.python.org/pt-br/3/library/collections.html) inclui exemplos e receitas práticas para vários tipos de mapeamentos.
O código-fonte do Python para o módulo, Lib/collections/__init__.py, é uma excelente referência para qualquer um que
deseje criar novos tipos de mapeamentos ou entender a lógica dos tipos existentes. O capítulo 1 do Python Cookbook,
3rd ed. (https://fpy.li/pycook3) (O’Reilly), de David Beazley e Brian K. Jones traz 20 receitas práticas e perpicazes usando
estruturas de dados—a maioria mostrando formas inteligentes de usar dict .

Greg Gandenberger defende a continuidade do uso de collections.OrderedDict , com os argumentos de que


"explícito é melhor que implícito," compatibilidade retroativa, e o fato de algumas ferramentas e bilbiotecas
presumirem que a ordenação das chaves de um dict é irrelevante—nesse post: "Python Dictionaries Are Now
Ordered. Keep Using OrderedDict" (Os dicionários do Python agora são ordenados. Continue a usar OrderedDict)
(https://fpy.li/3-18) (EN).

A PEP 3106—​Revamping dict.keys(), .values() and .items() (Renovando dict.keys(), .values() e .items()) (https://fpy.li/pep3106)
(EN) foi onde Guido van Rossum apresentou o recurso de views de dicionário para o Python 3. No resumo, ele afirma
que a ideia veio da Java Collections Framework.

O PyPy (https://fpy.li/3-19) foi o primeiro interpretador Python a implementar a proposta de Raymond Hettinger de dicts
compactos, e eles escreverem em seu blog sobre isso, em "Faster, more memory efficient and more ordered dictionaries
on PyPy" (Dicionários mais rápidos, mais eficientes em termos de memória e mais ordenados no PyPy) (https://fpy.li/3-20)
(EN), reconhecendo que um layout similar foi adotado no PHP 7, como descrito em PHP’s new hashtable
implementation (A nova implementação de tabelas de hash do PHP) (https://fpy.li/3-21) (EN). É sempre muito bom quando
criadores citam trabalhos anteriores de outros.

Na PyCon 2017, Brandon Rhodes apresentou "The Dictionary Even Mightier" (O dicionário, ainda mais poderoso)
(https://fpy.li/3-22) (EN), uma continuação de sua apresentação animada clássica "The Mighty Dictionary" (O poderoso
dicionário) (https://fpy.li/3-23) (EN)—incluindo colisões de hash animadas! Outro vídeo atual mas mais aprofundado sobre
o funcionamento interno do dict do Python é "Modern Dictionaries" (Dicionários modernos) (https://fpy.li/3-24) (EN) de
Raymond Hettinger, onde ele nos diz que após inicialmente fracassar em convencer os desenvolvedores principais do
Python sobre os dicts compactos, ele persuadiu a equipe do PyPy, eles os adotaram, a ideia ganhou força, e finalmente
foi adicionada (https://fpy.li/3-25) ao CPython 3.6 por INADA Naoki. Para saber todos os detalhes, dê uma olhada nos
extensos comentários no código-fonte do CPython para Objects/dictobject.c (https://fpy.li/3-26) (EN) e no documento de
design em Objects/dictnotes.txt (https://fpy.li/3-27) (EN).
A justificativa para a adição de conjuntos ao Python está documentada na PEP 218—​Adding a Built-In Set Object Type
(Adicionando um objeto embutido de tipo conjunto) (https://fpy.li/pep218). Quando a PEP 218 foi aprovada, nenhuma
sintaxe literal especial foi adotada para conjuntos. Os literais set foram criados para o Python 3 e implementados
retroativamente no Python 2.7, assim como as compreensões de dict e set . Na PyCon 2019, eu apresentei "Set
Practice: learning from Python’s set types" (A Prática dos Conjuntos: aprendendo com os tipos conjunto do Python)
(https://fpy.li/3-29) (EN), descrevendo casos de uso de conjuntos em programas reais, falando sobre o design de sua API, e
sobre a implementação da uintset (https://fpy.li/3-30), uma classe de conjunto para elementos inteiros, usando um vetor
de bits ao invés de uma tabela de hash, inspirada por um exemplo do capítulo 6 do excelente The Go Programming
Language (A Linguagem de Programação Go) (http://gopl.io) (EN), de Alan Donovan e Brian Kernighan (Addison-Wesley).

A revista Spectrum, do IEEE, tem um artigo sobre Hans Peter Luhn, um prolífico inventor, que patenteou um conjunto
de cartões interligados que permitiam selecionar receitas de coquetéis a partir dos ingredientes disponíveis, entre
inúmeras outras invenções, incluindo…​tabelas de hash! Veja "Hans Peter Luhn and the Birth of the Hashing
Algorithm" (Hans Peter Luhn e o Nascimento do Algoritmo de Hash) (https://fpy.li/3-31).

Ponto de Vista
Açúcar sintático

Meu amigo Geraldo Cohen certa vez observou que o Python é "simples e correto."

Puristas de linguagens de programação gostam de desprezar a sintaxe como algo desimportante.

“ Syntactic sugar causes cancer of the semicolon. [39]

— Alan Perlis

A sintaxe é a interface de usuário de uma linguagem de programação, então tem muita importância na prática.

Antes de encontrar o Python, fiz um pouco de programação para a web usando Perl e PHP. A sintaxe para
mapeamentos nessas linguagens é muito útil, e eu sinto imensa saudade dela sempre que tenho que usar Java ou
C.

Uma boa sintaxe para mapeamentos literais é muito conveniente para configuração, para implementações
guiadas por tabelas, e para conter dados para prototipagem e testes. Essa foi uma das lições que os projetistas do
Go aprenderam com as linguagens dinâmicas. A falta de uma boa forma de expressar dados estruturados no
código empurrou a comunidade Java a adotar o prolixo e excessivamente complexo XML como formato de dados.

JSON foi proposto como "The Fat-Free Alternative to XML" (A alternativa sem gordura ao XML) (https://fpy.li/3-32) e
se tornou um imenso sucesso, substituindo o XML em vários contextos. Uma sintaxe concisa para listas e
dicionários resulta em um excelente formato para troca de dados.

O PHP e o Ruby imitaram a sintaxe de hash do Perl, usando => para ligar chaves a valores. O JavaScript usa :
como o Python. Por que usar dois caracteres, quando um já é legível o bastante?

O JSON veio do JavaScript, mas por acaso também é quase um subconjunto exato da sintaxe do Python. O JSON é
compatível com o Python, exceto pela grafia dos valores true , false , e null .

Armin Ronacher tuitou (https://fpy.li/3-33) que gosta de brincar com o espaço de nomes global do Python, para
acrescentar apelidos compatíveis com o JSON para o True , o False ,e o None do Python, pois daí ele pode colar
trechos de JSON diretamente no console. Sua ideia básica:
PYCON
>>> true, false, null = True, False, None
>>> fruit = {
... "type": "banana",
... "avg_weight": 123.2,
... "edible_peel": false,
... "species": ["acuminata", "balbisiana", "paradisiaca"],
... "issues": null,
... }
>>> fruit
{'type': 'banana', 'avg_weight': 123.2, 'edible_peel': False,
'species': ['acuminata', 'balbisiana', 'paradisiaca'], 'issues': None}

A sintaxe que todo mundo agora usa para trocar dados é a sintaxe de dict e list do Python. E então temos
uma sintaxe agradável com a conveniência da preservação da ordem de inserção.

Simples e correto.
4. Texto em Unicode versus Bytes
“ Humanos usam texto. Computadores falam em bytes. [40]

— Esther Nam e Travis Fischer

O Python 3 introduziu uma forte distinção entre strings de texto humano e sequências de bytes puros. A conversão
automática de sequências de bytes para texto Unicode ficou para trás no Python 2. Este capítulo trata de strings
Unicode, sequências de bytes, e das codificações usadas para converter umas nas outras.

Dependendo do que você faz com o Python, pode achar que entender o Unicode não é importante. Isso é improvável,
mas mesmo que seja o caso, não há como escapar da separação entre str e bytes , que agora exige conversões
explícitas. Como um bônus, você descobrirá que os tipos especializados de sequências binárias bytes e bytearray
oferecem recursos que a classe str "pau para toda obra" do Python 2 não oferecia.

Nesse capítulo, veremos os seguintes tópicos:

Caracteres, pontos de código e representações binárias


Recursos exclusivos das sequências binárias: bytes , bytearray , e memoryview

Codificando para o Unicode completo e para conjuntos de caracteres legados


Evitando e tratando erros de codificação
Melhores práticas para lidar com arquivos de texto
A armadilha da codificação default e questões de E/S padrão
Comparações seguras de texto Unicode com normalização
Funções utilitárias para normalização, case folding (equiparação maiúsculas/minúsculas) e remoção de sinais
diacríticos por força bruta
Ordenação correta de texto Unicode com locale e a biblioteca pyuca
Metadados de caracteres do banco de dados Unicode
APIs duais, que processam str e bytes

4.1. Novidades nesse capítulo


O suporte ao Unicode no Python 3 sempre foi muito completo e estável, então o acréscimo mais notável é a seção Seção
4.9.1, descrevendo um utilitário de linha de comando para busca no banco de dados Unicode—uma forma de encontrar
gatinhos sorridentes ou hieróglifos do Egito antigo.

Vale a pena mencionar que o suporte a Unicode no Windows ficou melhor e mais simples desde o Python 3.6, como
veremos na seção Seção 4.6.1.

Vamos começar então com os conceitos não-tão-novos mas fundamentais de caracteres, pontos de código e bytes.
Para essa segunda edição, expandi a seção sobre o módulo struct e o publiquei online em "Parsing
binary records with struct" (Analisando registros binários com struct) (https://fpy.li/4-3), (EN) no
fluentpython.com (http://fluentpython.com), o website que complementa o livro.
✒️ NOTA Lá você também vai encontrar o "Building Multi-character Emojis" (Criando emojis multi-caractere)
(https://fpy.li/4-4) (EN), descrevendo como combinar caracteres Unicode para criar bandeiras de
países, bandeiras de arco-íris, pessoas com tonalidades de pele diferentes e ícones de diferentes tipos
de famílias.

4.2. Questões de caracteres


O conceito de "string" é bem simples: uma string é uma sequência de caracteres. O problema está na definição de
"caractere".

Em 2023, a melhor definição de "caractere" que temos é um caractere Unicode. Consequentemente, os itens que
compõe um str do Python 3 são caracteres Unicode, como os itens de um objeto unicode no Python 2. Em contraste,
os itens de uma str no Python 2 são bytes, assim como os itens num objeto bytes do Python 3.

O padrão Unicode separa explicitamente a identidade dos caracteres de representações binárias específicas:

A identidade de um caractere é chamada de ponto de código (code point). É um número de 0 a 1.114.111 (na base
10), representado no padrão Unicode na forma de 4 a 6 dígitos hexadecimais precedidos pelo prefixo "U+", de
U+0000 a U+10FFFF. Por exemplo, o ponto de código da letra A é U+0041, o símbolo do Euro é U+20AC, e o símbolo
musical da clave de sol corresponde ao ponto de código U+1D11E. Cerca de 13% dos pontos de código válidos tem
caracteres atribuídos a si no Unicode 13.0.0, a versão do padrão usada no Python 3.10.
Os bytes específicos que representam um caractere dependem da codificação (encoding) usada. Uma codificação,
nesse contexto, é um algoritmo que converte pontos de código para sequências de bytes, e vice-versa. O ponto de
código para a letra A (U+0041) é codificado como um único byte, \x41 , na codificação UTF-8, ou como os bytes
\x41\x00 na codificação UTF-16LE. Em um outro exemplo, o UTF-8 exige três bytes para codificar o símbolo do
Euro (U+20AC): \xe2\x82\xac . Mas no UTF-16LE o mesmo ponto de código é U+20AC representado com dois bytes:
\xac\x20 .

Converter pontos de código para bytes é codificar; converter bytes para pontos de código é decodificar. Veja o Exemplo
1.

Exemplo 1. Codificando e decodificando

PYTHON3
>>> s = 'café'
>>> len(s) # (1)
4
>>> b = s.encode('utf8') # (2)
>>> b
b'caf\xc3\xa9' # (3)
>>> len(b) # (4)
5
>>> b.decode('utf8') # (5)
'café'

1. A str 'café' tem quatro caracteres Unicode.


2. Codifica str para bytes usando a codificação UTF-8.
3. bytes literais são prefixados com um b.
4. bytes b tem cinco bytes (o ponto de código para "é" é codificado com dois bytes em UTF-8).
5. Decodifica bytes para str usando a codificação UTF-8.
Um jeito fácil de sempre lembrar a distinção entre .decode() e .encode() é se convencer que

👉 DICA sequências de bytes podem ser enigmáticos dumps de código de máquina, ao passo que objetos str
Unicode são texto "humano". Daí que faz sentido decodificar bytes em str , para obter texto legível
por seres humanos, e codificar str em bytes , para armazenamento ou transmissão.

Apesar do str do Python 3 ser quase o tipo unicode do Python 2 com um novo nome, o bytes do Python 3 não é
meramente o velho str renomeado, e há também o tipo estreitamente relacionado bytearray . Então vale a pena
examinar os tipos de sequências binárias antes de avançar para questões de codificação/decodificação.

4.3. Os fundamentos do byte


Os novos tipos de sequências binárias são diferentes do str do Python 2 em vários aspectos. A primeira coisa
importante é que existem dois tipos embutidos básicos de sequências binárias: o tipo imutável bytes , introduzido no
Python 3, e o tipo mutável bytearray , introduzido há tempos, no Python 2.6[41]. A documentação do Python algumas
vezes usa o termo genérico "byte string" (string de bytes, na documentação em português) para se referir a bytes e
bytearray .

Cada item em bytes ou bytearray é um inteiro entre 0 e 255, e não uma string de um caractere, como no str do
Python 2. Entretanto, uma fatia de uma sequência binária sempre produz uma sequência binária do mesmo tipo—
incluindo fatias de tamanho 1. Veja o Exemplo 2.

Exemplo 2. Uma sequência de cinco bytes, como bytes e como `bytearray

PYCON
>>> cafe = bytes('café', encoding='utf_8') (1)
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] (2)
99
>>> cafe[:1] (3)
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr (4)
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] (5)
bytearray(b'\xa9')

1. bytes pode ser criado a partir de uma str , dada uma codificação.

2. Cada item é um inteiro em range(256) .

3. Fatias de bytes também são bytes —mesmo fatias de um único byte.

4. Não há uma sintaxe literal para bytearray : elas aparecem como bytearray() com um literal bytes como
argumento.
5. Uma fatia de bytearray também é uma bytearray .

O fato de my_bytes[0] obter um int mas my_bytes[:1] devolver uma sequência de bytes de

⚠️ AVISO tamanho 1 só é surpreeendente porque estamos acostumados com o tipo str do Python, onde s[0]
== s[:1] . Para todos os outros tipos de sequência no Python, um item não é o mesmo que uma fatia
de tamanho 1.
Apesar de sequências binárias serem na verdade sequências de inteiros, sua notação literal reflete o fato delas
frequentemente embutirem texto ASCII. Assim, quatro formas diferentes de apresentação são utilizadas, dependendo
do valor de cada byte:

Para bytes com código decimais de 32 a 126—do espaço ao ~ (til)—é usado o próprio caractere ASCII.
Para os bytes correspondendo ao tab, à quebra de linha, ao carriage return (CR) e à \ , são usadas as sequências de
escape \t , \n , \r , e \\ .
Se os dois delimitadores de string, ' e " , aparecem na sequência de bytes, a sequência inteira é delimitada com ',
e qualquer ' dentro da sequência é precedida do caractere de escape, assim \' .[42]
Para qualquer outro valor do byte, é usada uma sequência de escape hexadecimal (por exemplo, \x00 é o byte
nulo).

É por isso que no Exemplo 2 vemos b’caf\xc3\xa9' : os primeiros três bytes, b’caf' , estão na faixa de impressão do
ASCII, ao contrário dos dois últimos.

Tanto bytes quanto bytearray suportam todos os métodos de str , exceto aqueles relacionados formatação
( format , format_map ) e aqueles que dependem de dados Unicode, incluindo casefold , isdecimal , isidentifier ,
isnumeric , isprintable , e encode . Isso significa que você pode usar os métodos conhecidos de string, como
endswith , replace , strip , translate , upper e dezenas de outros, com sequências binárias—mas com
argumentos bytes em vez de str . Além disso, as funções de expressões regulares no módulo re também funcionam
com sequências binárias, se a regex for compilada a partir de uma sequência binária ao invés de uma str . Desde o
Python 3.5, o operador % voltou a funcionar com sequências binárias.[43]

As sequências binárias tem um método de classe que str não possui, chamado fromhex , que cria uma sequência
binária a partir da análise de pares de dígitos hexadecimais, separados opcionalmente por espaços:

PYCON
>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'

As outras formas de criar instâncias de bytes ou bytearray são chamadas a seus construtores com:

Uma str e um argumento nomeado encoding

Um iterável que forneça itens com valores entre 0 e 255


Um objeto que implemente o protocolo de buffer (por exemplo bytes , bytearray , memoryview , array.array ),
que copia os bytes do objeto fonte para a recém-criada sequência binária

Até o Python 3.5, era possível chamar bytes ou bytearray com um único inteiro, para criar uma

⚠️ AVISO sequência daquele tamanho inicializada com bytes nulos. Essa assinatura for descontinuada no
Python 3.5 e removida no Python 3.6. Veja a PEP 467—​Minor API improvements for binary
sequences (Pequenas melhorias na API para sequências binárias) (EN) (https://fpy.li/pep467).

Criar uma sequência binária a partir de um objeto tipo buffer é uma operação de baixo nível que pode envolver
conversão de tipos. Veja uma demonstração no Exemplo 3.

Exemplo 3. Inicializando bytes a partir de dados brutos de um array


PYCON
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2]) (1)
>>> octets = bytes(numbers) (2)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' (3)

1. O typecode 'h' criar um array de short integers (inteiros de 16 bits).


2. octets mantém uma cópia dos bytes que compõem numbers .

3. Esses são os 10 bytes que representam os 5 inteiros pequenos.

Criar um objeto bytes ou bytearray a partir de qualquer fonte tipo buffer vai sempre copiar os bytes. Já objetos
memoryview permitem compartilhar memória entre estruturas de dados binários, como vimos na seção Seção 2.10.2.

Após essa exploração básica dos tipos de sequências de bytes do Python, vamos ver como eles são convertidos de e para
strings.

4.4. Codificadores/Decodificadores básicos


A distribuição do Python inclui mais de 100 codecs (encoders/decoders, _codificadores/decodificadores) para conversão
de texto para bytes e vice-versa. Cada codec tem um nome, como 'utf_8' , e muitas vezes apelidos, tais como 'utf8' ,
'utf-8' , e 'U8' , que você pode usar como o argumento de codificação em funções como open() , str.encode() ,
bytes.decode() , e assim por diante. O Exemplo 4 mostra o mesmo texto codificado como três sequências de bytes
diferentes.

Exemplo 4. A string "El Niño" codificada com três codecs, gerando sequências de bytes muito diferentes

PYCON
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
... print(codec, 'El Niño'.encode(codec), sep='\t')
...
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

A Figura 1 mostra um conjunto de codecs gerando bytes a partir de caracteres como a letra "A" e o símbolo musical da
clave de sol. Observe que as últimas três codificações tem bytes múltiplos e tamanho variável.
Figura 1. Doze caracteres, seus pontos de código, e sua representação binária (em hexadecimal) em 7 codificações
diferentes (asteriscos indicam que o caractere não pode ser representado naquela codificação).

Aqueles asteriscos todos na Figura 1 deixam claro que algumas codificações, como o ASCII e mesmo o multi-byte
GB2312, não conseguem representar todos os caracteres Unicode. As codificações UTF, por outro lado, foram projetadas
para lidar com todos os pontos de código do Unicode.

As codificações apresentadas na Figura 1 foram escolhidas para montar uma amostra representativa:

latin1 a.k.a. iso8859_1


Importante por ser a base de outras codificações,tal como a cp1252 e o próprio Unicode (observe que os valores
binários do latin1 aparecem nos bytes do cp1252 e até nos pontos de código).

cp1252

Um superconjunto útil de latin1 , criado pela Microsoft, acrescentando símbolos convenientes como as aspas
curvas e o € (euro); alguns aplicativos de Windows chamam essa codificação de "ANSI", mas ela nunca foi um padrão
ANSI real.

cp437

O conjunto de caracteres original do IBM PC, com caracteres de desenho de caixas. Incompatível com o latin1 , que
surgiu depois.

gb2312

Padrão antigo para codificar ideogramas chineses simplificados usados na República da China; uma das várias
codificações muito populares para línguas asiáticas.

utf-8

De longe a codificação de 8 bits mais comum na web. Em julho de 2021, o "W3Techs: Usage statistics of character
encodings for websites" (https://fpy.li/4-5) afirma que 97% dos sites usam UTF-8, um grande avanço sobre os 81,4% de
setembro de 2014, quando escrevi este capítulo na primeira edição.

utf-16le
Uma forma do esquema de codificação UTF de 16 bits; todas as codificações UTF-16 suportam pontos de código
acima de U+FFFF, através de sequências de escape chamadas "pares substitutos".
A UTF-16 sucedeu a codificação de 16 bits original do Unicode 1.0—a UCS-2—há muito tempo, em

⚠️ AVISO 1996. A UCS-2 ainda é usada em muitos sistemas, apesar de ter sido descontinuada ainda no século
passado, por suportar apenas ponto de código até U+FFFF. Em 2021, mas de 57% dos pontos de
código alocados estava acima de U+FFFF, incluindo os importantíssimos emojis.

Após completar essa revisão das codificações mais comuns, vamos agora tratar das questões relativas a operações de
codificação e decodificação.

4.5. Entendendo os problemas de codificação/decodificação


Apesar de existir uma exceção genérica, UnicodeError , o erro relatado pelo Python em geral é mais específico: ou é
um UnicodeEncodeError (ao converter uma str para sequências binárias) ou é um UnicodeDecodeError (ao ler
uma sequência binária para uma str ). Carregar módulos do Python também pode geram um SyntaxError , quando
a codificação da fonte for inesperada. Vamos ver como tratar todos esses erros nas próximas seções.

A primeira coisa a observar quando aparece um erro de Unicode é o tipo exato da exceção. É um
👉 DICA UnicodeEncodeError , um UnicodeDecodeError , ou algum outro erro (por exemplo,
SyntaxError ) mencionando um problema de codificação? Para resolver o problema, você primeiro
precisa entendê-lo.

4.5.1. Tratando o UnicodeEncodeError


A maioria dos codecs não-UTF entendem apenas um pequeno subconjunto dos caracteres Unicode. Ao converter texto
para bytes, um UnicodeEncodeError será gerado se um caractere não estiver definido na codificação alvo, a menos
que seja fornecido um tratamento especial, passando um argumento errors para o método ou função de codificação.
O comportamento para tratamento de erro é apresentado no Exemplo 5.

Exemplo 5. Encoding to bytes: success and error handling

PYCON
>>> city = 'São Paulo'
>>> city.encode('utf_8') (1)
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1') (2)
b'S\xe3o Paulo'
>>> city.encode('cp437') (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore') (4)
b'So Paulo'
>>> city.encode('cp437', errors='replace') (5)
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace') (6)
b'S&#227;o Paulo'

1. As codificações UTF lidam com qualquer str

2. iso8859_1 também funciona com a string 'São Paulo' .

3. cp437 não consegue codificar o 'ã' ("a" com til). O método default de tratamento de erro, — 'strict' —gera um
UnicodeEncodeError .
4. O método de tratamento errors='ignore' pula os caracteres que não podem ser codificados; isso normalmente é
uma péssima ideia, levando a perda silenciosa de informação.
5. Ao codificar, errors='replace' substitui os caracteres não-codificáveis por um '?' ; aqui também há perda de
informação, mas os usuários recebem um alerta de que algo está faltando.
6. 'xmlcharrefreplace' substitui os caracteres não-codificáveis por uma entidade XML. Se você não pode usar UTF
e não pode perder informação, essa é a única opção.
O tratamento de erros de codecs é extensível. Você pode registrar novas strings para o argumento

✒️ NOTA errors passando um nome e uma função de tratamento de erros para a função
codecs.register_error function. Veja documentação de codecs.register_error
(https://docs.python.org/pt-br/3/library/codecs.html#codecs.register_error) (EN).

O ASCII é um subconjunto comum a todas as codificações que conheço, então a codificação deveria sempre funcionar
se o texto for composto exclusivamente por caracteres ASCII. O Python 3.7 trouxe um novo método booleano,
str.isascii() (https://fpy.li/4-7), para verificar se seu texto Unicode é 100% ASCII. Se for, você deve ser capaz de
codificá-lo para bytes em qualquer codificação sem gerar um UnicodeEncodeError .

4.5.2. Tratando o UnicodeDecodeError


Nem todo byte contém um caractere ASCII válido, e nem toda sequência de bytes é um texto codificado em UTF-8 ou
UTF-16 válidos; assim, se você presumir uma dessas codificações ao converter um sequência binária para texto, pode
receber um UnicodeDecodeError , se bytes inesperados forem encontrados.

Por outro lado, várias codificações de 8 bits antigas, como a 'cp1252' , a 'iso8859_1' e a 'koi8_r' são capazes de
decodificar qualquer série de bytes, incluindo ruído aleatório, sem reportar qualquer erro. Portanto, se seu programa
presumir a codificação de 8 bits errada, ele vai decodificar lixo silenciosamente.

👉 DICA Caracteres truncados ou distorcidos são conhecidos como "gremlins" ou "mojibake" (文字化け
—"texto modificado" em japonês).

O Exemplo 6 ilustra a forma como o uso do codec errado pode produzir gremlins ou um UnicodeDecodeError .

Exemplo 6. Decodificando de str para bytes: sucesso e tratamento de erro

PYCON
>>> octets = b'Montr\xe9al' (1)
>>> octets.decode('cp1252') (2)
'Montréal'
>>> octets.decode('iso8859_7') (3)
'Montrιal'
>>> octets.decode('koi8_r') (4)
'MontrИal'
>>> octets.decode('utf_8') (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace') (6)
'Montr�al'

1. A palavra "Montréal" codificada em latin1 ; '\xe9' é o byte para "é".


2. Decodificar com Windows 1252 funciona, pois esse codec é um superconjunto de latin1 .
3. ISO-8859-7 foi projetado para a língua grega, então o byte '\xe9' é interpretado de forma incorreta, e nenhum
erro é gerado.
4. KOI8-R é foi projetado para o russo. Agora '\xe9' significa a letra "И" do alfabeto cirílico.
5. O codec 'utf_8' detecta que octets não é UTF-8 válido, e gera um UnicodeDecodeError .

6. Usando 'replace' para tratamento de erro, o \xe9 é substituído por "�" (ponto de código #U+FFFD), o caractere
oficial do Unicode chamado REPLACEMENT CHARACTER , criado exatamente para representar caracteres
desconhecidos.

4.5.3. O SyntaxError ao carregar módulos com codificação inesperada


UTF-8 é a codificação default para fontes no Python 3, da mesma forma que ASCII era o default no Python 2. Se você
carregar um módulo .py contendo dados que não estejam em UTF-8, sem declaração codificação, receberá uma
mensagem como essa:

SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line


1, but no encoding declared; see https://python.org/dev/peps/pep-0263/
for details

Como o UTF-8 está amplamente instalado em sistemas GNU/Linux e macOS, um cenário onde isso tem mais chance de
ocorrer é na abertura de um arquivo .py criado no Windows, com cp1252 . Observe que esse erro ocorre mesmo no
Python para Windows, pois a codificação default para fontes de Python 3 é UTF-8 em todas as plataformas.

Para resolver esse problema, acrescente o comentário mágico coding no início do arquivo, como no Exemplo 7.

Exemplo 7. 'ola.py': um "Hello, World!" em português

PYTHON3
# coding: cp1252

print('Olá, Mundo!')

Agora que o código fonte do Python 3 não está mais limitado ao ASCII, e por default usa a excelente

👉 DICA codificação UTF-8, a melhor "solução" para código fonte em codificações antigas como 'cp1252' é
converter tudo para UTF-8 de uma vez, e não se preocupar com os comentários coding . E se seu
editor não suporta UTF-8, é hora de trocar de editor.

Suponha que você tem um arquivo de texto, seja ele código-fonte ou poesia, mas não sabe qual codificação foi usada.
Como detectar a codificação correta? Respostas na próxima seção.

4.5.4. Como descobrir a codificação de uma sequência de bytes


Como descobrir a codificação de uma sequência de bytes? Resposta curta: não é possível. Você precisa ser informado.

Alguns protocolos de comunicação e formatos de arquivo, como o HTTP e o XML, contêm cabeçalhos que nos dizem
explicitamente como o conteúdo está codificado. Você pode ter certeza que algumas sequências de bytes não estão em
ASCII, pois elas contêm bytes com valores acima de 127, e o modo como o UTF-8 e o UTF-16 são construídos também
limita as sequências de bytes possíveis.
O hack do Leo para adivinhar uma decodificação UTF-8
(Os próximos parágrafos vieram de uma nota escrita pelo revisor técnico Leonardo Rochael no rascunho desse
livro.)

Pela forma como o UTF-8 foi projetado, é quase impossível que uma sequência aleatória de bytes, ou mesmo uma
sequência não-aleatória de bytes de uma codificação diferente do UTF-8, seja acidentalmente decodificada como
lixo no UTF-8, ao invés de gerar um UnicodeDecodeError .

As razões para isso são que as sequências de escape do UTF-8 nunca usam caracteres ASCII, e tais sequências de
escape tem padrões de bits que tornam muito difícil que dados aleatórioas sejam UTF-8 válido por acidente.

Portanto, se você consegue decodificar alguns bytes contendo códigos > 127 como UTF-8, a maior probabilidade é
de sequência estar em UTF-8.

Trabalhando com os serviços online brasileiros, alguns dos quais alicerçados em back-ends antigos,
ocasionalmente precisei implementar uma estratégia de decodificação que tentava decodificar via UTF-8, e
tratava um UnicodeDecodeError decodificando via cp1252 . Uma estratégia feia, mas efetiva.

Entretanto, considerando que as linguagens humanas também tem suas regras e restrições, uma vez que você supõe
que uma série de bytes é um texto humano simples, pode ser possível intuir sua codificação usando heurística e
estatística. Por exemplo, se bytes com valor b'\x00' bytes forem comuns, é provável que seja uma codificação de 16
ou 32 bits, e não um esquema de 8 bits, pois caracteres nulos em texto simples são erros. Quando a sequência de bytes
`b'\x20\x00'` aparece com frequência, é mais provável que esse seja o caractere de espaço (U+0020) na codificação UTF-
16LE, e não o obscuro caractere U+2000 ( EN QUAD )—seja lá o que for isso.

É assim que o pacote "Chardet—​The Universal Character Encoding Detector (Chardet—O Detector Universal de
Codificações de Caracteres)" (https://fpy.li/4-8) trabalha para descobrir cada uma das mais de 30 codificações suportadas.
Chardet é uma biblioteca Python que pode ser usada em seus programas, mas que também inclui um utilitário de
comando de linha, chardetect . Aqui está a forma como ele analisa o código fonte desse capítulo:

BASH
$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99

Apesar de sequências binárias de texto codificado normalmente não trazerem dicas sobre sua codificação, os formatos
UTF podem preceder o conteúdo textual por um marcador de ordem dos bytes. Isso é explicado a seguir.

4.5.5. BOM: um gremlin útil


No Exemplo 4, você pode ter notado um par de bytes extra no início de uma sequência codificada em UTF-16. Aqui
estão eles novamente:

PYCON
>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

Os bytes são b'\xff\xfe' . Isso é um BOM—sigla para byte-order mark (marcador de ordem de bytes)—indicando a
ordenação de bytes "little-endian" da CPU Intel onde a codificação foi realizada.

Em uma máquina little-endian, para cada ponto de código, o byte menos significativo aparece primeiro: a letra 'E' ,
ponto de código U+0045 (decimal 69), é codificado nas posições 2 e 3 dos bytes como 69 e 0 :
PYCON
>>> list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]

Em uma CPU big-endian, a codificação seria invertida; 'E' seria codificado como 0 e 69 .

Para evitar confusão, a codificação UTF-16 precede o texto a ser codificado com o caractere especial invisível ZERO
WIDTH NO-BREAK SPACE (U+FEFF). Em um sistema little-endian, isso é codificado como b'\xff\xfe' (decimais 255,
254). Como, por design, não existe um caractere U+FFFE em Unicode, a sequência de bytes b'\xff\xfe' tem que ser o
ZERO WIDTH NO-BREAK SPACE em uma codificação little-endian, e então o codec sabe qual ordenação de bytes usar.

Há uma variante do UTF-16—​o UTF-16LE—​que é explicitamente little-endian, e outra que é explicitamente big-endian, o
UTF-16BE. Se você usá-los, um BOM não será gerado:

PYCON
>>> u16le = 'El Niño'.encode('utf_16le')
>>> list(u16le)
[69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
>>> u16be = 'El Niño'.encode('utf_16be')
>>> list(u16be)
[0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]

Se o BOM estiver presente, supõe-se que ele será filtrado pelo codec UTF-16, então recebemos apenas o conteúdo
textual efetivo do arquivo, sem o ZERO WIDTH NO-BREAK SPACE inicial.

O padrão Unicode diz que se um arquivo é UTF-16 e não tem um BOM, deve-se presumir que ele é UTF-16BE (big-
endian). Entretanto, a arquitetura x86 da Intel é little-endian, daí que há uma grande quantidade de UTF-16 little-endian
e sem BOM no mundo.

Toda essa questão de ordenação dos bytes (endianness) só afeta codificações que usam palavras com mais de um byte,
como UTF-16 e UTF-32. Uma grande vantagem do UTF-8 é produzir a mesma sequência independente da ordenação dos
bytes, então um BOM não é necessário. No entanto, algumas aplicações Windows (em especial o Notepad) mesmo assim
acrescentam o BOM a arquivos UTF-8—e o Excel depende do BOM para detectar um arquivo UTF-8, caso contrário ele
presume que o conteúdo está codificado com uma página de código do Windows. Essa codificação UTF-8 com BOM é
chamada UTF-8-SIG no registro de codecs do Python. O caractere U+FEFF codificado em UTF-8-SIG é a sequência de três
bytes b'\xef\xbb\xbf' . Então, se um arquivo começa com aqueles três bytes, é provavelmente um arquivo UTF-8
com um BOM.

A dica de Caleb sobre o UTF-8-SIG


Caleb Hattingh—um dos revisores técnicos—sugere sempre usar o codec UTF-8-SIG para ler
arquivos UTF-8. Isso é inofensivo, pois o UTF-8-SIG lê corretamente arquivos com ou sem um BOM, e
não devolve o BOM propriamente dito. Para escrever arquivos, recomendo usar UTF-8, para
interoperabilidade integral. Por exemplo, scripts Python podem ser tornados executáveis em
👉 DICA sistemas Unix, se começarem com o comentário: #!/usr/bin/env python3 . Os dois primeiros bytes
do arquivo precisam ser b'#!' para isso funcionar, mas o BOM rompe essa convenção. Se você tem
o requerimento específico de exportar dados para aplicativso que precisam do BOM, use o UTF-8-
SIG, mas esteja ciente do que diz a documentação sobre codecs
(https://docs.python.org/pt-br/3/library/codecs.html#encodings-and-unicode) (EN) do Python: "No UTF-8, o uso
do BOM é desencorajado e, em geral, deve ser evitado."

Vamos agora ver como tratar arquivos de texto no Python 3.


4.6. Processando arquivos de texto
A melhor prática para lidar com E/S de texto é o "Sanduíche de Unicode" (Unicode sandwich) (Figura 2).[44] Isso significa
que os bytes devem ser decodificados para str o mais cedo possível na entrada (por exemplo, ao abrir um arquivo
para leitura). O "recheio" do sanduíche é a lógica do negócio de seu programa, onde o tratamento do texto é realizado
exclusivamente sobre objetos str . Você nunca deveria codificar ou decodificar no meio de outro processamento. Na
saída, as str são codificadas para bytes o mais tarde possível. A maioria das frameworks web funcona assim, e
raramente encostamos em bytes ao usá-las. No Django, por exemplo, suas views devem produzir str em Unicode; o
próprio Django se encarrega de codificar a resposta para bytes , usando UTF-8 como default.

O Python 3 torna mais fácil seguir o conselho do sanduíche de Unicode, pois o embutido open() executa a
decodificação necessária na leitura e a codificação ao escrever arquivos em modo texto. Dessa forma, tudo que você
recebe de my_file.read() e passa para my_file.write(text) são objetos str .

Assim, usar arquivos de texto é aparentemente simples. Mas se você confiar nas codificações default, pode acabar
levando uma mordida.

Figura 2. O sanduíche de Unicode: melhores práticas atuais para processamento de texto.

Observe a sessão de console no Exemplo 8. Você consegue ver o erro?

Exemplo 8. Uma questão de plataforma na codificação (você pode ou não ver o problema se tentar isso na sua máquina)

PYCON
>>> open('cafe.txt', 'w', encoding='utf_8').write('café')
4
>>> open('cafe.txt').read()
'café'

O erro: especifiquei a codificação UTF-8 ao escrever o arquivo, mas não fiz isso na leitura, então o Python assumiu a
codificação de arquivo default do Windows—página de código 1252—e os bytes finais foram decodificados como os
caracteres 'é' ao invés de 'é' .

Executei o Exemplo 8 no Python 3.8.1, 64 bits, no Windows 10 (build 18363). Os mesmos comandos rodando em um
GNU/Linux ou um macOS recentes funcionam perfeitamente, pois a codificação default desses sistemas é UTF-8, dando
a falsa impressão que tudo está bem. Se o argumento de codificação fosse omitido ao abrir o arquivo para escrita, a
codificação default do locale seria usada, e poderíamos ler o arquivo corretamente usando a mesma codificação. Mas aí
o script geraria arquivos com conteúdo binário diferente dependendo da plataforma, ou mesmo das configurações do
locale na mesma plataforma, criando problemas de compatibilidade.
Código que precisa rodar em múltiplas máquinas ou múltiplas ocasiões não deveria jamais
👉 DICA depender de defaults de codificação. Sempre passe um argumento encoding= explícito ao abrir
arquivos de texto, pois o default pode mudar de uma máquina para outra ou de um dia para o outro.

Um detalhe curioso no Exemplo 8 é que a função write na primeira instrução informa que foram escritos quatro
caracteres, mas na linha seguinte são lidos cinco caracteres. O Exemplo 9 é uma versão estendida do Exemplo 8, e
explica esse e outros detalhes.

Exemplo 9. Uma inspeção mais atenta do Exemplo 8 rodando no Windows revela o bug e a solução do problema

PYCON
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp # (1)
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café') # (2)
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size # (3)
5
>>> fp2 = open('cafe.txt')
>>> fp2 # (4)
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding # (5)
'cp1252'
>>> fp2.read() # (6)
'café'
>>> fp3 = open('cafe.txt', encoding='utf_8') # (7)
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read() # (8)
'café'
>>> fp4 = open('cafe.txt', 'rb') # (9)
>>> fp4 # (10)
<_io.BufferedReader name='cafe.txt'>
>>> fp4.read() # (11)
b'caf\xc3\xa9'

1. Por default, open usa o modo texto e devolve um objeto TextIOWrapper com uma codificação específica.
2. O método write de um TextIOWrapper devolve o número de caracteres Unicode escritos.
3. os.stat diz que o arquivo tem 5 bytes; o UTF-8 codifica 'é' com 2 bytes, 0xc3 e 0xa9.
4. Abrir um arquivo de texto sem uma codificação explícita devolve um TextIOWrapper com a codificação
configurada para um default do locale.
5. Um objeto TextIOWrapper tem um atributo de codificação que pode ser inspecionado: neste caso, cp1252 .

6. Na codificação cp1252 do Windows, o byte 0xc3 é um "Ã" (A maiúsculo com til), e 0xa9 é o símbolo de copyright.
7. Abrindo o mesmo arquivo com a codificação correta.
8. O resultado esperado: os mesmo quatro caracteres Unicode para 'café' .

9. A flag 'rb' abre um arquivo para leitura em modo binário.


10. O objeto devolvido é um BufferedReader , e não um TextIOWrapper .

11. Ler do arquivo obtém bytes, como esperado.


Não abra arquivos de texto no modo binário, a menos que seja necessário analisar o conteúdo do
arquivo para determinar sua codificação—e mesmo assim, você deveria estar usando o Chardet em
👉 DICA vez de reinventar a roda (veja a seção Seção 4.5.4).

Programas comuns só deveriam usar o modo binário para abrir arquivos binários, como arquivos
de imagens raster ou bitmaps.

O problema no Exemplo 9 vem de se confiar numa configuração default ao se abrir um arquivo de texto. Há várias
fontes de tais defaults, como mostra a próxima seção.

4.6.1. Cuidado com os defaults de codificação


Várias configurações afetam os defaults de codificação para E/S no Python. Veja o script default_encodings.py script no
Exemplo 10.

Exemplo 10. Explorando os defaults de codificação

PYTHON3
import locale
import sys

expressions = """
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""

my_file = open('dummy', 'w')

for expression in expressions.split():


value = eval(expression)
print(f'{expression:>30} -> {value!r}')

A saída do Exemplo 10 no GNU/Linux (Ubuntu 14.04 a 19.10) e no macOS (10.9 a 10.14) é idêntica, mostrando que UTF-
8 é usado em toda parte nesses sistemas:

BASH
$ python3 default_encodings.py
locale.getpreferredencoding() -> 'UTF-8'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'UTF-8'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'

No Windows, porém, a saída é o Exemplo 11.


Exemplo 11. Codificações default, no PowerShell do Windows 10 (a saída é a mesma no cmd.exe)

BASH
> chcp (1)
Active code page: 437
> python default_encodings.py (2)
locale.getpreferredencoding() -> 'cp1252' (3)
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp1252' (4)
sys.stdout.isatty() -> True (5)
sys.stdout.encoding -> 'utf-8' (6)
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'

1. chcp mostra a página de código ativa para o console: 437 .

2. Executando default_encodings.py, com a saída direcionada para o console.


3. locale.getpreferredencoding() é a configuração mais importante.
4. Arquivos de texto usam`locale.getpreferredencoding()` por default.
5. A saída está direcionada para o console, então sys.stdout.isatty() é True .

6. Agora, sys.stdout.encoding não é a mesma que a página de código informada por chcp !

O suporte a Unicode no próprio Windows e no Python para Windows melhorou desde que escrevi a primeira edição
deste livro. O Exemplo 11 costumava informar quatro codificações diferentes no Python 3.4 rodando no Windows 7. As
codificações para stdout , stdin , e stderr costumavam ser iguais à da página de código ativa informada pelo
comando chcp , mas agora são todas utf-8 , graças à PEP 528—​Change Windows console encoding to UTF-8 (Mudar a
codificação do console no Windows para UTF-8) (https://fpy.li/pep528) (EN), implementada no Python 3.6, e ao suporte a
Unicode no PowerShell do cmd.exe (desde o Windows 1809, de outubro de 2018).[45] É esquisito que o chcp e o
sys.stdout.encoding reportem coisas diferentes quando o stdout está escrevendo no console, mas é ótimo
podermos agora escrever strings Unicode sem erros de codificação no Windows—a menos que o usuário redirecione a
saída para um arquivo, como veremos adiante. Isso não significa que todos os seus emojis favoritos vão aparecer: isso
também depende da fonte usada pelo console.

Outra mudança foi a PEP 529—​Change Windows filesystem encoding to UTF-8 (Mudar a codificação do sistema de
arquivos do Windows para UTF-8) (https://fpy.li/pep529), também implementada no Python 3.6, que modificou a
codificação do sistema de arquivos (usada para representar nomes de diretórios e de arquivos), da codificação
proprietária MBCS da Microsoft para UTF-8.

Entretanto, se a saída do Exemplo 10 for redirecionada para um arquivo, assim…​

BASH
Z:\>python default_encodings.py > encodings.log

…​aí o valor de sys.stdout.isatty() se torna False , e sys.stdout.encoding


é determinado por
locale.getpreferredencoding() , 'cp1252' naquela máquina—mas sys.stdin.encoding e
sys.stderr.encoding seguem como utf-8 .
No Exemplo 12, usei a expressão de escape’\N{}'` para literais Unicode, escrevendo o nome oficial do
caractere dentro do \N{} . Isso é bastante prolixo, mas explícito e seguro: o Python gera um
👉 DICA SyntaxError se o nome não existir—bem melhor que escrever um número hexadecimal que pode
estar errado, mas isso só será descoberto muito mais tarde. De qualquer forma, você provavelmente
vai querer escrever um comentário explicando os códigos dos caracteres, então a verbosidade do
\N{} é fácil de aceitar.

Isso significa que um script como o Exemplo 12 funciona quando está escrevendo no console, mas pode falhar quando
a saída é redirecionada para um arquivo.

Exemplo 12. stdout_check.py

PYTHON3
import sys
from unicodedata import name

print(sys.version)
print()
print('sys.stdout.isatty():', sys.stdout.isatty())
print('sys.stdout.encoding:', sys.stdout.encoding)
print()

test_chars = [
'\N{HORIZONTAL ELLIPSIS}', # exists in cp1252, not in cp437
'\N{INFINITY}', # exists in cp437, not in cp1252
'\N{CIRCLED NUMBER FORTY TWO}', # not in cp437 or in cp1252
]

for char in test_chars:


print(f'Trying to output {name(char)}:')
print(char)

O Exemplo 12 mostra o resultado de uma chamada a sys.stdout.isatty() , o valor de sys.​


stdout.encoding , e
esses três caracteres:

'…' HORIZONTAL ELLIPSIS —existe no CP 1252 mas não no CP 437.

'∞' INFINITY —existe no CP 437 mas não no CP 1252.

'㊷' CIRCLED NUMBER FORTY TWO —não existe nem no CP 1252 nem no CP 437.

Quando executo o stdout_check.py no PowerShell ou no cmd.exe, funciona como visto na Figura 3.


Figura 3. Executando stdout_check.py no PowerShell.

Apesar de chcpinformar o código ativo como 437, sys.stdout.encoding é UTF-8, então tanto HORIZONTAL
ELLIPSIS quanto INFINITY são escritos corretamente. O CIRCLED NUMBER FORTY TWO é substituído por um
retângulo, mas nenhum erro é gerado. Presume-se que ele seja reconhecido como um caractere válido, mas a fonte do
console não tem o glifo para mostrá-lo.

Entretanto, quando redireciono a saída de stdout_check.py para um arquivo, o resultado é o da Figura 4.

Figura 4. Executanto stdout_check.py no PowerShell, redirecionando a saída.

O primeiro problema demonstrado pela Figura 4 é o UnicodeEncodeError mencionando o caractere '\u221e' ,


porque sys.stdout.encoding é 'cp1252' —uma página de código que não tem o caractere INFINITY .

Lendo out.txt com o comando type —ou um editor de Windows como o VS Code ou o Sublime Text—mostra que, ao
invés do HORIZONTAL ELLIPSIS, consegui um 'à' ( LATIN SMALL LETTER A WITH GRAVE ). Acontece que o valor
binário 0x85 no CP 1252 significa '…' , mas no CP 437 o mesmo valor binário representa o 'à' . Então, pelo visto, a
página de código ativa tem alguma importância, não de uma forma razoável ou útil, mas como uma explicação parcial
para uma experiência ruim com o Unicode.
Para realizar esses experimentos, usei um laptop configurado para o mercado norte-americano,

✒️ NOTA rodando Windows 10 OEM. Versões de Windows localizadas para outros países podem ter
configurações de codificação diferentes. No Brasil, por exemplo, o console do Windows usa a página
de código 850 por default—​e não a 437.

Para encerrar esse enlouquecedor tópico de codificações default, vamos dar uma última olhada nas diferentes
codificações no Exemplo 11:

Se você omitir o argumento encoding ao abrir um arquivo, o default é dado por


locale.getpreferredencoding() ( 'cp1252' no Exemplo 11).
Antes do Python 3.6, a codificação de sys.stdout|stdin|stderr costumava ser determinada pela variável do
ambiente PYTHONIOENCODING (https://docs.python.org/pt-br/3/using/cmdline.html#envvar-PYTHONIOENCODING)—agora essa
variável é ignorada, a menos que PYTHONLEGACYWINDOWSSTDIO
(https://docs.python.org/pt-br/3/using/cmdline.html#envvar-PYTHONLEGACYWINDOWSSTDIO) seja definida como uma string
não-vazia. Caso contrário, a codificação da E/S padrão será UTF-8 para E/S interativa, ou definida por
locale.getpreferredencoding() , se a entrada e a saída forem redirecionadas para ou de um arquivo.

sys.getdefaultencoding() é usado internamente pelo Python em conversões implícitas de dados binários de ou


para str . Não há suporte para mudar essa configuração.

sys.getfilesystemencoding() é usado para codificar/decodificar nomes de arquivo (mas não o conteúdo dos
arquivos). Ele é usado quando open() recebe um argumento str para um nome de arquivo; se o nome do
arquivo é passado como um argumento bytes , ele é entregue sem modificação para a API do sistema operacional.

Já faz muito anos que, no GNU/Linux e no macOS, todas essas codificações são definidas como UTF-8
por default, então a E/S entende e exibe todos os caracteres Unicode. No Windows, não apenas

✒️ NOTA codificações diferentes são usadas no mesmo sistema, elas também são, normalmente, páginas de
código como 'cp850' ou 'cp1252' , que suportam só o ASCII com 127 caracteres adicionais (que
por sua vez são diferentes de uma codificação para a outra). Assim, usuários de Windows tem muito
mais chances de cometer erros de codificação, a menos que sejam muito cuidadosos.

Resumindo, a configuração de codificação mais importante devolvida por locale.getpreferredencoding() é a


default para abrir arquivos de texto e para sys.stdout/stdin/stderr , quando eles são redirecionados para
arquivos. Entretanto, a documentação (https://docs.python.org/pt-br/3/library/locale.html#locale.getpreferredencoding) diz (em
parte):

“ locale.getpreferredencoding(do_setlocale=True)

Retorna a codificação da localidade usada para dados de texto, de acordo com as preferências
do usuário. As preferências do usuário são expressas de maneira diferente em sistemas
diferentes e podem não estar disponíveis programaticamente em alguns sistemas, portanto,
essa função retorna apenas uma estimativa. […​]

Assim, o melhor conselho sobre defaults de codificação é: não confie neles.

Você evitará muitas dores de cabeça se seguir o conselho do sanduíche de Unicode, e sempre tratar codificações de
forma explícita em seus programas. Infelizmente, o Unicode é trabalhoso mesmo se você converter seus bytes para
str corretamente. As duas próximas seções tratam de assuntos que são simples no reino do ASCII, mas ficam muito
complexos no planeta Unicode: normalização de texto (isto é, transformar o texto em uma representação uniforme
para comparações) e ordenação.

4.7. Normalizando o Unicode para comparações confiáveis


Comparações de strings são dificultadas pelo fato do Unicode ter combinações de caracteres: sinais diacríticos e outras
marcações que são anexadas aos caractere anterior, ambos aparecendo juntos como um só caractere quando
impressos.

Por exemplo, a palavra "café" pode ser composta de duas formas, usando quatro ou cinco pontos de código, mas o
resultado parece exatamente o mesmo:

PYCON
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False

Colocar COMBINING ACUTE ACCENT (U+0301) após o "e" resulta em "é". No padrão Unicode, sequências como 'é' e
'e\u0301' são chamadas de "equivalentes canônicas", e se espera que as aplicações as tratem como iguais. Mas o
Python vê duas sequências de pontos de código diferentes, e não as considera iguais.

A solução é a unicodedata.normalize() . O primeiro argumento para essa função é uma dessas quatro strings:
'NFC' , 'NFD' , 'NFKC' , e 'NFKD' . Vamos começar pelas duas primeiras.

A Forma Normal C (NFC) combina os ponto de código para produzir a string equivalente mais curta, enquanto a NFD
decompõe, expandindo os caracteres compostos em caracteres base e separando caracteres combinados. Ambas as
normalizações fazem as comparações funcionarem da forma esperada, como mostra o próximo exemplo:

PYCON
>>> from unicodedata import normalize
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) == normalize('NFC', s2)
True
>>> normalize('NFD', s1) == normalize('NFD', s2)
True

Drivers de teclado normalmente geram caracteres compostos, então o texto digitado pelos usuários estará na NFC por
default. Entretanto, por segurança, pode ser melhor normalizar as strings com normalize('NFC', user_text) antes
de salvá-las A NFC também é a forma de normalização recomendada pelo W3C em "Character Model for the World
Wide Web: String Matching and Searching" (Um Modelo de Caracteres para a World Wide Web: Correspondência de
Strings e Busca) (https://fpy.li/4-15) (EN).

Alguns caracteres singulares são normalizados pela NFC em um outro caractere singular. O símbolo para o ohm (Ω), a
unidade de medida de resistência elétrica, é normalizado para a letra grega ômega maiúscula. Eles são visualmente
idênticos, mas diferentes quando comparados, então a normalizaçào é essencial para evitar surpresas:
PYCON
>>> from unicodedata import normalize, name
>>> ohm = '\u2126'
>>> name(ohm)
'OHM SIGN'
>>> ohm_c = normalize('NFC', ohm)
>>> name(ohm_c)
'GREEK CAPITAL LETTER OMEGA'
>>> ohm == ohm_c
False
>>> normalize('NFC', ohm) == normalize('NFC', ohm_c)
True

As outras duas formas de normalização são a NFKC e a NFKD, a letra K significando "compatibilidade". Essas são
formas mais fortes de normalizaçào, afetando os assim chamados "caracteres de compatibilidade". Apesar de um dos
objetivos do Unicode ser a existência de um único ponto de código "canônico" para cada caractere, alguns caracteres
aparecem mais de uma vez, para manter compatibilidade com padrões pré-existentes. Por exemplo, o MICRO SIGN , µ
( U+00B5 ), foi adicionado para permitir a conversão bi-direcional com o latin1 , que o inclui, apesar do mesmo
caractere ser parte do alfabeto grego com o ponto de código U+03BC ( GREEK SMALL LETTER MU ). Assim, o símbolo de
micro é considerado um "caractere de compatibilidade".

Nas formas NFKC e NFKD, cada caractere de compatibilidade é substituído por uma "decomposição de
compatibilidade" de um ou mais caracteres, que é considerada a representação "preferencial", mesmo se ocorrer
alguma perda de formatação—idealmente, a formatação deveria ser responsabilidade de alguma marcação externa,
não parte do Unicode. Para exemplificar, a decomposição de compatibilidade da fração um meio, '½' ( U+00BD ), é a
sequência de três caracteres '1/2' , e a decomposição de compatibilidade do símbolo de micro, 'µ' ( U+00B5 ), é o mu
minúsculo, 'μ' ( U+03BC ).[46]

É assim que a NFKC funciona na prática:

PYCON
>>> from unicodedata import normalize, name
>>> half = '\N{VULGAR FRACTION ONE HALF}'
>>> print(half)
½
>>> normalize('NFKC', half)
'1⁄2'
>>> for char in normalize('NFKC', half):
... print(char, name(char), sep='\t')
...
1 DIGIT ONE
⁄ FRACTION SLASH
2 DIGIT TWO
>>> four_squared = '4²'
>>> normalize('NFKC', four_squared)
'42'
>>> micro = 'µ'
>>> micro_kc = normalize('NFKC', micro)
>>> micro, micro_kc
('µ', 'μ')
>>> ord(micro), ord(micro_kc)
(181, 956)
>>> name(micro), name(micro_kc)
('MICRO SIGN', 'GREEK SMALL LETTER MU')

Ainda que '1⁄2' seja um substituto razoável para '½' , e o símbolo de micro ser realmente a letra grega mu
minúscula, converter '4²' para '42' muda o sentido. Uma aplicação poderia armazenar '4²' como
'4<sup>2</sup>' , mas a função normalize não sabe nada sobre formatação. Assim, NFKC ou NFKD podem perder
ou distorcer informações, mas podem produzir representações intermediárias convenientes para buscas ou indexação.
Infelizmente, com o Unicode tudo é sempre mais complicado do que parece à primeira vista. Para o VULGAR FRACTION
ONE HALF , a normalização NFKC produz 1 e 2 unidos pelo FRACTION SLASH , em vez do SOLIDUS , também conhecido
como "barra" ("slash" em inglês)—o familiar caractere com código decimal 47 em ASCII. Portanto, buscar pela
sequência ASCII de três caracteres '1/2' não encontraria a sequência Unicode normalizada.

⚠️ AVISO As normalizações NFKC e NFKD causam perda de dados e devem ser aplicadas apenas em casos
especiais, como busca e indexação, e não para armazenamento permanente do texto.

Ao preparar texto para busca ou indexação, há outra operação útil: case folding (NT: algo como "dobra" ou "mudança"
de caixa), nosso próximo assunto.

4.7.1. Case Folding


Case folding é essencialmente a conversão de todo o texto para minúsculas, com algumas transformações adicionais. A
operação é suportada pelo método str.casefold() .

Para qualquer string s contendo apenas caracteres latin1 , s.casefold() produz o mesmo resultado de
s.lower() , com apenas duas exceções—o símbolo de micro, 'µ' , é trocado pela letra grega mu minúscula (que é
exatamente igual na maioria das fontes) e a letra alemã Eszett (ß), também chamada "s agudo" (scharfes S) se torna "ss":

PYCON
>>> micro = 'µ'
>>> name(micro)
'MICRO SIGN'
>>> micro_cf = micro.casefold()
>>> name(micro_cf)
'GREEK SMALL LETTER MU'
>>> micro, micro_cf
('µ', 'μ')
>>> eszett = 'ß'
>>> name(eszett)
'LATIN SMALL LETTER SHARP S'
>>> eszett_cf = eszett.casefold()
>>> eszett, eszett_cf
('ß', 'ss')

Há quase 300 pontos de código para os quais str.casefold() e str.lower() devolvem resultados diferentes.

Como acontece com qualquer coisa relacionada ao Unicode, case folding é um tópico complexo, com muitos casos
linguísticos especiais, mas o grupo central de desenvolvedores do Python fez um grande esforço para apresentar uma
solução que, espera-se, funcione para a maioria dos usuários.

Nas próximas seções vamos colocar nosso conhecimento sobre normalização para trabalhar, desenvolvendo algumas
funções utilitárias.

4.7.2. Funções utilitárias para correspondência de texto normalizado


Como vimos, é seguro usar a NFC e a NFD, e ambas permitem comparações razoáveis entre strings Unicode. A NFC é a
melhor forma normalizada para a maioria das aplicações, e str.casefold() é a opção certa para comparações
indiferentes a maiúsculas/minúsculas.

Se você precisa lidar com texto em muitas línguas diferentes, seria muito útil acrescentar às suas ferramentas de
trabalho um par de funções como nfc_equal e fold_equal , do Exemplo 13.

Exemplo 13. normeq.py: normalized Unicode string comparison


PYTHON3
"""
Utility functions for normalized Unicode string comparison.

Using Normal Form C, case sensitive:

>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1 == s2
False
>>> nfc_equal(s1, s2)
True
>>> nfc_equal('A', 'a')
False

Using Normal Form C with case folding:

>>> s3 = 'Straße'
>>> s4 = 'strasse'
>>> s3 == s4
False
>>> nfc_equal(s3, s4)
False
>>> fold_equal(s3, s4)
True
>>> fold_equal(s1, s2)
True
>>> fold_equal('A', 'a')
True

"""

from unicodedata import normalize

def nfc_equal(str1, str2):


return normalize('NFC', str1) == normalize('NFC', str2)

def fold_equal(str1, str2):


return (normalize('NFC', str1).casefold() ==
normalize('NFC', str2).casefold())

Além da normalização e do case folding do Unicode—ambos partes desse padrão—algumas vezes faz sentido aplicar
transformações mais profundas, como por exemplo mudar 'café' para 'cafe' . Vamos ver quando e como na
próxima seção.

4.7.3. "Normalização" extrema: removendo sinais diacríticos


O tempero secreto da busca do Google inclui muitos truques, mas um deles aparentemente é ignorar sinais diacríticos
(acentos e cedilhas, por exemplo), pelo menos em alguns contextos. Remover sinais diacríticos não é uma forma
regular de normalização, pois muitas vezes muda o sentido das palavras e pode produzir falsos positivos em uma
busca. Mas ajuda a lidar com alguns fatos da vida: as pessoas às vezes são preguiçosas ou desconhecem o uso correto
dos sinais diacríticos, e regras de ortografia mudam com o tempo, levando acentos a desaparecerem e reaparecerem
nas línguas vivas.

Além do caso da busca, eliminar os acentos torna as URLs mais legíveis, pelo menos nas línguas latinas. Veja a URL do
artigo da Wikipedia sobre a cidade de São Paulo:

https://en.wikipedia.org/wiki/S%C3%A3o_Paulo
O trecho %C3%A3 é a renderização em UTF-8 de uma única letra, o "ã" ("a" com til). A forma a seguir é muito mais fácil
de reconhecer, mesmo com a ortografia incorreta:

https://en.wikipedia.org/wiki/Sao_Paulo

Para remover todos os sinais diacríticos de uma str , você pode usar uma função como a do Exemplo 14.

Exemplo 14. simplify.py: função para remover todas as marcações combinadas

PY
import unicodedata
import string

def shave_marks(txt):
"""Remove all diacritic marks"""
norm_txt = unicodedata.normalize('NFD', txt) # (1)
shaved = ''.join(c for c in norm_txt
if not unicodedata.combining(c)) # (2)
return unicodedata.normalize('NFC', shaved) # (3)

1. Decompõe todos os caracteres em caracteres base e marcações combinadas.


2. Filtra e retira todas as marcações combinadas.
3. Recompõe todos os caracteres.

Exemplo 15 mostra alguns usos para shave_marks .

Exemplo 15. Dois exemplos de uso da shave_marks do Exemplo 14

PYCON
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> shave_marks(order)
'“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”' (1)
>>> Greek = 'Ζέφυρος, Zéfiro'
>>> shave_marks(Greek)
'Ζεφυρος, Zefiro' (2)

1. Apenas as letras "è", "ç", e "í" foram substituídas.


2. Tanto "έ" quando "é" foram substituídas.

A função shave_marks do Exemplo 14 funciona bem, mas talvez vá longe demais. Frequentemente, a razão para
remover os sinais diacríticos é transformar texto de uma língua latina para ASCII puro, mas shave_marks também
troca caracteres não-latinos—​como letras gregas—​que nunca se tornarão ASCII apenas pela remoção de seus acentos.
Então faz sentido analisar cada caractere base e remover as marcações anexas apenas se o caractere base for uma letra
do alfabeto latino. É isso que o Exemplo 16 faz.

Exemplo 16. Função para remover marcações combinadas de caracteres latinos (comando de importação omitidos, pois
isso é parte do módulo simplify.py do Exemplo 14)
PY
def shave_marks_latin(txt):
"""Remove all diacritic marks from Latin base characters"""
norm_txt = unicodedata.normalize('NFD', txt) # (1)
latin_base = False
preserve = []
for c in norm_txt:
if unicodedata.combining(c) and latin_base: # (2)
continue # ignore diacritic on Latin base char
preserve.append(c) # (3)
# if it isn't a combining char, it's a new base char
if not unicodedata.combining(c): # (4)
latin_base = c in string.ascii_letters
shaved = ''.join(preserve)
return unicodedata.normalize('NFC', shaved) # (5)

1. Decompõe todos os caracteres em caracteres base e marcações combinadas.


2. Pula as marcações combinadas quando o caractere base é latino.
3. Caso contrário, mantém o caractere original.
4. Detecta um novo caractere base e determina se ele é latino.
5. Recompõe todos os caracteres.

Um passo ainda mais radical substituiria os símbolos comuns em textos de línguas ocidentais (por exemplo, aspas
curvas, travessões, os círculos de bullet points, etc) em seus equivalentes ASCII . É isso que a função asciize faz no
Exemplo 17.

Exemplo 17. Transforma alguns símbolos tipográficos ocidentais em ASCII (este trecho também é parte do simplify.py
do Exemplo 14)

PY
single_map = str.maketrans("""‚ƒ„ˆ‹‘’“”•–—˜›""", # (1)
"""'f"^<''""---~>""")

multi_map = str.maketrans({ # (2)


'€': 'EUR',
'…': '...',
'Æ': 'AE',
'æ': 'ae',
'Œ': 'OE',
'œ': 'oe',
'™': '(TM)',
'‰': '<per mille>',
'†': '**',
'‡': '***',
})

multi_map.update(single_map) # (3)

def dewinize(txt):
"""Replace Win1252 symbols with ASCII chars or sequences"""
return txt.translate(multi_map) # (4)

def asciize(txt):
no_marks = shave_marks_latin(dewinize(txt)) # (5)
no_marks = no_marks.replace('ß', 'ss') # (6)
return unicodedata.normalize('NFKC', no_marks) # (7)
1. Cria uma tabela de mapeamento para substituição de caractere para caractere.
2. Cria uma tabela de mapeamento para substituição de string para caractere.
3. Funde as tabelas de mapeamento.
4. dewinize não afeta texto em ASCII ou latin1 , apenas os acréscimos da Microsoft ao latin1 no cp1252 .

5. Aplica dewinize e remove as marcações de sinais diacríticos.


6. Substitui o Eszett por "ss" (não estamos usando case folding aqui, pois queremos preservar maiúsculas e
minúsculas).
7. Aplica a normalização NFKC para compor os caracteres com seus pontos de código de compatibilidade.

O Exemplo 18 mostra a asciize em ação.

Exemplo 18. Dois exemplos usando asciize , do Exemplo 17

PYCON
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> dewinize(order)
'"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."' (1)
>>> asciize(order)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."' (2)

1. dewinize substitui as aspas curvas, os bullets, e o ™ (símbolo de marca registrada).


2. asciize aplica dewinize , remove os sinais diacríticos e substiui o 'ß' .

Cada língua tem suas próprias regras para remoção de sinais diacríticos. Por exemplo, os alemães
⚠️ AVISO trocam o 'ü' por 'ue' . Nossa função asciize não é tão refinada, então pode ou não ser
adequada para a sua língua. Contudo, ela é aceitável para o português.

Resumindo, as funções em simplify.py vão bem além da normalização padrão, e realizam um cirurgia profunda no
texto, com boas chances de mudar seu sentido. Só você pode decidir se deve ir tão longe, conhecendo a língua alvo, os
seus usuários e a forma como o texto transformado será utilizado.

Isso conclui nossa discussão sobre normalização de texto Unicode.

Vamos agora ordenar nossos pensamentos sobre ordenação no Unicode.

4.8. Ordenando texto Unicode


O Python ordena sequências de qualquer tipo comparando um por um os itens em cada sequência. Para strings, isso
significa comparar pontos de código. Infelizmente, isso produz resultados inaceitáveis para qualquer um que use
caracteres não-ASCII.

Considere ordenar uma lista de frutas cultivadas no Brazil:

PYCON
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
As regras de ordenação variam entre diferentes locales, mas em português e em muitas línguas que usam o alfabeto
latino, acentos e cedilhas raramente fazem diferença na ordenação.[47] Então "cajá" é lido como "caja," e deve vir antes
de "caju."

A lista fruits ordenada deveria ser:

PYTHON3
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

O modo padrão de ordenar texto não-ASCII em Python é usar a função locale.strxfrm que, de acordo com a
documentação do módulo locale (https://docs.python.org/pt-br/3/library/locale.html?highlight=strxfrm#locale.strxfrm),
"Transforma uma string em uma que pode ser usada em comparações com reconhecimento de localidade."

Para poder usar locale.strxfrm , você deve primeiro definir um locale adequado para sua aplicação, e rezar para
que o SO o suporte. A sequência de comando no Exemplo 19 pode funcionar para você.

Exemplo 19. locale_sort.py: using the locale.strxfrm function as the sort key

PYTHON3
import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale)
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)

Executando o Exemplo 19 no GNU/Linux (Ubuntu 19.10) com o locale pt_BR.UTF-8 instalado, consigo o resultado
correto:

PYTHON3
'pt_BR.UTF-8'
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Portanto, você precisa chamar setlocale(LC_COLLATE, «your_locale») antes de usar locale.strxfrm como a
chave de ordenação.

Porém, aqui vão algumas ressalvas:

Como as configurações de locale são globais, não é recomendado chamar setlocale em uma biblioteca. Sua
aplicação ou framework deveria definir o locale no início do processo, e não mudá-lo mais depois disso.
O locale desejado deve estar instalado no SO, caso contrário setlocale gera uma exceção de locale.Error:
unsupported locale setting .

Você tem que saber como escrever corretamente o nome do locale.


O locale precisa ser corretamente implementado pelos desenvolvedores do SO. Tive sucesso com o Ubuntu 19.10,
mas não no macOS 10.14. No macOS, a chamada setlocale(LC_COLLATE, 'pt_BR.UTF-8') devolve a string
'pt_BR.UTF-8' sem qualquer reclamação. Mas sorted(fruits, key=locale.strxfrm) produz o mesmo
resultado incorreto de sorted(fruits) . Também tentei os locales fr_FR , es_ES , e de_DE no macOS, mas
locale.strxfrm nunca fez seu trabalho direito.[48]

Portanto, a solução da biblioteca padrão para ordenação internacionalizada funciona, mas parece ter suporte
adequado apenas no GNU/Linux (talvez também no Windows, se você for um especialista). Mesmo assim, ela depende
das configurações do locale, criando dores de cabeça na implantação.
Felizmente, há uma solução mais simples: a biblioteca pyuca, disponivel no PyPI.

4.8.1. Ordenando com o Algoritmo de Ordenação do Unicode


James Tauber, contribuidor muito ativo do Django, deve ter sentido essa nossa mesma dor, e criou a pyuca
(https://fpy.li/4-17), uma implementação integralmente em Python do Algoritmo de Ordenação do Unicode (UCA, sigla em
inglês para Unicode Collation Algorithm). O Exemplo 20 mostra como ela é fácil de usar.

Exemplo 20. Using the pyuca.Collator.sort_key method

PYCON
>>> import pyuca
>>> coll = pyuca.Collator()
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=coll.sort_key)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Isso é simples e funciona no GNU/Linux, no macOS, e no Windows, pelo menos com a minha pequena amostra.

A pyuca não leva o locale em consideração. Se você precisar personalizar a ordenação, pode fornecer um caminho
para uma tabela própria de ordenação para o construtor Collator() . Sem qualquer configuração adicional, a
biblioteca usa o allkeys.txt (https://fpy.li/4-18), incluído no projeto. Esse arquivo é apenas uma cópia da Default Unicode
Collation Element Table (Tabela Default de Ordenação de Elementos Unicode) (https://fpy.li/4-19) do Unicode.org .

PyICU: A recomendação do Miro para ordenação com Unicode


(O revisor técnico Miroslav Šedivý é um poliglota e um especialista em Unicode. Eis o que ele
escreveu sobre a pyuca.)

👉 DICA A pyuca tem um algoritmo de ordenação que não respeita o padrão de ordenação de linguagens
individuais. Por exemplo, [a letra] Ä em alemão fica entre o A e o B, enquanto em sueco ela vem
depois do Z. Dê uma olhada na PyICU (https://fpy.li/4-20), que funciona como locale sem modificar o
locale do processo. Ela também é necessária se você quiser mudar a capitalização de iİ/ıI em turco. A
PyICU inclui uma extensão que precisa ser compilada, então pode ser mais difícil de instalar em
alguns sistemas que a pyuca, que é toda feita em Python.

E por sinal, aquela tabela de ordenação é um dos muitos arquivos de dados que formam o banco de dados do Unicode,
nosso próximo assunto.

4.9. O banco de dados do Unicode


O padrão Unicode fornece todo um banco de dados—na forma de vários arquivos de texto estruturados—que inclui
não apenas a tabela mapeando pontos de código para nomes de caracteres, mas também metadados sobre os
caracteres individuais e como eles se relacionam. Por exemplo, o banco de dados do Unicode registra se um caractere
pode ser impresso, se é uma letra, um dígito decimal ou algum outro símbolo numérico. É assim que os métodos de
str isalpha , isprintable , isdecimal e isnumeric funcionam. str.casefold também usa informação de uma
tabela do Unicode.
A função unicodedata.category(char) devolve uma categoria de char com duas letras, do
banco de dados do Unicode. Os métodos de alto nível de str são mais fáceis de usar. Por exemplo,
✒️ NOTA label.isalpha() (https://docs.python.org/pt-br/3.10/library/stdtypes.html#str.isalpha) devolve True se
todos os caracteres em label pertencerem a uma das seguintes categorias: Lm , Lt , Lu , Ll , or
Lo . Para descobrir o que esses códigos significam, veja "General Category" (https://fpy.li/4-22) (EN) no
artigo "Unicode character property" (https://fpy.li/4-23) (EN) da Wikipedia em inglês.

4.9.1. Encontrando caracteres por nome


O módulo unicodedata tem funções para obter os metadados de caracteres, incluindo unicodedata.name() , que
devolve o nome oficial do caractere no padrão. A Figura 5 demonstra essa função.[49]

Figura 5. Explorando unicodedata.name() no console do Python.

Você pode usar a função name() para criar aplicações que permitem aos usuários buscarem caracteres por nome. A
Figura 6 demonstra o script de comando de linha cf.py, que recebe como argumentos uma ou mais palavras, e lista os
caracteres que tem aquelas palavras em seus nomes Unicode oficiais. O código fonte completo de cf.py aparece no
Exemplo 21.

Figura 6. Usando cf.py para encontrar gatos sorridentes.

O suporte a emojis varia muito entre sistemas operacionais e aplicativos. Nos últimos anos, o
terminal do macOS tem oferecido o melhor suporte para emojis, seguido por terminais gráficos
GNU/Linux modernos. O cmd.exe e o PowerShell do Windows agora suportam saída Unicode, mas
⚠️ AVISO enquanto escrevo essa seção, em janeiro de 2020, eles ainda não mostram emojis—pelo menos não
sem configurações adicionais. O revisor técnico Leonardo Rochael me falou sobre um novo terminal
para Windows da Microsoft (https://fpy.li/4-24), de código aberto, que pode ter um suporte melhor a
Unicode que os consoles antigos da Microsoft. Não tive tempo de testar.

No Exemplo 21, observe que o comando if , na função find , usa o método .issubset() para testar rapidamente se
todas as palavras no conjunto query aparecem na lista de palavras criada a partir do nome do caractere. Graças à rica
API de conjuntos do Python, não precisamos de um loop for aninhado e de outro if para implementar essa
verificação

Exemplo 21. cf.py: o utilitário de busca de caracteres


PYTHON3
#!/usr/bin/env python3
import sys
import unicodedata

START, END = ord(' '), sys.maxunicode + 1 # (1)

def find(*query_words, start=START, end=END): # (2)


query = {w.upper() for w in query_words} # (3)
for code in range(start, end):
char = chr(code) # (4)
name = unicodedata.name(char, None) # (5)
if name and query.issubset(name.split()): # (6)
print(f'U+{code:04X}\t{char}\t{name}') # (7)

def main(words):
if words:
find(*words)
else:
print('Please provide words to find.')

if __name__ == '__main__':
main(sys.argv[1:])

1. Configura os defaults para a faixa de pontos de código da busca.


2. find aceita query_words e somente argumentos nomeados (opcionais) para limitar a faixa da busca, facilitando
os testes.
3. Converte query_words em um conjunto de strings capitalizadas.
4. Obtém o caractere Unicode para code .

5. Obtém o nome do caractere, ou None se o ponto de código não estiver atribuído a um caractere.
6. Se há um nome, separa esse nome em uma lista de palavras, então verifica se o conjunto query é um subconjunto
daquela lista.
7. Mostra uma linha com o ponto de código no formato U+9999 , o caractere e seu nome.

O módulo unicodedata tem outras funções interessantes. A seguir veremos algumas delas, relacionadas a obter
informação de caracteres com sentido numérico.

4.9.2. O sentido numérico de caracteres


O módulo unicodedata inclui funções para determinar se um caractere Unicode representa um número e, se for esse
o caso, seu valor numérico em termos humanos—em contraste com o número de seu ponto de código.

O Exemplo 22 demonstra o uso de unicodedata.name() e unicodedata.numeric() , junto com os métodos


.isdecimal() e .isnumeric() de str .

Exemplo 22. Demo do banco de dados Unicode de metadados de caracteres numéricos (as notas explicativas descrevem
cada coluna da saída)
PY
import unicodedata
import re

re_digit = re.compile(r'\d')

sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'

for char in sample:


print(f'U+{ord(char):04x}', # (1)
char.center(6), # (2)
're_dig' if re_digit.match(char) else '-', # (3)
'isdig' if char.isdigit() else '-', # (4)
'isnum' if char.isnumeric() else '-', # (5)
f'{unicodedata.numeric(char):5.2f}', # (6)
unicodedata.name(char), # (7)
sep='\t')

1. Ponto de código no formato U+0000 .

2. O caractere, centralizado em uma str de tamanho 6.


3. Mostra re_dig se o caractere casa com a regex r'\d' .

4. Mostra isdig se char.isdigit() é True .

5. Mostra isnum se char.isnumeric() é True .

6. Valor numérico formatado com tamanho 5 e duas casa decimais.


7. O nome Unicode do caractere.

Executar o Exemplo 22 gera a Figura 7, se a fonte do seu terminal incluir todos aqueles símbolos.

Figura 7. Terminal do macOS mostrando os caracteres numéricos e metadados correspondentes; re_dig significa que
o caractere casa com a expressão regular r'\d' .

A sexta coluna da Figura 7 é o resultado da chamada a unicodedata.numeric(char) com o caractere. Ela mostra que
o Unicode sabe o valor numérico de símbolos que representam números. Assim, se você quiser criar uma aplicação de
planilha que suporta dígitos tamil ou numerais romanos, vá fundo!

A Figura 7 mostra que a expressão regular r'\d' casa com o dígito "1" e com o dígito devanágari 3, mas não com
alguns outros caracteres considerados dígitos pela função isdigit . O módulo re não é tão conhecedor de Unicode
quanto deveria ser. O novo módulo regex , disponível no PyPI, foi projetado para um dia substituir o re , e fornece um
suporte melhor ao Unicode.[50] Voltaremos ao módulo re na próxima seção.
Ao longo desse capítulo, usamos várias funções de unicodedata , mas há muitas outras que não mencionamos. Veja a
documentação da biblioteca padrão para o módulo unicodedata (https://docs.python.org/pt-br/3/library/unicodedata.html).

A seguir vamos dar uma rápida passada pelas APIs de modo dual, com funções que aceitam argumentos str ou
bytes e dão a eles tratamento especial dependendo do tipo.

4.10. APIs de modo dual para str e bytes


A biblioteca padrão do Python tem funções que aceitam argumentos str ou bytes e se comportam de forma
diferente dependendo do tipo recebido. Alguns exemplos podem ser encontrados nos módulos re e os .

4.10.1. str versus bytes em expressões regulares


Se você criar uma expressão regular com bytes , padrões tal como \d e \w vão casar apenas com caracteres ASCII;
por outro lado, se esses padrões forem passados como str , eles vão casar com dígitos Unicode ou letras além do ASCII.
O Exemplo 23 e a Figura 8 comparam como letras, dígitos ASCII, superescritos e dígitos tamil casam em padrões str e
bytes .

Exemplo 23. ramanujan.py: compara o comportamento de expressões regulares simples como str e como bytes

PY
import re

re_numbers_str = re.compile(r'\d+') # (1)


re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+') # (2)
re_words_bytes = re.compile(rb'\w+')

text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef" # (3)


" as 1729 = 1³ + 12³ = 9³ + 10³.") # (4)

text_bytes = text_str.encode('utf_8') # (5)

print(f'Text\n {text_str!r}')
print('Numbers')
print(' str :', re_numbers_str.findall(text_str)) # (6)
print(' bytes:', re_numbers_bytes.findall(text_bytes)) # (7)
print('Words')
print(' str :', re_words_str.findall(text_str)) # (8)
print(' bytes:', re_words_bytes.findall(text_bytes)) # (9)

1. As duas primeiras expressões regulares são do tipo str .

2. As duas últimas são do tipo bytes .

3. Texto Unicode para ser usado na busca, contendo os dígitos tamil para 1729 (a linha lógica continua até o símbolo
de fechamento de parênteses).
4. Essa string é unida à anterior no momento da compilação (veja "2.4.2. String literal concatenation" (Concatenação
de strings literais) (https://docs.python.org/pt-br/3/reference/lexical_analysis.html#string-literal-concatenation) em A Referência
da Linguagem Python).
5. Uma string bytes é necessária para a busca com as expressões regulares bytes .

6. O padrão str r'\d+' casa com os dígitos ASCII e tamil.


7. O padrão bytes rb'\d+' casa apenas com os bytes ASCII para dígitos.
8. O padrão str r'\w+' casa com letras, superescritos e dígitos tamil e ASCII.
9. O padrão bytes rb'\w+' casa apenas com bytes ASCII para letras e dígitos.
Figura 8. Captura de tela da execução de ramanujan.py do Exemplo 23.

O Exemplo 23 é um exemplo trivial para destacar um ponto: você pode usar expressões regulares com str ou bytes ,
mas nesse último caso os bytes fora da faixa do ASCII são tratados como caracteres que não representam dígitos nem
palavras.

Para expressões regulares str , há uma marcação re.ASCII , que faz \w , \W , \b , \B , \d , \D , \s , e \S


executarem um casamento apenas com ASCII. Veja a documentaçào do módulo re
(https://docs.python.org/pt-br/3/library/re.html) para maiores detalhes.

Outro módulo importante é o os .

4.10.2. str versus bytes nas funções de os


O kernel do GNU/Linux não conhece Unicode então, no mundo real, você pode encontrar nomes de arquivo compostos
de sequências de bytes que não são válidas em nenhum esquema razoável de codificação, e não podem ser
decodificados para str . Servidores de arquivo com clientes usando uma variedade de diferentes SOs são
particularmente inclinados a apresentar esse cenário.

Para mitigar esse problema, todas as funções do módulo os que aceitam nomes de arquivo ou caminhos podem
receber seus argumentos como str ou bytes . Se uma dessas funções é chamada com um argumento str , o
argumento será automaticamente convertido usando o codec informado por sys.getfilesystemencoding() , e a
resposta do SO será decodificada com o mesmo codec. Isso é quase sempre o que se deseja, mantendo a melhor prática
do sanduíche de Unicode.

Mas se você precisa lidar com (e provavelmente corrigir) nomes de arquivo que não podem ser processados daquela
forma, você pode passar argumentos bytes para as funções de os , e receber bytes de volta. Esse recurso permite
que você processe qualquer nome de arquivo ou caminho, independende de quantos gremlins encontrar. Veja o
Exemplo 24.

Exemplo 24. listdir com argumentos str e bytes , e os resultados

PYCON
>>> os.listdir('.') # (1)
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.') # (2)
[b'abc.txt', b'digits-of-\xcf\x80.txt']

1. O segundo nome de arquivo é "digits-of-π.txt" (com a letra grega pi).


2. Dado um argumento byte , listdir devolve nomes de arquivos como bytes: b'\xcf\x80' é a codificação UTF-8
para a letra grega pi.
Para ajudar no processamento manual de sequências str ou bytes que são nomes de arquivos ou caminhos, o
módulo os fornece funções especiais de codificação e decodificação, os.fsencode(name_or_path) e
os.fsdecode(name_or_path) . Ambas as funções aceitam argumentos dos tipos str , bytes ou, desde o Python 3.6,
um objeto que implemente a interface os.PathLike .

O Unicode é um buraco de coelho bem fundo. É hora de encerrar nossa exploração de str e bytes .

4.11. Resumo do capítulo


Começamos o capítulo descartando a noção de que 1 caractere == 1 byte . A medida que o mundo adota o Unicode,
precisamos manter o conceito de strings de texto separado das sequências binárias que as representam em arquivos, e
o Python 3 aplica essa separação.

Após uma breve passada pelos tipos de dados sequências binárias— bytes , bytearray , e memoryview —,
mergulhamos na codificação e na decodificação, com uma amostragem dos codec importantes, seguida por abordagens
para prevenir ou lidar com os abomináveis UnicodeEncodeError , UnicodeDecodeError e os SyntaxError causados
pela codificação errada em arquivos de código-fonte do Python.

A seguir consideramos a teoria e a prática de detecção de codificação na ausência de metadados: em teoria, não pode
ser feita, mas na prática o pacote Chardet consegue realizar esse feito para uma grande quantidade de codificações
populares. Marcadores de ordem de bytes foram apresentados como a única dica de codificação encontrada em
arquivos UTF-16 e UTF-32—​algumas vezes também em arquivos UTF-8.

Na seção seguinte, demonstramos como abrir arquivos de texto, uma tarefa fácil exceto por uma armadilha: o
argumento nomeado encoding= não é obrigatório quando se abre um arquivo de texto, mas deveria ser. Se você não
especificar a codificação, terminará com um programa que consegue produzir "texto puro" que é incompatível entre
diferentes plataformas, devido a codificações default conflitantes. Expusemos então as diferentes configurações de
codificação usadas pelo Python, e como detectá-las.

Uma triste realidade para usuários de Windows é o fato dessas configurações muitas vezes terem valores diferentes
dentro da mesma máquina, e desses valores serem mutuamente incompatíveis; usuários do GNU/Linux e do macOS,
por outro lado, vivem em um lugar mais feliz, onde o UTF-8 é o default por (quase) toda parte.

O Unicode fornece múltiplas formas de representar alguns caracteres, então a normalização é um pré-requisito para a
comparação de textos. Além de explicar a normalização e o case folding, apresentamos algumas funções úteis que
podem ser adapatadas para as suas necessidades, incluindo transformações drásticas como a remoção de todos os
acentos. Vimos como ordenar corretamente texto Unicode, usando o módulo padrão locale —com algumas restrições
—e uma alternativa que não depende de complexas configurações de locale: a biblioteca externa pyuca.

Usamos o banco de dados do Unicode para programar um utilitário de comando de linha que busca caracteres por
nome—​em 28 linha de código, graças ao poder do Python. Demos uma olhada em outros metadados do Unicode, e
vimos rapidamente as APIs de modo dual, onde algumas funções podem ser chamadas com argumentos str ou
bytes , produzindo resultados diferentes.

4.12. Leitura complementar


A palestra de Ned Batchelder na PyCon US 2012, "Pragmatic Unicode, or, How Do I Stop the Pain?" (Unicode Pragmático,
ou, Como Eu Fiz a Dor Sumir?) (https://fpy.li/4-28) (EN), foi marcante. Ned é tão profissional que forneceu uma transcrição
completa da palestra, além dos slides e do vídeo.

"Character encoding and Unicode in Python: How to (╯°□°)╯︵ ┻━┻ with dignity" (Codificação de caracteres e o Unicode
no Python: como (╯°□°)╯︵ ┻━┻ com dignidade) (slides (https://fpy.li/4-1), vídeo (https://fpy.li/4-2)) (EN) foi uma excelente
palestra de Esther Nam e Travis Fischer na PyCon 2014, e foi onde encontrei a concisa epígrafe desse capítulo:
"Humanos usam texto. Computadores falam em bytes."
Lennart Regebro—​um dos revisores técnicos da primeira edição desse livro—​compartilha seu "Useful Mental Model of
Unicode (UMMU)" (Modo Mental Útil do Unicode) em um post curto, "Unconfusing Unicode: What Is Unicode?"
(Desconfundindo o Unicode: O Que É O Unicode?) (https://fpy.li/4-31) (EN). O Unicode é um padrão complexo, então o
UMMU de Lennart é realmente um ponto de partida útil.

O "Unicode HOWTO" (https://docs.python.org/pt-br/3/howto/unicode.html) oficial na documentação do Python aborda o


assunto por vários ângulos diferentes, de uma boa introdução histórica a detalhes de sintaxe, codecs, expressões
regulares, nomes de arquivo, e boas práticas para E/S sensível ao Unicode (isto é, o sanduíche de Unicode), com vários
links adicionais de referências em cada seção.

O Chapter 4, "Strings" (Capítulo 4, "Strings") (https://fpy.li/4-33), do maravilhosos livro Dive into Python 3 (https://fpy.li/4-34)
(EN), de Mark Pilgrim (Apress), também fornece uma ótima introdução ao suporte a Unicode no Python 3. No mesmo
livro, o Capítulo 15 (https://fpy.li/4-35) descreve como a biblioteca Chardet foi portada do Python 2 para o Python 3, um
valioso estudo de caso, dado que a mudança do antigo tipo str para o novo bytes é a causa da maioria das dores da
migração, e esta é uma preocupação central em uma biblioteca projetada para detectar codificações.

Se você conhece Python 2 mas é novo no Python 3, o artigo "What’s New in Python 3.0" (O quê há de novo no Python 3.0)
(https://fpy.li/4-36) (EN), de Guido van Rossum, tem 15 pontos resumindo as mudanças, com vários links. Guido inicia com
uma afirmação brutal: "Tudo o que você achava que sabia sobre dados binários e Unicode mudou". O post de Armin
Ronacher em seu blog, "The Updated Guide to Unicode on Python" O Guia Atualizado do Unicode no Python
(https://fpy.li/4-37), é bastante profundo e realça algumas das armadilhas do Unicode no Python (Armin não é um grande
fã do Python 3).

O capítulo 2 ("Strings and Text" Strings e Texto) do Python Cookbook, 3rd ed. (https://fpy.li/pycook3) (EN) (O’Reilly), de
David Beazley e Brian K. Jones, tem várias receitas tratando de normalização de Unicode, sanitização de texto, e
execução de operações orientadas para texto em sequências de bytes. O capítulo 5 trata de arquivos e E/S, e inclui a
"Recipe 5.17. Writing Bytes to a Text File" (Receita 5.17. Escrevendo Bytes em um Arquivo de Texto), mostrando que sob
qualquer arquivo de texto há sempre uma sequência binária que pode ser acessada diretamente quando necessário.
Mais tarde no mesmo livro, o módulo struct é usado em "Recipe 6.11. Reading and Writing Binary Arrays of
Structures" (Receita 6.11. Lendo e Escrevendo Arrays Binárias de Estruturas).

O blog "Python Notes" de Nick Coghlan tem dois posts muito relevantes para esse capítulo: "Python 3 and ASCII
Compatible Binary Protocols" (Python 3 e os Protocolos Binários Compatíveis com ASCII) (https://fpy.li/4-38) (EN) e
"Processing Text Files in Python 3" (Processando Arquivos de Texto em Python 3) (https://fpy.li/4-39) (EN). Fortemente
recomendado.

Uma lista de codificações suportadas pelo Python está disponível em "Standard Encodings"
(https://docs.python.org/pt-br/3/library/codecs.html#standard-encodings) (EN), na documentação do módulo codecs . Se você
precisar obter aquela lista de dentro de um programa, pode ver como isso é feito no script /Tools/unicode/listcodecs.py
(https://fpy.li/4-41), que acompanha o código-fonte do CPython.

Oa livros Unicode Explained (https://fpy.li/4-42) (Unicode Explicado) (EN), de Jukka K. Korpela (O’Reilly) e Unicode
Demystified (Unicode Desmistificado) (https://fpy.li/4-43), de Richard Gillam (Addison-Wesley) não são específicos sobre o
Python, nas foram muito úteis para meu estudo dos conceitos do Unicode. Programming with Unicode (Programando
com Unicode) (https://fpy.li/4-44), de Victor Stinner, é um livro gratuito e publicado pelo próprio autor (Creative Commons
BY-SA) tratando de Unicode em geral, bem como de ferramentas e APIs no contexto dos principais sistemas
operacionais e algumas linguagens de programação, incluindo Python.
As páginas do W3C "Case Folding: An Introduction" (Case Folding: Uma Introdução) (https://fpy.li/4-45) (EN) e "Character
Model for the World Wide Web: String Matching" (O Modelo de Caracteres para a World Wide Web: Correspondência de
Strings) (https://fpy.li/4-15) (EN) tratam de conceitos de normalização, a primeira uma suave introdução e a segunda uma
nota de um grupo de trabalho escrita no seco jargão dos padrões—o mesmo tom do "Unicode Standard Annex #15—​
Unicode Normalization Forms" (Anexo 15 do Padrão Unicode—Formas de Normalização do Unicode) (https://fpy.li/4-47)
(EN). A seção "Frequently Asked Questions, Normalization" (Perguntas Frequentes, Normalização) (https://fpy.li/4-48) (EN)
do Unicode.org (https://fpy.li/4-49) é mais fácil de ler, bem como o "NFC FAQ" (https://fpy.li/4-50) (EN) de Mark Davis—​autor de
vários algoritmos do Unicode e presidente do Unicode Consortium quando essa seção foi escrita.

Em 2016, o Museu de Arte Moderna (MoMA) de New York adicionou à sua coleção o emoji original (https://fpy.li/4-51)
(EN), os 176 emojis desenhados por Shigetaka Kurita em 1999 para a NTT DOCOMO—a provedora de telefonia móvel
japonesa. Indo mais longe no passado, a Emojipedia (https://fpy.li/4-52) (EN) publicou o artigo "Correcting the Record on
the First Emoji Set" (Corrigindo o Registro [Histórico] sobre o Primeiro Conjunto de Emojis) (https://fpy.li/4-53) (EN),
atribuindo ao SoftBank do Japão o mais antigo conjunto conhecido de emojis, implantado em telefones celulares em
1997. O conjunto do SoftBank é a fonte de 90 emojis que hoje fazem parte do Unicode, incluindo o U+1F4A9 ( PILE OF
POO ). O emojitracker.com (https://fpy.li/4-54), de Matthew Rothenberg, é um painel ativo mostrando a contagem do uso de
emojis no Twitter, atualizado em tempo real. Quando escrevo isso, FACE WITH TEARS OF JOY (U+1F602) é o emoji mais
popular no Twitter, com mais de 3.313.667.315 ocorrências registradas.

Ponto de vista
Nomes não-ASCII no código-fonte: você deveria usá-los?

O Python 3 permite identificadores não-ASCII no código-fonte:

PYTHON3
>>> ação = 'PBR' # ação = stock
>>> ε = 10**-6 # ε = epsilon

Algumas pessoas não gostam dessa ideia. O argumento mais comum é que se limitar aos caracteres ASCII torna a
leitura e a edição so código mais fácil para todo mundo. Esse argumento erra o alvo: você quer que seu código-
fonte seja legível e editável pela audiência pretendida, e isso pode não ser "todo mundo". Se o código pertence a
uma corporação multinacional, ou se é um código aberto e você deseja contribuidores de todo o mundo, os
identificadores devem ser em inglês, e então tudo o que você precisa é do ASCII.

Mas se você é uma professora no Brasil, seus alunos vão achar mais fácil ler código com variáveis e nomes de
função em português, e escritos corretamente. E eles não terão nenhuma dificuldade para digitar as cedilhas e as
vogais acentuadas em seus teclados localizados.

Agora que o Python pode interpretar nomes em Unicode, e que o UTF-8 é a codificação padrão para código-fonte,
não vejo motivo para codificar identificadores em português sem acentos, como fazíamos no Python 2, por
necessidade—a menos que seu código tenha que rodar também no Python 2. Se os nomes estão em português,
excluir os acentos não vai tornar o código mais legível para ninguém.

Esse é meu ponto de vista como um brasileiro falante de português, mas acredito que se aplica além de fronteiras
e a outras culturas: escolha a linguagem humana que torna o código mais legível para sua equipe, e então use
todos os caracteres necessários para a ortografia correta.

O que é "texto puro"?

Para qualquer um que lide diariamente com texto em línguas diferentes do inglês, "texto puro" não significa
"ASCII". O Glossário do Unicode (https://fpy.li/4-55) (EN) define texto puro dessa forma:


“ Texto codificado por computador que consiste apenas de uma sequência de pontos de
código de um dado padrão, sem qualquer outra informação estrutural ou de formatação.

Essa definição começa muito bem, mas não concordo com a parte após a vírgula. HTML é um ótimo exemplo de
um formato de texto puro que inclui informação estrutural e de formatação. Mas ele ainda é texto puro, porque
cada byte em um arquivo desse tipo está lá para representar um caractere de texto, em geral usando UTF-8. Não
há bytes com significado não-textual, como você encontra em documentos .png ou .xls, onde a maioria dos bytes
representa valores binários empacotados, como valores RGB ou números de ponto flutuante. No texto puro,
números são representados como sequências de caracteres de dígitos.

Estou escrevendo esse livro em um formato de texto puro chamado—ironicamente— AsciiDoc (https://fpy.li/4-56),
que é parte do conjunto de ferramentas do excelente Atlas book publishing platform (paltaforma de publicação de
livros Atlas) (https://fpy.li/4-57) da O’Reilly. Os arquivos fonte de AsciiDoc são texto puro, mas são UTF-8, e não ASCII.
Se fosse o contrário, escrever esse capítulo teria sido realmente doloroso. Apesar do nome, o AsciiDoc é muito
bom.

O mundo do Unicode está em constante expansão e, nas margens, as ferramentas de apoio nem sempre existem.
Nem todos os caracteres que eu queria exibir estavam disponíveis nas fontes usadas para renderizar o livro. Por
isso tive que usar imagens em vez de listagens em vários exemplos desse capítulo. Por outro lado, os terminais do
Ubuntu e do macOS exibem a maioria do texto Unicode muito bem—incluindo os caracteres japoneses para a
palavra "mojibake": 文字化け.

Como os ponto de código numa str são representados na RAM?

A documentação oficial do Python evita falar sobre como os pontos de código de uma str são armazenados na
memória. Realmente, é um detalhe de implementação. Em teoria, não importa: qualquer que seja a
representação interna, toda str precisa ser codificada para bytes na saída.

Na memória, o Python 3 armazena cada str como uma sequência de pontos de código, usando um número fixo
de bytes por ponto de código, para permitir um acesso direto eficiente a qualquer caractere ou fatia.

Desde o Python 3.3, ao criar um novo objeto str o interpretador verifica os caracteres no objeto, e escolhe o
layout de memória mais econômico que seja adequado para aquela str em particular: se existirem apenas
caracteres na faixa latin1 , aquela str vai usar apenas um byte por ponto de código. Caso contrário, podem ser
usados dois ou quatro bytes por ponto de código, dependendo da str . Isso é uma simplificação; para saber todos
os detalhes, dê uma olhada an PEP 393—​Flexible String Representation (Representação Flexível de Strings)
(https://fpy.li/pep393) (EN).

A representação flexível de strings é similar à forma como o tipo int funciona no Python 3: se um inteiro cabe
em uma palavra da máquina, ele será armazenado em uma palavra da máquina. Caso contrário, o interpretador
muda para uma representação de tamanho variável, como aquela do tipo long do Python 2. É bom ver as boas
ideias se espalhando.

Entretanto, sempre podemos contar com Armin Ronacher para encontrar problemas no Python 3. Ele me
explicou porque, na prática, essa não é uma ideia tão boa assim: basta um único RAT (U+1F400) para inflar um
texto, que de outra forma seria inteiramente ASCII, e transformá-lo em um array sugadora de memória, usando
quatro bytes por caractere, quando um byte seria o suficiente para todos os caracteres exceto o RAT. Além disso,
por causa de todas as formas como os caracteres Unicode se combinam, a capacidade de buscar um caractere
arbitrário pela posição é superestimada—e extrair fatias arbitrárias de texto Unicode é no mínimo ingênuo, e
muitas vezes errado, produzindo mojibake. Com os emojis se tornando mais populares, esses problemas vão só
piorar.
5. Fábricas de classes de dados
Classes de dados são como crianças. São boas como um ponto de partida mas, para participarem como um objeto
adulto, precisam assumir alguma responsabilidade.

Martin Fowler and Kent Beck em Refactoring, primeira edição, Capítulo 3, seção "Bad Smells in Code, Data Class"
(Mau cheiro no código, classe de dados), página 87 (Addison-Wesley).

O Python oferece algumas formas de criar uma classe simples, apenas uma coleção de campos, com pouca ou nenhuma
funcionalidade adicional. Esse padrão é conhecido como "classe de dados"—e dataclasses é um dos pacotes que
suporta tal modelo. Este capítulo trata de três diferentes fábricas de classes que podem ser utilizadas como atalhos para
escrever classes de dados:

collections.namedtuple

A forma mais simples—disponível desde o Python 2.6.

typing.NamedTuple

Uma alternativa que requer dicas de tipo nos campos—desde o Python 3.5, com a sintaxe class adicionada no 3.6.

@dataclasses.dataclass

Um decorador de classe que permite mais personalização que as alternativas anteriores, acrescentando várias
opções e, potencialmente, mais complexidade—desde o Python 3.7.

Após falar sobre essas fábricas de classes, vamos discutir o motivo de classe de dados ser também o nome um code
smell: um padrão de programação que pode ser um sintoma de um design orientado a objetos ruim.

A classe typing.TypedDict pode parecer apenas outra fábrica de classes de dados. Ela usa uma
sintaxe similar, e é descrita pouco após typing.NamedTuple na documentação do módulo typing
(https://docs.python.org/pt-br/3/library/typing.html#typing.TypedDict) (EN) do Python 3.11.

✒️ NOTA Entretanto, TypedDict não cria classes concretas que possam ser instanciadas. Ela é apenas a
sintaxe para escrever dicas de tipo para parâmetros de função e variáveis que aceitarão valores de
mapeamentos como registros, enquanto suas chaves serão os nomes dos campos. Nós veremos mais
sobre isso na seção Seção 15.3 do Capítulo 15.

5.1. Novidades nesse capítulo


Este capítulo é novo, aparece nessa segunda edição do Python Fluente. A seção Seção 5.3 era parte do capítulo 2 da
primeira edição, mas o restante do capítulo é inteiramente inédito.

Vamos começar por uma visão geral, por alto, das três fábricas de classes.

5.2. Visão geral das fábricas de classes de dados


Considere uma classe simples, representando um par de coordenadas geográficas, como aquela no Exemplo 1.

Exemplo 1. class/coordinates.py
PY3
class Coordinate:

def __init__(self, lat, lon):


self.lat = lat
self.lon = lon

A tarefa da classe Coordinate é manter os atributos latitude e longitude. Escrever o __init__ padrão fica cansativo
muito rápido, especialmente se sua classe tiver mais que alguns poucos atributos: cada um deles é mencionado três
vezes! E aquele código repetitivo não nos dá sequer os recursos básicos que esperamos de um objeto Python:

PYCON
>>> from coordinates import Coordinate
>>> moscow = Coordinate(55.76, 37.62)
>>> moscow
<coordinates.Coordinate object at 0x107142f10> (1)
>>> location = Coordinate(55.76, 37.62)
>>> location == moscow (2)
False
>>> (location.lat, location.lon) == (moscow.lat, moscow.lon) (3)
True

1. O __repr__ herdado de object não é muito útil.


2. O == não faz sentido; o método __eq__ herdado de object compara os IDs dos objetos.
3. Comparar duas coordenadas exige a comparação explícita de cada atributo.

As fábricas de classes de dados tratadas nesse capítulo fornecem automaticamente os métodos __init__ , __repr__ ,
e __eq__ necessários, além alguns outros recursos úteis.

Nenhuma das fábricas de classes discutidas aqui depende de herança para funcionar. Tanto
collections.namedtuple quanto typing.NamedTuple criam subclasses de tuple . O
✒️ NOTA @dataclass é um decorador de classe, não afeta de forma alguma a hierarquia de classes. Cada um
deles utiliza técnicas diferentes de metaprogramação para injetar métodos e atributos de dados na
classe em construção.

Aqui está uma classe Coordinate criada com uma namedtuple —uma função fábrica que cria uma subclasse de
tuple com o nome e os campos especificados:

PYCON
>>> from collections import namedtuple
>>> Coordinate = namedtuple('Coordinate', 'lat lon')
>>> issubclass(Coordinate, tuple)
True
>>> moscow = Coordinate(55.756, 37.617)
>>> moscow
Coordinate(lat=55.756, lon=37.617) (1)
>>> moscow == Coordinate(lat=55.756, lon=37.617) (2)
True

1. Um __repr__ útil.
2. Um __eq__ que faz sentido.

A typing.NamedTuple , mais recente, oferece a mesma funcionalidade e acrescenta anotações de tipo a cada campo:
PYCON
>>> import typing
>>> Coordinate = typing.NamedTuple('Coordinate',
... [('lat', float), ('lon', float)])
>>> issubclass(Coordinate, tuple)
True
>>> typing.get_type_hints(Coordinate)
{'lat': <class 'float'>, 'lon': <class 'float'>}

Uma tupla nomeada e com dicas de tipo pode também ser construída passando os campos como
argumentos nomeados, assim:

👉 DICA Coordinate = typing.NamedTuple('Coordinate', lat=float, lon=float)


PYCON

Além de ser mais legível, essa forma permite fornecer o mapeamento de campos e tipos como
**fields_and_types .

Desde o Python 3.6, typing.NamedTuple pode também ser usada em uma instrução class , com as anotações de tipo
escritas como descrito na PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis)
(https://fpy.li/pep526) (EN). É muito mais legível, e torna fácil sobrepor métodos ou acrescentar métodos novos. O
Exemplo 2 é a mesma classe Coordinate , com um par de atributos float e um __str__ personalziado, para
mostrar a coordenada no formato 55.8°N, 37.6°E.

Exemplo 2. typing_namedtuple/coordinates.py

PY
from typing import NamedTuple

class Coordinate(NamedTuple):
lat: float
lon: float

def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'

Apesar de NamedTuple aparecer na declaração class como uma superclasse, não é esse o caso.
typing.NamedTuple usa a funcionalidade avançada de uma metaclasse[51] para personalizar a
criação da classe do usuário. Veja isso:

⚠️ AVISO >>> issubclass(Coordinate, typing.NamedTuple)


PYCON

False
>>> issubclass(Coordinate, tuple)
True

No método __init__ gerado por typing.NamedTuple , os campos aparecem como parâmetros e na mesma ordem em
que aparecem na declaração class .

Assim como typing.NamedTuple , o decorador dataclass suporta a sintaxe da PEP 526 (https://fpy.li/pep526) (EN) para
declarar atributos de instância. O decorador lê as anotações das variáveis e gera métodos automaticamente para sua
classe. Como comparação, veja a classe Coordinate equivante escrita com a ajuda do decorador dataclass , como
mostra o Exemplo 3.
Exemplo 3. dataclass/coordinates.py

PY
from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float

def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'

Observe que o corpo das classes no Exemplo 2 e no Exemplo 3 são idênticos—a diferença está na própria declaração
class . O decorador @dataclass não depende de herança ou de uma metaclasse, então não deve interferir no uso
desses mecanismos pelo usuário.[52] A classe Coordinate no Exemplo 3 é uma subclasse de object .

5.2.1. Principais recursos


As diferentes fábricas de classes de dados tem muito em comum, como resume a Tabela 12.

Tabela 12. Recursos selecionados, comparando as três fábricas de classes de dados; x é uma instância de uma classe de
dados daquele tipo
namedtuple NamedTuple dataclass

instâncias mutáveis NÃO NÃO SIM

sintaxe de declaração de NÃO SIM SIM


classe

criar um dict x._asdict() x._asdict() dataclasses.asdict(x)

obter nomes dos campos x._fields x._fields [f.name for f in


dataclasses.fields(x)]

obter defaults x._field_defaults x._field_defaults [f.default for f in


dataclasses.fields(x)]

obter tipos dos campos N/A x.__annotations__ x.__annotations__

nova instância com x._replace(…) x._replace(…) dataclasses.replace(x, …)


modificações

nova classe durante a namedtuple(…) NamedTuple(…) dataclasses.make_dataclass(…)


execução
As classes criadas por typing.NamedTuple e @dataclass tem um atributo __annotations__ ,
contendo as dicas de tipo para os campos. Entretanto, ler diretamente de __annotations__ não é
recomendado. Em vez disso, a melhor prática recomendada para obter tal informação é chamar
inspect.get_annotations(MyClass)
⚠️ AVISO (https://docs.python.org/pt-br/3.10/library/inspect.html#inspect.get_annotations) (a partir do Python 3.10—EN)
ou typing.​
get_​
type_​
hints(MyClass)
(https://docs.python.org/pt-br/3/library/typing.html#typing.get_type_hints) (Python 3.5 a 3.9—EN). Isso porque
tais funções fornecem serviços adicionais, como a resolução de referências futuras nas dicas de tipo.
Voltaremos a isso bem mais tarde neste livro, na seção Seção 15.5.1.

Vamos agora detalhar aqueles recursos principais.

Instâncias mutáveis
A diferença fundamental entre essas três fábricas de classes é que collections.namedtuple e typing.NamedTuple
criam subclasses de tuple , e portanto as instâncias são imutáveis. Por default, @dataclass produz classes mutáveis.
Mas o decorador aceita o argumento nomeado frozen —que aparece no Exemplo 3. Quando frozen=True , a classe
vai gerar uma exceção se você tentar atribuir um valor a um campo após a instância ter sido inicializada.

Sintaxe de declaração de classe


Apenas typing.NamedTuple e dataclass suportam a sintaxe de declaração de class regular, tornando mais fácil
acrescentar métodos e docstrings à classe que está sendo criada.

Construir um dict
As duas variantes de tuplas nomeadas fornecem um método de instância ( ._asdict ), para construir um objeto dict
a partir dos campos de uma instância de classe de dados. O módulo dataclasses fornece uma função para fazer o
mesmo: dataclasses.asdict .

Obter nomes dos campos e valores default


Todas as três fábricas de classes permitem que você obtenha os nomes dos campos e os valores default (que podem ser
configurados para cada campo). Nas classes de tuplas nomeadas, aqueles metadados estão nos atributos de classe
._fields e ._fields_defaults . Você pode obter os mesmos metadados em uma classe decorada com dataclass
usando a função fields do módulo dataclasses . Ele devolve uma tupla de objetos Field com vários atributos,
incluindo name e default .

Obter os tipos dos campos


Classes definidas com a ajuda de typing.NamedTuple e @dataclass contêm um mapeamento dos nomes dos campos
para seus tipos, o atributo de classe __annotations__ . Como já mencionado, use a função typing.get_type_hints
em vez de ler diretamente de __annotations__ .

Nova instância com modificações


Dada uma instância de tupla nomeada x , a chamada x._replace(**kwargs) devolve uma nova instância com os
valores de alguns atributos modificados, de acordo com os argumentos nomeados incluídos na chamada. A função de
módulo dataclasses.replace(x, **kwargs) faz o mesmo para uma instância de uma classe decorada com
dataclass .

Nova classe durante a execução


Apesar da sintaxe de declaração de classe ser mais legível, ela é fixa no código. Uma framework pode ter a necessidade
de criar classes de dados durante a execução. Para tanto, podemos usar a sintaxe default de chamada de função de
collections.namedtuple , que também é suportada por typing.NamedTuple . O módulo dataclasses oferece a
função make_dataclass , com o mesmo propósito.
Após essa visão geral dos principais recursos das fábricas de classes de dados, vamos examinar cada uma delas mais de
perto, começando pela mais simples.

5.3. Tuplas nomeadas clássicas


A função collections.namedtuple é uma fábrica que cria subclasses de tuple , acrescidas de nomes de campos, um
nome de classe, e um __repr__ informativo. Classes criadas com namedtuple podem ser usadas onde quer que uma
tupla seja necessária. Na verdade, muitas funções da biblioteca padrão, que antes devolviam tuplas agora devolvem,
por conveniência, tuplas nomeadas, sem afetar de forma alguma o código do usuário.

👉 DICA Cada instância de uma classe criada por namedtuple usa exatamente a mesma quantidade de
memória usada por uma tupla, pois os nomes dos campos são armazenados na classe.

O Exemplo 4 mostra como poderíamos definir uma tupla nomeada para manter informações sobre uma cidade.

Exemplo 4. Definindo e usando um tipo tupla nomeada

PYCON
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates') (1)
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) (2)
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population (3)
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'

1. São necessários dois parâmetros para criar uma tupla nomeada: um nome de classe e uma lista de nomes de
campos, que podem ser passados como um iterável de strings ou como uma única string com os nomes delimitados
por espaços.
2. Na inicializaçòa de uma instância, os valores dos campos devem ser passados como argumentos posicionais
separados (uma tuple , por outro lado, é inicializada com um único iterável)
3. É possível acessar os campos por nome ou por posição.

Como uma subclasse de tuple , City herda métodos úteis, tal como __eq__ e os métodos especiais para operadores
de comparação—incluindo __lt__ , que permite ordenar listas de instâncias de City .

Uma tupla nomeada oferece alguns atributos e métodos além daqueles herdados de tuple . O Exemplo 5 demonstra os
mais úteis dentre eles: o atributo de classe _fields , o método de classe _make(iterable) , e o método de instância
_asdict() .

Exemplo 5. Atributos e métodos das tuplas nomeadas (continuando do exenplo anterior)


PYCON
>>> City._fields (1)
('name', 'country', 'population', 'location')
>>> Coordinate = namedtuple('Coordinate', 'lat lon')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, Coordinate(28.613889, 77.208889))
>>> delhi = City._make(delhi_data) (2)
>>> delhi._asdict() (3)
{'name': 'Delhi NCR', 'country': 'IN', 'population': 21.935,
'location': Coordinate(lat=28.613889, lon=77.208889)}
>>> import json
>>> json.dumps(delhi._asdict()) (4)
'{"name": "Delhi NCR", "country": "IN", "population": 21.935,
"location": [28.613889, 77.208889]}'

1. ._fields é uma tupla com os nomes dos campos da classe.


2. ._make() cria uma City a partir de um iterável; City(*delhi_data) faria o mesmo.
3. ._asdict() devolve um dict criado a partir da instância de tupla nomeada.
4. ._asdict() é útil para serializar os dados no formato JSON, por exemplo.

Até o Python 3.7, o método _asdict devolvia um OrderedDict . Desde o Python 3.8, ele devolve um
dict simples—o que não causa qualquer problema, agora que podemos confiar na ordem de
⚠️ AVISO inserção das chaves. Se você precisar de um OrderedDict , a documentação do _asdict
(https://docs.python.org/pt-br/3.8/library/collections.html#collections.somenamedtuple._asdict) (EN) recomenda
criar um com o resultado: OrderedDict(x._asdict()) .

Desde o Python 3.7, a namedtuple aceita o argumento nomeado defaults , fornecendo um iterável de N valores
default para cada um dos N campos mais à direita na definição da classe. O Exemplo 6 mostra como definir uma tupla
nomeada Coordinate com um valor default para o campo reference .

Exemplo 6. Atributos e métodos das tuplas nomeadas, continuando do Exemplo 5

PYCON
>>> Coordinate = namedtuple('Coordinate', 'lat lon reference', defaults=['WGS84'])
>>> Coordinate(0, 0)
Coordinate(lat=0, lon=0, reference='WGS84')
>>> Coordinate._field_defaults
{'reference': 'WGS84'}

Na seção Seção 5.2.1.2, mencionei que é mais fácil programar métodos com a sintaxe de classe suportada por
typing.NamedTuple and @dataclass . Você também pode acrescentar métodos a uma namedtuple , mas é um
remendo. Pule a próxima caixinha se você não estiver interessada em gambiarras.

Remendando uma tupla nomeada para injetar um método


Lembre como criamos a classe Card class no Exemplo 1 do Capítulo 1:

PY3
Card = collections.namedtuple('Card', ['rank', 'suit'])

Mas tarde no Capítulo 1, escrevi uma função spades_high , para ordenação. Seria bom que aquela lógica
estivesse encapsulada em um método de Card , mas acrescentar spades_high a Card sem usar uma declaração
class exige um remendo rápido: definir a função e então atribuí-la a um atributo de classe. O Exemplo 7 mostra
como isso é feito:
Exemplo 7. frenchdeck.doctest: Acrescentando um atributo de classe e um método a Card , a namedtuple da
seção Seção 1.2

PYCON
>>> Card.suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) # (1)
>>> def spades_high(card): # (2)
... rank_value = FrenchDeck.ranks.index(card.rank)
... suit_value = card.suit_values[card.suit]
... return rank_value * len(card.suit_values) + suit_value
...
>>> Card.overall_rank = spades_high # (3)
>>> lowest_card = Card('2', 'clubs')
>>> highest_card = Card('A', 'spades')
>>> lowest_card.overall_rank() # (4)
0
>>> highest_card.overall_rank()
51

1. Acrescenta um atributo de classe com valores para cada naipe.


2. A função spades_high vai se tornar um método; o primeiro argumento não precisa ser chamado de
self . Como um método, ela de qualquer forma terá acesso à instância que recebe a chamada.

3. Anexa a função à classe Card como um método chamado overall_rank .

4. Funciona!

Para uma melhor legibilidade e para ajudar na manutenção futura, é muito melhor programar métodos dentro
de uma declaração class . Mas é bom saber que essa gambiarra é possível, pois às vezes pode ser útil.[53]

Isso foi apenas um pequeno desvio para demonstrar o poder de uma linguagem dinâmica.

Agora vamos ver a variante typing.NamedTuple .

5.4. Tuplas nomeadas com tipo


A classe Coordinate com um campo default, do Exemplo 6, pode ser escrita usando typing.NamedTuple , como se vê
no Exemplo 8.

Exemplo 8. typing_namedtuple/coordinates2.py

PY
from typing import NamedTuple

class Coordinate(NamedTuple):
lat: float # (1)
lon: float
reference: str = 'WGS84' # (2)

1. Todo campo de instância precisa ter uma anotação de tipo.


2. O campo de instância reference é anotado com um tipo e um valor default.

As classes criadas por typing.NamedTuple não tem qualquer método além daqueles que collections.namedtuple
também gera—e aquele herdados de tuple . A única diferença é a presença do atributo de classe __annotations__ —
que o Python ignora completamente durante a execução do programa.
Dado que o principal recurso de typing.NamedTuple são as anotações de tipo, vamos dar uma rápida olhada nisso
antes de continuar nossa exploração das fábricas de classes de dados.

5.5. Introdução às dicas de tipo


Dicas de tipo—também chamadas anotações de tipo—são formas de declarar o tipo esperado dos argumentos, dos
valores devolvidos, das variáveis e dos atributos de funções.

A primeira coisa que você precisa saber sobre dicas de tipo é que elas não são impostas de forma alguma pelo
compilador de bytecode ou pelo interpretador do Python.

Essa é uma introdução muito breve sobre dicas de tipo, suficiente apenas para que a sintaxe e o
propósito das anotações usadas nas declarações de typing.NamedTuple e @dataclass façam

✒️ NOTA sentido. Vamos trata de anotações de tipo nas assinaturas de função no Capítulo 8 e de anotações
mais avançadas no Capítulo 15. Aqui vamos ver principalmente dicas com tipos embutidos simples,
tais como str , int , e float , que são provavelmente os tipos mais comuns usados para anotar
campos em classes de dados.

5.5.1. Nenhum efeito durante a execução


Pense nas dicas de tipo do Python como "documentação que pode ser verificada por IDEs e verificadores de tipo".

Isso porque as dicas de tipo não tem qualquer impacto sobre o comportamento de programas em Python durante a
execução. Veja o Exemplo 9.

Exemplo 9. O Python não exige dicas de tipo durante a execução de um programa

PY
>>> import typing
>>> class Coordinate(typing.NamedTuple):
... lat: float
... lon: float
...
>>> trash = Coordinate('Ni!', None)
>>> print(trash)
Coordinate(lat='Ni!', lon=None) # (1)

1. Eu avisei: não há verificação de tipo durante a execução!

Se você incluir o código do Exemplo 9 em um módulo do Python, ela vai rodar e exibir uma Coordinate sem sentido,
e sem gerar qualquer erro ou aviso:

BASH
$ python3 nocheck_demo.py
Coordinate(lat='Ni!', lon=None)

O objetivo primário das dicas de tipo é ajudar os verificadores de tipo externos, como o Mypy (https://fpy.li/mypy) ou o
verificador de tipo embutido do PyCharm IDE (https://fpy.li/5-5). Essas são ferramentas de análise estática: elas verificam
código-fonte Python "parado", não código em execução.

Para observar o efeito das dicas de tipo, é necessário executar umas dessas ferramentas sobre seu código—como um
linter (analisador de código). Por exemplo, eis o quê o Mypy tem a dizer sobre o exemplo anterior:
BASH
$ mypy nocheck_demo.py
nocheck_demo.py:8: error: Argument 1 to "Coordinate" has
incompatible type "str"; expected "float"
nocheck_demo.py:8: error: Argument 2 to "Coordinate" has
incompatible type "None"; expected "float"

Como se vê, dada a definição de Coordinate , o Mypy sabe que os dois argumentos para criar um instância devem ser
do tipo float , mas atribuição a trash usa uma str e None .[54]

Vamos falar agora sobre a sintaxe e o significado das dicas de tipo.

5.5.2. Sintaxe de anotação de variáveis


Tanto typing.NamedTuple quanto @dataclass usam a sintaxe de anotações de variáveis definida na PEP 526
(https://fpy.li/pep526) (EN). Vamos ver aqui uma pequena introdução àquela sintaxe, no contexto da definição de
atributos em declarações class .

A sintaxe básica da anotação de variáveis é :

PY3
var_name: some_type

A seção "Acceptable type hints" (_Dicas de tipo aceitáveis) (https://fpy.li/5-6), na PEP 484, explica o que são tipo aceitáveis.
Porém, no contexto da definição de uma classe de dados, os tipos mais úteis geralmente serão os seguintes:

Uma classe concreta, por exemplo str ou FrenchDeck .

Um tipo de coleção parametrizada, como list[int] , tuple[str, float] , etc.

typing.Optional , por exemplo Optional[str] —para declarar um campo que pode ser uma str ou None .

Você também pode inicializar uma variável com um valor. Em uma declaração de typing.NamedTuple ou
@dataclass , aquele valor se tornará o default daquele atributo quando o argumento correspondente for omitido na
chamada de inicialização:

PY3
var_name: some_type = a_value

5.5.3. O significado das anotações de variáveis


Vimos, no tópico Seção 5.5.1, que dicas de tipo não tem qualquer efeito durante a execução de um programa. Mas no
momento da importação—quando um módulo é carregado—o Python as lê para construir o dicionário
__annotations__ , que typing.NamedTuple e @dataclass então usam para aprimorar a classe.

Vamos começar essa exploração no Exemplo 10, com uma classe simples, para mais tarde ver que recursos adicionais
são acrescentados por typing.NamedTuple e @dataclass .

Exemplo 10. meaning/demo_plain.py: uma classe básica com dicas de tipo

PYTHON3
class DemoPlainClass:
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)

1. a se torna um registro em __annotations__ , mas é então descartada: nenhum atributo chamado a é criado na
classe.
2. b é salvo como uma anotação, e também se torna um atributo de classe com o valor 1.1 .

3. c é só um bom e velho atributo de classe básico, sem uma anotação.


Podemos checar isso no console, primeiro lendo o __annotations__ da DemoPlainClass , e daí tentando obter os
atributos chamados a , b , e c :

PYCON
>>> from demo_plain import DemoPlainClass
>>> DemoPlainClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoPlainClass.a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'DemoPlainClass' has no attribute 'a'
>>> DemoPlainClass.b
1.1
>>> DemoPlainClass.c
'spam'

Observe que o atributo especial __annotations__ é criado pelo interpretador para registrar dicas de tipo que
aparecem no código-fonte—mesmo em uma classe básica.

O a sobrevive apenas como uma anotação, não se torna um atributo da classe, porque nenhum valor é atribuído a ele.
[55] O b e o c são armazenados como atributos de classe porque são vinculados a valores.

Nenhum desses três atributos estará em uma nova instância de DemoPlainClass . Se você criar um objeto o =
DemoPlainClass() , o.a vai gerar um AttributeError , enquanto o.b e o.c vão obter os atributos de classe com
os valores 1.1 e 'spam' —que é apenas o comportamento normal de um objeto Python.

Inspecionando uma typing.NamedTuple


Agora vamos examinar uma classe criada com typing.NamedTuple (Exemplo 11), usando os mesmos atributos e
anotações da DemoPlainClass do Exemplo 10.

Exemplo 11. meaning/demo_nt.py: uma classe criada com typing.NamedTuple

PYTHON3
import typing

class DemoNTClass(typing.NamedTuple):
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)

1. a se torna uma anotação e também um atributo de instância.


2. b é outra anotação, mas também se torna um atributo de instância com o valor default 1.1 .

3. c é só um bom e velho atributo de classe comum; não será mencionado em nenhuma anotação.

Inspecionando a DemoNTClass , temos o seguinte:


PYCON
>>> from demo_nt import DemoNTClass
>>> DemoNTClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoNTClass.a
<_collections._tuplegetter object at 0x101f0f940>
>>> DemoNTClass.b
<_collections._tuplegetter object at 0x101f0f8b0>
>>> DemoNTClass.c
'spam'

Aqui vemos as mesmas anotações para a e b que vimos no Exemplo 10. Mas typing.NamedTuple cria os atributos de
classe a e b . O atributo c é apenas um atributo de classe simples, com o valor 'spam' .

Os atributos de classe a e b são descritores (descriptors)—um recurso avançado tratado no [attribute_descriptors].


Por ora, pense neles como similares a um getter de propriedades do objeto[56]: métodos que não exigem o operador
explícito de chamada () para obter um atributo de instância. Na prática, isso significa que a e b vão funcionar como
atributos de instância somente para leitura—o que faz sentido, se lembrarmos que instâncias de DemoNTClass são
apenas tuplas chiques, e tuplas são imutáveis.

A DemoNTClass também recebe uma docstring personalizada:

PYCON
>>> DemoNTClass.__doc__
'DemoNTClass(a, b)'

Let’s inspect an instance of DemoNTClass :

PYCON
>>> nt = DemoNTClass(8)
>>> nt.a
8
>>> nt.b
1.1
>>> nt.c
'spam'

Para criar nt , precisamos passar pelo menos o argumento a para DemoNTClass . O construtor também aceita um
argumento b , mas como este último tem um valor default (de 1.1 ), ele é opcional. Como esperado, o objeto nt possui
os atributos a e b ; ele não tem um atributo c , mas o Python obtém c da classe, como de hábito.

Se você tentar atribuir valores para nt.a , nt.b , nt.c , ou mesmo para nt.z , vai gerar uma exceção Attribute​
Error , com mensagens de erro sutilmente distintas. Tente fazer isso, e reflita sobre as mensagens.

Inspecionando uma classe decorada com dataclass


Vamos agora examinar o Exemplo 12.

Exemplo 12. meaning/demo_dc.py: uma classe decorada com @dataclass

PYTHON3
from dataclasses import dataclass

@dataclass
class DemoDataClass:
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)
1. a se torna uma anotação, e também um atributo de instância controlado por um descritor.
2. b é outra anotação, e também se torna um atributo de instância com um descritor e um valor default de 1.1 .

3. c é apenas um atributo de classe comum; nenhuma anotação se refere a ele.

Podemos então verificar o __annotations__ , o __doc__ , e os atributos a , b , c no DataClass :


Demo​

PYCON
>>> from demo_dc import DemoDataClass
>>> DemoDataClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoDataClass.__doc__
'DemoDataClass(a: int, b: float = 1.1)'
>>> DemoDataClass.a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'DemoDataClass' has no attribute 'a'
>>> DemoDataClass.b
1.1
>>> DemoDataClass.c
'spam'

O __annotations__ e o __doc__ não guardam surpresas. Entretanto, não há um atributo chamado a em


DemoDataClass —diferente do que ocorre na DemoNTClass do Exemplo 11, que inclui um descritor para obter a das
instâncias da classe, como atributos somente para leitura (aquele misterioso <_collections.tuplegetter> ). Isso
ocorre porque o atributo a só existirá nas instâncias de DemoDataClass . Será um atributo público, que poderemos
obter e definir, a menos que a classe seja frozen. Mas b e c existem como atributos de classe, com b contendo o valor
default para o atributo de instância b , enquanto c é apenas um atributo de classe que não será vinculado a
instâncias.

Vejamos como se parece uma instância de DemoDataClass :

PYCON
>>> dc = DemoDataClass(9)
>>> dc.a
9
>>> dc.b
1.1
>>> dc.c
'spam'

Novamente, a e b são atributos de instância, e c é um atributo de classe obtido através da instância.

Como mencionado, instâncias de DemoDataClass são mutáveis—e nenhuma verificação de tipo é realizada durante a
execução:

PYCON
>>> dc.a = 10
>>> dc.b = 'oops'

Podemos fazer atribuições ainda mais ridículas:

PYCON
>>> dc.c = 'whatever'
>>> dc.z = 'secret stash'

Agora a instância dc tem um atributo c —mas isso não muda o atributo de classe c . E podemos adicionar um novo
atributo z . Isso é o comportamento normal do Python: instâncias regulares podem ter seus próprios atributos, que não
aparecem na classe.[57]
5.6. Mais detalhes sobre @dataclass
Até agora, só vimos exemplos simples do uso de @dataclass . Esse decorador aceita vários argumentos nomeados. Esta
é sua assinatura:

PY3
@dataclass(*, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False)

O * na primeira posição significa que os parâmetros restantes são todos parâmetros nomeados. A Tabela 13 os
descreve.

Tabela 13. Parâmetros nomeados aceitos pelo decorador @dataclass

Option Meaning Default Notes

init Gera o __init__ True Ignorado se o __init__ for implementado


pelo usuário.

repr Gera o __repr__ True Ignorado se o __repr__ for implementado


pelo usuário.

eq Gera o __eq__ True Ignorado se o __eq__ for implementado pelo


usuário.

order Gera __lt__ , False Se True , causa uma exceção se eq=False ,


__le__ , __gt__ , ou se qualquer dos métodos de comparação
__ge__ que seriam gerados estiver definido ou for
herdado.

unsafe_hash Gera o __hash__ False Semântica complexa e várias restrições—


veja a: documentação de dataclass
(https://docs.python.org/pt-
br/3/library/dataclasses.html#dataclasses.dataclass)
.

frozen Cria instâncias False As instâncias estarão razoavelmente


"imutáveis" protegidas contra mudanças acidentais, mas
não serão realmente imutáveis.[58]

Os defaults são, de fato, as configurações mais úteis para os casos de uso mais comuns. As opções mais prováveis de
serem modificadas de seus defaults são:

frozen=True

Protege as instâncias da classe de modificações acidentais.

order=True

Permite ordenar as instâncias da classe de dados.

Dada a natureza dinâmica de objetos Python, não é muito difícil para um programador curioso contornar a proteção
oferecida por frozen=True . Mas os truques necessários são fáceis de perceber em uma revisão do código.
Se tanto o argumento eq quanto o frozen forem True , @dataclass produz um método __hash__ adequado, e daí
as instâncias serão hashable. O __hash__ gerado usará dados de todos os campos que não forem individualmente
excluídos usando uma opção de campo, que veremos na seção Seção 5.6.1. Se frozen=False (o default), @dataclass
definirá __hash__ como None , sinalizando que as instâncias não são hashable, e portanto sobrepondo o __hash__
de qualquer superclasse.

A PEP 557—Data Classes (Classe de Dados) (https://fpy.li/pep557) (EN) diz o seguinte sobre unsafe_hash :

“ Apesar de não
__hash__com
ser recomendado, você pode forçar Classes de Dados a criarem um método
. Pode ser esse o caso, se sua classe for logicamente
unsafe_hash=True
imutável e mesmo assim possa ser modificada. Este é um caso de uso especializado e deve ser
considerado com cuidado.

Deixo o por aqui. Se você achar que precisa usar essa opção, leia a documentação de
unsafe_hash
dataclasses.dataclass (https://docs.python.org/pt-br/3/library/dataclasses.html#dataclasses.dataclass).

Outras personalizações da classe de dados gerada podem ser feitas no nível dos campos.

5.6.1. Opções de campo


Já vimos a opção de campo mais básica: fornecer (ou não) um valor default junto com a dica de tipo. Os campos de
instância declarados se tornarão parâmetros no __init__ gerado. O Python não permite parâmetros sem um default
após parâmetros com defaults. Então, após declarar um campo com um valor default, cada um dos campos seguintes
deve também ter um default.

Valores default mutáveis são a fonte mais comum de bugs entre desenvolvedores Python iniciantes. Em definições de
função, um valor default mutável é facilmente corrompido, quando uma invocação da função modifica o default,
mudando o comportamento nas invocações posteriores—um tópico que vamos explorar na seção Seção 6.5.1 (no
Capítulo 6). Atributos de classe são frequentemente usados como valores default de atributos para instâncias, inclusive
em classes de dados. E o @dataclass usa os valores default nas dicas de tipo para gerar parâmetros com defaults no
__init__ . Para prevenir bugs, o @dataclass rejeita a definição de classe que aparece no Exemplo 13.

Exemplo 13. dataclass/club_wrong.py: essa classe gera um ValueError

PY3
@dataclass
class ClubMember:
name: str
guests: list = []

Se você carregar o módulo com aquela classe ClubMember , o resultado será esse:

BASH
$ python3 club_wrong.py
Traceback (most recent call last):
File "club_wrong.py", line 4, in <module>
class ClubMember:
...several lines omitted...
ValueError: mutable default <class 'list'> for field guests is not allowed:
use default_factory

A mensagem do ValueError explica o problema e sugere uma solução: usar a default_factory . O Exemplo 14
mostra como corrigir a ClubMember .
Exemplo 14. dataclass/club.py: essa definição de ClubMember funciona

PY3
from dataclasses import dataclass, field

@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)

No campo do Exemplo 14, em vez de uma lista literal, o valor default é definido chamando a função
guests
dataclasses.field com default_factory=list .

O parâmetro default_factory permite que você forneça uma função, classe ou qualquer outro invocável, que será
chamado com zero argumentos, para gerar um valor default a cada vez que uma instância da classe de dados for
criada. Dessa forma, cada instância de ClubMember terá sua própria list —ao invés de todas as instâncias
compartilharem a mesma list da classe, que raramente é o que queremos, e muitas vezes é um bug.

É bom que @dataclass rejeite definições de classe com uma list default em um campo.
Entretanto, entenda que isso é uma solução parcial, que se aplica apenas a list , dict e set .
⚠️ AVISO Outros valores mutáveis usados como default não serão apontados por @dataclass . É sua
responsabilidade entender o problema e se lembrar de usar uma factory default para definir valores
default mutáveis.

Se você estudar a documentação do módulo dataclasses (https://docs.python.org/pt-br/3/library/dataclasses.html), verá um


campo list definido com uma sintaxe nova, como no Exemplo 15.

Exemplo 15. dataclass/club_generic.py: essa definição de ClubMember é mais precisa

PY3
from dataclasses import dataclass, field

@dataclass
class ClubMember:
name: str
guests: list[str] = field(default_factory=list) # (1)

1. list[str] significa "uma lista de str."

A nova sintaxe list[str] é um tipo genérico parametrizado: desde o Python 3.9, o tipo embutido list aceita aquela
notação com colchetes para especificar o tipo dos itens da lista.

Antes do Python 3.9, as coleções embutidas não suportavam a notação de tipagem genérica. Como
uma solução temporária, há tipos correspondentes de coleções no módulo typing . Se você precisa
⚠️ AVISO de uma dica de tipo para uma list parametrizada no Python 3.8 ou anterior, você tem que
importar e usar o tipo List de typing : List[str] . Leia mais sobre isso na caixa Suporte a tipos
de coleção descontinuados.

Vamos tratar dos tipos genéricos no Capítulo 8. Por ora, observe que o Exemplo 14 e o Exemplo 15 estão ambos
corretos, e que o verificador de tipagem Mypy não reclama de nenhuma das duas definições de classe.
A diferença é que aquele guests: list significa que guests pode ser uma list de objetos de qualquer natureza,
enquanto guests: list[str] diz que guests deve ser uma list na qual cada item é uma str . Isso permite que o
verificador de tipos encontre (alguns) bugs em código que insira itens inválidos na lista, ou que leia itens dali.

A default_factory é possivelmente a opção mais comum da função field , mas há várias outras, listadas na Tabela
14.

Tabela 14. Argumentos nomeados aceitos pela função field

Option Meaning Default

default Valor default para o campo _MISSING_TYPE [59]

default_factory função com 0 parâmetros usada _MISSING_TYPE


para produzir um valor default

init Incluir o campo nos parâmetros de True


__init__

repr Incluir o campo em __repr__ True

compare Usar o campo nos métodos de True


comparação __eq__ , __lt__ , etc.

hash Incluir o campo no cálculo de None[60]


__hash__

metadata Mapeamento com dados definidos None


pelo usuário; ignorado por
@dataclass

A opção default existe porque a chamada a field toma o lugar do valor default na anotação do campo. Se você
quisesse criar um campo athlete com o valor default False , e também omitir aquele campo do método __repr__ ,
escreveria o seguinte:

PY3
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)
athlete: bool = field(default=False, repr=False)

5.6.2. Processamento pós-inicialização


O método __init__ gerado por @dataclass apenas recebe os argumentos passados e os atribui—ou seus valores
default, se o argumento não estiver presente—aos atributos de instância, que são campos da instância. Mas pode ser
necessário fazer mais que isso para inicializar a instância. Se for esse o caso, você pode fornecer um método
__post_init__ . Quando esse método existir, @dataclass acrescentará código ao __init__ gerado para invocar
__post_init__ como o último passo da inicialização.

Casos de uso comuns para __post_init__ são validação e o cálculo de valores de campos baseado em outros campos.
Vamos estudar um exemplo simples, que usa __post_init__ pelos dois motivos.

Primeiro, dê uma olhada no comportamento esperado de uma subclasse de ClubMember , chamada


HackerClubMember , como descrito por doctests no Exemplo 16.
Exemplo 16. dataclass/hackerclub.py: doctests para HackerClubMember

PY3
"""
``HackerClubMember`` objects accept an optional ``handle`` argument::

>>> anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')


>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')

If ``handle`` is omitted, it's set to the first part of the member's name::

>>> leo = HackerClubMember('Leo Rochael')


>>> leo
HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')

Members must have a unique handle. The following ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::

>>> leo2 = HackerClubMember('Leo DaVinci')


Traceback (most recent call last):
...
ValueError: handle 'Leo' already exists.

To fix, ``leo2`` must be created with an explicit ``handle``::

>>> leo2 = HackerClubMember('Leo DaVinci', handle='Neo')


>>> leo2
HackerClubMember(name='Leo DaVinci', guests=[], handle='Neo')
"""

Observe que precisamos fornecer handle como um argumento nomeado, pois HackerClubMember herda name e
guests de ClubMember , e acrescenta o campo handle . A docstring gerada para HackerClubMember mostra a ordem
dos campos na chamada de inicialização:

PYCON
>>> HackerClubMember.__doc__
"HackerClubMember(name: str, guests: list = <factory>, handle: str = '')"

Aqui <factory> é um caminho mais curto para dizer que algum invocável vai produzir o valor default para guests
(no nosso caso, a fábrica é a classe list ). O ponto é o seguinte: para fornecer um handle mas não um guests ,
precisamos passar handle como um argumento nomeado.

A seção "Herança (https://docs.python.org/pt-br/3/library/dataclasses.html#inheritance) na documentação do módulo


dataclasses explica como a ordem dos campos é analisada quando existem vários níveis de herança.

No Capítulo 14 vamos falar sobre o uso indevido da herança, especialmente quando as superclasses
✒️ NOTA não são abstratas. Criar uma hierarquia de classes de dados é, em geral, uma má ideia, mas nos
serviu bem aqui para tornar o Exemplo 17 mais curto, e permitir que nos concentrássemos na
declaração do campo handle e na validação com __post_init__ .

O Exemplo 17 mostra a implementação.

Exemplo 17. dataclass/hackerclub.py: código para HackerClubMember


PY3
from dataclasses import dataclass
from club import ClubMember

@dataclass
class HackerClubMember(ClubMember): # (1)
all_handles = set() # (2)
handle: str = '' # (3)

def __post_init__(self):
cls = self.__class__ # (4)
if self.handle == '': # (5)
self.handle = self.name.split()[0]
if self.handle in cls.all_handles: # (6)
msg = f'handle {self.handle!r} already exists.'
raise ValueError(msg)
cls.all_handles.add(self.handle) # (7)

1. HackerClubMember estende ClubMember .

2. all_handles é um atributo de classe.


3. handle é um campo de instância do tipo str , com uma string vazia como valor default; isso o torna opcional.

4. Obtém a classe da instância.


5. Se self.handle é a string vazia, a define como a primeira parte de name .

6. Se self.handle está em cls.all_handles , gera um ValueError .

7. Insere o novo handle em cls.all_handles .

O Exemplo 17 funciona como esperado, mas não é satisfatório pra um verificador estático de tipos. A seguir veremos a
razão disso, e como resolver o problema.

5.6.3. Atributos de classe tipados


Se verificarmos os tipos de Exemplo 17 com o Mypy, seremos repreendidos:

$ mypy hackerclub.py
hackerclub.py:37: error: Need type annotation for "all_handles"
(hint: "all_handles: Set[<type>] = ...")
Found 1 error in 1 file (checked 1 source file)

Infelizmente, a dica fornecida pelo Mypy (versão 0.910 quando essa seção foi revisada) não é muito útil no contexto do
uso de @dataclass . Primeiro, ele sugere usar Set , mas desde o Python 3.9 podemos usar set —sem a necessidade de
importar Set de typing . E mais importante, se acrescentarmos uma dica de tipo como set[…] a all_handles ,
@dataclass vai encontrar essa anotação e transformar all_handles em um campo de instância. Vimos isso
acontecer na seção Seção 5.5.3.2.

A forma de contornar esse problema definida na PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de
Variáveis) (https://fpy.li/5-11) (EN) é horrível. Para criar uma variável de classe com uma dica de tipo, precisamos usar um
pseudo-tipo chamado typing.ClassVar , que aproveita a notação de tipos genéricos ( [] ) para definir o tipo da
variável e também para declará-la como um atributo de classe.

Para fazer felizes tanto o verificador de tipos quando o @dataclass , deveríamos declarar o all_handles do Exemplo
17 assim:

PY3
all_handles: ClassVar[set[str]] = set()
Aquela dica de tipo está dizendo o seguinte:

“ all_handles é um atributo de classe do tipo set-de-str, com um set vazio como valor default.

Para escrever aquela anotação precisamos também importar ClassVar do módulo typing .

O decorador @dataclass não se importa com os tipos nas anotações, exceto em dois casos, e este é um deles: se o tipo
for ClassVar , um campo de instância não será gerado para aquele atributo.

O outro caso onde o tipo de um campo é relevante para @dataclass é quando declaramos variáveis apenas de
inicialização, nosso próximo tópico.

5.6.4. Variáveis de inicialização que não são campos


Algumas vezes pode ser necessário passar para __init__ argumentos que não são campos de instância. Tais
argumentos são chamados "argumentos apenas de inicialização" (init-only variables) pela documentação de
dataclasses (https://docs.python.org/pt-br/3/library/dataclasses.html#init-only-variables). Para declarar um argumento desses,
o módulo dataclasses oferece o pseudo-tipo InitVar , que usa a mesma sintaxe de typing.ClassVar . O exemplo
dados na documentação é uma classe de dados com um campo inicializado a partir de um banco de dados, e o objeto
banco de dados precisa ser passado para o __init__ .

O Exemplo 18 mostra o código que ilustra a seção "Variáveis de inicialização apenas"


(https://docs.python.org/pt-br/3/library/dataclasses.html#init-only-variables).

Exemplo 18. Exemplo da documentação do módulo dataclasses


(https://docs.python.org/pt-br/3/library/dataclasses.html#init-only-variables)

PY3
@dataclass
class C:
i: int
j: int = None
database: InitVar[DatabaseType] = None

def __post_init__(self, database):


if self.j is None and database is not None:
self.j = database.lookup('j')

c = C(10, database=my_database)

Veja como o atributo database é declarado. InitVar vai evitar que @dataclass trate database como um campo
regular. Ele não será definido como um atributo de instância, e a função dataclasses.fields não vai listá-lo.
Entretanto, database será um dos argumentos aceitos pelo __init__ gerado, e também será passado para o
__post_init__ . Ao escrever aquele método é preciso adicionar o argumento correspondente à sua assinatura, como
mostra o Exemplo 18.

Esse longo tratamento de @dataclass cobriu os recursos mais importantes desse decorador—alguns deles
apareceram em seções anteriores, como na Seção 5.2.1, onde falamos em paralelo das três fábricas de classes de dados.
A documentação de dataclasses (https://docs.python.org/pt-br/3/library/dataclasses.html#init-only-variables) e a PEP 526—​
Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (https://fpy.li/pep526) (EN) têm todos os detalhes.

Na próxima seção apresento um exemplo mais completo com o @dataclass .

5.6.5. Exemplo de @dataclass: o registro de recursos do Dublin Core


Frequentemente as classes criadas com o @dataclass vão ter mais campos que os exemplos muito curtos
apresentados até aqui. O Dublin Core (https://fpy.li/5-12) (EN) oferece a fundação para um exemplo mais típico de
@dataclass .

“ Ovideos,
Dublin Core é um esquema de metadados que visa descrever objetos digitais, tais como,
sons, imagens, textos e sites na web. Aplicações de Dublin Core utilizam XML e o RDF
(Resource Description Framework).[61]
— Dublin Core na Wikipedia

O padrão define 15 campos opcionais; a classe Resource , no Exemplo 19, usa 8 deles.

Exemplo 19. dataclass/resource.py: código de Resource , uma classe baseada nos termos do Dublin Core

PY3
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum, auto
from datetime import date

class ResourceType(Enum): # (1)


BOOK = auto()
EBOOK = auto()
VIDEO = auto()

@dataclass
class Resource:
"""Media resource description."""
identifier: str # (2)
title: str = '<untitled>' # (3)
creators: list[str] = field(default_factory=list)
date: Optional[date] = None # (4)
type: ResourceType = ResourceType.BOOK # (5)
description: str = ''
language: str = ''
subjects: list[str] = field(default_factory=list)

1. Esse Enum vai fornecer valores de um tipo seguro para o campo Resource.type .

2. identifier é o único campo obrigatório.


3. title é o primeiro campo com um default. Isso obriga todos os campos abaixo dele a fornecerem defaults.
4. O valor de date pode ser uma instância de datetime.date ou None .

5. O default do campo type é ResourceType.BOOK .

O Exemplo 20 mostra um doctest, para demonstrar como um registro Resource aparece no código.

Exemplo 20. dataclass/resource.py: código de Resource , uma classe baseada nos termos do Dublin Core
PY3
>>> description = 'Improving the design of existing code'
>>> book = Resource('978-0-13-475759-9', 'Refactoring, 2nd Edition',
... ['Martin Fowler', 'Kent Beck'], date(2018, 11, 19),
... ResourceType.BOOK, description, 'EN',
... ['computer programming', 'OOP'])
>>> book # doctest: +NORMALIZE_WHITESPACE
Resource(identifier='978-0-13-475759-9', title='Refactoring, 2nd Edition',
creators=['Martin Fowler', 'Kent Beck'], date=datetime.date(2018, 11, 19),
type=<ResourceType.BOOK: 1>, description='Improving the design of existing code',
language='EN', subjects=['computer programming', 'OOP'])

O __repr__ gerado pelo @dataclass é razoável, mas podemos torná-lo mais legível. Esse é o formato que queremos
para repr(book) :

PY3
>>> book # doctest: +NORMALIZE_WHITESPACE
Resource(
identifier = '978-0-13-475759-9',
title = 'Refactoring, 2nd Edition',
creators = ['Martin Fowler', 'Kent Beck'],
date = datetime.date(2018, 11, 19),
type = <ResourceType.BOOK: 1>,
description = 'Improving the design of existing code',
language = 'EN',
subjects = ['computer programming', 'OOP'],
)

O Exemplo 21 é o código para o __repr__ , produzindo o formato que aparece no trecho anterior. Esse exemplo usa
dataclass.fields para obter os nomes dos campos da classe de dados.

Exemplo 21. dataclass/resource_repr.py : código para o método __repr__ , implementado na classe Resource do
Exemplo 19

PY3
def __repr__(self):
cls = self.__class__
cls_name = cls.__name__
indent = ' ' * 4
res = [f'{cls_name}('] # (1)
for f in fields(cls): # (2)
value = getattr(self, f.name) # (3)
res.append(f'{indent}{f.name} = {value!r},') # (4)

res.append(')') # (5)
return '\n'.join(res) # (6)

1. Dá início à lista res , para criar a string de saída com o nome da classe e o parênteses abrindo.

2. Para cada campo f na classe…​


3. …​obtém o atributo nomeado da instância.
4. Anexa uma linha indentada com o nome do campo e repr(value) —é isso que o !r faz.
5. Acrescenta um parênteses fechando.
6. Cria uma string de múltiplas linhas a partir de res , e devolve essa string.

Com esse exemplo, inspirado pelo espírito de Dublin, Ohio, concluímos nosso passeio pelas fábricas de classes de dados
do Python.
Classes de dados são úteis, mas podem estar sendo usadas de forma excessiva em seu projeto. A próxima seção explica
isso.

5.7. A classe de dados como cheiro no código


Independente de você implementar uma classe de dados escrevendo todo o código ou aproveitando as facilidades
oferecidas por alguma das fábricas de classes descritas nesse capítulo, fique alerta: isso pode sinalizar um problema
em seu design.

No Refactoring: Improving the Design of Existing Code (Refatorando: Melhorando o Design de Código Existente), 2nd ed.
(https://martinfowler.com/books/refactoring.html) (Addison-Wesley), Martin Fowler e Kent Beck apresentam um catálogo de
"cheiros no código"[62]—padrões no código que podem indicar a necessidade de refatoração. O verbete entitulado "Data
Class" (Classe de Dados) começa assim:

“ Essas são classes que tem campos, métodos para obter e definir os campos, e nada mais. Tais
classes são recipientes burros de dados, e muitas vezes são manipuladas de forma
excessivamente detalhada por outras classes.

No site pessoal de Fowler, há um post muito esclarecedor chamado "Code Smell" (Cheiro no Código) (https://fpy.li/5-14)
(EN). Esse texto é muito relevante para nossa discussão, pois o autor usa a classe de dados como um exemplo de cheiro
no código, e sugere alternativas para lidar com ele. Abaixo está a tradução integral daquele artigo.[63]

Cheiros no Código
De Martin Fowler

Um cheiro no código é um indicação superficial que frequentemente corresponde a um problema mais profundo
no sistema. O termo foi inventado por Kent Beck, enquanto ele me ajudava com meu livro, Refactoring
(https://fpy.li/5-15).

A rápida definição acima contém um par de detalhes sutis. Primeiro, um cheiro é, por definição, algo rápido de
detectar—é "cheiroso", como eu disse recentemente. Um método longo é um bom exemplo disso—basta olhar o
código e ver mais de uma dúzia de linhas de Java para meu nariz se contrair.

O segundo detalhe é que cheiros nem sempre indicam um problema. Alguns métodos longos são bons. É preciso
ir mais fundo para ver se há um problema subjacente ali. Cheiros não são inerentemente ruins por si só—eles
frequentemente são o indicador de um problema, não o problema propriamente dito.

Os melhores cheiros são algo fácil de detectar e que, na maioria das vezes, leva a problemas realmente
interessantes. Classes de dados (classes contendo só dados e nenhum comportamento [próprio]) são um bom
exemplo. Você olha para elas e se pergunta que comportamento deveria fazer parte daquela classe. Então você
começa a refatorar, para incluir ali aquele comportamento. Muitas vezes, algumas perguntas simples e essas
refatorações iniciais são um passo vital para transformar um objeto anêmico em alguma coisa que realmente
tenha classe.

Uma coisa boa sobre cheiros é sua facilidade de detecção por pessoas inexperientes, mesmo aquelas pessoas que
não conhecem o suficiente para avaliar se há mesmo um problema ou , se existir, para corrigí-lo. Soube de um
líder de uma equipe de desenvolvimento que elege um "cheiro da semana", e pede às pessoas que procurem
aquele cheiro e o apresentem para colegas mais experientes. Fazer isso com um cheiro por vez é uma ótima
maneira de ensinar gradualmente os membros da equipe a serem programadores melhores.
A principal ideia da programação orientada a objetos é manter o comportamento e os dados juntos, na mesma unidade
de código: uma classe. Se uma classe é largamente utilizada mas não tem qualquer comportamento próprio
significativo, é bem provável que o código que interage com as instâncias dessa classe esteja espalhado (ou mesmo
duplicado) em métodos e funções ao longo de todo o sistema—uma receita para dores de cabeça na manutenção. Por
isso, as refatorações de Fowler para lidar com uma classe de dados envolvem trazer responsabilidades de volta para a
classe.

Levando o que foi dito acima em consideração, há alguns cenários comuns onde faz sentido ter um classe de dados
com pouco ou nenhum comportamento.

5.7.1. A classe de dados como um esboço


Nesse cenário, a classe de dados é uma implementação simplista inicial de uma classe, para dar início a um novo
projeto ou módulo. Com o tempo, a classe deve ganhar seus próprios métodos, deixando de depender de métodos de
outras classes para operar sobre suas instâncias. O esboço é temporário; ao final do processo, sua classe pode se tornar
totalmente independente da fábrica usada inicialmente para criá-la.

O Python também é muito usado para resolução rápida de problemas e para experimentaçào, e nesses casos é aceitável
deixar o esboço pronto para uso.

5.7.2. A classe de dados como representação intermediária


Uma classe de dados pode ser útil para criar registros que serão exportados para o JSON ou algum outro formato de
intercomunicação, ou para manter dados que acabaram de ser importados, cruzando alguma fronteira do sistema.
Todas as fábricas de classes de dados do Python oferecem um método ou uma função para converter uma instância em
um dict simples, e você sempre pode invocar o construtor com um dict , usado para passar argumentos nomeados
expandidos com ** . Um dict desses é muito similar a um registro JSON.

Nesse cenário, as instâncias da classe de dados devem ser tratadas como objetos imutáveis—mesmo que os campos
sejam mutáveis, não deveriam ser modificados nessa forma intermediária. Mudá-los significa perder o principal
benefício de manter os dados e o comportamento próximos. Quando o processo de importação/exportação exigir
mudança nos valores, você deve implementar seus próprios métodos de fábrica, em vez de usar os métodos "as dict"
existentes ou os construtores padrão.

Vamos agora mudar de assunto e aprender como escrever padrões que "casam" com instâncias de classes arbitrárias,
não apenas com as sequências e mapeamentos que vimos nas seções Seção 2.6 e Seção 3.3.

5.8. Pattern Matching com instâncias de classes


Padrões de classe são projetados para "casar" com instâncias de classes por tipo e—opcionalmente—por atributos. O
sujeito de um padrão de classe pode ser uma instância de qualquer classe, não apenas instâncias de classes de dados.
[64]

Há três variantes de padrões de classes: simples, nomeado e posicional. Vamos estudá-las nessa ordem.

5.8.1. Padrões de classe simples


Já vimos um exemplo de padrões de classe simples usados como sub-padrões na seção Seção 2.6:

PYTHON3
case [str(name), _, _, (float(lat), float(lon))]:

Aquele padrão "casa" com uma sequência de quatro itens, onde o primeiro item deve ser uma instância de str eo
último item deve ser um tupla de dois elementos, com duas instâncias de float .
A sintaxe dos padrões de classe se parece com a invocação de um construtor. Abaixo temos um padrão de classe que
"casa" com valores float sem vincular uma variável (o corpo do case pode ser referir a x diretamente, se
necessário):

PYTHON3
match x:
case float():
do_something_with(x)

Mas isso aqui possivelmente será um bug no seu código:

PYTHON3
match x:
case float: # DANGER!!!
do_something_with(x)

No exemplo anterior, case float: "casa" com qualquer sujeito, pois o Python entende float como uma variável, que
é então vinculada ao sujeito.

A sintaxe float(x) do padrão simples é um caso especial que se aplica apenas a onze tipos embutidos "abençoados",
listados no final da seção "Class Patterns" (Padrões de Classe) (https://fpy.li/5-16) (EN) da PEP 634—Structural Pattern
Matching: Specification ((Pattern Matching Estrutural: Especificação) (https://fpy.li/pep634):

bool bytearray bytes dict float frozenset int list set str tuple

Nessas classes, a variável que parece um argumento do construtor—por exemplo, o x em float(x) —é vinculada a
toda a instância do sujeito ou à parte do sujeito que "casa" com um sub-padrão, como exemplificado por str(name) no
padrão de sequência que vimos antes:

PYTHON3
case [str(name), _, _, (float(lat), float(lon))]:

Se a classe não de um daqueles onze tipos embutidos "abençoados", então essas variáveis parecidas com argumentos
representam padrões a serem testados com atributos de uma instância daquela classe.

5.8.2. Padrões de classe nomeados


Para entender como usar padrões de classe nomeados, observe a classe City e suas cinco instâncias no Exemplo 22,
abaixo.

Exemplo 22. A classe City e algumas instâncias


PYTHON3
import typing

class City(typing.NamedTuple):
continent: str
name: str
country: str

cities = [
City('Asia', 'Tokyo', 'JP'),
City('Asia', 'Delhi', 'IN'),
City('North America', 'Mexico City', 'MX'),
City('North America', 'New York', 'US'),
City('South America', 'São Paulo', 'BR'),
]

Dadas essas definições, a seguinte função devolve uma lista de cidades asiáticas:

PYTHON3
def match_asian_cities():
results = []
for city in cities:
match city:
case City(continent='Asia'):
results.append(city)
return results

O padrão City(continent='Asia') encontra qualquer instância de City onde o atributo continent seja igual a
'Asia' , independente do valor dos outros atributos.

Para coletar o valor do atributo country , você poderia escrever:

PYTHON3
def match_asian_countries():
results = []
for city in cities:
match city:
case City(continent='Asia', country=cc):
results.append(cc)
return results

O padrão City(continent='Asia', country=cc) encontra as mesmas cidades asiáticas, como antes, mas agora a
variável cc está vinculada ao atributo country da instância. Isso inclusive funciona se a variável do padrão também
se chamar country :

PYTHON3
match city:
case City(continent='Asia', country=country):
results.append(country)

Padrões de classe nomeados são bastante legíveis, e funcionam com qualquer classe que possua atributos de instância
públicos. Mas eles são um tanto prolixos.

Padrões de classe posicionais são mais convenientes em alguns casos, mas exigem suporte explícito da classe do sujeito,
como veremos a seguir.

5.8.3. Padrões de classe posicionais


Dadas as definições do Exemplo 22, a seguinte função devolveria uma lista de cidades asiáticas, usando um padrão de
classe posicional:

PYTHON3
def match_asian_cities_pos():
results = []
for city in cities:
match city:
case City('Asia'):
results.append(city)
return results

O padrão City('Asia') encontra qualquer instância de City na qual o valor do primeiro atributo seja Asia ,
independente do valor dos outros atributos.

Se você quiser obter o valor do atributo country , poderia escrever:

PYTHON3
def match_asian_countries_pos():
results = []
for city in cities:
match city:
case City('Asia', _, country):
results.append(country)
return results

O padrão City('Asia', _, country) encontra as mesmas cidades de antes, mas agora variável country está
vinculada ao terceiro atributo da instância.

Eu falei do "primeiro" ou do "terceiro" atributos, mas o quê isso realmente significa?

City (ou qualquer classe) funciona com padrões posicionais graças a um atributo de classe especial chamado
__match_args__ , que as fábricas de classe vistas nesse capítulo criam automaticamente. Esse é o valor de
__match_args__ na classe City :

PYCON
>>> City.__match_args__
('continent', 'name', 'country')

Como se vê, __match_args__ declara os nomes dos atributos na ordem em que eles serão usados em padrões
posicionais.

Na seção Seção 11.8 vamos escrever código para definir __match_args__ em uma classe que criaremos sem a ajuda
de uma fábrica de classes.

Você pode combinar argumentos nomeados e posicionais em um padrão. Alguns, mas não todos, os

👉 DICA atributos de instância disponíveis para o match podem estar listados no __match_args__ . Dessa
forma, algumas vezes pode ser necessário usar argumentos nomeados em um padrão, além dos
argumentos posicionais.

Hora de um resumo de capítulo.


5.9. Resumo do Capítulo
O tópico principal desse capítulo foram as fábricas de classes de dados collections.namedtuple ,
typing.NamedTuple , e dataclasses.dataclass . Vimos como cada uma delas gera classes de dados a partir de
descrições, fornecidas como argumentos a uma função fábrica ou, no caso das duas últimas, a partir de uma declaração
class com dicas de tipo. Especificamente, ambas as variantes de tupla produzem subclasses de tuple , acrescentando
apenas a capacidade de acessar os campos por nome, e criando também um atributo de classe _fields , que lista os
nomes dos campos na forma de uma tupla de strings.

A seguir colocamos lado a lado os principais recursos de cada uma das três fábricas de classes, incluindo como extrair
dados da instância como um dict , como obter os nomes e valores default dos campos, e como criar uma nova
instância a partir de uma instância existente.

Isso levou ao nosso primeiro contato com dicas de tipo, especialmente aquelas usadas para anotar atributos em uma
declaração class , usando a notação introduzida no Python 3.6 com a PEP 526—Syntax for Variable Annotations
(Sintaxe para Anotações de Variáveis) (https://fpy.li/pep526) (EN). O aspecto provavelmente mais surpreeendente das dicas
de tipo em geral é o fato delas não terem qualquer efeito durante a execução. O Python continua sendo uma linguagem
dinâmica. Ferramentas externas, como o Mypy, são necessárias para aproveitar a informação de tipagem na detecção
de erros via análise estática do código-fonte. Após um resumo básico da sintaxe da PEP 526, estudamos os efeitos das
anotações em uma classe simples e em classes criadas por typing.NamedTuple e por @dataclass .

A seguir falamos sobre os recursos mais usados dentre os oferecidos por @dataclass , e sobre a opção
default_factory da função dataclasses.field . Também demos uma olhada nas dicas de pseudo-tipo especiais
typing.ClassVar e dataclasses.InitVar , importantes no contexto das classes de dados. Esse tópico central foi
concluído com um exemplo baseado no schema Dublin Core, ilustrando como usar dataclasses.fields para iterar
sobre os atributos de uma instância de Resource em um __repr__ personalizado.

Então alertamos contra os possíveis usos abusivos das classes de dados, frustrando um princípio básico da
programação orientada a objetos: os dados e as funções que acessam os dados devem estar juntos na mesma classe.
Classes sem uma lógica podem ser um sinal de uma lógica fora de lugar.

Na última seção, vimos como o pattern matching funciona com instâncias de qualquer classe como sujeitos—e não
apenas das classes criadas com as fábricas apresentadas nesse capítulo.

5.10. Leitura complementar


A documentação padrão do Python para as fábricas de classes de dados vistas aqui é muito boa, e inclui muitos
pequenos exemplos.

Em especial para @dataclass , a maior parte da PEP 557—Data Classes (Classes de Dados) (https://fpy.li/pep557) (EN) foi
copiada para a documentação do módulo dataclasses (https://docs.python.org/pt-br/3/library/dataclasses.html) . Entretanto,
algumas seções informativas da PEP 557 (https://fpy.li/pep557) não foram copiadas, incluindo "Why not just use
namedtuple?" (Por que simplesmente não usar namedtuple?) (https://fpy.li/5-18), "Why not just use typing.NamedTuple?"
(Por que simplesmente não usar typing.NamedTuple?) (https://fpy.li/5-19), e a seção "Rationale" (Justificativa)
(https://fpy.li/5-20), que termina com a seguinte Q&A:

“ Quando não é apropriado usar Classes de Dados?


Quando for exigida compatibilidade da API com tuplas de dicts. Quando for exigida validação de
tipo além daquela oferecida pelas PEPs 484 e 526 , ou quando for exigida validação ou
conversão de valores.
— Eric V. Smith
PEP 557 "Justificativa"

Em RealPython.com (https://fpy.li/5-21), Geir Arne Hjelle escreveu um "Ultimate guide to data classes in Python 3.7" (O guia
definitivo das classes de dados no Python 3.7) (https://fpy.li/5-22) (EN) muito completo.

Na PyCon US 2018, Raymond Hettinger apresentou "Dataclasses: The code generator to end all code generators" (video)
(Dataclasses: O gerador de código para acabar com todos os geradores de código) (https://fpy.li/5-23) (EN).

Para mais recursos e funcionalidade avançada, incluindo validação, o projeto attrs (https://fpy.li/5-24) (EN), liderado por
Hynek Schlawack, surgiu anos antes de dataclasses e oferece mais facilidades, com a promessa de "trazer de volta a
alegria de criar classes, liberando você do tedioso trabalho de implementar protocolos de objeto (também conhecidos
como métodos dunder)".

A influência do attrs sobre o @dataclass é reconhecida por Eric V. Smith na PEP 557. Isso provavelmente inclui a mais
importante decisão de Smith sobre a API: o uso de um decorador de classe em vez de uma classe base ou de uma
metaclasse para realizar a tarefa.

Glyph—fundador do projeto Twisted—escreveu uma excelente introdução à attrs em "The One Python Library
Everyone Needs" (A Biblioteca Python que Todo Mundo Precisa Ter) (https://fpy.li/5-25) (EN). A documentação da attrs
inclui uma discussão aobre alternativas (https://fpy.li/5-26).

O autor de livros, instrutor e cientista maluco da computação Dave Beazley escreveu o cluegen (https://fpy.li/5-27), um
outro gerador de classes de dados. Se você já assistiu alguma palestra do David, sabe que ele é um mestre na
metaprogramação Python a partir de princípios básicos. Então achei inspirador descobrir, no arquivo README.md do
cluegen, o caso de uso concreto que o motivou a criar uma alternativa ao @dataclass do Python, e sua filosofia de
apresentar uma abordagem para resolver o problema, ao invés de fornecer uma ferramenta: a ferramenta pode
inicialmente ser mais rápida de usar , mas a aboradagem é mais flexível e pode ir tão longe quanto você deseje.

Sobre a classe de dados como um cheiro no código, a melhor fonte que encontrei foi livro de Martin Fowler, Refactoring
("Refatorando"), 2ª ed. A versão mais recente não traz a citação da epígrafe deste capitulo, "Classes de dados são como
crianças…​", mas apesar disso é a melhor edição do livro mais famoso de Fowler, em especial para pythonistas, pois os
exemplos são em JavaScript moderno, que é mais próximo do Python que do Java—a linguagem usada na primeira
edição.

O site Refactoring Guru (Guru da Refatoração) (https://fpy.li/5-28) (EN) também tem uma descrição do data class code smell
(classe de dados como cheiro no código) (https://fpy.li/5-29) (EN).

Ponto de vista
O verbete para "Guido" (https://fpy.li/5-30) no "The Jargon File" (https://fpy.li/5-31) (EN) é sobre Guido van Rossum.
Entre outras coisa, ele diz:

“ Diz a lenda que o atributo mais importante de Guido, além do próprio Python, é a máquina
do tempo de Guido, um aparelho que se diz que ele possui por causa da frequência irritante
com que pedidos de usuários por novos recursos recebem como resposta "Eu implementei
isso noite passada mesmo…​"

Por um longo tempo, uma das peças ausentes da sintaxe do Python foi uma forma rápida e padronizada de
declarar atributos de instância em uma classe. Muitas linguagens orientadas a objetos incluem esse recurso. Aqui
está parte da definição da classe Point em Smalltalk:
Object subclass: #Point
instanceVariableNames: 'x y'
classVariableNames: ''
package: 'Kernel-BasicObjects'

A segunda linha lista os nomes dos atributos de instância x e y . Se existissem atributos de classe, eles estariam
na terceira linha.

O Python sempre teve uma forma fácil de declarar um atributo de classe, se ele tiver um valor inicial. Mas
atributos de instância são muito mais comuns, e os programadores Python tem sido obrigados a olhar dentro do
método __init__ para encontrá-los, sempre temerosos que podem existir atributos de instância sendo criados
em outro lugar na classe—ou mesmo por funções e métodos de outras classes.

Agora temos o @dataclass , viva!

Mas ele traz seus próprios problemas

Primeiro, quando você usa @dataclass , dicas de tipo não são opcionais. Pelos últimos sete anos, desde a PEP 484
—Type Hints (Dicas de Tipo) (https://fpy.li/pep484) (EN), nos prometeram que elas sempre seriam opcionais. Agora
temos um novo recurso importante na linguagem que exige dicas de tipo. Se você não gosta de toda essa
tendência de tipagem estática, pode querer usar a attrs (https://fpy.li/5-24) no lugar do @dataclass .

Em segundo lugar, a sintaxe da PEP 526 (https://fpy.li/pep526) (EN) para anotar atributos de instância e de classe
inverte a convenção consagrada para declarações de classe: tudo que era declarado no nível superior de um
bloco class era um atributo de classe (métodos também são atributos de classe). Com a PEP 526 e o
@dataclass , qualquer atributo declarado no nível superior com uma dica de tipo se torna um atributo de
instância:

PYTHON3
@dataclass
class Spam:
repeat: int # instance attribute

Aqui, repeat também é um atributo de instância:

PYTHON3
@dataclass
class Spam:
repeat: int = 99 # instance attribute

Mas se não houver dicas de tipo, subitamente estamos de volta os bons velhos tempos quando declarações no
nível superior da classe pertencem apenas à classe:

PYTHON3
@dataclass
class Spam:
repeat = 99 # class attribute!

Por fim, se você desejar anotar aquele atributo de classe com um tipo, não pode usar tipos regulares, porque
então ele se tornará um atributo de instância. Você tem que recorrer a aquela anotação usando o pseudo-tipo
ClassVar :

PYTHON3
@dataclass
class Spam:
repeat: ClassVar[int] = 99 # aargh!
Aqui estamos falando sobre uma exceçao da exceção da regra. Me parece algo muito pouco pythônico.

Não tomei parte nas discussões que levaram à PEP 526 ou à PEP 557—Data Classes (Classes de Dados)
(https://fpy.li/pep557), mas aqui está uma sintaxe alternativa que eu gostaria de ver:

PYTHON3
@dataclass
class HackerClubMember:
.name: str # (1)
.guests: list = field(default_factory=list)
.handle: str = ''

all_handles = set() # (2)

1. Atributos de instância devem ser declarados com um prefixo ..

2. Qualquer nome de atributo que não tenha um prefixo . é um atributo de classe (como sempre foram).

A gramática da linguagem teria que mudar para acomodar isso. Mas acho essa forma muito legível, e ela evita o
problema da exceção-da-exceção.

Queria poder pegar a máquina do tempo de Gudo emprestada e voltar a 2017, para convencer os
desenvolvedores principais a aceitarem essa ideia.
6. Referências, mutabilidade, e memória
“ “Você está triste,” disse o Cavaleiro em um tom de voz ansioso: “deixe eu cantar para você uma
canção reconfortante. […] O nome da canção se chama ‘OLHOS DE HADOQUE’.”

“Oh, esse é o nome da canção?,” disse Alice, tentando parecer interessada.

“Não, você não entendeu,” retorquiu o Cavaleiro, um pouco irritado. “É assim que o nome É
CHAMADO. O nome na verdade é ‘O ENVELHECIDO HOMEM VELHO.‘”
— Adaptado de “Alice Através do Espelho e o que Ela Encontrou Lá”
de Lewis Caroll

Alice e o Cavaleiro dão o tom do que veremos nesse capítulo. O tema é a distinção entre objetos e seus nomes; um nome
não é o objeto; o nome é algo diferente.

Começamos o capítulo apresentando uma metáfora para variáveis em Python: variáveis são rótulos, não caixas. Se
variáveis de referência não são novidade para você, a analogia pode ainda ser útil para ilustrar questões de aliasing
(“apelidamento”) para alguém.

Depois discutimos os conceitos de identidade, valor e apelidamento de objetos. Uma característica surpreendente das
tuplas é revelada: elas são imutáveis, mas seus valores podem mudar. Isso leva a uma discussão sobre cópias rasas e
profundas. Referências e parâmetros de funções são o tema seguinte: o problema com parâmetros mutáveis por default
e formas seguras de lidar com argumentos mutáveis passados para nossas funções por clientes.

As últimas seções do capítulo tratam de coleta de lixo (“garbage collection”), o comando del e de algumas peças que o
Python prega com objetos imutáveis.

É um capítulo bastante árido, mas os tópicos tratados podem causar muitos bugs sutis em programas reais em Python.

6.1. Novidades nesse capítulo


Os tópicos tratados aqui são muito estáveis e fundamentais. Não foi introduzida nenhuma mudança digna de nota
nesta segunda edição.

Acrescentei um exemplo usando is para testar a existência de um objeto sentinela, e um aviso sobre o mau uso do
operador is no final de Seção 6.3.1.

Este capítulo estava na Parte IV, mas decidi abordar esses temas mais cedo, pois eles funcionam melhor como o
encerramento da Parte II, “Estruturas de Dados”, que como abertura de “Práticas de Orientação a Objetos"

✒️ NOTA A seção sobre “Referências Fracas” da primeira edição deste livro agora é um post em
fluentpython.com (https://fpy.li/weakref).

Vamos começar desaprendendo que uma variável é como uma caixa onde você guarda dados.

6.2. Variáveis não são caixas


Em 1997, fiz um curso de verão sobre Java no MIT. A professora, Lynn Stein [65] , apontou que a metáfora comum, de
“variáveis como caixas”, na verdade, atrapalha o entendimento de variáveis de referência em linguagens orientadas a
objetos. As variáveis em Python são como variáveis de referência em Java; uma metáfora melhor é pensar em uma
variável como um rótulo (ou etiqueta) que associa um nome a um objeto. O exemplo e a figura a seguir ajudam a
entender o motivo disso.
Exemplo 1 é uma interação simples que não pode ser explicada por “variáveis como caixas”. A Figura 1 ilustra o motivo
de metáfora da caixa estar errada em Python, enquanto etiquetas apresentam uma imagem mais útil para entender
como variáveis funcionam.

Exemplo 1. As variáveis a e b mantém referências para a mesma lista, não cópias da lista.

PYCON
>>> a = [1, 2, 3] (1)
>>> b = a (2)
>>> a.append(4) (3)
>>> b (4)
[1, 2, 3, 4]

1. Cria uma lista [1, 2, 3] e a vincula à variável a.

2. Vincula a variável b ao mesmo valor referenciado por a.

3. Modifica a lista referenciada por a , anexando um novo item.

4. É possível ver o efeito através da variável b . Se você pensar em b como uma caixa que guardava uma cópia de
[1, 2, 3] da caixa a , este comportamento não faz sentido.

Figura 1. Se você imaginar variáveis como caixas, não é possível entender a atribuição em Python; por outro lado,
pense em variáveis como etiquetas autocolantes e Exemplo 1 é facilmente explicável.

Assim, a instrução b = a não copia o conteúdo de uma caixa a para uma caixa b . Ela cola uma nova etiqueta b no
objeto que já tem a etiqueta a .

A professora Stein também falava sobre atribuição de uma maneira bastante específica. Por exemplo, quando discutia
sobre um objeto representando uma gangorra em uma simulação, ela dizia: “A variável g foi atribuída à gangorra”,
mas nunca “A gangorra foi atribuída à variável g”. Com variáveis de referência, faz muito mais sentido dizer que a
variável é atribuída a um objeto, não o contrário. Afinal, o objeto é criado antes da atribuição. Exemplo 2 prova que o
lado direito de uma atribuição é processado primeiro.

Já que o verbo “atribuir” é usado de diferentes maneiras, uma alternativa útil é “vincular”: a declaração de atribuição
em Python x = … vincula o nome x ao objeto criado ou referenciado no lado direito. E o objeto precisa existir antes
que um nome possa ser vinculado a ele, como demonstra Exemplo 2.

Exemplo 2. Variáveis são vinculadas a objetos somente após os objetos serem criados
PYCON
>>> class Gizmo:
... def __init__(self):
... print(f'Gizmo id: {id(self)}')
...
>>> x = Gizmo()
Gizmo id: 4301489152 (1)
>>> y = Gizmo() * 10 (2)
Gizmo id: 4301489432 (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>>
>>> dir() (4)
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']

1. A saída Gizmo id: … é um efeito colateral da criação de uma instância de Gizmo .

2. Multiplicar uma instância de Gizmo levanta uma exceção.


3. Aqui está a prova que um segundo Gizmo foi de fato instanciado antes que a multiplicação fosse tentada.
4. Mas a variável y nunca foi criada, porque a exceção aconteceu enquanto a parte direita da atribuição estava
sendo executada.

Para entender uma atribuição em Python, leia primeiro o lado direito: é ali que o objeto é criado ou
👉 DICA recuperado. Depois disso, a variável do lado esquerdo é vinculada ao objeto, como uma etiqueta
colada a ele. Esqueça as caixas.

Como variáveis são apenas meras etiquetas, nada impede que um objeto tenha várias etiquetas vinculadas a si. Quando
isso acontece, você tem apelidos (aliases), nosso próximo tópico.

6.3. Identidade, igualdade e apelidos


Lewis Carroll é o pseudônimo literário do Prof. Charles Lutwidge Dodgson. O Sr. Carroll não é apenas igual ao Prof.
Dodgson, eles são exatamente a mesma pessoa. Exemplo 3 expressa essa ideia em Python.

Exemplo 3. charles e lewis se referem ao mesmo objeto

PYCON
>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
>>> lewis = charles (1)
>>> lewis is charles
True
>>> id(charles), id(lewis) (2)
(4300473992, 4300473992)
>>> lewis['balance'] = 950 (3)
>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}

1. lewis é um apelido para charles .

2. O operador is e a função id confirmam essa afirmação.


3. Adicionar um item a lewis é o mesmo que adicionar um item a charles .
Entretanto, suponha que um impostor—vamos chamá-lo de Dr. Alexander Pedachenko—diga que é o verdadeiro
Charles L. Dodgson, nascido em 1832. Suas credenciais podem ser as mesmas, mas o Dr. Pedachenko não é o Prof.
Dodgson. Figura 2 ilustra esse cenário.

Figura 2. charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor
igual.

Exemplo 4 implementa e testa o objeto alex como apresentado em Figura 2.

Exemplo 4. alex e charles são iguais quando comparados, mas alex não é charles

PYCON
>>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} (1)
>>> alex == charles (2)
True
>>> alex is not charles (3)
True

1. alex é uma referência a um objeto que é uma réplica do objeto vinculado a charles .

2. Os objetos são iguais quando comparados devido à implementação de __eq__ na classe dict .

3. Mas são objetos distintos. Essa é a forma pythônica de escrever a negação de uma comparação de identidade: a is
not b .

Exemplo 3 é um exemplo de apelidamento (aliasing). Naquele código, lewis e charles são apelidos: duas variáveis
vinculadas ao mesmo objeto. Por outro lado, alex não é um apelido para charles : essas variáveis estão vinculadas a
objetos diferentes. Os objetos vinculados a alex e charles tem o mesmo valor — é isso que == compara — mas tem
identidades diferentes.

Na The Python Language Reference (Referência da Linguagem Python), https://docs.python.org/pt-


br/3/reference/datamodel.html#objects-values-and-types está escrito:

“ Aendereço
identidade de um objeto nunca muda após ele ter sido criado; você pode pensar nela como o
do objeto na memória. O operador compara a identidade de dois objetos; a função
is
id() retorna um inteiro representando essa identidade.

O verdadeiro significado do ID de um objeto depende da implementação da linguagem. Em CPython, id() retorna o


endereço de memória do objeto, mas outro interpretador Python pode retornar algo diferente. O ponto fundamental é
que o ID será sempre um valor numérico único, e ele jamais mudará durante a vida do objeto.
Na prática, nós raramente usamos a função id() quando programamos. A verificação de identidade é feita, na maior
parte das vezes, com o operador is , que compara os IDs dos objetos, então nosso código não precisa chamar id()
explicitamente. A seguir vamos falar sobre is versus == .

Para o revisor técnico Leonardo Rochael, o uso mais frequente de id() ocorre durante o processo
👉 DICA de debugging, quando o repr() de dois objetos são semelhantes, mas você precisa saber se duas
referências são apelidos ou apontam para objetos diferentes. Se as referências estão em contextos
diferentes — por exemplo, em stack frames diferentes — pode não ser viável usar is .

6.3.1. Escolhendo Entre == e is


O operador == compara os valores de objetos (os dados que eles contêm), enquanto is compara suas identidades.

Quando estamos programando, em geral, nos preocupamos mais com os valores que com as identidades dos objetos,
então == aparece com mais frequência que is em programas Python.

Entretanto, se você estiver comparando uma variável com um singleton (um objeto único) faz mais sentido usar is . O
caso mais comum, de longe, é verificar se a variável está vinculada a None . Esta é a forma recomendada de fazer isso:

PYTHON3
x is None

E a forma apropriada de escrever sua negação é:

PYTHON3
x is not None

None é o singleton mais comum que testamos com is . Objetos sentinela são outro exemplo de singletons que
testamos com is . Veja um modo de criar e testar um objeto sentinela:

PYTHON3
END_OF_DATA = object()
# ... many lines
def traverse(...):
# ... more lines
if node is END_OF_DATA:
return
# etc.

O operador is é mais rápido que == , pois não pode ser sobrecarregado. Daí o Python não precisa encontrar e invocar
métodos especiais para calcular seu resultado, e o processamento é tão simples quanto comparar dois IDs inteiros. Por
outro lado, a == b é açúcar sintático para a.__eq__(b) . O método __eq__ , herdado de object , compara os IDs dos
objetos, então produz o mesmo resultado de is . Mas a maioria dos tipos embutidos sobrepõe __eq__ com
implementações mais úteis, que levam em consideração os valores dos atributos dos objetos. A determinação da
igualdade pode envolver muito processamento—​por exemplo, quando se comparam coleções grandes ou estruturas
aninhadas com muitos níveis.

Normalmente estamos mais interessados na igualdade que na identidade de objetos. Checar se o

⚠️ AVISO objeto é None é o único caso de uso comum do operador is . A maioria dos outros usos que eu vejo
quando reviso código estão errados. Se você não estiver seguro, use == . Em geral, é o que você quer,
e ele também funciona com None , ainda que não tão rápido.

Para concluir essa discussão de identidade versus igualdade, vamos ver como o tipo notoriamente imutável tuple não
é assim tão invariável quanto você poderia supor.
6.3.2. A imutabilidade relativa das tuplas
As tuplas, como a maioria das coleções em Python — lists, dicts, sets, etc..— são contêiners: eles armazenam referências
para objetos.[66]

Se os itens referenciados forem mutáveis, eles poderão mudar, mesmo que tupla em si não mude. Em outras palavras, a
imutabilidade das tuplas, na verdade, se refere ao conteúdo físico da estrutura de dados tupla (isto é, as referências
que ela mantém), e não se estende aos objetos referenciados.

Exemplo 5 ilustra uma situação em que o valor de uma tupla muda como resultado de mudanças em um objeto
mutável ali referenciado. O que não pode nunca mudar em uma tupla é a identidade dos itens que ela contém.

Exemplo 5. t1 e t2 inicialmente são iguais, mas a mudança em um item mutável dentro da tupla t1 as torna
diferentes

PYCON
>>> t1 = (1, 2, [30, 40]) (1)
>>> t2 = (1, 2, [30, 40]) (2)
>>> t1 == t2 (3)
True
>>> id(t1[-1]) (4)
4302515784
>>> t1[-1].append(99) (5)
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1]) (6)
4302515784
>>> t1 == t2 (7)
False

1. t1 é imutável, mas t1[-1] é mutável.


2. Cria a tupla t2 , cujos itens são iguais àqueles de t1 .

3. Apesar de serem objetos distintos, quando comparados t1 e t2 são iguais, como esperado.
4. Obtém o ID da lista na posição t1[-1] .

5. Modifica diretamente a lista t1[-1] .

6. O ID de t1[-1] não mudou, apenas seu valor.


7. t1 e t2 agora são diferentes

Essa imutabilidade relativa das tuplas está por trás do enigma Seção 2.8.3. Essa também é razão pela qual não é
possível gerar o hash de algumas tuplas, como vimos em Seção 3.4.1.

A distinção entre igualdade e identidade tem outras implicações quando você precisa copiar um objeto. Uma cópia é
um objeto igual com um ID diferente. Mas se um objeto contém outros objetos, é preciso que a cópia duplique os
objetos internos ou eles podem ser compartilhados? Não há uma resposta única. A seguir discutimos esse ponto.

6.4. A princípio, cópias são rasas


A forma mais fácil de copiar uma lista (ou a maioria das coleções mutáveis nativas) é usando o construtor padrão do
próprio tipo. Por exemplo:
PYCON
>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1) (1)
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1 (2)
True
>>> l2 is l1 (3)
False

1. list(l1) cria uma cópia de l1 .

2. As cópias são iguais…​


3. …​mas se referem a dois objetos diferentes.

Para listas e outras sequências mutáveis, o atalho l2 = l1[:] também cria uma cópia.

Contudo, tanto o construtor quanto [:] produzem uma cópia rasa (shallow copy). Isto é, o contêiner externo é
duplicado, mas a cópia é preenchida com referências para os mesmos itens contidos no contêiner original. Isso
economiza memória e não causa qualquer problema se todos os itens forem imutáveis. Mas se existirem itens
mutáveis, isso pode gerar surpresas desagradáveis.

Em Exemplo 6 criamos uma lista contendo outra lista e uma tupla, e então fazemos algumas mudanças para ver como
isso afeta os objetos referenciados.

Se você tem um computador conectado à internet disponível, recomendo fortemente que você
assista à animação interativa do Exemplo 6 em Online Python Tutor (https://fpy.li/6-3). No momento em
👉 DICA que escrevo, o link direto para um exemplo pronto no pythontutor.com não estava funcionando de
forma estável. Mas a ferramenta é ótima, então vale a pena gastar seu tempo copiando e colando o
código.

Exemplo 6. Criando uma cópia rasa de uma lista contendo outra lista; copie e cole esse código para vê-lo animado no
Online Python Tutor

PYTHON3
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # (1)
l1.append(100) # (2)
l1[1].remove(55) # (3)
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22] # (4)
l2[2] += (10, 11) # (5)
print('l1:', l1)
print('l2:', l2)

1. l2 é uma cópia rasa de l1 . Este estado está representado em Figura 3.

2. Concatenar 100 a l1 não tem qualquer efeito sobre l2 .

3. Aqui removemos 55 da lista interna l1[1] . Isso afeta l2 , pois l2[1] está associado à mesma lista em l1[1] .

4. Para um objeto mutável como a lista referida por l2[1] , o operador += altera a lista diretamente. Essa mudança é
visível em l1[1] , que é um apelido para l2[1] .
5. += em uma tupla cria uma nova tupla e reassocia a variável l2[2] a ela. Isso é equivalente a fazer l2[2] =
l2[2] + (10, 11) . Agora as tuplas na última posição de l1 e l2 não são mais o mesmo objeto. Veja Figura 4.
Figura 3. Estado do programa imediatamente após a atribuição l2 = list(l1) em Exemplo 6. l1 e l2 se referem a
listas diferentes, mas as listas compartilham referências para um mesmo objeto interno, a lista [66, 55, 44] e para a
tupla (7, 8, 9) . (Diagrama gerado pelo Online Python Tutor)

A saída de Exemplo 6 é Exemplo 7, e o estado final dos objetos está representado em Figura 4.

Exemplo 7. Saída de Exemplo 6

PYTHON3
l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

Figura 4. Estado final de l1 e l2 : elas ainda compartilham referências para o mesmo objeto lista, que agora contém
[66, 44, 33, 22] , mas a operação l2[2] += (10, 11) criou uma nova tupla com conteúdo (7, 8, 9, 10, 11) ,
sem relação com a tupla (7, 8, 9) referenciada por l1[2] . (Diagram generated by the Online Python Tutor.)
Já deve estar claro que cópias rasas são fáceis de criar, mas podem ou não ser o que você quer. Nosso próximo tópico é
a criação de cópias profundas.

6.4.1. Cópias profundas e cópias rasas


Trabalhar com cópias rasas nem sempre é um problema, mas algumas vezes você vai precisar criar cópias profundas
(isto é, cópias que não compartilham referências de objetos internos). O módulo copy oferece as funções deepcopy e
copy , que retornam cópias profundas e rasas de objetos arbitrários.

Para ilustrar o uso de copy() e deepcopy() , Exemplo 8 define uma classe simples, Bus , representando um ônibus
escolar que é carregado com passageiros, e então pega ou deixa passageiros ao longo de sua rota.

Exemplo 8. Bus pega ou deixa passageiros

PY
class Bus:

def __init__(self, passengers=None):


if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)

def pick(self, name):


self.passengers.append(name)

def drop(self, name):


self.passengers.remove(name)

Agora, no Exemplo 9 interativo, vamos criar um objeto bus (bus1) e dois clones—uma cópia rasa (bus2) e uma cópia
profunda (bus3)—para ver o que acontece quando bus1 deixa um passageiro.

Exemplo 9. Os efeitos do uso de copy versus deepcopy

PYCON
>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752) (1)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David'] (2)
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800) (3)
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David'] (4)

1. Usando copy e deepcopy , criamos três instâncias distintas de Bus .

2. Após bus1 deixar 'Bill' , ele também desaparece de bus2 .

3. A inspeção do atributo dos passengers mostra que bus1 e bus2 compartilham o mesmo objeto lista, pois bus2
é uma cópia rasa de bus1 .
4. bus3 é uma cópia profunda de bus1 , então seu atributo passengers se refere a outra lista.
Observe que, em geral, criar cópias profundas não é uma questão simples. Objetos podem conter referências cíclicas
que fariam um algoritmo ingênuo entrar em um loop infinito. A função 'deepcopy' lembra dos objetos já copiados, de
forma a tratar referências cíclicas de modo elegante. Isso é demonstrado em Exemplo 10.

Exemplo 10. Referências cíclicas: b tem uma referência para a e então é concatenado a a ; ainda assim, deepcopy
consegue copiar a .

PYCON
>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]

Além disso, algumas vezes uma cópia profunda pode ser profunda demais. Por exemplo, objetos podem ter referências
para recursos externos ou para singletons (objetos únicos) que não devem ser copiados. Você pode controlar o
comportamento de copy e de deepcopy implementando os métodos especiais __copy__ e __deepcopy__ , como
descrito em https://docs.python.org/pt-br/3/library/copy.html [documentação do módulo copy ]

O compartilhamento de objetos através de apelidos também explica como a passagens de parâmetros funciona em
Python, e o problema do uso de tipos mutáveis como parâmetros default. Vamos falar sobre essas questões a seguir.

6.5. Parâmetros de função como referências


O único modo de passagem de parâmetros em Python é a chamada por compartilhamento (call by sharing). É o mesmo
modo usado na maioria das linguagens orientadas a objetos, incluindo Javascript, Ruby e Java (em Java isso se aplica
aos tipos de referência; tipos primitivos usam a chamada por valor). Chamada por compartilhamento significa que
cada parâmetro formal da função recebe uma cópia de cada referência nos argumentos. Em outras palavras, os
parâmetros dentro da função se tornam apelidos dos argumentos.

O resultado desse esquema é que a função pode modificar qualquer objeto mutável passado a ela como parâmetro,
mas não pode mudar a identidade daqueles objetos (isto é, ela não pode substituir integralmente um objeto por outro).
Exemplo 11 mostra uma função simples usando += com um de seus parâmetros. Quando passamos números, listas e
tuplas para a função, os argumentos originais são afetados de maneiras diferentes.

Exemplo 11. Uma função pode mudar qualquer objeto mutável que receba
PYCON
>>> def f(a, b):
... a += b
... return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y (1)
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b (2)
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u) (3)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))

1. O número x não se altera.


2. A lista a é alterada.
3. A tupla t não se altera.

Outra questão relacionada a parâmetros de função é o uso de valores mutáveis como defaults, discutida a seguir.

6.5.1. Porque evitar tipos mutáveis como default em parâmetros


Parâmetros opcionais com valores default são um ótimo recurso para definição de funções em Python, permitindo que
nossas APIs evoluam mantendo a compatibilidade com versões anteriores. Entretanto, você deve evitar usar objetos
mutáveis como valores default em parâmetros.

Para ilustrar esse ponto, em Exemplo 12, modificamos o método __init__ da classe Bus de Exemplo 8 para criar
HauntedBus . Tentamos ser espertos: em vez do valor default passengers=None , temos passengers=[] , para evitar o
if do __init__ anterior. Essa "esperteza" causa problemas.

Exemplo 12. Uma classe simples ilustrando o perigo de um default mutável

PY
class HauntedBus:
"""A bus model haunted by ghost passengers"""

def __init__(self, passengers=[]): # (1)


self.passengers = passengers # (2)

def pick(self, name):


self.passengers.append(name) # (3)

def drop(self, name):


self.passengers.remove(name)

1. Quando o argumento passengers não é passado, esse parâmetro é vinculado ao objeto lista default, que
inicialmente está vazio.
2. Essa atribuição torna self.passengers um apelido de passengers , que por sua vez é um apelido para a lista
default, quando um argumento passengers não é passado para a função.
3. Quando os métodos .remove() e .append() são usados com self.passengers , estamos, na verdade, mudando
a lista default, que é um atributo do objeto-função.
Exemplo 13 mostra o comportamento misterioso de HauntedBus .

Exemplo 13. Ônibus assombrados por passageiros fantasmas

PYCON
>>> bus1 = HauntedBus(['Alice', 'Bill']) (1)
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers (2)
['Bill', 'Charlie']
>>> bus2 = HauntedBus() (3)
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus() (4)
>>> bus3.passengers (5)
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers (6)
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers (7)
True
>>> bus1.passengers (8)
['Bill', 'Charlie']

1. bus1 começa com uma lista de dois passageiros.


2. Até aqui, tudo bem: nenhuma surpresa em bus1 .

3. bus2 começa vazio, então a lista vazia default é vinculada a self.passengers .

4. bus3 também começa vazio, e novamente a lista default é atribuída.


5. A lista default não está mais vazia!
6. Agora Dave , pego pelo bus3 , aparece no bus2 .

7. O problema: bus2.passengers e bus3.passengers se referem à mesma lista.


8. Mas bus1.passengers é uma lista diferente.

O problema é que instâncias de HauntedBus que não recebem uma lista de passageiros inicial acabam todas
compartilhando a mesma lista de passageiros entre si.

Este tipo de bug pode ser muito sutil. Como Exemplo 13 demonstra, quando HauntedBus recebe uma lista com
passageiros como parâmetro, ele funciona como esperado. As coisas estranhas acontecem somente quando
HauntedBus começa vazio, pois aí self.passengers se torna um apelido para o valor default do parâmetro
passengers . O problema é que cada valor default é processado quando a função é definida — i.e., normalmente
quando o módulo é carregado — e os valores default se tornam atributos do objeto-função. Assim, se o valor default é
um objeto mutável e você o altera, a alteração vai afetar todas as futuras chamadas da função.

Após executar as linhas do exemplo em Exemplo 13, você pode inspecionar o objeto HauntedBus.__init__ e ver os
estudantes fantasma assombrando o atributo __defaults__ :
PYCON
>>> dir(HauntedBus.__init__) # doctest: +ELLIPSIS
['__annotations__', '__call__', ..., '__defaults__', ...]
>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)

Por fim, podemos verificar que bus2.passengers é um apelido vinculado ao primeiro elemento do atributo
HauntedBus.__init__.__defaults__ :

PYCON
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True

O problema com defaults mutáveis explica porque None é normalmente usado como valor default para parâmetros
que podem receber valores mutáveis. Em Exemplo 8, __init__ checa se o argumento passengers é None . Se for,
self.passengers é vinculado a uma nova lista vazia. Se passengers não for None , a implementação correta
vincula uma cópia daquele argumento a self.passengers . A próxima seção explica porque copiar o argumento é
uma boa prática.

6.5.2. Programação defensiva com argumentos mutáveis


Ao escrever uma função que recebe um argumento mutável, você deve considerar com cuidado se o cliente que chama
sua função espera que o argumento passado seja modificado.

Por exemplo, se sua função recebe um dict e precisa modificá-lo durante seu processamento, esse efeito colateral
deve ou não ser visível fora da função? A resposta, na verdade, depende do contexto. É tudo uma questão de alinhar as
expectativas do autor da função com as do cliente da função.

O último exemplo com ônibus neste capítulo mostra como o TwilightBus viola as expectativas ao compartilhar sua
lista de passageiros com seus clientes. Antes de estudar a implementação, veja como a classe TwilightBus funciona
pela perspectiva de um cliente daquela classe, em Exemplo 14.

Exemplo 14. Passageiros desaparecem quando são deixados por um TwilightBus

PYCON
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] (1)
>>> bus = TwilightBus(basketball_team) (2)
>>> bus.drop('Tina') (3)
>>> bus.drop('Pat')
>>> basketball_team (4)
['Sue', 'Maya', 'Diana']

1. basketball_team contém o nome de cinco estudantes.


2. Um TwilightBus é carregado com o time.
3. O bus deixa uma estudante, depois outra.
4. As passageiras desembarcadas desapareceram do time de basquete!

TwilightBus viola o "Princípio da Menor Surpresa Possível", uma boa prática do design de interfaces.[67] É
certamente espantoso que quando o ônibus deixa uma estudante, seu nome seja removido da escalação do time de
basquete.

Exemplo 15 é a implementação de TwilightBus e uma explicação do problema.

Exemplo 15. Uma classe simples mostrando os perigos de mudar argumentos recebidos
PY
class TwilightBus:
"""A bus model that makes passengers vanish"""

def __init__(self, passengers=None):


if passengers is None:
self.passengers = [] # (1)
else:
self.passengers = passengers #(2)

def pick(self, name):


self.passengers.append(name)

def drop(self, name):


self.passengers.remove(name) # (3)

1. Aqui nós cuidadosamente criamos uma lista vazia quando passengers é None .

2. Entretanto, esta atribuição transforma self.passengers em um apelido para passengers , que por sua vez é um
apelido para o argumento efetivamente passado para __init__ (i.e. basketball_team em Exemplo 14).
3. Quando os métodos .remove() e .append() são usados com self.passengers , estamos, na verdade,
modificando a lista original recebida como argumento pelo construtor.

O problema aqui é que o ônibus está apelidando a lista passada para o construtor. Ao invés disso, ele deveria manter
sua própria lista de passageiros. A solução é simples: em __init__ , quando o parâmetro passengers é fornecido,
self.passengers deveria ser inicializado com uma cópia daquela lista, como fizemos, de forma correta, em Exemplo
8:

PYTHON3
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) (1)

1. Cria uma cópia da lista passengers , ou converte o argumento para list se ele não for uma lista.

Agora nossa manipulação interna da lista de passageiros não afetará o argumento usado para inicializar o ônibus. E
com uma vantagem adicional, essa solução é mais flexível: agora o argumento passado no parâmetro passengers
pode ser uma tupla ou qualquer outro tipo iterável, como set ou mesmo resultados de uma consulta a um banco de
dados, pois o construtor de list aceita qualquer iterável. Ao criar nossa própria lista, estamos também assegurando
que ela suporta os métodos necessários, .remove() e .append() , operações que usamos nos métodos .pick() e
.drop() .

A menos que um método tenha o objetivo explícito de alterar um objeto recebido como argumento,
você deveria pensar bem antes de apelidar tal objeto e simplesmente vinculá-lo a uma variável

👉 DICA interna de sua classe. Quando em dúvida, crie uma cópia. Os clientes de sua classe ficarão mais
felizes. Claro, criar uma cópia não é grátis: há um custo de processamento e uso de memória.
Entretanto, uma API que causa bugs sutis é, em geral, um problema bem maior que uma que seja
um pouco mais lenta ou que use mais recursos.

Agora vamos conversar sobre um dos comandos mais incompreendidos em Python: del .
6.6. del e coleta de lixo
“ Oselesobjetos nunca são destruídos explicitamente; no entanto, quando eles se tornam inacessíveis,
podem ser coletados como lixo.
— “Modelo de Dados” capítulo de A Referência da Linguagem Python

A primeira estranheza sobre del é ele não ser uma função, mas um comando.

Escrevemos del x e não del(x) — apesar dessa última forma funcionar também, mas apenas porque as expressões
x e (x) em geral terem o mesmo significado em Python.

O segundo aspecto surpreendente é que del apaga referências, não objetos. A coleta de lixo pode eliminar um objeto
da memória como resultado indireto de del , se a variável apagada for a última referência ao objeto. Reassociar uma
variável também pode reduzir a zero o número de referências a um objeto, causando sua destruição.

PYCON
>>> a = [1, 2] (1)
>>> b = a (2)
>>> del a (3)
>>> b (4)
[1, 2]
>>> b = [3] (5)

1. Cria o objeto [1, 2] e vincula a a ele.


2. Vincula b ao mesmo objeto [1, 2] .

3. Apaga a referência a.

4. [1, 2] não é afetado, pois b ainda aponta para ele.


5. Reassociar b a um objeto diferente remove a última referência restante a [1, 2] . Agora o coletor de lixo pode
descartar aquele objeto.

Existe um método especial __del__ , mas ele não causa a remoção de uma instância e não deve ser
usado em seu código. __del__ é invocado pelo interpretador Python quando a instância está
prestes a ser destruída, para dar a ela a chance de liberar recursos externos. É muito raro ser preciso
⚠️ AVISO implementar __del__ em seu código, mas ainda assim alguns programadores Python perdem
tempo codando este método sem necessidade. O uso correto de __del__ é bastante complexo.
Consulte __del__ (https://docs.python.org/pt-br/3/reference/datamodel.html#object.%5C_%5C_del__) no
capítulo "Modelo de Dados" em A Referência da Linguagem Python.

Em CPython, o algoritmo primário de coleta de lixo é a contagem de referências. Essencialmente, cada objeto mantém
uma contagem do número de referências apontando para si. Assim que a contagem chega a zero, o objeto é
imediatamente destruído: CPython invoca o método __del__ no objeto (se definido) e daí libera a memória alocada
para aquele objeto. Em CPython 2.0, um algoritmo de coleta de lixo geracional foi acrescentado, para detectar grupos
de objetos envolvidos em referências cíclicas — grupos que pode ser inacessíveis mesmo que existam referências
restantes, quando todas as referências mútuas estão contidas dentro daquele grupo. Outras implementações de Python
tem coletores de lixo mais sofisticados, que não se baseiam na contagem de referências, o que significa que o método
__del__ pode não ser chamado imediatamente quando não existem mais referências ao objeto. Veja "PyPy, Garbage
Collection, and a Deadlock" (https://fpy.li/6-7) (EN) by A. Jesse Jiryu Davis para uma discussão sobre os usos próprios e
impróprios de __del__ .

Para demonstrar o fim da vida de um objeto, Exemplo 16 usa weakref.finalize para registrar uma função callback a
ser chamada quando o objeto é destruído.
Exemplo 16. Assistindo o fim de um objeto quando não resta nenhuma referência apontando para ele

PYCON
>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1 (1)
>>> def bye(): (2)
... print('...like tears in the rain.')
...
>>> ender = weakref.finalize(s1, bye) (3)
>>> ender.alive (4)
True
>>> del s1
>>> ender.alive (5)
True
>>> s2 = 'spam' (6)
...like tears in the rain.
>>> ender.alive
False

1. s1 e s2 são apelidos do mesmo conjunto, {1, 2, 3} .

2. Essa função não deve ser um método associado ao objeto prestes a ser destruído, nem manter uma referência para
o objeto.
3. Registra o callback bye no objeto referenciado por s1 .

4. O atributo .alive é True antes do objeto finalize ser chamado.


5. Como vimos, del não apaga o objeto, apenas a referência s1 a ele.
6. Reassociar a última referência, s2 , torna {1, 2, 3} inacessível. Ele é destruído, o callback bye é invocado, e
ender.alive se torna False .

O ponto principal de Exemplo 16 é mostrar explicitamente que del não apaga objetos, mas que objetos podem ser
apagados como uma consequência de se tornarem inacessíveis após o uso de del .

Você pode estar se perguntando porque o objeto {1, 2, 3} foi destruído em Exemplo 16. Afinal, a referência s1 foi
passada para a função finalize , que precisa tê-la mantido para conseguir monitorar o objeto e invocar o callback.
Isso funciona porque finalize mantém uma referência fraca (weak reference) para {1, 2, 3}. Referências fracas não
aumentam a contagem de referências de um objeto. Assim, uma referência fraca não evita que o objeto alvo seja
destruído pelo coletor de lixo. Referências fracas são úteis em cenários de caching, pois não queremos que os objetos
"cacheados" sejam mantidos vivos apenas por terem uma referência no cache.

✒️ NOTA Referências fracas são um tópico muito especializado, então decidi retirá-lo dessa segunda edição.
Em vez disso, publiquei a nota "Weak References" em fluentpython.com (https://fpy.li/weakref).

6.7. Peças que Python prega com imutáveis


Esta seção opcional discute alguns detalhes que, na verdade, não são muito importantes para

✒️ NOTA usuários de Python, e que podem não se aplicar a outras implementações da linguagem ou mesmo a
futuras versões de CPython. Entretanto, já vi muita gente tropeçar nesses casos laterais e daí passar
a usar o operador is de forma incorreta, então acho que vale a pena mencionar esses detalhes.

Eu fiquei surpreso em descobrir que, para uma tupla t , a chamada t[:] não cria uma cópia, mas devolve uma
referência para o mesmo objeto. Da mesma forma, tuple(t) também retorna uma referência para a mesma tupla.[68]
Exemplo 17 demonstra esse fato.

Exemplo 17. Uma tupla construída a partir de outra é, na verdade, exatamente a mesma tupla.

PYCON
>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1 (1)
True
>>> t3 = t1[:]
>>> t3 is t1 (2)
True

1. t1 e t2 estão vinculadas ao mesmo objeto


2. Assim como t3 .

O mesmo comportamento pode ser observado com instâncias de str , bytes e frozenset . Observe que frozenset
não é uma sequência, então fs[:] não funciona se fs é um frozenset . Mas fs.copy() tem o mesmo efeito: ele
trapaceia e retorna uma referência ao mesmo objeto, e não uma cópia, como mostra Exemplo 18.[69]

Exemplo 18. Strings literais podem criar objetos compartilhados.

PYCON
>>> t1 = (1, 2, 3)
>>> t3 = (1, 2, 3) # (1)
>>> t3 is t1 # (2)
False
>>> s1 = 'ABC'
>>> s2 = 'ABC' # (3)
>>> s2 is s1 # (4)
True

1. Criando uma nova tupla do zero.


2. t1 e t3 são iguais, mas não são o mesmo objeto.
3. Criando uma segunda str do zero.
4. Surpresa: a e b se referem à mesma str !

O compartilhamento de strings literais é uma técnica de otimização chamada internalização (interning). O CPython usa
uma técnica similar com inteiros pequenos, para evitar a duplicação desnecessária de números que aparecem com
muita frequência em programas, como 0, 1, -1, etc. Observe que o CPython não internaliza todas as strings e inteiros, e
o critério pelo qual ele faz isso é um detalhe de implementação não documentado.

Nunca dependa da internalização de str ou int ! Sempre use == em vez de is para verificar a
⚠️ AVISO igualdade de strings ou inteiros. A internalização é uma otimização para uso interno do
interpretador Python.

Os truques discutidos nessa seção, incluindo o comportamento de frozenset.copy() , são mentiras inofensivas que
economizam memória e tornam o interpretador mais rápido. Não se preocupe, elas não trarão nenhum problema, pois
se aplicam apenas a tipos imutáveis. Provavelmente, o melhor uso para esse tipo de detalhe é ganhar apostas contra
outros Pythonistas.[70]
6.8. Resumo do capítulo
Todo objeto em Python tem uma identidade, um tipo e um valor. Apenas o valor do objeto pode mudar ao longo do
tempo.[71]

Se duas variáveis se referem a objetos imutáveis de igual valor ( a == b is True ), na prática, dificilmente importa se
elas se referem a cópias de mesmo valor ou são apelidos do mesmo objeto, porque o valor de objeto imutável não
muda, com uma exceção. A exceção são as coleções imutáveis, como as tuplas: se uma coleção imutável contém
referências para itens mutáveis, então seu valor pode de fato mudar quando o valor de um item mutável for
modificado. Na prática, esse cenário não é tão comum. O que nunca muda numa coleção imutável são as identidades
dos objetos mantidos ali. A classe frozenset não sofre desse problema, porque ela só pode manter elementos
hashable, e o valor de um objeto hashable não pode mudar nunca, por definição.

O fato de variáveis manterem referências tem muitas consequências práticas para a programação em Python:

Uma atribuição simples não cria cópias.


Uma atribuição composta com += ou *= cria novos objetos se a variável à esquerda da atribuição estiver
vinculada a um objeto imutável, mas pode modificar um objeto mutável diretamente.
Atribuir um novo valor a uma variável existente não muda o objeto previamente vinculado à variável. Isso se
chama reassociar (rebinding); a variável está agora associada a um objeto diferente. Se aquela variável era a última
referência ao objeto anterior, aquele objeto será eliminado pela coleta de lixo.

Parâmetros de função são passados como apelidos, o que significa que a função pode alterar qualquer objeto
mutável recebido como argumento. Não há como evitar isso, exceto criando cópias locais ou usando objetos
imutáveis (i.e., passando uma tupla em vez de uma lista)
Usar objetos mutáveis como valores default de parâmetros de função é perigoso, pois se os parâmetros forem
modificados pela função, o default muda, afetando todas as chamadas posteriores que usem o default.

Em CPython, os objetos são descartados assim que o número de referências a eles chega a zero. Eles também podem ser
descartados se formarem grupos com referências cíclicas sem nenhuma referência externa ao grupo.

Em algumas situações, pode ser útil manter uma referência para um objeto que não irá — por si só — manter o objeto
vivo. Um exemplo é uma classe que queira manter o registro de todas as suas instâncias atuais. Isso pode ser feito com
referências fracas, um mecanismo de baixo nível encontrado nas úteis coleções WeakValueDictionary ,
WeakKeyDictionary , WeakSet , e na função finalize do módulo weakref .

Para saber mais, leia "Weak References" em fluentpython.com (https://fpy.li/weakref).

6.9. Para saber mais


O capítulo "Modelo de Dados" (https://docs.python.org/pt-br/3/reference/datamodel.html) de A Referência da Linguagem Python
inicia com uma explicação bastante clara sobre identidades e valores de objetos.

Wesley Chun, autor da série Core Python, apresentou Understanding Python’s Memory Model, Mutability, and Methods
(https://fpy.li/6-8) (EN) na EuroPython 2011, discutindo não apenas o tema desse capítulo como também o uso de métodos
especiais.

Doug Hellmann escreveu os posts "copy – Duplicate Objects" (https://fpy.li/6-9) (EN) e "weakref—Garbage-Collectable
References to Objects" (https://fpy.li/6-10) (EN), cobrindo alguns dos tópicos que acabamos de tratar.
Você pode encontrar mais informações sobre o coletor de lixo geracional do CPython em the gc — Interface para o
coletor de lixo¶ (https://docs.python.org/pt-br/3/library/gc.html), que começa com a frase "Este módulo fornece uma interface
para o opcional garbage collector". O adjetivo "opcional" usado aqui pode ser surpreendente, mas o capítulo "Modelo
de Dados" (https://docs.python.org/pt-br/3/reference/datamodel.html) também afirma:

“ Uma implementação tem permissão para adiar a coleta de lixo ou omiti-la completamente — é
uma questão de detalhe de implementação como a coleta de lixo é implementada, desde que
nenhum objeto que ainda esteja acessível seja coletado.

Pablo Galindo escreveu um texto mais aprofundado sobre o Coletor de Lixo em Python, em "Design of CPython’s
Garbage Collector" (https://fpy.li/6-12) (EN) no Python Developer’s Guide (https://fpy.li/6-13), voltado para contribuidores
novos e experientes da implementação CPython.

O coletor de lixo do CPython 3.4 aperfeiçoou o tratamento de objetos contendo um método __del__ , como descrito em
PEP 442—​Safe object finalization (https://fpy.li/6-14) (EN).

A Wikipedia tem um artigo sobre string interning (https://fpy.li/6-15) (EN), que menciona o uso desta técnica em várias
linguagens, incluindo Python.

A Wikipedia também tem um artigo sobre "Haddocks' Eyes" (https://fpy.li/6-16), a canção de Lewis Carroll que mencionei
no início deste capítulo. Os editores da Wikipedia escreveram que a letra é usada em trabalhos de lógica e filosofia
"para elaborar o status simbólico do conceito de 'nome': um nome como um marcador de identificação pode ser
atribuído a qualquer coisa, incluindo outro nome, introduzindo assim níveis diferentes de simbolização."

Ponto de vista
Tratamento igual para todos os objetos

Eu aprendi Java antes de conhecer Python. O operador == em Java nunca me pareceu funcionar corretamente. É
muito mais comum que programadores estejam preocupados com a igualdade que com a identidade. Mas para
objetos (não tipos primitivos), o == em Java compara referências, não valores dos objetos. Mesmo para algo tão
básico quanto comparar strings, Java obriga você a usar o método .equals . E mesmo assim, há outro problema:
se você escrever a.equals(b) e a for null , você causa uma null pointer exception (exceção de ponteiro nulo).
Os projetistas do Java sentiram necessidade de sobrecarregar + para strings; por que não mantiveram essa ideia
e sobrecarregaram == também?

Python faz melhor. O operador == compara valores de objetos; is compara referências. E como Python permite
sobrecarregar operadores, == funciona de forma sensata com todos os objetos na biblioteca padrão, incluindo
None , que é um objeto verdadeiro, ao contrário do Null de Java.

E claro, você pode definir __eq__ nas suas próprias classes para controlar o que == significa para suas
instâncias. Se você não sobrecarregar __eq__ , o método herdado de object compara os IDs dos objetos, então a
regra básica é que cada instância de uma classe definida pelo usuário é considerada diferente.

Estas são algumas das coisas que me fizeram mudar de Java para Python assim que terminei de ler The Python
Tutorial em uma tarde de setembro de 1998.

Mutabilidade
Este capítulo não seria necessário se todos os objetos em Python fossem imutáveis. Quando você está lidando com
objetos imutáveis, não faz diferença se as variáveis guardam os objetos em si ou referências para objetos
compartilhados.

Se a == b é verdade, e nenhum dos dois objetos pode mudar, eles podem perfeitamente ser o mesmo objeto. Por
isso a internalização de strings é segura. A identidade dos objetos ser torna importante apenas quando esses
objetos podem mudar.

Em programação funcional "pura", todos os dados são imutáveis: concatenar algo a uma coleção, na verdade, cria
uma nova coleção. Elixir é uma linguagem funcional prática e fácil de aprender, na qual todos os tipos nativos são
imutáveis, incluindo as listas.

Python, por outro lado, não é uma linguagem funcional, menos uma ainda uma linguagem pura. Instâncias de
classes definidas pelo usuário são mutáveis por default em Python — como na maioria das linguagens orientadas
a objetos. Ao criar seus próprios objetos, você tem que tomar o cuidado adicional de torná-los imutáveis, se este
for um requisito. Cada atributo do objeto precisa ser também imutável, senão você termina criando algo como
uma tupla: imutável quanto ao ID do objeto, mas seu valor pode mudar se a tupla contiver um objeto mutável.

Objetos mutáveis também são a razão pela qual programar com threads é tão difícil: threads modificando objetos
sem uma sincronização apropriada podem corromper dados. Sincronização excessiva, por outro lado, causa
deadlocks. A linguagem e a plataforma Erlang — que inclui Elixir — foi projetada para maximizar o tempo de
execução em aplicações distribuídas de alta concorrência, tais como aplicações de controle de telecomunicações.
Naturalmente, eles escolheram tornar os dados imutáveis por default.

Destruição de objetos e coleta de lixo

Não há qualquer mecanismo em Python para destruir um objeto diretamente, e essa omissão é, na verdade, uma
grande qualidade: se você pudesse destruir um objeto a qualquer momento, o que aconteceria com as referências
que apontam para ele?

A coleta de lixo em CPython é feita principalmente por contagem de referências, que é fácil de implementar, mas
vulnerável a vazamentos de memória (memory leaks) quando existem referências cíclicas. Assim, com a versão
2.0 (de outubro de 2000), um coletor de lixo geracional foi implementado, e ele consegue dispor de objetos
inatingíveis que foram mantidos vivos por ciclos de referências.

Mas a contagem de referências ainda está lá como mecanismo básico, e ela causa a destruição imediata de
objetos com zero referências. Isso significa que, em CPython — pelo menos por hora — é seguro escrever:

PYTHON3
open('test.txt', 'wt', encoding='utf-8').write('1, 2, 3')

Este código é seguro porque a contagem de referências do objeto file será zero após o método write retornar.
Entretanto, a mesma linha não é segura em Jython ou IronPython, que usam o coletor de lixo dos runtimes de
seus ambientes (a Java VM e a .NET CLR, respectivamente), que são mais sofisticados, mas não se baseiam em
contagem de referências, e podem demorar mais para destruir o objeto e fechar o arquivo. Em todos os casos,
incluindo em CPython, a melhor prática é fechar o arquivo explicitamente, e a forma mais confiável de fazer isso
é usando o comando with , que garante o fechamento do arquivo mesmo se acontecerem exceções enquanto ele
estiver aberto. Usando with , a linha anterior se torna:

PYTHON3
with open('test.txt', 'wt', encoding='utf-8') as fp:
fp.write('1, 2, 3')
Se você estiver interessado no assunto de coletores de lixo, você talvez queira ler o artigo de Thomas Perl,
"Python Garbage Collector Implementations: CPython, PyPy and GaS" (https://fpy.li/6-17) (EN), onde eu aprendi esses
detalhes sobre a segurança de open().write() em CPython.

Passagem de parâmetros: chamada por compartilhamento

Uma maneira popular de explicar como a passagem de parâmetros funciona em Python é a frase: "Parâmetros
são passados por valor, mas os valores são referências." Isso não está errado, mas causa confusão porque os
modos mais comuns de passagem de parâmetros nas linguagens antigas são chamada por valor (a função recebe
uma cópia dos argumentos) e chamada por referência (a função recebe um ponteiro para o argumento). Em
Python, a função recebe uma cópia dos argumentos, mas os argumentos são sempre referências. Então o valor
dos objetos referenciados podem ser alterados pela função, se eles forem mutáveis, mas sua identidade não. Além
disso, como a função recebe uma cópia da referência em um argumento, reassociar essa referência no corpo da
função não tem qualquer efeito fora da função. Adotei o termo chamada por compartilhamento depois de ler
sobre esse assunto em Programming Language Pragmatics, 3rd ed., de Michael L. Scott (Morgan Kaufmann),
section "8.3.1: Parameter Modes."
Parte II: Funções como objetos
7. Funções como objetos de primeira classe
“ Nunca achei que o Python tenha sido fortemente influenciado por linguagens funcionais,
independente do que outros digam ou pensem. Eu estava muito mais familiarizado com
linguagens imperativas, como o C e o Algol e, apesar de ter tornado as funções objetos de
primeira classe, não via o Python como uma linguagem funcional.[72][73]
— Guido van Rossum
BDFL do Python

No Python, funções são objetos de primeira classe. Estudiosos de linguagens de programação definem um "objeto de
primeira classe" como uma entidade programática que pode ser:

Criada durante a execução de um programa


Atribuída a uma variável ou a um elemento em uma estrutura de dados
Passada como argumento para uma função
Devolvida como o resultado de uma função

Inteiros, strings e dicionários são outros exemplos de objetos de primeira classe no Python—nada de incomum aqui.
Tratar funções como objetos de primeira classe é um recurso essencial das linguagens funcionais, tais como Clojure,
Elixir e Haskell. Entretanto, funções de primeira classe são tão úteis que foram adotadas por linguagens muito
populares, como o Javascript, o Go e o Java (desde o JDK 8), nenhuma das quais alega ser uma "linguagem funcional".

Esse capítulo e quase toda a Parte III do livro exploram as aplicações práticas de se tratar funções como objetos.

O termo "funções de primeira classe" é largamente usado como uma forma abreviada de "funções
👉 DICA como objetos de primeira classe". Ele não é ideal, por sugerir a existência de uma "elite" entre
funções. No Python, todas as funções são de primeira classe.

7.1. Novidades nesse capítulo


A seção Seção 7.5 se chamava "Sete sabores de objetos invocáveis" na primeira edição deste livro. Os novos invocáveis
são corrotinas nativas e geradores assíncronos, introduzidos no Python 3.5 e 3.6, respectivamente. Ambos serão
estudados no Capítulo 21, mas são mencionados aqui, ao lado dos outros invocáveis.

A seção Seção 7.7.1 é nova, e fala de um recurso que surgiu no Python 3.8.

Transferi a discussão sobre acesso a anotações de funções durante a execução para a seção Seção 15.5. Quando escrevi
a primeira edição, a PEP 484—Type Hints (Dicas de Tipo) (https://fpy.li/pep484) (EN) ainda estava sendo considerada, e as
anotações eram usadas de várias formas diferentes. Desde o Python 3.5, anotações precisam estar em conformidade
com a PEP 484. Assim, o melhor lugar para falar delas é durante a discussão das dicas de tipo.

A primeira edição desse livro continha seções sobre a introspecção de objetos função, que desciam a

✒️ NOTA detalhes de baixo nível e distraiam o leitor do assunto principal do capítulo. Fundi aquelas seções
em um post entitulado "Introspection of Function Parameters" (Introspecção de Parâmetros de
Funções) (https://fpy.li/7-2), no fluentpython.com.

Agora vamos ver porque as funções do Python são objetos completos.


7.2. Tratando uma função como um objeto
A sessão de console no Exemplo 1 mostra que funções do Python são objetos. Ali criamos uma função, a chamamos,
lemos seu atributo __doc__ e verificamos que o próprio objeto função é uma instância da classe function .

Exemplo 1. Cria e testa uma função, e então lê seu __doc__ e verifica seu tipo

PYCON
>>> def factorial(n): (1)
... """returns n!"""
... return 1 if n < 2 else n * factorial(n - 1)
...
>>> factorial(42)
1405006117752879898543142606244511569936384000000000
>>> factorial.__doc__ (2)
'returns n!'
>>> type(factorial) (3)
<class 'function'>

1. Isso é uma sessão do console, então estamos criando uma função "durante a execução".
2. __doc__ é um dos muitos atributos de objetos função.
3. factorial é um instância da classe function .

O atributo __doc__ é usado para gerar o texto de ajuda de um objeto. No console do Python, o comando
help(factorial) mostrará uma tela como a da Figura 1.

Figura 1. Tela de ajuda para factorial ; o texto é criado a partir do atributo __doc__ da função.

O Exemplo 2 mostra a natureza de "primeira classe" de um objeto função. Podemos atribuir tal objeto a uma variável
fact e invocá-lo por esse nome. Podemos também passar factorial como argumento para a função map
(https://docs.python.org/pt-br/3/library/functions.html#map). Invocar map(function, iterable) devolve um iterável no qual
cada item é o resultado de uma chamada ao primeiro argumento (uma função) com elementos sucessivos do segundo
argumento (um iterável), range(11) no exemplo.

Exemplo 2. Usa factorial usando de um nome diferentes, e passa factorial como um argumento
PYCON
>>> fact = factorial
>>> fact
<function factorial at 0x...>
>>> fact(5)
120
>>> map(factorial, range(11))
<map object at 0x...>
>>> list(map(factorial, range(11)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

Ter funções de primeira classe permite programar em um estilo funcional. Um dos marcos da programação funcional
(https://pt.wikipedia.org/wiki/Programa%C3%A7%C3%A3o_funcional) é o uso de funções de ordem superior, nosso próximo
tópico.

7.3. Funções de ordem superior


Uma função que recebe uma função como argumento ou devolve uma função como resultado é uma função de ordem
superior. Uma dessas funções é map , usada no Exemplo 2. Outra é a função embutida sorted : o argumento opcional
key permite fornecer uma função, que será então aplicada na ordenação de cada item, como vimos na seção Seção 2.9.
Por exemplo, para ordenar uma lista de palavras por tamanho, passe a função len como key , como no Exemplo 3.

Exemplo 3. Ordenando uma lista de palavras por tamanho

PYCON
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
>>>

Qualquer função com um argumento pode ser usada como chave. Por exemplo, para criar um dicionário de rimas pode
ser útil ordenar cada palavra escrita ao contrário. No Exemplo 4, observe que as palavras na lista não são modificadas
de forma alguma; apenas suas versões escritas na ordem inversa são utilizadas como critério de ordenação. Por isso as
berries aparecem juntas.

Exemplo 4. Ordenando uma lista de palavras pela ordem inversa de escrita

PYCON
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>

No paradigma funcional de programação, algumas das funções de ordem superior mais conhecidas são map , filter ,
reduce , e apply . A função apply foi descontinuada no Python 2.3 e removida no Python 3, por não ser mais
necessária. Se você precisar chamar uma função com um conjuntos dinâmico de argumentos, pode escrever
fn(*args, **kwargs) no lugar de apply(fn, args, kwargs) .

As funções de ordem superior map , filter , e reduce ainda estão por aí, mas temos alternativas melhores para a
maioria de seus casos de uso, como mostra a próxima seção.

7.3.1. Substitutos modernos para map, filter, e reduce


Linguagens funcionais normalmente oferecem as funções de ordem superior map , filter , and reduce (algumas
vezes com nomes diferentes). As funções map e filter ainda estão embutidas no Python mas, desde a introdução das
compreensões de lista e das expressões geradoras, não são mais tão importantes. Uma listcomp ou uma genexp fazem o
mesmo que map e filter combinadas, e são mais legíveis. Considere o Exemplo 5.

Exemplo 5. Listas de fatoriais produzidas com map e filter , comparadas com alternativas escritas com
compreensões de lista

PYCON
>>> list(map(factorial, range(6))) (1)
[1, 1, 2, 6, 24, 120]
>>> [factorial(n) for n in range(6)] (2)
[1, 1, 2, 6, 24, 120]
>>> list(map(factorial, filter(lambda n: n % 2, range(6)))) (3)
[1, 6, 120]
>>> [factorial(n) for n in range(6) if n % 2] (4)
[1, 6, 120]
>>>

1. Cria uma lista de fatoriais de 0! a 5!.


2. Mesma operação, com uma compreensão de lista.
3. Lista de fatoriais de números ímpares até 5!, usando map e filter .

4. A compreensão de lista realiza a mesma tarefa, substituindo map e filter , e tornando lambda desnecessário.

No Python 3, map e filter devolvem geradores—uma forma de iterador—então sua substituta direta é agora uma
expressão geradora (no Python 2, essas funções devolviam listas, então sua alternativa mais próxima era a
compreensão de lista).

A função reduce foi rebaixada de função embutida, no Python 2, para o módulo functools no Python 3. Seu caso de
uso mais comum, a soma, é melhor servido pela função embutida sum , disponível desde que o Python 2.3 (lançado em
2003). E isso é uma enorme vitória em termos de legibilidade e desempenho (veja Exemplo 6 abaixo).

Exemplo 6. Soma de inteiros até 99, realizada com reduce e sum

PYCON
>>> from functools import reduce (1)
>>> from operator import add (2)
>>> reduce(add, range(100)) (3)
4950
>>> sum(range(100)) (4)
4950
>>>

1. A partir do Python 3.0, reduce deixou de ser uma função embutida.


2. Importa add para evitar a criação de uma função apenas para somar dois números.
3. Soma os inteiros até 99.
4. Mesma operação, com sum —não é preciso importar nem chamar reduce e add .

✒️ NOTA A ideia comum de sum e reduce é aplicar alguma operação sucessivamente a itens em uma série,
acumulando os resultados anteriores, reduzindo assim uma série de valores a um único valor.
Outras funções de redução embutidas são all e any :

all(iterable)

Devolve True se não há nenhum elemento falso no iterável; all([]) devolve True .

any(iterable)

Devolve True se qualquer elemento do iterable for verdadeiro; any([]) devolve False .

Dou um explicação mais completa sobre reduce na seção Seção 12.7, onde um exemplo mais longo, atravessando
várias seções, cria um contexto significativo para o uso dessa função. As funções de redução serão resumidas mais à
frente no livro, na seção Seção 17.10, quando estivermos tratando dos iteráveis. Para usar uma função de ordem
superior, às vezes é conveniente criar um pequena função, que será usada apenas uma vez. As funções anônimas
existem para isso. Vamos falar delas a seguir.

7.4. Funções anônimas


A palavra reservada lambda cria uma função anônima dentro de uma expressão Python.

Entretanto, a sintaxe simples do Python força os corpos de funções lambda a serem expressões puras. Em outras
palavras, o corpo não pode conter outras instruções Python como while , try , etc. A atribuição com = também é uma
instrução, então não pode ocorrer em um lambda . A nova sintaxe da expressão de atribuição, usando := , pode ser
usada. Porém, se você precisar dela, seu lambda provavelmente é muito complicado e difícil de ler, e deveria ser
refatorado para um função regular usando def .

O melhor uso das funções anônimas é no contexto de uma lista de argumentos para uma função de ordem superior.
Por exemplo, o Exemplo 7 é o exemplo do dicionário de rimas do Exemplo 4 reescrito com lambda , sem definir uma
função reverse .

Exemplo 7. Ordenando uma lista de palavras escritas na ordem inversa usando lambda

PYCON
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>

Fora do contexto limitado dos argumentos das funções de ordem superior, funções anônimas raramente são úteis no
Python. As restrições sintáticas tendem a tornar ilegíveis ou intratáveis as lambdas não-triviais. Se uma lambda é
difícil de ler, aconselho fortemente seguir o comselho de Fredrik Lundh sobre refatoração.

A receita de Fredrik Lundh para refatoração de lambdas


Se você encontrar um trecho de código difícil de entender por causa de uma lambda , Fredrik Lundh sugere o
seguinte procedimento de refatoração:

1. Escreva um comentário explicando o que diabos aquela lambda faz.


2. Estude o comentário por algum tempo, e pense em um nome que traduza sua essência.
3. Converta a lambda para uma declaração def , usando aquele nome.

4. Remova o comentário.
Esse passos são uma citação do "Programação Funcional—COMO FAZER)
(https://docs.python.org/pt-br/3/howto/functional.html), uma leitura obrigatória.

A sintaxe lambda é apenas açúcar sintático: uma expressão lambda cria um objeto função, exatamente como a
declaração def . Esse é apenas um dos vários tipos de objetos invocáveis no Python. Na próxima seção revisamos todos
eles.

7.5. Os nove sabores de objetos invocáveis


O operador de invocação () pode ser aplicado a outros objetos além de funções. Para determinar se um objeto é
invocável, use a função embutida callable() . No Python 3.9, a documentação do modelo de dados
(https://docs.python.org/pt-br/3/reference/datamodel.html#the-standard-type-hierarchy) lista nove tipos invocáveis:

Funções definidas pelo usuário


Criadas com comandos def ou expressões lambda .

Funções embutidas
Uma funções implementadas em C (no CPython), como len ou time.strftime .

Métodos embutidos
Métodos implementados em C, como dict.get .

Métodos
Funções definidas no corpo de uma classe.

Classes
Quando invocada, uma classe executa seu método __new__ para criar uma instância, e a seguir __init__ , para
inicializá-la. Então a instância é devolvida ao usuário. Como não existe um operador new no Python, invocar uma
classe é como invocar uma função.[74]

Instâncias de classe
Se uma classe define um método __call__ , suas instâncias podem então ser invocadas como funções—esse é o
assunto da próxima seção.

Funções geradoras
Funções ou métodos que usam a palavra reservada yield em seu corpo. Quando chamadas, devolvem um objeto
gerador.

Funções de corrotinas nativas


Funções ou métodos definidos com async def . Quando chamados, devolvem um objeto corrotina. Introduzidas no
Python 3.5.

Funções geradoras assíncronas


Funções ou métodos definidos com async def , contendo yield em seu corpo. Quando chamados, devolvem um
gerador assíncrono para ser usado com async for . Introduzidas no Python 3.6.

Funções geradoras, funções de corrotinas nativas e geradoras assíncronas são diferentes de outros invocáveis: os
valores devolvidos tais funções nunca são dados da aplicação, mas objetos que exigem processamento adicional, seja
para produzir dados da aplicação, seja para realizar algum trabalho útil. Funções geradoras devolvem iteradores.
Ambos são tratados no Capítulo 17. Funções de corrotinas nativas e funções geradoras assíncronas devolvem objetos
que só funcionam com a ajuda de uma framework de programação assíncrona, tal como asyncio. Elas são o assunto do
Capítulo 21.
Dada a variedade dos tipos de invocáveis existentes no Python, a forma mais segura de determinar
se um objeto é invocável é usando a função embutida callable() :

👉 DICA >>> abs, str, 'Ni!'


(<built-in function abs>, <class 'str'>, 'Ni!')
>>> [callable(obj) for obj in (abs, str, 'Ni!')]
[True, True, False]

Vamos agora criar instâncias de classes que funcionam como objetos invocáveis.

7.6. Tipos invocáveis definidos pelo usuário


Não só as funções Python são objetos reais, também é possível fazer com que objetos Python arbitrários se comportem
como funções. Para isso basta implementar o método de instância __call__ .

O Exemplo 8 implementa uma classe BingoCage . Uma instância é criada a partir de qualquer iterável, e mantém uma
list interna de itens, em ordem aleatória. Invocar a instância extrai um item.[75]

Exemplo 8. bingocall.py: Uma BingoCage faz apenas uma coisa: escolhe itens de uma lista embaralhada

PY
import random

class BingoCage:

def __init__(self, items):


self._items = list(items) # (1)
random.shuffle(self._items) # (2)

def pick(self): # (3)


try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage') # (4)

def __call__(self): # (5)


return self.pick()

1. __init__ aceita qualquer iterável; criar uma cópia local evita efeitos colaterais inesperados sobre qualquer list
passada como argumento.
2. shuffle sempre vai funcionar, pois self._items é uma list .

3. O método principal.
4. Se self._items está vazia, gera uma exceção com uma mensagem apropriada.
5. Atalho para bingo.pick() : bingo() .

Aqui está uma demonstração simples do Exemplo 8. Observe como uma instância de bingo pode ser invocada como
uma função, e como a função embutida callable() a reconhece como um objeto invocável:
PY
>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True

Uma classe que implemente __call__ é uma forma fácil de criar objetos similares a funções, com algum estado
interno que precisa ser mantido de uma invocação para outra, como os itens restantes na BingoCage . Outro bom caso
de uso para __call__ é a implementação de decoradores. Decoradores devem ser invocáveis, e muitas vezes é
conveniente "lembrar" algo entre chamadas ao decorador (por exemplo, para memoization—a manutenção dos
resultados de algum processamento complexo e/ou demorado para uso posterior) ou para separar uma implementação
complexa por diferentes métodos.

A abordagem funcional para a criação de funções com estado interno é através do uso de clausuras (closures).
Clausuras e decoradores são o assunto do Capítulo 9.

Vamos agora explorar a poderosa sintaxe oferecida pelo Python para declarar parâmetros de funções, e para passar
argumentos para elas.

7.7. De parâmetros posicionais a parâmetros somente nomeados


Um dos melhores recursos das funções Python é seu mecanismo extremamente flexível de tratamento de parâmetros.
Intimamente relacionados a isso são os usos de * e ** para desempacotar iteráveis e mapeamentos em argumentos
separados quando chamamos uma função. Para ver esses recursos em ação, observe o código do Exemplo 9 e os testes
mostrando seu uso no Exemplo 10.

Exemplo 9. tag gera elementos HTML; um argumento somente nomeado class_ é usado para passar atributos
"class"; o _ é necessário porque class é uma palavra reservada no Python

PY
def tag(name, *content, class_=None, **attrs):
"""Generate one or more HTML tags"""
if class_ is not None:
attrs['class'] = class_
attr_pairs = (f' {attr}="{value}"' for attr, value
in sorted(attrs.items()))
attr_str = ''.join(attr_pairs)
if content:
elements = (f'<{name}{attr_str}>{c}</{name}>'
for c in content)
return '\n'.join(elements)
else:
return f'<{name}{attr_str} />'

A função tag pode ser invocada de muitas formas, como demonstra o Exemplo 10.

Exemplo 10. Algumas das muitas formas de invocar a função tag do Exemplo 9
PY
>>> tag('br') # (1)
'<br />'
>>> tag('p', 'hello') # (2)
'<p>hello</p>'
>>> print(tag('p', 'hello', 'world'))
<p>hello</p>
<p>world</p>
>>> tag('p', 'hello', id=33) # (3)
'<p id="33">hello</p>'
>>> print(tag('p', 'hello', 'world', class_='sidebar')) # (4)
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
>>> tag(content='testing', name="img") # (5)
'<img content="testing" />'
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'class': 'framed'}
>>> tag(**my_tag) # (6)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'

1. Um argumento posicional único produz uma tag vazia com aquele nome.
2. Quaisquer argumentos após o primeiro serão capturados por *content na forma de uma tuple .

3. Argumentos nomeados que não são mencionados explicitamente na assinatura de tag são capturados por
**attrs como um dict .

4. O parâmetro class_ só pode ser passado como um argumento nomeado.


5. O primeiro argumento posicional também pode ser passado como argumento nomeado.
6. Prefixar o dict my_tag com ** passa todos os seus itens como argumentos separados, que são então vinculados
aos parâmetros nomeados, com o restante sendo capturado por **attrs . Nesse caso podemos ter um nome
'class' no dict de argumentos, porque ele é uma string, e não colide com a palavra reservada class .

Argumentos somente nomeados são um recurso do Python 3. No Exemplo 9, o parâmetro class_ só pode ser passado
como um argumento nomeado—ele nunca captura argumentos posicionais não-nomeados. Para especificar
argumentos somente nomeados ao definir uma função, eles devem ser nomeados após o argumento prefixado por * .
Se você não quer incluir argumentos posicionais variáveis, mas ainda assim deseja incluir argumentos somente
nomeados, coloque um * sozinho na assinatura, assim:

PYCON
>>> def f(a, *, b):
... return a, b
...
>>> f(1, b=2)
(1, 2)
>>> f(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given

Observe que argumentos somente nomeados não precisam ter um valor default: eles podem ser obrigatórios, como o
b no exemplo acima.

7.7.1. Parâmetros somente posicionais


Desde o Python 3.8, assinaturas de funções definidas pelo usuário podem especificar parâmetros somente posicionais.
Esse recurso sempre existiu para funções embutidas, tal como divmod(a, b) , que só pode ser chamada com
parâmetros posicionais, e não na forma divmod(a=10, b=4) .
Para definir uma função que requer parâmetros somente posicionais, use / na lista de parâmetros.

Esse exemplo, de "O que há de novo no Python 3.8"


(https://docs.python.org/pt-br/3/whatsnew/3.8.html#positional-only-parameters), mostra como emular a função embutida
divmod :

PYTHON
def divmod(a, b, /):
return (a // b, a % b)

Todos os argumentos à esquerda da / são somente posicionais. Após a / , você pode especificar outros argumentos,
que funcionam como da forma usual.

⚠️ AVISO Uma / na lista de parâmetros é um erro de sintaxe no Python 3.7 ou anteriores.

Por exemplo, considere a função tag do Exemplo 9. Se quisermos que o parâmetro name seja somente posicional,
podemos acrescentar uma / após aquele parâmetro na assinatura da função, assim:

PYTHON
def tag(name, /, *content, class_=None, **attrs):
...

Você pode encontrar outros exemplos de parâmetros somente posicionais no já citado "O que há de novo no Python
3.8" (https://docs.python.org/pt-br/3/whatsnew/3.8.html#positional-only-parameters) e na PEP 570 (https://fpy.li/pep570).

Após esse mergulho nos recursos flexíveis de declaração de argumentos no Python, o resto desse capítulo trata dos
pacotes da biblioteca padrão mais úteis para programar em um estilo funcional.

7.8. Pacotes para programação funcional


Apesar de Guido deixar claro que não projetou o Python para ser uma linguagem de programação funcional, o estilo de
programação funcional pode ser amplamente utilizado, graças a funções de primeira classe, pattern matching e o
suporte de pacotes como operator e functools , dos quais falaremos nas próximas duas seções..

7.8.1. O módulo operator


Na programação funcional, é muitas vezes conveniente usar um operador aritmético como uma função. Por exemplo,
suponha que você queira multiplicar uma sequência de números para calcular fatoriais, mas sem usar recursão. Para
calcular a soma, podemos usar sum , mas não há uma função equivalente para multiplicação. Você poderia usar reduce
—como vimos na seção Seção 7.3.1—mas isso exige um função para multiplicar dois itens da sequência. O Exemplo 11
mostra como resolver esse problema usando lambda .

Exemplo 11. Fatorial implementado com `reduce`e uma função anônima

PYTHON3
from functools import reduce

def factorial(n):
return reduce(lambda a, b: a*b, range(1, n+1))

O módulo operator oferece funções equivalentes a dezenas de operadores, para você não precisar escrever funções
triviais como lambda a, b: a*b . Com ele, podemos reescrever o Exemplo 11 como o Exemplo 12.

Exemplo 12. Fatorial implementado com reduce e operator.mul


PYTHON3
from functools import reduce
from operator import mul

def factorial(n):
return reduce(mul, range(1, n+1))

Outro grupo de "lambdas de um só truque" que operator substitui são funções para extrair itens de sequências ou
para ler atributos de objetos: itemgetter e attrgetter são fábricas que criam funções personalizadas para fazer
exatamente isso.

O Exemplo 13 mostra um uso frequente de itemgetter : ordenar uma lista de tuplas pelo valor de um campo. No
exemplo, as cidades são exibidas por ordem de código de país (campo 1). Essencialmente, itemgetter(1) cria uma
função que, dada uma coleção, devolve o item no índice 1. Isso é mais fácil de escrever e ler que lambda fields:
fields[1] , que faz a mesma coisa.

Exemplo 13. Demonstração de itemgetter para ordenar uma lista de tuplas (mesmos dados do Exemplo 8)

PYCON
>>> metro_data = [
... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
... ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
... ]
>>>
>>> from operator import itemgetter
>>> for city in sorted(metro_data, key=itemgetter(1)):
... print(city)
...
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))

Se você passar múltiplos argumentos de indice para itemgetter , a função criada por ela vai devolver tuplas com os
valores extraídos, algo que pode ser útil para ordenar usando chaves múltiplas:

PYCON
>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
... print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'São Paulo')
>>>

Como itemgetter usa o operador [] , ela suporta não apenas sequências, mas também mapeamentos e qualquer
classe que implemente __getitem__ .

Uma irmã de itemgetter é attrgetter , que cria funções para extrair atributos por nome. Se você passar os nomes
de vários atributos como argumentos para attrgetter , ela vai devolver um tupla de valores. Além disso, se o nome
de qualquer argumento contiver um . (ponto), attrgetter navegará por objetos aninhados para encontrar o
atributo. Esses comportamento são apresentados no Exemplo 14. Não é exatamente uma sessão de console curta, pois
precisamos criar uma estrutura aninhada para demonstrar o tratamento de atributos com . por attrgetter .
Exemplo 14. Demonstração de attrgetter para processar uma lista previamente definida de namedtuple chamada
metro_data (a mesma lista que aparece no Exemplo 13)

PYCON
>>> from collections import namedtuple
>>> LatLon = namedtuple('LatLon', 'lat lon') # (1)
>>> Metropolis = namedtuple('Metropolis', 'name cc pop coord') # (2)
>>> metro_areas = [Metropolis(name, cc, pop, LatLon(lat, lon)) # (3)
... for name, cc, pop, (lat, lon) in metro_data]
>>> metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLon(lat=35.689722,
lon=139.691667))
>>> metro_areas[0].coord.lat # (4)
35.689722
>>> from operator import attrgetter
>>> name_lat = attrgetter('name', 'coord.lat') # (5)
>>>
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')): # (6)
... print(name_lat(city)) # (7)
...
('São Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)

1. Usa namedtuple para definir LatLon .

2. Também define Metropolis .

3. Cria a lista metro_areas com instâncias de Metropolis ; observe o desempacotamento da tupla aninhada para
extrair (lat, lon) e usá-los para criar o LatLon do atributo coord de Metropolis .
4. Obtém a latitude de dentro de metro_areas[0] .
5. Define um attrgetter para obter name e o atributo aninhado coord.lat .

6. Usa attrgetter novamente para ordenar uma lista de cidades pela latitude.
7. Usa o attrgetter definido em 5 para exibir apenas o nome e a latitude da cidade.

Abaixo está uma lista parcial das funções definidas em operator (nomes iniciando com _ foram omitidos por serem,
em sua maioria, detalhes de implementação):

PYCON
>>> [name for name in dir(operator) if not name.startswith('_')]
['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains',
'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt',
'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imatmul',
'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior',
'ipow', 'irshift', 'is_', 'is_not', 'isub', 'itemgetter',
'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul',
'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos',
'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor']

A maior parte dos 54 nomes listados é auto-evidente. O grupo de nomes formados por um i inicial e o nome de outro
operador—por exemplo iadd , iand , etc—correspondem aos operadores de atribuição aumentada—por exemplo, += ,
&= , etc. Essas funções mudam seu primeiro argumento no mesmo lugar, se o argumento for mutável; se não,
funcionam como seus pares sem o prefixo i : simplemente devolvem o resultado da operação.
Das funções restantes de operator , methodcaller será a última que veremos. Ela é algo similar a attrgetter e
itemgetter , no sentido de criarem uma função durante a execução. A função criada invoca por nome um método do
objeto passado como argumento, como mostra o Exemplo 15.

Exemplo 15. Demonstração de methodcaller : o segundo teste mostra a vinculação de argumentos adicionais

PYCON
>>> from operator import methodcaller
>>> s = 'The time has come'
>>> upcase = methodcaller('upper')
>>> upcase(s)
'THE TIME HAS COME'
>>> hyphenate = methodcaller('replace', ' ', '-')
>>> hyphenate(s)
'The-time-has-come'

O primeiro teste no Exemplo 15 está ali apenas para mostrar o funcionamento de methodcaller ; se você precisa usar
str.upper como uma função, basta chamá-lo na classe str , passando uma string como argumento, assim:

PYCON
>>> str.upper(s)
'THE TIME HAS COME'

O segundo teste do Exemplo 15 mostra que methodcaller pode também executar uma aplicação parcial para fixar
alguns argumentos, como faz a função functools.partial . Esse é nosso próximo tópico.

7.8.2. Fixando argumentos com functools.partial


O módulo functools oferece várias funções de ordem superior. Já vimos reduce na seção Seção 7.3.1. Uma outra é
partial : dado um invocável, ela produz um novo invocável com alguns dos argumentos do invocável original
vinculados a valores pré-determinados. Isso é útil para adaptar uma função que recebe um ou mais argumentos a uma
API que requer uma função de callback com menos argumentos. O Exemplo 16 é uma demonstração trivial.

Exemplo 16. Empregando partial para usar uma função com dois argumentos onde é necessário um invocável com
apenas um argumento

PYCON
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) (1)
>>> triple(7) (2)
21
>>> list(map(triple, range(1, 10))) (3)
[3, 6, 9, 12, 15, 18, 21, 24, 27]

1. Cria uma nova função triple a partir de mul , vinculando o primeiro argumento posicional a 3 .

2. Testa a função.
3. Usa triple com map ; mul não funcionaria com map nesse exemplo.

Um exemplo mais útil envolve a função unicode.normalize , que vimos na seção Seção 4.7. Se você trabalha com
texto em muitas línguas diferentes, pode querer aplicar unicode.normalize('NFC', s) a qualquer string s , antes de
compará-la ou armazená-la. Se você precisa disso com frequência, é conveninete ter uma função nfc para executar
essa tarefa, como no Exemplo 17.

Exemplo 17. Criando uma função conveniente para normalizar Unicode com partial
PYCON
>>> import unicodedata, functools
>>> nfc = functools.partial(unicodedata.normalize, 'NFC')
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> s1 == s2
False
>>> nfc(s1) == nfc(s2)
True

partial recebe um invocável como primeiro argumento, seguido de um número arbitrário de argumentos
posicionais e nomeados para vincular.

O Exemplo 18 mostra o uso de partial com a função tag (do Exemplo 9), para fixar um argumento posicional e um
argumento nomeado.

Exemplo 18. Demonstração de partial aplicada à função tag , do Exemplo 9

PYCON
>>> from tagger import tag
>>> tag
<function tag at 0x10206d1e0> (1)
>>> from functools import partial
>>> picture = partial(tag, 'img', class_='pic-frame') (2)
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />' (3)
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', class_='pic-frame') (4)
>>> picture.func (5)
<function tag at 0x10206d1e0>
>>> picture.args
('img',)
>>> picture.keywords
{'class_': 'pic-frame'}

1. Importa tag do Exemplo 9 e mostra seu ID.


2. Cria a função picture a partir de tag , fixando o primeiro argumento posicional em 'img' e o argumento
nomeado class_ em 'pic-frame' .
3. picture funciona como esperado.
4. partial() devolve um objeto functools.partial .[76]

5. Um objeto functools.partial tem atributos que fornecem acesso à função original e aos argumentos fixados.

A função functools.partialmethod faz o mesmo que partial , mas foi projetada para trabalhar com métodos.

O módulo functools também inclui funções de ordem superior para serem usadas como decoradores de função, tais
como cache e singledispatch , entre outras. Essas funções são tratadas no Capítulo 9, que também explica como
implementar decoradores personalizados.

7.9. Resumo do capítulo


O objetivo deste capítulo foi explorar a natureza das funções como objetos de primeira classe no Python. As principais
consequências disso são a possibilidade de atribuir funções a variáveis, passá-las para outras funções, armazená-las em
estruturas de dados e acessar os atributos de funções, permitindo que frameworks e ferramentas usem essas
informações.
Funções de ordem superior, parte importante da programação funcional, são comuns no Python. As funções embutidas
sorted , min e max , além de functools.partial , são exemplos de funções de ordem superior muito usadas na
linguagem. O uso de map , filter e reduce já não é tão frequente como costumava ser, graças às compreensões de
lista (e estruturas similares, como as expressões geradoras) e à adição de funções embutidas de redução como sum ,
all e any .

Desde o Python 3.6, existem nove sabores de invocáveis, de funções simples criadas com lambda a instâncias de
classes que implementam __call__ . Geradoras e corrotinas também são invocáveis, mas seu comportamento é muito
diferente daquele de outros invocáveis. Todos os invocáveis podem ser detectados pela função embutida callable() .
Invocáveis oferecem uma rica sintaxe para declaração de parâmetros formais, incluindo parâmetros nomeados,
parâmetros somente posicionais e anotações.

Por fim, vimos algumas funções do módulo operator e functools.partial , que facilitam a programação funcional,
minimizando a necessidade de uso da sintaxe funcionalmente inepta de lambda .

7.10. Leitura complementar


Nos próximos capítulos, continuaremos nossa jornada pela programação com objetos função. O Capítulo 8 é dedicado
às dicas de tipo nos parâmetros de função e nos valores devolvidos por elas. O Capítulo 9 mergulha nos decoradores de
função—um tipo especial de função de ordem superior—e no mescanismo de clausura (closure) que os faz funcionar. O
Capítulo 10 mostra como as funções de primeira classe podem simplificar alguns padrões clássicos de projetos (design
patterns) orientados a objetos.

Em A Referência da Linguagem Python, a seção "3.2. A hierarquia de tipos padrão"


(https://docs.python.org/pt-br/3/reference/datamodel.html#the-standard-type-hierarchy) mostra os noves tipos invocáveis,
juntamente com todos os outros tipos embutidos.

O capítulo 7 do Python Cookbook (https://fpy.li/pycook3) (EN), 3ª ed. (O’Reilly), de David Beazley e Brian K. Jones, é um
excelente complemento a esse capítulo, bem como ao Capítulo 9, tratando basicamente dos mesmos conceitos, mas com
uma abordagem diferente.

Veja a PEP 3102—​Keyword-Only Arguments (Argumentos somente nomeados) (https://fpy.li/pep3102) (EN) se você estiver
interessada na justificativa e nos casos desse recurso.

Uma ótima introdução à programação funcional em Python é o "Programação Funcional COMO FAZER"
(https://docs.python.org/pt-br/3/howto/functional.html), de A. M. Kuchling. O principal foco daquele texto, entretanto, é o uso
de iteradores e geradoras, assunto do Capítulo 17.

A questão no StackOverflow, "Python: Why is functools.partial necessary?" (Python: Por que functools.partial é
necessária?) (https://fpy.li/7-12) (EN), tem uma resposta muito informativa (e engraçada) escrita por Alex Martelli, co-autor
do clássico Python in a Nutshell (O’Reilly).

Refletindo sobre a pergunta "Seria o Python uma linguagem funcional?", criei uma de minhas palestras favoritas,
"Beyond Paradigms" ("Para Além dos Paradigmas"), que apresentei na PyCaribbean, na PyBay e na PyConDE. Veja os
slides (https://fpy.li/7-13) (EN) e o vídeo (https://fpy.li/7-14) (EN) da apresentação em Berlim—onde conheci Miroslav Šedivý e
Jürgen Gmach, dois dos revisores técnicos desse livro.

Ponto de vista
O Python é uma linguagem funcional?
Em algum momento do ano 2000, eu estava participando de uma oficna de Zope na Zope Corporation, nos EUA,
quando Guido van Rossum entrou na sala (ele não era o instrutor). Na seção de perguntas e respostas que se
seguiu, alguém perguntou quais recursos do Python ele tinha trazido de outras linguagens. A resposta de Guido:
"Tudo que é bom no Python foi roubado de outras linguagens."

Shriram Krishnamurthi, professor de Ciência da Computação na Brown University, inicia seu artigo, "Teaching
Programming Languages in a Post-Linnaean Age" (Ensinando Linguagens de Programação em uma Era Pós-
Taxonomia-de-Lineu) (https://fpy.li/7-15) (EN), assim:

“ Osuma"paradigmas" de linguagens de programação são um legado moribundo e tedioso de


era passada. Os atuais projetistas de linguagens não tem qualquer respeito por eles,
então por que nossos cursos aderem servilmente a tais "paradigmas"?

Nesse artigo, o Python é mencionado nominalmente na seguinte passagem:

“ Ecomcomoas sutilezas
descrever linguagens como Python, Ruby, ou Perl? Seus criadores não tem paciência
dessas nomenclaturas de Lineu; eles pegam emprestados todos os recursos
que desejam, criando misturas que desafiam totalmente uma caracterização.

Krishnamurthi argumenta que, ao invés de tentar classificar as linguagens com alguma taxonomia, seria mais
útil olhar para elas como agregados de recursos. Suas ideias inspiraram minha palestra "Beyond Paradigms"
("Para Além dos Paradigmas"), mencionada no final da seção Seção 7.10.

Mesmo se esse não fosse o objetivo de Guido, dotar o Python de funções de primeira classe abriu as portas para a
programação funcional. Em seu post, "Origins of Python’s 'Functional' Features" (_As Origens dos Recursos
'Funcionais' dp Python) (https://fpy.li/7-1) (EN), ele afirma que map , filter , e reduce foram a primeira motivação
para a inclusão do lambda ao Python. Todos esses recursos foram adicionados juntos ao Python 1.0 em 1994, por
Amrit Prem , de acordo com o Misc/HISTORY (https://fpy.li/7-17) (EN) no código-fonte do CPython.

Funções como map , filter , e reduce surgiram inicialmente no Lisp, a linguagem funcional original. O Lisp,
entretanto, não limita o que pode ser feito dentro de uma lambda , pois tudo em List é uma expressão. O Python
usa uma sintaxe orientada a comandos, na qual as expressões não podem conter comandos, e muitas das
estruturas da linguagem são comandos—​incluindo`try/catch`, que é o que eu mais sinto falta quando escrevo uma
lambda . É o preço a pagar pela sintaxe extremamente legível do Python.[77] O Lisp tem muitas virtudes, mas
legibilidade não é uma delas.

Ironicamente, roubar a sintaxe de compreensão de lista de outra linguagem funcional—Haskell—reduziu


significativamente a necessidade de usar map e filter , e também lambda .

Além da sintaxe limitada das funções anônimas, o maior obstáculo para uma adoção mais ampla de idiomas de
programação funcional no Python é a ausência da eliminação de chamadas de cauda, uma otimização que
permite o processamento, de forma eficiente em termos de memória, de uma função que faz uma chamada
recursiva na "cauda" de seu corpo. Em outro post de blog, "Tail Recursion Elimination" (Eliminação de Recursão de
Cauda) (https://fpy.li/7-18) (EN), Guido apresenta várias razões pelas quais tal otimização não é adequada ao Python.
O post é uma ótima leitura por seus argumentos técnicos, mas mais ainda pelas primeiras três e mais importantes
razões dadas serem questões de usabilidade. O Python não é gostoso de usar, aprender e ensinar por acidente.
Guido o fez assim.

Então cá estamos: o Python não é, por projeto, uma linguagem funcional—seja lá o quê isso signifique. O Python
só pega emprestadas algumas boas ideias de linguagens funcionais.
O problema das funções anônimas

Além das restrições sintáticas específicas do Python, funções anônimas tem uma séria desvantagem em qualquer
linguagem: elas não tem nome.

Estou brincando, mas não muito. Os stack traces são mais fáceis de ler quando as funções tem nome. Funções
anônimas são um atalho conveniente, nos divertimos programando com elas, mas algumas vezes elas são levadas
longe demais—especialmente se a linguagem e o ambiente encorajam o aninhamento profundo de funções
anônimas, com faz o Javascript combinado com o Node.js. Ter muitas funções anônimas aninhadas torna a
depuração e o tratamento de erros mais difíceis. A programação assíncrona no Python é mais estruturada, talvez
pela sintaxe limitada do lambda impedir seu abuso e forçar uma abordagem mais explícita. Promessas, futuros e
diferidos são conceitos usados nas APIs assíncronas modernas. Prometo escrever mais sobre programação
assíncrona no futuro, mas esse assunto será diferido[78] até o Capítulo 21.
8. Dicas de tipo em funções
É preciso enfatizar que Python continuará sendo uma linguagem de tipagem dinâmica, e os autores não tem
qualquer intenção de algum dia tornar dicas de tipo obrigatórias, mesmo que por mera convenção.

Guido van Rossum, Jukka Lehtosalo, e Łukasz Langa, PEP 484—Type Hints PEP 484—Type Hints (https://fpy.li/8-1)
(EN), "Rationale and Goals"; negritos mantidos do original.

Dicas de tipo foram a maior mudança na história do Python desde a unificação de tipos e classes (https://fpy.li/descr101) no
Python 2.2, lançado em 2001. Entretanto, as dicas de tipo não beneficiam igualmente a todos as pessoas que usam
Python. Por isso deverão ser sempre opcionais.

A PEP 484—Type Hints (https://fpy.li/pep484) introduziu a sintaxe e a semântica para declarações explícitas de tipo em
argumentos de funções, valores de retorno e variáveis. O objetivo é ajudar ferramentas de desenvolvimento a
encontrarem bugs nas bases de código em Python através de análise estática, isto é, sem precisar efetivamente
executar o código através de testes.

Os maiores beneficiários são engenheiros de software profissionais que usam IDEs (Ambientes de Desenvolvimento
Integrados) e CI (Integração Contínua). A análise de custo-benefício que torna as dicas de tipo atrativas para esse grupo
não se aplica a todos os usuários de Python.

A base de usuários de Python vai muito além dessa classe de profissionais. Ela inclui cientistas, comerciantes,
jornalistas, artistas, inventores, analistas e estudantes de inúmeras áreas — entre outros. Para a maioria deles, o custo
de aprender dicas de tipo será certamente maior — a menos que já conheçam uma outra linguagem com tipos estáticos,
subtipos e tipos genéricos. Os benefícios serão menores para muitos desses usuários, dada a forma como que eles
interagem com Python, o tamanho menor de suas bases de código e de suas equipes — muitas vezes "equipes de um".

A tipagem dinâmica, default do Python, é mais simples e mais expressiva quando estamos escrevendo programas para
explorar dados e ideias, como é o caso em ciência de dados, computação criativa e para aprender.

Este capítulo se concentra nas dicas de tipo de Python nas assinaturas de função. Capítulo 15 explora as dicas de tipo no
contexto de classes e outros recursos do módulo typing .

Os tópicos mais importantes aqui são:

Uma introdução prática à tipagem gradual com Mypy


As perspectivas complementares da duck typing (tipagem pato) e da tipagem nominal
A revisão para principais categorias de tipos que podem surgir em anotações — isso representa cerca de 60% do
capítulo
Os parâmetros variádicos das dicas de tipo ( args , *kwargs )

As limitações e desvantagens das dicas de tipo e da tipagem estática.

8.1. Novidades nesse capítulo


Este capítulo é completamente novo. As dicas de tipo apareceram no Python 3.5, após eu ter terminado de escrever a
primeira edição de Python Fluente.

Dadas as limitações de um sistema de tipagem estática, a melhor ideia da PEP 484 foi propor um sistema de tipagem
gradual. Vamos começar definindo o que isso significa.
8.2. Sobre tipagem gradual
A PEP 484 introduziu no Python um sistema de tipagem gradual. Outras linguagens com sistemas de tipagem gradual
são o Typescript da Microsoft, Dart (a linguagem do SDK Flutter, criado pelo Google), e o Hack (um dialeto de PHP
criado para uso na máquina virtual HHVM do Facebook). O próprio verificador de tipo MyPy começou como uma
linguagem: um dialeto de Python de tipagem gradual com seu próprio interpretador. Guido van Rossum convenceu o
criador do MyPy, Jukka Lehtosalo, a transformá-lo em uma ferramenta para checar código Python anotado.

Eis uma função com anotações de tipos:

PY
def tokenize(s: str) -> list[str]:
"Convert a string into a list of tokens."
return s.replace('(', ' ( ').replace(')', ' ) ').split()

A assinatura informa que a função tokenize recebe uma str e devolve list[str] : uma lista de strings. A utilidade
dessa função será explicada no Exemplo 13.

Um sistema de tipagem gradual:

É opcional
Por default, o verificador de tipo não deve emitir avisos para código que não tenha dicas de tipo. Em vez disso, o
verificador supõe o tipo Any quando não consegue determinar o tipo de um objeto. O tipo Any é considerado
compatível com todos os outros tipos.

Não captura erros de tipagem durante a execução do código


Dicas de tipo são usadas por verificadores de tipo, analisadores de código-fonte (linters) e IDEs para emitir avisos.
Eles não evitam que valores inconsistentes sejam passados para funções ou atribuídos a variáveis durante a
execução. Por exemplo, nada impede que alguém chame tokenie(42) , apesar da anotação de tipo do argumento
s: str ). A chamada ocorrerá, e teremos um erro de execução no corpo da função.

Não melhora o desempenho


Anotações de tipo fornecem dados que poderiam, em tese, permitir otimizações do bytecode gerado. Mas, até julho
de 2021, tais otimizações não ocorrem em nenhum ambiente Python que eu conheça.[79]

O melhor aspecto de usabilidade da tipagem gradual é que as anotações são sempre opcionais.

Nos sistemas de tipagem estáticos, a maioria das restrições de tipo são fáceis de expressar, muitas são desajeitadas,
muitas são difíceis e algumas são impossíveis: Por exemplo, em julho de 2021, tipos recursivos não tinham suporte —
veja as questões #182, Define a JSON type (https://fpy.li/8-2) (EN) sobre o JSON e #731, Support recursive types
(https://fpy.li/8-3) (EN) do MyPy.

É perfeitamente possível que você escreva um ótimo programa Python, que consiga passar por uma boa cobertura de
testes, mas ainda assim não consiga acrescentar dicas de tipo que satisfaçam um verificador de tipagem. Não tem
problema; esqueça as dicas de tipo problemáticas e entregue o programa!

Dicas de tipo são opcionais em todos os níveis: você pode criar ou usar pacotes inteiros sem dicas de tipo, pode silenciar
o verificador ao importar um daqueles pacotes sem dicas de tipo para um módulo onde você use dicas de tipo, e você
também pode adicionar comentários especiais, para fazer o verificador de tipos ignorar linhas específicas do seu
código.
Tentar impor uma cobertura de 100% de dicas de tipo irá provavelmente estimular seu uso de forma
impensada, apenas para satisfazer essa métrica. Isso também vai impedir equipes de aproveitarem
👉 DICA da melhor forma possível o potencial e a flexibilidade do Python. Código sem dicas de tipo deveria
ser aceito sem objeções quando anotações tornassem o uso de uma API menos amigável ou quando
complicassem em demasia seu desenvolvimento.

8.3. Tipagem gradual na prática


Vamos ver como a tipagem gradual funciona na prática, começando com uma função simples e acrescentando
gradativamente a ela dicas de tipo, guiados pelo Mypy.

Há muitos verificadores de tipo para Python compatíveis com a PEP 484, incluindo o pytype
(https://fpy.li/8-4) do Google, o Pyright (https://fpy.li/8-5) da Microsoft, o Pyre (https://fpy.li/8-6) do Facebook
— além de verificadores incluídos em IDEs como o PyCharm. Eu escolhi usar o Mypy
✒️ NOTA (https://fpy.li/mypy) nos exemplos por ele ser o mais conhecido. Entretanto, algum daqueles outros
pode ser mais adequado para alguns projetos ou equipes. O Pytype, por exemplo, foi projetado para
lidar com bases de código sem nenhuma dica de tipo e ainda assim gerar recomendações úteis. Ele é
mais tolerante que o MyPy, e consegue também gerar anotações para o seu código.

Vamos anotar uma função show_count , que retorna uma string com um número e uma palavra no singular ou no
plural, dependendo do número:

PYCON
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no birds'

Exemplo 19 mostra o código-fonte de show_count , sem anotações.

Exemplo 19. show_count de messages.py sem dicas de tipo.

PY
def show_count(count, word):
if count == 1:
return f'1 {word}'
count_str = str(count) if count else 'no'
return f'{count_str} {word}s'

8.3.1. Usando o Mypy


Para começar a verificação de tipo, rodamos o comando mypy passando o módulo messages.py como parâmetro:

…/no_hints/ $ pip install mypy


[muitas mensagens omitidas...]
…/no_hints/ $ mypy messages.py
Success: no issues found in 1 source file

Na configuração default, o Mypy não encontra nenhum problema com o Exemplo 19.
Durante a revisão deste capítulo estou usando Mypy 0.910, a versão mais recente no momento (em
julho de 2021). A "Introduction" (https://fpy.li/8-7) (EN) do Mypy adverte que ele "é oficialmente
⚠️ AVISO software beta. Mudanças ocasionais irão quebrar a compatibilidade com versões mais antigas." O
Mypy está gerando pelo menos um relatório diferente daquele que recebi quando escrevi o capítulo,
em abril de 2020. E quando você estiver lendo essas linhas, talvez os resultados também sejam
diferentes daqueles mostrados aqui.

Se a assinatura de uma função não tem anotações, Mypy a ignora por default — a menos que seja configurado de outra
forma.

O Exemplo 20 também inclui testes de unidade do pytest . Este é código de messages_test.py.

Exemplo 20. messages_test.py sem dicas de tipo.

PY
from pytest import mark

from messages import show_count

@mark.parametrize('qty, expected', [
(1, '1 part'),
(2, '2 parts'),
])
def test_show_count(qty, expected):
got = show_count(qty, 'part')
assert got == expected

def test_show_count_zero():
got = show_count(0, 'part')
assert got == 'no parts'

Agora vamos acrescentar dicas de tipo, guiados pelo Mypy.

8.3.2. Tornando o Mypy mais rigoroso


A opção de linha de comando --disallow-untyped-defs faz o Mypy apontar todas as definições de função que não
tenham dicas de tipo para todos os argumentos e para o valor de retorno.

Usando --disallow-untyped-defs com o arquivo de teste produz três erros e uma observação:

…/no_hints/ $ mypy --disallow-untyped-defs messages_test.py


messages.py:14: error: Function is missing a type annotation
messages_test.py:10: error: Function is missing a type annotation
messages_test.py:15: error: Function is missing a return type annotation
messages_test.py:15: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files (checked 1 source file)

Nas primeiras etapas da tipagem gradual, prefiro usar outra opção:

--disallow-incomplete-defs .

Inicialmente o Mypy não me dá nenhuma nova informação:

…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py


Success: no issues found in 1 source file
Agora vou acrescentar apenas o tipo do retorno a show_count em messages.py:

def show_count(count, word) -> str:

Isso é suficiente para fazer o Mypy olhar para o código. Usando a mesma linha de comando anterior para verificar
messages_test.py fará o Mypy examinar novamente o messages.py:

…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py


messages.py:14: error: Function is missing a type annotation
for one or more arguments
Found 1 error in 1 file (checked 1 source file)

Agora posso gradualmente acrescentar dicas de tipo, função por função, sem receber avisos sobre as funções onde
ainda não adicionei anotações Essa é uma assinatura completamente anotada que satisfaz o Mypy:

PY
def show_count(count: int, word: str) -> str:

Em vez de digitar opções de linha de comando como --disallow-incomplete-defs , você pode


salvar sua configuração favorita da forma descrita na página Mypy configuration file (https://fpy.li/8-8)
(EN) na documentação do Mypy. Você pode incluir configurações globais e configurações específicas
para cada módulo. Aqui está um mypy.ini simples, para servir de base:
👉 DICA
[mypy]
python_version = 3.9
warn_unused_configs = True
disallow_incomplete_defs = True

8.3.3. Um valor default para um argumento


A função show_count no Exemplo 19 só funciona com substantivos regulares. Se o plural não pode ser composto
acrescentando um 's' , devemos deixar o usuário fornecer a forma plural, assim:

PYCON
>>> show_count(3, 'mouse', 'mice')
'3 mice'

Vamos experimentar um pouco de "desenvolvimento orientado a tipos." Primeiro acrescento um teste usando aquele
terceiro argumento. Não esqueça de adicionar a dica do tipo de retorno à função de teste, senão o Mypy não vai
inspecioná-la.

PY3
def test_irregular() -> None:
got = show_count(2, 'child', 'children')
assert got == '2 children'

O Mypy detecta o erro:

PY
…/hints_2/ $ mypy messages_test.py
messages_test.py:22: error: Too many arguments for "show_count"
Found 1 error in 1 file (checked 1 source file)

Então edito show_count , acrescentando o argumento opcional plural no Exemplo 21.


Exemplo 21. showcount de hints_2/messages.py com um argumento opcional

PY
def show_count(count: int, singular: str, plural: str = '') -> str:
if count == 1:
return f'1 {singular}'
count_str = str(count) if count else 'no'
if not plural:
plural = singular + 's'
return f'{count_str} {plural}'

E agora o Mypy reporta "Success."

Aqui está um erro de digitação que o Python não reconhece. Você consegue encontrá-lo?

PY
def hex2rgb(color=str) -> tuple[int, int, int]:

O relatório de erros do Mypy não é muito útil:

⚠️ AVISO colors.py:24: error: Function is missing a type


annotation for one or more arguments

A dica de tipo para o argumento color deveria ser color: str . Eu escrevi color=str , que não é
uma anotação: ele determina que o valor default de color é str .

Pela minha experiência, esse é um erro comum e fácil de passar desapercebido, especialmente em
dicas de tipo complexas.

Os seguintes detalhes são considerados um bom estilo para dicas de tipo:

Sem espaço entre o nome do parâmetro e o : ; um espaço após o :

Espaços dos dois lados do = que precede um valor default de parâmetro

Por outro lado, a PEP 8 diz que não deve haver espaço em torno de = se não há nenhuma dica de tipo para aquele
parâmetro específico.

Estilo de Código: use flake8 e blue


Em vez de decorar essas regrinhas bobas, use ferramentas como flake8 (https://fpy.li/8-9) e blue (https://fpy.li/8-10). O
flake8 informa sobre o estilo do código e várias outras questões, enquanto o blue reescreve o código-fonte com
base na (maioria) das regras prescritas pela ferramenta de formatação de código black (https://fpy.li/8-11).

Se o objetivo é impor um estilo de programação "padrão", blue é melhor que black, porque segue o estilo próprio
do Python, de usar aspas simples por default e aspas duplas como alternativa.

PY
>>> "I prefer single quotes"
'I prefer single quotes'

No CPython, a preferência por aspas simples está incorporada no repr() , entre outros lugares. O módulo doctest
(https://fpy.li/doctest) depende do repr() usar aspas simples por default.
Um dos autores do blue é Barry Warsaw (https://fpy.li/8-12), co-autor da PEP 8, core developer do Python desde 1994
e membro do Python’s Steering Council desde 2019. Daí estamos em ótima companhia quando escolhemos usar
aspas simples.

Se você precisar mesmo usar o black, use a opção black -S . Isso deixará suas aspas intocadas.

8.3.4. Usando None como default


No Exemplo 21, o parâmetro plural está anotado como str , e o valor default é '' . Assim não há conflito de tipo.

Eu gosto dessa solução, mas em outros contextos None é um default melhor. Se o parâmetro opcional requer um tipo
mutável, então None é o único default sensato, como vimos na Seção 6.5.1.

Com None como default para o parâmetro plural , a assinatura ficaria assim:

PY
from typing import Optional

def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

Vamos destrinchar essa linha:

Optional[str] significa que plural pode ser uma str ou None .

É obrigatório fornecer explicitamente o valor default = None .

Se você não atribuir um valor default a plural , o runtime do Python vai tratar o parâmetro como obrigatório.
Lembre-se: durante a execução do programa, as dicas de tipo são ignoradas.

Veja que é preciso importar Optional do módulo typing . Quando importamos tipos, é uma boa prática usar a
sintaxe from typing import X , para reduzir o tamanho das assinaturas das funções.

Optional não é um bom nome, pois aquela anotação não torna o argumento opcional. O que o

⚠️ AVISO torna opcional é a atribuição de um valor default ao parâmetro. Optional[str] significa apenas: o
tipo desse parâmetro pode ser str ou NoneType . Nas linguagens Haskell e Elm, um tipo parecido
se chama Maybe .

Agora que tivemos um primeiro contato concreto com a tipagem gradual, vamos examinar o que o conceito de tipo
significa na prática.

8.4. Tipos são definidos pelas operações possíveis


“ Háconjunto
muitas definições do conceito de tipo na literatura. Aqui vamos assumir que tipo é um
de valores e um conjunto de funções que podem ser aplicadas àqueles valores.
— PEP 483—A Teoria das Dicas de Tipo

Na prática, é mais útil considerar o conjunto de operações possíveis como a caraterística definidora de um tipo.[80]

Por exemplo, pensando nas operações possíveis, quais são os tipos válidos para x na função a seguir?

PYTHON
def double(x):
return x * 2
O tipo do parâmetro x pode ser numérico ( int , complex , Fraction , numpy.uint32 , etc.), mas também pode ser
uma sequência ( str , tuple , list , array ), uma numpy.array N-dimensional, ou qualquer outro tipo que
implemente ou herde um método __mul__ que aceite um inteiro como argumento.

Entretanto, considere a anotação double abaixo. Ignore por enquanto a ausência do tipo do retorno, vamos nos
concentrar no tipo do parâmetro:

PYTHON
from collections import abc

def double(x: abc.Sequence):


return x * 2

Um verificador de tipo irá rejeitar esse código. Se você informar ao Mypy que x é do tipo abc.Sequence , ele vai
marcar x * 2 como erro, pois a Sequence ABC (https://fpy.li/8-13) não implementa ou herda o método __mul__ .
Durante a execução, o código vai funcionar com sequências concretas como str , tuple , list , array , etc., bem
como com números, pois durante a execução as dicas de tipo são ignoradas. Mas o verificador de tipo se preocupa
apenas com o que estiver explicitamente declarado, e abc.Sequence não suporta __mul__ .

Por essa razão o título dessa seção é "Tipos São Definidos pelas Operações Possíveis." O runtime do Python aceita
qualquer objeto como argumento x nas duas versões da função double . O cálculo de x * 2 pode funcionar, ou pode
causar um TypeError , se a operação não for suportada por x . Por outro lado, Mypy vai marcar x * 2 como um erro
quando analisar o código-fonte anotado de double , pois é uma operação não suportada pelo tipo declarado x:
abc.Sequence .

Em um sistema de tipagem gradual, acontece uma interação entre duas perspectivas diferentes de tipo:

Duck typing ("tipagem pato")


A perspectiva adotada pelo Smalltalk — a primeira linguagem orientada a objetos — bem como em Python,
JavaScript, e Ruby. Objetos tem tipo, mas variáveis (incluindo parâmetros) não. Na prática, não importa qual o tipo
declarado de um objeto, importam apenas as operações que ele efetivamente suporta. Se eu posso invocar
birdie.quack() então, nesse contexto, birdie é um pato. Por definição, duck typing só é aplicada durante a
execução, quando se tenta aplicar operações sobre os objetos. Isso é mais flexível que a tipagem nominal, ao preço de
permitir mais erros durante a execução.[81]

Tipagem nominal
É a perspectiva adotada em C++, Java, e C#, e suportada em Python anotado. Objetos e variáveis tem tipos. Mas
objetos só existem durante a execução, e o verificador de tipo só se importa com o código-fonte, onde as variáveis
(incluindo parâmetros de função) tem anotações com dicas de tipo. Se Duck é uma subclasse de Bird , você pode
atribuir uma instância de Duck a um parâmetro anotado como birdie: Bird . Mas no corpo da função, o
verificador considera a chamada birdie.quack() ilegal, pois birdie é nominalmente um Bird , e aquela classe
não fornece o método .quack() . Não interessa que o argumento real, durante a execução, é um Duck , porque a
tipagem nominal é aplicada de forma estática. O verificador de tipo não executa qualquer pedaço do programa, ele
apenas lê o código-fonte. Isso é mais rígido que duck typing, com a vantagem de capturar alguns bugs durante o
desenvolvimento, ou mesmo em tempo real, enquanto o código está sendo digitado em um IDE.

O Exemplo 22 é um exemplo bobo que contrapõe duck typing e tipagem nominal, bem como verificação de tipo estática
e comportamento durante a execução.[82]

Exemplo 22. birds.py


PY
class Bird:
pass

class Duck(Bird): # (1)


def quack(self):
print('Quack!')

def alert(birdie): # (2)


birdie.quack()

def alert_duck(birdie: Duck) -> None: # (3)


birdie.quack()

def alert_bird(birdie: Bird) -> None: # (4)


birdie.quack()

1. Duck é uma subclasse de Bird .

2. alert não tem dicas de tipo, então o verificador a ignora.


3. alert_duck aceita um argumento do tipo Duck .

4. alert_bird aceita um argumento do tipo Bird .

Verificando birds.py com Mypy, encontramos um problema:

…/birds/ $ mypy birds.py


birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)

Só de analisar o código fonte, Mypy percebe que alert_bird é problemático: a dica de tipo declara o parâmetro
birdie como do tipo Bird , mas o corpo da função chama birdie.quack() — e a classe Bird não tem esse método.

Agora vamos tentar usar o módulo birds em daffy.py no Exemplo 23.

Exemplo 23. daffy.py

PY
from birds import *

daffy = Duck()
alert(daffy) # (1)
alert_duck(daffy) # (2)
alert_bird(daffy) # (3)

1. Chamada válida, pois alert não tem dicas de tipo.


2. Chamada válida, pois alert_duck recebe um argumento do tipo Duck e daffy é um Duck .

3. Chamada válida, pois alert_bird recebe um argumento do tipo Bird , e daffy também é um Bird —a
superclasse de Duck .

Mypy reporta o mesmo erro em daffy.py, sobre a chamada a quack na função alert_bird definida em birds.py:

…/birds/ $ mypy daffy.py


birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)
Mas o Mypy não vê qualquer problema com daffy.py em si: as três chamadas de função estão OK.

Agora, rodando daffy.py, o resultado é o seguinte:

…/birds/ $ python3 daffy.py


Quack!
Quack!
Quack!

Funciona perfeitamente! Viva o duck typing!

Durante a execução do programa, o Python não se importa com os tipos declarados. Ele usa apenas duck typing. O
Mypy apontou um erro em alert_bird , mas a chamada da função com daffy funciona corretamente quando
executada. À primeira vista isso pode surpreender muitos pythonistas: um verificador de tipo estático muitas vezes
encontra erros em código que sabemos que vai funcionar quanto executado.

Entretanto, se daqui a alguns meses você for encarregado de estender o exemplo bobo do pássaro, você agradecerá ao
Mypy. Observe esse módulo woody.py module, que também usa birds , no Exemplo 24.

Exemplo 24. woody.py

PY
from birds import *

woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)

O Mypy encontra dois erros ao verificar woody.py:

…/birds/ $ mypy woody.py


birds.py:16: error: "Bird" has no attribute "quack"
woody.py:5: error: Argument 1 to "alert_duck" has incompatible type "Bird";
expected "Duck"
Found 2 errors in 2 files (checked 1 source file)

O primeiro erro é em birds.py: a chamada a birdie.quack() em alert_bird , que já vimos antes. O segundo erro é
em woody.py: woody é uma instância de Bird , então a chamada alert_duck(woody) é inválida, pois aquela função
exige um Duck. Todo Duck é um Bird , mas nem todo Bird é um Duck .

Durante a execução, nenhuma das duas chamadas em woody.py funcionariam. A sucessão de falhas é melhor ilustrada
em uma sessão no console, através das mensagens de erro, no Exemplo 25.

Exemplo 25. Erros durante a execução e como o Mypy poderia ter ajudado
PY
>>> from birds import *
>>> woody = Bird()
>>> alert(woody) # (1)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_duck(woody) # (2)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_bird(woody) # (3)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'

1. O Mypy não tinha como detectar esse erro, pois não há dicas de tipo em alert .

2. O Mypy avisou do problema: Argument 1 to "alert_duck" has incompatible type "Bird"; expected "Duck"
(Argumento 1 para alert_duck é do tipo incompatível "Bird"; argumento esperado era "Duck")
3. O Mypy está avisando desde o Exemplo 22 que o corpo da função alert_bird está errado: "Bird" has no
attribute "quack" (Bird não tem um atributo "quack")

Este pequeno experimento mostra que o duck typing é mais fácil para o iniciante e mais flexível, mas permite que
operações não suportadas causem erros durante a execução. A tipagem nominal detecta os erros antes da execução,
mas algumas vezes rejeita código que seria executado sem erros - como a chamada a alert_bird(daffy) no Exemplo
23.

Mesmo que funcione algumas vezes, o nome da função alert_bird está incorreto: seu código exige um objeto que
suporte o método .quack() , que não existe em Bird .

Nesse exemplo bobo, as funções tem uma linha apenas. Mas na vida real elas poderiam ser mais longas, e poderiam
passar o argumento birdie para outras funções, e a origem daquele argumento poderia estar a muitas chamadas de
função de distância, tornando difícil localizar a causa do erro durante a execução. O verificador de tipos impede que
muitos erros como esse aconteçam durante a execução de um programa.

O valor das dicas de tipo é questionável em exemplos minúsculo que cabem em um livro. Os
benefícios crescem conforme o tamanho da base de código afetada. É por essa razão que empresas
✒️ NOTA com milhões de linhas de código em Python - como a Dropbox, o Google e o Facebook - investiram
em equipes e ferramentas para promover a adoção global de dicas de tipo internamente, e hoje tem
partes significativas e crescentes de sua base de código checadas para tipo em suas linhas (pipeline)
de integração contínua.

Nessa seção exploramos as relações de tipos e operações no duck typing e na tipagem nominal, começando com a
função simples double() — que deixamos sem dicas de tipo. Agora vamos dar uma olhada nos tipos mais importantes
ao anotar funções.

Vamos ver um bom modo de adicionar dicas de tipo a double() quando examinarmos Seção 8.5.10. Mas antes disso,
há tipos mais importantes para conhecer.
8.5. Tipos próprios para anotações
Quase todos os tipos em Python podem ser usados em dicas de tipo, mas há restrições e recomendações. Além disso, o
módulo typing introduziu constructos especiais com uma semântica às vezes surpreendente.

Essa seção trata de todos os principais tipos que você pode usar em anotações:

typing.Any

Tipos e classes simples


typing.Optional e typing.Union

Coleções genéricas, incluindo tuplas e mapeamentos


Classes base abstratas
Iteradores genéricos
Genéricos parametrizados e TypeVar

typing.Protocols — crucial para duck typing estático


typing.Callable

typing.NoReturn — um bom modo de encerrar essa lista.

Vamos falar de um de cada vez, começando por um tipo que é estranho, aparentemente inútil, mas de uma
importância fundamental.

8.5.1. O tipo Any


A pedra fundamental de qualquer sistema gradual de tipagem é o tipo Any , também conhecido como o tipo dinâmico.
Quando um verificador de tipo vê um função sem tipo como esta:

PYTHON
def double(x):
return x * 2

ele supõe isto:

PYTHON
def double(x: Any) -> Any:
return x * 2

Isso significa que o argumento x e o valor de retorno podem ser de qualquer tipo, inclusive de tipos diferentes.
Assume-se que Any pode suportar qualquer operação possível.

Compare Any com object . Considere essa assinatura:

PYTHON
def double(x: object) -> object:

Essa função também aceita argumentos de todos os tipos, porque todos os tipos são subtipo-de object .

Entretanto, um verificador de tipo vai rejeitar essa função:

PYTHON
def double(x: object) -> object:
return x * 2
O problema é que object não suporta a operação __mul__ . Veja o que diz o Mypy:

…/birds/ $ mypy double_object.py


double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)

Tipos mais gerais tem interfaces mais restritas, isto é, eles suportam menos operações. A classe object implementa
menos operações que abc.Sequence , que implementa menos operações que abc.MutableSequence , que por sua vez
implementa menos operações que list .

Mas Any é um tipo mágico que reside tanto no topo quanto na base da hierarquia de tipos. Ele é simultaneamente o
tipo mais geral - então um argumento n: Any aceita valores de qualquer tipo - e o tipo mais especializado, suportando
assim todas as operações possíveis. Pelo menos é assim que o verificador de tipo entende Any .

Claro, nenhum tipo consegue suportar qualquer operação possível, então usar Any impede o verificador de tipo de
cumprir sua missão primária: detectar operações potencialmente ilegais antes que seu programa falhe e levante uma
exceção durante sua execução.

Subtipo-de versus consistente-com


Sistemas tradicionais de tipagem nominal orientados a objetos se baseiam na relação subtipo-de. Dada uma classe T1 e
uma subclasse T2 , então T2 é subtipo-de T1 .

Observe este código:

PYTHON
class T1:
...

class T2(T1):
...

def f1(p: T1) -> None:


...

o2 = T2()

f1(o2) # OK

A chamada f1(o2) é uma aplicação do Princípio de Substituição de Liskov (Liskov Substitution Principle—LSP).

Barbara Liskov[83] na verdade definiu é subtipo-de em termos das operações suportadas. Se um objeto do tipo T2
substitui um objeto do tipo T1 e o programa continua se comportando de forma correta, então T2 é subtipo-de T1 .

Seguindo com o código visto acima, essa parte mostra uma violação do LSP:

PYTHON
def f2(p: T2) -> None:
...

o1 = T1()

f2(o1) # type error

Do ponto de vista das operações suportadas, faz todo sentido: como uma subclasse, T2 herda e precisa suportar todas
as operações suportadas por T1 . Então uma instância de T2 pode ser usada em qualquer lugar onde se espera uma
instância de T1 . Mas o contrário não é necessariamente verdadeiro: T2 pode implementar métodos adicionais, então
uma instância de T1 não pode ser usada onde se espera uma instância de T2 . Este foco nas operações suportadas se
reflete no nome _behavioral subtyping (subtipagem comportamental) (https://fpy.li/8-15) (EN), também usado para se
referir ao LSP.
Em um sistema de tipagem gradual há outra relação, consistente-com (consistent-with), que se aplica sempre que
subtipo-de puder ser aplicado, com disposições especiais para o tipo Any .

As regras para consistente-com são:

1. Dados T1 e um subtipo T2 , então T2 é consistente-com T1 (substituição de Liskov).


2. Todo tipo é consistente-com Any : você pode passar objetos de qualquer tipo em um argumento declarado como de
tipo `Any.
3. Any é consistente-com todos os tipos: você sempre pode passar um objeto de tipo Any onde um argumento de
outro tipo for esperado.

Considerando as definições anteriores dos objetos o1 e o2 , aqui estão alguns exemplos de código válido, ilustrando as
regras #2 e #3:

PYTHON
def f3(p: Any) -> None:
...

o0 = object()
o1 = T1()
o2 = T2()

f3(o0) #
f3(o1) # tudo certo: regra #2
f3(o2) #

def f4(): # tipo implícito de retorno: `Any`


...

o4 = f4() # tipo inferido: `Any`

f1(o4) #
f2(o4) # tudo certo: regra #3
f3(o4) #

Todo sistema de tipagem gradual precisa de um tipo coringa como Any

O verbo "inferir" é um sinônimo bonito para "adivinhar", quando usado no contexto da análise de
tipos. Verificadores de tipo modernos, em Python e outras linguagens, não precisam de anotações de
👉 DICA tipo em todo lugar porque conseguem inferir o tipo de muitas expressões. Por exemplo, se eu
escrever x = len(s) * 10 , o verificador não precisa de uma declaração local explícita para saber
que x é um int , desde que consiga encontrar dicas de tipo para len em algum lugar.

Agora podemos explorar o restante dos tipos usados em anotações.

8.5.2. Tipos simples e classes


Tipos simples como int , float , str , e bytes podem ser usados diretamente em dicas de tipo. Classes concretas da
biblioteca padrão, de pacotes externos ou definidas pelo usuário — FrenchDeck , Vector2d , e Duck - também podem
ser usadas em dicas de tipo.

Classes base abstratas também são úteis aqui. Voltaremos a elas quando formos estudar os tipos coleção, e em Seção
8.5.7.
Para classes, consistente-com é definido como subtipo_de: uma subclasse é consistente-com todas as suas superclasses.

Entretanto, "a praticidade se sobrepõe à pureza", então há uma exceção importante, discutida em seguida.

int é Consistente-Com complex


Não há nenhuma relação nominal de subtipo entre os tipo nativos int , float e complex : eles são
subclasses diretas de object . Mas a PEP 484 declara (https://fpy.li/cardxvi) que int é consistente-com
👉 DICA float , e float é consistente-com complex . Na prática, faz sentido: int implementa todas as
operações que float implementa, e int implementa operações adicionais também - operações
binárias como & , | , << , etc. O resultado final é o seguinte: int é consistente-com complex . Para
i = 3 , i.real é 3 e i.imag é 0 .

8.5.3. Os tipos Optional e Union


Nós vimos o tipo especial Optional em Seção 8.3.4. Ele resolve o problema de ter None como default, como no
exemplo daquela seção:

PY
from typing import Optional

def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

A sintaxe Optional[str] é na verdade um atalho para Union[str, None] , que significa que o tipo de plural pode
ser str ou None .

Uma sintaxe melhor para Optional e Union em Python 3.10


Desde o Python 3.10 é possível escrever str | bytes em vez de Union[str, bytes] . É menos
digitação, e não há necessidade de importar Optional ou Union de typing . Compare a sintaxe
antiga com a nova para a dica de tipo do parâmetro plural em show_count :

👉 DICA plural: Optional[str] = None # before


PY

plural: str | None = None # after

O operador | também funciona com isinstance e issubclass para declarar o segundo


argumento: isinstance(x, int | str) . Para saber mais, veja PEP 604—Complementary syntax
for Union[] (https://fpy.li/pep604) (EN).

A assinatura da função nativa ord é um exemplo simples de Union - ela aceita str or bytes , e retorna um int :[84]

PY
def ord(c: Union[str, bytes]) -> int: ...

Aqui está um exemplo de uma função que aceita uma str , mas pode retornar uma str ou um float :

PY
from typing import Union

def parse_token(token: str) -> Union[str, float]:


try:
return float(token)
except ValueError:
return token
Se possível, evite criar funções que retornem o tipo Union , pois esse tipo exige um esforço extra do usuário: pois para
saber o que fazer com o valor recebido da função será necessário verificar o tipo daquele valor durante a execução.
Mas a parse_token no código acima é um caso de uso razoável no contexto de interpretador de expressões simples.

Na Seção 4.10, vimos funções que aceitam tanto str quanto bytes como argumento, mas
retornam uma str se o argumento for str ou bytes , se o argumento for bytes . Nesses casos, o
👉 DICA tipo de retorno é determinado pelo tipo da entrada, então Union não é uma solução precisa. Para
anotar tais funções corretamente, precisamos usar um tipo variável - apresentado em Seção 8.5.9 -
ou sobrecarga (overloading), que veremos na Seção 15.2.

Union[] exige pelo menos dois tipos. Tipos Union aninhados tem o mesmo efeito que uma Union "achatada" . Então
esta dica de tipo:

PY
Union[A, B, Union[C, D, E]]

é o mesmo que:

PY
Union[A, B, C, D, E]

Union é mais útil com tipos que não sejam consistentes entre si. Por exemplo: Union[int, float] é redundante, pois
int é consistente-com float . Se você usar apenas float para anotar o parâmetro, ele vai também aceitar valores
int .

8.5.4. Coleções genéricas


A maioria das coleções em Python são heterogêneas.

Por exemplo, você pode inserir qualquer combinação de tipos diferentes em uma list . Entretanto, na prática isso não
é muito útil: se você colocar objetos em uma coleção, você certamente vai querer executar alguma operação com eles
mais tarde, e normalmente isso significa que eles precisam compartilhar pelo menos um método comum.[85]

Tipos genéricos podem ser declarados com parâmetros de tipo, para especificar o tipo de item com o qual eles
conseguem trabalhar.

Por exemplo, uma list pode ser parametrizada para restringir o tipo de elemento ali contido, como se pode ver no
Exemplo 26.

Exemplo 26. tokenize com dicas de tipo para Python ≥ 3.9

PYTHON
def tokenize(text: str) -> list[str]:
return text.upper().split()

Em Python ≥ 3.9, isso significa que tokenize retorna uma list onde todos os elementos são do tipo str .

As anotações stuff: list e stuff: list[Any] significam a mesma coisa: stuff é uma lista de objetos de qualquer
tipo.

👉 DICA Se você estiver usando Python 3.8 ou anterior, o conceito é o mesmo, mas você precisa de mais
código para funcionar - como explicado em Suporte a tipos de coleção descontinuados.
A PEP 585—Type Hinting Generics In Standard Collections (https://fpy.li/8-16) (EN) lista as coleções da biblioteca padrão
que aceitam dicas de tipo genéricas. A lista a seguir mostra apenas as coleções que usam a forma mais simples de dica
de tipo genérica, container[item] :

list collections.deque abc.Sequence abc.MutableSequence


set abc.Container abc.Set abc.MutableSet
frozenset abc.Collection

Os tipos tuple e mapping aceitam dicas de tipo mais complexas, como veremos em suas respectivas seções.

No Python 3.10, não há uma boa maneira de anotar array.array , levando em consideração o argumento typecode
do construtor, que determina se o array contém inteiros ou floats. Um problema ainda mais complicado é verificar a
faixa dos inteiros, para prevenir OverflowError durante a execução, ao se adicionar novos elementos. Por exemplo,
um array com typecode=B só pode receber valores int de 0 a 255. Até o Python 3.11, o sistema de tipagem estática
do Python não consegue lidar com esse desafio.

Suporte a tipos de coleção descontinuados


(Você pode pular esse box se usa apenas Python 3.9 ou posterior.)

Em Python 3.7 e 3.8, você precisa importar um __future__ para fazer a notação [] funcionar com as coleções
nativas, tal como list , como ilustrado no Exemplo 27.

Exemplo 27. tokenize com dicas de tipo para Python ≥ 3.7

PYTHON
from __future__ import annotations

def tokenize(text: str) -> list[str]:


return text.upper().split()

O __future__ não funciona com Python 3.6 ou anterior. O Exemplo 28 mostra como anotar tokenize de uma
forma que funciona com Python ≥ 3.5.

Exemplo 28. tokenize com dicas de tipo para Python ≥ 3.5

PYTHON
from typing import List

def tokenize(text: str) -> List[str]:


return text.upper().split()

Para fornecer um suporte inicial a dicas de tipo genéricas, os autores da PEP 484 criaram dúzias de tipos
genéricos no módulo typing . A Tabela 15 mostra alguns deles. Para a lista completa, consulte a documentação
do módulo typing (https://docs.python.org/pt-br/3/library/typing.html) .

Tabela 15. Alguns tipos de coleção e seus equivalentes nas dicas de tipo
Collection Type hint equivalent

list typing.List

set typing.Set
Collection Type hint equivalent

frozenset typing.FrozenSet

collections.deque typing.Deque

collections.abc.MutableSequence typing.MutableSequence

collections.abc.Sequence typing.Sequence

collections.abc.Set typing.AbstractSet

collections.abc.MutableSet typing.MutableSet

A PEP 585—Type Hinting Generics In Standard Collections (https://fpy.li/pep585) deu início a um processo de vários
anos para melhorar a usabilidade das dicas de tipo genéricas. Podemos resumir esse processo em quatro etapas:

1. Introduzir from future import annotations no Python 3.7 para permitir o uso das classes da biblioteca
padrão como genéricos com a notação list[str] .
2. Tornar aquele comportamento o default a partir do Python 3.9: list[str] agora funciona sem que future
precise ser importado.
3. Descontinuar (deprecate) todos os tipos genéricos do módulo typing .[86] Avisos de descontinuação não serão
emitidos pelo interpretador Python, porque os verificadores de tipo devem sinalizar os tipos descontinuados
quando o programa sendo verificado tiver como alvo Python 3.9 ou posterior.
4. Remover aqueles tipos genéricos redundantes na primeira versão de Python lançada cinco anos após o
Python 3.9. No ritmo atual, esse deverá ser o Python 3.14, também conhecido como Python Pi.

Agora vamos ver como anotar tuplas genéricas.

8.5.5. Tipos tuple


Há três maneiras de anotar os tipos tuple .

Tuplas como registros (records)


Tuplas como registro com campos nomeados
Tuplas como sequências imutáveis.

Tuplas como registros


Se você está usando uma tuple como um registro, use o tipo tuple nativo e declare os tipos dos campos dentro dos
[] .

Por exemplo, a dica de tipo seria tuple[str, float, str] para aceitar uma tupla com nome da cidade, população e
país: ('Shanghai', 24.28, 'China') .

Observe uma função que recebe um par de coordenadas geográficas e retorna uma Geohash (https://fpy.li/8-18), usada
assim:

PYCON
>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'
O Exemplo 29 mostra a definição da função geohash , usando o pacote geolib do PyPI.

Exemplo 29. coordinates.py com a função geohash

PY
from geolib import geohash as gh # type: ignore # (1)

PRECISION = 9

def geohash(lat_lon: tuple[float, float]) -> str: # (2)


return gh.encode(*lat_lon, PRECISION)

1. Esse comentário evita que o Mypy avise que o pacote geolib não tem nenhuma dica de tipo.
2. O parâmetro lat_lon , anotado como uma tuple com dois campos float .

👉 DICA Com Python < 3.9, importe e use typing.Tuple nas dicas de tipo. Este tipo está descontinuado mas
permanecerá na biblioteca padrão pelo menos até 2024.

Tuplas como registros com campos nomeados


Para a anotar uma tupla com muitos campos, ou tipos específicos de tupla que seu código usa com frequência,
recomendo fortemente usar typing.NamedTuple , como visto no Capítulo 5. O Exemplo 30 mostra uma variante de
Exemplo 29 com NamedTuple .

Exemplo 30. coordinates_named.py com NamedTuple , Coordinates e a função geohash

PY
from typing import NamedTuple

from geolib import geohash as gh # type: ignore

PRECISION = 9

class Coordinate(NamedTuple):
lat: float
lon: float

def geohash(lat_lon: Coordinate) -> str:


return gh.encode(*lat_lon, PRECISION)

Como explicado na Seção 5.2, typing.NamedTuple é uma factory de subclasses de tuple , então Coordinate é
consistente-com tuple[float, float] , mas o inverso não é verdadeiro - afinal, Coordinate tem métodos extras
adicionados por NamedTuple , como ._asdict() , e também poderia ter métodos definidos pelo usuário.

Na prática, isso significa que é seguro (do ponto de vista do tipo de argumento) passar uma instância de Coordinate
para a função display , definida assim:

PY
def display(lat_lon: tuple[float, float]) -> str:
lat, lon = lat_lon
ns = 'N' if lat >= 0 else 'S'
ew = 'E' if lon >= 0 else 'W'
return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'

Tuplas como sequências imutáveis


Para anotar tuplas de tamanho desconhecido, usadas como listas imutáveis, você precisa especificar um único tipo,
seguido de uma vírgula e …​(isto é o símbolo de reticências do Python, formado por três pontos, não o caractere
Unicode U+2026 — HORIZONTAL ELLIPSIS ).

Por exemplo, tuple[int, …​


] é uma tupla com itens int .

As reticências indicam que qualquer número de elementos >= 1 é aceitável. Não há como especificar campos de tipos
diferentes para tuplas de tamanho arbitrário.

As anotações stuff: tuple[Any, …​] e stuff: tuple são equivalentes: stuff é uma tupla de tamanho
desconhecido contendo objetos de qualquer tipo.

Aqui temos um função columnize , que transforma uma sequência em uma tabela de colunas e células, na forma de
uma lista de tuplas de tamanho desconhecido. É útil para mostrar os itens em colunas, assim:

PYCON
>>> animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus'.split()
>>> table = columnize(animals)
>>> table
[('drake', 'koala', 'yak'), ('fawn', 'lynx', 'zapus'), ('heron', 'tahr'),
('ibex', 'xerus')]
>>> for row in table:
... print(''.join(f'{word:10}' for word in row))
...
drake koala yak
fawn lynx zapus
heron tahr
ibex xerus

O Exemplo 31 mostra a implementação de columnize . Observe o tipo do retorno:

PY
list[tuple[str, ...]]

Exemplo 31. columnize.py retorna uma lista de tuplas de strings

PY
from collections.abc import Sequence

def columnize(
sequence: Sequence[str], num_columns: int = 0
) -> list[tuple[str, ...]]:
if num_columns == 0:
num_columns = round(len(sequence) ** 0.5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]

8.5.6. Mapeamentos genéricos


Tipos de mapeamento genéricos são anotados como MappingType[KeyType, ValueType] . O tipo nativo dict e os
tipos de mapeamento em collections e collections.abc aceitam essa notação em Python ≥ 3.9. Para versões mais
antigas, você deve usar typing.Dict e outros tipos de mapeamento no módulo typing , como discutimos em Suporte
a tipos de coleção descontinuados.

O Exemplo 32 mostra um uso na prática de uma função que retorna um índice invertido (https://fpy.li/8-19) para permitir
a busca de caracteres Unicode pelo nome — uma variação do Exemplo 21 mais adequada para código server-side
(também chamado back-end), como veremos no Capítulo 21.
Dado o início e o final dos códigos de caractere Unicode, name_index retorna um dict[str, set[str]] , que é um
índice invertido mapeando cada palavra para um conjunto de caracteres que tem aquela palavra em seus nomes. Por
exemplo, após indexar os caracteres ASCII de 32 a 64, aqui estão os conjuntos de caracteres mapeados para as palavras
'SIGN' e 'DIGIT' , e a forma de encontrar o caractere chamado 'DIGIT EIGHT' :

PYCON
>>> index = name_index(32, 65)
>>> index['SIGN']
{'$', '>', '=', '+', '<', '%', '#'}
>>> index['DIGIT']
{'8', '5', '6', '2', '3', '0', '1', '4', '7', '9'}
>>> index['DIGIT'] & index['EIGHT']
{'8'}

O Exemplo 32 mostra o código fonte de charindex.py com a função name_index . Além de uma dica de tipo dict[] ,
este exemplo tem três outros aspectos que estão aparecendo pela primeira vez no livro.

Exemplo 32. charindex.py

PY
import sys
import re
import unicodedata
from collections.abc import Iterator

RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1

def tokenize(text: str) -> Iterator[str]: # (1)


"""return iterable of uppercased words"""
for match in RE_WORD.finditer(text):
yield match.group().upper()

def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
index: dict[str, set[str]] = {} # (2)
for char in (chr(i) for i in range(start, end)):
if name := unicodedata.name(char, ''): # (3)
for word in tokenize(name):
index.setdefault(word, set()).add(char)
return index

1. tokenize é uma função geradora. Capítulo 17 é sobre geradores.


2. A variável local indexestá anotada. Sem a dica, o Mypy diz: Need type annotation for 'index' (hint:
"index: dict[<type>, <type>] = …​ ") .

3. Eu usei o operador morsa (walrus operator) := na condição do if . Ele atribui o resultado da chamada a
unicodedata.name() a name , e a expressão inteira é calculada a partir daquele resultado. Quando o resultado é
'' , isso é falso, e o index não é atualizado.[87]

✒️ NOTA Ao usar dict como um registro, é comum que todas as chaves sejam do tipo
tipos diferentes dependendo das chaves. Isso é tratado na Seção 15.3.
str , com valores de

8.5.7. Classes bases abstratas

“ Seja conservador no que envia, mas liberal no que aceita.


— lei de Postel
ou o Princípio da Robustez
A Tabela 15 apresenta várias classes abstratas de collections.abc . Idealmente, uma função deveria aceitar
argumentos desses tipos abstratos—​ou seus equivalentes de typing antes do Python 3.9—​e não tipos concretos. Isso
dá mais flexibilidade a quem chama a função.

Considere essa assinatura de função:

PY3
from collections.abc import Mapping

def name2hex(name: str, color_map: Mapping[str, int]) -> str:

Usar abc.Mapping permite ao usuário da função fornecer uma instância de dict , defaultdict , ChainMap , uma
subclasse de UserDict subclass, ou qualquer outra classe que seja um subtipo-de Mapping .

Por outro lado, veja essa assinatura:

PY3
def name2hex(name: str, color_map: dict[str, int]) -> str:

Agora color_map tem que ser um dict ou um de seus subtipos, tal como defaultdict ou OrderedDict .
Especificamente, uma subclasse de collections.UserDict não passaria pela verificação de tipo para color_map , a
despeito de ser a maneira recomendada de criar mapeamentos definidos pelo usuário, como vimos na Seção 3.6.5. O
Mypy rejeitaria um UserDict ou uma instância de classe derivada dele, porque UserDict não é uma subclasse de
dict ; eles são irmãos. Ambos são subclasses de abc.MutableMapping .[88]

Assim, em geral é melhor usar abc.Mapping ou abc.MutableMapping em dicas de tipos de parâmetros, em vez de
dict (ou typing.Dict em código antigo). Se a função name2hex não precisar modificar o color_map recebido, a
dica de tipo mais precisa para color_map é abc.Mapping . Desse jeito, quem chama não precisa fornecer um objeto
que implemente métodos como setdefault , pop , e update , que fazem parte da interface de MutableMapping , mas
não de Mapping . Isso reflete a segunda parte da lei de Postel: "[seja] liberal no que aceita."

A lei de Postel também nos diz para sermos conservadores no que enviamos. O valor de retorno de uma função é
sempre um objeto concreto, então a dica de tipo do valor de saída deve ser um tipo concreto, como no exemplo em
Seção 8.5.4 — que usa list[str] :

PYTHON
def tokenize(text: str) -> list[str]:
return text.upper().split()

No verbete de typing.List (https://docs.python.org/pt-br/3/library/typing.html#typing.List) (EN - Tradução abaixo não oficial),


a documentação do Python diz:

“ Versão genérica de . Útil para anotar tipos de retorno. Para anotar argumentos é
list
preferível usar um tipo de coleção abstrata , tal como ou .
Sequence Iterable

Comentários similares aparecem nos verbetes de typing.Dict (https://fpy.li/8-21) e typing.Set (https://fpy.li/8-22).

Lembre-se que a maioria dos ABCs de collections.abc e outras classes concretas de collections , bem como as
coleções nativas, suportam notação de dica de tipo genérica como collections.deque[str] desde o Python 3.9. As
coleções correspondentes em typing só precisavam suportar código escrito em Python 3.8 ou anterior. A lista
completa de classes que se tornaram genéricas aparece em na seção "Implementation" (https://fpy.li/8-16) da PEP 585—
Type Hinting Generics In Standard Collections (https://fpy.li/pep585) (EN).

Para encerrar nossa discussão de ABCs em dicas de tipo, precisamos falar sobre os ABCs numbers .
A queda da torre numérica
O pacote numbers (https://docs.python.org/pt-br/3/library/numbers.html) define a assim chamada torre numérica (numeric
tower) descrita na PEP 3141—A Type Hierarchy for Numbers (https://fpy.li/pep3141) (EN). A torre é uma hierarquia linear
de ABCs, com Number no topo:

Number

Complex

Real

Rational

Integral

Esses ABCs funcionam perfeitamente para checagem de tipo durante a execução, mas eles não são suportados para
checagem de tipo estática. A seção "Numeric Tower" (https://fpy.li/cardxvi) da PEP 484 rejeita os ABCs numbers e manda
tratar os tipo nativos complex , float , e int como casos especiais, como explicado em int é Consistente-Com
complex. Vamos voltar a essa questão na Seção 13.6.8, em Capítulo 13, que é dedicada a comparar protocolos e ABCs

Na prática, se você quiser anotar argumentos numéricos para checagem de tipo estática, existem algumas opções:

1. Usar um dos tipo concretos, int , float , ou complex — como recomendado pela PEP 488.
2. Declarar um tipo union como Union[float, Decimal, Fraction] .

3. Se você quiser evitar a codificação explícita de tipos concretos, usar protocolos numéricos como SupportsFloat ,
tratados na Seção 13.6.2.

A seção Seção 8.5.10 abaixo é um pré-requisito para entender protocolos numéricos.

Antes disso, vamos examinar um dos ABCs mais úteis para dicas de tipo: Iterable .

8.5.8. Iterable
A documentação de typing.List (https://docs.python.org/pt-br/3/library/typing.html#typing.List) que eu citei acima
recomenda Sequence e Iterable para dicas de tipo de parâmetros de função.

Esse é um exemplo de argumento Iterable , na função math.fsum da biblioteca padrão:

PYTHON
def fsum(__seq: Iterable[float]) -> float:

Arquivos Stub e o Projeto Typeshed


Até o Python 3.10, a biblioteca padrão não tem anotações, mas o Mypy, o PyCharm, etc, conseguem
encontrar as dicas de tipo necessárias no projeto Typeshed (https://fpy.li/8-26), na forma de arquivos
stub: arquivos de código-fonte especiais, com uma extensão .pyi, que contém assinaturas anotadas de
👉 DICA métodos e funções, sem a implementação - muito parecidos com headers em C.

A assinatura para math.fsum está em /stdlib/2and3/math.pyi (https://fpy.li/8-27). Os sublinhados


iniciais em __seq são uma convenção estabelecida na PEP 484 para parâmetros apenas posicionais,
como explicado em Seção 8.6.

O Exemplo 33 é outro exemplo do uso de um parâmetro Iterable , que produz itens que são tuple[str, str] . A
função é usada assim:
PYCON
>>> l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
>>> text = 'mad skilled noob powned leet'
>>> from replacer import zip_replace
>>> zip_replace(text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'

O Exemplo 33 mostra a implementação.

Exemplo 33. replacer.py

PY
from collections.abc import Iterable

FromTo = tuple[str, str] # (1)

def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # (2)


for from_, to in changes:
text = text.replace(from_, to)
return text

1. FromTo é um apelido de tipo: eu atribui tuple[str, str] a FromTo , para tornar a assinatura de zip_replace
mais legível.
2. changes tem que ser um Iterable[FromTo] ; é o mesmo que escrever Iterable[tuple[str, str]] , mas é mais
curto e mais fácil de ler.

O TypeAlias Explícito em Python 3.10


PEP 613—Explicit Type Aliases (https://fpy.li/pep613) introduziu um tipo especial, o TypeAlias , para
tornar as atribuições que criam apelidos de tipos mais visíveis e mais fáceis para os verificadores de
tipo. A partir do Python 3.10, esta é a forma preferencial de criar um apelidos de tipo.
👉 DICA PY3
from typing import TypeAlias

FromTo: TypeAlias = tuple[str, str]

abc.Iterable versus abc.Sequence


Tanto math.fsum quanto replacer.zip_replace tem que percorrer todos os argumentos do Iterable para
produzir um resultado. Dado um iterável sem fim tal como o gerador itertools.cycle como entrada, essas funções
consumiriam toda a memória e derrubariam o processo Python. Apesar desse perigo potencial, é muito comum no
Python moderno se oferecer funções que aceitam um Iterable como argumento, mesmo se elas tem que processar a
estrutura inteira para obter um resultado. Isso dá a quem chama a função a opção de fornecer um gerador como dado
de entrada, em vez de uma sequência pré-construída, com uma grande economia potencial de memória se o número
de itens de entrada for grande.

Por outro lado, a função columnize no Exemplo 31 requer uma Sequence , não um Iterable , pois ela precisa obter
a len() do argumento para calcular previamente o número de linhas.

Assim como Sequence , o melhor uso de Iterable é como tipo de argumento. Ele é muito vago como um tipo de
saída. Uma função deve ser mais precisa sobre o tipo concreto que retorna.

O tipo Iterator , usado como tipo do retorno no Exemplo 32, está intimamente relacionado a Iterable . Voltaremos a
ele em Capítulo 17, que trata de geradores e iteradores clássicos.
8.5.9. Genéricos parametrizados e TypeVar
Um genérico parametrizado é um tipo genérico, escrito na forma list[T] , onde T é um tipo variável que será
vinculado a um tipo específico a cada uso. Isso permite que um tipo de parâmetro seja refletido no tipo resultante.

O Exemplo 34 define sample , uma função que recebe dois argumentos: uma Sequence de elementos de tipo T e um
int . Ela retorna uma list de elementos do mesmo tipo T , escolhidos aleatoriamente do primeiro argumento.

O Exemplo 34 mostra a implementação.

Exemplo 34. sample.py

PY
from collections.abc import Sequence
from random import shuffle
from typing import TypeVar

T = TypeVar('T')

def sample(population: Sequence[T], size: int) -> list[T]:


if size < 1:
raise ValueError('size must be >= 1')
result = list(population)
shuffle(result)
return result[:size]

Aqui estão dois exemplos do motivo de eu usar um tipo variável em sample :

Se chamada com uma tupla de tipo tuple[int, …​] — que é consistente-com Sequence[int] - então o tipo
parametrizado é int , então o tipo de retorno é list[int] .
Se chamada com uma str — que é consistente-com Sequence[str] — então o tipo parametrizado é str , e o tipo
do retorno é list[str] .

Por que TypeVar é necessário?


Os autores da PEP 484 queriam introduzir dicas de tipo ao acrescentar o módulo typing , sem
mudar nada mais na linguagem. Com uma metaprogramação inteligente, eles poderiam fazer o
operador [] funcionar para classes como Sequence[T] . Mas o nome da variável T dentro dos
✒️ NOTA colchetes precisa ser definido em algum lugar - ou o interpretador Python necessitaria de mudanças
mais profundas, para suportar a notação de tipos genéricos como um caso especial de [] . Por isso o
construtor typing.TypeVar é necessário: para introduzir o nome da variável no namespace (espaço
de nomes) corrente. Linguagens como Java, C# e TypeScript não exigem que o nome da variável seja
declarado previamente, então eles não tem nenhum equivalente da classe TypeVar do Python.

Outro exemplo é a função statistics.mode da biblioteca padrão, que retorna o ponto de dado mais comum de uma
série.

Aqui é uma exemplo de uso da documentação (https://docs.python.org/pt-br/3/library/statistics.html#statistics.mode):

PYCON
>>> mode([1, 1, 2, 3, 3, 3, 3, 4])
3

Sem o uso de TypeVar , mode poderia ter uma assinatura como a apresentada no Exemplo 35.

Exemplo 35. mode_float.py: mode que opera com float e seus subtipos [89]
PY
from collections import Counter
from collections.abc import Iterable

def mode(data: Iterable[float]) -> float:


pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]

Muitos dos usos de mode envolvem valores int ou float , mas o Python tem outros tipos numéricos, e é desejável
que o tipo de retorno siga o tipo dos elementos do Iterable recebido. Podemos melhorar aquela assinatura usando
TypeVar . Vamos começar com uma assinatura parametrizada simples, mas errada.

PYTHON3
from collections.abc import Iterable
from typing import TypeVar

T = TypeVar('T')

def mode(data: Iterable[T]) -> T:

Quando aparece pela primeira vez na assinatura, o tipo parametrizado T pode ser qualquer tipo. Da segunda vez que
aparece, ele vai significar o mesmo tipo que da primeira vez.

Assim, qualquer iterável é consistente-com Iterable[T] , incluindo iterável de tipos unhashable que
collections.Counter não consegue tratar. Precisamos restringir os tipos possíveis de se atribuir a T . Vamos ver
maneiras diferentes de fazer isso nas duas seções seguintes.

TypeVar restrito
O TypeVar aceita argumentos posicionais adicionais para restringir o tipo parametrizado. Podemos melhorar a
assinatura de mode para aceitar um número específico de tipos, assim:

PYTHON3
from collections.abc import Iterable
from decimal import Decimal
from fractions import Fraction
from typing import TypeVar

NumberT = TypeVar('NumberT', float, Decimal, Fraction)

def mode(data: Iterable[NumberT]) -> NumberT:

Está melhor que antes, e era a assinatura de mode em statistics.pyi (https://fpy.li/8-30), o arquivo stub em typeshed em
25 de maio de 2020.

Entretanto, a documentação em statistics.mode (https://fpy.li/8-28) inclui esse exemplo:

PYCON
>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'

Na pressa, poderíamos apenas adicionar str à definição de NumberT :

PYTHON3
NumberT = TypeVar('NumberT', float, Decimal, Fraction, str)
Com certeza funciona, mas NumberT estaria muito mal batizado se aceitasse str . Mais importante, não podemos ficar
listando tipos para sempre, cada vez que percebermos que mode pode lidar com outro deles. Podemos fazer com
melhor com um outro recurso de TypeVar , como veremos a seguir.

TypeVar delimitada
Examinando o corpo de mode no Exemplo 35, vemos que a classe Counter é usada para classificação. Counter é
baseada em dict , então o tipo do elemento do iterável data precisa ser hashable.

A princípio, essa assinatura pode parecer que funciona:

PYTHON3
from collections.abc import Iterable, Hashable

def mode(data: Iterable[Hashable]) -> Hashable:

Agora o problema é que o tipo do item retornado é Hashable : um ABC que implementa apenas o método __hash__ .
Então o verificador de tipo não vai permitir que façamos nada com o valor retornado, exceto chamar seu método
hash() . Não é muito útil.

A solução está em outro parâmetro opcional de TypeVar : o parâmetro representado pela palavra-chave bound . Ele
estabelece um limite superior para os tipos aceitos. No Exemplo 36, temos bound=Hashable . Isso significa que o tipo
do parâmetro pode ser Hashable ou qualquer subtipo-de Hashable .[90]

Exemplo 36. mode_hashable.py: igual a Exemplo 35, mas com uma assinatura mais flexível

PY
from collections import Counter
from collections.abc import Iterable, Hashable
from typing import TypeVar

HashableT = TypeVar('HashableT', bound=Hashable)

def mode(data: Iterable[HashableT]) -> HashableT:


pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]

Em resumo:

Um tipo variável restrito será concretizado em um dos tipos nomeados na declaração do TypeVar.
Um tipo variável delimitado será concretizado pata o tipo inferido da expressão - desde que o tipo inferido seja
consistente-com o limite declarado pelo argumento bound= do TypeVar.

É um pouco lamentável que a palavra-chave do argumento para declarar um TypeVar delimitado


tenha sido chamado bound= , pois o verbo "to bind" (ligar ou vincular) é normalmente usado para
✒️ NOTA indicar o estabelecimento do valor de uma variável, que na semântica de referência do Python é
melhor descrita como vincular (bind) um nome a um valor. Teria sido menos confuso se a palavra-
chave do argumento tivesse sido chamada boundary= .

O construtor de typing.TypeVar tem outros parâmetros opcionais - covariant e contravariant — que veremos
em Capítulo 15, Seção 15.7.

Agora vamos concluir essa introdução a TypeVar com AnyStr .


O tipo variável pré-definido AnyStr
O módulo typing inclui um TypeVar pré-definido chamado AnyStr . Ele está definido assim:

PYTHON3
AnyStr = TypeVar('AnyStr', bytes, str)

AnyStr é usado em muitas funções que aceitam tanto bytes quanto str , e retornam valores do tipo recebido.

Agora vamos ver typing.Protocol , um novo recurso do Python 3.8, capaz de permitir um uso de dicas de tipo mais
pythônico.

8.5.10. Protocolos estáticos


Em programação orientada a objetos, o conceito de um "protocolo" como uma interface informal é
tão antigo quando o Smalltalk, e foi uma parte essencial do Python desde o início. Entretanto, no
✒️ NOTA contexto de dicas de tipo, um protocolo é uma subclasse de typing.Protocol , definindo uma
interface que um verificador de tipo pode analisar. Os dois tipos de protocolo são tratados em
Capítulo 13. Aqui apresento apenas uma rápida introdução no contexto de anotações de função.

O tipo Protocol , como descrito em PEP 544—Protocols: Structural subtyping (static duck typing) (https://fpy.li/pep544)
(EN), é similar às interfaces em Go: um tipo protocolo é definido especificando um ou mais métodos, e o verificador de
tipo analisa se aqueles métodos estão implementados onde um tipo daquele protocolo é usado.

Em Python, uma definição de protocolo é escrita como uma subclasse de typing.Protocol . Entretanto, classes que
implementam um protocolo não precisam herdar, registrar ou declarar qualquer relação com a classe que define o
protocolo. É função do verificador de tipo encontrar os tipos de protocolos disponíveis e exigir sua utilização.

Abaixo temos um problema que pode ser resolvido com a ajuda de Protocol e TypeVar . Suponha que você quisesse
criar uma função top(it, n) , que retorna os n maiores elementos do iterável it :

PYCON
>>> top([4, 1, 5, 2, 6, 7, 3], 3)
[7, 6, 5]
>>> l = 'mango pear apple kiwi banana'.split()
>>> top(l, 3)
['pear', 'mango', 'kiwi']
>>>
>>> l2 = [(len(s), s) for s in l]
>>> l2
[(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')]
>>> top(l2, 3)
[(6, 'banana'), (5, 'mango'), (5, 'apple')]

Um genérico parametrizado top ficaria parecido com o mostrado no Exemplo 37.

Exemplo 37. a função top function com um parâmetro de tipo T indefinido

PY
def top(series: Iterable[T], length: int) -> list[T]:
ordered = sorted(series, reverse=True)
return ordered[:length]

O problema é, como restringir T ? Ele não pode ser Any ou object , pois series precisa funcionar com sorted . A
sorted nativa na verdade aceita Iterable[Any] , mas só porque o parâmetro opcional key recebe uma função que
calcula uma chave de ordenação arbitrária para cada elemento. O que acontece se você passar para sorted uma lista
de objetos simples, mas não fornecer um argumento key ? Vamos tentar:
PYCON
>>> l = [object() for _ in range(4)]
>>> l
[<object object at 0x10fc2fca0>, <object object at 0x10fc2fbb0>,
<object object at 0x10fc2fbc0>, <object object at 0x10fc2fbd0>]
>>> sorted(l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'object' and 'object'

A mensagem de erro mostra que sorted usa o operador < nos elementos do iterável. É só isso? Vamos tentar outro
experimento rápido:[91]

PYCON
>>> class Spam:
... def __init__(self, n): self.n = n
... def __lt__(self, other): return self.n < other.n
... def __repr__(self): return f'Spam({self.n})'
...
>>> l = [Spam(n) for n in range(5, 0, -1)]
>>> l
[Spam(5), Spam(4), Spam(3), Spam(2), Spam(1)]
>>> sorted(l)
[Spam(1), Spam(2), Spam(3), Spam(4), Spam(5)]

Isso confirma a suspeita: eu consigo passar um lista de Spam para sort , porque Spam implementa __lt__ —o
método especial subjacente ao operador < .

Então o parâmetro de tipo T no Exemplo 37 deveria ser limitado a tipos que implementam __lt__ . No Exemplo 36,
precisávamos de um parâmetro de tipo que implementava __hash__ , para poder usar typing.Hashable como limite
superior do parâmetro de tipo. Mas agora não há um tipo adequado em typing ou abc para usarmos, então
precisamos criar um.

O Exemplo 38 mostra o novo tipo SupportsLessThan , um Protocol .

Exemplo 38. comparable.py: a definição de um tipo Protocol , SupportsLessThan

PY
from typing import Protocol, Any

class SupportsLessThan(Protocol): # (1)


def __lt__(self, other: Any) -> bool: ... # (2)

1. Um protocolo é uma subclasse de typing.Protocol .

2. O corpo do protocolo tem uma ou mais definições de método, com …​em seus corpos.

Um tipo T é consistente-com um protocolo P se T implementa todos os métodos definido em P , com assinaturas de


tipo correspondentes.

Dado SupportsLessThan , nós agora podemos definir essa versão funcional de top no Exemplo 39.

Exemplo 39. top.py: definição da função top usando uma TypeVar com bound=SupportsLessThan
PY
from collections.abc import Iterable
from typing import TypeVar

from comparable import SupportsLessThan

LT = TypeVar('LT', bound=SupportsLessThan)

def top(series: Iterable[LT], length: int) -> list[LT]:


ordered = sorted(series, reverse=True)
return ordered[:length]

Vamos testar top . O Exemplo 40 mostra parte de uma bateria de testes para uso com o pytest . Ele tenta chamar top
primeiro com um gerador de expressões que produz tuple[int, str] , e depois com uma lista de object . Com a
lista de object , esperamos receber uma exceção de TypeError .

Exemplo 40. top_test.py: visão parcial da bateria de testes para top

PY
from collections.abc import Iterator
from typing import TYPE_CHECKING # (1)

import pytest

from top import top

# muitas linhas omitidas

def test_top_tuples() -> None:


fruit = 'mango pear apple kiwi banana'.split()
series: Iterator[tuple[int, str]] = ( # (2)
(len(s), s) for s in fruit)
length = 3
expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
result = top(series, length)
if TYPE_CHECKING: # (3)
reveal_type(series) # (4)
reveal_type(expected)
reveal_type(result)
assert result == expected

# intentional type error


def test_top_objects_error() -> None:
series = [object() for _ in range(4)]
if TYPE_CHECKING:
reveal_type(series)
with pytest.raises(TypeError) as excinfo:
top(series, 3) # (5)
assert "'<' not supported" in str(excinfo.value)

1. A constante typing.TYPE_CHECKING é sempre False durante a execução do programa, mas os verificadores de


tipo fingem que ela é True quando estão fazendo a verificação.
2. Declaração de tipo explícita para a variável series , para tornar mais fácil a leitura da saída do Mypy.[92]

3. Esse if evita que as três linhas seguintes sejam executadas durante o teste.
4. reveal_type() não pode ser chamada durante a execução, porque não é uma função regular, mas sim um
mecanismo de depuração do Mypy - por isso não há import para ela. Mypy vai produzir uma mensagem de
depuração para cada chamada à pseudo-função reveal_type() , mostrando o tipo inferido do argumento.
5. Essa linha será marcada pelo Mypy como um erro.
Os testes anteriores são bem sucedidos - mas eles funcionariam de qualquer forma, com ou sem dicas de tipo em
top.py. Mais precisamente, se eu verificar aquele arquivo de teste com o Mypy, verei que o TypeVar está funcionando
como o esperado. Veja a saída do comando mypy no Exemplo 41.

Desde o Mypy 0.910 (julho de 2021), em alguns casos a saída de reveal_type não mostra
⚠️ AVISO precisamente os tipos que eu declarei, mas mostra tipos compatíveis. Por exemplo, eu não usei
typing.Iterator e sim abc.Iterator . Por favor, ignore esse detalhe. O relatório do Mypy ainda é
útil. Vou fingir que esse problema do Mypy já foi corrigido quando for discutir os resultados.

Exemplo 41. Saída do mypy top_test.py (linha quebradas para facilitar a leitura)

…/comparable/ $ mypy top_test.py


top_test.py:32: note:
Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]" (1)
top_test.py:33: note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test.py:34: note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]" (2)
top_test.py:41: note:
Revealed type is "builtins.list[builtins.object*]" (3)
top_test.py:43: error:
Value of type variable "LT" of "top" cannot be "object" (4)
Found 1 error in 1 file (checked 1 source file)

1. Em test_top_tuples , reveal_type(series) mostra que ele é um Iterator[tuple[int, str]] — que eu


declarei explicitamente.
2. reveal_type(result) confirma que o tipo produzido pela chamada a top é o que eu queria: dado o tipo de
series , o result é list[tuple[int, str]] .
3. Em test_top_objects_error , reveal_type(series) mostra que ele é uma list[object*] . Mypy põe um *
após qualquer tipo que tenha sido inferido: eu não anotei o tipo de series nesse teste.
4. Mypy marca o erro que esse teste produz intencionalmente: o tipo dos elementos do Iterable series não pode
ser object (ele tem que ser do tipo SupportsLessThan ).

A principal vantagem de um tipo protocolo sobre os ABCs é que o tipo não precisa de nenhuma declaração especial
para ser consistente-com um tipo protocolo. Isso permite que um protocolo seja criado aproveitando tipos pré-
existentes, ou tipos implementados em bases de código que não estão sob nosso controle. Eu não tenho que derivar ou
registrar str , tuple , float , set , etc. com SupportsLessThan para usá-los onde um parâmetro
SupportsLessThan é esperado. Eles só precisam implementar __lt__ . E o verificador de tipo ainda será capaz de
realizar seu trabalho, porque SupportsLessThan está explicitamente declarado como um Protocol — diferente dos
protocolos implícitos comuns no duck typing, que são invisíveis para o verificador de tipos.

A classe especial Protocol foi introduzida na PEP 544—Protocols: Structural subtyping (static duck typing)
(https://fpy.li/pep544). O Exemplo 39 demonstra porque esse recurso é conhecido como duck typing estático (static duck
typing): a solução para anotar o parâmetro series de top era dizer "O tipo nominal de series não importa, desde
que ele implemente o método __lt__ ." Em Python, o duck typing sempre permitiu dizer isso de forma implícita,
deixando os verificadores de tipo estáticos sem ação. Um verificador de tipo não consegue ler o código fonte em C do
CPython, ou executar experimentos no console para descobrir que sorted só requer que seus elementos suportem < .

Agora podemos tornar o duck typing explícito para os verificadores estáticos de tipo. Por isso faz sentido dizer que
typing.Protocol nos oferece duck typing estático.[93]
Há mais para falar sobre typing.Protocol . Vamos voltar a ele na Parte IV, onde Capítulo 13 compara as abordagens
da tipagem estrutural, do duck typing e dos ABCs - outro modo de formalizar protocolos. Além disso, a Seção 15.2 (no
Capítulo 15) explica como declarar assinaturas de funções de sobrecarga (overload) com @typing.overload , e inclui
um exemplo bastante extenso usando typing.Protocol e uma TypeVar delimitada.

O typing.Protocol torna possível anotar a função double na Seção 8.4 sem perder
✒️ NOTA funcionalidade. O segredo é definir uma classe de protocolo com o método __mul__ . Convido o
leitor a fazer isso como um exercício. A solução está na Seção 13.6.1 (Capítulo 13).

8.5.11. Callable
Para anotar parâmetros de callback ou objetos callable retornados por funções de ordem superior, o módulo
collections.abc oferece o tipo Callable , disponível no módulo typing para quem ainda não estiver usando
Python 3.9. Um tipo Callable é parametrizado assim:

PYTHON3
Callable[[ParamType1, ParamType2], ReturnType]

A lista de parâmetros - [ParamType1, ParamType2] — pode ter zero ou mais tipos.

Aqui está um exemplo no contexto de uma função repl , parte do interpretador iterativo simples que veremos na
Seção 18.3:[94]

PYTHON3
def repl(input_fn: Callable[[Any], str] = input]) -> None:

Durante a utilização normal, a função repl usa a input nativa do Python para ler expressões inseridas pelo usuário.
Entretanto, para testagem automatizada ou para integração com outras fontes de input, repl aceita um parâmetro
input_fn opcional: um Callable com o mesmo parâmetro e tipo de retorno de input .

A input nativa tem a seguinte assinatura no typeshed:

PYTHON3
def input(__prompt: Any = ...) -> str: ...

A assinatura de input é consistente-com esta dica de tipo Callable

PYTHON3
Callable[[Any], str]

Não existe sintaxe para a nomear tipo de argumentos opcionais ou de palavra-chave. A documentação
(https://docs.python.org/pt-br/3/library/typing.html#typing.Callable) de typing.Callable diz "tais funções são raramente
usadas como tipo de callback." Se você precisar de um dica de tipo para acompanhar uma função com assinatura
flexível, substitua o lista de parâmetros inteira por …​- assim:

PYTHON3
Callable[..., ReturnType]

A interação de parâmetros de tipo genéricos com uma hierarquia de tipos introduz um novo conceito: variância.

Variância em tipos callable


Imagine um sistema de controle de temperatura com uma função update simples, como mostrada no Exemplo 42. A
função update chama a função probe para obter a temperatura atual, e chama display para mostrar a
temperatura para o usuário. probe e display são ambas passadas como argumentos para update , por motivos
didáticos. O objetivo do exemplo é contrastar duas anotações de Callable : uma com um tipo de retorno e outro com
um tipo de parâmetro.
Exemplo 42. Ilustrando a variância.

PY
from collections.abc import Callable

def update( # (1)


probe: Callable[[], float], # (2)
display: Callable[[float], None] # (3)
) -> None:
temperature = probe()
# imagine lots of control code here
display(temperature)

def probe_ok() -> int: # (4)


return 42

def display_wrong(temperature: int) -> None: # (5)


print(hex(temperature))

update(probe_ok, display_wrong) # type error # (6)

def display_ok(temperature: complex) -> None: # (7)


print(temperature)

update(probe_ok, display_ok) # OK # (8)

1. update recebe duas funções callable como argumentos.


2. probe precisa ser uma callable que não recebe nenhuma argumento e retorna um float

3. display recebe um argumento float e retorna None .

4. probe_ok é consistente-com Callable[[], float] porque retornar um int não quebra código que espera um
float .

5. display_wrong não é consistente-com Callable[[float], None] porque não há garantia que uma função
esperando um int consiga lidar com um float ; por exemplo, a função hex do Python aceita um int mas
rejeita um float .
6. O Mypy marca essa linha porque display_wrong é incompatível com a dica de tipo no parâmetro display em
update .

7. display_ok é consistente_com Callable[[float], None] porque uma função que aceita um complex também
consegue lidar com um argumento float .
8. Mypy está satisfeito com essa linha.

Resumindo, não há problema em fornecer uma função de callback que retorne um int quando o código espera uma
função callback que retorne um float , porque um valor int sempre pode ser usado onde um float é esperado.

Formalmente, dizemos que Callable[[], int] é subtipo-de Callable[[], float] — assim como int é subtipo-de
float . Isso significa que Callable é covariante no que diz respeito aos tipos de retorno, porque a relação subtipo-de
dos tipos int e float aponta na mesma direção que os tipo Callable que os usam como tipos de retorno.

Por outro lado, é um erro de tipo fornecer uma função callback que recebe um argumento int quando é necessário
um callback que possa processar um float .
Formalmente, Callable[[int], None] não é subtipo-de Callable[[float], None] . Apesar de int ser subtipo-de
float , no Callable parametrizado a relação é invertida: Callable[[float], None] é subtipo-de
Callable[[int], None] . Assim dizemos que aquele Callable é contravariante a respeito dos tipos de parâmetros
declarados.

A Seção 15.7 no Capítulo 15 explica variância em mais detalhes e com exemplos de tipos invariantes, covariantes e
contravariantes.

Por hora, saiba que a maioria dos tipos genéricos parametrizados são invariantes, portanto mais
simples. Por exemplo, se eu declaro scores: list[float] , isso me diz exatamente o que posso
atribuir a scores . Não posso atribuir objetos declarados como list[int] ou list[complex] :

👉 DICA Um objeto list[int] não é aceitável porque ele não pode conter valores
código pode precisar colocar em scores .
float que meu

Um objeto list[complex] não é aceitável porque meu código pode precisar ordenar scores
para encontrar a mediana, mas complex não fornece o método __lt__ , então list[complex]
não é ordenável.

Agora chegamos ou último tipo especial que examinaremos nesse capítulo.

8.5.12. NoReturn
Esse é um tipo especial usado apenas para anotar o tipo de retorno de funções que nunca retornam. Normalmente, elas
existem para gerar exceções. Há dúzias dessas funções na biblioteca padrão.

Por exemplo, sys.exit() levanta SystemExit para encerrar o processo Python.

Sua assinatura no typeshed é:

PYTHON3
def exit(__status: object = ...) -> NoReturn: ...

O parâmetro __status__ é apenas posicional, e tem um valor default. Arquivos stub não contém valores default, em
vez disso eles usam …​. O tipo de __status é object , o que significa que pode também ser None , assim seria
redundante escrever Optional[object] .

Na [class_metaprog], o [checked_class_bottom_ex] usa NoReturn em __flag_unknown_attrs , um método projetado


para produzir uma mensagem de erro completa e amigável, e então levanta um AttributeError .

A última seção desse capítulo épico é sobre parâmetros posicionais e variádicos

8.6. Anotando parâmetros apenas posicionais e variádicos


Lembra da função tag do Exemplo 9? Da última vez que vimos sua assinatura foi em Seção 7.7.1:

PYTHON
def tag(name, /, *content, class_=None, **attrs):

Aqui está tag , completamente anotada e ocupando várias linhas - uma convenção comum para assinaturas longas,
com quebras de linha como o formatador blue (https://fpy.li/8-10) faria:
PYTHON
from typing import Optional

def tag(
name: str,
/,
*content: str,
class_: Optional[str] = None,
**attrs: str,
) -> str:

Observe a dica de tipo *content: str , para parâmetros posicionais arbitrários; Isso significa que todos aqueles
argumentos tem que ser do tipo str . O tipo da variável local content no corpo da função será tuple[str, …​] .

A dica de tipo para argumentos de palavra-chave arbitrários é attrs: str neste exemplo, portanto o tipo de
attrs dentro da função será dict[str, str] . Para uma dica de tipo como attrs: float , o tipo de attrs na
função seria dict[str, float] .``

Se for necessário que o parâmetro attrs aceite valores de tipos diferentes, é preciso usar uma Union[] ou Any :
**attrs: Any .

A notação / para parâmetros puramente posicionais só está disponível com Python ≥ 3.8. Em Python 3.7 ou anterior,
isso é um erro de sintaxe. A convenção da PEP 484 (https://fpy.li/8-36) é prefixar o nome cada parâmetro puramente
posicional com dois sublinhados. Veja a assinatura de tag novamente, agora em duas linhas, usando a convenção da
PEP 484:

PYTHON
from typing import Optional

def tag(__name: str, *content: str, class_: Optional[str] = None,


**attrs: str) -> str:

O Mypy entende e aplica as duas formas de declarar parâmetros puramente posicionais.

Para encerrar esse capítulo, vamos considerar brevemente os limites das dicas de tipo e do sistema de tipagem estática
que elas suportam.

8.7. Tipos imperfeitos e testes poderosos


Os mantenedores de grandes bases de código corporativas relatam que muitos bugs são encontrados por verificadores
de tipo estáticos, e o custo de resolvê-los é menor que se os mesmos bugs fossem descobertos apenas após o código
estar rodando em produção. Entretanto, é essencial observar que a testagem automatizada era uma prática padrão
largamente adotada muito antes da tipagem estática ser introduzida nas empresas que eu conheço.

Mesmo em contextos onde ela é mais benéfica, a tipagem estática não pode ser elevada a árbitro final da correção. Não
é difícil encontrar:

Falsos Positivos
Ferramentas indicam erros de tipagem em código correto.

Falsos Negativos
Ferramentas não indicam erros em código incorreto.

Além disso, se formos forçados a checar o tipo de tudo, perdemos um pouco do poder expressivo do Python:
Alguns recursos convenientes não podem ser checados de forma estática: por exemplo, o desempacotamento de
argumentos como em config(**settings) .
Recursos avançados como propriedades, descritores, metaclasses e metaprogramação em geral, têm suporte muito
deficiente ou estão além da compreensão dos verificadores de tipo
Verificadores de tipo ficam obsoletos e/ou incompatíveis após o lançamento de novas versões do Python, rejeitando
ou mesmo quebrando ao analisar código com novos recursos da linguagem - algumas vezes por mais de um ano.

Restrições comuns de dados não podem ser expressas no sistema de tipo - mesmo restrições simples. Por exemplo,
dicas de tipo são incapazes de assegurar que "quantidade deve ser um inteiro > 0" ou que "label deve ser uma string
com 6 a 12 letras em ASCII." Em geral, dicas de tipo não são úteis para localizar erros na lógica do negócio subjacente ao
código.

Dadas essas ressalvas, dicas de tipo não podem ser o pilar central da qualidade do software, e torná-las obrigatórias
sem qualquer exceção só amplificaria os aspectos negativos.

Considere o verificador de tipo estático como uma das ferramentas na estrutura moderna de integração de código, ao
lado de testadores, analisadores de código (linters), etc. O objetivo de uma estrutura de produção de integração de
código é reduzir as falhas no software, e testes automatizados podem encontrar muitos bugs que estão fora do alcance
de dicas de tipo. Qualquer código que possa ser escrito em Python pode ser testado em Python - com ou sem dicas de
tipo.

O título e a conclusão dessa seção foram inspirados pelo artigo "Strong Typing vs. Strong Testing"
(https://fpy.li/8-37) (EN) de Bruce Eckel, também publicado na antologia The Best Software Writing I
(https://fpy.li/8-38) (EN), editada por Joel Spolsky (Apress). Bruce é um fã de Python, e autor de livros
✒️ NOTA sobre C++, Java, Scala, e Kotlin. Naquele texto, ele conta como foi um defensor da tipagem estática
até aprender Python, e conclui: "Se um programa em Python tem testes de unidade adequados, ele
poderá ser tão robusto quanto um programa em C++, Java, ou C# com testes de unidade adequados
(mas será mais rápido escrever os testes em Python).

Isso encerra nossa cobertura das dicas de tipo em Python por agora. Elas serão também o ponto central do Capítulo 15,
que trata de classes genéricas, variância, assinaturas sobrecarregadas, coerção de tipos (type casting), entre outros
tópicos. Até lá, as dicas de tipo aparecerão em várias funções ao longo do livro.

8.8. Resumo do capítulo


Começamos com uma pequena introdução ao conceito de tipagem gradual, depois adotamos uma abordagem prática. É
difícil ver como a tipagem gradual funciona sem uma ferramenta que efetivamente leia as dicas de tipo, então
desenvolvemos uma função anotada guiados pelos relatórios de erro do Mypy.

Voltando à ideia de tipagem gradual, vimos como ela é um híbrido do duck typing tradicional de Python e da tipagem
nominal mais familiar aos usuários de Java, C++ e de outra linguagens de tipagem estática.

A maior parte do capítulo foi dedicada a apresentar os principais grupos de tipos usados em anotações. Muitos dos
tipos discutidos estão relacionados a tipos conhecidos de objetos do Python, tais como coleções, tuplas e callables -
estendidos para suportar notação genérica do tipo Sequence[float] . Muitos daqueles tipos são substitutos
temporários, implementados no módulo typing antes que os tipos padrão fossem modificados para suportar
genéricos, no Python 3.9.

Alguns desses tipos são entidade especiais. Any , Optional , Union , e NoReturn não tem qualquer relação com
objetos reais na memória, existem apenas no domínio abstrato do sistema de tipos.
Estudamos genéricos parametrizados e variáveis de tipo, que trazem mais flexibilidade para as dicas de tipo sem
sacrificar a segurança da tipagem.

Genéricos parametrizáveis se tornam ainda mais expressivos com o uso de Protocol . Como só surgiu no Python 3.8,
Protocol ainda não é muito usado - mas é de uma enorme importância. Protocol permite duck typing estático: É a
ponte fundamental entre o núcleo do Python, coberto pelo duck typing, e a tipagem nominal que permite a
verificadores de tipo estáticos encontrarem bugs.

Ao discutir alguns desses tipos, usamos o Mypy para localizar erros de checagem de tipo e tipos inferidos, com a ajuda
da função mágica reveal_type() do Mypy.

A seção final mostrou como anotar parâmetros exclusivamente posicionais e variádicos.

Dicas de tipo são um tópico complexo e em constante evolução. Felizmente elas são um recurso opcional. Vamos
manter o Python acessível para a maior base de usuários possível, e parar de defender que todo código Python precisa
ter dicas de tipo - como já presenciei em sermões públicos de evangelistas da tipagem.

Nosso BDFL[95] emérito liderou a movimento de inclusão de dicas de tipo em Python, então é muito justo que esse
capítulo comece e termine com palavras dele.

“ Não gostaria de uma versão de Python na qual eu fosse moralmente obrigado a adicionar dicas
de tipo o tempo todo. Eu realmente acho que dicas de tipo tem seu lugar, mas há muitas
ocasiões em que elas não valem a pena, e é maravilhoso que possamos escolher usá-las.[96]
— Guido van Rossum

8.9. Para saber mais


Bernát Gábor escreveu em seu excelente post, "The state of type hints in Python" (https://fpy.li/8-41) (EN):

“ Dicas de Tipo deveriam ser usadas sempre que valha à pena escrever testes de unidade .
Eu sou um grande fã de testes, mas também escrevo muito código exploratório. Quando estou explorando, testes e
dicas de tipo não ajudam. São um entrave.

Esse post do Gábor é uma das melhores introduções a dicas de tipo em Python que eu já encontrei, junto com o texto de
Geir Arne Hjelle, "Python Type Checking (Guide)" (https://fpy.li/8-42) (EN). "Hypermodern Python Chapter 4: Typing"
(https://fpy.li/8-43) (EN), de Claudio Jolowicz, é uma introdução mas curta que também fala de validação de checagem de
tipo durante a execução.

Para uma abordagem mais aprofundada, a documentação do Mypy (https://fpy.li/8-44) é a melhor fonte. Ela é útil
independente do verificador de tipo que você esteja usando, pois tem páginas de tutorial e de referência sobre tipagem
em Python em geral - não apenas sobre o próprio Mypy.

Lá você também encontrará uma conveniente página de referência (ou _cheat sheet) (https://fpy.li/8-45) (EN) e uma
página muito útil sobre problemas comuns e suas soluções (https://fpy.li/8-46) (EN).

A documentação do módulo typing (https://docs.python.org/pt-br/3/library/typing.html) é uma boa referência rápida, mas


não entra em muitos detalhes.

A PEP 483—The Theory of Type Hints (https://fpy.li/pep483) (EN) inclui uma explicação aprofundada sobre variância,
usando Callable para ilustrar a contravariância. As referências definitivas são as PEP relacionadas a tipagem. Já
existem mais de 20 delas. A audiência alvo das PEPs são os core developers (desenvolvedores principais da linguagem
em si) e o Steering Council do Python, então elas pressupõe uma grande quantidade de conhecimento prévio, e
certamente não são uma leitura leve.
Como já mencionado, o Capítulo 15 cobre outros tópicos sobre tipagem, e a Seção 15.10 traz referências adicionais,
incluindo a Tabela 16, com a lista das PEPs sobre tipagem aprovadas ou em discussão até o final de 2021.

"Awesome Python Typing" (https://fpy.li/8-47) é uma ótima coleção de links para ferramentas e referências.

Ponto de vista
Apenas Pedale

“ Esqueça as desconfortáveis bicicletas ultraleves, as malhas brilhantes, os sapatos


desajeitados que se prendem a pedais minúsculos, o esforço de quilômetros intermináveis.
Em vez disso, faça como você fazia quando era criança - suba na sua bicicleta e descubra o
puro prazer de pedalar.
— Grant Petersen
Just Ride: A Radically Practical Guide to Riding Your Bike (Apenas Pedale: Um Guia Radicalmente Prático sobre o Uso de
sua Bicicleta) (Workman Publishing)

Se programar não é sua profissão principal, mas uma ferramenta útil no seu trabalho ou algo que você faz para
aprender, experimentar e se divertir, você provavelmente não precisa de dicas de tipo mais que a maioria dos
ciclistas precisa de sapatos com solas rígidas e presilhas metálicas.

Apenas programe.

O Efeito Cognitivo da Tipagem

Eu me preocupo com o efeito que as dicas de tipo terão sobre o estilo de programação em Python.

Concordo que usuários da maioria das APIs se beneficiam de dicas de tipo. Mas o Python me atraiu - entre outras
razões - porque proporciona funções tão poderosas que substituem APIs inteiras, e podemos escrever nós
mesmos funções poderosas similares. Considere a função nativa max() (https://fpy.li/8-48). Ela é poderosa,
entretanto fácil de entender. Mas vou mostrar na Seção 15.2.1 que são necessárias 14 linhas de dicas de tipo para
anotar corretamente essa função - sem contar um typing.Protocol e algumas definições de TypeVar para
sustentar aquelas dicas de tipo.

Me inquieta que a coação estrita de dicas de tipo em bibliotecas desencorajem programadores de sequer
considerarem programar funções assim no futuro.

De acordo com o verbete em inglês na Wikipedia, "relatividade linguística" (https://fpy.li/8-49) — ou a hipótese


Sapir–Whorf — é um "princípio alegando que a estrutura de uma linguagem afeta a visão de mundo ou a
cognição de seus falantes"

A Wikipedia continua:

A versão forte diz que a linguagem determina o pensamento, e que categorias linguísticas limitam e
determinam as categorias cognitivas.
A versão fraca diz que as categorias linguísticas e o uso apenas influenciam o pensamento e as decisões.

Linguistas em geral concordam que a versão forte é falsa, mas há evidência empírica apoiando a versão fraca.
Não conheço estudos específicos com linguagens de programação, mas na minha experiência, elas tiveram
grande impacto sobre a forma como eu abordo problemas. A primeira linguagem de programação que usei
profissionalmente foi o Applesoft BASIC, na era dos computadores de 8 bits. Recursão não era diretamente
suportada pelo BASIC - você tinha que produzir sua própria pilha de chamada (call stack) para obter recursão.
Então eu nunca considerei usar algoritmos ou estruturas de dados recursivos. Eu sabia, em algum nível
conceitual, que tais coisas existiam, mas elas não eram parte de meu arsenal de técnicas de resolução de
problemas.

Décadas mais tarde, quando aprendi Elixir, gostei de resolver problemas com recursão e usei essa técnica além da
conta - até descobrir que muitas das minhas soluções seriam mais simples se que usasse funções existentes nos
módulos Enum e Stream do Elixir. Aprendi que código de aplicações em Elixir idiomático raramente contém
chamadas recursivas explícitas - em vez disso, usam enums e streams que implementam recursão por trás das
cortinas.

A relatividade linguística pode explicar a ideia recorrente (e também não provada) que aprender linguagens de
programação diferentes torna alguém um programador melhor, especialmente quando as linguagens em questão
suportam diferentes paradigmas de programação. Praticar com Elixir me tornou mais propenso a aplicar
patterns funcionais quando escrevo programas em Python ou Go.

Agora voltando à Terra.

O pacote requests provavelmente teria uma API muito diferente se Kenneth Reitz estivesse decidido (ou tivesse
recebido ordens de seu chefe) a anotar todas as suas funções. Seu objetivo era escrever uma API que fosse fácil de
usar, flexível e poderosa. Ele conseguiu, dada a fantástica popularidade de requests - em maio de 2020, ela
estava em #4 nas PyPI Stats (https://fpy.li/8-50), com 2,6 milhões de downloads diários. A #1 era a urllib3 , uma
dependência de requests .

Em 2017 os mantenedores de requests decidiram (https://fpy.li/8-51) não perder seu tempo escrevendo dicas de
tipo. Um deles, Cory Benfield, escreveu um email dizendo:

“ Acho que bibliotecas com APIs 'pythônicas' são as menos propensas a adotar esse sistema
de tipagem, pois ele vai adicionar muito pouco valor a elas.

Naquela mensagem, Benfield incluiu esse exemplo extremo de uma tentativa de definição de tipo para o
argumento de palavra-chave files em requests.request() (https://fpy.li/8-53):
Optional[
Union[
Mapping[
basestring,
Union[
Tuple[basestring, Optional[Union[basestring, file]]],
Tuple[basestring, Optional[Union[basestring, file]],
Optional[basestring]],
Tuple[basestring, Optional[Union[basestring, file]],
Optional[basestring], Optional[Headers]]
]
],
Iterable[
Tuple[
basestring,
Union[
Tuple[basestring, Optional[Union[basestring, file]]],
Tuple[basestring, Optional[Union[basestring, file]],
Optional[basestring]],
Tuple[basestring, Optional[Union[basestring, file]],
Optional[basestring], Optional[Headers]]
]
]
]
]

E isso assume essa definição:

Headers = Union[
Mapping[basestring, basestring],
Iterable[Tuple[basestring, basestring]],
]

Você acha que requests seria como é se os mantenedores insistissem em ter uma cobertura de dicas de tipo de
100%? SQLAlchemy é outro pacote importante que não trabalha muito bem com dicas de tipo.

O que torna essas bibliotecas fabulosas é incorporarem a natureza dinâmica do Python.

Apesar das dicas de tipo trazerem benefícios, há também um preço a ser pago.

Primeiro, há o significativo investimento em entender como o sistema de tipos funciona. Esse é um custo unitário.

Mas há também um custo recorrente, eterno.

Nós perdemos um pouco do poder expressivo do Python se insistimos que tudo precisa estar sob a checagem de
tipos. Recursos maravilhosos como desempacotamento de argumentos — e.g., config(**settings) — estão
além da capacidade de compreensão dos verificadores de tipo.

Se você quiser ter uma chamada como config(**settings) verificada quanto ao tipo, você precisa explicitar
cada argumento. Isso me traz lembranças de programas em Turbo Pascal, que escrevi 35 anos atrás.

Bibliotecas que usam metaprogramação são difíceis ou impossíveis de anotar. Claro que a metaprogramação
pode ser mal usada, mas isso também é algo que torna muitos pacotes do Python divertidos de usar.

Se dicas de tipo se tornarem obrigatórias sem exceções, por uma decisão superior em grande empresas, aposto
que logo veremos pessoas usando geração de código para reduzir linhas de código padrão em programas Python -
uma prática comum com linguagens menos dinâmicas.
Para alguns projetos e contextos, dicas de tipo simplesmente não fazem sentido. Mesmo em contextos onde elas
fazer muito sentido, não fazem sentido o tempo todo. Qualquer política razoável sobre o uso de dicas de tipo
precisa conter exceções.

Alan Kay, o recipiente do Turing Award que foi um dos pioneiros da programação orientada a objetos, certa vez
disse:

“ Algumas pessoas são completamente religiosas no que diz respeito a sistemas de tipo, e
como um matemático eu adoro a ideia de sistemas de tipos, mas ninguém até agora
inventou um que tenha alcance o suficiente..[97]

Obrigado, Guido, pela tipagem opcional. Vamos usá-la como foi pensada, e não tentar anotar tudo em
conformidade estrita com um estilo de programação que se parece com Java 1.5.

Duck Typing FTW

Duck typing encaixa bem no meu cérebro, e duck typing estático é um bom compromisso, permitindo checagem
estática de tipo sem perder muito da flexibilidade que alguns sistemas de tipagem nominal só permitem ao custo
de muita complexidade - isso quando permitem.

Antes da PEP 544, toda essa ideia de dicas de tipo me parecia completamente não-pythônica, Fiquei muito feliz
quando vi typing.Protocol surgir em Python. Ele traz equilíbrio para a Força.

Genéricos ou Específicos?

De uma perspectiva de Python, o uso do termo "genérico" na tipagem é um retrocesso. Os sentidos comuns do
termo "genérico" são "aplicável integralmente a um grupo ou uma classe" ou "sem uma marca distintiva."

Considere list versus list[str] . o primeiro é genérico: aceita qualquer objeto. O segundo é específico: só
aceita str .

Por outro lado, o termo faz sentido em Java. Antes do Java 1.5, todas as coleções de Java (exceto a mágica array )
eram "específicas": só podiam conter referência a Object , então era necessário converter os itens que saim de
uma coleção antes que eles pudessem ser usados. Com Java 1.5, as coleções ganharam parâmetros de tipo, e se
tornaram "genéricas."
9. Decoradores e Clausuras
“ Houve uma certa quantidade de reclamações sobre a escolha do nome "decorador" para esse
recurso. A mais frequente foi sobre o nome não ser consistente com seu uso no livro da GoF. [98]

O nome decorator provavelmente se origina de seu uso no âmbito dos compiladores—​uma


árvore sintática é percorrida e anotada.
— PEP 318—Decorators for Functions and Methods ("Decoradores para Funções e Métodos"—EN)

Decoradores de função nos permitem "marcar" funções no código-fonte, para aprimorar de alguma forma seu
comportamento. É um mecanismo muito poderoso. Por exemplo, o decorador @functools.cache armazena um
mapeamento de argumentos para resultados, e depois usa esse mapeamento para evitar computar novamente o
resultado quando a função é chamada com argumentos já vistos. Isso pode acelerar muito uma aplicação.

Mas para dominar esse recurso é preciso antes entender clausuras (closures)—o nome dado à estrutura onde uma
função captura variáveis presentes no escopo onde a função é definida, necessárias para a execução da função
futuramente.[99]

A palavra reservada mais obscura do Python é nonlocal , introduzida no Python 3.0. É perfeitamente possível ter uma
vida produtiva e lucrativa programando em Python sem jamais usá-la, seguindo uma dieta estrita de orientação a
objetos centrada em classes. Entretanto, caso queira implementar seus próprios decoradores de função, precisa
entender clausuras, e então a necessidade de nonlocal fica evidente.

Além de sua aplicação aos decoradores, clausuras também são essenciais para qualquer tipo de programação
utilizando callbacks, e para programar em um estilo funcional quando isso fizer sentido.

O objetivo último deste capítulo é explicar exatamente como funcionam os decoradores de função, desde simples
decoradores de registro até os complicados decoradores parametrizados. Mas antes de chegar a esse objetivo,
precisamos tratar de:

Como o Python analisa a sintaxe de decoradores


Como o Python decide se uma variável é local
Porque clausuras existem e como elas funcionam
Qual problema é resolvido por nonlocal

Após criar essa base, poderemos então enfrentar os outros tópicos relativos aos decoradores:

A implementação de um decorador bem comportado


Os poderosos decoradores na biblioteca padrão: @cache , @lru_cache , e @singledispatch

A implementação de um decorador parametrizado

9.1. Novidades nesse capítulo


O decorador de caching functools.cache —introduzido no Python 3.9—é mais simples que o tradicional
functools.lru_cache , então falo primeiro daquele. Este último é tratado na seção Seção 9.9.2, incluindo a forma
simplificada introduzida no Python 3.8.

A seção Seção 9.9.3 foi expandida e agora inclui dicas de tipo, a forma recomendada de usar
functools.singledispatch desde o Python 3.7.
A seção Seção 9.10 agora inclui um exemplo baseado em classes, o Exemplo 27.

Tranferi o #rethinking_design_patterns para o final da [function_objects_part], para melhorar a fluidez do livro. E a


seção Seção 10.3 também aparece agora naquele capítulo, juntamente com outras variantes do padrão de projeto
Estratégia usando invocáveis.

Começamos com uma introdução muito suave aos decoradores, e dali seguiremos para o restante dos tópicos listados
no início do capítulo.

9.2. Introdução aos decoradores


Um decorador é um invocável que recebe outra função como um argumento (a função decorada).

Um decorador pode executar algum processamento com a função decorada, e ou a devolve ou a substitui por outra
função ou por um objeto invocável.[100]

Em outras palavras, supondo a existência de um decorador chamado decorate , esse código:

PYTHON3
@decorate
def target():
print('running target()')

tem o mesmo efeito de:

PYTHON3
def target():
print('running target()')

target = decorate(target)

O resultado final é o mesmo: após a execução de qualquer dos dois trechos, o nome target está vinculado a qualquer
que seja a função devolvida por decorate(target) —que tanto pode ser a função inicialmente chamada target
quanto uma outra função diferente.

Para confirmar que a função decorada é substituída, veja a sessão de console no Exemplo 1.

Exemplo 1. Um decorador normalmente substitui uma função por outra, diferente

PYCON
>>> def deco(func):
... def inner():
... print('running inner()')
... return inner (1)
...
>>> @deco
... def target(): (2)
... print('running target()')
...
>>> target() (3)
running inner()
>>> target (4)
<function deco.<locals>.inner at 0x10063b598>

1. deco devolve seu objeto função inner .

2. target é decorada por deco .


3. Invocar a target decorada causa, na verdade, a execução de inner .

4. A inspeção revela que target é agora uma referência a inner .


Estritamente falando, decoradores são apenas açúcar sintático. Como vimos, é sempre possível chamar um decorador
como um invocável normal, passando outra função como parâmetro. Algumas vezes isso inclusive é conveniente,
especialmente quando estamos fazendo metaprogramação—mudando o comportamento de um programa durante a
execução.

Três fatos essenciais nos dão um bom resumo dos decoradores:

Um decorador é uma função ou outro invocável.


Um decorador pode substituir a função decorada por outra, diferente.
Decoradores são executados imediatamente quando um módulo é carregado.

Vamos agora nos concentrar nesse terceiro ponto.

9.3. Quando o Python executa decoradores


Uma característica fundamental dos decoradores é serem executados logo após a função decorada ser definida. Isso
normalmente acontece no tempo de importação (isto é, quando um módulo é carregado pelo Python). Observe
registration.py no Exemplo 2.

Exemplo 2. O módulo registration.py

PY
registry = [] # (1)

def register(func): # (2)


print(f'running register({func})') # (3)
registry.append(func) # (4)
return func # (5)

@register # (6)
def f1():
print('running f1()')

@register
def f2():
print('running f2()')

def f3(): # (7)


print('running f3()')

def main(): # (8)


print('running main()')
print('registry ->', registry)
f1()
f2()
f3()

if __name__ == '__main__':
main() # (9)

1. registry vai manter referências para funções decoradas por @register .

2. register recebe uma função como argumento.


3. Exibe a função que está sendo decorada, para fins de demonstração.
4. Insere func em registry .

5. Devolve func : precisamos devolver uma função; aqui devolvemos a mesma função recebida como argumento.

6. f1 e f2 são decoradas por @register .

7. f3 não é decorada.
8. main mostra registry , depois chama f1() , f2() , e f3() .

9. main() só é invocada se registration.py for executado como um script.


O resultado da execução de registration.py se parece com isso:

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

Observe que register roda (duas vezes) antes de qualquer outra função no módulo. Quando register é chamada,
ela recebe o objeto função a ser decorado como argumento—por exemplo, <function f1 at 0x100631bf8> .

Após o carregamento do módulo, a lista registry contém referências para as duas funções decoradas: f1 e f2 . Essa
funções, bem como f3 , são executadas apenas quando chamadas explicitamente por main .

Se registration.py for importado (e não executado como um script), a saída é essa:

PYCON
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

Nesse momento, se você inspecionar registry , verá isso:

PYCON
>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

O ponto central do Exemplo 2 é enfatizar que decoradores de função são executados assim que o módulo é importado,
mas as funções decoradas só rodam quando são invocadas explicitamente. Isso ressalta a diferença entre o que
pythonistas chamam de tempo de importação e tempo de execução.

9.4. Decoradores de registro


Considerando a forma como decoradores são normalmente usados em código do mundo real, o Exemplo 2 é incomum
por duas razões:

A função do decorador é definida no mesmo módulo das funções decoradas. Em geral, um decorador real é definido
em um módulo e aplicado a funções de outros módulos.
O decorador register devolve a mesma função recebida como argumento. Na prática, a maior parte dos
decoradores define e devolve uma função interna.

Apesar do decorador register no Exemplo 2 devolver a função decorada inalterada, aquela técnica não é inútil.
Decoradores parecidos são usados por muitas frameworks Python para adicionar funções a um registro central—por
exemplo, um registro mapeando padrões de URLs para funções que geram respostas HTTP. Tais decoradores de
registro podem ou não modificar as funções decoradas.
Vamos ver um decorador de registro em ação na seção Seção 10.3 (do Capítulo 10).

A maioria dos decoradores modificam a função decorada. Eles normalmente fazem isso definindo e devolvendo uma
função interna para substituir a função decorada. E código que usa funções internas quase sempre depende de
clausuras para operar corretamente. Para entender as clausuras, precisamos dar um passo atrás e revisar como o
escopo de variáveis funciona no Python.

9.5. Regras de escopo de variáveis


No Exemplo 3, definimos e testamos uma função que lê duas variáveis: uma variável local a —definida como
parâmetro de função—e a variável b , que não é definida em lugar algum na função.

Exemplo 3. Função lendo uma variável local e uma variável global

PYCON
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined

O erro obtido não é surpreendente. Continuando do Exemplo 3, se atribuirmos um valor a um b global e então
chamarmos f1 , funciona:

PYCON
>>> b = 6
>>> f1(3)
3
6

Agora vamos ver um exemplo que pode ser surpreendente.

Dê uma olhada na função f2 , no Exemplo 4. As primeiras duas linhas são as mesmas da f1 do Exemplo 3, e então ela
faz uma atribuição a b . Mas para com um erro no segundo print , antes da atribuição ser executada.

Exemplo 4. A variável b é local, porque um valor é atribuído a ela no corpo da função

PYCON
>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
Observe que o a saída começa com 3 , provando que o comando print(a) foi executado. Mas o segundo, print(b) ,
nunca roda. Quando vi isso pela primeira vez me espantei, pensava que o 6 deveria ser exibido, pois há uma variável
global b , e a atribuição para a b local ocorre após print(b) .

Mas o fato é que, quando o Python compila o corpo da função, ele decide que b é uma variável local, por ser atribuída
dentro da função. O bytecode gerado reflete essa decisão, e tentará obter b no escopo local. Mais tarde, quando a
chamada f2(3) é realizada, o corpo de f2 obtém e exibe o valor da variável local a , mas ao tentar obter o valor da
variável local b , descobre que b não está vinculado a nada.

Isso não é um bug, mas uma escolha de projeto: o Python não exige que você declare variáveis, mas assume que uma
variável atribuída no corpo de uma função é local. Isso é muito melhor que o comportamento do Javascript, que
também não requer declarações de variáveis, mas se você esquecer de declarar uma variável como local (com var ),
pode acabar alterando uma variável global sem nem saber.

Se queremos que o interpretador trate b como uma variável global e também atribuir um novo valor a ela dentro da
função, usamos a declaração global :

PYCON
>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9

Nos exemplos anteriores, vimos dois escopos em ação:

O escopo global de módulo


Composto por nomes atribuídos a valores fora de qualquer bloco de classe ou função.

O escopo local da função f3


Composto por nomes atribuídos a valores como parâmetros, ou diretamente no corpo da função.

Há um outro escopo de onde variáveis podem vir, chamado nonlocal, e ele é fundamental para clausuras; vamos tratar
disso em breve.

Após ver mais de perto como o escopo de variáveis funciona no Python, podemos enfrentar as clasuras na próxima
seção, Seção 9.6. Se você tiver curiosidade sobre as diferenças no bytecode das funções no Exemplo 3 e no Exemplo 4,
veja o quadro a seguir.

Comparando bytecodes
O módulo dis module oferece uma forma fácil de descompilar o bytecode de funções do Python. Leia no
Exemplo 5 e no Exemplo 6 os bytecodes de f1 e f2 , do Exemplo 3 e do Exemplo 4, respectivamente.

Exemplo 5. Bytecode da função f1 do Exemplo 3


PY
>>> from dis import dis
>>> dis(f1)
2 0 LOAD_GLOBAL 0 (print) (1)
3 LOAD_FAST 0 (a) (2)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP

3 10 LOAD_GLOBAL 0 (print)
13 LOAD_GLOBAL 1 (b) (3)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
20 LOAD_CONST 0 (None)
23 RETURN_VALUE

1. Carrega o nome global print .

2. Carrega o nome local a.

3. Carrega o nome global b.

Compare o bytecode de f1 , visto no Exemplo 5 acima, com o bytecode de f2 no Exemplo 6.

Exemplo 6. Bytecode da função f2 do Exemplo 4

PY
>>> dis(f2)
2 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP

3 10 LOAD_GLOBAL 0 (print)
13 LOAD_FAST 1 (b) (1)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP

4 20 LOAD_CONST 1 (9)
23 STORE_FAST 1 (b)
26 LOAD_CONST 0 (None)
29 RETURN_VALUE

1. Carrega o nome local b . Isso mostra que o compilador considera b uma variável local, mesmo com uma
atribuição a b ocorrendo mais tarde, porque a natureza da variável—se ela é ou não local—não pode mudar
no corpo da função.

A máquina virtual (VM) do CPython que executa o bytecode é uma máquina de stack, então as operações LOAD e
POP se referem ao stack. A descrição mais detalhada dos opcodes do Python está além da finalidade desse livro,
mas eles estão documentados junto com o módulo, em "dis—Disassembler de bytecode do Python"
(https://docs.python.org/pt-br/3/library/dis.html).

9.6. Clausuras
Na blogosfera, as clausuras são algumas vezes confundidas com funções anônimas. Muita gente confunde os dois
conceitos por causa da história paralela dos dois recursos: definir funções dentro de outras funções não é tão comum
ou conveniente, até existirem funções anônimas. E clausuras só importam a partir do momento em que você tem
funções aninhadas. Daí que muitos aprendem as duas ideias ao mesmo tempo.
Na verdade, uma clausura é uma função—vamos chamá-la de f —com um escopo estendido, incorporando variáveis
referenciadas no corpo de f que não são nem variáveis globais nem variáveis locais de f . Tais variáveis devem vir do
escopo local de uma função externa que englobe f .

Não interessa aqui se a função é anônima ou não; o que importa é que ela pode acessar variáveis não-globais definidas
fora de seu corpo.

É um conceito difícil de entender, melhor ilustrado por um exemplo.

Imagine uma função avg , para calcular a média de uma série de valores que cresce continuamente; por exemplo, o
preço de fechamento de uma commodity através de toda a sua história. A cada dia, um novo preço é acrescentado, e a
média é computada levando em conta todos os preços até ali.

Começando do zero, avg poderia ser usada assim:

PYCON
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

Da onde vem avg , e onde ela mantém o histórico com os valores anteriores?

Para começar, o Exemplo 7 mostra uma implementação baseada em uma classe.

Exemplo 7. average_oo.py: uma classe para calcular uma média contínua

PYTHON3
class Averager():

def __init__(self):
self.series = []

def __call__(self, new_value):


self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)

A classe Averager cria instâncias invocáveis:

PYCON
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

O Exemplo 8, a seguir, é uma implementação funcional, usando a função de ordem superior make_averager .

Exemplo 8. average.py: uma função de ordem superior para a clacular uma média contínua
PYTHON3
def make_averager():
series = []

def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)

return averager

Quando invocada, make_averager devolve um objeto função averager . Cada vez que um averager é invocado, ele
insere o argumento recebido na série, e calcula a média atual, como mostra o Exemplo 9.

Exemplo 9. Testando o Exemplo 8

PYCON
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(15)
12.0

Note as semelhanças entre os dois exemplos: chamamos Averager() ou make_averager() para obter um objeto
invocável avg , que atualizará a série histórica e calculará a média atual. No Exemplo 7, avg é uma instância de
Averager , no Exemplo 8 é a função interna averager . Nos dois casos, basta chamar avg(n) para incluir n na série
e obter a média atualizada.

É óbvio onde o avg da classe Averager mantém o histórico: no atributo de instância self.series . Mas onde a
função avg no segundo exemplo encontra a series ?

Observe que series é uma variável local de make_averager , pois a atribuição series = [] acontece no corpo
daquela função. Mas quando avg(10) é chamada, make_averager já retornou, e seu escopo local há muito deixou de
existir.

Dentro de averager , series é uma variável livre. Esse é um termo técnico para designar uma variável que não está
vinculada no escopo local. Veja a Figura 1.
Figura 1. A clausura para averager estende o escopo daquela função para incluir a vinculação da variável livre
series .

Inspecionar o objeto averager devolvido mostra como o Python mantém os nomes de variáveis locais e livres no
atributo __code__ , que representa o corpo compilado da função. O Exemplo 10 demonstra isso.

Exemplo 10. Inspecionando a função criada por make_averager no Exemplo 8

PYCON
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

O valor de series é mantido no atributo __closure__ da função devolvida, avg . Cada item em avg.__closure__
corresponde a um nome em __code__ . Esses itens são cells , e tem um atributo chamado cell_contents , onde o
valor real pode ser encontrado. O Exemplo 11 mostra esses atributos.

Exemplo 11. Continuando do Exemplo 9

PYCON
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

Resumindo: uma clausura é uma função que retém os vínculos das variáveis livres que existem quando a função é
definida, de forma que elas possam ser usadas mais tarde, quando a função for invocada mas o escopo de sua definição
não estiver mais disponível.

Note que a única situação na qual uma função pode ter de lidar com variáveis externas não-globais é quando ela
estiver aninhada dentro de outra função, e aquelas variáveis sejam parte do escopo local da função externa.
9.7. A declaração nonlocal
Nossa implementação anterior de make_averager não era eficiente. No Exemplo 8, armazenamos todos os valores na
série histórica e calculamos sua sum cada vez que averager é invocada. Uma implementação melhor armazenaria
apenas o total e número de itens até aquele momento, e calcularia a média com esses dois números.

O Exemplo 12 é uma implementação errada, apenas para ilustrar um ponto. Você consegue ver onde o código quebra?

Exemplo 12. Um função de ordem superior incorreta para calcular um média contínua sem manter todo o histórico

PYTHON3
def make_averager():
count = 0
total = 0

def averager(new_value):
count += 1
total += new_value
return total / count

return averager

Se você testar o Exemplo 12, eis o resultado:

PYCON
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

O problema é que a instrução count += 1 significa o mesmo que count = count + 1 , quando count é um número
ou qualquer tipo imutável. Então estamos efetivamente atribuindo um valor a count no corpo de averager , e isso a
torna uma variável local. O mesmo problema afeta a variável total .

Não tivemos esse problema no Exemplo 8, porque nunca atribuimos nada ao nome series ; apenas chamamos
series.append e invocamos sum e len nele. Nos valemos, então, do fato de listas serem mutáveis.

Mas com tipos imutáveis, como números, strings, tuplas, etc., só é possível ler, nunca atualizar. Se você tentar revinculá-
las, como em count = count + 1 , estará criando implicitamente uma variável local count . Ela não será mais uma
variável livre, e assim não será armazenada na clausura.

A palavra reservada nonlocal foi introduzida no Python 3 para contornar esse problema. Ela permite declarar uma
variável como variável livre, mesmo quando ela for atribuída dentro da função. Se um novo valor é atribuído a uma
variável nonlocal , o vínculo armazenado na clausura é modificado. Uma implemetação correta da nossa última
versão de make_averager se pareceria com o Exemplo 13.

Exemplo 13. Calcula uma média contínua sem manter todo o histórico (corrigida com o uso de nonlocal )
PYTHON3
def make_averager():
count = 0
total = 0

def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count

return averager

Após estudar o nonlocal , podemos resumir como a consulta de variáveis funciona no Python.

9.7.1. A lógica da consulta de variáveis


Quando uma função é definida, o compilador de bytecode do Python determina como encontrar uma variável x que
aparece na função, baseado nas seguintes regras:[101]

Se há uma declaração global x , x vem de e é atribuída à variável global x do módulo.[102]


Se há uma declaração nonlocal x, x vem de e atribuída à variável local x na função circundante mais próxima
de onde x for definida.
Se x é um parâmetro ou tem um valor atribuído a si no corpo da função, então x é uma variável local.
Se x é referenciada mas não atribuída, e não é um parâmetro:
x será procurada nos escopos locais do corpos das funções circundantes (os escopos nonlocal).
Se x não for encontrada nos escopos circundantes, será lida do escopo global do módulo.
Se x não for encontrada no escopo global, será lida de __builtins__.__dict__ .

Tendo visto as clausuras do Python, podemos agora de fato implementar decoradores com funções aninhadas.

9.8. Implementando um decorador simples


O Exemplo 14 é um decorador que cronometra cada invocação da função decorada e exibe o tempo decorrido, os
argumentos passados, e o resultado da chamada.

Exemplo 14. clockdeco0.py: decorador simples que mostra o tempo de execução de funções

PYTHON3
import time

def clock(func):
def clocked(*args): # (1)
t0 = time.perf_counter()
result = func(*args) # (2)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked # (3)

1. Define a função interna clocked para aceitar qualquer número de argumentos posicionais.
2. Essa linha só funciona porque a clausura para clocked engloba a variável livre func .

3. Devolve a função interna para substituir a função decorada.


O Exemplo 15 demonstra o uso do decorador clock .

Exemplo 15. Usando o decorador clock

PYTHON3
import time
from clockdeco0 import clock

@clock
def snooze(seconds):
time.sleep(seconds)

@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)

if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

O resultado da execução do Exemplo 15 é o seguinte:

BASH
$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720

9.8.1. Como isso funciona


Lembre-se que esse código:

PYTHON3
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)

na verdade faz isso:

PYTHON3
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)

factorial = clock(factorial)

Então, nos dois exemplos, clock recebe a função factorial como seu argumento func (veja o Exemplo 14). Ela
então cria e devolve a função clocked , que o interpretador Python atribui a factorial (no primeiro exemplo, por
baixo dos panos). De fato, se você importar o módulo clockdeco_demo e verificar o __name__ de factorial , verá
isso:
PYCON
>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>

Então factorial agora mantém uma referência para a função clocked . Daqui por diante, cada vez que
factorial(n) for chamada, clocked(n) será executada. Essencialmente, clocked faz o seguinte:

1. Registra o tempo inicial t0 .

2. Chama a função factorial original, salvando o resultado.


3. Computa o tempo decorrido.
4. Formata e exibe os dados coletados.
5. Devolve o resultado salvo no passo 2.

Esse é o comportamento típico de um decorador: ele substitui a função decorada com uma nova função que aceita os
mesmos argumentos e (normalmente) devolve o que quer que a função decorada deveria devolver, enquanto realiza
também algum processamento adicional.

Em Padrões de Projetos, de Gamma et al., a descrição curta do padrão decorador começa com:
"Atribui dinamicamente responsabilidades adicionais a um objeto." Decoradores de função se
👉 DICA encaixam nessa descrição. Mas, no nível da implementação, os decoradores do Python guardam
pouca semelhança com o decorador clássico descrito no Padrões de Projetos original. O Ponto de
vista fala um pouco mais sobre esse assunto.

O decorador clock implementado no Exemplo 14 tem alguns defeitos: ele não suporta argumentos nomeados, e
encobre o __name__ e o __doc__ da função decorada. O Exemplo 16 usa o decorador functools.wraps para copiar
os atributos relevantes de func para clocked . E nessa nova versão os argumentos nomeados também são tratados
corretamente.

Exemplo 16. clockdeco.py: um decorador clock melhora

PYTHON3
import time
import functools

def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = [repr(arg) for arg in args]
arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
arg_str = ', '.join(arg_lst)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked

O functools.wraps é apenas um dos decoradores prontos para uso da biblioteca padrão. Na próxima seção veremos
o decorador mais impressionante oferecido por functools : cache .
9.9. Decoradores na biblioteca padrão
O Python tem três funções embutidas projetadas para decorar métodos: property , classmethod e staticmethod .
Vamos discutir property na seção Seção 22.4 e os outros na seção Seção 11.5.

No Exemplo 16 vimos outro decorador importante: functools.wraps , um auxiliar na criação de decoradores bem
comportados. Três dos decoradores mais interessantes da biblioteca padrão são cache , lru_cache e
singledispatch —todos do módulo functools . Falaremos deles a seguir.

9.9.1. Memoização com functools.cache


O decorador functools.cache implementa memoização:[103] uma técnica de otimização que funciona salvando os
resultados de invocações anteriores de uma função dispendiosa, evitando repetir o processamento para argumentos
previamente utilizados.

O functools.cache foi introduzido no Python 3.9. Se você precisar rodar esses exemplo no Python
👉 DICA 3.8, substitua @cache por @lru_cache . Em versões anteriores é preciso invocar o decorador,
escrevendo @lru_cache() , como explicado na seção Seção 9.9.2.

Uma boa demonstração é aplicar @cache à função recursiva, e dolorosamente lenta, que gera o enésimo número da
sequência de Fibonacci, como mostra o Exemplo 17.

Exemplo 17. O modo recursivo e extremamente dispendioso de calcular o enésimo número na série de Fibonacci

PYTHON3
from clockdeco import clock

@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
print(fibonacci(6))

Aqui está o resultado da execução de fibo_demo.py. Exceto pela última linha, toda a saída é produzida pelo decorador
clock :
TEXT
$ python3 fibo_demo.py
[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
[0.00000035s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001084s] fibonacci(2) -> 1
[0.00002074s] fibonacci(3) -> 2
[0.00009189s] fibonacci(4) -> 3
[0.00000029s] fibonacci(1) -> 1
[0.00000027s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000959s] fibonacci(2) -> 1
[0.00001905s] fibonacci(3) -> 2
[0.00000026s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000997s] fibonacci(2) -> 1
[0.00000028s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000031s] fibonacci(1) -> 1
[0.00001019s] fibonacci(2) -> 1
[0.00001967s] fibonacci(3) -> 2
[0.00003876s] fibonacci(4) -> 3
[0.00006670s] fibonacci(5) -> 5
[0.00016852s] fibonacci(6) -> 8
8

O desperdício é óbvio: fibonacci(1) é chamada oito vezes, fibonacci(2) cinco vezes, etc. Mas acrescentar apenas
duas linhas, para usar cache , melhora muito o desempenho. Veja o Exemplo 18.

Exemplo 18. Implementação mais rápida, usando caching

PYTHON3
import functools

from clockdeco import clock

@functools.cache # (1)
@clock # (2)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
print(fibonacci(6))

1. Essa linha funciona com Python 3.9 ou posterior. Veja a seção Seção 9.9.2 para uma alternativa que suporta versões
anteriores do Python.
2. Esse é um exemplo de decoradores empilhados: @cache é aplicado à função devolvida por @clock .
Decoradore empilhados
Para entender os decoradores empilahdos, lembre-se que a @ é açúcar sintático para indicar a
aplicação da função decoradora à função abaixo dela. Se houver mais de um decorador, eles se
comportam como chamadas a funções aninhadas. Isso:

PYTHON
@alpha
@beta

👉 DICA
def my_fn():
...

é o mesmo que isso:

PYTHON
my_fn = alpha(beta(my_fn))

Em outras palavras, o decorador beta é aplicado primeiro, e a função devolvida por ele é então
passada para alpha .

Usando o cache no Exemplo 18, a função fibonacci é chamada apenas uma vez para cada valor de n:

TEXT
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8

Em outro teste, para calcular fibonacci(30) , o Exemplo 18 fez as 31 chamadas necessárias em 0,00017s (tempo total),
enquanto o Exemplo 17 sem cache, demorou 12,09s em um notebook Intel Core i7, porque chamou fibonacci(1)
832.040 vezes, para um total de 2.692.537 chamadas.

Todos os argumentos recebidos pela função decorada devem ser hashable, pois o lru_cache subjacente usa um dict
para armazenar os resultados, e as chaves são criadas a partir dos argumentos posicionais e nomeados usados nas
chamados.

Além de tornar viáveis esses algoritmos recursivos tolos, @cache brilha de verdade em aplicações que precisam
buscar informações de APIs remotas.

O functools.cache pode consumir toda a memória disponível, se houver um número muito


⚠️ AVISO grande de itens no cache. Eu o considero mais adequado para scripts rápidos de linha de comando.
Para processos de longa duração, recomendo usar functools.lru_cache com um parâmetro
maxsize adequado, como explicado na próxima seção.

9.9.2. Usando o lru_cache


O decorador functools.cacheé, na realidade, um mero invólucro em torno da antiga função
functools.lru_cache , que é mais flexível e também compatível com o Python 3.8 e outras versões anteriores.
A maior vantagem de @lru_cache é a possibilidade de limitar seu uso de memória através do parâmetro maxsize ,
que tem um default bastante conservador de 128—significando que o cache pode manter no máximo 128 registros
simultâneos.

LRU é a sigla de Least Recently Used (NT: literalmente "Usado Menos Recentemente"), significando que registros mais
antigos, que há algum tempo não são lidos, são descartados para dar lugar a novos itens.

Desde o Python 3.8, lru_cache pode ser aplicado de duas formas. Abaixo vemos o modo mais simples em uso:

PYTHON3
@lru_cache
def costly_function(a, b):
...

A outra forma—disponível desde o Python 3.2—é invocá-lo como uma função, com () :

PYTHON3
@lru_cache()
def costly_function(a, b):
...

Nos dois casos, os parâmetros default seriam utilizados. São eles:

maxsize=128
Estabelece o número máximo de registros a serem armazenados. Após o cache estar cheio, o registro menos
recentemente usado é descartado, para dar lugar a cada novo item. Para um desempenho ótimo, maxsize deve ser
uma potência de 2. Se você passar maxsize=None , a lógica LRU é desabilitada e o cache funciona mais rápido, mas
os itens nunca são descartados, podendo levar a um consumo excessivo de memória. É assim que o
@functools.cache funciona.

typed=False

Determina se os resultados de diferentes tipos de argumentos devem ser armazenados separadamente, Por exemplo,
na configuração default, argumentos inteiros e de ponto flutuante considerados iguais são armazenados apenas uma
vez. Assim, haverá apenas uma entrada para as chamadas f(1) e f(1.0) . Se typed=True , aqueles argumentos
produziriam registros diferentes, possivelmente armazenando resultados distintos.

Eis um exemplo de invocação de @lru_cache com parâmetros diferentes dos defaults:

PYTHON3
@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
...

Vamos agora examinar outro decorador poderoso: functools.singledispatch .

9.9.3. Funções genéricas com despacho único


Imagine que estamos criando uma ferramenta para depurar aplicações web. Queremos gerar código HTML para tipos
diferentes de objetos Python.

Poderíamos começar com uma função como essa:


PYTHON3
import html

def htmlize(obj):
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'

Isso funcionará para qualquer tipo do Python, mas agora queremos estender a função para gerar HTML específico para
determinados tipos. Alguns exemplos seriam:

str

Substituir os caracteres de mudança de linha na string por '<br/>\n' e usar tags <p> tags em vez de <pre> .

int

Mostrar o número em formato decimal e hexadecimal (com um caso especial para bool ).

list
Gerar uma lista em HTML, formatando cada item de acordo com seu tipo.

float e Decimal
Mostrar o valor como de costume, mas também na forma de fração (por que não?).

O comportamento que desejamos aparece no Exemplo 19.

Exemplo 19. htmlize() gera HTML adaptado para diferentes tipos de objetos

PYCON
>>> htmlize({1, 2, 3}) # (1)
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre>&lt;built-in function abs&gt;</pre>'
>>> htmlize('Heimlich & Co.\n- a game') # (2)
'<p>Heimlich &amp; Co.<br/>\n- a game</p>'
>>> htmlize(42) # (3)
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}])) # (4)
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
>>> htmlize(True) # (5)
'<pre>True</pre>'
>>> htmlize(fractions.Fraction(2, 3)) # (6)
'<pre>2/3</pre>'
>>> htmlize(2/3) # (7)
'<pre>0.6666666666666666 (2/3)</pre>'
>>> htmlize(decimal.Decimal('0.02380952'))
'<pre>0.02380952 (1/42)</pre>'

1. A função original é registrada para object , então ela serve para capturar e tratar todos os tipos de argumentos
que não foram capturados pelas outras implementações.
2. Objetos str também passam por escape de HTML, mas são cercados por <p></p> , com quebras de linha <br/>
inseridas antes de cada '\n' .
3. Um int é exibido nos formatos decimal e hexadecimal, dentro de um bloco <pre></pre> .

4. Cada item na lista é formatado de acordo com seu tipo, e a sequência inteira é apresentada como uma lista HTML.
5. Apesar de ser um subtipo de int , bool recebe um tratamento especial.
6. Mostra Fraction como uma fração.
7. Mostra float e Decimal com a fração equivalemte aproximada.

Despacho único de funções


Como não temos no Python a sobrecarga de métodos ao estilo do Java, não podemos simplesmente criar variações de
htmlize com assinaturas diferentes para cada tipo de dado que queremos tratar de forma distinta. Uma solução
possível em Python seria transformar htmlize em uma função de despacho, com uma cadeia de if/elif/… ou
match/case/… chamando funções especializadas como htmlize_str , htmlize_int , etc. Isso não é extensível pelos
usuários de nosso módulo, e é desajeitado: com o tempo, a despachante htmlize de tornaria grande demais, e o
acoplamento entre ela e as funções especializadas seria excessivamente sólido.

O decorador functools.singledispatch permite que diferentes módulos contribuam para a solução geral, e que
você forneça facilmente funções especializadas, mesmo para tipos pertencentes a pacotes externos que não possam ser
editados. Se você decorar um função simples com @singledispatch , ela se torna o ponto de entrada para uma função
genérica: Um grupo de funções que executam a mesma operação de formas diferentes, dependendo do tipo do
primeiro argumento. É isso que signifca o termo despacho único. Se mais argumentos fossem usados para selecionar a
função específica, teríamos um despacho múltiplo. O Exemplo 20 mostra como funciona.

functools.singledispatch existe desde o python 3.4, mas só passou a suportar dicas de tipo no
⚠️ AVISO Python 3.7. As últimas duas funções no Exemplo 20 ilustram a sintaxe que funciona em todas as
versões do Python desde a 3.4.

Exemplo 20. @singledispatch cria uma @htmlize.register personalizada, para empacotar várias funções em uma
função genérica
PY
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers

@singledispatch # (1)
def htmlize(obj: object) -> str:
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'

@htmlize.register # (2)
def _(text: str) -> str: # (3)
content = html.escape(text).replace('\n', '<br/>\n')
return f'<p>{content}</p>'

@htmlize.register # (4)
def _(seq: abc.Sequence) -> str:
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'

@htmlize.register # (5)
def _(n: numbers.Integral) -> str:
return f'<pre>{n} (0x{n:x})</pre>'

@htmlize.register # (6)
def _(n: bool) -> str:
return f'<pre>{n}</pre>'

@htmlize.register(fractions.Fraction) # (7)
def _(x) -> str:
frac = fractions.Fraction(x)
return f'<pre>{frac.numerator}/{frac.denominator}</pre>'

@htmlize.register(decimal.Decimal) # (8)
@htmlize.register(float)
def _(x) -> str:
frac = fractions.Fraction(x).limit_denominator()
return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'

1. @singledispatch marca a função base, que trata o tipo object .

2. Cada função especializada é decorada com @«base».register .

3. O tipo do primeiro argumento passado durante a execução determina quando essa definição de função em
particular será utilizada. O nome das funções especilizadas é irrelevante; _ é uma boa escolha para deixar isso
claro.[104]
4. Registra uma nova função para cada tipo que precisa de tratamento especial, com uma dica de tipo correspondente
no primeiro parâmetro.
5. As ABCs em numbers são úteis para uso em conjunto com singledispatch .[105]

6. bool é um subtipo-de numbers.Integral , mas a lógica de singledispatch busca a implementação com o tipo
correspondente mais específico, independente da ordem na qual eles aparecem no código.
7. Se você não quiser ou não puder adicionar dicas de tipo à função decorada, você pode passar o tipo para o
decorador @«base».register . Essa sintaxe funciona em Python 3.4 ou posterior.
8. O decorador @«base».register devolve a função sem decoração, então é possível empilhá-los para registrar dois
ou mais tipos na mesma implementação.[106]
Sempre que possível, registre as funções especializadas para tratar ABCs (classes abstratas), tais como
numbers.Integral e abc.MutableSequence , ao invés das implementações concretas como int e list . Isso
permite ao seu código suportar uma variedade maior de tipos compatíveis. Por exemplo, uma extensão do Python pode
fornecer alternativas para o tipo int com número fixo de bits como subclasses de numbers.Integral .[107]

Usar ABCs ou typing.Protocol com @singledispatch permite que seu código suporte classes
👉 DICA existentes ou futuras que sejam subclasses reais ou virtuais daquelas ABCs, ou que implementem
aqueles protocolos. O uso de ABCs e o conceito de uma subclasse virtual são assuntos do Capítulo 13.

Uma qualidade notável do mecanismo de singledispatch é que você pode registrar funções especializadas em
qualquer lugar do sistema, em qualquer módulo. Se mais tarde você adicionar um módulo com um novo tipo definido
pelo usuário, é fácil acrescentar uma nova função específica para tratar aquele tipo. E é possível também escrever
funções personalizadas para classes que você não escreveu e não pode modificar.

O singledispatch foi uma adição muito bem pensada à biblioteca padrão, e oferece muitos outros recursos que não
me cabe descrever aqui. Uma boa referência é a PEP 443—​Single-dispatch generic functions (https://fpy.li/pep443) (EN)
mas ela não menciona o uso de dicas de tipo, acrescentado posteriormente. A documentação do módulo functools foi
aperfeiçoada e oferece um tratamento mais atualizado, com vários exemplos na seção referente ao singledispatch
(https://docs.python.org/pt-br/3/library/functools.html#functools.singledispatch) (EN).

O @singledispatch não foi criado para trazer para o Python a sobrecarga de métodos no estilo do
Java. Uma classe única com muitas variações sobrecarregadas de um método é melhor que uma
única função com uma longa sequênca de blocos if/elif/elif/elif . Mas as duas soluções são

✒️ NOTA incorretas, pois concentram responsabilidade excessiva em uma única unidade de código—a classe
ou a função. A vantagem de @singledispatch é seu suporte à extensão modular: cada módulo
pode registrar uma função especializada para cada tipo suportado. Em um caso de uso realista, as
implementações das funções genéricas não estariam todas no mesmo módulo, como ocorre no
Exemplo 20.

Vimos alguns decoradores recebendo argumentos, por exemplo @lru_cache() e o htmlize.register(float)


criado por @singledispatch no Exemplo 20. A próxima seção mostra como criar decoradores que aceitam
parâmetros.

9.10. Decoradores parametrizados


Ao analisar um decorador no código-fonte, o Python passa a função decorada como primeiro argumento para a função
do decorador. Mas como fazemos um decorador aceitar outros argumentos? A resposta é: criar uma fábrica de
decoradores que recebe aqueles argumentos e devolve um decorador, que é então aplicado à função a ser decorada.
Confuso? Com certeza. Vamos começar com um exemplo baseado no decorador mais simples que vimos: register no
Exemplo 21.

Exemplo 21. O módulo registration.py resumido, do Exemplo 2, repetido aqui por conveniência
PY
registry = []

def register(func):
print(f'running register({func})')
registry.append(func)
return func

@register
def f1():
print('running f1()')

print('running main()')
print('registry ->', registry)
f1()

9.10.1. Um decorador de registro parametrizado


Para tornar mais fácil a habilitação ou desabilitação do registro executado por register , faremos esse último aceitar
um parâmetro opcional active que, se False , não registra da função decorada. Conceitualmente, a nova função
register não é um decorador mas uma fábrica de decoradores. Quando chamada, ela devolve o decorador que será
realmente aplicado à função alvo.

Exemplo 22. Para aceitar parâmetros, o novo decorador register precisa ser invocado como uma função

PY
registry = set() # (1)

def register(active=True): # (2)


def decorate(func): # (3)
print('running register'
f'(active={active})->decorate({func})')
if active: # (4)
registry.add(func)
else:
registry.discard(func) # (5)

return func # (6)


return decorate # (7)

@register(active=False) # (8)
def f1():
print('running f1()')

@register() # (9)
def f2():
print('running f2()')

def f3():
print('running f3()')

1. registry é agora um set , tornando mais rápido acrescentar ou remover funções.

2. register recebe um argumento nomeado opcional.


3. A função interna decorate é o verdadeiro decorador; observe como ela aceita uma função como argumento.
4. Registra func apenas se o argumento active (obtido da clausura) for True .

5. Se not active e func in registry , remove a função.

6. Como decorate é um decorador, ele deve devolver uma função.


7. register é nossa fábrica de decoradores, então devolve decorate .

8. A fábrica @register precisa ser invocada como uma função, com os parâmetros desejados.
9. Mesmo se nenhum parâmetro for passado, ainda assim register deve ser chamado como uma função—
@register() —isto é, para devolver o verdadeiro decorador, decorate .
O ponto central aqui é que register() devolve decorate , que então é aplicado à função decorada.

O código do Exemplo 22 está em um módulo registration_param.py. Se o importarmos, veremos o seguinte:

PYCON
>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x10063c1e0>)
running register(active=True)->decorate(<function f2 at 0x10063c268>)
>>> registration_param.registry
[<function f2 at 0x10063c268>]

Veja como apenas a função f2 aparece no registry ; f1 não aparece porque active=False foi passado para a
fábrica de decoradores register , então o decorate aplicado a f1 não adiciona essa função a registry .

Se, ao invés de usar a sintaxe @ , usarmos register como uma função regular, a sintaxe necessária para decorar uma
função f seria register()(f) , para inserir f ao registry , ou register(active=False)(f) , para não inserí-la
(ou removê-la). Veja o Exemplo 23 para uma demonstração da adição e remoção de funções do registry .

Exemplo 23. Usando o módulo registration_param listado no Exemplo 22

PYCON
>>> from registration_param import *
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry # (1)
{<function f2 at 0x10073c268>}
>>> register()(f3) # (2)
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry # (3)
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2) # (4)
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry # (5)
{<function f3 at 0x10073c158>}

1. Quando o módulo é importado, f2 é inserida no registry .

2. A expressão register() devolve decorate , que então é aplicado a f3 .

3. A linha anterior adicionou f3 ao registry .

4. Essa chamada remove f2 do registry .

5. Confirma que apenas f3 permanece no registry .

O funcionamento de decoradores parametrizados é bastante complexo, e esse que acabamos de discutir é mais simples
que a maioria. Decoradores parametrizados em geral substituem a função decorada, e sua construção exige um nível
adicional de aninhamento. Vamos agora explorar a arquitetura de uma dessas pirâmides de funções.

9.10.2. Um decorador parametrizado de cronometragem


Nessa seção vamos revisitar o decorador clock , acrescentando um recurso: os usuários podem passar uma string de
formatação, para controlar a saída do relatório sobre função cronometrada. Veja o Exemplo 24.

Para simplificar, o Exemplo 24 está baseado na implementação inicial de clock no Exemplo 14, e
✒️ NOTA não na versão aperfeiçoada do Exemplo 16 que usa @functools.wraps , acrescentando assim mais
uma camada de função.

Exemplo 24. Módulo clockdeco_param.py: o decorador clock parametrizado

PY
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT): # (1)


def decorate(func): # (2)
def clocked(*_args): # (3)
t0 = time.perf_counter()
_result = func(*_args) # (4)
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # (5)
result = repr(_result) # (6)
print(fmt.format(**locals())) # (7)
return _result # (8)
return clocked # (9)
return decorate # (10)

if __name__ == '__main__':

@clock() # (11)
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)

1. clock é a nossa fábrica de decoradores parametrizados.


2. decorate é o verdadeiro decorador.
3. clocked envolve a função decorada.
4. _result é o resultado efetivo da função decorada.
5. _args mantém os verdadeiros argumentos de clocked , enquanto args éa str usada para exibição.
6. result éa str que representa _result , para exibição.

7. Usar **locals() aqui permite que qualquer variável local de clocked seja referenciada em fmt .[108]

8. clocked vai substituir a função decorada, então ela deve devolver o mesmo que aquela função devolve.
9. decorate devolve clocked .

10. clock devolve decorate .

11. Nesse auto-teste, clock() é chamado sem argumentos, então o decorador aplicado usará o formato default, str .

Se você rodar o Exemplo 24 no console, o resultado é o seguinte:


BASH
$ python3 clockdeco_param.py
[0.12412500s] snooze(0.123) -> None
[0.12411904s] snooze(0.123) -> None
[0.12410498s] snooze(0.123) -> None

Para exercitar a nova funcionalidade, vamos dar uma olhada em dois outros módulos que usam o clockdeco_param ,
o Exemplo 25 e o Exemplo 26, e nas saídas que eles geram.

Exemplo 25. clockdeco_param_demo1.py

PYTHON3
import time
from clockdeco_param import clock

@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)

Saída do Exemplo 25:

BASH
$ python3 clockdeco_param_demo1.py
snooze: 0.12414693832397461s
snooze: 0.1241159439086914s
snooze: 0.12412118911743164s

Exemplo 26. clockdeco_param_demo2.py

PYTHON3
import time
from clockdeco_param import clock

@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)

Saída do Exemplo 26:

BASH
$ python3 clockdeco_param_demo2.py
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s

Lennart Regebro—um dos revisores técnicos da primeira edição—argumenta seria melhor


programar decoradores como classes implementando __call__ , e não como funções (caso dos

✒️ NOTA exemplos nesse capítulo). Concordo que aquela abordagem é melhor para decoradores não-triviais.
Mas para explicar a ideia básica desse recurso da linguagem, funções são mais fáceis de entender.
Para técnicas robustas de criação de decoradores, veja as referências na Seção 9.12, especialmente o
blog de Graham Dumpleton e o módulo wrapt , .
A próxima seção traz um exemplo no estilo recomendado por Regebro e Dumpleton.

9.10.3. Um decorador de cronometragem em forma de classe


Como um último exemplo, o Exemplo 27 mostra a implementação de um decorador parametrizado clock ,
programado como uma classe com __call__ . Compare o Exemplo 24 com o Exemplo 27. Qual você prefere?

Exemplo 27. Módulo clockdeco_cls.py: decorador parametrizado clock , implementado como uma classe

PY
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

class clock: # (1)

def __init__(self, fmt=DEFAULT_FMT): # (2)


self.fmt = fmt

def __call__(self, func): # (3)


def clocked(*_args):
t0 = time.perf_counter()
_result = func(*_args) # (4)
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(self.fmt.format(**locals()))
return _result
return clocked

1. Ao invés de uma função externa clock , a classe clock é nossa fábrica de decoradores parametrizados. A nomeei
com um c minúsculo, para deixar claro que essa implementação é uma substituta direta para aquela no Exemplo
24.
2. O argumento passado em clock(my_format) é atribuído ao parâmetro fmt aqui. O construtor da classe devolve
uma instância de clock , com my_format armazenado em self.fmt .
3. __call__ torna a instância de clock invocável. Quando chamada, a instância substitui a função decorada com
clocked .

4. clocked envolve a função decorada.

Isso encerra nossa exploração dos decoradores de função. Veremos os decoradores de classe no [class_metaprog].

9.11. Resumo do capítulo


Percorremos um terreno acidentado nesse capítulo. Tentei tornar a jornada tão suave quanto possível, mas entramos
definitivamente nos domínios da meta-programação.

Partimos de um decorador simples @register , sem uma função interna, e terminamos com um @clock()
parametrizado envolvendo dois níveis de funções aninhadas.

Decoradores de registro, apesar de serem essencialmente simples, tem aplicações reais nas frameworks Python. Vamos
aplicar a ideia de registro em uma implementação do padrão de projeto Estratégia, no Capítulo 10.

Entender como os decoradores realmente funcionam exigiu falar da diferença entre tempo de importação e tempo de
execução. Então mergulhamos no escopo de variáveis, clausuras e a nova declaração nonlocal . Dominar as clausuras
e nonlocal é valioso não apenas para criar decoradores, mas também para escrever programas orientados a eventos
para GUIs ou E/S assíncrona com callbacks, e para adotar um estilo funcional quando fizer sentido.
Decoradores parametrizados quase sempre implicam em pelo menos dois níveis de funções aninhadas, talvez mais se
você quiser usar @functools.wraps , e produzir um decorador com um suporte melhor a técnicas mais avançadas.
Uma dessas técnicas é o empilhamento de decoradores, que vimos no Exemplo 18. Para decoradores mais sofisticados,
uma implementação baseda em classes pode ser mais fácil de ler e manter.

Como exemplos de decoradores parametrizados na biblioteca padrão, visitamos os poderosos @cache e


@singledispatch , do módulo functools .

9.12. Leitura complementar


O item #26 do livro Effective Python, 2nd ed. (https://fpy.li/effectpy) (EN) (Addison-Wesley), de Brett Slatkin, trata das
melhores práticas para decoradores de função, e recomenda sempre usar functools.wraps —que vimos no Exemplo
16.[109]

Graham Dumpleton tem, em seu blog, uma série de posts abrangentes (https://fpy.li/9-5) (EN) sobre técnicas para
implementar decoradores bem comportados, começando com "How you implemented your Python decorator is
wrong" (A forma como você implementou seu decorador em Python está errada) (https://fpy.li/9-6). Seus conhecimentos
profundos sobre o assunto também estão muito bem demonstrados no módulo wrapt (https://fpy.li/9-7), que Dumpleton
escreveu para simplificar a implementação de decoradores e invólucros (wrappers) dinâmicos de função, que
suportam introspecção e se comportam de forma correta quando decorados novamente, quando aplicados a métodos e
quando usados como descritores de atributos. O [attribute_descriptors] na [classes_protocols_part] é sobre descritores.

"Metaprogramming" (Metaprogramação) (https://fpy.li/9-8) (EN), o capítulo 9 do Python Cookbook, 3ª ed. de David Beazley
e Brian K. Jones (O’Reilly), tem várias receitas ilustrando desde decoradores elementares até alguns muito sofisticados,
incluindo um que pode ser invocado como um decorador regular ou como uma fábrica de decoradores, por exemplo,
@clock ou @clock() . É a "Recipe 9.6. Defining a Decorator That Takes an Optional Argument" (Receita 9.6. Definindo
um Decorador Que Recebe um Argumento Opcional) desse livro de receitas.

Michele Simionato criou um pacote com objetivo de "simplificar o uso de decoradores para o programador comum, e
popularizar os decoradores através da apresentação de vários exemplos não-triviais", de acordo com a documentação.
Ele está disponível no PyPI, em decorator package (pacote decorador) (https://fpy.li/9-9) (EN).

Criada quando os decoradores ainda eram um recurso novo no Python, a página wiki Python Decorator Library
(https://fpy.li/9-10) (EN) tem dezenas de exemplos. Como começou há muitos anos, algumas das técnicas apresentadas
foram suplantadas, mas ela ainda é uma excelente fonte de inspiração.

"Closures in Python" (Clausuras em Python) (https://fpy.li/9-11) (EN) é um post de blog curto de Fredrik Lundh, explicando
a terminologia das clausuras.

A PEP 3104—​Access to Names in Outer Scopes (Acesso a Nomes em Escopos Externos) (https://fpy.li/9-12) (EN) descreve a
introdução da declaração nonlocal , para permitir a re-vinculação de nomes que não são nem locais nem globais. Ela
também inclui uma excelente revisão de como essa questão foi resolvida em outras linguagens dinâmicas (Perl, Ruby,
JavaScript, etc.) e os prós e contras das opções de design disponíveis para o Python.

Em um nível mais teórico, a PEP 227—​Statically Nested Scopes (Escopos Estaticamente Aninhados) (https://fpy.li/9-13) (EN)
documenta a introdução do escopo léxico como um opção no Python 2.1 e como padrão no Python 2.2, explicando a
justificativa e as opções de design para a implementação de clausuras no Python.

A PEP 443 (https://fpy.li/9-14) (EN) traz a justificativa e uma descrição detalhada do mecanismo de funções genéricas de
despacho único. Um antigo (março de 2005) post de blog de Guido van Rossum, "Five-Minute Multimethods in Python"
(Multi-métodos em Python em Cinco Minutos) (https://fpy.li/9-15) (EN), mostra os passos para uma implementação de
funcões genéricas (também chamadas multi-métodos) usando decoradores. O código de multi-métodos de Guido é
interessante, mas é um exemplo didático. Para ver uma implementação moderna e pronta para ser usada em produção
de funções genéricas de despacho múltiplo, veja a Reg (https://fpy.li/9-16) de Martijn Faassen–autor da Morepath
(https://fpy.li/9-17), uma framework web guiada por modelos e compatível com REST.

Ponto de vista
Escopo dinâmico versus escopo léxico

O projetista de qualquer linguagem que contenha funções de primeira classe se depara com essa questão: sendo
um objeto de primeira classe, uma função é definida dentro de um determinado escopo, mas pode ser invocada
em outros escopos. A pergunta é: como avaliar as variáveis livres? A resposta inicial e mais simples é "escopo
dinâmico". Isso significa que variáveis livres são avaliadas olhando para dentro do ambiente onde a funcão é
invocada.

Se o Python tivesse escopo dinâmico e não tivesse clausuras, poderíamos improvisar avg —similar ao Exemplo 8
—assim:

PYCON
>>> ### this is not a real Python console session! ###
>>> avg = make_averager()
>>> series = [] # (1)
>>> avg(10)
10.0
>>> avg(11) # (2)
10.5
>>> avg(12)
11.0
>>> series = [1] # (3)
>>> avg(5)
3.0

1. Antes de usar avg , precisamos definir por nós mesmos series = [] , então precisamos saber que
averager (dentro de make_averager ) se refere a uma lista chamada series .

2. Por trás da cortina, series acumula os valores cuja média será calculada.
3. Quando series = [1] é executada, a lista anterior é perdida. Isso poderia ocorrer por acidente, ao se tratar
duas médias continuas independentes ao mesmo tempo.

Funções deveriam ser opacas, sua implementação invisível para os usuários. Mas com escopo dinâmico, se a
função usa variáveis livres, o programador precisa saber do funcionamento interno da função, para ser capaz de
configurar um ambiente onde ela execute corretamente. Após anos lutando com a linguagem de preparação de
documentos LaTeX, o excelente livro Practical LaTeX (LaTeX Prático), de George Grätzer (Springer), me ensinou
que as variáveis no LaTeX usam escopo dinâmico. Por isso me confundiam tanto!

O Lisp do Emacs também usa escopo dinâmico, pelo menos como default. Veja "Dynamic Binding" (Vinculação
Dinâmica) (https://fpy.li/9-18) no manual do Emacs para uma breve explicação.

O escopo dinâmico é mais fácil de implementar, e essa foi provavelmente a razão de John McCarthy ter tomado
esse caminho quando criou o Lisp, a primeira linguagem a ter funções de primeira classe. O texto de Paul
Graham, "The Roots of Lisp" (As Raízes do Lisp) (https://fpy.li/9-19) é uma explicação acessível do artigo original de
John McCarthy sobre a linguagem Lisp, "Recursive Functions of Symbolic Expressions and Their Computation by
Machine, Part I" (Funções Recursivas de Expressões Simbólicas e Sua Computação via Máquina) (https://fpy.li/9-20). O
artigo de McCarthy é uma obra prima no nível da Nona Sinfonia de Beethoven. Paul Graham o traduziu para o
resto de nós, da matemática para o inglês e o código executável.
O comentário de Paul Graham explica como o escopo dinâmico é complexo. Citando o "The Roots of Lisp":

“ Éexemplo
um testemunho eloquente dos perigos do escopo dinâmico, que mesmo o primeiro
de funções de ordem superior em Lisp estivesse errado por causa dele. Talvez, em
1960, McCarthy não estivesse inteiramente ciente das implicações do escopo dinâmico, que
continuou presente nas implementações de Lisp por um tempo surpreendentemente longo—
até Sussman e Steele desenvolverem o Scheme, em 1975. O escopo léxico não complica
demais a definição de eval , mas pode tornar mais difícil escrever compiladores.

Hoje em dia o escopo léxico é o padrão: variáveis livres são avaliadas considerando o ambiente onde a função foi
definida. O escopo léxico complica a implementação de linguagens com funções de primeira classe, pois requer o
suporte a clausuras. Por outro lado, o escopo léxico torna o código-fonte mais fácil de ler. A maioria das
linguagens inventadas desde o Algol tem escopo léxico. Uma exceção notável é o JavaScript, onde a variável
especial this é confusa, pois pode ter escopo léxico ou dinâmico, dependendo da forma como o código for
escrito (https://fpy.li/9-21) (EN).

Por muitos anos, o lambda do Python não ofereceu clausuras, contribuindo para a má fama deste recurso entre
os fãs da programação funcional na blogosfera. Isso foi resolvido no Python 2.2 (de dezembro de 2001), mas a
blogosfera tem uma memória muito boa. Desde então, lambda é embaraçoso apenas por sua sintaxe limitada.

O decoradores do Python e o padrão de projeto Decorador

Os decoradores de função do Python se encaixam na descrição geral dos decoradores de Gamma et al. em
Padrões de Projeto: "Acrescenta responsabilidades adicionais a um objeto de forma dinâmica. Decoradores
fornecem uma alternativa flexível à criação de subclasses para estender funcionalidade."

Ao nível da implementação, os decoradores do Python não lembram o padrão de projeto decorador clássico, mas
é possível fazer uma analogia.

No padrão de projeto, Decorador e Componente são classes abstratas. Uma instância de um decorador concreto
envolve uma instância de um componente concreto para adicionar comportamentos a ela. Citando Padrões de
Projeto:

“ Otransparente
decorador se adapta à interface do componente decorado, assim sua presença é
para os clientes do componente. O decorador encaminha requisições para o
componente e pode executar ações adicionais (tal como desenhar uma borda) antes ou
depois do encaminhamento. A transparência permite aninhar decoradores de forma
recursiva, possibilitando assim um número ilimitado de responsabilidades adicionais. (p.
175 da edição em inglês)

No Python, a funcão decoradora faz o papel de uma subclasse concreta de Decorador , e a função interna que ela
devolve é uma instância do decorador. A função devolvida envolve a função a ser decorada, que é análoga ao
componente no padrão de projeto. A função devolvida é transparente, pois se adapta à interface do componente
(ao aceitar os mesmos argumentos). Pegando emprestado da citação anterior, podemos adaptar a última frase
para dizer que "A transparência permite empilhar decoradores, possibilitando assim um número ilimitado de
comportamentos adicionais".
Veja que não estou sugerindo que decoradores de função devam ser usados para implementar o padrão
decorador em programas Python. Apesar disso ser possível em situações específicas, em geral o padrão decorador
é melhor implementado com classes representando o decorador e os componentes que ela vai envolver.
10. Padrões de projetos com funções de primeira classe
“ Conformidade a padrões não é uma medida de virtude. [110]

— Ralph Johnson
co-autor do clássico "Padrões de Projetos"

Em engenharia de software, um padrão de projeto (https://pt.wikipedia.org/wiki/Padr%C3%A3o_de_projeto_de_software) é uma


receita genérica para solucionar um problema de design frequente. Não é preciso conhecer padrões de projeto para
acompanhar esse capítulo, vou explicar os padrões usados nos exemplos.

O uso de padrões de projeto em programação foi popularizado pelo livro seminal Padrões de Projetos: Soluções
Reutilizáveis de Software Orientados a Objetos (Addison-Wesley), de Erich Gamma, Richard Helm, Ralph Johnson e John
Vlissides—também conhecidos como "the Gang of Four" (A Gangue dos Quatro). O livro é um catálogo de 23 padrões,
cada um deles composto por arranjos de classes e exemplificados com código em C++, mas assumidos como úteis
também em outras linguagens orientadas a objetos.

Apesar dos padrões de projeto serem independentes da linguagem, isso não significa que todo padrão se aplica a todas
as linguagens. Por exemplo, o Capítulo 17 vai mostrar que não faz sentido emular a receita do padrão Iterator
(Iterador) (https://fpy.li/10-2) (EN) no Python, pois esse padrão está embutido na linguagem e pronto para ser usado, na
forma de geradores—que não precisam de classes para funcionar, e exigem menos código que a receita clássica.

Os autores de Padrões de Projetos reconhecem, na introdução, que a linguagem usada na implementação determina
quais padrões são relevantes:

“ ANossos
escolha da linguagem de programação é importante, pois ela influencia nosso ponto de vista.
padrões supõe uma linguagem com recursos equivalentes aos do Smalltalk e do C++—e
essa escolha determina o que pode e o que não pode ser facilmente implementado. Se tivéssemos
presumido uma linguagem procedural, poderíamos ter incluido padrões de projetos chamados
"Herança", "Encapsulamento" e "Polimorfismo". Da mesma forma, alguns de nossos padrões são
suportados diretamente por linguagens orientadas a objetos menos conhecidas. CLOS, por
exemplo, tem multi-métodos, reduzindo a necessidade de um padrão como o Visitante.[111]

Em sua apresentação de 1996, "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas)
(https://fpy.li/norvigdp) (EN), Peter Norvig afirma que 16 dos 23 padrões no Padrões de Projeto original se tornam
"invisíveis ou mais simples" em uma linguagem dinâmica (slide 9). Ele está falando das linguagens Lisp e Dylan, mas
muitos dos recursos dinâmicos relevantes também estão presentes no Python. Em especial, no contexto de linguagens
com funções de primeira classe, Norvig sugere repensar os padrões clássicos conhecidos como Estratégia (Strategy),
Comando (Command), Método Template (Template Method) e Visitante (Visitor).

O objetivo desse capítulo é mostrar como—em alguns casos—as funções podem realizar o mesmo trabalho das classes,
com um código mais legível e mais conciso. Vamos refatorar uma implementaçao de Estratégia usando funções como
objetos, removendo muito código redundante. Vamos também discutir uma abordagem similar para simplificar o
padrão Comando.

10.1. Novidades nesse capítulo


Movi este capítulo para o final da Parte III, para poder então aplicar o decorador de registro na seção Seção 10.3, e
também usar dicas de tipo nos exemplos. A maior parte das dicas de tipo usadas nesse capítulo não são complicadas, e
ajudam na legibilidade.
10.2. Estudo de caso: refatorando Estratégia
Estratégia é um bom exemplo de um padrão de projeto que pode ser mais simples em Python, usando funções como
objetos de primeira classe. Na próxima seção vamos descrever e implementar Estratégia usando a estrutura "clássica"
descrita em Padrões de Projetos. Se você estiver familiarizado com o padrão clássico, pode pular direto para Seção
10.2.2, onde refatoramos o código usando funções, reduzindo significativamente o número de linhas.

10.2.1. Estratégia clássica


O diagrama de classes UML na Figura 1 retrata um arranjo de classes exemplificando o padrão Estratégia.

Figura 1. Diagrama de classes UML para o processamento de descontos em um pedido, implementado com o padrão de
projeto Estratégia.

O padrão Estratégia é resumido assim em Padrões de Projetos:

“ Define uma família de algoritmos, encapsula cada um deles, e os torna intercambiáveis.


Estratégia permite que o algoritmo varie de forma independente dos clientes que o usam.

Um exemplo claro de Estratégia, aplicado ao domínio do ecommerce, é o cálculo de descontos em pedidos de acordo
com os atributos do cliente ou pela inspeção dos itens do pedido.

Considere uma loja online com as seguintes regras para descontos:

Clientes com 1.000 ou mais pontos de fidelidade recebem um desconto global de 5% por pedido.
Um desconto de 10% é aplicado a cada item com 20 ou mais unidades no mesmo pedido.
Pedidos com pelo menos 10 itens diferentes recebem um desconto global de 7%.

Para simplificar, vamos assumir que apenas um desconto pode ser aplicado a cada pedido.
O diagrama de classes UML para o padrão Estratégia aparece na Figura 1. Seus participantes são:

Contexto (Context)
Oferece um serviço delegando parte do processamento para componentes intercambiáveis, que implementam
algoritmos alternativos. No exemplo de ecommerce, o contexto é uma classe Order , configurada para aplicar um
desconto promocional de acordo com um de vários algoritmos.

Estratégia (Strategy)
A interface comum dos componentes que implementam diferentes algoritmos. No nosso exemplo, esse papel cabe a
uma classe abstrata chamada Promotion .

Estratégia concreta (Concrete strategy)


Cada uma das subclasses concretas de Estratégia. FidelityPromo , BulkPromo , e LargeOrderPromo são as três
estratégias concretas implementadas.

O código no Exemplo 1 segue o modelo da Figura 1. Como descrito em Padrões de Projetos, a estratégia concreta é
escolhida pelo cliente da classe de contexto. No nosso exemplo, antes de instanciar um pedido, o sistema deveria, de
alguma forma, selecionar o estratégia de desconto promocional e passá-la para o construtor de Order . A seleção da
estratégia está fora do escopo do padrão.

Exemplo 1. Implementação da classe Order com estratégias de desconto intercambiáveis


PY
from abc import ABC, abstractmethod
from collections.abc import Sequence
from decimal import Decimal
from typing import NamedTuple, Optional

class Customer(NamedTuple):
name: str
fidelity: int

class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal

def total(self) -> Decimal:


return self.price * self.quantity

class Order(NamedTuple): # the Context


customer: Customer
cart: Sequence[LineItem]
promotion: Optional['Promotion'] = None

def total(self) -> Decimal:


totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))

def due(self) -> Decimal:


if self.promotion is None:
discount = Decimal(0)
else:
discount = self.promotion.discount(self)
return self.total() - discount

def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'

class Promotion(ABC): # the Strategy: an abstract base class


@abstractmethod
def discount(self, order: Order) -> Decimal:
"""Return discount as a positive dollar amount"""

class FidelityPromo(Promotion): # first Concrete Strategy


"""5% discount for customers with 1000 or more fidelity points"""

def discount(self, order: Order) -> Decimal:


rate = Decimal('0.05')
if order.customer.fidelity >= 1000:
return order.total() * rate
return Decimal(0)

class BulkItemPromo(Promotion): # second Concrete Strategy


"""10% discount for each LineItem with 20 or more units"""

def discount(self, order: Order) -> Decimal:


discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""

def discount(self, order: Order) -> Decimal:


distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)

Observe que no Exemplo 1, programei Promotion como uma classe base abstrata (ABC), para usar o decorador
@abstractmethod e deixar o padrão mais explícito.

O Exemplo 2 apresenta os doctests usados para demonstrar e verificar a operação de um módulo implementando as
regras descritas anteriormente.

Exemplo 2. Amostra de uso da classe Order com a aplicação de diferentes promoções

PYCON
>>> joe = Customer('John Doe', 0) # (1)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = (LineItem('banana', 4, Decimal('.5')), # (2)
... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, Decimal(5)))
>>> Order(joe, cart, FidelityPromo()) # (3)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo()) # (4)
<Order total: 42.00 due: 39.90>
>>> banana_cart = (LineItem('banana', 30, Decimal('.5')), # (5)
... LineItem('apple', 10, Decimal('1.5')))
>>> Order(joe, banana_cart, BulkItemPromo()) # (6)
<Order total: 30.00 due: 28.50>
>>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) # (7)
... for sku in range(10))
>>> Order(joe, long_cart, LargeOrderPromo()) # (8)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>

1. Dois clientes: joe tem 0 pontos de fidelidade, ann tem 1.100.


2. Um carrinho de compras com três itens.
3. A promoção FidelityPromo não dá qualquer desconto para joe .

4. ann recebe um desconto de 5% porque tem pelo menos 1.000 pontos.


5. O banana_cart contém 30 unidade do produto "banana" e 10 maçãs.
6. Graças à BulkItemPromo , joe recebe um desconto de $1,50 no preço das bananas.
7. O long_cart tem 10 itens diferentes, cada um custando $1,00.
8. joe recebe um desconto de 7% no pedido total, por causa da LargerOrderPromo .

O Exemplo 1 funciona perfeitamente bem, mas a mesma funcionalidade pode ser implementada com menos linhas de
código em Python, se usarmos funções como objetos. Veremos como fazer isso na próxima seção.

10.2.2. Estratégia baseada em funções


Cada estratégia concreta no Exemplo 1 é uma classe com um único método, discount . Além disso, as instâncias de
estratégia não tem nenhum estado (nenhum atributo de instância). Você poderia dizer que elas se parecem muito com
funções simples, e estaria certa. O Exemplo 3 é uma refatoração do Exemplo 1, substituindo as estratégias concretas por
funções simples e removendo a classe abstrata Promo . São necessários apenas alguns pequenos ajustes na classe
Order .[112]

Exemplo 3. A classe Order com as estratégias de descontos implementadas como funções


PY
from collections.abc import Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, Callable, NamedTuple

class Customer(NamedTuple):
name: str
fidelity: int

class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal

def total(self):
return self.price * self.quantity

@dataclass(frozen=True)
class Order: # the Context
customer: Customer
cart: Sequence[LineItem]
promotion: Optional[Callable[['Order'], Decimal]] = None # (1)

def total(self) -> Decimal:


totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))

def due(self) -> Decimal:


if self.promotion is None:
discount = Decimal(0)
else:
discount = self.promotion(self) # (2)
return self.total() - discount

def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'

# (3)

def fidelity_promo(order: Order) -> Decimal: # (4)


"""5% discount for customers with 1000 or more fidelity points"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)

def bulk_item_promo(order: Order) -> Decimal:


"""10% discount for each LineItem with 20 or more units"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount

def large_order_promo(order: Order) -> Decimal:


"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)

1. Essa dica de tipo diz: promotion pode ser None , ou pode ser um invocável que recebe uma Order como
argumento e devolve um Decimal .
2. Para calcular o desconto, chama o invocável self.promotion , passando self como um argumento. Veja a razão
disso logo abaixo.
3. Nenhuma classe abstrata.
4. Cada estratégia é uma função.

Por que self.promotion(self)?


Na classe Order , promotion não é um método. É um atributo de instância que por acaso é
invocável. Então a primeira parte da expressão, self.promotion , busca aquele invocável. Mas, ao
👉 DICA invocá-lo, precisamos fornecer uma instância de Order , que neste caso é self . Por isso self
aparece duas vezes na expressão.

A seção [methods_are_descriptors_sec] vai explicar o mecanismo que vincula automaticamente


métodos a instâncias. Mas isso não se aplica a promotion , pois ela não é um método.

O código no Exemplo 3 é mais curto que o do Exemplo 1. Usar a nova Order é também um pouco mais simples, como
mostram os doctests no Exemplo 4.

Exemplo 4. Amostra do uso da classe Order com as promoções como funções

PYCON
>>> joe = Customer('John Doe', 0) # (1)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo) # (2)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo) # (3)
<Order total: 30.00 due: 28.50>
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
... for item_code in range(10)]
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>

1. Mesmos dispositivos de teste do Exemplo 1.


2. Para aplicar uma estratégia de desconto a uma Order , basta passar a função de promoção como argumento.

3. Uma função de promoção diferente é usada aqui e no teste seguinte.

Observe os textos explicativos do Exemplo 4—não há necessidade de instanciar um novo objeto promotion com cada
novo pedido: as funções já estão disponíveis para serem usadas.
É interessante notar que no Padrões de Projetos, os autores sugerem que: "Objetos Estratégia muitas vezes são bons
"peso mosca" (flyweight)".[113] Uma definição do padrão Peso Mosca em outra parte daquele texto afirma: "Um peso
mosca é um objeto compartilhado que pode ser usado em múltiplos contextos simultaneamente."[114] O
compartilhamento é recomendado para reduzir o custo da criação de um novo objeto concreto de estratégia, quando a
mesma estratégia é aplicada repetidamente a cada novo contexto—no nosso exemplo, a cada nova instância de Order .
Então, para contornar uma desvantagem do padrão Estratégia—seu custo durante a execução—os autores
recomendam a aplicação de mais outro padrão. Enquanto isso, o número de linhas e custo de manutenção de seu
código vão se acumulando.

Um caso de uso mais espinhoso, com estratégias concretas complexas mantendo estados internos, pode exigir a
combinação de todas as partes dos padrões de projeto Estratégia e Peso Mosca. Muitas vezes, porém, estratégias
concretas não tem estado interno; elas lidam apenas com dados vindos do contexto. Neste caso, não tenha dúvida, use
as boas e velhas funções ao invés de escrever classes de um só metodo implementando uma interface de um só método
declarada em outra classe diferente. Uma função pesa menos que uma instância de uma classe definida pelo usuário, e
não há necessidade do Peso Mosca, pois cada função da estratégia é criada apenas uma vez por processo Python,
quando o módulo é carregado. Uma função simples também é um "objeto compartilhado que pode ser usado em
múltiplos contextos simultaneamente".

Uma vez implementado o padrão Estratégia com funções, outras possibilidades nos ocorrem. Suponha que você queira
criar uma "meta-estratégia", que seleciona o melhor desconto disponível para uma dada Order . Nas próximas seções
vamos estudar as refatorações adicionais para implementar esse requisito, usando abordagens que se valem de
funções e módulos vistos como objetos.

10.2.3. Escolhendo a melhor estratégia: uma abordagem simples


Dados os mesmos clientes e carrinhos de compras dos testes no Exemplo 4, vamos agora acrescentar três testes
adicionais ao Exemplo 5.

Exemplo 5. A funcão best_promo aplica todos os descontos e devolve o maior

PY
>>> Order(joe, long_cart, best_promo) # (1)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) # (2)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo) # (3)
<Order total: 42.00 due: 39.90>

1. best_promo selecionou a larger_order_promo para o cliente joe .

2. Aqui joe recebeu o desconto de bulk_item_promo , por comprar muitas bananas.

3. Encerrando a compra com um carrinho simples, best_promo deu à cliente fiel ann o desconto da
fidelity_promo .

A implementação de best_promo é muito simples. Veja o Exemplo 6.

Exemplo 6. best_promo encontra o desconto máximo iterando sobre uma lista de funções
PY
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # (1)

def best_promo(order: Order) -> Decimal: # (2)


"""Compute the best discount available"""
return max(promo(order) for promo in promos) # (3)

1. promos : lista de estratégias implementadas como funções.

2. best_promo recebe uma instância de Order como argumento, como as outras funções *_promo .

3. Usando uma expressão geradora, aplicamos cada uma das funções de promos a order , e devolvemos o maior
desconto encontrado.

O Exemplo 6 é bem direto: promos é uma list de funções. Depois que você se acostuma à ideia de funções como
objetos de primeira classe, o próximo passo é notar que construir estruturas de dados contendo funções muitas vezes
faz todo sentido.

Apesar do Exemplo 6 funcionar e ser fácil de ler, há alguma duplicação que poderia levar a um bug sutil: para
adicionar uma nova estratégia, precisamos escrever a função e lembrar de incluí-la na lista promos . De outra forma a
nova promoção só funcionará quando passada explicitamente como argumento para Order , e não será considerada
por best_promotion .

Vamos examinar algumas soluções para essa questão.

10.2.4. Encontrando estratégias em um módulo


Módulos também são objetos de primeira classe no Python, e a biblioteca padrão oferece várias funções para lidar com
eles. A função embutida globals é descrita assim na documentação do Python:

globals()

Devolve um dicionário representando a tabela de símbolos globais atual. Isso é sempre o dicionário do módulo atual
(dentro de uma função ou método, esse é o módulo onde a função ou método foram definidos, não o módulo de onde
são chamados).

O Exemplo 7 é uma forma um tanto hacker de usar globals para ajudar best_promo a encontrar automaticamente
outras funções *_promo disponíveis.

Exemplo 7. A lista promos é construída a partir da introspecção do espaço de nomes global do módulo

PY
from decimal import Decimal
from strategy import Order
from strategy import (
fidelity_promo, bulk_item_promo, large_order_promo # (1)
)

promos = [promo for name, promo in globals().items() # (2)


if name.endswith('_promo') and # (3)
name != 'best_promo' # (4)
]

def best_promo(order: Order) -> Decimal: # (5)


"""Compute the best discount available"""
return max(promo(order) for promo in promos)
1. Importa as funções de promoções, para que fiquem disponíveis no espaço de nomes global.[115]
2. Itera sobre cada item no dict devolvido por globals() .

3. Seleciona apenas aqueles valores onde o nome termina com o sufixo _promo e…​
4. …​filtra e remove a própria best_promo , para evitar uma recursão infinita quando best_promo for invocada.
5. Nenhuma mudança em best_promo .

Outra forma de coletar as promoções disponíveis seria criar um módulo e colocar nele todas as funções de estratégia,
exceto best_promo .

No Exemplo 8, a única mudança significativa é que a lista de funções de estratégia é criada pela introspecção de um
módulo separado chamado promotions . Veja que o Exemplo 8 depende da importação do módulo promotions bem
como de inspect , que fornece funções de introspecção de alto nível.

Exemplo 8. A lista promos é construída a partir da introspecção de um novo módulo, promotions

PY
from decimal import Decimal
import inspect

from strategy import Order


import promotions

promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]

def best_promo(order: Order) -> Decimal:


"""Compute the best discount available"""
return max(promo(order) for promo in promos)

A função inspect.getmembers devolve os atributos de um objeto—neste caso, o módulo promotions —


opcionalmente filtrados por um predicado (uma função booleana). Usamos inspect.isfunction para obter apenas
as funções do módulo.

O Exemplo 8 funciona independente dos nomes dados às funções; tudo o que importa é que o módulo promotions
contém apenas funções que, dado um pedido, calculam os descontos. Claro, isso é uma suposição implícita do código.
Se alguém criasse uma função com uma assinatura diferente no módulo promotions , best_promo geraria um erro ao
tentar aplicá-la a um pedido.

Poderíamos acrescentar testes mais estritos para filtrar as funções, por exemplo inspecionando seus argumentos. O
ponto principal do Exemplo 8 não é oferecer uma solução completa, mas enfatizar um uso possível da introspecção de
módulo.

Uma alternativa mais explícita para coletar dinamicamente as funções de desconto promocional seria usar um
decorador simples. É nosso próximo tópico.

10.3. Padrão Estratégia aperfeiçoado com um decorador


Lembre-se que nossa principal objeção ao Exemplo 6 foi a repetição dos nomes das funções em suas definições e na
lista promos , usada pela função best_promo para determinar o maior desconto aplicável. A repetição é problemática
porque alguém pode acrescentar uma nova função de estratégia promocional e esquecer de adicioná-la manualmente
à lista promos —caso em que best_promo vai silenciosamente ignorar a nova estratégia, introduzindo no sistema um
bug sutil. O Exemplo 9 resolve esse problema com a técnica vista na seção Seção 9.4.
Exemplo 9. A lista promos é preeenchida pelo decorador Promotion

PY
Promotion = Callable[[Order], Decimal]

promos: list[Promotion] = [] # (1)

def promotion(promo: Promotion) -> Promotion: # (2)


promos.append(promo)
return promo

def best_promo(order: Order) -> Decimal:


"""Compute the best discount available"""
return max(promo(order) for promo in promos) # (3)

@promotion # (4)
def fidelity(order: Order) -> Decimal:
"""5% discount for customers with 1000 or more fidelity points"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)

@promotion
def bulk_item(order: Order) -> Decimal:
"""10% discount for each LineItem with 20 or more units"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount

@promotion
def large_order(order: Order) -> Decimal:
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)

1. A lista promos é global no módulo, e começa vazia.


2. Promotion é um decorador de registro: ele devolve a função promo inalterada, após inserí-la na lista promos .

3. Nenhuma mudança é necessária em best_promo , pois ela se baseia na lista promos .

4. Qualquer função decorada com @promotion será adicionada a promos .

Essa solução tem várias vantagens sobre aquelas apresentadas anteriormente:

As funções de estratégia de promoção não precisam usar nomes especiais—não há necessidade do sufixo _promo .

O decorador @promotion realça o propósito da função decorada, e também torna mais fácil desabilitar
temporariamente uma promoção: basta transformar a linha do decorador em comentário.
Estratégias de desconto promocional podem ser definidas em outros módulos, em qualquer lugar do sistema, desde
que o decorador @promotion seja aplicado a elas.
Na próxima seção vamos discutir Comando (Command)—outro padrão de projeto que é algumas vezes implementado
via classes de um só metodo, quando funções simples seriam suficientes.

10.4. O padrão Comando


Comando é outro padrão de projeto que pode ser simplificado com o uso de funções passadas como argumentos. A
Figura 2 mostra o arranjo das classes nesse padrão.

Figura 2. Diagrama de classes UML para um editor de texto controlado por menus, implementado com o padrão de
projeto Comando. Cada comando pode ter um destinatário (receiver) diferente: o objeto que implementa a ação. Para
PasteCommand , o destinatário é Document. Para OpenCommand , o destinatário á a aplicação.

O objetivo de Comando é desacoplar um objeto que invoca uma operação (o invoker ou remetente) do objeto
fornecedor que implementa aquela operação (o receiver ou destinatário). No exemplo em Padrões de Projetos, cada
remetente é um item de menu em uma aplicação gráfica, e os destinatários são o documento sendo editado ou a
própria aplicação.

A ideia é colocar um objeto Command entre os dois, implementando uma interface com um único método, execute ,
que chama algum método no destinatário para executar a operação desejada. Assim, o remetente não precisa conhecer
a interface do destinatário, e destinatários diferentes podem ser adaptados com diferentes subclasses de Command . O
remetente é configurado com um comando concreto, e o opera chamando seu método execute . Observe na Figura 2
que MacroCommand pode armazenar um sequência de comandos; seu método execute() chama o mesmo método em
cada comando armazenado.

Citando Padrões de Projetos, "Comandos são um substituto orientado a objetos para callbacks." A pergunta é:
precisamos de um substituto orientado a objetos para callbacks? Algumas vezes sim, mas nem sempre.

Em vez de dar ao remetente uma instância de Command , podemos simplesmente dar a ele uma função. Em vez de
chamar command.execute() , o remetente pode apenas chamar command() . O MacroCommand pode ser programado
como uma classe que implementa __call__ . Instâncias de MacroCommand seriam invocáveis, cada uma mantendo
uma lista de funções para invocação futura, como implementado no Exemplo 10.
Exemplo 10. Cada instância de MacroCommand tem uma lista interna de comandos

PYTHON3
class MacroCommand:
"""A command that executes a list of commands"""

def __init__(self, commands):


self.commands = list(commands) # (1)

def __call__(self):
for command in self.commands: # (2)
command()

1. Criar uma nova lista com os itens do argumento commands garante que ela seja iterável e mantém uma cópia local
de referências a comandos em cada instância de MacroCommand .
2. Quando uma instância de MacroCommand é invocada, cada comando em self.commands é chamado em
sequência.

Usos mais avançados do padrão Comando—para implementar "desfazer", por exemplo—podem exigir mais que uma
simples função de callback. Mesmo assim, o Python oferece algumas alternativas que merecem ser consideradas:

Uma instância invocável como MacroCommand no Exemplo 10 pode manter qualquer estado que seja necessário, e
oferecer outros métodos além de __call__ .
Uma clausura pode ser usada para manter o estado interno de uma função entre invocações.

Isso encerra nossa revisão do padrão Comando usando funções de primeira classe. Por alto, a abordagem aqui foi
similar à que aplicamos a Estratégia: substituir as instâncias de uma classe participante que implementava uma
interface de método único por invocáveis. Afinal, todo invocável do Python implementa uma interface de método
único, e esse método se chama __call__ .

10.5. Resumo do Capítulo


Como apontou Peter Norvig alguns anos após o surgimento do clássico Padrões de Projetos, "16 dos 23 padrões tem
implementações qualitativamente mais simples em Lisp ou Dylan que em C++, pelo menos para alguns usos de cada
padrão" (slide 9 da apresentação de Norvig, "Design Patterns in Dynamic Languages" presentation (https://fpy.li/10-4)
(Padrões de Projetos em Linguagens Dinâmicas)). O Python compartilha alguns dos recursos dinâmicos das linguagens
Lisp e Dylan, especialmente funções de primeira classe, nosso foco nesse capítulo.

Na mesma palestra citada no início deste capítulo, refletindo sobre o 20º aniversário de Padrões de Projetos: Soluções
Reutilizáveis de Software Orientados a Objetos, Ralph Johnson afirmou que um dos defeitos do livro é: "Excesso de
ênfase nos padrões como linhas de chegada, em vez de como etapas em um processo de design".[116] Neste capítulo
usamos o padrão Estratégia como ponto de partida: uma solução que funcionava, mas que simplificamos usando
funções de primeir classe.

Em muitos casos, funções ou objetos invocáveis oferecem um caminho mais natural para implementar callbacks em
Python que a imitação dos padrões Estratégia ou Comando como descritos por Gamma, Helm, Johnson, e Vlissides em
Padrões de Projetos. A refatoração de Estratégia e a discussão de Comando nesse capítulo são exemplos de uma ideia
mais geral: algumas vezes você pode encontrar uma padrão de projeto ou uma API que exigem que seus componentes
implementem uma interface com um único método, e aquele método tem um nome que soa muito genérico, como
"executar", "rodar" ou "fazer". Tais padrões ou APIs podem frequentemente ser implementados em Python com menos
código repetitivo, usando funções como objetos de primeira classe.
10.6. Leitura complementar
A "Receita 8.21. Implementando o Padrão Visitante" (Receipt 8.21. Implementing the Visitor Pattern) no Python
Cookbook, 3ª ed. (https://fpy.li/pycook3) (EN), mostra uma implementação elegante do padrão Visitante, na qual uma classe
NodeVisitor trata métodos como objetos de primeira classe.

Sobre o tópico mais geral de padrões de projetos, a oferta de leituras para o programador Python não é tão numerosa
quando aquela disponível para as comunidades de outras linguagens.

Learning Python Design Patterns ("Aprendendo os Padrões de Projeto do Python"), de Gennadiy Zlobin (Packt), é o único
livro inteiramente dedicado a padrões em Python que encontrei. Mas o trabalho de Zlobin é muito breve (100 páginas)
e trata de apenas 8 dos 23 padrões de projeto originais.

Expert Python Programming ("Programação Avançada em Python"), de Tarek Ziadé (Packt), é um dos melhores livros de
Python de nível intermediário, e seu capítulo final, "Useful Design Patterns" (Padrões de Projetos Úteis), apresenta
vários dos padrões clássicos de uma perspectiva pythônica.

Alex Martelli já apresentou várias palestras sobre padrões de projetos em Python. Há um vídeo de sua apresentação na
EuroPython (https://fpy.li/10-5) (EN) e um conjunto de slides em seu site pessoal (https://fpy.li/10-6) (EN). Ao longo dos anos,
encontrei diferentes jogos de slides e vídeos de diferentes tamanhos, então vale a pena tentar uma busca mais ampla
com o nome dele e as palavras "Python Design Patterns". Um editor me contou que Martelli está trabalhando em um
livro sobre esse assunto. Eu certamente comprarei meu exemplar assim que estiver disponível.

Há muitos livros sobre padrões de projetos no contexto do Java mas, dentre todos eles, meu preferido é Head First
Design Patterns ("Mergulhando de Cabeça nos Padrões de Projetos"), 2ª ed., de Eric Freeman e Elisabeth Robson
(O’Reilly). Esse volume explica 16 dos 23 padrões clássicos. Se você gosta do estilo amalucado da série Head First e
precisa de uma introdução a esse tópico, vai adorar esse livro. Ele é centrado no Java, mas a segunda edição foi
atualizada para refletir a introdução de funções de primeira classe naquela linguagem, tornando alguns dos exemplos
mais próximos de código que escreveríamos em Python.

Para um olhar moderno sobre padrões, do ponto de vista de uma linguagem dinâmica com duck typing e funções de
primeira classe, Design Patterns in Ruby ("Padrões de Projetos em Ruby") de Russ Olsen (Addison-Wesley) traz muitas
ideias aplicáveis também ao Python. A despeito de suas muitas diferenças sintáticas, no nível semântico o Python e o
Ruby estão mais próximos entre si que do Java ou do C++.

Em "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (https://fpy.li/norvigdp)


(slides), Peter Norvig mostra como funções de primeira classe (e outros recursos dinâmicos) tornam vários dos padrões
de projeto originais mais simples ou mesmo desnecessários.

A "Introdução" do Padrões de Projetos original, de Gamma et al. já vale o preço do livro—mais até que o catálogo de 23
padrões, que inclui desde receitas muito importantes até algumas raramente úteis. Alguns princípios de projetos de
software muito citados, como "Programe para uma interface, não para uma implementação" e "Prefira a composição de
objetos à herança de classe", vem ambos daquela introdução.

A aplicação de padrões a projetos se originou com o arquiteto Christopher Alexander et al., e foi apresentada no livro A
Pattern Language ("Uma Linguagem de Padrões") (Oxford University Press). A ideia de Alexander é criar um
vocabulário padronizado, permitindo que equipes compartilhem decisões comuns em projetos de edificações. M. J.
Dominus wrote “‘Design Patterns’ Aren’t” (Padrões de Projetos Não São) (https://fpy.li/10-7), uma curiosa apresentação de
slides acompanhada de um texto, argumentando que a visão original de Alexander sobre os padrões é mais profunda e
mais humanista e também aplicável à engenharia de software.
Ponto de vista
O Python tem funções de primeira classe e tipos de primeira classe, e Norvig afima que esses recursos afetam 10
dos 23 padrões (no slide 10 de "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens
Dinâmicas) (https://fpy.li/norvigdp)). No Capítulo 9, vimos que o Python também tem funções genéricas (na seção
Seção 9.9.3), uma forma limitada dos multi-métodos do CLOS, que Gamma et al. sugerem como uma maneira
mais simples de implementar o padrão clássico Visitante (Visitor). Norvig, por outro lado, diz (no slide 10) que os
multi-métodos simplificam o padrão Construtor (Builder). Ligar padrões de projetos a recursos de linguagens não
é uma ciência exata.

Em cursos a redor do mundo todo, padrões de projetos são frequentemente ensinados usando exemplos em Java.
Ouvi mais de um estudante dizer que eles foram levados a crer que os padrões de projeto originais são úteis
qualquer que seja a linguagem usada na implementação. A verdade é que os 23 padrões "clássicos" de Padrões de
Projetos se aplicam muito bem ao Java, apesar de terem sido apresentados principalmente no contexto do C++—
no livro, alguns deles tem exemplos em Smalltalk. Mas isso não significa que todos aqueles padrões podem ser
aplicados de forma igualmente satisfatória a qualquer linguagem. Os autores dizem explicitamente, logo no início
de seu livro, que "alguns de nossos padrões são suportados diretamente por linguagens orientadas a objetos
menos conhecidas" (a citação completa apareceu na primeira página deste capítulo).

A bibliografia do Python sobre padrões de projetos é muito pequena, se comparada à existente para Java, C++ ou
Ruby. Na seção Seção 10.6, mencionei Learning Python Design Patterns ("Aprendendo Padrões de Projeto do
Python"), de Gennadiy Zlobin, que foi publicado apenas em novembro de 2013. Para se ter uma ideia, Design
Patterns in Ruby ("Padrões de Projetos em Ruby"), de Russ Olsen, foi publicado em 2007 e tem 384 páginas—284 a
mais que a obra de Zlobin.

Agora que o Python está se tornando cada vez mais popular no ambiente acadêmico, podemos esperar que novos
livros sobre padrões de projetos sejam escritos no contexto de nossa linguagem. Além disso, o Java 8 introduziu
referências a métodos e funções anônimas, e esses recursos muito esperados devem incentivar o surgimento de
novas abordagens aos padrões em Java—reconhecendo que, à medida que as linguagens evoluem, nosso
entendimento sobre a forma de aplicação dos padrões de projetos clássicos deve também evoluir.

O call selvagem

Enquanto trabalhávamos juntos para dar os toques finais a este livro, o revisor técnico Leonardo Rochael pensou:

Se funções tem um método __call__ , e métodos também são invocáveis, será que os métodos __call__
também tem um método __call__ ?

Não sei se a descoberta dele tem alguma utilidade, mas eis um fato engraçado:
PYCON
>>> def turtle():
... return 'eggs'
...
>>> turtle()
'eggs'
>>> turtle.__call__()
'eggs'
>>> turtle.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__.__call__.__call__()
'eggs'

Turtles all the way down (https://fpy.li/10-8)[117]


Parte III: Classes e protocolos
11. Um objeto pythônico
“ Para uma biblioteca ou framework, ser pythônica significa tornar tão fácil e tão natural quanto
possível que uma programadora Python descubra como realizar uma tarefa. [118]

— Martijn Faassen
criador de frameworks Python e JavaScript

Graças ao Modelo de Dados do Python, nossos tipos definidos pelo usuário podem se comportar de forma tão natural
quanto os tipos embutidos. E isso pode ser realizado sem herança, no espírito do duck typing: implemente os métodos
necessários e seus objetos se comportarão da forma esperada.

Nos capítulos anteriores, estudamos o comportamento de vários objetos embutidos. Vamos agora criar classes
definidas pelo usuário que se portam como objetos Python reais. As classes na sua aplicação provavelmente não
precisam nem devem implementar tantos métodos especiais quanto os exemplos nesse capítulo. Mas se você estiver
escrevendo uma biblioteca ou uma framework, os programadores que usarão suas classes talvez esperem que elas se
comportem como as classes fornecidas pelo Python. Satisfazer tal expectativa é um dos jeitos de ser "pythônico".

Esse capítulo começa onde o Capítulo 1 terminou, mostrando como implementar vários métodos especiais comumente
vistos em objetos Python de diferentes tipos.

Veremos como:

Suportar as funções embutidas que convertem objetos para outros tipos (por exemplo, repr() , bytes() ,
complex() , etc.)

Implementar um construtor alternativo como um método da classe


Estender a mini-linguagem de formatação usada pelas f-strings, pela função embutida format() e pelo método
str.format()

Fornecer acesso a atributos apenas para leitura


Tornar um objetos hashable, para uso em sets e como chaves de dict

Economizar memória com __slots__

Vamos fazer tudo isso enquanto desenvolvemos Vector2d , um tipo simples de vetor euclidiano bi-dimensional. No
Capítulo 12, o mesmo código servirá de base para uma classe de vetor N-dimensional.

A evolução do exemplo será interrompida para discutirmos dois tópicos conceituais:

Como e quando usar os decoradores @classmethod e @staticmethod

Atributos privados e protegidos no Python: uso, convenções e limitações

11.1. Novidades nesse capítulo


Acrescentei uma nova epígrafe e também algumas palavras ao segundo parágrafo do capítulo, para falar do conceito
de "pythônico"—que na primeira edição era discutido apenas no final do livro.

A seção Seção 11.6 foi atualizada para mencionar as f-strings, introduzidas no Python 3.6. É uma mudança pequena,
pois as f-strings suportam a mesma mini-linguagem de formatação que a função embutida format() e o método
str.format() , então quaisquer métodos __format__ implementados antes vão funcionar também com as f-strings.
O resto do capítulo quase não mudou—os métodos especiais são praticamente os mesmos desde o Python 3.0, e as
ideias centrais apareceram no Python 2.2.

Vamos começar pelos métodos de representação de objetos.

11.2. Representações de objetos


Todas as linguagens orientadas a objetos tem pelo menos uma forma padrão de se obter uma representação de
qualquer objeto como uma string. O Python tem duas formas:

repr()
Devolve uma string representando o objeto como o desenvolvedor quer vê-lo. É o que aparece quando o console do
Python ou um depurador mostram um objeto.

str()

Devolve uma string representando o objeto como o usuário quer vê-lo. É o que aparece quando se passa um objeto
como argumento para print() .

Os métodos especiais __repr__ e __str__ suportam repr() e str() , como vimos no Capítulo 1.

Existem dois métodos especiais adicionais para suportar representações alternativas de objetos, __bytes__ e
__format__ . O método __bytes__ é análogo a __str__ : ele é chamado por bytes() para obter um objeto
representado como uma sequência de bytes. Já __format__ é usado por f-strings, pela função embutida format() e
pelo método str.format() . Todos eles chamam obj.format(format_spec) para obter versões de exibição de
objetos usando códigos de formatação especiais. Vamos tratar de __bytes__ na próxima seção e de __format__ logo
depois.

Se você está vindo do Python 2, lembre-se que no Python 3 __repr__ , __str__ e __format__
⚠️ AVISO devem sempre devolver strings Unicode (tipo str ). Apenas __bytes__ deveria devolver uma
sequência de bytes (tipo bytes ).

11.3. A volta da classe Vector


Para demonstrar os vários métodos usados para gerar representações de objetos, vamos criar uma classe Vector2d ,
similar à que vimos no Capítulo 1. O Exemplo 1 ilustra o comportamento básico que esperamos de uma instância de
Vector2d .

Exemplo 1. Instâncias de Vector2d têm várias representações


PY
>>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y) # (1)
3.0 4.0
>>> x, y = v1 # (2)
>>> x, y
(3.0, 4.0)
>>> v1 # (3)
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1)) # (4)
>>> v1 == v1_clone # (5)
True
>>> print(v1) # (6)
(3.0, 4.0)
>>> octets = bytes(v1) # (7)
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1) # (8)
5.0
>>> bool(v1), bool(Vector2d(0, 0)) # (9)
(True, False)

1. Os componentes de um Vector2d podem ser acessados diretamente como atributos (não é preciso invocar
métodos getter).
2. Um Vector2d pode ser desempacotado para uma tupla de variáveis.
3. O repr de um Vector2d emula o código-fonte usado para construir a instância.
4. Usar eval aqui mostra que o repr de um Vector2d é uma representação fiel da chamada a seu construtor.[119]
5. Vector2d suporta a comparação com == ; isso é útil para testes.

6. print chama str , que no caso de Vector2d exibe um par ordenado.


7. bytes usa o método __bytes__ para produzir uma representação binária.
8. abs usa o método __abs__ para devolver a magnitude do Vector2d .

9. bool usa o método __bool__ para devolver False se o Vector2d tiver magnitude zero, caso contrário esse
método devolve True .

O Vector2d do Exemplo 1 é implementado em vector2d_v0.py (no Exemplo 2). O código está basedo no Exemplo 2,
exceto pelos métodos para os operadores + e * , que veremos mais tarde no Capítulo 16. Vamos acrescentar o método
para == , já que ele é útil para testes. Nesse ponto, Vector2d usa vários métodos especiais para oferecer operações que
um pythonista espera encontrar em um objeto bem projetado.

Exemplo 2. vector2d_v0.py: todos os métodos até aqui são métodos especiais


PY
from array import array
import math

class Vector2d:
typecode = 'd' # (1)

def __init__(self, x, y):


self.x = float(x) # (2)
self.y = float(y)

def __iter__(self):
return (i for i in (self.x, self.y)) # (3)

def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self) # (4)

def __str__(self):
return str(tuple(self)) # (5)

def __bytes__(self):
return (bytes([ord(self.typecode)]) + # (6)
bytes(array(self.typecode, self))) # (7)

def __eq__(self, other):


return tuple(self) == tuple(other) # (8)

def __abs__(self):
return math.hypot(self.x, self.y) # (9)

def __bool__(self):
return bool(abs(self)) # (10)

1. typecode é um atributo de classe, usado na conversão de instâncias de Vector2d de/para bytes .

2. Converter x e y para float em __init__ captura erros mais rápido, algo útil quando Vector2d é chamado
com argumentos inadequados.
3. __iter__ torna um Vector2d iterável; é isso que faz o desempacotamento funcionar (por exemplo, x, y =
my_vector ). Vamos implementá-lo aqui usando uma expressão geradora para produzir os componentes, um após
outro.[120]
4. O __repr__ cria uma string interpolando os componentes com {!r} , para obter seus repr ; como Vector2d é
iterável, *self alimenta format com os componentes x e y .
5. Dado um iterável Vector2d , é fácil criar uma tuple para exibição como um par ordenado.
6. Para gerar bytes , convertemos o typecode para bytes e concatenamos…​
7. …​bytes convertidos a partir de um array criada iterando sobre a instância.
8. Para comparar rapidamente todos os componentes, cria tuplas a partir dos operandos. Isso funciona para
operandos que sejam instâncias de Vector2d , mas tem problemas. Veja o alerta abaixo.
9. A magnitude é o comprimento da hipotenusa do triângulo retângulo de catetos formados pelos componentes x e
y.

10. __bool__ usa abs(self) para computar a magnitude, então a converte para bool ; assim, 0.0 se torna False ,
qualquer valor diferente de zero é True .
O método __eq__ no Exemplo 2 funciona para operandos Vector2d , mas também devolve True

⚠️ AVISO ao comparar instâncias de Vector2d a outros iteráveis contendo os mesmos valores numéricos (por
exemplo, Vector(3, 4) == [3, 4] ). Isso pode ser considerado uma característica ou um bug. Essa
discussão terá que esperar até o Capítulo 16, onde falamos de sobrecarga de operadores.

Temos um conjunto bastante completo de métodos básicos, mas ainda precisamos de uma maneira de reconstruir um
Vector2d a partir da representação binária produzida por bytes() .

11.4. Um construtor alternativo


Já que podemos exportar um Vector2d na forma de bytes, naturalmente precisamos de um método para importar um
Vector2d de uma sequência binária. Procurando na biblioteca padrão por algo similar, descobrimos que
array.array tem um método de classe chamado .frombytes , adequado a nossos propósitos—​já o vimos na seção
Seção 2.10.1. Adotamos o mesmo nome e usamos sua funcionalidade em um método de classe para Vector2d em
vector2d_v1.py (no Exemplo 3).

Exemplo 3. Parte de vector2d_v1.py: esse trecho mostra apenas o método de classe frombytes , acrescentado à
definição de Vector2d em vector2d_v0.py (no Exemplo 2)

PY
@classmethod # (1)
def frombytes(cls, octets): # (2)
typecode = chr(octets[0]) # (3)
memv = memoryview(octets[1:]).cast(typecode) # (4)
return cls(*memv) # (5)

1. O decorador classmethod modifica um método para que ele possa ser chamado diretamente em uma classe.
2. Nenhum argumento self ; em vez disso, a própria classe é passada como primeiro argumento—por convenção
chamado cls .
3. Lê o typecode do primeiro byte.
4. Cria uma memoryview a partir da sequência binária octets , e usa o typecode para convertê-la.[121]
5. Desempacota a memoryview resultante da conversão no par de argumentos necessários para o construtor.

Acabei de usar um decorador classmethod , e ele é muito específico do Python. Vamos então falar um pouco disso.

11.5. classmethod versus staticmethod


O decorador classmethod não é mencionado no tutorial do Python, nem tampouco o staticmethod . Qualquer um
que tenha aprendido OO com Java pode se perguntar porque o Python tem esses dois decoradores, e não apenas um
deles.

Vamos começar com classmethod . O Exemplo 3 mostra seu uso: definir um método que opera na classe, e não em
suas instâncias. O classmethod muda a forma como o método é chamado, então recebe a própria classe como
primeiro argumento, em vez de uma instância. Seu uso mais comum é em construtores alternativos, como frombytes
no Exemplo 3. Observe como a última linha de frombytes de fato usa o argumento cls , invocando-o para criar uma
nova instância: cls(*memv) .

O decorador staticmethod , por outro lado, muda um método para que ele não receba qualquer primeiro argumento
especial. Essencialmente, um método estático é apenas uma função simples que por acaso mora no corpo de uma
classe, em vez de ser definida no nível do módulo. O Exemplo 4 compara a operação de classmethod e
staticmethod .
Exemplo 4. Comparando o comportamento de classmethod e staticmethod

PYCON
>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args # (1)
... @staticmethod
... def statmeth(*args):
... return args # (2)
...
>>> Demo.klassmeth() # (3)
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth() # (4)
()
>>> Demo.statmeth('spam')
('spam',)

1. klassmeth apenas devolve todos os argumentos posicionais.


2. statmeth faz o mesmo.
3. Não importa como ele seja invocado, Demo.klassmeth recebe sempre a classe Demo como primeiro argumento.
4. Demo.statmeth se comporta exatamente como uma boa e velha função.

O decorador classmethod é obviamente útil mas, em minha experiência, bons casos de uso para
staticmethod são muito raros. Talvez a função, mesmo sem nunca tocar na classe, seja
✒️ NOTA intimamente relacionada a essa última. Daí você pode querer que ela fique próxima no seu código. E
mesmo assim, definir a função logo antes ou logo depois da classe, no mesmo módulo, é perto o
suficiente na maioria dos casos.[122]

Agora que vimos para que serve o classmethod (e que o staticmethod não é muito útil), vamos voltar para a
questão da representação de objetos e entender como gerar uma saída formatada.

11.6. Exibição fomatada


As f-strings, a função embutida format() e o método str.format() delegam a formatação efetiva para cada tipo,
chamando seu método .__format__(format_spec) . O format_spec especifica a formatação desejada, e é:

O segundo argumento em format(my_obj, format_spec) , ou

O que quer que apareça após os dois pontos ( : ) em um campo de substituição delimitado por {} dentro de uma f-
string ou o fmt em fmt.str.format()

Por exemplo:

PYCON
>>> brl = 1 / 4.82 # BRL to USD currency conversion rate
>>> brl
0.20746887966804978
>>> format(brl, '0.4f') # (1)
'0.2075'
>>> '1 BRL = {rate:0.2f} USD'.format(rate=brl) # (2)
'1 BRL = 0.21 USD'
>>> f'1 USD = {1 / brl:0.2f} BRL' # (3)
'1 USD = 4.82 BRL'
1. A formatação especificada é '0.4f' .

2. A formatação especificada é '0.2f' . O rate no campo de substituição não é parte da especificação de formato.
Ele determina qual argumento nomeado de .format() entra no campo de substituição.
3. Novamente, a especificação é '0.2f' . A expressão 1 / brl não é parte dela.

O segundo e o terceiro textos explicativos apontam um fato importante: uma string de formatação tal
como’{0.mass:5.3e}'` na verdade usa duas notações separadas. O '0.mass' à esquerda dos dois pontos é a parte
field_name da sintaxe de campo de substituição, e pode ser uma expressão arbitrária em uma f-string. O '5.3e'
após os dois pontos é a especificação do formato. A notação usada na especificação do formato é chamada Mini-
Linguagem de Especificação de Formato (https://docs.python.org/pt-br/3/library/string.html#formatspec).

Se f-strings, format() e str.format() são novidades para você, minha experiência como
professor me informa que é melhor estudar primeiro a função embutida format() , que usa apenas
a Mini-Linguagem de Especificação de Formato
(https://docs.python.org/pt-br/3/library/string.html#formatspec). Após pegar o jeito desta última, leia

👉 DICA "Literais de string formatados" (https://docs.python.org/pt-br/3/reference/lexical_analysis.html#f-strings) e


"Sintaxe das string de formato" (https://docs.python.org/pt-br/3/library/string.html#format-string-syntax), para
aprender sobre a notação de campo de substituição ( {:} ), usada em f-strings e no método
str.format() (incluindo os marcadores de conversão !s , !r , e !a ). F-strings não tornam
str.format() obsoleto: na maioria dos casos f-strings resolvem o problema, mas algumas vezes é
melhor especificar a string de formatação em outro lugar (e não onde ela será renderizada).

Alguns tipos embutidos tem seus próprios códigos de apresentação na Mini-Linguagem de Especificação de Formato.
Por exemplo—entre muitos outros códigos—o tipo int suporta b e x , para saídas em base 2 e base 16,
respectivamente, enquanto float implementa f , para uma exibição de ponto fixo, e % , para exibir porcentagens:

PYCON
>>> format(42, 'b')
'101010'
>>> format(2 / 3, '.1%')
'66.7%'

A Mini-Linguagem de Especificação de Formato é extensível, porque cada classe interpreta o argumento format_spec
como quiser. Por exemplo, as classes no módulo datetime usam os mesmos códigos de formatação nas funções
strftime() e em seus métodos __format__ . Veja abaixo alguns exemplos de uso da função format() e do método
str.format() :

PYCON
>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now, '%H:%M:%S')
'18:49:05'
>>> "It's now {:%I:%M %p}".format(now)
"It's now 06:49 PM"

Se a classe não possuir um __format__ , o método herdado de object devolve str(my_object) . Como Vector2d
tem um __str__ , isso funciona:

PYCON
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'

Entretanto, se você passar um especificador de formato, object.__format__ gera um TypeError :


PYCON
>>> format(v1, '.3f')
Traceback (most recent call last):
...
TypeError: non-empty format string passed to object.__format__

Vamos corrigir isso implementando nossa própria mini-linguagem de formatação. O primeiro passo será presumir que
o especificador de formato fornecido pelo usuário tem por objetivo formatar cada componente float do vetor. Esse é
o resultado esperado:

PYCON
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'

O Exemplo 5 implementa __format__ para produzir as formatações vistas acima.

Exemplo 5. O método Vector2d.__format__ , versão #1

PYTHON3
# inside the Vector2d class

def __format__(self, fmt_spec=''):


components = (format(c, fmt_spec) for c in self) # (1)
return '({}, {})'.format(*components) # (2)

1. Usa a função embutida format para aplicar o fmt_spec a cada componente do vetor, criando um iterável de
strings formatadas.
2. Insere as strings formatadas na fórmula '(x, y)' .

Agora vamos acrescentar um código de formatação personalizado à nossa mini-linguagem: se o especificador de


formato terminar com 'p' , vamos exibir o vetor em coordenadas polares: <r, θ> , onde r é a magnitute e θ (theta) é
o ângulo em radianos. O restante do especificador de formato (o que quer que venha antes do 'p' ) será usado como
antes.

Ao escolher a letra para um código personalizado de formato, evitei sobrepor códigos usados por
outros tipos. Na Mini-Linguagem de Especificação de Formato

👉 DICA (https://docs.python.org/pt-br/3/library/string.html#formatspec) vemos que inteiros usam os códigos


'bcdoxXn' , floats usam 'eEfFgGn%' e strings usam 's' . Então escolhi 'p' para coordenadas
polares. Como cada classe interpreta esses códigos de forma independente, reutilizar uma letra em
um formato personalizado para um novo tipo não é um erro, mas pode ser confuso para os usuários.

Para gerar coordenadas polares, já temos o método __abs__ para a magnitude. Vamos então escrever um método
angle simples, usando a função math.atan2() , para obter o ângulo. Eis o código:

PYTHON3
# inside the Vector2d class

def angle(self):
return math.atan2(self.y, self.x)
Com isso, podemos agora aperfeiçoar nosso __format__ para gerar coordenadas polares. Veja o Exemplo 6.

Exemplo 6. O método Vector2d.__format__ , versão #2, agora com coordenadas polares

PY
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'): # (1)
fmt_spec = fmt_spec[:-1] # (2)
coords = (abs(self), self.angle()) # (3)
outer_fmt = '<{}, {}>' # (4)
else:
coords = self # (5)
outer_fmt = '({}, {})' # (6)
components = (format(c, fmt_spec) for c in coords) # (7)
return outer_fmt.format(*components) # (8)

1. O formato termina com 'p' : usa coordenadas polares.

2. Remove o sufixo 'p' de fmt_spec .

3. Cria uma tuple de coordenadas polares: (magnitude, angle) .

4. Configura o formato externo com chaves de ângulo.


5. Caso contrário, usa os componentes x, y de self para coordenadas retângulares.
6. Configura o formato externo com parênteses.
7. Gera um iterável cujos componentes são strings formatadas.
8. Insere as strings formatadas no fornato externo.

Com o Exemplo 6, obtemos resultados como esses:

PYCON
>>> format(Vector2d(1, 1), 'p')
'<1.4142135623730951, 0.7853981633974483>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'

Como mostrou essa seção, não é difícil estender a Mini-Linguagem de Especificação de Formato para suportar tipos
definidos pelo usuário.

Vamos agora passar a um assunto que vai além das aparências: tornar nosso Vector2d hashable, para podermos criar
conjuntos de vetores ou usá-los como chaves em um dict .

11.7. Um Vector2d hashable


Da forma como ele está definido até agora, as instâncias de nosso Vector2d não são hashable, então não podemos
colocá-las em um set :
PYCON
>>> v1 = Vector2d(3, 4)
>>> hash(v1)
Traceback (most recent call last):
...
TypeError: unhashable type: 'Vector2d'
>>> set([v1])
Traceback (most recent call last):
...
TypeError: unhashable type: 'Vector2d'

Para tornar um Vector2d hashable, precisamos implementar __hash__ ( __eq__ também é necessário, mas já temos
esse método). Além disso, precisamos tornar imutáveis as instâncias do vetor, como vimos na seção Seção 3.4.1.

Nesse momento, qualquer um pode fazer v1.x = 7 , e não há nada no código sugerindo que é proibido modificar um
Vector2d . O comportamento que queremos é o seguinte:

PYCON
>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 7
Traceback (most recent call last):
...
AttributeError: can't set attribute

Faremos isso transformando os componentes x e y em propriedades apenas para leitura no Exemplo 7.

Exemplo 7. vector2d_v3.py: apenas as mudanças necessárias para tornar Vector2d imutável são exibidas aqui; a
listagem completa está no Exemplo 11

PY
class Vector2d:
typecode = 'd'

def __init__(self, x, y):


self.__x = float(x) # (1)
self.__y = float(y)

@property # (2)
def x(self): # (3)
return self.__x # (4)

@property # (5)
def y(self):
return self.__y

def __iter__(self):
return (i for i in (self.x, self.y)) # (6)

# remaining methods: same as previous Vector2d

1. Usa exatamente dois sublinhados como prefixo (com zero ou um sublinhado como sufixo), para tornar um atributo
privado.[123]
2. O decorador @property marca o método getter de uma propriedade.
3. O método getter é nomeado de acordo com o nome da propriedade pública que ele expõe: x.

4. Apenas devolve self.__x .

5. Repete a mesma fórmula para a propriedade y.


6. Todos os métodos que apenas leem os componentes x e y podem permanecer como estavam, lendo as
propriedades públicas através de self.x e self.y em vez de usar os atributos privados. Então essa listagem
omite o restante do código da classe.
Vector.x e Vector.y são exemplos de propriedades apenas para leitura. Propriedades para
✒️ NOTA leitura/escrita serão tratadas no Capítulo 22, onde mergulhamos mais fundo no decorador
@property .

Agora que nossos vetores estão razoavelmente protegidos contra mutação acidental, podemos implementar o método
__hash__ . Ele deve devolver um int e, idealmente, levar em consideração os hashs dos atributos do objeto usados
também no método __eq__ , pois objetos que são considerados iguais ao serem comparados devem ter o mesmo hash.
A documentação (https://docs.python.org/pt-br/3/reference/datamodel.html#object.__hash__) do método especial __hash__
sugere computar o hash de uma tupla com os componentes, e é isso que fazemos no Exemplo 8.

Exemplo 8. vector2d_v3.py: implementação de hash

PYTHON3
# inside class Vector2d:

def __hash__(self):
return hash((self.x, self.y))

Com o acréscimo do método __hash__ , temos agora vetores hashable:

PYCON
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> hash(v1), hash(v2)
(1079245023883434373, 1994163070182233067)
>>> {v1, v2}
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

Não é estritamente necessário implementar propriedades ou proteger de alguma forma os atributos

👉 DICA de instância para criar um tipo hashable. Só é necessário implementar corretamente __hash__ e
__eq__ . Mas, supostamente, o valor de um objeto hashable nunca deveria mudar, então isso me dá
uma boa desculpa para falar sobre propriedades apenas para leitura.

Se você estiver criando um tipo com um valor numérico escalar que faz sentido, você pode também implementar os
métodos __int__ e __float__ , invocados pelos construtores int() e float() , que são usados, em alguns
contextos, para coerção de tipo. Há também o método __complex__ , para suportar o construtor embutido
complex() . Talvez Vector2d pudesse oferecer o __complex__ , mas deixo isso como um exercício para vocês.

11.8. Suportando o pattern matching posicional


Até aqui, instâncias de Vector2d são compatíveis com o pattern matching com instâncias de classe—vistos na Seção
5.8.2.

No Exemplo 9, todos aqueles padrões nomeados funcionam como esperado.

Exemplo 9. Padrões nomeados para sujeitos Vector2d —requer Python 3.10


PY
def keyword_pattern_demo(v: Vector2d) -> None:
match v:
case Vector2d(x=0, y=0):
print(f'{v!r} is null')
case Vector2d(x=0):
print(f'{v!r} is vertical')
case Vector2d(y=0):
print(f'{v!r} is horizontal')
case Vector2d(x=x, y=y) if x==y:
print(f'{v!r} is diagonal')
case _:
print(f'{v!r} is awesome')

Entretanto, se tentamos usar um padrão posicional, como esse:

PY
case Vector2d(_, 0):
print(f'{v!r} is horizontal')

o resultado é esse:

TypeError: Vector2d() accepts 0 positional sub-patterns (1 given)

Para fazer Vector2dfuncionar com padrões posicionais, precisamos acrescentar um atributo de classe chamado
__match_args__ , listando os atributos de instância na ordem em que eles serão usados no pattern matching
posicional.

PY
class Vector2d:
__match_args__ = ('x', 'y')

# etc...

Agora podemos economizar alguma digitação ao escrever padrões para usar contra sujeitos Vector2d , como se vê no
Exemplo 10.

Exemplo 10. Padrões posicionais para sujeitos Vector2d —requer Python 3.10

PY
def positional_pattern_demo(v: Vector2d) -> None:
match v:
case Vector2d(0, 0):
print(f'{v!r} is null')
case Vector2d(0):
print(f'{v!r} is vertical')
case Vector2d(_, 0):
print(f'{v!r} is horizontal')
case Vector2d(x, y) if x==y:
print(f'{v!r} is diagonal')
case _:
print(f'{v!r} is awesome')

O atributo de classe __match_args__ não precisa incluir todos os atributos públicos de instância. Em especial, se o
__init__ da classe tem argumentos obrigatórios e opcionais, que são depois vinculados a atributos de instância, pode
ser razoável nomear apenas os argumentos obrigatórios em __match_args__ , omitindo os opcionais.
Vamos dar um passo atrás e revisar tudo o que programamos até aqui no Vector2d .

11.9. Listagem completa Vector2d, versão 3


Já estamos trabalhando no Vector2d há algum tempo, mostrando apenas trechos isolados. O Exemplo 11 é uma
listagem completa e consolidada de vector2d_v3.py, incluindo os doctests que usei durante o desenvolvimento.

Exemplo 11. vector2d_v3.py: o pacote completo


PYTHON3
"""
A two-dimensional vector class

>>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y)
3.0 4.0
>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> print(v1)
(3.0, 4.0)
>>> octets = bytes(v1)
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1)
5.0
>>> bool(v1), bool(Vector2d(0, 0))
(True, False)

Test of ``.frombytes()`` class method:

>>> v1_clone = Vector2d.frombytes(bytes(v1))


>>> v1_clone
Vector2d(3.0, 4.0)
>>> v1 == v1_clone
True

Tests of ``format()`` with Cartesian coordinates:

>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'

Tests of the ``angle`` method::

>>> Vector2d(0, 0).angle()


0.0
>>> Vector2d(1, 0).angle()
0.0
>>> epsilon = 10**-8
>>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
True
>>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
True

Tests of ``format()`` with polar coordinates:

>>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS


'<1.414213..., 0.785398...>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'
Tests of `x` and `y` read-only properties:

>>> v1.x, v1.y


(3.0, 4.0)
>>> v1.x = 123
Traceback (most recent call last):
...
AttributeError: can't set attribute 'x'

Tests of hashing:

>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> len({v1, v2})
2

"""

from array import array


import math

class Vector2d:
__match_args__ = ('x', 'y')

typecode = 'd'

def __init__(self, x, y):


self.__x = float(x)
self.__y = float(y)

@property
def x(self):
return self.__x

@property
def y(self):
return self.__y

def __iter__(self):
return (i for i in (self.x, self.y))

def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)

def __str__(self):
return str(tuple(self))

def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(array(self.typecode, self)))

def __eq__(self, other):


return tuple(self) == tuple(other)

def __hash__(self):
return hash((self.x, self.y))

def __abs__(self):
return math.hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))

def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'):
fmt_spec = fmt_spec[:-1]
coords = (abs(self), self.angle())
outer_fmt = '<{}, {}>'
else:
coords = self
outer_fmt = '({}, {})'
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(*components)

@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)

Recordando, nessa seção e nas anteriores vimos alguns dos métodos especiais essenciais que você pode querer
implementar para obter um objeto completo.

Você deve implementar esses métodos especiais apenas se sua aplicação precisar deles. Os usuários
finais não se importam se os objetos que compõem uma aplicação são pythônicos ou não.
✒️ NOTA Por outro lado, se suas classes são parte de uma biblioteca para ser usada por outros programadores
Python, você não tem como adivinhar como eles vão usar seus objetos. E eles estarão esperando ver
esses comportamentos pythônicos que descrevemos aqui.

Como programado no Exemplo 11, Vector2d é um exemplo didático com uma lista extensiva de métodos especiais
relacionados à representação de objetos, não um modelo para qualquer classe definida pelo usuário.

Na próxima seção, deixamos o Vector2d de lado por um tempo para discutir o design e as desvantagens do
mecanismo de atributos privados no Python—o prefixo de duplo sublinhado em self.__x .

11.10. Atributos privados e "protegidos" no Python


Em Python, não há como criar variáveis privadas como as criadas com o modificador private no Java. O que temos
no Python é um mecanismo simples para prevenir que um atributo "privado" em uma subclasse seja acidentalmente
sobrescrito.

Considere o seguinte cenário: alguém escreveu uma classe chamada Dog , que usa um atributo de instância mood
internamente, sem expô-lo. Você precisa criar a uma subclasse Beagle de Dog . Se você criar seu próprio atributo de
instância mood , sem saber da colisão de nomes, vai afetar o atributo mood usado pelos métodos herdados de Dog . Isso
seria bem complicado de depurar.

Para prevenir esse tipo de problema, se você nomear o atributo de instância no formato __mood (dois sublinhados
iniciais e zero ou no máximo um sublinhado no final), o Python armazena o nome no __dict__ da instância,
prefixado com um sublinhado seguido do nome da classe. Na classe Dog , por exemplo, __mood se torna _Dog__mood
e em Beagle ele será _Beagle__mood .

Esse recurso da linguagem é conhecido pela encantadora alcunha de desfiguração de nome ("name mangling").

O Exemplo 12 mostra o resultado na classe Vector2d do Exemplo 7.

Exemplo 12. Nomes de atributos privados são "desfigurados", prefixando-os com o _ e o nome da classe
PYCON
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}
>>> v1._Vector2d__x
3.0

A desfiguração do nome é sobre alguma proteção, não sobre segurança: ela foi projetada para evitar acesso acidental,
não ataques maliciosos. A Figura 1 ilustra outro dipositivo de proteção.

Qualquer um que saiba como os nomes privados são modificados pode ler o atributo privado diretamente, como
mostra a última linha do Exemplo 12—isso na verdade é útil para depuração e serialização. Isso também pode ser
usado para atribuir um valor a um componente privado de um Vector2d , escrevendo v1._Vector2d__x = 7 . Mas se
você estiver fazendo isso com código em produção, não poderá reclamar se alguma coisa explodir.

A funcionalidade de desfiguração de nomes não é amada por todos os pythonistas, nem tampouco a aparência
estranha de nomes escritos como self.__x . Muitos preferem evitar essa sintaxe e usar apenas um sublinhado no
prefixo para "proteger" atributos da forma convencional (por exemplo, self._x ). Críticos da desfiguração automática
com o sublinhado duplo sugerem que preocupações com modificações acidentais a atributos deveriam ser tratadas
através de convenções de nomenclatura. Ian Bicking—criador do pip, do virtualenv e de outros projetos—escreveu:

“ Nunca, de forma alguma, use dois sublinhados como prefixo. Isso é irritantemente privado. Se
colisão de nomes for uma preocupação, use desfiguração explícita de nomes em seu lugar (por
exemplo, _MyThing_blahblah ). Isso é essencialmente a mesma coisa que o sublinhado duplo,
mas é transparente onde o sublinhado duplo é obscuro.[124]

Figura 1. Uma cobertura sobre um interruptor é um dispositivo de proteção, não de segurança: ele previne acidentes,
não sabotagem

O prefixo de sublinhado único não tem nenhum significado especial para o interpretador Python, quando usado em
nomes de atributo. Mas essa é uma convenção muito presente entre programadores Python: tais atributos não devem
ser acessados de fora da classe.[125] É fácil respeitar a privacidade de um objeto que marca seus atributos com um
único _ , da mesma forma que é fácil respeitar a convenção de tratar como constantes as variáveis com nomes
inteiramente em maiúsculas.
Atributos com um único _ como prefixo são chamados "protegidos" em algumas partes da documentação do Python.
[126] A prática de "proteger" atributos por convenção com a forma self._x é muito difundida, mas chamar isso de
atributo "protegido" não é tão comum. Alguns até falam em atributo "privado" nesses casos.

Concluindo: os componentes de Vector2d são "privados" e nossas instâncias de Vector2d são "imutáveis"—com
aspas irônicas—pois não há como tornar uns realmente privados e outras realmente imutáveis.[127]

Vamos agora voltar à nossa classe Vector2d . Na próxima seção trataremos de um atributo (e não um método) especial
que afeta o armazenamento interno de um objeto, com um imenso impacto potencial sobre seu uso de memória, mas
pouco efeito sobre sua interface pública: __slots__ .

11.11. Economizando memória com __slots__


Por default, o Python armazena os atributos de cada instância em um dict chamado __dict__ . Como vimos em
Seção 3.9, um dict ocupa um espaço significativo de memória, mesmo com as otimizações mencionadas naquela
seção. Mas se você definir um atributo de classe chamado __slots__ , que mantém uma sequência de nomes de
atributos, o Python usará um modelo alternativo de armazenamento para os atributos de instância: os atributos
nomeados em __slots__ serão armazenados em um array de referências oculto, que usa menos memória que um
dict . Vamos ver como isso funciona através de alguns exemplos simples, começando pelo Exemplo 13.

Exemplo 13. A classe Pixel usa __slots__

PYCON
>>> class Pixel:
... __slots__ = ('x', 'y') # (1)
...
>>> p = Pixel() # (2)
>>> p.__dict__ # (3)
Traceback (most recent call last):
...
AttributeError: 'Pixel' object has no attribute '__dict__'
>>> p.x = 10 # (4)
>>> p.y = 20
>>> p.color = 'red' # (5)
Traceback (most recent call last):
...
AttributeError: 'Pixel' object has no attribute 'color'

1. __slots__ deve estar presente quando a classe é criada; acrescentá-lo ou modificá-lo posteriormente não tem
qualquer efeito. Os nomes de atributos podem estar em uma tuple ou em uma list . Prefiro usar uma tuple ,
para deixar claro que não faz sentido modificá-la.
2. Cria uma instância de Pixel , pois os efeitos de __slots__ são vistos nas instâncias.
3. Primeiro efeito: instâncias de Pixel não têm um __dict__ .

4. Define normalmente os atributos p.x e p.y .

5. Segundo efeito: tentar definir um atributo não listado em __slots__ gera um AttributeError .

Até aqui, tudo bem. Agora vamos criar uma subclasse de Pixel , no Exemplo 14, para ver o lado contraintuitivo de
__slots__ .

Exemplo 14. OpenPixel é uma subclasse de Pixel


PYCON
>>> class OpenPixel(Pixel): # (1)
... pass
...
>>> op = OpenPixel()
>>> op.__dict__ # (2)
{}
>>> op.x = 8 # (3)
>>> op.__dict__ # (4)
{}
>>> op.x # (5)
8
>>> op.color = 'green' # (6)
>>> op.__dict__ # (7)
{'color': 'green'}

1. OpenPixel não declara qualquer atributo próprio.


2. Surpresa: instâncias de OpenPixel têm um __dict__ .

3. Se você definir o atributo x (nomeado no __slots__ da classe base Pixel )…​

4. …​ele não será armazenado no __dict__ da instância…​


5. …​mas sim no array oculto de referências na instância.
6. Se você definir um atributo não nomeado no __slots__ …​

7. …​ele será armazenado no __dict__ da instância.

O Exemplo 14 mostra que o efeito de __slots__ é herdado apenas parcialmente por uma subclasse. Para se assegurar
que instâncias de uma subclasse não tenham o __dict__ , é preciso declarar __slots__ novamente na subclasse.

Se você declarar __slots__ = () (uma tupla vazia), as instâncias da subclasse não terão um __dict__ e só aceitarão
atributos nomeados no __slots__ da classe base.

Se você quiser que uma subclasse tenha atributos adicionais, basta nomeá-los em __slots__ , como mostra o Exemplo
15.

Exemplo 15. The ColorPixel , another subclass of Pixel

PYCON
>>> class ColorPixel(Pixel):
... __slots__ = ('color',) # (1)
>>> cp = ColorPixel()
>>> cp.__dict__ # (2)
Traceback (most recent call last):
...
AttributeError: 'ColorPixel' object has no attribute '__dict__'
>>> cp.x = 2
>>> cp.color = 'blue' # (3)
>>> cp.flavor = 'banana'
Traceback (most recent call last):
...
AttributeError: 'ColorPixel' object has no attribute 'flavor'

1. Em resumo, o __slots__ da superclasse é adicionado ao __slots__ da classe atual. Não esqueça que tuplas com
um único elemento devem ter uma vírgula no final.
2. Instâncias de ColorPixel não tem um __dict__ .
3. Você pode definir atributos declarados no __slots__ dessa classe e nos de suas superclasses, mas nenhum outro.
Curiosamente, também é possível colocar o nome '+dict+' em __slots__ . Neste caso, as instâncias vão manter os
atributos nomeados em __slots__ num array de referências da instância, mas também vão aceitar atributos criados
dinamicamente, que serão armazenados no habitual __dict__ . Isso é necessário para usar o decorador
@cached_property (tratado na seção Seção 22.3.5).

Naturalmente, incluir __dict__ em __slots__ pode desviar completamente do objetivo deste último, dependendo
do número de atributos estáticos e dinâmicos em cada instância, e de como eles são usados. Otimização descuidada é
pior que otimização prematura: adiciona complexidade sem colher qualquer benefício.

Outro atributo de instância especial que você pode querer manter é __weakref__ , necessário para que objetos
suportem referências fracas (mencionadas brevemente na seção Seção 6.6). Esse atributo existe por default em
instâncias de classes definidas pelo usuário. Entretanto, se a classe define __slots__ , e é necessário que as instâncias
possam ser alvo de referências fracas, então é preciso incluir __weakref__ entre os atributos nomeados em
__slots__ .

Vejamos agora o efeito da adição de __slots__ a Vector2d .

11.11.1. Uma medida simples da economia gerada por __slots__


Exemplo 16 mostra a implementação de __slots__ em Vector2d .

Exemplo 16. vector2d_v3_slots.py: o atributo __slots__ é a única adição a Vector2d

PY
class Vector2d:
__match_args__ = ('x', 'y') # (1)
__slots__ = ('__x', '__y') # (2)

typecode = 'd'
# methods are the same as previous version

1. __match_args__ lista os nomes dos atributos públicos, para pattern matching posicional.
2. __slots__ , por outro lado, lista os nomes dos atributos de instância, que neste caso são atributos privados.

Para medir a economia de memória, escrevi o script mem_test.py. Ele recebe, como argumento de linha de comando, o
nome de um módulo com uma variante da classe Vector2d , e usa uma compreensão de lista para criar uma list
com 10.000.000 de instâncias de Vector2d . Na primeira execução, vista no Exemplo 17, usei vector2d_v3.Vector2d
(do Exemplo 7); na segunda execução usei a versão com __slots__ do Exemplo 16.

Exemplo 17. mem_test.py cria 10 milhões de instâncias de Vector2d , usando a classe definida no módulo nomeado
BASH
$ time python3 mem_test.py vector2d_v3
Selected Vector2d type: vector2d_v3.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 6,983,680
Final RAM usage: 1,666,535,424

real 0m11.990s
user 0m10.861s
sys 0m0.978s
$ time python3 mem_test.py vector2d_v3_slots
Selected Vector2d type: vector2d_v3_slots.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 6,995,968
Final RAM usage: 577,839,104

real 0m8.381s
user 0m8.006s
sys 0m0.352s

Como revela o Exemplo 17, o uso de RAM do script cresce para 1,55 GB quando o __dict__ de instância é usado em
cada uma das 10 milhões de instâncias de Vector2d , mas isso se reduz a 551 MB quando Vector2d tem um atributo
__slots__ . A versão com __slots__ também é mais rápida. O script mem_test.py neste teste lida basicamente com o
carregamento do módulo, a medição da memória utilizada e a formatação de resultados. O código-fonte pode ser
encontrado no repositório fluentpython/example-code-2e (https://fpy.li/11-11).

Se você precisa manipular milhões de objetos com dados numéricos, deveria na verdade estar
usando os arrays do NumPy (veja a seção Seção 2.10.3), que são eficientes no de uso de memória, e

👉 DICA também tem funções para processamento numérico extremamente otimizadas, muitas das quais
operam sobre o array inteiro ao mesmo tempo. Projetei a classe Vector2d apenas como um
contexto para a discussão de métodos especiais, pois sempre que possível tento evitar exemplos
vagos com Foo e Bar .

11.11.2. Resumindo os problemas com __slots__


O atributo de classe __slots__ pode proporcionar uma economia significativa de memória se usado corretamente,
mas existem algumas ressalvas:

É preciso lembrar de redeclarar __slots__ em cada subclasse, para evitar que suas instâncias tenham um
__dict__ .

Instâncias só poderão ter os atributos listados em __slots__ , a menos que __dict__ seja incluído em __slots__
(mas isso pode anular a economia de memória).
Classe que usam __slots__ não podem usar o decorador @cached_property , a menos que nomeiem __dict__
explicitamente em __slots__ .
Instâncias não podem ser alvo de referências fracas, a menos que __weakref__ seja incluído em __slots__ .

O último tópico do capítulo trata da sobreposição de um atributo de classe em instâncias e subclasses.

11.12. Sobrepondo atributos de classe


Um recurso característico do Python é a forma como atributos de classe podem ser usados como valores default para
atributos de instância. Vector2d contém o atributo de classe typecode . Ele é usado duas vezes no método
__bytes__ , mas é lido intencionalmente como self.typecode . As instâncias de Vector2d são criadas sem um
atributo typecode próprio, então self.typecode vai, por default, se referir ao atributo de classe
Vector2d.typecode .
Mas se incluirmos um atributo de instância que não existe, estamos criando um novo atributo de instância—por
exemplo, um atributo de instância typecode —e o atributo de classe com o mesmo nome permanece intocado.
Entretanto, daí em diante, sempre que algum código referente àquela instância contiver self.typecode , o typecode
da instância será usado, na prática escondendo o atributo de classe de mesmo nome. Isso abre a possibilidade de
personalizar uma instância individual com um typecode diferente.

O Vector2d.typecode default é 'd' : isso significa que cada componente do vetor será representado como um
número de ponto flutuante de dupla precisão e 8 bytes de tamanho quando for exportado para bytes . Se definirmos o
typecode de uma instância Vector2d como 'f' antes da exportação, cada componente será exportado como um
número de ponto flutuante de precisão simples e 4 bytes de tamanho.. O Exemplo 18 demonstra isso.

✒️ NOTA Estamos falando do acréscimo de um atributo de instância, assim o Exemplo 18 usa a


implementação de Vector2d sem __slots__ , como aparece no Exemplo 11.

Exemplo 18. Personalizando uma instância pela definição do atributo typecode , que antes era herdado da classe

PYCON
>>> from vector2d_v3 import Vector2d
>>> v1 = Vector2d(1.1, 2.2)
>>> dumpd = bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'
>>> len(dumpd) # (1)
17
>>> v1.typecode = 'f' # (2)
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf) # (3)
9
>>> Vector2d.typecode # (4)
'd'

1. A representação default em bytes tem 17 bytes de comprimento.


2. Define typecode como 'f' na instância v1 .

3. Agora bytes tem 9 bytes de comprimento.


4. Vector2d.typecode não foi modificado; apenas a instância v1 usa o typecode 'f' .

Isso deixa claro porque a exportação para bytes de um Vector2d tem um prefixo typecode : queríamos suportar
diferentes formatos de exportação.

Para modificar um atributo de classe, é preciso redefiní-lo diretamente na classe, e não através de uma instância.
Poderíamos modificar o typecode default para todas as instâncias (que não tenham seu próprio typecode ) assim:

PYCON
>>> Vector2d.typecode = 'f'

Porém, no Python, há uma maneira idiomática de obter um efeito mais permanente, e de ser mais explícito sobre a
modificação. Como atributos de classe são públicos, eles são herdados por subclasses. Então é uma prática comum
fazer a subclasse personalizar um atributo da classe. As views baseadas em classes do Django usam amplamente essa
técnica. O Exemplo 19 mostra como se faz.
Exemplo 19. O ShortVector2d é uma subclasse de Vector2d , que apenas sobrepõe o typecode default

PYCON
>>> from vector2d_v3 import Vector2d
>>> class ShortVector2d(Vector2d): # (1)
... typecode = 'f'
...
>>> sv = ShortVector2d(1/11, 1/27) # (2)
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035) # (3)
>>> len(bytes(sv)) # (4)
9

1. Cria ShortVector2d como uma subclasse de Vector2d apenas para sobrepor o atributo de classe typecode .

2. Cria sv , uma instância de ShortVector2d , para demonstração.

3. Verifica o repr de sv .

4. Verifica que a quantidade de bytes exportados é 9, e não 17 como antes.

Esse exemplo também explica porque não escrevi explicitamente o class_name em __repr__ , optando
Vector2d.​
por obtê-lo de type(self).__name__ , assim:

PYTHON3
# inside class Vector2d:

def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)

Se eu tivesse escrito o class_name explicitamente, subclasses de Vector2d como ShortVector2d teriam que
sobrescrever __repr__ só para mudar o class_name . Lendo o nome do type da instância, tornei __repr__ mais
seguro de ser herdado.

Aqui termina nossa conversa sobre a criação de uma classe simples, que se vale do modelo de dados para se adaptar
bem ao restante do Python: oferecendo diferentes representações do objeto, fornecendo um código de formatação
personalizado, expondo atributos somente para leitura e suportando hash() para se integrar a conjuntos e
mapeamentos.

11.13. Resumo do capítulo


O objetivo desse capítulo foi demonstrar o uso dos métodos especiais e as convenções na criação de uma classe
pythônica bem comportada.

Será vector2d_v3.py (do Exemplo 11) mais pythônica que vector2d_v0.py (do Exemplo 2)? A classe Vector2d em
vector2d_v3.py com certeza utiliza mais recursos do Python. Mas decidir qual das duas implementações de Vector2d é
mais adequada, a primeira ou a última, depende do contexto onde a classe será usada. O "Zen of Python" (Zen do
Python), de Tim Peter, diz:

“ Simples é melhor que complexo.


Um objeto deve ser tão simples quanto seus requerimentos exigem—e não um desfile de recursos da linguagem. Se o
código for parte de uma aplicação, ele deveria se concentrar naquilo que for necessário para suportar os usuários
finais, e nada mais. Se o código for parte de uma biblioteca para uso por outros programadores, então é razoável
implementar métodos especiais que suportam comportamentos esperados por pythonistas. Por exemplo, __eq__ pode
não ser necessário para suportar um requisito do negócio, mas torna a classe mais fácil de testar.
Minha meta, ao expandir o código do Vector2d , foi criar um contexto para a discussão dos métodos especiais e das
convenções de programação em Python. Os exemplos neste capítulo demonstraram vários dos métodos especiais vistos
antes na Tabela 1 (do Capítulo 1):

Métodos de representação de strings e bytes: __repr__ , __str__ , __format__ e __bytes__

Métodos para reduzir um objeto a um número: __abs__ , __bool__ e __hash__

O operador __eq__ , para suportar testes e hashing (juntamente com __hash__ )

Quando suportamos a conversão para bytes , também implementamos um construtor alternativo,


Vector2d.frombytes() , que nos deu um contexto para falar dos decoradores @classmethod (muito conveniente) e
@staticmethod (não tão útil: funções a nível do módulo são mais simples). O método frombytes foi inspirado pelo
método de mesmo nome na classe array.array .

Vimos que a Mini-Linguagem de Especificação de Formato (https://docs.python.org/pt-br/3/library/string.html#formatspec) é


extensível, ao implementarmos um método __format__ que analisa uma format_spec fornecida à função embutida
format(obj, format_spec) ou dentro de campos de substituição '{:«format_spec»}' em f-strings ou ainda strings
usadas com o método str.format() .

Para preparar a transformação de instâncias de Vector2d em hashable, fizemos um esforço para torná-las imutáveis,
ao menos prevenindo modificações acidentais, programando os atributos x e y como privados, e expondo-os como
propriedades apenas para leitura. Nós então implementamos __hash__ usando a técnica recomendada, aplicar o
operador xor aos hashes dos atributos da instância.

Discutimos a seguir a economia de memória e as ressalvas de se declarar um atributo __slots__ em Vector2d .


Como o uso de __slots__ tem efeitos colaterais, ele só faz real sentido quando é preciso processar um número muito
grande de instâncias—pense em milhões de instâncias, não apenas milhares. Em muitos destes casos, usar a pandas
(https://fpy.li/pandas) pode ser a melhor opção.

O último tópico tratado foi a sobreposição de um atributo de classe acessado através das instâncias (por exemplo,
self.typecode ). Fizemos isso primeiro criando um atributo de instância, depois criando uma subclasse e
sobrescrevendo o atributo no nível da classe.

Por todo o capítulo, apontei como escolhas de design nos exemplos foram baseadas no estudo das APIs dos objetos
padrão do Python. Se esse capítulo pode ser resumido em uma só frase, seria essa:

“ Para criar objetos pythônicos, observe como se comportam objetos reais do Python.
— Antigo provérbio chinês

11.14. Leitura complementar


Este capítulo tratou de vários dos métodos especiais do modelo de dados, então naturalmente as referências primárias
são as mesmas do Capítulo 1, onde tivemos uma ideia geral do mesmo tópico. Por conveniência, vou repetir aquelas
quatro recomendações anteriores aqui, e acrescentar algumas outras:

O capítulo "Modelo de Dados" (https://docs.python.org/pt-br/3/reference/datamodel.html) em A Referência da


Linguagem Python
A maioria dos métodos usados nesse capítulo estão documentados em "3.3.1. Personalização básica"
(https://docs.python.org/pt-br/3/reference/datamodel.html#basic-customization).

Python in a Nutshell, 3ª ed., (https://fpy.li/pynut3) de Alex Martelli, Anna Ravenscroft, e Steve Holden
Trata com profundidade dos métodos especiais .
Python Cookbook, 3ª ed. (https://fpy.li/pycook3), de David Beazley e Brian K. Jones
Práticas modernas do Python demonstradas através de receitas. Especialmente o Capítulo 8, "Classes and Objects"
(Classes e Objetos), que contém várias receitas relacionadas às discussões deste capítulo.

Python Essential Reference, 4ª ed., de David Beazley


Trata do modelo de dados em detalhes, apesar de falar apenas do Python 2.6 e do 3.0 (na quarta edição). Todos os
conceitos fundamentais são os mesmos, e a maior parte das APIs do Modelo de Dados não mudou nada desde o
Python 2.2, quando os tipos embutidos e as classes definidas pelo usuário foram unificados.
Em 2015—o ano que terminei a primeira edição de Python Fluente—Hynek Schlawack começou a desenvolver o pacote
attrs . Da documentação de attrs :

“ do tediosoé umtrabalho
attrs pacote Python que vai trazer de volta a de alegria , liberando você
criar classes
de implementar protocolos de objeto (também conhecidos como métodos
dunder)

Mencionei attrs como uma alternativa mais poderosa ao @dataclass na Seção 5.10. As fábricas de classes de dados
do Capítulo 5, assim como attrs , equipam suas classes automaticamente com vários métodos especiais. Mas saber
como programar métodos especiais ainda é essencial para entender o que aqueles pacotes fazem, para decidir se você
realmente precisa deles e para—quando necessário—sobrescrever os métodos que eles geram.

Vimos neste capítulo todos os métodos especiais relacionados à representação de objetos, exceto __index__ e
__fspath__ . Discutiremos __index__ no Capítulo 12, na seção Seção 12.5.2. Não vou tratar de __fspath__ . Para
aprender sobre esse método, veja a PEP 519—Adding a file system path protocol (Adicionando um protocolo de caminho
de sistema de arquivos) (https://fpy.li/pep519) (EN).

Uma percepção precoce da necessidade de strings de representação diferentes para objetos apareceu no Smalltalk. O
artigo de 1996 "How to Display an Object as a String: printString and displayString" (Como Mostrar um Objeto como
uma String: printString and displayString) (https://fpy.li/11-13) (EN), de Bobby Woolf, discute a implementação dos métodos
printString e displayString naquela linguagem. Foi desse artigo que peguei emprestado as expressivas descrições
"como o desenvolvedor quer vê-lo" e "como o usuário quer vê-lo" para definir repr() e str() , na seção Seção 11.2.

Ponto de Vista
Propriedades ajudam a reduzir custos iniciais

Nas primeiras versões de Vector2d , os atributos x e y eram públicos, como são, por default, todos os atributos
de instância e classe no Python. Naturalmente, os usuários de vetores precisam acessar seus componentes.
Apesar de nossos vetores serem iteráveis e poderem ser desempacotados em um par de variáveis, também é
desejável poder escrever my_vector.x e my_vector.y para obter cada componente.

Quando sentimos a necessidade de evitar modificações acidentais dos atributos x e y , implementamos


propriedades, mas nada mudou no restante do código ou na interface pública de Vector2d , como se verifica
através dos doctests. Continuamos podendo acessar my_vector.x and my_vector.y .

Isso mostra que podemos sempre iniciar o desenvolvimento de nossas classes da maneira mais simples possível,
com atributos públicos, pois quando (ou se) nós mais tarde precisarmos impor mais controle, com getters e
setters, estes métodos podem ser implementados usando propriedades, sem mudar nada no código que já
interage com nossos objetos através dos nomes que eram, inicialmente, simples atributos públicos ( x e y , por
exemplo).
Essa abordagem é o oposto daquilo que é encorajado pela linguagem Java: um programador Java não pode
começar com atributos públicos simples e apenas mais tarde, se necessário, implementar propriedades, porque
elas não existem naquela linguagem. Portanto, escrever getters e setters é a regra em Java—mesmo quando esses
métodos não fazem nada de útil—porque a API não pode evoluir de atributos públicos simples para getters e
setters sem quebrar todo o código que já use aqueles atributos.

Além disso, como Martelli, Ravenscroft e Holden observam no Python in a Nutshell, 3rd ed. (https://fpy.li/pynut3),
digitar chamadas a getters e setters por toda parte é patético. Você é obrigado a escrever coisas como:

PYTHON3
>>> my_object.set_foo(my_object.get_foo() + 1)

Apenas para fazer isso:

PYTHON3
>>> my_object.foo += 1

Ward Cunningham, inventor do wiki e um pioneiro da Programação Extrema (Extreme Programming),


recomenda perguntar: "Qual a coisa mais simples que tem alguma chance de funcionar?" A ideia é se concentrar
no objetivo.[128] Implementar setters e getters desde o início é uma distração em relação ao objetivo Em Python,
podemos simplesmente usar atributos públicos, sabendo que podemos transformá-los mais tarde em
propriedades, se essa necessidade surgir .

Proteção versus segurança em atributos privados

“ OemPerlsuanãosalatemdenenhum amor por privacidade forçada. Ele preferiria que você não entrasse
estar [apenas] por não ter sido convidado, e não porque ele tem uma
espingarda.
— Larry Wall
criador do Perl

O Python e o Perl estão em pólos opostos em vários aspectos, mas Guido e Larry parecem concordar sobre a
privacidade de objetos.

Ensinando Python para muitos programadores Java ao longo do anos, descobri que muitos deles tem uma fé
excessiva nas garantias de privacidade oferecidas pelo Java. E na verdade, os modificadores private e
protected do Java normalmente fornecem defesas apenas contra acidentes (isto é, proteção). Eles só oferecem
segurança contra ataques mal-intencionados se a aplicação for especialmente configurada e implantada sob um
SecurityManager (https://fpy.li/11-15) (EN) do Java, e isso raramente acontece na prática, mesmo em configurações
corporativas atentas à segurança.

Para provar meu argumento, gostaria de mostrar essa classe Java (o Exemplo 20).

Exemplo 20. Confidential.java: uma classe Java com um campo privado chamado secret
JAVA
public class Confidential {

private String secret = "";

public Confidential(String text) {


this.secret = text.toUpperCase();
}
}

No Exemplo 20, armazeno o text no campo secret após convertê-lo todo para caixa alta, só para deixar óbvio
que qualquer coisa que esteja naquele campo estará escrito inteiramente em maiúsculas.

A verdadeira demonstração consiste em rodar expose.py com Jython. Aquele script usa introspecção ("reflexão"—
reflection—no jargão do Java) para obter o valor de um campo privado. O código aparece no Exemplo 21.

Exemplo 21. expose.py: código em Jython para ler o conteúdo de um campo privado em outra classe

PYTHON3
#!/usr/bin/env jython
# NOTE: Jython is still Python 2.7 in late2020

import Confidential

message = Confidential('top secret text')


secret_field = Confidential.getDeclaredField('secret')
secret_field.setAccessible(True) # break the lock!
print 'message.secret =', secret_field.get(message)

Executando o Exemplo 21, o resultado é esse:

BASH
$ jython expose.py
message.secret = TOP SECRET TEXT

A string 'TOP SECRET TEXT' foi lida do campo privado secret da classe Confidential .

Não há magia aqui: expose.py usa a API de reflexão do Java para obter uma referência para o campo privado
chamado 'secret' , e então chama secret_field.setAccessible(True) para tornar acessível seu conteúdo.
A mesma coisa pode ser feita com código Java, claro (mas exige mais que o triplo de linhas; veja o arquivo
Expose.java (https://fpy.li/11-16) no repositório de código do Python Fluente (https://fpy.li/code)).

A chamada .setAccessible(True) só falhará se o script Jython ou o programa principal em Java (por exemplo,
a Expose.class ) estiverem rodando sob a supervisão de um SecurityManager (https://fpy.li/11-15) (EN). Mas, no
mundo real, aplicações Java raramente são implantadas com um SecurityManager —com a exceção das applets
Java, quando elas ainda era suportadas pelos navegadores.

Meu ponto: também em Java, os modificadores de controle de acesso são principalmente sobre proteção e não
segurança, pelo menos na prática. Então relaxe e aprecie o poder dado a você pelo Python. E use esse poder com
responsabilidade.
12. Métodos especiais para sequências
“ Não queira saber se aquilo é-um pato: veja se ele grasna-como-um pato, anda-como-um pato,
etc., etc., dependendo de qual subconjunto exato de comportamentos de pato você precisa para
usar em seus jogos de linguagem. ( comp.lang.python , Jul. 26, 2000)
— Alex Martelli

Neste capítulo, vamos criar uma classe Vector , para representar um vetor multidimensional—um avanço
significativo sobre o Vector2D bidimensional do Capítulo 11. Vector vai se comportar como uma simples sequência
imutável padrão do Python. Seus elementos serão números de ponto flutuante, e ao final do capítulo a classe suportará
o seguinte:

O protocolo de sequência básico: __len__ e __getitem__

Representação segura de instâncias com muitos itens


Suporte adequado a fatiamento, produzindo novas instâncias de Vector

Hashing agregado, levando em consideração o valor de cada elemento contido na sequência


Um extensão personalizada da linguagem de formatação

Também vamos implementar, com __getattr__ , o acesso dinâmico a atributos, como forma de substituir as
propriedades apenas para leitura que usamos no Vector2d —apesar disso não ser típico de tipos sequência.

Nossa apresentação voltada para o código será interrompida por uma discussão conceitual sobre a ideia de protocolos
como uma interface informal. Vamos discuitr a relação entre protocolos e duck typing, e as implicações práticas disso
na criação de seus próprios tipos

12.1. Novidades nesse capítulo


Não ocorreu qualquer grande modificação neste capítulo. Há uma breve discussão nova sobre o typing.Protocol em
um quadro de dicas, no final da seção Seção 12.4.

Na seção Seção 12.5.2, a implementação do __getitem__ no Exemplo 6 está mais concisa e robusta que o exemplo na
primeira edição, graças ao duck typing e ao operator.index . Essa mudança foi replicada para as implementações
seguintes de Vector aqui e no Capítulo 16.

Vamos começar.

12.2. Vector: Um tipo sequência definido pelo usuário


Nossa estratégia na implementação de Vector será usar composição, não herança. Vamos armazenar os componentes
em um array de números de ponto flutuante, e implementar os métodos necessários para que nossa classe Vector se
comporte como uma sequência plana imutável.

Mas antes de implementar os métodos de sequência, vamos desenvolver uma implementação básica de Vector
compatível com nossa classe Vector2d , vista anteriormente—​exceto onde tal compatibilidade não fizer sentido.
Aplicações de vetores além de três dimensões
Quem precisa de vetores com 1.000 dimensões? Vetores N-dimensionais (com valores grandes de N) são bastante
utilizados em recuperação de informação, onde documentos e consultas textuais são representados como vetores,
com uma dimensão para cada palavra. Isso se chama Modelo de Espaço Vetorial (https://fpy.li/12-1) (EN). Nesse
modelo, a métrica fundamental de relevância é a similaridade de cosseno—o cosseno do ângulo entre o vetor
representando a consulta e o vetor representando o documento. Conforme o ângulo diminui, o valor do cosseno
aumenta, indicando a relevância do documento para aquela consulta: cosseno próximo de 0 significa pouca
relevância; próximo de 1 indica alta relevância.

Dito isto, a classe Vector nesse capítulo é um exemplo didático. O objetivo é apenas demonstrar alguns métodos
especiais do Python no contexto de um tipo sequência, sem grandes conceitos matemáticos.

A NumPy e a SciPy são as ferramentas que você precisa para fazer cálculos vetoriais em aplicações reais. O pacote
gensim (https://fpy.li/12-2) do PyPi, de Radim Řehůřek, implementa a modelagem de espaço vetorial para
processamento de linguagem natural e recuperação de informação, usando a NumPy e a SciPy.

12.3. Vector versão #1: compatível com Vector2d


A primeira versão de Vector deve ser tão compatível quanto possível com nossa classe Vector2d desenvolvida
anteriormente.

Entretanto, pela própria natureza das classes, o construtor de Vector não é compatível com o construtor de
Vector2d . Poderíamos fazer Vector(3, 4) e Vector(3, 4, 5) funcionarem, recebendo argumentos arbitrários
com *args em __init__ . Mas a melhor prática para um construtor de sequências é receber os dados através de um
argumento iterável, como fazem todos os tipos embutidos de sequências. O Exemplo 1 mostra algumas maneiras de
instanciar objetos do nosso novo Vector .

Exemplo 1. Testes de Vector.__init__ e Vector.__repr__

PYCON
>>> Vector([3.1, 4.2])
Vector([3.1, 4.2])
>>> Vector((3, 4, 5))
Vector([3.0, 4.0, 5.0])
>>> Vector(range(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

Exceto pela nova assinatura do construtor, me assegurei que todos os testes realizados com Vector2d (por exemplo,
Vector2d(3, 4) ) são bem sucedidos e produzem os mesmos resultados com um Vector de dois componentes, como
Vector([3, 4]) .

Quando um Vector tem mais de seis componentes, a string produzida por repr() é abreviada
com …​, como visto na última linha do Exemplo 1. Isso é fundamental para qualquer tipo de coleção

⚠️ AVISO que possa conter um número grande de itens, pois repr é usado na depuração—e você não quer
que um único objeto grande ocupe milhares de linhas em seu console ou arquivo de log. Use o
módulo reprlib para produzir representações de tamanho limitado, como no Exemplo 2. O
módulo reprlib se chamava repr no Python 2.7.

O Exemplo 2 lista a implementação de nossa primeira versão de Vector (esse exemplo usa como base o código
mostrado no #ex_vector2d_v0 e no #ex_vector2d_v1 do Capítulo 11).
Exemplo 2. vector_v1.py: derived from vector2d_v1.py

PY
from array import array
import reprlib
import math

class Vector:
typecode = 'd'

def __init__(self, components):


self._components = array(self.typecode, components) # (1)

def __iter__(self):
return iter(self._components) # (2)

def __repr__(self):
components = reprlib.repr(self._components) # (3)
components = components[components.find('['):-1] # (4)
return f'Vector({components})'

def __str__(self):
return str(tuple(self))

def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components)) # (5)

def __eq__(self, other):


return tuple(self) == tuple(other)

def __abs__(self):
return math.hypot(*self) # (6)

def __bool__(self):
return bool(abs(self))

@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv) # (7)

1. O atributo de instância "protegido" self._components vai manter um array com os componentes do Vector .

2. Para permitir iteração, devolvemos um itereador sobre self._components.[129]


3. Usa reprlib.repr() para obter um representação de tamanho limitado de self._components (por exemplo,
array('d', [0.0, 1.0, 2.0, 3.0, 4.0, …​]) ).

4. Remove o prefixo array('d', eo ) final, antes de inserir a string em uma chamada ao construtor de Vector .

5. Cria um objeto bytes diretamente de self._components .

6. Desde o Python 3.8, math.hypot aceita pontos N-dimensionais. Já usei a seguinte expressão antes:
math.sqrt(sum(x * x for x in self)) .

7. A única mudança necessária no frombytes anterior é na última linha: passamos a memoryview diretamente para
o construtor, sem desempacotá-la com * , como fazíamos antes.

O modo como usei reprlib.repr pede alguma elaboração. Essa função produz representações seguras de estruturas
grandes ou recursivas, limitando a tamanho da string devolvida e marcando o corte com '…​' . Eu queria que o repr
de um Vector se parecesse com Vector([3.0, 4.0, 5.0]) e não com Vector(array('d', [3.0, 4.0, 5.0])) ,
porque a existência de um array dentro de um Vector é um detalhe de implementação. Como essas chamadas ao
construtor criam objetos Vector idênticos, preferi a sintaxe mais simples, usando um argumento list .
Ao escrever o __repr__ , poderia ter produzido uma versão para exibição simplificada de components com essa
expressão: reprlib.repr(list(self._components)) . Isso, entretanto, geraria algum desperdício, pois eu estaria
copiando cada item de self._components para uma list apenas para usar a list no repr . Em vez disso, decidi
aplicar reprlib.repr diretamente no array self._components , e então remover os caracteres fora dos [] . É isso o
que faz a segunda linha do __repr__ no Exemplo 2.

Por seu papel na depuração, chamar repr() em um objeto não deveria nunca gerar uma exceção.

👉 DICA Se alguma coisa der errado dentro de sua implementação de __repr__ , você deve lidar com o
problema e fazer o melhor possível para produzir uma saída aproveitável, que dê ao usuário uma
chance de identificar o recipiente ( self ).

Observe que os métodos __str__ , __eq__ , e __bool__ são idênticos a suas versões em Vector2d , e apenas um
caracter mudou em frombytes (um * foi removido na última linha). Isso é um dos benefícios de termos tornado o
Vector2d original iterável.

Aliás, poderíamos ter criado Vector como uma subclasse de Vector2d , mas escolhi não fazer isso por duas razões.
Em primeiro lugar, os construtores incompatíveis de fato tornam a relação de super/subclasse desaconselhável. Eu até
poderia contornar isso como um tratamento engenhoso dos parâmetros em __init__ , mas a segunda razão é mais
importante: queria que Vector fosse um exemplo independente de uma classe que implementa o protocolo de
sequência. É o que faremos a seguir, após uma discussão sobre o termo protocolo.

12.4. Protocolos e o duck typing


Já no Capítulo 1, vimos que não é necessário herdar de qualquer classe em especial para criar um tipo sequência
completamente funcional em Python; basta implementar os métodos que satisfazem o protocolo de sequência. Mas de
que tipo de protocolo estamos falando?

No contexto da programação orientada a objetos, um protocolo é uma interface informal, definida apenas na
documentação (e não no código). Por exemplo, o protocolo de sequência no Python implica apenas no métodos
__len__ e __getitem__ . Qualquer classe Spam , que implemente esses métodos com a assinatura e a semântica
padrões, pode ser usada em qualquer lugar onde uma sequência for esperada. É irrelevante se Spam é uma subclasse
dessa ou daquela outra classe; tudo o que importa é que ela fornece os métodos necessários. Vimos isso no Exemplo 1,
reproduzido aqui no Exemplo 3.

Exemplo 3. Código do Exemplo 1, reproduzido aqui por conveniência


PYTHON3
import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):


return self._cards[position]

A classe FrenchDeck , no Exemplo 3, pode tirar proveito de muitas facilidades do Python por implementar o protocolo
de sequência, mesmo que isso não esteja declarado em qualquer ponto do código. Um programador Python experiente
vai olhar para ela e entender que aquilo é uma sequência, mesmo sendo apenas uma subclasse de object . Dizemos
que ela é uma sequênca porque ela se comporta como uma sequência, e é isso que importa.

Isso ficou conhecido como duck typing (literalmente "tipagem pato"), após o post de Alex Martelli citado no início deste
capítulo.

Como protocolos são informais e não obrigatórios, muitas vezes é possível resolver nosso problema implementando
apenas parte de um protocolo, se sabemos o contexto específico em que a classe será utilizada. Por exemplo, apenas
__getitem__ basta para suportar iteração; não há necessidade de fornecer um __len__ .

Com a PEP 544—Protocols: Structural subtyping (static duck typing) (Protocolos:sub-tipagem


estrutural (duck typing estático)) (https://fpy.li/pep544) (EN), o Python 3.8 suporta classes protocolo:
subclasses de typing.Protocol , que estudamos na seção Seção 8.5.10. Esse novo uso da palavra

👉 DICA protocolo no Python tem um significado relacionado, mas diferente. Quando preciso diferenciá-los,
escrevo protocolo estático para me referir aos protocolos formalizados em classes protocolo
(subclasses de typing.Protocol ), e protocolos dinâmicos para o sentido tradicional. Uma diferença
fundamental é que implementações de um protocolo estático precisam oferecer todos os métodos
definidos na classe protocolo. A seção Seção 13.3 no Capítulo 13 traz maiores detalhes.

Vamos agora implementar o protocolo sequência em Vector , primeiro sem suporte adequado ao fatiamento, que
acrescentaremos mais tarde.

12.5. Vector versão #2: Uma sequência fatiável


Como vimos no exemplo da classe FrenchDeck , suportar o protocolo de sequência é muito fácil se você puder delegar
para um atributo sequência em seu objeto, como nosso array self._components . Esses __len__ e __getitem__ de
uma linha são um bom começo:
PYTHON3
class Vector:
# many lines omitted
# ...

def __len__(self):
return len(self._components)

def __getitem__(self, index):


return self._components[index]

Após tais acréscimos, agora todas as seguintes operações funcionam:

PYCON
>>> v1 = Vector([3, 4, 5])
>>> len(v1)
3
>>> v1[0], v1[-1]
(3.0, 5.0)
>>> v7 = Vector(range(7))
>>> v7[1:4]
array('d', [1.0, 2.0, 3.0])

Como se vê, até o fatiamento é suportado—mas não muito bem. Seria melhor se uma fatia de um Vector fosse
também uma instância de Vector , e não um array . A antiga classe FrenchDeck tem um problema similar: quando
ela é fatiada, o resultado é uma list . No caso de Vector , muito da funcionalidade é perdida quando o fatiamento
produz arrays simples.

Considere os tipos sequência embutidos: cada um deles, ao ser fatiado, produz uma nova instância de seu próprio tipo,
e não de algum outro tipo.

Para fazer Vector produzir fatias como instâncias de Vector , não podemos simplesmente delegar o fatiamento para
array . Precisamos analisar os argumentos recebidos em __getitem__ e fazer a coisa certa.

Vejamos agora como o Python transforma a sintaxe my_seq[1:3] em argumentos para my_seq.__getitem__(...) .

12.5.1. Como funciona o fatiamento


Uma demonstração vale mais que mil palavras, então dê uma olhada no Exemplo 4.

Exemplo 4. Examinando o comportamento de __getitem__ e fatias

PYCON
>>> class MySeq:
... def __getitem__(self, index):
... return index # (1)
...
>>> s = MySeq()
>>> s[1] # (2)
1
>>> s[1:4] # (3)
slice(1, 4, None)
>>> s[1:4:2] # (4)
slice(1, 4, 2)
>>> s[1:4:2, 9] # (5)
(slice(1, 4, 2), 9)
>>> s[1:4:2, 7:9] # (6)
(slice(1, 4, 2), slice(7, 9, None))

1. Para essa demonstração, o método __getitem__ simplesmente devolve o que for passado a ele.
2. Um único índice, nada de novo.
3. A notação 1:4 se torna slice(1, 4, None) .

4. slice(1, 4, 2) significa comece em 1, pare em 4, ande de 2 em 2.


5. Surpresa: a presença de vírgulas dentro do [] significa que __getitem__ recebe uma tupla.
6. A tupla pode inclusive conter vários objetos slice .
Vamos agora olhar mais de perto a própria classe slice , no Exemplo 5.

Exemplo 5. Inspecionando os atributos da classe slice

PYCON
>>> slice # (1)
<class 'slice'>
>>> dir(slice) # (2)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__',
'__hash__', '__init__', '__le__', '__lt__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'indices', 'start', 'step', 'stop']

1. slice é um tipo embutido (que já vimos antes na seção Seção 2.7.2).


2. Inspecionando uma slice descobrimos os atributos de dados start , stop , e step , e um método indices .

No Exemplo 5, a chamada dir(slice) revela um atributo indices , um método pouco conhecido mas muito
interessante. Eis o que diz help(slice.indices) :

S.indices(len) → (start, stop, stride)


Supondo uma sequência de tamanho len , calcula os índices start (início) e stop (fim), e a extensão do stride
(passo) da fatia estendida descrita por S . Índices fora dos limites são recortados, exatamente como acontece em uma
fatia normal.

Em outras palavras, indices expõe a lógica complexa implementada nas sequências embutidas, para lidar
graciosamente com índices inexistentes ou negativos e com fatias maiores que a sequência original. Esse método
produz tuplas "normalizadas" com os inteiros não-negativos start , stop , e stride ajustados para uma sequência
de um dado tamanho.

Aqui estão dois exemplos, considerando uma sequência de len == 5 , por exemplo 'ABCDE' :

PYCON
>>> slice(None, 10, 2).indices(5) # (1)
(0, 5, 2)
>>> slice(-3, None, None).indices(5) # (2)
(2, 5, 1)

1. 'ABCDE'[:10:2] é o mesmo que 'ABCDE'[0:5:2] .

2. 'ABCDE'[-3:] é o mesmo que 'ABCDE'[2:5:1] .

No código de nosso Vector não vamos precisar do método slice.indices() , pois quando recebermos uma fatia
como argumento vamos delegar seu tratamento para o array interno _components . Mas quando você não puder
contar com os serviços de uma sequência subjacente, esse método ajuda evita a necessidade de implementar uma
lógica sutil.
Agora que sabemos como tratar fatias, vamos ver a implementação aperfeiçoada de Vector.__getitem__ .

12.5.2. Um __getitem__ que trata fatias


O Exemplo 6 lista os dois métodos necessários para fazer Vector se comportar como uma sequência: __len__ e
__getitem__ (com o último implementado para tratar corretamente o fatiamento).

Exemplo 6. Parte de vector_v2.py: métodos __len__ e __getitem__ adicionados à classe Vector , de vector_v1.py (no
Exemplo 2)

PY
def __len__(self):
return len(self._components)

def __getitem__(self, key):


if isinstance(key, slice): # (1)
cls = type(self) # (2)
return cls(self._components[key]) # (3)
index = operator.index(key) # (4)
return self._components[index] # (5)

1. Se o argumento key é uma slice …​

2. …​obtém a classe da instância (isto é, Vector ) e…​

3. …​invoca a classe para criar outra instância de Vector a partir de uma fatia do array _components .

4. Se podemos obter um index de key …​

5. …​devolve o item específico de _components .

A função operator.index() chama o método especial __index__ . A função e o método especial foram definidos na
PEP 357—Allowing Any Object to be Used for Slicing (Permitir que Qualquer Objeto seja Usado para Fatiamento)
(https://fpy.li/pep357) (EN), proposta por Travis Oliphant, para permitir que qualquer um dos numerosos tipos de inteiros
na NumPy fossem usados como argumentos de índices e fatias. A diferença principal entre operator.index() e
int() é que o primeiro foi projetado para esse propósito específico. Por exemplo, int(3.14) devolve 3 , mas
operator.index(3.14) gera um TypeError , porque um float não deve ser usado como índice.

O uso excessivo de isinstance pode ser um sinal de design orientado a objetos ruim, mas tratar
fatias em __getitem__ é um caso de uso justificável. Na primeira edição, também usei um teste
✒️ NOTA isinstance com key , para verificar se esse argumento era um inteiro. O uso de operator.index
evita esse teste, e gera um Type​Error com uma mensagem muito informativa, se não for possível
obter o index a partir de key . Observe a última mensagem de erro no Exemplo 7, abaixo.

Após a adição do código do Exemplo 6 à classe Vector class, temos o comportamento apropriado para fatiamento,
como demonstra o Exemplo 7 .

Exemplo 7. Testes do Vector.__getitem__ aperfeiçoado, do Exemplo 6


PYCON
>>> v7 = Vector(range(7))
>>> v7[-1] # (1)
6.0
>>> v7[1:4] # (2)
Vector([1.0, 2.0, 3.0])
>>> v7[-1:] # (3)
Vector([6.0])
>>> v7[1,2] # (4)
Traceback (most recent call last):
...
TypeError: 'tuple' object cannot be interpreted as an integer

1. Um índice inteiro recupera apenas o valor de um componente, um float .

2. Uma fatia como índice cria um novo Vector .

3. Um fatia de len == 1 também cria um Vector .

4. Vector não suporta indexação multidimensional, então tuplas de índices ou de fatias geram um erro.

12.6. Vector versão #3: acesso dinâmico a atributos


Ao evoluir Vector2d para Vector , perdemos a habilidade de acessar os componentes do vetor por nome (por
exemplo, v.x , v.y ). Agora estamos trabalhando com vetores que podem ter um número grande de componentes.
Ainda assim, pode ser conveniente acessar os primeiros componentes usando letras como atalhos, algo como x , y , z
em vez de v[0] , v[1] , and v[2] .

Aqui está a sintaxe alternativa que queremos oferecer para a leitura dos quatro primeiros componentes de um vetor:

PYCON
>>> v = Vector(range(10))
>>> v.x
0.0
>>> v.y, v.z, v.t
(1.0, 2.0, 3.0)

No Vector2d , oferecemos acesso somente para leitura a x e y através do decorador @property (veja o Exemplo 7).
Poderíamos incluir quatro propriedades no Vector , mas isso seria tedioso. O método especial __getattr__ nos
fornece uma opção melhor.

O método __getattr__ é invocado pelo interpretador quando a busca por um atributo falha. Simplificando, dada a
expressão my_obj.x , o Python verifica se a instância de my_obj tem um atributo chamado x ; em caso negativo, a
busca passa para a classe ( my_obj.__class__ ) e depois sobe pelo diagrama de herança.[130] Se por fim o atributo x
não for encontrado, o método __getattr__ , definido na classe de my_obj , é chamado com self e o nome do
atributo em formato de string (por exemplo, 'x' ).

O Exemplo 8 lista nosso método __getattr__ . Ele basicamente verifica se o atributo desejado é uma das letras xyzt .
Em caso positivo, devolve o componente correspondente do vetor.

Exemplo 8. Parte de vector_v3.py: método __getattr__ acrescentado à classe Vector


PY
__match_args__ = ('x', 'y', 'z', 't') # (1)

def __getattr__(self, name):


cls = type(self) # (2)
try:
pos = cls.__match_args__.index(name) # (3)
except ValueError: # (4)
pos = -1
if 0 <= pos < len(self._components): # (5)
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' # (6)
raise AttributeError(msg)

1. Define __match_args__ para permitir pattern matching posicional sobre os atributos dinâmicos suportados por
__getattr__ .[131]

2. Obtém a classe de Vector , para uso posterior.

3. Tenta obter a posição de name em __match_args__ .

4. .index(name) gera um ValueError quando name não é encontrado; define pos como -1 . (Eu preferiria usar
algo como str.find aqui, mas tuple não implementa esse método.)
5. Se pos está dentro da faixa de componentes disponíveis, devolve aquele componente.
6. Se chegamos até aqui, gera um AttributeError com uma mensagem de erro padrão.

Não é difícil implementar __getattr__ , mas neste caso não é o suficiente. Observe a interação bizarra no Exemplo 9.

Exemplo 9. Comportamento inapropriado: realizar uma atribuição a v.x não gera um erro, mas introduz uma
inconsistência

PYCON
>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x # (1)
0.0
>>> v.x = 10 # (2)
>>> v.x # (3)
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0]) # (4)

1. Acessa o elemento v[0] como v.x .

2. Atribui um novo valor a v.x . Isso deveria gera uma exceção.

3. Ler v.x obtém o novo valor, 10 .

4. Entretanto, os componentes do vetor não mudam.

Você consegue explicar o que está acontecendo? Em especial, por que v.x devolve 10 na segunda consulta (<3>), se
aquele valor não está presente no array de componentes do vetor? Se você não souber responder de imediato, estude a
explicação de __getattr__ que aparece logo antes do Exemplo 8. A razão é um pouco sutil, mas é um alicerce
fundamental para entender grande parte do que veremos mais tarde no livro.

Após pensar um pouco sobre essa questão, siga em frente e leia a explicação para o que aconteceu.
A inconsistência no Exemplo 9 ocorre devido à forma como __getattr__ funciona: o Python só chama esse método
como último recurso, quando o objeto não contém o atributo nomeado. Entretanto, após atribuirmos v.x = 10 , o
objeto v agora contém um atributo x , e então __getattr__ não será mais invocado para obter v.x : o interpretador
vai apenas devolver o valor 10 , que agora está vinculado a v.x . Por outro lado, nossa implementação de
__getattr__ não leva em consideração qualquer atributo de instância diferente de self._components , de onde ele
obtém os valores dos "atributos virtuais" listados em __match_args__ .

Para evitar essa inconsistência, precisamos modificar a lógica de definição de atributos em nossa classe Vector .

Como você se lembra, nos nossos últimos exemplos de Vector2d no Capítulo 11, tentar atribuir valores aos atributos
de instância .x ou .y gerava um AttributeError . Em Vector , queremos produzir a mesma exceção em resposta a
tentativas de atribuição a qualquer nome de atributo com um única letra, só para evitar confusão. Para fazer isso,
implementaremos __setattr__ , como listado no Exemplo 10.

Exemplo 10. Parte de vector_v3.py: o método __setattr__ na classe Vector

PY
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1: # (1)
if name in cls.__match_args__: # (2)
error = 'readonly attribute {attr_name!r}'
elif name.islower(): # (3)
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = '' # (4)
if error: # (5)
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value) # (6)

1. Tratamento especial para nomes de atributos com uma única letra.


2. Se name está em __match_args__ , configura mensagens de erro específicas.

3. Se name é uma letra minúscula, configura a mensagem de erro sobre todos os nomes de uma única letra.
4. Caso contrário, configura uma mensagem de erro vazia.
5. Se existir uma mensagem de erro não-vazia, gera um AttributeError .

6. Caso default: chama __setattr__ na superclasse para obter o comportamento padrão.

A função super() fornece uma maneira de acessar dinamicamente métodos de superclasses, uma
👉 DICA necessidade em uma linguagem dinâmica que suporta herança múltipla, como o Python. Ela é usada
para delegar alguma tarefa de um método em uma subclasse para um método adequado em uma
superclasse, como visto no Exemplo 10. Falaremos mais sobre super na seção Seção 14.4.

Ao escolher a menssagem de erro para mostrar com AttributeError , primeiro eu verifiquei o comportamento do
tipo embutido complex , pois ele é imutável e tem um par de atributos de dados real and imag . Tentar mudar
qualquer um dos dois em uma instância de complex gera um AttributeError com a mensagem "can’t set
attribute" ("não é possível [re]-definir o atributo"). Por outro lado, a tentativa de modificar um atributo protegido
por uma propriedade, como fizemos no Seção 11.7, produz a mensagem "read-only attribute" ("atributo apenas
para leitura"). Eu me inspirei em ambas as frases para definir a string error em __setitem__ , mas fui mais explícito
sobre os atributos proibidos.
Observe que não estamos proibindo a modificação de todos os atributos, apenas daqueles com nomes compostos por
uma única letra minúscula, para evitar conflitos com os atributos apenas para leitura suportados, x , y , z , e t .

Sabendo que declarar __slots__ no nível da classe impede a definição de novos atributos de
instância, é tentador usar esse recurso em vez de implementar __setattr__ como fizemos.
⚠️ AVISO Entretanto, por todas as ressalvas discutidas na seção Seção 11.11.2, usar __slots__ apenas para
prevenir a criação de atributos de instância não é recomendado. __slots__ deve ser usado apenas
para economizar memória, e apenas quando isso for um problema real.

Mesmo não suportando escrita nos componentes de Vector , aqui está uma lição importante deste exemplo: muitas
vezes, quando você implementa __getattr__ , é necessário também escrever o __setattr__ , para evitar
comportamentos inconsistentes em seus objetos.

Para permitir a modificação de componentes, poderíamos implementar __setitem__ , para permitir v[0] = 1.1 ,
e/ou __setattr__ , para fazer v.x = 1.1 funcionar. Mas Vector permanecerá imutável, pois queremos torná-lo
hashable, na próxima seção.

12.7. Vector versão #4: o hash e um == mais rápido


Vamos novamente implementar um método __hash__ . Juntamente com o __eq__ existente, isso tornará as instâncias
de Vector hashable.

O __hash__ do Vector2d (no Exemplo 8) computava o hash de uma tuple construída com os dois componentes,
self.x and self.y . Nós agora podemos estar lidando com milhares de componentes, então criar uma tuple pode
ser caro demais. Em vez disso, vou aplicar sucessivamente o operador ^ (xor) aos hashes de todos os componentes,
assim: v[0] ^ v[1] ^ v[2] . É para isso que serve a função functools.reduce . Anteriormente afirmei que reduce
não é mais tão popular quanto antes,[132] mas computar o hash de todos os componentes do vetor é um bom caso de
uso para ela. A Figura 1 ilustra a ideia geral da função reduce .

Figura 1. Funções de redução— reduce , sum , any , all —produzem um único resultado agregado a partir de uma
sequência ou de qualquer objeto iterável finito.

Até aqui vimos que functools.reduce() pode ser substituída por sum() . Vamos agora explicar exatamente como ela
funciona. A ideia chave é reduzir uma série de valores a um valor único. O primeiro argumento de reduce() é uma
função com dois argumentos, o segundo argumento é um iterável. Vamos dizer que temos uma função fn , que recebe
dois argumentos, e uma lista lst . Quando chamamos reduce(fn, lst) , fn será aplicada ao primeiro par de
elementos de lst — fn(lst[0], lst[1]) —produzindo um primeiro resultado, r1 . Então fn é aplicada a r1 e ao
próximo elemento— fn(r1, lst[2]) —produzindo um segundo resultado, r2 . Agora fn(r2, lst[3]) é chamada
para produzir r3 …​e assim por diante, até o último elemento, quando finalmente um único elemento, rN , é
produzido e devolvido.
Aqui está como reduce poderia ser usada para computar 5! (o fatorial de 5):

PYCON
>>> 2 * 3 * 4 * 5 # the result we want: 5! == 120
120
>>> import functools
>>> functools.reduce(lambda a,b: a*b, range(1, 6))
120

Voltando a nosso problema de hash, o Exemplo 11 demonstra a ideia da computação de um xor agregado, fazendo isso
de três formas diferente: com um loop for e com dois modos diferentes de usar reduce .

Exemplo 11. Três maneiras de calcular o xor acumulado de inteiros de 0 a 5

PYCON
>>> n = 0
>>> for i in range(1, 6): # (1)
... n ^= i
...
>>> n
1
>>> import functools
>>> functools.reduce(lambda a, b: a^b, range(6)) # (2)
1
>>> import operator
>>> functools.reduce(operator.xor, range(6)) # (3)
1

1. xor agregado com um loop for e uma variável de acumulação.


2. functools.reduce usando uma função anônima.
3. functools.reduce substituindo a lambda personalizada por operator.xor .

Das alternativas apresentadas no Exemplo 11, a última é minha favorita, e o loop for vem a seguir. Qual sua
preferida?

Como visto na seção Seção 7.8.1, operator oferece a funcionalidade de todos os operadores infixos do Python em
formato de função, diminuindo a necessidade do uso de lambda .

Para escrever Vector.__hash__ no meu estilo preferido precisamos importar os módulos functools e operator .
Exemplo 12 apresenta as modificações relevantes.

Exemplo 12. Parte de vector_v4.py: duas importações e o método __hash__ adicionados à classe Vector de
vector_v3.py
PYTHON3
from array import array
import reprlib
import math
import functools # (1)
import operator # (2)

class Vector:
typecode = 'd'

# many lines omitted in book listing...

def __eq__(self, other): # (3)


return tuple(self) == tuple(other)

def __hash__(self):
hashes = (hash(x) for x in self._components) # (4)
return functools.reduce(operator.xor, hashes, 0) # (5)

# more lines omitted...

1. Importa functools para usar reduce .

2. Importa operator para usar xor .

3. Não há mudanças em __eq__ ; listei-o aqui porque é uma boa prática manter __eq__ e __hash__ próximos no
código-fonte, pois eles precisam trabalhar juntos.
4. Cria uma expressão geradora para computar sob demanda o hash de cada componente.
5. Alimenta reduce com hashes e a função xor , para computar o código hash agregado; o terceiro argumento, 0 , é
o inicializador (veja o próximo aviso).

Ao usar reduce , é uma boa prática fornecer o terceiro argumento, reduce(function, iterable,
initializer) , para prevenir a seguinte exceção: TypeError: reduce() of empty sequence with
no initial value ("TypeError: reduce() de uma sequência vazia sem valor inicial"— uma mensagem
⚠️ AVISO excelente: explica o problema e diz como resolvê-lo) . O initializer é o valor devolvido se a
sequência for vazia e é usado como primeiro argumento no loop de redução, e portanto deve ser o
elemento neutro da operação. Assim, o initializer para + , | , ^ deve ser 0 , mas para * e &
deve ser 1 .

Da forma como está implementado, o método __hash__ no Exemplo 12 é um exemplo perfeito de uma computação de
map-reduce (mapeia e reduz). Veja a (Figura 2).
Figura 2. Map-reduce: aplica uma função a cada item para gerar uma nova série (map), e então computa o agregado
(reduce).

A etapa de mapeamento produz um hash para cada componente, e a etapa de redução agrega todos os hashes com o
operador xor. Se usarmos map em vez de uma genexp, a etapa de mapeamento fica ainda mais visível:

PYTHON3
def __hash__(self):
hashes = map(hash, self._components)
return functools.reduce(operator.xor, hashes)

A solução com map seria menos eficiente no Python 2, onde a função map cria uma nova list com

👉 DICA os resultados. Mas no Python 3, map é preguiçosa (lazy): ela cria um gerador que produz os
resultados sob demanda, e assim economiza memória—exatamente como a expressão geradora que
usamos no método __hash__ do Exemplo 8.

E enquanto estamos falando de funções de redução, podemos substituir nossa implementação apressada de __eq__
com uma outra, menos custosa em termos de processamento e uso de memória, pelo menos para vetores grandes.
Como visto no Exemplo 2, temos esta implementação bastante concisa de __eq__ :

PYTHON3
def __eq__(self, other):
return tuple(self) == tuple(other)

Isso funciona com Vector2d e com Vector —e até considera Vector([1, 2]) igual a (1, 2) , o que pode ser um
problema, mas por ora vamos ignorar esta questão[133]. Mas para instâncias de Vector , que podem ter milhares de
componentes, esse método é muito ineficiente. Ele cria duas tuplas copiando todo o conteúdo dos operandos, apenas
para usar o __eq__ do tipo tuple . Para Vector2d (com apenas dois componentes), é um bom atalho. Mas não para
grandes vetores multidimensionais. Uma forma melhor de comparar um Vector com outro Vector ou iterável seria
o código do Exemplo 13.

Exemplo 13. A implementação de Vector.__eq__ usando zip em um loop for , para uma comparação mais eficiente
PYTHON3
def __eq__(self, other):
if len(self) != len(other): # (1)
return False
for a, b in zip(self, other): # (2)
if a != b: # (3)
return False
return True # (4)

1. Se as len dos objetos são diferentes, eles não são iguais.


2. zip produz um gerador de tuplas criadas a partir dos itens em cada argumento iterável. Veja a caixa O fantástico
zip, se zip for novidade para você. Em 1, a comparação com len é necessária porque zip para de produzir
valores sem qualquer aviso quando uma das fontes de entrada se exaure.
3. Sai assim que dois componentes sejam diferentes, devolvendo False .

4. Caso contrário, os objetos são iguais.

O nome da função zip vem de zíper, pois esse objeto físico funciona engatando pares de dentes
👉 DICA tomados dos dois lados do zíper, uma boa analogia visual para o que faz zip(left, right) .
Nenhuma relação com arquivos comprimidos.

O Exemplo 13 é eficiente, mas a função all pode produzir a mesma computação de um agregado do loop for em
apenas uma linha: se todas as comparações entre componentes correspoendentes nos operandos forem True , o
resultado é True . Assim que uma comparação é False , all devolve False . O Exemplo 14 mostra um __eq__
usando all .

Exemplo 14. A implementação de Vector.__eq__ usando zip e all : mesma lógica do Exemplo 13

PYTHON3
def __eq__(self, other):
return len(self) == len(other) and all(a == b for a, b in zip(self, other))

Observe que primeiro comparamos o len() dos operandos porque se os tamanhos são diferentes é desnecessário
comparar os itens.

O Exemplo 14 é a implementação que escolhemos para __eq__ em vector_v4.py.

O fantástico zip
Ter um loop for que itera sobre itens sem perder tempo com variáveis de índice é muito bom e evita muitos
bugs, mas exige algumas funções utilitárias especiais. Uma delas é a função embutida zip , que facilita a iteração
em paralelo sobre dois ou mais iteráveis, devolvendo tuplas que você pode desempacotar em variáveis, uma para
cada item nas entradas paralelas. Veja o Exemplo 15.

Exemplo 15. A função embutida zip trabalhando


PYCON
>>> zip(range(3), 'ABC') # (1)
<zip object at 0x10063ae48>
>>> list(zip(range(3), 'ABC')) # (2)
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> list(zip(range(3), 'ABC', [0.0, 1.1, 2.2, 3.3])) # (3)
[(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2)]
>>> from itertools import zip_longest # (4)
>>> list(zip_longest(range(3), 'ABC', [0.0, 1.1, 2.2, 3.3], fillvalue=-1))
[(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2), (-1, -1, 3.3)]

1. zip devolve um gerador que produz tuplas sob demanda.


2. Cria uma list apenas para exibição; nós normalmente iteramos sobre o gerador.
3. zip para sem aviso quando um dos iteráveis é exaurido.
4. A função itertools.zip_longest se comporta de forma diferente: ela usa um fillvalue opcional (por
default None ) para preencher os valores ausentes, e assim consegue gerar tuplas até que o último iterável
seja exaurido.

A nova opção de zip() no Python 3.10


Escrevi na primeira edição deste livro que zip encerrar silenciosamente ao final do iterável
mais curto era surpreendente—e não era uma boa característica em uma API. Ignorar parte
dos dados de entrada sem qualquer alerta pode levar a bugs sutis. Em vez disso, zip deveria
✒️ NOTA gerar um ValueError se os iteráveis não forem todos do mesmo tamanho, como acontece
quando se desempacota um iterável para uma tupla de variáveis de tamanho diferente—
alinhado à política de falhar rápido do Python. A PEP 618—Add Optional Length-Checking To
zip (https://fpy.li/pep618) acrescentou um argumento opcional strict à função zip , para fazê-
la de comportar dessa forma. Isso foi implementado no Python 3.10.

A função zip pode também ser usada para transpor uma matriz, representada como iteráveis aninhados. Por
exemplo:

PYCON
>>> a = [(1, 2, 3),
... (4, 5, 6)]
>>> list(zip(*a))
[(1, 4), (2, 5), (3, 6)]
>>> b = [(1, 2),
... (3, 4),
... (5, 6)]
>>> list(zip(*b))
[(1, 3, 5), (2, 4, 6)]

Se você quiser entender zip , passe algum tempo descobrindo como esses exemplos funcionam.

A função embutida enumerate é outra função geradora usada com frequência em loops for , para evitar
manipulação direta de variáveis índice. Quem não estiver familiarizado com enumerate deveria estudar a seção
dedicada a ela na documentação das "Funções embutidas"
(https://docs.python.org/pt-br/3/library/functions.html#enumerate). As funções embutidas zip e enumerate , bem como
várias outras funções geradores na biblioteca padrão, são tratadas na seção Seção 17.9.

Vamos encerrar esse capítulo trazendo de volta o método __format__ do Vector2d para o Vector .
12.8. Vector versão #5: Formatando
O método __format__ de Vector será parecido com o mesmo método em Vector2d , mas em vez de fornecer uma
exibição personalizada em coordenadas polares, Vector usará coordenadas esféricas—também conhecidas como
coordendas "hiperesféricas", pois agora suportamos n dimensões, e as esferas são "hiperesferas", em 4D e além[134].
Como consequência, mudaremos também o sufixo do formato personalizado de 'p' para 'h' .

Como vimos na seção Seção 11.6, ao estender a Minilinguagem de especificação de formato


(https://docs.python.org/pt-br/3/library/string.html#formatspec) é melhor evitar a reutilização dos códigos de
formato usados por tipos embutidos. Especialmente, nossa minilinguagens estendida também usa os
👉 DICA códigos de formato dos números de ponto flutuante ( 'eEfFgGn%' ), em seus significados originais,
então devemos certamente evitar qualquer um daqueles. Inteiros usam 'bcdoxXn' e strings usam
's' . Escolhi 'p' para as coordenadas polares de Vector2d . O código 'h' para coordendas
hiperesféricas é uma boa opção.

Por exemplo, dado um objeto Vector em um espaço 4D ( len(v) == 4 ), o código 'h' irá produzir uma linha como
<r, Φ₁, Φ₂, Φ₃> , onde r é a magnitude ( abs(v) ), e o restante dos números são os componentes angulares Φ₁, Φ₂,
Φ₃.

Aqui estão algumas amostras do formato de coordenadas esféricas em 4D, retiradas dos doctests de vector_v5.py (veja o
Exemplo 16):

PYCON
>>> format(Vector([-1, -1, -1, -1]), 'h')
'<2.0, 2.0943951023931957, 2.186276035465284, 3.9269908169872414>'
>>> format(Vector([2, 2, 2, 2]), '.3eh')
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
'<1.00000, 1.57080, 0.00000, 0.00000>'

Antes de podermos implementar as pequenas mudanças necessárias em __format__ , precisamos escrever um par de
métodos de apoio: angle(n) , para computar uma das coordenadas angulares (por exemplo, Φ₁), e angles() , para
devolver um iterável com todas as coordenadas angulares. Não vou descrever a matemática aqui; se você tiver
curiosidade, a página “n-sphere” (https://fpy.li/nsphere) (EN: ver Nota 6) da Wikipedia contém as fórmulas que usei para
calcular coordenadas esféricas a partir das coordendas cartesianas no array de componentes de Vector .

O Exemplo 16 é a listagem completa de vector_v5.py, consolidando tudo que implementamos desde a seção Seção 12.3,
e acrescentando a formatação personalizada

Exemplo 16. vector_v5.py: doctests e todo o código da versão final da classe Vector ; as notas explicativas enfatizam os
acréscimos necessários para suportar __format__
PY
"""
A multidimensional ``Vector`` class, take 5

A ``Vector`` is built from an iterable of numbers::

>>> Vector([3.1, 4.2])


Vector([3.1, 4.2])
>>> Vector((3, 4, 5))
Vector([3.0, 4.0, 5.0])
>>> Vector(range(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

Tests with two dimensions (same results as ``vector2d_v1.py``)::

>>> v1 = Vector([3, 4])


>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1
Vector([3.0, 4.0])
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> print(v1)
(3.0, 4.0)
>>> octets = bytes(v1)
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1)
5.0
>>> bool(v1), bool(Vector([0, 0]))
(True, False)

Test of ``.frombytes()`` class method:

>>> v1_clone = Vector.frombytes(bytes(v1))


>>> v1_clone
Vector([3.0, 4.0])
>>> v1 == v1_clone
True

Tests with three dimensions::

>>> v1 = Vector([3, 4, 5])


>>> x, y, z = v1
>>> x, y, z
(3.0, 4.0, 5.0)
>>> v1
Vector([3.0, 4.0, 5.0])
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> print(v1)
(3.0, 4.0, 5.0)
>>> abs(v1) # doctest:+ELLIPSIS
7.071067811...
>>> bool(v1), bool(Vector([0, 0, 0]))
(True, False)

Tests with many dimensions::

>>> v7 = Vector(range(7))
>>> v7
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
>>> abs(v7) # doctest:+ELLIPSIS
9.53939201...

Test of ``.__bytes__`` and ``.frombytes()`` methods::

>>> v1 = Vector([3, 4, 5])


>>> v1_clone = Vector.frombytes(bytes(v1))
>>> v1_clone
Vector([3.0, 4.0, 5.0])
>>> v1 == v1_clone
True

Tests of sequence behavior::

>>> v1 = Vector([3, 4, 5])


>>> len(v1)
3
>>> v1[0], v1[len(v1)-1], v1[-1]
(3.0, 5.0, 5.0)

Test of slicing::

>>> v7 = Vector(range(7))
>>> v7[-1]
6.0
>>> v7[1:4]
Vector([1.0, 2.0, 3.0])
>>> v7[-1:]
Vector([6.0])
>>> v7[1,2]
Traceback (most recent call last):
...
TypeError: 'tuple' object cannot be interpreted as an integer

Tests of dynamic attribute access::

>>> v7 = Vector(range(10))
>>> v7.x
0.0
>>> v7.y, v7.z, v7.t
(1.0, 2.0, 3.0)

Dynamic attribute lookup failures::

>>> v7.k
Traceback (most recent call last):
...
AttributeError: 'Vector' object has no attribute 'k'
>>> v3 = Vector(range(3))
>>> v3.t
Traceback (most recent call last):
...
AttributeError: 'Vector' object has no attribute 't'
>>> v3.spam
Traceback (most recent call last):
...
AttributeError: 'Vector' object has no attribute 'spam'

Tests of hashing::

>>> v1 = Vector([3, 4])


>>> v2 = Vector([3.1, 4.2])
>>> v3 = Vector([3, 4, 5])
>>> v6 = Vector(range(6))
>>> hash(v1), hash(v3), hash(v6)
(7, 2, 1)

Most hash codes of non-integers vary from a 32-bit to 64-bit CPython build::

>>> import sys


>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
True

Tests of ``format()`` with Cartesian coordinates in 2D::

>>> v1 = Vector([3, 4])


>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'

Tests of ``format()`` with Cartesian coordinates in 3D and 7D::

>>> v3 = Vector([3, 4, 5])


>>> format(v3)
'(3.0, 4.0, 5.0)'
>>> format(Vector(range(7)))
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'

Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::

>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS


'<1.414213..., 0.785398...>'
>>> format(Vector([1, 1]), '.3eh')
'<1.414e+00, 7.854e-01>'
>>> format(Vector([1, 1]), '0.5fh')
'<1.41421, 0.78540>'
>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS
'<1.73205..., 0.95531..., 0.78539...>'
>>> format(Vector([2, 2, 2]), '.3eh')
'<3.464e+00, 9.553e-01, 7.854e-01>'
>>> format(Vector([0, 0, 0]), '0.5fh')
'<0.00000, 0.00000, 0.00000>'
>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS
'<2.0, 2.09439..., 2.18627..., 3.92699...>'
>>> format(Vector([2, 2, 2, 2]), '.3eh')
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
'<1.00000, 1.57080, 0.00000, 0.00000>'
"""

from array import array


import reprlib
import math
import functools
import operator
import itertools # (1)

class Vector:
typecode = 'd'

def __init__(self, components):


self._components = array(self.typecode, components)

def __iter__(self):
return iter(self._components)

def __repr__(self):
components = reprlib.repr(self._components)
components = components[components.find('['):-1]
return f'Vector({components})'

def __str__(self):
return str(tuple(self))

def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components))

def __eq__(self, other):


return (len(self) == len(other) and
all(a == b for a, b in zip(self, other)))

def __hash__(self):
hashes = (hash(x) for x in self)
return functools.reduce(operator.xor, hashes, 0)

def __abs__(self):
return math.hypot(*self)

def __bool__(self):
return bool(abs(self))

def __len__(self):
return len(self._components)

def __getitem__(self, key):


if isinstance(key, slice):
cls = type(self)
return cls(self._components[key])
index = operator.index(key)
return self._components[index]

__match_args__ = ('x', 'y', 'z', 't')

def __getattr__(self, name):


cls = type(self)
try:
pos = cls.__match_args__.index(name)
except ValueError:
pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg)

def angle(self, n): # (2)


r = math.hypot(*self[n:])
a = math.atan2(r, self[n-1])
if (n == len(self) - 1) and (self[-1] < 0):
return math.pi * 2 - a
else:
return a

def angles(self): # (3)


return (self.angle(n) for n in range(1, len(self)))

def __format__(self, fmt_spec=''):


if fmt_spec.endswith('h'): # hyperspherical coordinates
fmt_spec = fmt_spec[:-1]
coords = itertools.chain([abs(self)],
self.angles()) # (4)
outer_fmt = '<{}>' # (5)
else:
coords = self
outer_fmt = '({})' # (6)
components = (format(c, fmt_spec) for c in coords) # (7)
return outer_fmt.format(', '.join(components)) # (8)

@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv)

1. Importa itertools para usar a função chain em __format__ .

2. Computa uma das coordendas angulares, usando fórmulas adaptadas do artigo n-sphere (https://fpy.li/nsphere) (EN:
ver Nota 6) na Wikipedia.
3. Cria uma expressão geradora para computar sob demanda todas as coordenadas angulares.
4. Produz uma genexp usando itertools.chain , para iterar de forma contínua sobre a magnitude e as coordenadas
angulares.
5. Configura uma coordenada esférica para exibição, com os delimitadores de ângulo ( < e > ).

6. Configura uma coordenda cartesiana para exibição, com parênteses.


7. Cria uma expressão geradoras para formatar sob demanda cada item de coordenada.
8. Insere componentes formatados, separados por vírgulas, dentro de delimitadores ou parênteses.

Estamos fazendo uso intensivo de expressões geradoras em __format__ , angle , e angles , mas

✒️ NOTA nosso foco aqui é fornecer um __format__ para levar Vector ao mesmo nível de implementação
de Vector2d . Quando tratarmos de geradores, no Capítulo 17, vamos usar parte do código de
Vector nos exemplos, e lá os recursos dos geradores serão explicados em detalhes.

Isso conclui nossa missão nesse capítulo. A classe Vector será aperfeiçoada com operadores infixos no Capítulo 16.
Nosso objetivo aqui foi explorar técnicas para programação de métodos especiais que são úteis em uma grande
variedade de classes de coleções.

12.9. Resumo do capítulo


A classe Vector , o exemplo que desenvolvemos nesse capítulo, foi projetada para ser compatível com Vector2d ,
exceto pelo uso de uma assinatura de construtor diferente, aceitando um único argumento iterável, como fazem todos
os tipos embutidos de sequências. O fato de Vector se comportar como uma sequência apenas por implementar
__getitem__ e __len__ deu margem a uma discussão sobre protocolos, as interfaces informais usadas em
linguagens com duck typing.

A seguir vimos como a sintaxe my_seq[a:b:c] funciona por baixo dos panos, criando um objeto slice(a, b, c) e
entregando esse objeto a __getitem__ . Armados com esse conhecimento, fizemos Vector responder corretamente
ao fatiamento, devolvendo novas instâncias de Vector , como se espera de qualquer sequência pythônica.

O próximo passo foi fornecer acesso somente para leitura aos primeiros componentes de Vector , usando uma
notação do tipo my_vec.x . Fizemos isso implementando __getattr__ . Fazer isso abriu a possibilidade de incentivar
o usuário a atribuir àqueles componentes especiais, usando a forma my_vec.x = 7 , revelando um possível bug.
Consertamos o problema implementando também __setattr__ , para barrar a atribuição de valores a atributos cujos
nomes tenham apenas uma letra. É comum, após escrever um __getattr__ , ser necessário adicionar também
__setattr__ , para evitar comportamento inconsistente.
Implementar a função __hash__ nos deu um contexto perfeito para usar functools.reduce , pois precisávamos
aplicar o operador xor ( ^ ) sucessivamente aos hashes de todos os componentes de Vector , para produzir um código
de hash agregado referente a todo o Vector . Após aplicar reduce em __hash__ , usamos a função embutida de
redução all , para criar um método __eq__ mais eficiente.

O último aperfeiçoamento a Vector foi reimplementar o método __format__ de Vector2d , para suportar
coordenadas esféricas como alternativa às coordenadas cartesianas default. Usamos bastante matemática e vários
geradores para programar __format__ e suas funções auxiliares, mas esses são detalhes de implementação—e
voltaremos aos geradores no Capítulo 17. O objetivo daquela última seção foi suportar um formato personalizado,
cumprindo assim a promessa de um Vector capaz de fazer tudo que um Vector2d faz e algo mais.

Como fizemos no Capítulo 11, muitas vezes aqui olhamos como os objetos padrão do Python se comportam, para
emulá-los e dar a Vector uma aparência "pythônica".

No Capítulo 16 vamos implemenar vários operadores infixos em Vector . A matemática será muito mais simples que
aquela no método angle() daqui, mas explorar como os operadores infixos funcionam no Python é uma grande lição
sobre design orientado a objetos. Mas antes de chegar à sobrecarga de operadores, vamos parar um pouco de trabalhar
com uma única classe e olhar para a organização de múltiplas classes com interfaces e herança, os assuntos dos
capítulos #ifaces_prot_abc e #inheritance.

12.10. Leitura complementar


A maioria dos métodos especiais tratados no exemplo de Vector também apareceram no exemplo do Vector2d , no
Capítulo 11, então as referências na seção Seção 11.14 ali são todas relevantes aqui também.

A poderosa função de ordem superior reduce também é conhecida como fold (dobrar), accumulate (acumular),
aggregate (agregar), compress (comprimir), e inject (injetar). Para mais informações, veja o artigo "Fold (higher-order
function)" ("Dobrar (função de ordem superior)") (https://fpy.li/12-5) (EN), que apresenta aplicações daquela função de
ordem superior, com ênfase em programação funcional com estruturas de dados recursivas. O artigo também inclui
uma tabela mostrando funções similares a fold em dezenas de linguagens de programação.

Em "What’s New in Python 2.5" (Novidades no Python 2.5) (https://docs.python.org/2.5/whatsnew/pep-357.html) (EN) há uma
pequena explicação sobre __index__ , projetado para suportar métodos __getitem__ , como vimos na seção Seção
12.5.2. A PEP 357—Allowing Any Object to be Used for Slicing (Permitir que Qualquer Objeto seja Usado para
Fatiamento) (https://fpy.li/pep357) detalha a necessidade daquele método especial na perspectiva de um implementador de
uma extensão em C—Travis Oliphant, o principal criador da NumPy. As muitas contribuições de Oliphant tornaram o
Python uma importante linguagem para computação científica, que por sua vez posicionou a linguagem como a
escolha preferencial para aplicações de aprendizagem de máquina.

Ponto de vista
Protocolos como interfaces informais

Protocolos não são uma invenção do Python. Os criadores do Smalltalk, que também cunharam a expressão
"orientado a objetos", usavam "protocolo" como um sinônimo para aquilo que hoje chamamos de interfaces.
Alguns ambientes de programação Smalltalk permitiam que os programadores marcassem um grupo de métodos
como um protocolo, mas isso era meramente um artefato de documentação e navegação, e não era imposto pela
linguagem. Por isso acredito que "interface informal" é uma explicação curta razoável para "protocolo" quando
falo para uma audiência mais familiar com interfaces formais (e impostas pelo compilador).
Protocolos bem estabelecidos ou consagrados evoluem naturalmente em qualquer linguagem que usa tipagem
dinâmica (isto é, quando a verificação de tipo acontece durante a execução), porque não há informação estática
de tipo em assinaturas de métodos e em variáveis. Ruby é outra importante linguagem orientada a objetos que
tem tipagem dinâmica e usa protocolos.

Na documentação do Python, muitas vezes podemos perceber que um protocolo está sendo discutido pelo uso de
linguagem como "um objeto similar a um arquivo". Isso é uma forma abreviada de dizer "algo que se comporta
como um arquivo, implementando as partes da interface de arquivo relevantes ao contexto".

Você poderia achar que implementar apenas parte de um protocolo é um desleixo, mas isso tem a vantagem de
manter as coisas simples. A Seção 3.3 (https://docs.python.org/pt-br/3/reference/datamodel.html#special-method-names) do
capítulo "Modelo de Dados" na documentação do Python sugere que:

“ Aoemulação
implementar uma classe que emula qualquer tipo embutido, é importante que a
seja implementada apenas na medida em que faça sentido para o objeto que está
sendo modelado. Por exemplo, algumas sequências podem funcionar bem com a
recuperação de elementos individuais, mas extrair uma fatia pode não fazer sentido.

Quando não precisamos escrever métodos inúteis apenas para cumprir o contrato de uma interface
excessivamente detalhista e para manter o compilador feliz, fica mais fácil seguir o princípio KISS
(https://pt.wikipedia.org/wiki/Princ%C3%ADpio_KISS).

Por outro lado, se você quiser usar um verificador de tipo para checar suas implementações de protocolos, então
uma definição mais estrita de "protocolo" é necessária. É isso que typing.Protocol nos fornece.

Terei mais a dizer sobre protocolos e interfaces no Capítulo 13, onde esses conceitos são o assunto principal.

As origens do duck typing

Creio que a comunidade Ruby, mais que qualquer outra, ajudou a popularizar o termo "duck typing" (tipagem
pato), ao pregar para as massas de usuários de Java. Mas a expressão já era usada nas discussões do Python muito
antes do Ruby ou do Python se tornarem "populares". De acordo com a Wikipedia, um dos primeiros exemplos de
uso da analogia do pato, no contexto da programação orientada a objetos, foi uma mensagem para Python-list
(https://fpy.li/12-11) (EN), escrita por Alex Martelli e datada de 26 de julho de 2000: "polymorphism (was Re: Type
checking in python?)" (polimorfismo (era Re: Verificação de tipo em python?)) (https://fpy.li/12-9). Foi dali que veio a
citação no início desse capítulo. Se você tiver curiosidade sobre as origens literárias do termo "duck typing", e a
aplicação desse conceito de orientação a objetos em muitas linguagens, veja a página "Duck typing"
(https://pt.wikipedia.org/wiki/Duck_typing) na Wikipedia.

Um __format__ seguro, com usabilidade aperfeiçoada

Ao implementar __format__ , não tomei qualquer precaução a respeito de instâncias de Vector com um
número muito grande de componentes, como fizemos no __repr__ usando reprlib . A justificativa é que
repr() é usado para depuração e registro de logs, então precisa sempre gerar uma saída minimamente
aproveitável, enquanto __format__ é usado para exibir resultados para usuários finais, que presumivelmente
desejam ver o Vector inteiro. Se isso for considerado inconveniente, então seria legal implementar um nova
extensão à Minilinguagem de especificação de formato.

O quê eu faria: por default, qualquer Vector formatado mostraria um número razoável mas limitado de
componentes, digamos uns 30. Se existirem mais elementos que isso, o comportamento default seria similar ao de
reprlib : cortar o excesso e colocar …​em seu lugar. Entretanto, se o especificador de formato terminar com um
código especial * , significando "all" (todos), então a limitação de tamanho seria desabilitada. Assim, um usuário
ignorante do problema de exibição de vetores muito grandes não será acidentalmente penalizado. Mas se a
limitação default se tornar incômoda, a presença das …​iria incentivar o usuário a consultar a documentação e
descobrir o código de formatação * .
A busca por uma soma pythônica

Não há uma resposta única para a "O que é pythônico?", da mesma forma que não há uma resposta única para "O
que é belo?". Dizer, como eu mesmo muitas vezes faço, que significa usar "Python idiomático" não é 100%
satisfatório, porque talvez o que é "idiomático" para você não seja para mim. Sei de uma coisa: "idiomático" não
significa o uso dos recursos mais obscuros da linguagem.

Na Python-list (https://fpy.li/12-11) (EN), há uma thread de abril de 2003 chamada "Pythonic Way to Sum n-th List
Element?" (A forma pythônica de somar os "n" elementos de uma lista) (https://fpy.li/12-12). Ela é relevante para nossa
discussão de reduce acima nesse capítulo.

O autor original, Guy Middleton, pediu melhorias para essa solução, afirmando não gostar de usar lambda :[135]

PYCON
>>> my_list = [[1, 2, 3], [40, 50, 60], [9, 8, 7]]
>>> import functools
>>> functools.reduce(lambda a, b: a+b, [sub[1] for sub in my_list])
60

Esse código usa muitos idiomas: lambda , reduce e uma compreensão de lista. Ele provavelmente ficaria em
último lugar em um concurso de popularidade, pois ofende quem odeia lambda e também aqueles que
desprezam as compreensões de lista—praticamente os dois lados de uma disputa.

Se você vai usar lambda , provavelmente não há razão para usar uma compreensão de lista—exceto para
filtragem, que não é o caso aqui.

Aqui está uma solução minha que agradará os amantes de lambda :

PYCON
>>> functools.reduce(lambda a, b: a + b[1], my_list, 0)
60

Não tomei parte na discussão original, e não usaria o trecho acima em código real, pois eu também não gosto
muito de lambda . Mas eu queria mostrar um exemplo sem uma compreensão de lista.

A primeira resposta veio de Fernando Perez, criador do IPython, e realça como o NumPy suporta arrays n-
dimensionais e fatiamento n-dimensional:

PYCON
>>> import numpy as np
>>> my_array = np.array(my_list)
>>> np.sum(my_array[:, 1])
60

Acho a solução de Perez boa, mas Guy Middleton elegiou essa próxima solução, de Paul Rubin e Skip Montanaro:

PYCON
>>> import operator
>>> functools.reduce(operator.add, [sub[1] for sub in my_list], 0)
60

Então Evan Simpson perguntou, "Há algo errado com isso?":


PYCON
>>> total = 0
>>> for sub in my_list:
... total += sub[1]
...
>>> total
60

Muitos concordaram que esse código era bastante pythônico. Alex Martelli chegou a dizer que provavelmente
seria assim que Guido escreveria a solução.

Gosto do código de Evan Simpson, mas também gosto do comentário de David Eppstein sobre ele:

“ Sesomavocêdequer a soma de uma lista de itens, deveria escrever isso para se parecer com "a
uma lista de itens", não para se parecer com "faça um loop sobre esses itens,
mantenha uma outra variável t, execute uma sequência de adições". Por que outra razão
temos linguagens de alto nível, senão para expressar nossas intenções em um nível mais
alto e deixar a linguagem se preocupar com as operações de baixo nível necessárias para
implementá-las?

E daí Alex Martelli voltou para sugerir:

“ APython
soma é necessária com tanta frequência que eu não me importaria de forma alguma se o
a tornasse uma função embutida. Mas "reduce(operator.add, …​" não é mesmo uma
boa maneira de expressar isso, na minha opinião (e vejam que, como um antigo
APLista[136] e um apreciador da FP[137], eu deveria gostar daquilo, mas não gosto.).

Martelli então sugere uma função sum() , que ele mesmo programa e propõe para o Python. Ela se torna uma
função embutida no Python 2.3, lançado apenas três meses após aquela conversa na lista. E a sintaxe preferida de
Alex se torna a regra:

PYCON
>>> sum([sub[1] for sub in my_list])
60

No final do ano seguinte (novembro de 2004), o Python 2.4 foi lançado e incluia expressões geradoras, fornecendo
o que agora é, na minha opinião, a resposta mais pythônica para a pergunta original de Guy Middleton:

PYCON
>>> sum(sub[1] for sub in my_list)
60

Isso não só é mais legível que reduce , também evita a armadilha da sequência vazia: sum([]) é 0 , simples
assim.

Na mesma conversa, Alex Martelli sugeriu que a função embutida reduce do Python 2 trazia mais problemas
que soluções, porque encorajava idiomas de programação difíceis de explicar. Ele foi bastante convincente: a
função foi rebaixada para o módulo functools no Python 3.

Ainda assim, functools.reduce tem seus usos. Ela resolveu o problema de nosso Vector.__hash__ de uma
forma que eu chamaria de pythônica.
13. 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 Seção 8.4 do (Capítulo 8).

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 Capítulo 1.

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 (https://fpy.li/pep484).
Este não é o foco desse capítulo. A maior parte do Capítulo 8 e do Capítulo 15 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 Seção 8.5.10 (Capítulo 8), e continuamos nesse capítulo.

13.1. 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.
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):

Seção 13.3 compara duas formas de tipagem estrutural com protocolos - isto é, o lado esquerdo do Mapa.
Seção 13.4 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.
Seção 13.5 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.
Seção 13.6 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.
13.2. 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 introdução do capítulo e o Mapa de Sistemas de Tipagem (Figura 1) são novos. Essa é a chave da maior parte do
conteúdo novo - e de todos os outros capítulos relacionados à tipagem em Python ≥ 3.8.
Seção 13.3 explica as semelhanças e diferenças entre protocolos dinâmicos e estáticos.
Seção 13.4.3 praticamente reproduz o conteúdo da primeira edição, mas foi atualizada e agora tem um título de
seção que enfatiza sua importância.
Seção 13.6 é toda nova. Ela se apoia na apresentação inicial em Seção 8.5.10 (Capítulo 8).
Os diagramas de classe de collections.abc nas Figuras #sequence_uml_repeat, #mutablesequence_uml, and
#collections_uml foram atualizados para incluir a Collection ABC, do Python 3.6.

A primeira edição de Python Fluente tinha uma seção encorajando o uso das ABCs numbers para goose typing. Na
Seção 13.6.8 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.

13.3. 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 Seção 12.4 que um objeto protocolo especifica métodos que um objeto precisa oferecer para cumprir um
papel.

O exemplo FrenchDeck no Capítulo 1 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__


PYCON
>>> class Vowels:
... def __getitem__(self, i):
... return 'AEIOU'[i]
...
>>> v = Vowels()
>>> v[0]
'A'
>>> v[-1]
'U'
>>> for c in v: print(c)
...
A
E
I
O
U
>>> 'E' in v
True
>>> 'Z' in v
False

Implementar __getitem__ é o suficiente para obter itens pelo índice, e também para permitir iteração e o operador
in . O método especial __getitem__ é de fato o ponto central do protocolo de sequência.

Veja essa parte do Manual de referência da API Python/C (https://docs.python.org/pt-br/3/c-api/index.html), "Seção Protocolo de
Sequência" (https://docs.python.org/pt-br/3/c-api/sequence.html):

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) (https://fpy.li/pep544) (EN) no Python
3.8, a palavra "protocolo" ganhou um novo sentido em Python - um sentido próximo, mas diferente. Como vimos na
Seção 8.5.10 (Capítulo 8), 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" (https://docs.python.org/pt-br/3/reference/datamodel.html)
em A Referência da Linguagem Python.

Protocolo estático
Um protocolo como definido pela PEP 544—Protocols: Structural subtyping (static duck typing) (https://fpy.li/pep544), 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.

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

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

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 .
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
👉 DICA 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 Capítulo 1 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 Exemplo 1)

PYTHON3
import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):


return self._cards[position]

Muitos dos exemplos no Capítulo 1 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 (https://fpy.li/13-3).
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.

13.4.2. 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 .[138]

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 (https://docs.python.org/pt-br/3/library/random.html#random.shuffle) como
"Embaralha a sequência x internamente."

A função random.shuffle padrão é usada assim:

PYCON
>>> from random import shuffle
>>> l = list(range(10))
>>> shuffle(l)
>>> l
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]

👉 DICA 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

PYCON
>>> 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)
PYCON
>>> 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" (https://docs.python.org/pt-br/3/reference/datamodel.html#emulating-container-types). 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.

13.4.3. 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.[139] 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:

PYTHON3
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 Seção 17.3. 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 (https://fpy.li/13-8) 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

PYTHON3
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 (https://fpy.li/13-9)):

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

13.5. 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 (https://docs.python.org/pt-br/3/glossary.html#term-abstract-base-class) 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 , seriam desajeitadas ou sutilmente erradas
hasattr()
(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 .[140]

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.

Eu sou muito agradecido a meus amigos Alex MArtekli e Anna Ravenscroft. Mostrei a eles o primeiro
✒️ NOTA 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 (https://fpy.li/13-11) 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:

PYTHON3
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)[141]

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 (https://fpy.li/13-12) 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:

PYCON
>>> 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 outra
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:

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

Detalhes sobre o uso de register são tratados em Seção 13.5.6, 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

✒️ NOTA Sequence com as seguintes linhas:

PYTHON3
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 das 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 uma
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.

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

PYTHON3
from collections import namedtuple, abc

Card = namedtuple('Card', ['rank', 'suit'])

class FrenchDeck2(abc.MutableSequence):
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):


return self._cards[position]

def __setitem__(self, position, value): # (1)


self._cards[position] = value

def __delitem__(self, position): # (2)


del self._cards[position]

def insert(self, position, value): # (3)


self._cards.insert(position, value)

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.

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

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
👉 DICA __contains__ que executa uma busca binária usando a função bisect (https://fpy.li/13-13) da
biblioteca padrão.

Veja "Managing Ordered Sequences with Bisect" (https://fpy.li/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.

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

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 é

👉 DICA implementado fora do pacote collections — em Lib/_collections_abc.py (https://fpy.li/13-14) — então


é importado separado de collections . O outro módulo abc é apenas abc (i.e., Lib/abc.py
(https://fpy.li/13-15)), 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 (https://fpy.li/13-16) 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.[142]

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 Capítulo 3.

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 Seção 3.11.1.

Iterator
Observe que iterator é subclasse de Iterable . Discutimos melhor isso adiante, em Capítulo 17.

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 Capítulo 1 e na seção Seção
13.4.1. A documentação de collections.abc.Iterable
(https://docs.python.org/pt-br/3/library/collections.abc.html#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.

13.5.3. 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 Seção
8.5.7, 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.[143] 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.
A ABC vai ser chamada Tombola , seguindo o nome italiano do bingo (e do recipiente balançante 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.

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.
PY
import abc

class Tombola(abc.ABC): # (1)

@abc.abstractmethod
def load(self, iterable): # (2)
"""Add items from an iterable."""

@abc.abstractmethod
def pick(self): # (3)
"""Remove item at random, returning it.

This method should raise `LookupError` when the instance is empty.


"""

def loaded(self): # (4)


"""Return `True` if there's at least 1 item, `False` otherwise."""
return bool(self.inspect()) # (5)

def inspect(self):
"""Return a sorted tuple with the items currently inside."""
items = []
while True: # (6)
try:
items.append(self.pick())
except LookupError:
break
self.load(items) # (7)
return tuple(items)

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

Um método abstrato na verdade pode ter uma implementação. Mas mesmo que tenha, as subclasses

👉 DICA 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 (https://docs.python.org/pt-br/3/library/abc.html) 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.
Figura 6. Parte da hierarquia da classe Exception .[145]

➊ 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

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

13.5.4. 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 é:

PYTHON3
class MyABC(abc.ABC):
@classmethod
@abc.abstractmethod
def an_abstract_classmethod(cls, ...):
pass
A ordem dos decoradores de função empilhados importa, e no caso de @abstractmethod , a
documentação é explícita:

⚠️ AVISO “ Quando @abstractmethod é aplicado em combinação com outros descritores de


método, ele deve ser aplicado como o decorador mais interno…​ [146]

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.

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

PY
import random

from tombola import Tombola

class BingoCage(Tombola): # (1)

def __init__(self, items):


self._randomizer = random.SystemRandom() # (2)
self._items = []
self.load(items) # (3)

def load(self, items):


self._items.extend(items)
self._randomizer.shuffle(self._items) # (4)

def pick(self): # (5)


try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')

def __call__(self): # (6)


self.pick()

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 (https://docs.python.org/pt-br/3/library/os.html#os.urandom).
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 Exemplo 8.
6. __call__também é de Exemplo 8. 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

PY
import random

from tombola import Tombola

class LottoBlower(Tombola):

def __init__(self, iterable):


self._balls = list(iterable) # (1)

def load(self, iterable):


self._balls.extend(iterable)

def pick(self):
try:
position = random.randrange(len(self._balls)) # (2)
except ValueError:
raise LookupError('pick from empty LottoBlower')
return self._balls.pop(position) # (3)

def loaded(self): # (4)


return bool(self._balls)

def inspect(self): # (5)


return tuple(self._balls)

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 Seção 13.4.3, 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.[147]
Chegamos agora à característica dinâmica crucial da goose typing: declarar subclasses virtuais com o método
register

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

Subclasses virtuais não herdam da ABC na qual se registram, e sua conformidade com a interface da

⚠️ AVISO 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 (https://fpy.li/13-22).

O método register normalmente é invocado como uma função comum (veja Seção 13.5.7), 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.
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


PYTHON3
from random import randrange

from tombola import Tombola

@Tombola.register # (1)
class TomboList(list): # (2)

def pick(self):
if self: # (3)
position = randrange(len(self))
return self.pop(position) # (4)
else:
raise LookupError('pop from empty TomboList')

load = list.extend # (5)

def loaded(self):
return bool(self) # (6)

def inspect(self):
return tuple(self)

# Tombola.register(TomboList) # (7)

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

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 :

PYCON
>>> 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.[149] Se você inspecionar o __mro__ de TomboList , verá que ele lista
apenas as superclasses "reais" - list e object :

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

13.5.7. 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 (https://fpy.li/13-24) do módulo
collections.abc , os tipos nativos tuple , str , range , e memoryview são registrados como subclasses virtuais de
Sequence assim:

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

13.5.8. 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.[150] 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 :

PYCON
>>> 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 (https://fpy.li/13-25)

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

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 (https://fpy.li/13-26). Cuidado: ele tem muitos
ifs e duas chamadas recursivas. No Python 3.7, Ivan Levkivskyi and Inada Naoki reescreveram em C
✒️ NOTA a maior parte da lógica do módulo abc , para melhorar o desempenho. Veja Python issue #31333
(https://fpy.li/13-27). 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
(https://fpy.li/13-28).

É 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
(https://fpy.li/13-29) 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 Spamuma 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.

13.6. Protocolos estáticos


Vimos algo sobre protocolos estáticos em Seção 8.5.10 (Capítulo 8). Até considerei deixar toda a

✒️ NOTA 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 Seção 8.4.

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

PYCON
>>> 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.[151]

Graças ao duck typing, double funciona mesmo com tipos do futuro, tal como a classe Vector aprimorada que
veremos no Seção 16.5 (Capítulo 16):

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


PY
from typing import TypeVar, Protocol

T = TypeVar('T') # (1)

class Repeatable(Protocol):
def __mul__(self: T, repeat_count: int) -> T: ... # (2)

RT = TypeVar('RT', bound=Repeatable) # (3)

def double(x: RT) -> RT: # (4)


return x * 2

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 (https://fpy.li/pep544) é "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__ .

13.6.2. 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
Seção 13.5.8.

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 (https://fpy.li/13-30):

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 (https://fpy.li/13-31) do protocolo typing.SupportsComplex .

Exemplo 14. código-fonte do protocolo typing.SupportsComplex


PYTHON3
@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__ .[152] 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

PYCON
>>> 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:

PYTHON
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:
PYTHON
>>> 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 Seção 13.6.8.

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.

Se você estiver usando um verificador de tipo externo, há uma vantagem nas verificações explícitas
👉 DICA 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:

PYTHON
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 :

PYTHON
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:

PYTHON
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:

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

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

PYCON
>>> 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 Seção 8.5.12.

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:

PYCON
>>> 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.
O problema específico com o tipo complex foi resolvido no Python 3.10.0b4, com a remoção do
método complex.__float__ .

⚠️ AVISO 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.[153]

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

13.6.4. Suportando um protocolo estático


Lembra da classe Vector2d , que desenvolvemos em Capítulo 11? 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 Exemplo 11. 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

PY
def __complex__(self):
return complex(self.x, self.y)

@classmethod
def fromcomplex(cls, datum):
return cls(datum.real, datum.imag) # (1)

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 Exemplo 11, temos o seguinte:

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


PY
def __abs__(self) -> float: # (1)
return math.hypot(self.x, self.y)

def __complex__(self) -> complex: # (2)


return complex(self.x, self.y)

@classmethod
def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # (3)
c = complex(datum) # (4)
return cls(c.real, c.imag)

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 (https://fpy.li/pep563), 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.[154]
Quando isso acontecer, a importação será redundante mas inofensiva.

Agora vamos criar - e depois estender - um novo protocolo estático.

13.6.5. Projetando um protocolo estático


Quando estudamos goose typing, vimos a ABC Tombola em Seção 13.5.3. 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


PYTHON3
from typing import Protocol, runtime_checkable, Any

@runtime_checkable
class RandomPicker(Protocol):
def pick(self) -> Any: ...

O método pick retorna Any . Em Seção 15.8 veremos como tornar RandomPicker um tipo
✒️ NOTA 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

PYTHON3
import random
from typing import Any, Iterable, TYPE_CHECKING

from randompick import RandomPicker # (1)

class SimplePicker: # (2)


def __init__(self, items: Iterable) -> None:
self._items = list(items)
random.shuffle(self._items)

def pick(self) -> Any: # (3)


return self._items.pop()

def test_isinstance() -> None: # (4)


popper: RandomPicker = SimplePicker([1]) # (5)
assert isinstance(popper, RandomPicker) # (6)

def test_item_type() -> None: # (7)


items = [1, 2]
popper = SimplePicker(items)
item = popper.pick()
assert item in items
if TYPE_CHECKING:
reveal_type(item) # (8)
assert isinstance(item, int)

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 Exemplo 40, 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 :

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

13.6.6. 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 escreveu um post
definindo a interface papel (https://fpy.li/13-33)[155], uma ideia útil de ter em mente ao desenvolver protocolos.

Além disso, algumas vezes vemos 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, algo 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
(https://pt.wikipedia.org/wiki/Princ%C3%ADpio_da_segrega%C3%A7%C3%A3o_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" (https://fpy.li/13-35) (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 para protocolos que oferecem métodos que podem ser chamados (e.g.,
SupportsX SupportsInt ,
SupportsRead , SupportsReadSeek ).[156]

Use para protocolos que tem atributos que podem ser lidos ou escritos, ou métodos getter/setter(e.g.,
HasX
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)" (https://fpy.li/13-36) (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

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

PYTHON3
from typing import Protocol, runtime_checkable
from randompick import RandomPicker

@runtime_checkable # (1)
class LoadableRandomPicker(RandomPicker, Protocol): # (2)
def load(self, Iterable) -> None: ... # (3)

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.[157]
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.[158]
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.

13.6.8. As ABCs em numbers e os novod protocolos numéricos


Como vimos em Seção 8.5.7.1, 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 (https://fpy.li/13-39) — 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 .

De forma algo surpreendente, decimal.Decimal não é registrado como uma subclasse virtual de

👉 DICA 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
(https://fpy.li/13-40),

Lá você encontrará as seguintes definições, que são usadas para anotar várias funções:
PYTHON
_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 Seção
13.6.2.

Infelizmente, durante a execução os protocolos numéricos podem deixar você na mão. Como mencionado em Seção
13.6.3, 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.[159]

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:

PYCON
>>> 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.[160] 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.

PYCON
>>> 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:

PYTHON3
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 .[161] 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__ .[162]

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 (https://fpy.li/cardxvi) (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__ .

Eu só encontrei resultados inesperados usando verificações com isinstance em conjunto com os


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

13.7. 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 Seção 13.4, 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 Seção 13.6, onde retomamos o estudo do duck typing estático, que havia começado no
Capítulo 8, em Seção 8.5.10. 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 Seção 13.6.8, 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.

13.8. 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" (https://fpy.li/13-42) (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"
(https://fpy.li/13-43) (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
(https://fpy.li/13-44), na Pyramid web framework (https://fpy.li/13-45), e na framework de programação assíncrona Twisted
(https://fpy.li/13-46), um projeto fundado por Glyph.[163]

Ó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., (https://fpy.li/pynut3) 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" (https://fpy.li/13-47) (EN) ("Contratos
em Python: Uma Conversa com Guido van Rossum, Parte IV"). O post "Dynamic Typing" (https://fpy.li/13-48) (EN)
("Tipagem Dinâmica"), de Martin Fowler, traz uma avaliação perspicaz e equilibrada deste debate. Ele também
escreveu "Role Interface" (https://fpy.li/13-33) (EN) ("Interface Papel"), que mencionei na Seção 13.6.6. 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"
(https://fpy.li/13-50) (EN) ("Protocolos e subtipagem estrutural").

As referências restantes são todas sobre goose typing.

Beazley and Jones’s Python Cookbook (https://fpy.li/pycook3), 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 (https://fpy.li/13-51)
(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 (https://fpy.li/13-52) (EN) apresenta a justificativa para as ABCs. A PEP 3141
—​A Type Hierarchy for Numbers (https://fpy.li/13-53) (EN) apresenta as ABCs do módulo numbers (https://fpy.li/13-54), mas a
discussão no Mypy issue #3186 "int is not a Number?" (https://fpy.li/13-55) 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 (https://fpy.li/13-56), discutindo formas de anotar tipos numéricos.

Vou continuar monitorando o Mypy issue #3186 (https://fpy.li/13-55) 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" (https://fpy.li/13-58), um post no
Martin Fowler’s collective blog (https://fpy.li/13-59).

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 (https://fpy.li/pep3107) 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 (https://fpy.li/pep484) 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 (https://fpy.li/13-60), o Wing (https://fpy.li/13-61), e o VS Code (https://fpy.li/13-62).

A PEP 526—Syntax for Variable Annotations (https://fpy.li/pep526) 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 (https://fpy.li/pep563) e a PEP 560—Core support for
typing module and generic types (https://fpy.li/pep560), 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 (https://fpy.li/pep585).

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.

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.
14. Herança: para o bem ou para o mal
“ […​
] precisávamos de toda uma teoria melhor sobre herança (e ainda precisamos). Por exemplo,
herança e instanciação (que é um tipo de herança) embaralham tanto a pragmática (tal como
fatorar o código para economizar espaço) quanto a semântica (usada para um excesso de
tarefas tais como: especialização, generalização, especiação, etc.).[164]
— Alan Kay
Os Primórdios do Smalltalk

Esse capítulo é sobre herança e criação de subclasses. Vou presumir um entendimento básico desses conceitos, que
você pode ter aprendido lendo O Tutorial do Python (https://docs.python.org/pt-br/3/tutorial/classes.html), ou por experiências
com outra linguagem orientada a objetos popular, tal como Java, C# ou C++. Aqui vamos nos concentrar em quatro
características do Python:

A função super()

As armadilhas na criação de subclasses de tipos embutidos


Herança múltipla e a ordem de resolução de métodos
Classes mixin

Herança múltipla acontece quando uma classe tem mais de uma classe base. O C++ a suporta; Java e C# não. Muitos
consideram que a herança múltipla não vale a quantidade de problemas que causa. Ela foi deliberadamente deixada
de fora do Java, após seu aparente abuso nas primeiras bases de código C++.

Esse capítulo introduz a herança múltipla para aqueles que nunca a usaram, e fornece alguma orientação sobre como
lidar com herança simples ou múltipla, se você precisar usá-la.

Em 2021, quando escrevo essas linhas, há uma forte reação contra o uso excessivo de herança em geral—não apenas
herança múltipla—porque superclasses e subclasses são fortemente acopladas, ou seja, interdependentes. Esse
acoplamento forte significa que modificações em uma classe pode ter efeitos inesperados e de longo alcance em suas
subclasses, tornando os sistemas frágeis e difíceis de entender.

Entretanto, ainda temos que manter os sistemas existentes, que podem ter complexas hierarquias de classe, ou
trabalhar com frameworks que nos obrigam a usar herança—algumas vezes até herança múltipla.

Vou ilustar as aplicações práticas da herança múltipla com a biblioteca padrão, o framework para programação web
Django e o toolkit para programação de interface gráfica Tkinter.

14.1. Novidades nesse capítulo


Não há nenhum recurso novo no Python no que diz respeito ao assunto desse capítulo, mas fiz inúmeras modificações
baseadas nos comentários dos revisores técnicos da segunda edição, especialmente Leonardo Rochael e Caleb Hattingh.

Escrevi uma nova seção de abertura, tratando especificamente da função embutida super() , e mudei os exemplos na
seção Seção 14.4, para explorar mais profundamente a forma como super() no suporte à herança múltipla
cooperativa.

A seção Seção 14.5 também é nova. A seção Seção 14.6 foi reorganizada, e apresenta exemplos mais simples de mixin
vindos da bilbioteca padrão, antes de apresentar o grande framework Django e as complicadas hierarquias do Tkinter.
Como o próprio título sugere, as ressalvas à herança sempre foram um dos temas principais desse capítulo. Mas como
cada vez mais desenvolvedores consideram essa técnica problemática, acrescentei alguns parágrafos sobre como evitar
a herança no final do Seção 14.8 e da Seção 14.9.

Vamos começar com uma revisão da misteriosa função super() .

14.2. A função super()


O uso consistente da função embutida super() é essencial na criação de programas Python orientados a objetos fáceis
de manter.

Quando uma subclasse sobrepõe um método de uma superclasse, esse novo método normalmente precisa invocar o
método correspondente na superclasse. Aqui está o modo recomendado de fazer isso, tirado de um exemplo da
documentação do módulo collections, na seção "OrderedDict Examples and Recipes" (OrderedDict: Exemplos e Receitas)
(https://docs.python.org/pt-br/3/library/collections.html#ordereddict-examples-and-recipes) (EN).:[165]

PY
class LastUpdatedOrderedDict(OrderedDict):
"""Store items in the order they were last updated"""

def __setitem__(self, key, value):


super().__setitem__(key, value)
self.move_to_end(key)

Para executar sua tarefa, LastUpdatedOrderedDict sobrepõe __setitem__ para:

1. Usar super().__setitem__ , invocando aquele método na superclasse e permitindo que ele insira ou atualize o
par chave/valor.
2. Invocar self.move_to_end , para garantir que a key atualizada esteja na última posição.

Invocar um __init__ sobreposto é particulamente importante, para permitir que a superclasse execute sua parte na
inicialização da instância.

Se você aprendeu programação orientada a objetos com Java, com certeza se lembra que, naquela
linguagem, um método construtor invoca automaticamente o construtor sem argumentos da
superclasse. O Python não faz isso. Se acostume a escrever o seguinte código padrão:
👉 DICA def __init__(self, a, b) :
PY

super().__init__(a, b)
... # more initialization code

Você pode já ter visto código que não usa super() , e em vez disso chama o método na superclasse diretamente, assim:

PY
class NotRecommended(OrderedDict):
"""This is a counter example!"""

def __setitem__(self, key, value):


OrderedDict.__setitem__(self, key, value)
self.move_to_end(key)

Essa alternativa até funciona nesse caso em particular, mas não é recomendado por duas razões. Primeiro, codifica a
superclasse explicitamente. O nome OrderedDict aparece na declaração class e também dentro de __setitem__ .
Se, no futuro, alguém modificar a declaração class para mudar a classe base ou adicionar outra, pode se esquecer de
atualizar o corpo de __setitem__ , introduzindo um bug.
A segunda razão é que super implementa lógica para tratar hierarquias de classe com herança múltipla. Voltaremos a
isso na seção Seção 14.4. Para concluir essa recapitulação de super , é útil rever como essa função era invocada no
Python 2, porque a assinatura antiga, com dois argumentos, é reveladora:

PY
class LastUpdatedOrderedDict(OrderedDict):
"""This code works in Python 2 and Python 3"""

def __setitem__(self, key, value):


super(LastUpdatedOrderedDict, self).__setitem__(key, value)
self.move_to_end(key)

Os dois argumento de super são agora opcionais. O compilador de bytecode do Python 3 obtém e fornece ambos
examinando o contexto circundante, quando super() é invocado dentro de um método. Os argumentos são:

type

O início do caminho para a superclasse que implementa o método desejado. Por default, é a classe que possui o
método onde a chamada a super() aparece.

object_or_type

O objeto (para chamadas a métodos de instância) ou classe (para chamadas a métodos de classe) que serão os
recipientes da chamada ao método. Por default, é self se a chamada super() acontece em um método de
instância.

Independente desses argumentos serem fornecidos por você ou pelo compilador, a chamada a super() devolve um
objeto proxy dinâmico que encontra um método (tal como __setitem__ no exemplo) em uma superclasse do
parâmetro type e a vincula ao object_or_type , de modo que não precisamos passar explicitamente o recipiente
( self ) quando invocamos o método.

No Python 3, ainda é permitido passar explicitamente o primeiro e o segundo argumentos a super() .[166] Mas eles são
necessários apenas em casos especiais, tal como pular parte do MRO (sigla de Method Resolution Order—ORM, Ordem
de Resolução de Métodos), para testes ou depuração, ou para contornar algum comportamento indesejado em uma
superclasse.

Vamos agora discutir as ressalvas à criação de subclasses de tipos embutidos.

14.3. É complicado criar subclasses de tipos embutidos


Nas primeiras versões do Python não era possível criar subclasses de tipos embutidos como list ou dict . Desde o
Python 2.2 isso é possível, mas há restrição importante: o código (escrito em C) dos tipos embutidos normalmente não
chama os métodos sobrepostos por classes definidas pelo usuário. Há uma boa descrição curta do problema na
documentação do PyPy, na seção "Differences between PyPy and CPython" ("Diferenças entre o PyPy e o CPython"),
"Subclasses of built-in types" (Subclasses de tipos embutidos) (https://fpy.li/pypydif):

“ Oficialmente, o CPython não tem qualquer regra sobre exatamente quando um método
sobreposto de subclasses de tipos embutidos é ou não invocado implicitamente. Como uma
aproximação, esses métodos nunca são chamados por outros métodos embutidos do mesmo
objeto. Por exemplo, um __getitem__ sobreposto em uma subclasse de dict nunca será
invocado pelo método get() do tipo embutido.

O Exemplo 21 ilustra o problema.


Exemplo 21. Nossa sobreposição de __setitem__ é ignorado pelos métodos __init__ e __update__ to tipo
embutido dict

PYCON
>>> class DoppelDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2) # (1)
...
>>> dd = DoppelDict(one=1) # (2)
>>> dd
{'one': 1}
>>> dd['two'] = 2 # (3)
>>> dd
{'one': 1, 'two': [2, 2]}
>>> dd.update(three=3) # (4)
>>> dd
{'three': 3, 'one': 1, 'two': [2, 2]}

1. DoppelDict.__setitem__ duplica os valores ao armazená-los (por nenhuma razão em especial, apenas para
termos um efeito visível). Ele funciona delegando para a superclasse.
2. O método __init__ , herdado de dict , claramente ignora que __setitem__ foi sobreposto: o valor de 'one'
não foi duplicado.
3. O operador [] chama nosso __setitem__ e funciona como esperado: 'two' está mapeado para o valor
duplicado [2, 2] .
4. O método update de dict também não usa nossa versão de __setitem__ : o valor de 'three' não foi
duplicado.

Esse comportamento dos tipos embutidos é uma violação de uma regra básica da programação orientada a objetos: a
busca por métodos deveria sempre começar pela classe do recipiente ( self ), mesmo quando a chamada ocorre dentro
de um método implementado na superclasse. Isso é o que se chama "vinculação tardia" ("late binding"), que Alan Kay—
um dos criadores do Smalltalk—considera ser uma característica básica da programação orientada a objetos: em
qualquer chamada na forma x.method() , o método exato a ser chamado deve ser determinado durante a execução,
baseado na classe do recipiente x .[167] Este triste estado de coisas contribui para os problemas que vimos na seção
Seção 3.5.3.

O problema não está limitado a chamadas dentro de uma instância—saber se self.get() chama self.getitem() —
mas também acontece com métodos sobrepostos de outras classes que deveriam ser chamados por métodos
embutidos. O Exemplo 22 foi adaptado da documentação do PyPy (https://fpy.li/14-5) (EN).

Exemplo 22. O __getitem__ de AnswerDict é ignorado por dict.update

PYCON
>>> class AnswerDict(dict):
... def __getitem__(self, key): # (1)
... return 42
...
>>> ad = AnswerDict(a='foo') # (2)
>>> ad['a'] # (3)
42
>>> d = {}
>>> d.update(ad) # (4)
>>> d['a'] # (5)
'foo'
>>> d
{'a': 'foo'}
1. AnswerDict.__getitem__ sempre devolve 42 , independente da chave.

2. ad é um AnswerDict carregado com o par chave-valor ('a', 'foo') .

3. ad['a'] devolve 42 , como esperado.

4. d é uma instância direta de dict , que atualizamos com ad .

5. O método dict.update ignora nosso AnswerDict.__getitem__ .

Criar subclasses diretamente de tipos embutidos como dict ou list ou str é um processo
propenso ao erro, pois os métodos embutidos quase sempre ignoram as sobreposições definidas pelo
⚠️ AVISO usuário. Em vez de criar subclasses de tipos embutidos, derive suas classes do módulo collections
(https://docs.python.org/pt-br/3/library/collections.html), usando as classes UserDict , UserList , e
UserString , que foram projetadas para serem fáceis de estender.

Se você criar uma subclasse de collections.UserDict em vez de dict , os problemas expostos no Exemplo 21 e no
Exemplo 22 desaparecem. Veja o Exemplo 23.

Exemplo 23. DoppelDict2 and AnswerDict2 funcionam como esperado, porque estendem UserDict e não dict

PYCON
>>> import collections
>>>
>>> class DoppelDict2(collections.UserDict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2)
...
>>> dd = DoppelDict2(one=1)
>>> dd
{'one': [1, 1]}
>>> dd['two'] = 2
>>> dd
{'two': [2, 2], 'one': [1, 1]}
>>> dd.update(three=3)
>>> dd
{'two': [2, 2], 'three': [3, 3], 'one': [1, 1]}
>>>
>>> class AnswerDict2(collections.UserDict):
... def __getitem__(self, key):
... return 42
...
>>> ad = AnswerDict2(a='foo')
>>> ad['a']
42
>>> d = {}
>>> d.update(ad)
>>> d['a']
42
>>> d
{'a': 42}

Como um experimento, para medir o trabalho extra necessário para criar uma subclasse de um tipo embutido,
reescrevi a classe StrKeyDict do Exemplo 9, para torná-la uma subclasse de dict em vez de UserDict . Para fazê-la
passar pelo mesmo banco de testes, tive que implementar __init__ , get , e update , pois as versões herdadas de
dict se recusaram a cooperar com os métodos sobrepostos __missing__ , __contains__ e __setitem__ . A
subclasse de UserDict no Exemplo 9 tem 16 linhas, enquanto a subclasse experimental de dict acabou com 33
linhas.[168]
Para deixar claro: essa seção tratou de um problema que se aplica apenas à delegação a métodos dentro do código em C
dos tipos embutidos, e afeta apenas classes derivadas diretamente daqueles tipos. Se você criar uma subclasse de uma
classe escrita em Python, tal como UserDict ou MutableMapping , não vai encontrar esse problema.[169]

Vamos agora examinar uma questão que aparece na herança múltipla: se uma classe tem duas superclasses, como o
Python decide qual atributo usar quando invocamos super().attr , mas ambas as superclasses tem um atributo com
esse nome?

14.4. Herança múltipla e a Ordem de Resolução de Métodos


Qualquer linguagem que implemente herança múltipla precisa lidar com o potencial conflito de nomes, quando
superclasses contêm métodos com nomes iguais. Isso é chamado "o problema do diamante", ilustrado na Figura 9 e no
Exemplo 24.

Figura 9. Esquerda: Sequência de ativação para a chamada leaf1.ping() . Direita: Sequência de ativação para a
chamada leaf1.pong() .

Exemplo 24. diamond.py: classes Leaf , A , B , Root formam o grafo na Figura 9


PYTHON3
class Root: # (1)
def ping(self):
print(f'{self}.ping() in Root')

def pong(self):
print(f'{self}.pong() in Root')

def __repr__(self):
cls_name = type(self).__name__
return f'<instance of {cls_name}>'

class A(Root): # (2)


def ping(self):
print(f'{self}.ping() in A')
super().ping()

def pong(self):
print(f'{self}.pong() in A')
super().pong()

class B(Root): # (3)


def ping(self):
print(f'{self}.ping() in B')
super().ping()

def pong(self):
print(f'{self}.pong() in B')

class Leaf(A, B): # (4)


def ping(self):
print(f'{self}.ping() in Leaf')
super().ping()

1. Root fornece ping , pong , e __repr__ (para facilitar a leitura da saída).


2. Os métodos ping e pong na classe A chamam super() .

3. Apenas o método ping na classe B chama super() .

4. A classe Leaf implementa apenas ping , e chama super() .

Vejamos agora o efeito da invocação dos métodos ping e pong em uma instância de Leaf (Exemplo 25).

Exemplo 25. Doctests para chamadas a ping e pong em um objeto Leaf

PYTHON3
>>> leaf1 = Leaf() # (1)
>>> leaf1.ping() # (2)
<instance of Leaf>.ping() in Leaf
<instance of Leaf>.ping() in A
<instance of Leaf>.ping() in B
<instance of Leaf>.ping() in Root

>>> leaf1.pong() # (3)


<instance of Leaf>.pong() in A
<instance of Leaf>.pong() in B

1. leaf1 é uma instância de Leaf .


2. Chamar leaf1.ping() ativa os métodos ping em Leaf , A , B , e Root , porque os métodos ping nas três
primeiras classes chamam super().ping() .
3. Chamar leaf1.pong() ativa pong em A através da herança, que por sua vez chama super.pong() , ativando
B.pong .
As sequências de ativação que aparecem no Exemplo 25 e na Figura 9 são determinadas por dois fatores:

A ordem de resolução de métodos da classe Leaf .

O uso de super() em cada método.

Todas as classes possuem um atributo chamado __mro__ , que mantém uma tupla de referências a superclasses, na
ordem de resolução dos métodos, indo desde a classe corrente até a classe object .[170] Para a classe Leaf class, o
__mro__ é o seguinte:

PYCON
>>> Leaf.__mro__ # doctest:+NORMALIZE_WHITESPACE
(<class 'diamond1.Leaf'>, <class 'diamond1.A'>, <class 'diamond1.B'>,
<class 'diamond1.Root'>, <class 'object'>)

Olhando para a Figura 9, pode parecer que a ORM descreve uma busca em largura (ou amplitude)
(https://pt.wikipedia.org/wiki/Busca_em_largura), mas isso é apenas uma coincidência para essa
hierarquia de classes em particular. A ORM é computada por um algoritmo conhecido, chamado C3.

✒️ NOTA Seu uso no Python está detalhado no artigo "The Python 2.3 Method Resolution Order" (A Ordem de
Resolução de Métodos no Python 2.3) (https://fpy.li/14-10), de Michele Simionato. É um texto difícil, mas
Simionato escreve: "…​a menos que você faça amplo uso de herança múltipla e mantenha
hierarquias não-triviais, não é necessário entender o algoritmo C3, e você pode facilmente ignorar
este artigo."

A ORM determina apenas a ordem de ativação, mas se um método específico será ou não ativado em cada uma das
classes vai depender de cada implementação chamar ou não super() .

Considere o experimento com o método pong . A classe Leaf não sobrepõe aquele método, então a chamada
leaf1.pong() ativa a implementação na próxima classe listada em Leaf.__mro__ : a classe A . O método A.pong
chama super().pong() . A classe B class é e próxima na ORM, portanto B.pong é ativado. Mas aquele método não
chama super().pong() , então a sequência de ativação termina ali.

Além do grafo de herança, a ORM também leva em consideração a ordem na qual as superclasses aparecem na
declaração da uma subclasse. Em outras palavras, se em diamond.py (no Exemplo 24) a classe Leaf fosse declarada
como Leaf(B, A) , daí a classe B apareceria antes de A em Leaf.__mro__ . Isso afetaria a ordem de ativação dos
métodos ping , e também faria leaf1.pong() ativar B.pong através da herança, mas A.pong e Root.pong nunca
seriam executados, porque B.pong não chama super() .

Quando um método invoca super() , ele é um método cooperativo. Métodos cooperativos permitem a herança múltipla
cooperativa. Esses termos são intencionais: para funcionar, a heranca múltipla no Python exige a cooperação ativa dos
métodos envolvidos. Na classe B , ping coopera, mas pong não.

Um método não-cooperativo pode ser a causa de bugs sutis. Muitos programadores, lendo o Exemplo
⚠️ AVISO 24, poderiam esperar que, quando o método A.pong invoca super.pong() , isso acabaria por
ativar Root.pong . Mas se B.pong for ativado antes, ele deixa a bola cair. Por isso, é recomendado
que todo método m de uma classe não-base chame super().m() .
Métodos cooperativos devem ter assinaturas compatíveis, porque nunca se sabe se A.ping será chamado antes ou
depois de B.ping . A sequência de ativação depende da ordem de A e B na declaração de cada subclasse que herda de
ambos.

O Python é uma linguagem dinâmica, então a interação de super() com a ORM também é dinâmica. O Exemplo 26
mostra um resultado surpreendente desse comportamento dinâmico.

Exemplo 26. diamond2.py: classes para demonstrar a natureza dinâmica de super()

PYTHON3
from diamond import A # (1)

class U(): # (2)


def ping(self):
print(f'{self}.ping() in U')
super().ping() # (3)

class LeafUA(U, A): # (4)


def ping(self):
print(f'{self}.ping() in LeafUA')
super().ping()

1. A classe A vem de diamond.py (no Exemplo 24).


2. A classe U não tem relação com A ou`Root` do módulo diamond .

3. O que super().ping() faz? Resposta: depende. Continue lendo.


4. LeafUA é subclasse de U e A , nessa ordem.

Se você criar uma instância de U e tentar chamar ping , ocorre um erro:

PYCON
>>> u = U()
>>> u.ping()
Traceback (most recent call last):
...
AttributeError: 'super' object has no attribute 'ping'

O objeto 'super' devolvido por super() não tem um atributo 'ping' , porque o ORM de U tem duas classes: U e
object , e este último não tem um atributo chamado 'ping' .

Entretanto, o método U.ping não é inteiramente sem solução. Veja isso:

PYCON
>>> leaf2 = LeafUA()
>>> leaf2.ping()
<instance of LeafUA>.ping() in LeafUA
<instance of LeafUA>.ping() in U
<instance of LeafUA>.ping() in A
<instance of LeafUA>.ping() in Root
>>> LeafUA.__mro__ # doctest:+NORMALIZE_WHITESPACE
(<class 'diamond2.LeafUA'>, <class 'diamond2.U'>,
<class 'diamond.A'>, <class 'diamond.Root'>, <class 'object'>)

A chamada super().ping() em LeafUA ativa U.ping , que também coopera chamando super().ping() , ativando
A.ping e, por fim, Root.ping .
Observe que as clsses base de LeafUA são (U, A) , nessa ordem. Se em vez disso as bases fossem (A, U) , daí
leaf2.ping() nunca chegaria a U.ping , porque o super().ping() em A.ping ativaria Root.ping , e esse último
não chama super() .

Em um programa real, uma classe como U poderia ser uma classe mixin: uma classe projetada para ser usada junto
com outras classes em herança múltipla, fornecendo funcionalidade adicional. Vamos estudar isso em breve, na seção
Seção 14.5.

Para concluir essa discussão sobre a ORM, a Figura 10 ilustra parte do complexo grafo de herança múltipla do toolkit de
interface gráfica Tkinter, da biblioteca padrão do Python.

Figura 10. Esquerda: diagrama UML da classe e das superclasses do componente Text do Tkinter. Direita: O longo e
sinuoso caminho de Text.__mro__ , desenhado com as setas pontilhadas.

Para estudar a figura, comece pela classe Text , na parte inferior. A classe Text implementa um componente de texto
completo, editável e com múltiplas linhas. Ele sozinho fornece muita funcionalidade, mas também herda muitos
métodos de outras classes. A imagem à esquerda mostra um diagrama de classe UML simples. À direita, a mesma
imagem é decorada com setas mostrando a ORM, como listada no Exemplo 27 com a ajuda de uma função de
conveniência print_mro .

Exemplo 27. MRO de tkinter.Text

PYCON
>>> def print_mro(cls):
... print(', '.join(c.__name__ for c in cls.__mro__))
>>> import tkinter
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

Vamos agora falar sobre mixins.


14.5. Classes mixin
Uma classe mixin é projetada para ser herdada em conjunto com pelo menos uma outra classe, em um arranjo de
herança múltipla. Uma mixin não é feita para ser a única classe base de uma classe concreta, pois não fornece toda a
funcionalidade para um objeto concreto, apenas adicionando ou personalizando o comportamento de classes filhas ou
irmãs.

Classes mixin são uma convenção sem qualquer suporte explícito no Python e no C++. O Ruby

✒️ NOTA permite a definição explícita e o uso de módulos que funcionam como mixins—coleções de métodos
que podem ser incluídas para adicionar funcionalidade a uma classe. C#, PHP, e Rust implementam
traits (características ou traços ou aspectos), que são também uma forma explícita de mixin.

Vamos ver um exemplo simples mas conveniente de uma classe mixin.

14.5.1. Mapeamentos maiúsculos


O Exemplo 28 mostra a UpperCaseMixin , uma classe criada para fornecer acesso indiferente a maiúsculas/minúsculas
para mapeamentos com chaves do tipo string, convertendo todas as chaves para maiúsculas quando elas são
adicionadas ou consultadas.

Exemplo 28. uppermixin.py: UpperCaseMixin suporta mapeamentos indiferentes a maiúsculas/minúsculas

PYTHON3
import collections

def _upper(key): # (1)


try:
return key.upper()
except AttributeError:
return key

class UpperCaseMixin: # (2)


def __setitem__(self, key, item):
super().__setitem__(_upper(key), item)

def __getitem__(self, key):


return super().__getitem__(_upper(key))

def get(self, key, default=None):


return super().get(_upper(key), default)

def __contains__(self, key):


return super().__contains__(_upper(key))

1. Essa função auxiliar recebe uma key de qualquer tipo e tenta devolver key.upper() ; se isso falha, devolve a key
inalterada.
2. A mixin implementa quatro métodos essenciais de mapeamentos, sempre chamando `super()`com a chave em
maiúsculas, se possível.

Como todos os métodos de UpperCaseMixin chamam super() , esta mixin depende de uma classe irmã que
implemente ou herde métodos com a mesma assinatura. Para dar sua contribuição, uma mixin normalmente precisa
aparecer antes de outras classes na ORM de uma subclasse que a use. Na prática, isso significa que mixins devem
aparecer primeiro na tupla de classes base em uma declaração de classe. O Exemplo 29 apresenta dois exemplos.

Exemplo 29. uppermixin.py: duas classes que usam UpperCaseMixin


PYTHON3
class UpperDict(UpperCaseMixin, collections.UserDict): # (1)
pass

class UpperCounter(UpperCaseMixin, collections.Counter): # (2)


"""Specialized 'Counter' that uppercases string keys""" # (3)

1. UpperDict não precisa de qualquer implementação própria, mas UpperCaseMixin deve ser a primeira classe
base, caso contrário os métodos chamados seriam os de UserDict .
2. UpperCaseMixin também funciona com Counter .

3. Em vez de pass , é melhor fornecer uma docstring para satisfazer a necessidade sintática de um corpo não-vazio
na declaração class .

Aqui estão alguns doctests de uppermixin.py (https://fpy.li/14-11), para UpperDict :

PYCON
>>> d = UpperDict([('a', 'letter A'), (2, 'digit two')])
>>> list(d.keys())
['A', 2]
>>> d['b'] = 'letter B'
>>> 'b' in d
True
>>> d['a'], d.get('B')
('letter A', 'letter B')
>>> list(d.keys())
['A', 2, 'B']

E uma rápida demonstração de UpperCounter :

PYCON
>>> c = UpperCounter('BaNanA')
>>> c.most_common()
[('A', 3), ('N', 2), ('B', 1)]

UpperDict e UpperCounter parecem quase mágica, mas tive que estudar cuidadosamente o código de UserDict e
Counter para fazer UpperCaseMixin trabalhar com eles.

Por exemplo, minha primeira versão de UpperCaseMixin não incluia o método get . Aquela versão funcionava com
UserDict , mas não com Counter . A classe UserDict herda get de collections.abc.Mapping , e aquele get
chama __getitem__ , que implementei. Mas as chaves não eram transformadas em maiúsculas quando uma
UpperCounter era carregada no __init__ . Isso acontecia porque Counter.__init__ usa Counter.update , que por
sua vez recorre ao método get herdado de dict . Entretanto, o método get na classe dict não chama
__getitem__ . Esse é o núcleo do problema discutido na seção Seção 3.5.3. É também uma dura advertência sobre a
natureza frágil e intrincada de programas que se apoiam na herança, mesmo nessa pequena escala.

A próxima seção apresenta vários exemplos de herança múltipla, muitas vezes usando classes mixin.

14.6. Herança múltipla no mundo real


No livro Design Patterns ("Padrões de Projetos"),[171] quase todo o código está em C++, mas o único exemplo de herança
múltipla é o padrão Adapter ("Adaptador"). Em Python a herança múltipla também não é regra, mas há exemplos
importantes, que comentarei nessa seção.

14.6.1. ABCs também são mixins


Na biblioteca padrão do Python, o uso mais visível de herança múltipla é o pacote collections.abc . Nenhuma
controvérsia aqui: afinal, até o Java suporta herança múltipla de interfaces, e ABCs são declarações de interface que
podem, opcionalmente, fornecer implementações concretas de métodos.[172]

A documentação oficial do Python para collections.abc (https://docs.python.org/pt-br/3/library/collections.abc.html) (EN)


usa o termo mixin method ("método mixin") para os métodos concretos implementados em muitas das coleções nas
ABCs. As ABCs que oferecem métodos mixin cumprem dois papéis: elas são definições de interfaces e também classes
mixin. Por exemplo, a implementação de collections.UserDict (https://fpy.li/14-14) (EN) recorre a vários dos métodos
mixim fornecidos por collections.abc.MutableMapping .

14.6.2. ThreadingMixIn e ForkingMixIn


O pacote http.server (https://docs.python.org/pt-br/3/library/http.server.html) inclui as classes HTTPServer e
ThreadingHTTPServer . Essa última foi adicionada ao Python 3.7. Sua documentação diz:

classe http.server.ThreadingHTTPServer (server_address, RequestHandlerClass)


Essa classe é idêntica a HTTPServer , mas trata requisições com threads, usando a ThreadingMixIn . Isso é útil para
lidar com navegadores web que abrem sockets prematuramente, situação na qual o HTTPServer esperaria
indefinidamente.

Este é o código-fonte completo (https://fpy.li/14-16) da classe ThreadingHTTPServer no Python 3.10:

PYTHON
class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True

O código-fonte (https://fpy.li/14-17) de socketserver.ThreadingMixIn tem 38 linhas, incluindo os comentários e as


docstrings. O Exemplo 30 apresenta um resumo de sua implementação.

Exemplo 30. Parte de Lib/socketserver.py no Python 3.10

PYTHON3
class ThreadingMixIn:
"""Mixin class to handle each request in a new thread."""

# 8 lines omitted in book listing

def process_request_thread(self, request, client_address): # (1)


... # 6 lines omitted in book listing

def process_request(self, request, client_address): # (2)


... # 8 lines omitted in book listing

def server_close(self): # (3)


super().server_close()
self._threads.join()

1. process_request_thread não chama super() porque é um método novo, não uma sobreposição. Sua
implementação chama três métodos de instância que HTTPServer oferece ou herda.
2. Isso sobrepõe o método process_request , que HTTPServer herda de socketserver.BaseServer , iniciando
uma thread e delegando o trabalho efetivo para a process_request_thread que roda naquela thread. O método
não chama super() .
3. server_close chama super().server_close() para parar de receber requisições, e então espera que as
threads iniciadas por process_request terminem sua execução.
A ThreadingMixIn aparece junto com ForkingMixIn na documentação do módulo socketserver
(https://docs.python.org/pt-br/3/library/socketserver.html#socketserver.ForkingMixIn). Essa última classe foi projetada para
suportar servidores concorrentes baseados na os.fork() (https://docs.python.org/pt-br/3/library/os.html#os.fork), uma API
para iniciar processos filhos, disponível em sistemas Unix (ou similares) compatíveis com a POSIX
(https://pt.wikipedia.org/wiki/POSIX).

14.6.3. Mixins de views genéricas no Django


Não é necessário entender de Django para acompanhar essa seção. Uso uma pequena parte do

✒️ NOTA framework como um exemplo prático de herança múltipla, e tentarei fornecer todo o pano de fundo
necessário (supondo que você tenha alguma experiência com desenvolvimento web no lado
servidor, com qualquer linguagem ou framework).

No Django, uma view é um objeto invocável que recebe um argumento request —um objeto representando uma
requisição HTTP—e devolve um objeto representando uma resposta HTTP. Nosso interesse aqui são as diferentes
respostas. Elas podem ser tão simples quanto um redirecionamento, sem nenhum conteúdo em seu corpo, ou tão
complexas quando uma página de catálogo de uma loja online, renderizada a partir de uma template HTML e listando
múltiplas mercadorias, com botões de compra e links para páginas com detalhes.

Originalmente, o Django oferecia uma série de funções, chamadas views genéricas, que implementavam alguns casos
de uso comuns. Por exemplo, muitos sites precisam exibir resultados de busca que incluem dados de inúmeros itens,
com listagens ocupando múltiplas páginas, cada resultado contendo também um link para uma página de informações
detalhadas sobre aquele item. No Django, uma view de lista e uma view de detalhes são feitas para funcionarem juntas,
resolvendo esse problema: uma view de lista renderiza resultados de busca , e uma view de detalhes produz uma
página para cada item individual.

Entretanto, as views genéricas originais eram funções, então não eram extensíveis. Se quiséssemos algo algo similar
mas não exatamente igual a uma view de lista genérica, era preciso começar do zero.

O conceito de views baseadas em classes foi introduzido no Django 1.3, juntamente com um conjunto de classes de
views genéricas divididas em classes base, mixins e classes concretas prontas para o uso. No Django 3.2, as classes base
e as mixins estão no módulo base do pacote django.views.generic , ilustrado na Figura 11. No topo do diagrama
vemos duas classes que se encarregam de responsabilidades muito diferentes: View e TemplateResponseMixin .
Figura 11. Diagrama de classes UML do módulo django.views.generic.base .

Um recurso fantástico para estudar essas classes é o site Classy Class-Based Views (https://fpy.li/14-21)
👉 DICA (EN), onde se pode navegar por elas facilmente, ver todos os métodos em cada classe (métodos
herdados, sobrepostos e adicionados), os diagramas de classes, consultar sua documentação e
estudar seu código-fonte no GitHub (https://fpy.li/14-22).

View é a classe base de todas as views (ela poderia ser uma ABC), e oferece funcionalidade essencial como o método
dispatch , que delega para métodos de "tratamento" como get , head , post , etc., implementados por subclasses
concretas para tratar os diversos verbos HTTP.[173] A classe RedirectView herda apenas de View , e podemos ver que
ela implementa get , head , post , etc.

Se é esperado que as subclasses concretas de View implementem os métodos de tratamento, por que aqueles métodos
não são parte da interface de View ? A razão: subclasses são livres para implementar apenas os métodos de tratamento
que querem suportar. Uma TemplateView é usada apenas para exibir conteúdo, então ela implementa apenas get . Se
uma requisição HTTP POST é enviada para uma TemplateView , o método herdado View.dispatch verifica que não
há um método de tratamento para post , e produz uma resposta HTTP 405 Method Not Allowed .[174]
A TemplateResponseMixin fornece funcionalidade que interessa apenas a views que precisam usar uma template.
Uma RedirectView , por exemplo, não tem qualquer conteúdo em seu corpo, então não precisa de uma template e não
herda dessa mixin. TemplateResponseMixin fornece comportamentos para TemplateView e outras views que
renderizam templates, tal como ListView , DetailView , etc., definidas nos sub-pacotes de django.views.generic .
A Figura 12 mostra o módulo django.views.generic.list e parte do módulo base .

Para usuários do Django, a classe mais importante na Figura 12 é ListView , uma classe agregada sem qualquer código
(seu corpo é apenas uma docstring). Quando instanciada, uma ListView tem um atributo de instância object_list ,
através do qual a template pode interagir para mostrar o conteúdo da página, normalmente o resultado de uma
consulta a um banco de dados, composto de múltiplos objetos. Toda a funcionalidade relacionada com a geração desse
iterável de objetos vem da MultipleObjectMixin . Essa mixin também oferece uma lógica complexa de paginação—
para exibir parte dos resultados em uma página e links para mais páginas.

Suponha que você queira criar uma view que não irá renderizar uma template, mas sim produzir uma lista de objetos
em formato JSON. Para isso existe BaseListView . Ela oferece um ponto inicial de extensão fácil de usar, unindo a
funcionalidade de View e de MultipleObjectMixin , mas sem a sobrecarga do mecanismo de templates.

A API de views baseadas em classes do Django é um exemplo melhor de herança múltipla que o Tkinter. Em especial, é
fácil entender suas classes mixin: cada uma tem um propósito bem definido, e todos os seus nomes contêm o sufixo …
Mixin .

Figura 12. Diagrama de classe UML para o módulo django.views.generic.list . Aqui as três classes do módulo base
aparecem recolhidas (veja a Figura 11). A classe ListView não tem métodos ou atributos: é uma classe agregada.
Views baseadas em classes não são universalmente aceitas por usuários do Django. Muitos as usam de forma limitada,
como caixas opacas. Mas quando é necessário criar algo novo, muitos programadores Django continuam criando
funções monolíticas de views, para abarcar todas aquelas responsabilidades, ao invés de tentar reutilizar as views base
e as mixins.

Demora um certo tempo para aprender a usar as views baseadas em classes e a forma de estendê-las para suprir
necessidades específicas de uma aplicação, mas considero que vale a pena estudá-las. Elas eliminam muito código
repetitivo, tornam mais fácil reutilizar soluções, e melhoram até a comunicação das equipes—por exemplo, pela
definição de nomes padronizados para as templates e para as variáveis passadas para contextos de templates. Views
baseadas em classes são views do Django "on rails"[175].

14.6.4. Herança múltipla no Tkinter


Um exemplo extremo de herança múltipla na biblioteca padrão do Python é o toolkit de interface gráfica Tkinter
(https://docs.python.org/pt-br/3/library/tkinter.html). Usei parte da hierarquia de componentes do Tkinter para ilustrar a ORM
na Figura 10. A Figura 13 mostra todos as classes de componentes no pacote base tkinter (há mais componentes
gráficos no subpacote tkinter.ttk (https://docs.python.org/pt-br/3/library/tkinter.ttk.html)).

Figura 13. Diagrama de classes resumido da hierarquia de classes de interface gráfica do Tkinter; classes etiquetadas
com «mixin» são projetadas para oferecer metodos concretos a outras classes, através de herança múltipla.

No momento em que escrevo essa seção, o Tkinter já tem 25 anos de idade. Ele não é um exemplo das melhores
práticas atuais. Mas mostra como a herança múltipla era usada quando os programadores ainda não conheciam suas
desvantagens. E vai nos servir de contra-exemplo, quando tratarmos de algumas boas práticas, na próxima seção.

Considere as seguintes classes na Figura 13:

➊ Toplevel : A classe de uma janela principal em um aplicação Tkinter.


➋ Widget : A superclasse de todos os objetos visíveis que podem ser colocados em uma janela.

➌ Button : Um componente de botão simples.

➍ Entry : Um campo de texto editável de uma única linha.

➎ Text : Um campo de texto editável de múltiplas linhas.

Aqui estão as ORMs dessas classes, como exibidas pela função print_mro do Exemplo 27:

PYCON
>>> import tkinter
>>> print_mro(tkinter.Toplevel)
Toplevel, BaseWidget, Misc, Wm, object
>>> print_mro(tkinter.Widget)
Widget, BaseWidget, Misc, Pack, Place, Grid, object
>>> print_mro(tkinter.Button)
Button, Widget, BaseWidget, Misc, Pack, Place, Grid, object
>>> print_mro(tkinter.Entry)
Entry, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, object
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

Pelos padrões atuais, a hierarquia de classes do Tkinter é muito profunda. Poucas partes da
bilbioteca padrão do Python tem mais que três ou quatro níveis de classes concretas, e o mesmo
pode ser dito da biblioteca de classes do Java. Entretanto, é interessante observar que algumas das

✒️ NOTA hierarquias mais profundas da biblioteca de classes do Java são precisamente os pacotes
relacionados à programação de interfaces gráficas: java.awt (https://fpy.li/14-26) e javax.swing
(https://fpy.li/14-27). O Squeak (https://fpy.li/14-28), uma versão moderna e aberta do Smalltalk, inclui o
poderoso e inovador toolkit de interface gráfica Morphic, também com uma hierarquia de classes
profunda. Na minha experiência, é nos toolkits de interface gráfica que a herança é mais útil.

Observe como essas classes se relacionam com outras:

Toplevel é a única classe gráfica que não herda de Widget , porque ela é a janela primária e não se comporta
como um componente; por exemplo, ela não pode ser anexada a uma janela ou moldura (frame). Toplevel herda
de Wm , que fornece funções de acesso direto ao gerenciador de janelas do ambiente, para tarefas como definir o
título da janela e configurar suas bordas.
Widget herda diretamente de BaseWidget e de Pack , Place , e Grid . As últimas três classes são gerenciadores
de geometria: são responsáveis por organizar componentes dentro de uma janela ou moldura. Cada uma delas
encapsula uma estratégia de layout e uma API de colocação de componentes diferente.
Button , como a maioria dos componentes, descende diretamente apenas de Widget , mas indiretamente de Misc ,
que fornece dezenas de métodos para todos os componentes.
Entry é subclasse de Widget e XView , que suporta rolagem horizontal.

Text é subclasse de Widget , XView e YView (para rolagem vertical).

Vamos agora discutir algumas boas práticas de herança múltipla e examinar se o Tkinter as segue.

14.7. Lidando com a herança


Aquilo que Alan Kay escreveu na epígrafe continua sendo verdade: ainda não existe um teoria geral sobre herança que
possa guiar os programadores. O que temos são regras gerais, padrões de projetos, "melhores práticas", acrônimos
perspicazes, tabus, etc. Alguns desses nos dão orientações úteis, mas nenhum deles é universalmente aceito ou sempre
aplicável.
É fácil criar designs frágeis e incompreensíveis usando herança, mesmo sem herança múltipla. Como não temos uma
teoria abrangente, aqui estão algumas dicas para evitar grafos de classes parecidos com espaguete.

14.7.1. Prefira a composição de objetos à herança de classes


O título dessa subseção é o segundo princípio do design orientado a objetos, do livro Padrões de Projetos,[176] e é o
melhor conselho que posso oferecer aqui. Uma vez que você se sinta confortável com a herança, é fácil usá-la em
excesso. Colocar objetos em uma hierarquia elegante apela para nosso senso de ordem; programadores fazem isso por
pura diversão.

Preferir a composição leva a designs mais flexíveis. Por exemplo, no caso da classe tkinter.Widget , em vez de herdar
os métodos de todos os gerenciadores de geometria, instâncias do componente poderiam manter uma referência para
um gerenciador de geometria, e invocar seus métodos. Afinal, um Widget não deveria "ser" um gerenciador de
geometria, mas poderia usar os serviços de um deles por delegação. E daí você poderia adicionar um novo gerenciador
de geometria sem afetar a hierarquia de classes do componente e sem se preocupar com colisões de nomes. Mesmo
com herança simples, este princípio aumenta a flexibilidade, porque a subclasses são uma forma de acoplamento forte,
e árvores de herança muito altas tendem a ser frágeis.

A composição e a delegação podem substituir o uso de mixins para tornar comportamentos disponíveis para diferentes
classes, mas não podem substituir o uso de herança de interfaces para definir uma hierarquia de tipos.

14.7.2. Em cada caso, entenda o motivo do uso da herança


Ao lidarmos com herança múltipla, é útil ter claras as razões pelas quais subclasses são criadas em cada caso específico.
As principais razões são:

Herança de interface cria um subtipo, implicando em uma relação "é-um". A melhor forma de fazer isso é usando
ABCs.
Herança de implementação evita duplicação de código pela reutilização. Mixins podem ajudar nisso.

Na prática, frequentemente ambos os usos são simultâneos, mas sempre que você puder tornar a intenção clara, vá em
frente. Herança para reutilização de código é um detalhe de implementação, e muitas vezes pode ser substituída por
composição e delegação. Por outro lado, herança de interfaces é o fundamento de qualquer framework. Se possível, a
herança de interfaces deveria usar apenas ABCs como classes base.

14.7.3. Torne a interface explícita com ABCs


No Python moderno, se uma classe tem por objetivo definir uma interface, ela deveria ser explicitamente uma ABC ou
uma subclasse de typing.Protocol . Uma ABC deveria ser subclasse apenas de abc.ABC ou de outras ABCs. A
herança múltipla de ABCs não é problemática.

14.7.4. Use mixins explícitas para reutilizar código


Se uma classe é projetada para fornecer implementações de métodos para reutilização por múltiplas subclasses não
relacionadas, sem implicar em uma relação do tipo "é-uma", ele deveria ser uma classe mixin explícita.
Conceitualmente, uma mixin não define um novo tipo; ela simplesmente empacota métodos para reutilização. Uma
mixin não deveria nunca ser instanciada, e classes concretas não devem herdar apenas de uma mixin. Cada mixin
deveria fornecer um único comportamento específico, implementando poucos métodos intimamente relacionados.
Mixins devem evitar manter qualquer estado interno; isto é, uma classe mixin não deve ter atributos de instância.

No Python, não há uma maneira formal de declarar uma classe como mixin. Assim, é fortemente recomendado que
seus nomes incluam o sufixo Mixin .

14.7.5. Ofereça classes agregadas aos usuários


“ Uma classe construída principalmente herdando de mixins,
comportamento próprios, é chamada de classe agregada. [177]
sem adicionar estrutura ou

— Grady Booch et al.


Object-Oriented Analysis and Design with Applications

Se alguma combinação de ABCs ou mixins for especialmente útil para o código cliente, ofereça uma classe que una
essas funcionalidades de uma forma sensata.

Por exemplo, aqui está o código-fonte (https://fpy.li/14-29) completo da classe ListView do Django, do canto inferior
direito da Figura 12:

PYTHON3
class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
"""
Render some list of objects, set by `self.model` or `self.queryset`.
`self.queryset` can actually be any iterable of items, not just a queryset.
"""

O corpo de ListView é vazio[178], mas a classe fornece um serviço útil: ela une uma mixin e uma classe base que
devem ser usadas em conjunto.

Outro exemplo é tkinter.Widget (https://fpy.li/14-30), que tem quatro classes base e nenhum método ou atributo
próprios—apenas uma docstring. Graças à classe agregada Widget , podemos criar um novo componente com as
mixins necessárias, sem precisar descobrir em que ordem elas devem ser declaradas para funcionarem como desejado.

Observe que classes agregadas não precisam ser inteiramente vazias (mas frequentemente são).

14.7.6. Só crie subclasses de classes criadas para serem herdadas


Em um comentário sobre esse capítulo, o revisor técnico Leonardo Rochael sugeriu o alerta abaixo.

Criar subclasses e sobrepor métodos de qualquer classe complexa é um processo muito suscetível a
erros, porque os métodos da superclasse podem ignorar as sobreposições da subclasse de formas
⚠️ AVISO inesperadas. Sempre que possível, evite sobrepor métodos, ou pelo menos se limite a criar
subclasses de classes projetadas para serem facilmente estendidas, e apenas daquelas formas pelas
quais a classe foi desenhada para ser estendida.

É um ótimo conselho, mas como descobrimos se uma classe foi projetada para ser estendida?

A primeira resposta é a documentação (algumas vezes na forma de docstrings ou até de comentários no código). Por
exemplo, o pacote socketserver (https://docs.python.org/pt-br/3/library/socketserver.html) (EN) do Python é descrito como
"um framework para servidores de rede". Sua classe BaseServer
(https://docs.python.org/pt-br/3/library/socketserver.html#socketserver.BaseServer) (EN) foi projetada para a criação de
subclasses, como o próprio nome sugere. E mais importante, a documentação e a docstring (https://fpy.li/14-33) (EN) no
código-fonte da classe informa explicitamente quais de seus métodos foram criados para serem sobrepostos por
subclasses.

No Python ≥ 3.8 uma nova forma de tornar tais restrições de projeto explícitas foi oferecida pela PEP 591—Adding a
final qualifier to typing (Acrescentando um qualificador "final" à tipagem) (https://fpy.li/pep591) (EN). A PEP introduz um
decorador @final (https://docs.python.org/pt-br/3/library/typing.html#typing.final), que pode ser aplicado a classes ou a
métodos individuais, de forma que IDEs ou verificadores de tipo podem identificar tentativas equivocadas de criar
subclasses daquelas classes ou de sobrepor aqueles métodos.[179]
14.7.7. Evite criar subclasses de classes concretas
Criar subclasses de classes concretas é mais perigoso que criar subclasses de ABCs e mixins, pois instâncias de classes
concretas normalmente tem um estado interno, que pode ser facilmente corrompido se sobrepusermos métodos que
dependem daquele estado. Mesmo se nossos métodos cooperarem chamando super() , e o estado interno seja
mantido através da sintaxe __x , restarão ainda inúmeras formas pelas quais a sobreposição de um método pode
introduzir bugs.

No Pássaros aquáticos e as ABCs, Alex Martelli cita More Effective C++, de Scott Meyer, que diz: "toda classe não-final
(não-folha) deveria ser abstrata". Em outras palavras, Meyer recomenda que subclasses deveriam ser criadas apenas a
partir de classes abstratas.

Se você precisar usar subclasses para reutilização de código, então o código a ser reutilizado deve estar em métodos
mixin de ABCs, ou em classes mixin explicitamente nomeadas.

Vamos agora analisar o Tkinter do ponto de vista dessas recomendações

14.7.8. Tkinter: O bom, o mau e o feio


A[180] maioria dos conselhos da seção anterior não são seguidos pelo Tkinter, com a notável excessão de "Seção 14.7.5".
E mesmo assim, esse não é um grande exemplo, pois a composição provavelmente funcionaria melhor para integrar os
gerenciadores de geometria a Widget , como discutido na seção Seção 14.7.1.

Mas lembre-se que o Tkinter é parte da biblioteca padrão desde o Python 1.1, lançado em 1994. O Tkinter é uma
camada sobreposta ao excelente toolkit Tk GUI, da linguagem Tcl. O combo Tcl/Tk não é, na origem, orientado a objetos,
então a API Tk é basicamente um imenso catálogo de funções. Entretanto, o toolkit é orientado a objetos por projeto,
apesar de não o ser em sua implementação Tcl original.

A docstring de tkinter.Widget começa com as palavras "Internal class" (Classe interna). Isso sugere que Widget
deveria provavelmente ser uma ABC. Apesar da classe Widget não ter métodos próprios, ela define uma interface. Sua
mensagem é: "Você pode contar que todos os componentes do Tkinter vão oferecer os métodos básicos de componente
( __init__ , destroy , e dezenas de funções da API Tk), além dos métodos de todos os três gerenciadores de
geometria". Vamos combinar que essa não é uma boa definição de interface (é abrangente demais), mas ainda assim é
uma interface, e Widget a "define" como a união das interfaces de suas superclasses.

A classe Tk , qie encapsula a lógica da aplicação gráfica, herda de Wm e Misc , nenhuma das quais é abstrata ou mixin
( Wm não é uma mixin adequada, porque TopLevel é subclasse apenas dela). O nome da classe Misc é—sozinho—é
um forte code smell. Misc tem mais de 100 métodos, e todos os componentes herdam dela. Por que é necessário que
cada um dos componentes tenham métodos para tratamento do clipboard, seleção de texto, gerenciamento de timer e
coisas assim? Não é possível colar algo em um botão ou selecionar texto de uma barra de rolagem. Misc deveria ser
dividida em várias classes mixin especializadas, e nem todos os componentes deveriam herdar de todas aquelas
mixins.

Para ser justo, como usuário do Tkinter você não precisa, de forma alguma, entender ou usar herança múltipla. Ela é
um detalhe de implementação, oculto atrás das classes de componentes que serão instanciadas ou usadas como base
para subclasses em seu código. Mas você sofrerá as consequências da herança múltipla excessiva quando digitar
dir(tkinter.Button) e tentar encontrar um método específico em meio aos 214 atributos listados. E terá que
enfrentar a complexidade, caso decida implementar um novo componente Tk.
Apesar de ter problemas, o Tkinter é estável, flexível, e fornece um look-and-feel moderno se você
usar o pacote tkinter.ttk e seus componentes tematizados. Além disso, alguns dos componentes
👉 DICA originais, como Canvas e Text , são inacreditavelmente poderosos. Em poucas horas é possível
transformar um objeto Canvas em uma aplicação de desenho simples mas completa. Se você está
interessada em programação de interfaces gráficas, com certeza vale a pena considerar o Tkinter e o
Tcl/Tk.

Aqui termina nossa viagem através do labirinto da herança.

14.8. Resumo do capítulo


Esse capítulo começou com uma revisão da função super() no contexto de herança simples. Daí discutimos o
problema da criação de subclasses de tipos embutidos: seus métodos nativos, implementados em C, não invocam os
métodos sobrepostos em subclasses, exceto em uns poucos casos especiais. É por isso que, quando precisamos de tipos
list , dict , ou str personalizados, é mais fácil criar subclasses de UserList , UserDict , ou UserString —todos
definidos no módulo collections (https://docs.python.org/pt-br/3/library/collections.html)—, que na verdade encapsulam os
tipos embutidos correspondentes e delegam operações para aqueles—três exemplos a favor da composição sobre a
herança na biblioteca padrão. Se o comportamento desejado for muito diferente daquilo que os tipos embutidos
oferecem, pode ser mais fácil criar uma subclasse da ABC apropriada em collections.abc
(https://docs.python.org/pt-br/3/library/collections.abc.html), e escrever sua própria implementação.

O restante do capítulo foi dedicado à faca de dois gumes da herança múltipla. Primeiro vimos como a ordem de
resolução de métodos, definida no atributo de classe __mro__ , trata o problema de conflitos potenciais de nomes em
métodos herdados. Também examinamos como a função embutida super() se comporta em hierarquias com herança
múltipla, e como ela algumas vezes se comporta de forma inesperada. O comportamento de super() foi projetado
para suportar classes mixin, que estudamos usando o exemplo simples de UpperCaseMixin (para mapeamentos
indiferentes a maiúsculas/minúsculas).

Exploramos como a herança múltipla e os métodos mixin são usados nas ABCs do Python, bem como nos mixins de
threading e forking de socketserver . Usos mais complexos de herança múltipla foram exemplificados com as views
baseadas em classes do Django e com o toolkit de interface gráfica Tkinter. Apesar do Tkinter não ser um exemplo das
melhores práticas modernas, é um exemplo de hierarquias de classe complexas que podemos encontrar em sistemas
legados.

Encerrando o capítulo, apresentamos sete recomendações para lidar com herança, e aplicamos alguns daqueles
conselhos em um comentário sobre a hierarquia de classes do Tkinter.

Rejeitar a herança—mesmo a herança simples—é uma tendência moderna. Go é uma das mais bem sucedidas
linguagens criadas no século 21. Ela não inclui um elemento chamado "classe", mas você pode construir tipos que são
estruturas (structs) de campos encapsulados, e anexar métodos a essas estruturas. Em Go é possível definir interfaces,
que são verificadas pelo compilador usando tipagem estrutural, também conhecida como duck typing estática—algo
muito similar ao que temos com os tipos protocolo desde o Python 3.8. Essa linguagem também tem uma sintaxe
especial para a criação de tipos e interfaces por composição, mas não há suporte a herança—nem entre interfaces.

Então talvez o melhor conselho sobre herança seja: evite-a se puder. Mas, frequentemente, não temos essa opção: as
frameworks que usamos nos impõe suas escolhas de design.
14.9. Leitura complementar
“ NoComoqueé muito
diz respeito à legibilidade, composição feita de forma adequada é superior a herança.
mais frequente ler o código que escrevê-lo, como regra geral evite subclasses, mas
em especial não misture os vários tipos de herança e não crie subclasses para compartilhar
código.
— Hynek Schlawack
Subclassing in Python Redux

Durante a revisão final desse livro, o revisor técnico Jürgen Gmach recomendou o post "Subclassing in Python Redux"
(O ressurgimento das subclasses em Python) (https://fpy.li/14-37), de Hynek Schlawack—a fonte da citação acima.
Schlawack é o autor do popular pacote attrs, e foi um dos principais contribuidores do framework de programação
assíncrona Twisted, um projeto criado por Glyph Lefkowitz em 2002. De acordo com Schlawack, após algum tempo os
desenvolvedores perceberam que tinham usado subclasses em excesso no projeto. O post é longo, e cita outros posts e
palestras importantes. Muito recomendado.

Naquela mesma conclusão, Hynek Schlawack escreve: "Não esqueça que, na maioria dos casos, tudo o que você precisa
é de uma função." Concordo, e é precisamente por essa razão que Python Fluente trata em detalhes das funções, antes
de falar de classes e herança. Meu objetivo foi mostrar o quanto você pode alcançar com funções se valendo das classes
na biblioteca padrão, antes de criar suas próprias classes.

A criação de subclasses de tipos embutidos, a função super , e recursos avançados como descritores e metaclasses,
foram todos introduzidos no artigo "Unifying types and classes in Python 2.2" (Unificando tipos e classes em Python 2.2)
(https://fpy.li/descr101) (EN), de Guido van Rossum. Desde então, nada realmente importante mudou nesses recursos. O
Python 2.2 foi uma proeza fantástica de evolução da linguagem, adicionando vários novos recursos poderosos em um
todo coerente, sem quebrar a compatibilidade com versões anteriores. Os novo recursos eram 100% opcionais. Para
usá-los, bastava programar explicitamente uma subclasse de object —direta ou indiretamente—, para criar uma
assim chamada "classe no novo estilo". No Python 3, todas as classes são subclasses de object .

O Python Cookbook, 3ª ed. (https://fpy.li/pycook3), de David Beazley e Brian K. Jones (O’Reilly) inclui várias receitas
mostrando o uso de super() e de classes mixin. Você pode começar pela esclarecedora seção "8.7. Calling a Method on
a Parent Class" (Invocando um Método em uma Superclasse) (https://fpy.li/14-38), e seguir as referências internas a partir
dali.

O post "Python’s super() considered super!" (O super() do Python é mesmo super!) (https://fpy.li/14-39) (EN), de Raymond
Hettinger, explica o funcionamento de super e a herança múltipla de uma perspectiva positiva. Ele foi escrito em
resposta a "Python’s Super is nifty, but you can’t use it (Previously: Python’s Super Considered Harmful)" O Super do
Python é bacana, mas você não deve usá-lo (Antes: Super do Python Considerado Nocivo) (https://fpy.li/14-40) (EN), de James
Knight. A resposta de Martijn Pieters a "How to use super() with one argument?" (Como usar super() com um só
argumento?) (https://fpy.li/14-41) (EN) inclui uma explicação concisa e aprofundada de super , incluindo sua relação com
descritores, um conceito que estudaremos apenas no [attribute_descriptors]. Essa é a natureza de super . Ele é simples
de usar em casos de uso básicos, mas é uma ferramenta poderosa e complexa, que alcança alguns dos recursos
dinâmicos mais avançados do Python, raramente encontrados em outras linguagens.

Apesar dos títulos daqueles posts, o problema não é exatamente com a função embutida super —que no Python 3 não
é tão feia quanto era no Python 2. A questão real é a herança múltipla, algo inerentemente complicado e traiçoeiro.
Michele Simionato vai além da crítica, e de fato oferece uma solução em seu "Setting Multiple Inheritance Straight"
(Colocando a Herança Múltipla em seu Lugar) (https://fpy.li/14-42) (EN): ele implementa traits ("traços"), uma forma
explícita de mixin originada na linguagem Self. Simionato escreveu, em seu blog, uma longa série de posts sobre
herança múltipla em Python, incluindo "The wonders of cooperative inheritance, or using super in Python 3" (As
maravilhas da herança cooperativa, ou usando super em Python 3) (https://fpy.li/14-43) (EN); "Mixins considered harmful,"
part 1 (Mixins consideradas nocivas) (https://fpy.li/14-44) (EN) e part 2 (https://fpy.li/14-45) (EN); e "Things to Know About
Python Super," part 1 (O que você precisa saber sobre o super do Python) (https://fpy.li/14-46) (EN), part 2 (https://fpy.li/14-47)
(EN), e part 3 (https://fpy.li/14-48) (EN). Os posts mais antigos usam a sintaxe de super do Python 2, mas ainda são
relevantes.
Eu li a primeira edição do Object-Oriented Analysis and Design, 3ª ed., de Grady Booch et al., e o recomendo fortemente
como uma introdução geral ao pensamento orientado a objetos, independente da linguagem de programação. É um dos
raros livros que trata da herança múltipla sem ideias pré-concebidas.

Hoje, mais que nunca, é de bom tom evitar a herança, então cá estão duas referências sobre como fazer isso. Brandon
Rhodes escreveu "The Composition Over Inheritance Principle" (O Princípio da Composição Antes da Herança)
(https://fpy.li/14-49) (EN), parte de seu excelente guia Python Design Patterns (Padrões de Projetos no Python)
(https://fpy.li/14-50). Augie Fackler e Nathaniel Manista apresentaram "The End Of Object Inheritance & The Beginning Of
A New Modularity" (O Fim da Herança de Objetos & O Início de Uma Nova Modularidade) (https://fpy.li/14-51) na PyCon
2013. Fackler e Manista falam sobre organizar sistemas em torno de interfaces e das funções que lidam com os objetos
que implementam aquelas interfaces, evitando o acoplamento estreito e os pontos de falha de classes e da herança. Isso
me lembra muito a maneira de pensar do Go, mas aqui os autores a defendem para o Python.

Soapbox
Pense nas classes realmente necessárias

“ [Nós] começamos a defender a ideia de herança como uma maneira de permitir que
iniciantes pudessem construir [algo] a partir de frameworks que só poderiam ser
projetadas por especialistas[181]. (Agradeço a meu amigo Cristiano Anderson, que
compartilhou essa referência quando eu estava escrevendo esse capítulo).
— Alan Kay
The Early History of Smalltalk ("Os Primórdios do Smalltalk")

A imensa maioria dos programadores escreve aplicações, não frameworks. Mesmo aqueles que escrevem
frameworks provavelmente passam muito (ou a maior parte) de seu tempo escrevendo aplicações. Quando
escrevemos aplicações, normalmente não precisamos criar hierarquias de classes. No máximo escrevemos
classes que são subclasses de ABCs ou de outras classes oferecidas pelo framework. Como desenvolvedores de
aplicações, é muito raro precisarmos escrever uma classe que funcionará como superclasse de outra. As classes
que escrevemos são, quase sempre, "classes folha" (isto é, folhas na árvore de herança).

Se, trabalhando como desenvolvedor de aplicações, você se pegar criando hierarquias de classe de múltiplos
níveis, quase certamente uma ou mais das seguintes alternativas se aplica:

Você está reinventando a roda. Procure um framework ou biblioteca que forneça componentes que possam
ser reutilizados em sua aplicação.
Você está usando um framework mal projetada. Procure uma alternativa.
Você está complicando demais o processo. Lembre-se do Princípio KISS.
Você ficou entediado programando aplicações e decidiu criar um novo framework. Parabéns e boa sorte!

Também é possível que todas as alternativas acima se apliquem à sua situação: você ficou entediado e decidiu
reinventar a roda, escrevendo seu próprio framework mal projetado e excessivamente complexo, e está sendo
forçado a programar classe após classe para resolver problemas triviais. Espero que você esteja se divertindo, ou
pelo menos que esteja sendo pago para fazer isso.
Tipos embutidos mal-comportados: bug ou feature?

Os tipos embutidos dict , list , e str são blocos básicos essenciais do próprio Python, então precisam ser
rápidos—qualquer problema de desempenho ali teria severos impactos em praticamente todo o resto. É por isso
que o CPython adotou atalhos que fazem com que métodos embutidos se comportem mal, ao não cooperarem
com os métodos sobrepostos por subclasses. Um caminho possível para sair desse dilema seria oferecer duas
implementações para cada um desses tipos: um "interno", otimizado para uso pelo interpretador, e um externo,
facilmente extensível.

Mas isso nós já temos: UserDict , UserList , e UserString não são tão rápidos quanto seus equivalentes
embutidos, mas são fáceis de estender. A abordagem pragmática tomada pelo CPython significa que nós também
podemos usar, em nossas próprias aplicações, as implementações altamente otimizadas mas difíceis estender. E
isso faz sentido, considerando que não é tão frequente precisarmos de um mapeamento, uma lista ou uma string
customizados, mas usamos dict , list , e str diariamente. Só precisamos estar cientes dos compromissos
envolvidos.

Herança através das linguagens

Alan Kay criou o termo "orientado a objetos", e o Smalltalk tinha apenas herança simples, apesar de existirem
versões com diferentes formas de suporte a herança múltipla, incluindo os dialetos modernos de Smalltalk,
Squeak e Pharo, que suportam traits ("traços")--um dispositivo de linguagem que faz o papel de classes mixin, ao
mesmo tempo em que evita alguns dos problemas da herança múltipla.

A primeira linguagem popular a implementar herança múltipla foi o C++, e esse recurso foi abusado o suficiente
para que o Java—criado para ser um substituto do C++—fosse projetado sem suporte a herança múltipla de
implementação (isto é, sem classes mixin).Quer dizer, isso até o Java 8 introduzir os métodos default, que tornam
interfaces muito similares às classes abstratas usadas para definir interfaces em C++ e em Python. Depois do Java,
a linguagem da JVM mais usada é provavelmente o Scala, que implementa traits.

Outras linguagens que suportam traits são a última versão estável do PHP e do Groovy, bem como o Rust e o Raku
—a linguagem antes conhecida como Perl 6.[182] Então é correto dizer que traits estão na moda em 2021.

O Ruby traz uma perspectiva original para a herança múltipla: não a suporta, mas introduz mixins como um
recurso da linguagem. Uma classe Ruby pode incluir um módulo em seu corpo, e aí os métodos definidos no
módulo se tornam parte da implementação da classe. Essa é uma forma "pura" de mixin, sem herança envolvida,
e está claro que uma mixin Ruby não tem qualquer influência sobre o tipo da classe onde ela é usada. Isso oferece
os benefícios das mixins, evitando muitos de seus problemas mais comuns.

Duas novas linguagens orientadas a objetos que estão recebendo muita atenção limitam severamente a herança:
Go e Julia. Ambas giram em torno de programar "objetos", e suportam polimorfismo
(https://pt.wikipedia.org/wiki/Polimorfismo_(ci%C3%AAncia_da_computa%C3%A7%C3%A3o)), mas evitam o termo "classe",

Go não tem qualquer tipo de herança. Julia tem uma hierarquia de tipos, mas subtipos não podem herdar
estrutura, apenas comportamentos, e só é permitido criar subtipos de tipos abstratos. Além disso, os métodos de
Julia são implementados com despacho múltiplo—uma forma mais avançada do mecanismo que vimos na seção
Seção 9.9.3.
15. Mais dicas de tipo
“ Aprendi uma dolorosa lição: para programas pequenos, a tipagem dinâmica é ótima. Para
programas grandes é necessária uma abordagem mais disciplinada. E ajuda se a linguagem der
a você aquela disciplina, ao invés de dizer "Bem, faça o que quiser".[183]
— Guido van Rossum
um fã do Monty Python

Esse capítulo é uma continuação do Capítulo 8, e fala mais sobre o sistema de tipagem gradual do Python. Os tópicos
principais são:

Assinaturas de funções sobrepostas


typing.TypedDict : dando dicas de tipos para dicts usados como registros
Coerção de tipo
Acesso a dicas de tipo durante a execução
Tipos genéricos
Declarando uma classe genérica
Variância: tipos invariantes, covariantes e contravariantes
Protocolos estáticos genéricos

15.1. Novidades nesse capítulo


Esse capítulo é inteiramente novo, escrito para essa segunda edição de Python Fluente. Vamos começar com
sobreposições.

15.2. Assinaturas sobrepostas


No Python, funções podem aceitar diferentes combinações de argumentos.

O decorador @typing.overload permite anotar tais combinações. Isso é particularmente importante quando o tipo
devolvido pela função depende do tipo de dois ou mais parâmetros.

Considere a função embutida sum . Esse é o texto de help(sum) .[184]:

>>> help(sum)
sum(iterable, /, start=0)
Devolve a soma de um valor 'start' (default: 0) mais a soma dos números de um iterável

Quando o iterável é vazio, devolve o valor inicial ('start').


Essa função é direcionada especificamente para uso com valores numéricos e pode rejeitar tipos não-
numéricos.

A função embutida sum é escrita em C, mas o typeshed tem dicas de tipos sobrepostas para ela, em builtins.pyi
(https://fpy.li/15-2):

PYTHON3
@overload
def sum(__iterable: Iterable[_T]) -> Union[_T, int]: ...
@overload
def sum(__iterable: Iterable[_T], start: _S) -> Union[_T, _S]: ...
Primeiro, vamos olhar a sintaxe geral das sobreposições. Esse acima é todo o código sobre sum que você encontrará no
arquivo stub (.pyi). A implementação estará em um arquivo diferente. As reticências ( …​) não tem qualquer função
além de cumprir a exigência sintática para um corpo de função, como no caso de pass . Assim os arquivos .pyi são
arquivos Python válidos.

Como mencionado na seção Seção 8.6, os dois sublinhados prefixando __iterable são a convenção da PEP 484 para
argumentos apenas posicionais, que é verificada pelo Mypy. Isso significa que você pode invocar sum(my_list) , mas
não sum(__iterable = my_list) .

O verificador de tipo tenta fazer a correspondência entre os argumentos dados com cada assinatura sobreposta, em
ordem. A chamada sum(range(100), 1000) não casa com a primeira sobreposição, pois aquela assinatura tem
apenas um parâmetro. Mas casa com a segunda.

Você pode também usar @overload em um modulo Python regular, colocando as assinaturas sobrepostas logo antes
da assinatura real da função e de sua implementação. O Exemplo 1 mostra como sum apareceria anotada e
implementada em um módulo Python.

Exemplo 1. mysum.py: definição da função sum com assinaturaas sobrepostas

PYTHON3
import functools
import operator
from collections.abc import Iterable
from typing import overload, Union, TypeVar

T = TypeVar('T')
S = TypeVar('S') # (1)

@overload
def sum(it: Iterable[T]) -> Union[T, int]: ... # (2)
@overload
def sum(it: Iterable[T], /, start: S) -> Union[T, S]: ... # (3)
def sum(it, /, start=0): # (4)
return functools.reduce(operator.add, it, start)

1. Precisamos deste segundo TypeVar na segunda assinatura.


2. Essa assinatura é para o caso simples: sum(my_iterable) . O tipo do resultado pode ser T —o tipo dos elementos
que my_iterable produz—ou pode ser int , se o iterável for vazio, pois o valor default do parâmetro start é 0 .
3. Quando start é dado, ele pode ser de qualquer tipo S , então o tipo do resultado é Union[T, S] . É por isso que
precisamos de S . Se T fosse reutilizado aqui, então o tipo de start teria que ser do mesmo tipo dos elementos de
Iterable[T] .

4. A assinatura da implementação efetiva da função não tem dicas de tipo.

São muitas linhas para anotar uma função de uma única linha. Sim, eu sei, provavelmente isso é excessivo. Mas pelo
menos a função do exemplo não é foo .

Se você quiser aprender sobre @overload lendo código, o typeshed tem centenas de exemplos. Quando escrevo esse
capítulo, o arquivo stub (https://fpy.li/15-3) do typeshed para as funções embutidas do Python tem 186 sobreposições—
mais que qualquer outro na biblioteca padrão.
Aproveite a tipagem gradual

👉 DICA Tentar produzir código 100% anotado pode levar a dicas de tipo que acrescentam muito ruído e
pouco valor agregado. Refatoração para simplificar as dicas de tipo pode levar a APIs pesadas.
Algumas vezes é melhor ser pragmático, e deixar parte do código sem dicas de tipo.

As APIs convenientes e práticas que consideramos pythônicas são muitas vezes difíceis de anotar. Na próxima seção
veremos um exemplo: são necessárias seis sobreposições para anotar adequadamente a flexível função embutida max .

15.2.1. Sobreposição máxima


É difícil acrescentar dicas de tipo a funções que usam os poderosos recursos dinâmicos do Python.

Quando estudava o typeshed, enconterei o relatório de bug #4051 (https://fpy.li/shed4051) (EN): Mypy não avisou que é
ilegal passar None como um dos argumentos para a função embutida max() , ou passar um iterável que em algum
momento produz None . Nos dois casos, você recebe uma exceção como a seguinte durante a execução:

TypeError: '>' not supported between instances of 'int' and 'NoneType'

[NT: TypeError: '>' não é suportado entre instâncias de 'int' e 'NoneType']

A documentação de max começa com a seguinte sentença:

“ Devolve o maior item em um iterável ou o maior de dois ou mais argumentos.


Para mim, essa é uma descrição bastante intuitiva.

Mas se eu for anotar uma função descrita nesses termos, tenho que perguntar: qual dos dois? Um iterável ou dois ou
mais argumentos?

A realidade é mais complicada, porque max também pode receber dois argumentos opcionais: key e default .

Escrevi max em Python para tornar mais fácil ver a relação entre o funcionamento da função e as anotações
sobrepostas (a função embutida original é escrita em C); veja o Exemplo 2.

Exemplo 2. mymax.py: Versão da funcão max em Python


PYTHON3
# imports and definitions omitted, see next listing

MISSING = object()
EMPTY_MSG = 'max() arg is an empty sequence'

# overloaded type hints omitted, see next listing

def max(first, *args, key=None, default=MISSING):


if args:
series = args
candidate = first
else:
series = iter(first)
try:
candidate = next(series)
except StopIteration:
if default is not MISSING:
return default
raise ValueError(EMPTY_MSG) from None
if key is None:
for current in series:
if candidate < current:
candidate = current
else:
candidate_key = key(candidate)
for current in series:
current_key = key(current)
if candidate_key < current_key:
candidate = current
candidate_key = current_key
return candidate

O foco desse exemplo não é a lógica de max , então não vou perder tempo com a implementação, exceto para explicar
MISSING . A constante MISSING é uma instância única de object , usada como sentinela. É o valor default para o
argumento nomeado default= , de modo que max pode aceitar default=None e ainda assim distinguir entre duas
situações.

Quando first é um iterável vazio…​

1. O usuário não forneceu um argumento para default= , então ele é MISSING , e max gera um ValueError .

2. O usuário forneceu um valor para default= , incluindo None , e então max devolve o valor de default .

Para consertar o issue #4051 (https://fpy.li/shed4051), escrevi o código no Exemplo 3.[185]

Exemplo 3. mymax.py: início do módulo, com importações, definições e sobreposições


PYTHON3
from collections.abc import Callable, Iterable
from typing import Protocol, Any, TypeVar, overload, Union

class SupportsLessThan(Protocol):
def __lt__(self, other: Any) -> bool: ...

T = TypeVar('T')
LT = TypeVar('LT', bound=SupportsLessThan)
DT = TypeVar('DT')

MISSING = object()
EMPTY_MSG = 'max() arg is an empty sequence'

@overload
def max(__arg1: LT, __arg2: LT, *args: LT, key: None = ...) -> LT:
...
@overload
def max(__arg1: T, __arg2: T, *args: T, key: Callable[[T], LT]) -> T:
...
@overload
def max(__iterable: Iterable[LT], *, key: None = ...) -> LT:
...
@overload
def max(__iterable: Iterable[T], *, key: Callable[[T], LT]) -> T:
...
@overload
def max(__iterable: Iterable[LT], *, key: None = ...,
default: DT) -> Union[LT, DT]:
...
@overload
def max(__iterable: Iterable[T], *, key: Callable[[T], LT],
default: DT) -> Union[T, DT]:
...

Minha implementação de max em Python tem mais ou menos o mesmo tamanho daquelas importações e declarações
de tipo. Graças ao duck typing, meu código não tem nenhuma verificação usando isinstance , e fornece a mesma
verificação de erro daquelas dicas de tipo—mas apenas durante a execução, claro.

Um benefício fundamental de @overload é declarar o tipo devolvido da forma mais precisa possível, de acordo com os
tipos dos argumentos recebidos. Veremos esse benefício a seguir, estudando as sobreposições de max , em grupos de
duas ou três por vez.

Argumentos implementando SupportsLessThan, mas key e default não são fornecidos


PYTHON3
@overload
def max(__arg1: LT, __arg2: LT, *_args: LT, key: None = ...) -> LT:
...
# ... lines omitted ...
@overload
def max(__iterable: Iterable[LT], *, key: None = ...) -> LT:
...

Nesses casos, as entradas são ou argumentos separados do tipo LT que implementam SupportsLessThan , ou um
Iterable de itens desse tipo. O tipo devolvido por max é do mesmo tipo dos argumentos ou itens reais, como vimos
na seção Seção 8.5.9.2.

Amostras de chamadas que casam com essas sobreposições:


PYTHON3
max(1, 2, -3) # returns 2
max(['Go', 'Python', 'Rust']) # returns 'Rust'

Argumento key fornecido, mas default não


PYTHON3
@overload
def max(__arg1: T, __arg2: T, *_args: T, key: Callable[[T], LT]) -> T:
...
# ... lines omitted ...
@overload
def max(__iterable: Iterable[T], *, key: Callable[[T], LT]) -> T:
...

As entradas podem ser item separados de qualquer tipo T ou um único Iterable[T] , e key= deve ser um invocável
que recebe um argumento do mesmo tipo T , e devolve um valor que implementa SupportsLessThan . O tipo
devolvido por max é o mesmo dos argumentos reais.

Amostras de chamadas que casam com essas sobreposições:

PYTHON3
max(1, 2, -3, key=abs) # returns -3
max(['Go', 'Python', 'Rust'], key=len) # returns 'Python'

Argumento default fornecido, key não


PYTHON3
@overload
def max(__iterable: Iterable[LT], *, key: None = ...,
default: DT) -> Union[LT, DT]:
...

A entrada é um iterável de itens do tipo LT que implemente SupportsLessThan . O argumento default= é o valor
devolvido quando Iterable é vazio. Assim, o tipo devolvido por max deve ser uma Union do tipo LT e do tipo do
argumento default .

Amostras de chamadas que casam com essas sobreposições:

PYTHON3
max([1, 2, -3], default=0) # returns 2
max([], default=None) # returns None

Argumentos key e default fornecidos


PYTHON3
@overload
def max(__iterable: Iterable[T], *, key: Callable[[T], LT],
default: DT) -> Union[T, DT]:
...

As entradas são:

Um Iterable de itens de qualquer tipo T

Invocável que recebe um argumento do tipo T e devolve um valor do tipo LT , que implementa
SupportsLessThan

Um valor default de qualquer tipo DT

O tipo devolvido por max deve ser uma Union do tipo T e do tipo do argumento default :
PYTHON3
max([1, 2, -3], key=abs, default=None) # returns -3
max([], key=abs, default=None) # returns None

15.2.2. Lições da sobreposição de max


Dicas de tipo permitem ao Mypy marcar uma chamada como max([None, None]) com essa mensagem de erro:

mymax_demo.py:109: error: Value of type variable "_LT" of "max"


cannot be "None"

Por outro lado, ter de escrever tantas linhas para suportar o verificador de tipo pode desencorajar a criação de funções
convenientes e flexíveis como max . Se eu precisasse reinventar também a função min , poderia refatorar e reutilizar a
maior parte da implementação de max . Mas teria que copiar e colar todas as declarações de sobreposição—apesar
delas serem idênticas para min , exceto pelo nome da função.

Meu amigo João S. O. Bueno—um dos desenvolvedores Python mais inteligentes que conheço—escreveu o seguinte
tweet (https://fpy.li/15-4):

“ Apesar de ser difícil expressar a assinatura de —ela se encaixa muito facilmente em nossa
max
estrutura mental. Considero a expressividade das marcas de anotação muito limitadas, se
comparadas à do Python.

Vamos agora examinar o elemento de tipagem TypedDict . Ele não é tão útil quanto imaginei inicialmente, mas tem
seus usos. Experimentar com TypedDict demonstra as limitações da tipagem estática para lidar com estruturas
dinâmicas, tais como dados em formato JSON.

15.3. TypedDict
É tentador usar TypedDict para se proteger contra erros ao tratar estruturas de dados dinâmicas
como as respostas da API JSON. Mas os exemplos aqui deixam claro que o tratamento correto do
⚠️ AVISO JSON precisa acontecer durante a execução, e não com verificação estática de tipo. Para verificar
estruturas similares a JSON usando dicas de tipo durante a execução, dê uma olhada no pacote
pydantic (https://fpy.li/15-5) no PyPI.

Algumas vezes os dicionários do Python são usados como registros, as chaves interpretadas como nomes de campos e
os valores como valores dos campos de diferentes tipos. Considere, por exemplo, um registro descrevendo um livro, em
JSON ou Python:

JAVASCRIPT
{"isbn": "0134757599",
"title": "Refactoring, 2e",
"authors": ["Martin Fowler", "Kent Beck"],
"pagecount": 478}

Antes do Python 3.8, não havia uma boa maneira de anotar um registro como esse, pois os tipos de mapeamento que
vimos na seção Seção 8.5.6 limitam os valores a um mesmo tipo.

Aqui estão duas tentativas ruins de anotar um registro como o objeto JSON acima:

Dict[str, Any]
Os valores podem ser de qualquer tipo.
Dict[str, Union[str, int, List[str]]]
Difícil de ler, e não preserva a relação entre os nomes dos campos e seus respectivos tipos: title deve ser uma
str , ele não pode ser um int ou uma List[str] .
A PEP 589—TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys (TypedDict: Dicas de Tipo para Dicionários
com um Conjunto Fixo de Chaves) (https://fpy.li/pep589) enfrenta esse problema. O Exemplo 4 mostra um TypedDict
simples.

Exemplo 4. books.py: a definição de BookDict

PY
from typing import TypedDict

class BookDict(TypedDict):
isbn: str
title: str
authors: list[str]
pagecount: int

À primeira vista, typing.TypedDict pode parecer uma fábrica de classes de dados, similar a typing.NamedTuple —
tratada no Capítulo 5.

A similaridade sintática é enganosa. TypedDict é muito diferente. Ele existe apenas para o benefício de verificadores
de tipo, e não tem qualquer efeito durante a execução.

TypedDict fornece duas coisas:

Uma sintaxe similar à de classe para anotar uma dict com dicas de tipo para os valores de cada "campo".
Um construtor que informa ao verificador de tipo para esperar um dict com chaves e valores como especificados.

Durante a execução, um construtor de TypedDict como BookDict é um placebo: ele tem o mesmo efeito de uma
chamada ao construtor de dict com os mesmos argumentos.

O fato de BookDict criar um dict simples também significa que:

Os "campos" na definiçao da pseudoclasse não criam atributos de instância.


Não é possível escrever inicializadores com valores default para os "campos".
Definições de métodos não são permitidas.

Vamos explorar o comportamento de um BookDict durante a execução (no Exemplo 5).

Exemplo 5. Usando um BookDict , mas não exatamente como planejado


PYCON
>>> from books import BookDict
>>> pp = BookDict(title='Programming Pearls', # (1)
... authors='Jon Bentley', # (2)
... isbn='0201657880',
... pagecount=256)
>>> pp # (3)
{'title': 'Programming Pearls', 'authors': 'Jon Bentley', 'isbn': '0201657880',
'pagecount': 256}
>>> type(pp)
<class 'dict'>
>>> pp.title # (4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'title'
>>> pp['title']
'Programming Pearls'
>>> BookDict.__annotations__ # (5)
{'isbn': <class 'str'>, 'title': <class 'str'>, 'authors': typing.List[str],
'pagecount': <class 'int'>}

1. É possível invocar BookDict como um construtor de dict , com argumentos nomeados, ou passando um
argumento dict —incluindo um literal dict .
2. Oops…​Esqueci que authors deve ser uma lista. Mas tipagem gradual significa que não há checagem de tipo
durante a execução.
3. O resultado da chamada a BookDict é um dict simples…​
4. …​assim não é possível ler os campos usando a notação objeto.campo .

5. As dicas de tipo estão em BookDict.__annotations__ , e não em pp .

Sem um verificador de tipo, TypedDict é tão útil quanto comentários em um programa: pode ajudar a documentar o
código, mas só isso. As fábricas de classes do Capítulo 5, por outro lado, são úteis mesmo se você não usar um
verificador de tipo, porque durante a execução elas geram uma classe personalizada que pode ser instanciada. Elas
também fornecem vários métodos ou funções úteis, listadas na Tabela 12 do Capítulo 5.

O Exemplo 6 cria um BookDict válido e tenta executar algumas operações com ele. A seguir, o Exemplo 7 mostra como
TypedDict permite que o Mypy encontre erros.

Exemplo 6. demo_books.py: operações legais e ilegais em um BookDict


PY
from books import BookDict
from typing import TYPE_CHECKING

def demo() -> None: # (1)


book = BookDict( # (2)
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478
)
authors = book['authors'] # (3)
if TYPE_CHECKING: # (4)
reveal_type(authors) # (5)
authors = 'Bob' # (6)
book['weight'] = 4.2
del book['title']

if __name__ == '__main__':
demo()

1. Lembre-se de adicionar o tipo devolvido, assim o Mypy não ignora a função.


2. Este é um BookDict válido: todas as chaves estão presentes, com valores do tipo correto.
3. O Mypy vai inferir o tipo de authors a partir da anotação na chave 'authors' em BookDict .

4. typing.TYPE_CHECKING só é True quando os tipos no programa estão sendo verificados. Durante a execução ele é
sempre falso.
5. O if anterior evita que reveal_type(authors) seja chamado durante a execução. reveal_type não é uma
função do Python disponível durante a execução, mas sim um instrumento de depuração fornecido pelo Mypy. Por
isso não há um import para ela. Veja sua saída no Exemplo 7.
6. As últimas três linhas da função demo são ilegais. Elas vão causar mensagens de erro no Exemplo 7.

Verificando a tipagem em demo_books.py, do Exemplo 6, obtemos o Exemplo 7.

Exemplo 7. Verificando os tipos em demo_books.py

…/typeddict/ $ mypy demo_books.py


demo_books.py:13: note: Revealed type is 'built-ins.list[built-ins.str]' (1)
demo_books.py:14: error: Incompatible types in assignment
(expression has type "str", variable has type "List[str]") (2)
demo_books.py:15: error: TypedDict "BookDict" has no key 'weight' (3)
demo_books.py:16: error: Key 'title' of TypedDict "BookDict" cannot be deleted (4)
Found 3 errors in 1 file (checked 1 source file)

1. Essa observação é o resultado de reveal_type(authors) .

2. O tipo da variável authors foi inferido a partir do tipo da expressão que a inicializou, book['authors'] . Você
não pode atribuir uma str para uma variável do tipo List[str] . Verificadores de tipo em geral não permitem
que o tipo de uma variável mude.[186]
3. Não é permitido atribuir a uma chave que não é parte da definição de BookDict .

4. Não se pode apagar uma chave que é parte da definição de BookDict .

Vejamos agora BookDict sendo usado em assinaturas de função, para checar o tipo em chamadas de função.
Imagine que você precisa gerar XML a partir de registros de livros como esse:

XML
<BOOK>
<ISBN>0134757599</ISBN>
<TITLE>Refactoring, 2e</TITLE>
<AUTHOR>Martin Fowler</AUTHOR>
<AUTHOR>Kent Beck</AUTHOR>
<PAGECOUNT>478</PAGECOUNT>
</BOOK>

Se você estivesse escrevendo o código em MicroPython, para ser integrado a um pequeno microcontrolador, poderia
escrever uma função parecida com a que aparece no Exemplo 8.[187]

Exemplo 8. books.py: a função to_xml

PY
AUTHOR_ELEMENT = '<AUTHOR>{}</AUTHOR>'

def to_xml(book: BookDict) -> str: # (1)


elements: list[str] = [] # (2)
for key, value in book.items():
if isinstance(value, list): # (3)
elements.extend(
AUTHOR_ELEMENT.format(n) for n in value) # (4)
else:
tag = key.upper()
elements.append(f'<{tag}>{value}</{tag}>')
xml = '\n\t'.join(elements)
return f'<BOOK>\n\t{xml}\n</BOOK>'

1. O principal objetivo do exemplo: usar BookDict em uma assinatura de função.


2. Se a coleção começa vazia, o Mypy não tem inferir o tipo dos elementos. Por isso a anotação de tipo é necessária
aqui.[188]
3. O Mypy entende testes com isinstance , e trata value como uma list neste bloco.
4. Quando usei key == 'authors' como condição do if que guarda esse bloco, o Mypy encontrou um erro nessa
linha: "object" has no attribute "__iter__" ("object" não tem um atributo "__iter__" ), porque inferiu o tipo de
value devolvido por book.items() como object , que não suporta o método __iter__ exigido pela expressão
geradora. O teste com isinstance funciona porque garante que value é uma list nesse bloco.

O Exemplo 9 mostra uma função que interpreta uma str JSON e devolve um BookDict .

Exemplo 9. books_any.py: a função from_json

PY
def from_json(data: str) -> BookDict:
whatever = json.loads(data) # (1)
return whatever # (2)

1. O tipo devolvido por json.loads() é Any .[189]

2. Posso devolver whatever —de tipo Any —porque Any é consistente-com todos os tipos, incluindo o tipo declarado
do valor devolvido, BookDict .
O segundo ponto do Exemplo 9 é muito importante de ter em mente: O Mypy não vai apontar qualquer problema nesse
código, mas durante a execução o valor em whatever pode não se adequar à estrutura de BookDict —na verdade,
pode nem mesmo ser um dict !

Se você rodar o Mypy com --disallow-any-expr , ele vai reclamar sobre as duas linhas no corpo de from_json :

…/typeddict/ $ mypy books_any.py --disallow-any-expr


books_any.py:30: error: Expression has type "Any"
books_any.py:31: error: Expression has type "Any"
Found 2 errors in 1 file (checked 1 source file)

As linhas 30 e 31 mencionadas no trecho acima são o corpo da função from_json . Podemos silenciar o erro de tipo
acrescentando uma dica de tipo à inicialização da variável whatever , como no Exemplo 10.

Exemplo 10. books.py: a função from_json com uma anotação de variável

PY
def from_json(data: str) -> BookDict:
whatever: BookDict = json.loads(data) # (1)
return whatever # (2)

1. --disallow-any-expr não gera erros quando uma expressão de tipo Any é imediatamente atribuída a uma
variável com uma dica de tipo.
2. Agora whatever é do tipo BookDict , o tipo declarado do valor devolvido.

Não se deixe enganar por uma falsa sensação de tipagem segura com o Exemplo 10! Olhando o
⚠️ AVISO código estático, o verificador de tipo não tem como prever se json.loads() irá devolver qualquer
coisa parecida com um BookDict . Apenas a validação durante a execução pode garantir isso.

A verificação de tipo estática é incapaz de prevenir erros cm código inerentemente dinâmico, como json.loads() ,
que cria objetos Python de tipos diferentes durante a execução. O Exemplo 11, o Exemplo 12 e o Exemplo 13
demonstram isso.

Exemplo 11. demo_not_book.py: from_json devolve um BookDict inválido, e to_xml o aceita


PY
from books import to_xml, from_json
from typing import TYPE_CHECKING

def demo() -> None:


NOT_BOOK_JSON = """
{"title": "Andromeda Strain",
"flavor": "pistachio",
"authors": true}
"""
not_book = from_json(NOT_BOOK_JSON) # (1)
if TYPE_CHECKING: # (2)
reveal_type(not_book)
reveal_type(not_book['authors'])

print(not_book) # (3)
print(not_book['flavor']) # (4)

xml = to_xml(not_book) # (5)


print(xml) # (6)

if __name__ == '__main__':
demo()

1. Essa linha não produz um BookDict válido—veja o conteúdo de NOT_BOOK_JSON .

2. Vamos deixar o Mypy revelar alguns tipos.


3. Isso não deve causar problemas: print consegue lidar com object e com qualquer outro tipo.
4. BookDict não tem uma chave 'flavor' , mas o fonte JSON tem…​o que vai acontecer??

5. Lembre-se da assinatura: def to_xml(book: BookDict) → str: .

6. Como será a saída XML?

Agora verificamos demo_not_book.py com o Mypy (no Exemplo 12).

Exemplo 12. Relatório do Mypy para demo_not_book.py, reformatado por legibilidade

…/typeddict/ $ mypy demo_not_book.py


demo_not_book.py:12: note: Revealed type is
'TypedDict('books.BookDict', {'isbn': built-ins.str,
'title': built-ins.str,
'authors': built-ins.list[built-ins.str],
'pagecount': built-ins.int})' (1)
demo_not_book.py:13: note: Revealed type is 'built-ins.list[built-ins.str]' (2)
demo_not_book.py:16: error: TypedDict "BookDict" has no key 'flavor' (3)
Found 1 error in 1 file (checked 1 source file)

1. O tipo revelado é o tipo nominal, não o conteúdo de not_book durante a execução.


2. De novo, este é o tipo nominal de not_book['authors'] , como definido em BookDict . Não o tipo durante a
execução.
3. Esse erro é para a linha print(not_book['flavor']) : essa chave não existe no tipo nominal.

Agora vamos executar demo_not_book.py, mostrando o resultado no Exemplo 13.

Exemplo 13. Resultado da execução de demo_not_book.py


…/typeddict/ $ python3 demo_not_book.py
{'title': 'Andromeda Strain', 'flavor': 'pistachio', 'authors': True} (1)
pistachio (2)
<BOOK> (3)
<TITLE>Andromeda Strain</TITLE>
<FLAVOR>pistachio</FLAVOR>
<AUTHORS>True</AUTHORS>
</BOOK>

1. Isso não é um BookDict de verdade.


2. O valor de not_book['flavor'] .

3. to_xml recebe um argumento BookDict , mas não há qualquer verificação durante a execução: entra lixo, sai lixo.

O Exemplo 13 mostra que demo_not_book.py devolve bobagens, mas não há qualquer erro durante a execução. Usar
um TypedDict ao tratar dados em formato JSON não resultou em uma tipagem segura.

Olhando o código de to_xml no Exemplo 8 através das lentes do duck typing, o argumento book deve fornecer um
método .items() que devolve um iterável de tuplas na forma (chave, valor) , onde:

chave deve ter um método .upper()

valor pode ser qualquer coisa

A conclusão desta demonstração: quando estamos lidando com dados de estrutura dinâmica, tal como JSON ou XML,
TypedDict não é, de forma alguma, um substituto para a validaçào de dados durante a execução. Para isso, use o
pydantic (https://fpy.li/15-5) (EN).

TypedDict tem mais recursos, incluindo suporte a chaves opcionais, uma forma limitada de herança e uma sintaxe de
declaração alternativa. Para saber mais sobre ele, revise a PEP 589—TypedDict: Type Hints for Dictionaries with a
Fixed Set of Keys (TypedDict: Dicas de Tipo para Dicionários com um Conjunto Fixo de Chaves) (https://fpy.li/pep589) (EN).

Vamos agora voltar nossas atenções para uma função que é melhor evitar, mas que algumas vezes é inevitável:
typing.cast .

15.4. Coerção de Tipo


Nenhum sistema de tipos é perfeito, nem tampouco os verificadores estáticos de tipo, as dicas de tipo no projeto
typeshed ou as dicas de tipo em pacotes de terceiros que as oferecem.

A função especial typing.cast() fornece uma forma de lidar com defeitos ou incorreções nas dicas de tipo em código
que não podemos consertar. A documentação do Mypy 0.930 (https://fpy.li/15-14) (EN) explica:

“ Coerções são usadas para silenciar avisos espúrios do verificador de tipos, e dão uma ajuda ao
verificador quando ele não consegue entender direito o que está acontecendo.

Durante a execução, typing.cast não faz absolutamente nada. Essa é sua implementação (https://fpy.li/15-15):
PYTHON
def cast(typ, val):
"""Cast a value to a type.
This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return val

A PEP 484 exige que os verificadores de tipo "acreditem cegamente" em cast . A seção "Casts" (Coerções) da PEP 484
(https://fpy.li/15-16) mostra um exemplo onde o verificador precisa da orientação de cast :

PYTHON
from typing import cast

def find_first_str(a: list[object]) -> str:


index = next(i for i, x in enumerate(a) if isinstance(x, str))
# We only get here if there's at least one string
return cast(str, a[index])

A chamada next() na expressão geradora vai devolver ou o índice de um item str ou gerar StopIteration . Assim,
find_first_str vai sempre devolver uma str se não for gerada uma exceção, e str é o tipo declarado do valor
devolvido.

Mas se a última linha for apenas return a[index] , o Mypy inferiria o tipo devolvido como object , porque o
argumento a é declarado como list[object] . Então cast() é necessário para guiar o Mypy.[190]

Aqui está outro exemplo com cast , desta vez para corrigir uma dica de tipo desatualizada na biblioteca padrão do
Python. No Exemplo 12, criei um objeto asyncio , Server , e queria obter o endereço que o servidor estava ouvindo.
Escrevi essa linha de código:

PYTHON
addr = server.sockets[0].getsockname()

Mas o Mypy informou o seguinte erro:

Value of type "Optional[List[socket]]" is not indexable

A dica de tipo para Server.sockets no typeshed, em maio de 2021, é válida para o Python 3.6, onde o atributo
sockets podia ser None . Mas no Python 3.7, sockets se tornou uma propriedade, com um getter que sempre
devolve uma list —que pode ser vazia, se o servidor não tiver um socket. E desde o Python 3.8, esse getter devolve
uma tuple (usada como uma sequência imutável).

Já que não posso consertar o typeshed nesse instante,[191] acrescentei um cast , assim:

PYTHON
from asyncio.trsock import TransportSocket
from typing import cast

# ... muitas linhas omitidas ...

socket_list = cast(tuple[TransportSocket, ...], server.sockets)


addr = socket_list[0].getsockname()
Usar cast nesse caso exigiu algumas horas para entender o problema e ler o código-fonte de asyncio, para encontrar o
tipo correto para sockets: a classe TransportSocket do módulo não-documentado asyncio.trsock . Também
precisei adicionar duas instruções import e mais uma linha de código para melhorar a legibilidade.[192] Mas agora o
código está mais seguro.

O leitor atento pode ser notado que sockets[0] poderia gerar um IndexError se sockets estiver vazio. Entretanto,
até onde entendo o asyncio , isso não pode acontecer no Exemplo 12, pois no momento em que leio o atributo
sockets , o server já está pronto para aceitar conexões , portanto o atributo não estará vazio. E, de qualquer forma,
IndexError ocorre durante a execução. O Mypy não consegue localizar esse problema nem mesmo em um caso trivial
como print([][0]) .

Não fique muito confortável usando cast para silenciar o Mypy, porque normalmente o Mypy está

⚠️ AVISO certo quando aponta um erro. Se você estiver usando cast com frequência, isso é um code smell
(cheiro no código) (https://fpy.li/15-20) (EN). Sua equipe pode estar fazendo um mau uso das dicas de
tipo, ou sua base de código pode ter dependências de baixa qualidaade.

Apesar de suas desvantagens, há usos válidos para cast . Eis algo que Guido van Rossum escreveu sobre isso:

“ Oocasionais?
que está errado com uma chamada a
[193]
cast() ou um comentário # type: ignore

É insensato banir inteiramente o uso de cast , principalmente porque as alternativas para contornar esses problemas
são piores:

# type: ignore é menos informativo.[194]


Usar Any é contagioso: já que Any é consistente-com todos os tipos, seu abuso pode produzir efeitos em cascata
através da inferência de tipo, minando a capacidade do verificador de tipo para detectar erros em outras partes do
código.

Claro, nem todos os contratempos de tipagem podem ser resolvidos com cast . Algumas vezes precisamos de # type:
ignore , do Any ocasional, ou mesmo deixar uma função sem dicas de tipo.

A seguir, vamos falar sobre o uso de anotações durante a execução.

15.5. Lendo dicas de tipo durante a execução


Durante a importação, o Python lê as dicas de tipo em funções, classes e módulos, e as armazena em atributos
chamados __annotations__ . Considere, por exemplo, a função clip function no Exemplo 14.[195]

Exemplo 14. clipannot.py: a assinatura anotada da função clip

PY
def clip(text: str, max_len: int = 80) -> str:

As dicas de tipo são armazenadas em um dict no atributo __annotations__ da função:

PYCON
>>> from clip_annot import clip
>>> clip.__annotations__
{'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}

A chave 'return' está mapeada para a dica do tipo devolvido após o símbolo → no Exemplo 14.
Observe que as anotações são avaliadas pelo interpretador no momento da importação, ao mesmo tempo em que os
valores default dos parâmetros são avaliados. Por isso os valores nas anotações são as classes Python str e int , e não
as strings 'str' and 'int' . A avaliação das anotações no momento da importação é o padrão desde o Python 3.10,
mas isso pode mudar se a PEP 563 (https://fpy.li/pep563) ou a PEP 649 (https://fpy.li/pep649) se tornarem o comportamento
padrão.

15.5.1. Problemas com anotações durante a execução


O aumento do uso de dicas de tipo gerou dois problemas:

Importar módulos usa mais CPU e memória quando são usadas muitas dicas de tipo.
Referências a tipos ainda não definidos exigem o uso de strings em vez do tipos reais.

As duas questões são relevantes. A primeira pelo que acabamos de ver: anotações são avaliadas pelo interpretador
durante a importação e armazenadas no atributo __annotations__ . Vamos nos concentrar agora no segundo
problema.

Armazenar anotações como string é necessário algumas vezes, por causa do problema da "referência adiantada"
(forward reference): quando uma dica de tipo precisa se referir a uma classe definida mais adiante no mesmo módulo.
Entretanto uma manifestação comum desse problema no código-fonte não se parece de forma alguma com uma
referência adiantada: quando um método devolve um novo objeto da mesma classe. Já que o objeto classe não está
definido até o Python terminar a avaliação do corpo da classe, as dicas de tipo precisam usar o nome da classe como
string. Eis um exemplo:

PY
class Rectangle:
# ... lines omitted ...
def stretch(self, factor: float) -> 'Rectangle':
return Rectangle(width=self.width * factor)

Escrever dicas de tipo com referências adiantadas como strings é a prática padrão e exigida no Python 3.10. Os
verificadores de tipo estáticos foram projetados desde o início para lidar com esse problema.

Mas durante a execução, se você escrever código para ler a anotação return de stretch , vai receber a string
'Rectangle' em vez de uma referência ao tipo real, a classe Rectangle . E aí seu código precisa descobrir o que
aquela string significa.

O módulo typing inclui três funções e uma classe categorizadas Introspection helpers (Auxiliares de introspecção)
(https://docs.python.org/pt-br/3/library/typing.html#introspection-helpers), a mais importantes delas sendo
typing.get_type_hints . Parte de sua documentação afirma:

get_type_hints(obj, globals=None, locals=None, include_extras=False)

[…​] Isso é muitas vezes igual a obj.__annotations__ . Além disso, referências adiantadas codificadas como strings
literais são tratadas por sua avaliação nos espaços de nomes globals e locals . […​]

Desde o Python 3.10, a nova função inspect.get_annotations(…) (https://fpy.li/15-25) deve ser

⚠️ AVISO usada, em vez de typing.​get_​type_​hints . Entretanto, alguns leitores podem ainda não estar
trabalhando com o Python 3.10, então usarei a typing.​get_​type_​hints nos exemplos, pois essa
função está disponível desde a adição do módulo typing , no Python 3.5.

A PEP 563—Postponed Evaluation of Annotations (_Avaliação Adiada de Anotações) (https://fpy.li/pep563) (EN) foi
aprovada para tornar desnecessário escrever anotações como strings, e para reduzir o custo das dicas de tipo durante a
execução. A ideia principal está descrita nessas duas sentenças do "Abstract" (https://fpy.li/15-26) (EN):


“ Esta PEP propõe modificar as anotações de funções e de variáveis, de forma que elas não mais
sejam avaliadas no momento da definição da função. Em vez disso, elas são preservadas em
__annotations__ na forma de strings..

A partir do Python 3.7, é assim que anotações são tratadas em qualquer módulo que comece com a seguinte instrução
import :

PY
from __future__ import annotations

Para demonstrar seu efeito, coloquei a mesma função clip do Exemplo 14 em um módulo clip_annot_post.py com
aquela linha de importação __future__ no início.

No console, esse é o resultado de importar aquele módulo e ler as anotações de clip :

PYCON
>>> from clip_annot_post import clip
>>> clip.__annotations__
{'text': 'str', 'max_len': 'int', 'return': 'str'}

Como se vê, todas as dicas de tipo são agora strings simples, apesar de não terem sido escritas como strings na
definição de clip (no Exemplo 14).

A função typing.get_type_hints consegue resolver muitas dicas de tipo, incluindo essas de clip :

PYCON
>>> from clip_annot_post import clip
>>> from typing import get_type_hints
>>> get_type_hints(clip)
{'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}

A chamada a get_type_hints nos dá os tipos resis—mesmo em alguns casos onde a dica de tipo original foi escrita
como uma string. Essa é a maneira recomendada de ler dicas de tipo durante a execução.

O comportamento prescrito na PEP 563 estava previsto para se tornar o default no Python 3.10, tornando a importação
com __future__ desnecessária. Entretanto, os mantenedores da FastAPI e do pydantic soaram o alarme, essa
mudança quebraria seu código, que se baseia em dicas de tipo durante a execução e não podem usar get_type_hints
de forma confiável.

Na discussão que se seguiu na lista de email python-dev, Łukasz Langa—autor da PEP 563—descreveu algumas
limitações daquela função:

“ […​
] a verdade é que tem limites que tornam seu uso geral custoso
typing.get_type_hints()
durante a execução e, mais importante, insuficiente para resolver todos os tipos. O exemplo
mais comum se refere a contextos não-globais nos quais tipos são gerados (isto é, classes
aninhadas, classes dentro de funções, etc.). Mas um dos principais exemplos de referências
adiantadas, classes com métodos aceitando ou devolvendo objetos de seu próprio tipo, também
não é tratado de forma apropriada por typing.get_type_hints() se um gerador de classes
for usado. Há alguns truques que podemos usar para ligar os pontos mas, de uma forma geral,
isso não é bom.[196]
O Steering Council do Python decidiu adiar a elevação da PEP 563 a comportamento padrão até o Python 3.11 ou
posterior, dando mais tempo aos desenvolvedores para criar uma solução para os problemas que a PEP 563 tentou
resolver, sem quebrar o uso dissseminado das dicas de tipo durante a execução. A PEP 649—Deferred Evaluation Of
Annotations Using Descriptors (Avaliação Adiada de Anotações Usando Descritores) (https://fpy.li/pep649) (EN) está sendo
considerada como uma possível solução, mas algum outro acordo ainda pode ser alcançado.

Resumindo: ler dicas de tipo durante a execução não é 100% confiável no Python 3.10 e provavelmente mudará em
alguma futura versão.

Empresas usando o Python em escala muito ampla desejam os benefícios da tipagem estática, mas
não querem pagar o preço da avaliação de dicas de tipo no momento da importação. A checagem
estática acontece nas estações de trabalho dos desenvolvedores e em servidores de integração
contínua dedicados, mas o carregamento de módulos acontece em uma frequência e um volume
muito mais altos, em servidores de produção, e esse custo não é desprezível em grande escala.
✒️ NOTA
Isso cria uma tensão na comunidade Python, entre aqueles que querem as dicas de tipo
armazenadas apenas como strings—para reduzir os custos de carregamento—versus aqueles que
também querem usar as dicas de tipo durante a execução, como os criadores e os usuários do
pydantic e da FastAPI, para quem seria mais fácil acessar diretamente os tipos, ao invés de
precisarem analisar strings nas anotações, uma tarefa desafiadora.

15.5.2. Lidando com o problema


Dada a instabilidade da situação atual, se você precisar ler anotações durante a execução, recomendo o seguinte:

Evite ler __annotations__ diretamente; em vez disso, use inspect.get_annotations (desde o Python 3.10) ou
typing.get_type_hints (desde o Python 3.5).
Escreva uma função personalizada própria, como um invólucro para in​spect​.get_annotations ou
typing.get_type_hints , e faça o restante de sua base de código chamar aquela função, de forma que mudanças
futuras fiquem restritas a um único local.

Para demonstrar esse segundo ponto, aqui estão as primeiras linhas da classe Checked , definida no
[checked_class_top_ex], classe que estudaremos no [class_metaprog]:

PY
class Checked:
@classmethod
def _fields(cls) -> dict[str, type]:
return get_type_hints(cls)
# ... more lines ...

O método de evita que outras partes do módulo dependam diretamente de


Checked._fields
typing.get_type_hints . Se get_type_hints mudar no futuro, exigindo lógica adicional, ou se eu quiser substituí-la
por inspect.get_annotations , a mudança estará limitada a Checked._fields e não afetará o restante do
programa.
Dadas as discussões correntes e as mudanças propostas para a inspeção de dicas de tipo durante a
execução, a página da documentação oficial "Boas Práticas de Anotação"
(https://docs.python.org/pt-br/3.10/howto/annotations.html) é uma leitura obrigatória, e a página deve ser

⚠️ AVISO atualizada até o lançamento do Python 3.11. Aquele how-to foi escrito por Larry Hastings, autor da
PEP 649—Deferred Evaluation Of Annotations Using Descriptors (Avaliação Adiada de Anotações
Usando Descritores) (https://fpy.li/pep649) (EN), uma proposta alternativa para tratar os problemas
gerados durante a execução pela PEP 563—Postponed Evaluation of Annotations (_Avaliação Adiada
de Anotações) (https://fpy.li/pep563) (EN).

As seções restantes desse capítulo cobrem tipos genéricos, começando pela forma de definir uma classe genérica, que
pode ser parametrizada por seus usuários.

15.6. Implementando uma classe genérica


No Exemplo 7, definimos a ABC Tombola : uma interface para classes que funcionam como um recipiente para sorteio
de bingo. A classe LottoBlower do Exemplo 10 é uma implementação concreta. Vamos agora estudar uma versão
genérica de LottoBlower , usada da forma que aparece no Exemplo 15.

Exemplo 15. generic_lotto_demo.py: usando uma classe genérica de sorteio de bingo

PY
from generic_lotto import LottoBlower

machine = LottoBlower[int](range(1, 11)) # (1)

first = machine.pick() # (2)


remain = machine.inspect() # (3)

1. Para instanciar uma classe genérica, passamos a ela um parâmetro de tipo concreto, como int aqui.
2. O Mypy irá inferir corretamente que first é um int …​

3. …​e que remain é uma tuple de inteiros.

Além disso, o Mypy aponta violações do tipo parametrizado com mensagens úteis, como ilustrado no Exemplo 16.

Exemplo 16. generic_lotto_errors.py: erros apontados pelo Mypy

PY
from generic_lotto import LottoBlower

machine = LottoBlower[int]([1, .2])


## error: List item 1 has incompatible type "float"; # (1)
## expected "int"

machine = LottoBlower[int](range(1, 11))

machine.load('ABC')
## error: Argument 1 to "load" of "LottoBlower" # (2)
## has incompatible type "str";
## expected "Iterable[int]"
## note: Following member(s) of "str" have conflicts:
## note: Expected:
## note: def __iter__(self) -> Iterator[int]
## note: Got:
## note: def __iter__(self) -> Iterator[str]
1. Na instanciação de LottoBlower[int] , o Mypy marca o float .

2. Na chamada .load('ABC') , o Mypy explica porque uma str não serve: str.__iter__ devolve um
Iterator[str] , mas LottoBlower[int] exige um Iterator[int] .

O Exemplo 17 é a implementação.

Exemplo 17. generic_lotto.py: uma classe genérica de sorteador de bingo

PY
import random

from collections.abc import Iterable


from typing import TypeVar, Generic

from tombola import Tombola

T = TypeVar('T')

class LottoBlower(Tombola, Generic[T]): # (1)

def __init__(self, items: Iterable[T]) -> None: # (2)


self._balls = list[T](items)

def load(self, items: Iterable[T]) -> None: # (3)


self._balls.extend(items)

def pick(self) -> T: # (4)


try:
position = random.randrange(len(self._balls))
except ValueError:
raise LookupError('pick from empty LottoBlower')
return self._balls.pop(position)

def loaded(self) -> bool: # (5)


return bool(self._balls)

def inspect(self) -> tuple[T, ...]: # (6)


return tuple(self._balls)

1. Declarações de classes genéricas muitas vezes usam herança múltipla, porque precisamos de uma subclasse de
Generic para declarar os parâmetros de tipo formais—nesse caso, T .

2. O argumento items em __init__ é do tipo Iterable[T] , que se torna Iterable[int] quando uma instância é
declarada como LottoBlower[int] .
3. O método load é igualmente restrito.
4. O tipo do valor devolvido T agora se torna int em um LottoBlower[int] .

5. Nenhuma variável de tipo aqui.


6. Por fim, T define o tipo dos itens na tuple devolvida.

A seção "User-defined generic types" (Tipos genéricos definidos pelo usuário)


👉 DICA (https://docs.python.org/pt-br/3/library/typing.html#user-defined-generic-types) (EN), na documentação do
módulo typing , é curta, inclui bons exemplos e fornece alguns detalhes que não menciono aqui.

Agora que vimos como implementar um classe genérica, vamos definir a terminologia para falar sobre tipos genéricos.

15.6.1. Jargão básico para tipos genéricos


Aqui estão algumas definições que encontrei estudando genéricos:[197]

Tipo genérico
Um tipo declarado com uma ou mais variáveis de tipo.
Exemplos: LottoBlower[T] , abc.Mapping[KT, VT]

Parâmetro de tipo formal


As variáveis de tipo que aparecem em um declaração de tipo genérica.
Exemplo: KT e VT no último exemplo: abc.Mapping[KT, VT]

Tipo parametrizado
Um tipo declarado com os parâmetros de tipo reais.
Exemplos: LottoBlower[int] , abc.Mapping[str, float]

Parâmetro de tipo real


Os tipos reais passados como parâmetros quando um tipo parametrizado é declarado.
Exemplo: o int em LottoBlower[int]

O próximo tópico é sobre como tornar os tipos genéricos mais flexíveis, introduzindo os conceitos de covariância,
contravariância e invariância.

15.7. Variância
Dependendo de sua experiência com genéricos em outras linguagens, essa pode ser a parte mais
difícil do livro. O conceito de variância é abstrato, e uma apresentação rigorosa faria essa seção se
parecer com páginas tiradas de um livro de matemática.

✒️ NOTA Na prática, a variância é mais relevante para autores de bibliotecas que querem suportar novos
tipos de contêineres genéricos ou fornecer uma API baseada em callbacks. Mesmo nesses casos, é
possível evitar muita complexidade suportando apenas contêineres invariantes—que é quase só o
que temos hoje na biblioteca padrão. Então, em uma primeira leitura você pode pular toda essa
seção, ou ler apenas as partes sobre tipos invariantes.

Já vimos o conceito de variância na seção Seção 8.5.11.1, aplicado a tipos genéricos Callable parametrizados. Aqui
vamos expandir o conceito para abarcar tipo genéricos de coleções, usando uma analogia do "mundo real" para tornar
mais concreto esse conceito abstrato.

Imagine uma cantina escolar que tenha como regra que apenas máquinas servindo sucos podem ser instaladas ali.[198]
Máquinas de bebida genéricas não são permitidas, pois podem servir refrigerantes, que foram banidos pela direção da
escola.[199]

15.7.1. Uma máquina de bebida invariante


Vamos tentar modelar o cenário da cantina com uma classe genérica BeverageDispenser , que pode ser
parametrizada com o tipo de bebida.. Veja o Exemplo 18.

Exemplo 18. invariant.py: definições de tipo e função install


PY
from typing import TypeVar, Generic

class Beverage: # (1)


"""Any beverage."""

class Juice(Beverage):
"""Any fruit juice."""

class OrangeJuice(Juice):
"""Delicious juice from Brazilian oranges."""

T = TypeVar('T') # (2)

class BeverageDispenser(Generic[T]): # (3)


"""A dispenser parameterized on the beverage type."""
def __init__(self, beverage: T) -> None:
self.beverage = beverage

def dispense(self) -> T:


return self.beverage

def install(dispenser: BeverageDispenser[Juice]) -> None: # (4)


"""Install a fruit juice dispenser."""

1. Beverage , Juice , e OrangeJuice formam uma hierarquia de tipos.


2. Uma declaração TypeVar simples.
3. BeverageDispenser é parametrizada pelo tipo de bebida.
4. install é uma função global do módulo. Sua dica de tipo faz valer a regra de que apenas máquinas de suco são
aceitáveis.

Dadas as definições no Exemplo 18, o seguinte código é legal:

PY
juice_dispenser = BeverageDispenser(Juice())
install(juice_dispenser)

Entretanto, isso não é legal:

PY
beverage_dispenser = BeverageDispenser(Beverage())
install(beverage_dispenser)
## mypy: Argument 1 to "install" has
## incompatible type "BeverageDispenser[Beverage]"
## expected "BeverageDispenser[Juice]"

Uma máquina que serve qualquer Beverage não é aceitável, pois a cantina exige uma máquina especializada em
Juice .

De forma um tanto surpreendente, este código também é ilegal:

PY
orange_juice_dispenser = BeverageDispenser(OrangeJuice())
install(orange_juice_dispenser)
## mypy: Argument 1 to "install" has
## incompatible type "BeverageDispenser[OrangeJuice]"
## expected "BeverageDispenser[Juice]"
Uma máquina especializada em OrangeJuice também não é permitida. Apenas BeverageDispenser[Juice] serve.
No jargão da tipagem, dizemos que BeverageDispenser(Generic[T]) é invariante quando
BeverageDispenser[OrangeJuice] não é compatível com BeverageDispenser[Juice] —apesar do fato de
OrangeJuice ser um subtipo-de Juice .

Os tipos de coleções mutáveis do Python—tal como list e set —são invariantes. A classe LottoBlower do Exemplo
17 também é invariante.

15.7.2. Uma máquina de bebida covariante


Se quisermos ser mais flexíveis, e modelar as máquinas de bebida como uma classe genérica que aceite alguma bebida
e também seus subtipos, precisamos tornar a classe covariante. O Exemplo 19 mostra como declararíamos
BeverageDispenser .

Exemplo 19. covariant.py: type definitions and install function

PY
T_co = TypeVar('T_co', covariant=True) # (1)

class BeverageDispenser(Generic[T_co]): # (2)


def __init__(self, beverage: T_co) -> None:
self.beverage = beverage

def dispense(self) -> T_co:


return self.beverage

def install(dispenser: BeverageDispenser[Juice]) -> None: # (3)


"""Install a fruit juice dispenser."""

1. Define covariant=True ao declarar a variável de tipo; _co é o sufixo convencional para parâmetros de tipo
covariantes no typeshed.
2. Usa T_co para parametrizar a classe especial Generic .

3. As dicas de tipo para install são as mesmas do Exemplo 18.

O código abaixo funciona porque tanto a máquina de Juice quanto a de OrangeJuice são válidas em uma
BeverageDispenser covariante:

PY
juice_dispenser = BeverageDispenser(Juice())
install(juice_dispenser)

orange_juice_dispenser = BeverageDispenser(OrangeJuice())
install(orange_juice_dispenser)

mas uma máquina de uma Beverage arbitrária não é aceitável:

PY
beverage_dispenser = BeverageDispenser(Beverage())
install(beverage_dispenser)
## mypy: Argument 1 to "install" has
## incompatible type "BeverageDispenser[Beverage]"
## expected "BeverageDispenser[Juice]"

Isso é uma covariância: a relação de subtipo das máquinas parametrizadas varia na mesma direção da relação de
subtipo dos parâmetros de tipo.
15.7.3. Uma lata de lixo contravariante
Vamos agora modelar a regra da cantina para a instalação de uma lata de lixo. Vamos supor que a comida e a bebida
são servidas em recipientes biodegradáveis, e as sobras e utensílios descartáveis também são biodegradáveis. As latas
de lixo devem ser adequadas para resíduos biodegradáveis.

Neste exemplo didático, vamos fazer algumas suposições e classificar o lixo em uma hierarquia
simplificada:

Refuse (Resíduo) é o tipo mais geral de lixo. Todo lixo é resíduo.

✒️ NOTA Biodegradable (Biodegradável) é um tipo de lixo que é decomposto por microrganismos ao


longo do tempo. Parte do Refuse não é Biodegradable .
Compostable (Compostável) é um tipo específico de lixo Biodegradable que pode ser
transformado de em fertilizante orgânico, em um processo de compostagem. Na nossa definição,
nem todo lixo Biodegradable é Compostable .

Para modelar a regra descrevendo uma lata de lixo aceitável na cantina, precisamos introduzir o conceito de
"contravariância" através de um exemplo, apresentado no Exemplo 20.

Exemplo 20. contravariant.py: definições de tipo e a função install

PY
from typing import TypeVar, Generic

class Refuse: # (1)


"""Any refuse."""

class Biodegradable(Refuse):
"""Biodegradable refuse."""

class Compostable(Biodegradable):
"""Compostable refuse."""

T_contra = TypeVar('T_contra', contravariant=True) # (2)

class TrashCan(Generic[T_contra]): # (3)


def put(self, refuse: T_contra) -> None:
"""Store trash until dumped."""

def deploy(trash_can: TrashCan[Biodegradable]): # (4)


"""Deploy a trash can for biodegradable refuse."""

1. Uma hierarquia de tipos para resíduos: Refuse é o tipo mais geral, Compostable o mais específico.
2. T_contra é o nome convencional para uma variável de tipo contravariante.
3. TrashCan é contravariante ao tipo de resíduo.
4. A função deploy exige uma lata de lixo compatível com TrashCan[Biodegradable] .

Dadas essas definições, os seguintes tipos de lata de lixo são aceitáveis:

PY
bio_can: TrashCan[Biodegradable] = TrashCan()
deploy(bio_can)

trash_can: TrashCan[Refuse] = TrashCan()


deploy(trash_can)
A função aceita uma TrashCan[Refuse] , pois ela pode receber qualquer tipo de resíduo, incluindo
deploy
Biodegradable . Entretanto, uma TrashCan[Compostable] não serve, pois ela não pode receber Biodegradable :

PY
compost_can: TrashCan[Compostable] = TrashCan()
deploy(compost_can)
## mypy: Argument 1 to "deploy" has
## incompatible type "TrashCan[Compostable]"
## expected "TrashCan[Biodegradable]"

Vamos resumir os conceitos vistos até aqui.

15.7.4. Revisão da variância


A variância é uma propriedade sutil. As próximas seções recapitulam o conceito de tipos invariantes, covariantes e
contravariantes, e fornecem algumas regras gerais para pensar sobre eles.

Tipos invariantes
Um tipo genérico L é invariante quando não há nenhuma relação de supertipo ou subtipo entre dois tipos
parametrizados, independente da relação que possa existir entre os parâmetros concretos. Em outras palavras, se L é
invariante, então L[A] não é supertipo ou subtipo de L[B] . Eles são inconsistentes em ambos os sentidos.

Como mencionado, as coleções mutáveis do Python são invariantes por default. O tipo list é um bom exemplo:
list[int] não é consistente-com list[float] , e vice-versa.

Em geral, se um parâmetro de tipo formal aparece em dicas de tipo de argumentos a métodos, e o mesmo parâmetro
aparece nos tipos devolvidos pelo método, aquele parâmetro deve ser invariante, para garantir a segurança de tipo na
atualização e leitura da coleção.

Por exemplo, aqui está parte das dicas de tipo para o tipo embutido list no typeshed (https://fpy.li/15-30):

PY
class list(MutableSequence[_T], Generic[_T]):
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, iterable: Iterable[_T]) -> None: ...
# ... lines omitted ...
def append(self, __object: _T) -> None: ...
def extend(self, __iterable: Iterable[_T]) -> None: ...
def pop(self, __index: int = ...) -> _T: ...
# etc...

Veja que _T aparece entre os argumentos de __init__ , append e extend , e como tipo devolvido por pop . Não há
como tornar segura a tipagem dessa classe se ela for covariante ou contravariante em _T .

Tipos covariantes
Considere dois tipos A e B , onde B é consistente-com A , e nenhum deles é Any . Alguns autores usam os símbolos <:
e :> para indicar relações de tipos como essas:

A :> B

A é um supertipo-de ou igual a B.

B <: A

B é um subtipo-de ou igual a A.

Dado A :> B , um tipo genérico C é covariante quando C[A] :> C[B] .


Observe que a direção da seta no símbolo :> é a mesma nos dois casos em que A está à esquerda de B . Tipos
genéricos covariantes seguem a relação de subtipo do tipo real dos parâmetros.

Contêineres imutáveis podem ser covariantes. Por exemplo, é assim que a classe typing.FrozenSet está
documentada (https://docs.python.org/pt-br/3.10/library/typing.html#typing.FrozenSet) como covariante com uma variável de
tipo usando o nome convencional T_co :

PY
class FrozenSet(frozenset, AbstractSet[T_co]):

Aplicando a notação :> a tipos parametrizados, temos:

float :> int


frozenset[float] :> frozenset[int]

Iteradores são outro exemplo de genéricos covariantes: eles não são coleções apenas para leitura como um
frozenset , mas apenas produzem saídas. Qualquer código que espere um abc.Iterator[float] que produz
números de ponto flutuante pode usar com segurança um abc.Iterator[int] que produz inteiros. Tipos Callable
são covariantes no tipo devolvido por uma razão similar.

Tipos contravariantes
Dado A :> B , um tipo genérico K é contravariante se K[A] <: K[B] .

Tipos genéricos contravariantes revertem a relação de subtipo dos tipos reais dos parâmetros .

A classe TrashCan exemplifica isso:

Refuse :> Biodegradable


TrashCan[Refuse] <: TrashCan[Biodegradable]

Um contêiner contravariante normalmente é uma estrutura de dados só para escrita, também conhecida como
"coletor" ("sink"). Não há exemplos de coleções desse tipo na biblioteca padrão, mas existem alguns tipos com
parâmetros de tipo contravariantes.

Callable[[ParamType, …], ReturnType] é contravariante nos tipos dos parâmetros, mas covariante no
ReturnType , como vimos na seção Seção 8.5.11.1. Além disso, Generator (https://fpy.li/15-32), Coroutine
(https://fpy.li/typecoro), e AsyncGenerator (https://fpy.li/15-33) têm um parâmetro de tipo contravariante. O tipo
Generator está descrito na seção Seção 17.13.3; Coroutine e AsyncGenerator são descritos no Capítulo 21.

Para efeito da presente discussão sobre variância, o ponto principal é que parâmetros formais contravariantes definem
o tipo dos argumentos usados para invocar ou enviar dados para o objeto, enquanto parâmetros formais covariantes
definem os tipos de saídas produzidos pelo objeto—o tipo devolvido por uma função ou produzido por um gerador. Os
significados de "enviar" e "produzir" são explicados na seção Seção 17.13.

Dessas observações sobre saídas covariantes e entradas contravariantes podemos derivar algumas orientações úteis.

Regras gerais de variância


Por fim, aqui estão algumas regras gerais a considerar quando estamos pensando sobre variância:

Se um parâmetro de tipo formal define um tipo para dados que saem de um objeto, ele pode ser covariante.
Se um parâmetro de tipo formal define um tipo para dados que entram em um objeto, ele pode ser contravariante.
Se um parâmetro de tipo formal define um tipo para dados que saem de um objeto e o mesmo parâmetro define um
tipo para dados que entram em um objeto, ele deve ser invariante.
Na dúvida, use parâmetros de tipo formais invariantes. Não haverá prejuízo se no futuro precisar usar parâmetros
de tipo covariantes ou contravariantes, pois nestes casos a tipagem é mais aberta e não quebrará códigos existentes.
Callable[[ParamType, …], ReturnType] demonstra as regras #1 e #2: O ReturnType é covariante, e cada
ParamType é contravariante.

Por default, TypeVar cria parâmetros formais invariantes, e é assim que as coleções mutáveis na biblioteca padrão são
anotadas.

Nossa discussão sobre variância continua na seção Seção 17.13.3.

A seguir, vamos ver como definir protocolos estáticos genéricos, aplicando a ideia de covariância a alguns novos
exemplos.

15.8. Implementando um protocolo estático genérico


A biblioteca padrão do Python 3.10 fornece alguns protocolos estáticos genéricos. Um deles é SupportsAbs ,
implementado assim no módulo typing (https://fpy.li/15-34):

PYTHON3
@runtime_checkable
class SupportsAbs(Protocol[T_co]):
"""An ABC with one abstract method __abs__ that is covariant in its
return type."""
__slots__ = ()

@abstractmethod
def __abs__(self) -> T_co:
pass

T_co é declarado de acordo com a convenção de nomenclatura:

PYTHON3
T_co = TypeVar('T_co', covariant=True)

Graças a SupportsAbs , o Mypy considera válido o seguinte código, como visto no Exemplo 21.

Exemplo 21. abs_demo.py: uso do protocolo genérico SupportsAbs


PYTHON3
import math
from typing import NamedTuple, SupportsAbs

class Vector2d(NamedTuple):
x: float
y: float

def __abs__(self) -> float: # (1)


return math.hypot(self.x, self.y)

def is_unit(v: SupportsAbs[float]) -> bool: # (2)


"""'True' if the magnitude of 'v' is close to 1."""
return math.isclose(abs(v), 1.0) # (3)

assert issubclass(Vector2d, SupportsAbs) # (4)

v0 = Vector2d(0, 1) # (5)
sqrt2 = math.sqrt(2)
v1 = Vector2d(sqrt2 / 2, sqrt2 / 2)
v2 = Vector2d(1, 1)
v3 = complex(.5, math.sqrt(3) / 2)
v4 = 1 # (6)

assert is_unit(v0)
assert is_unit(v1)
assert not is_unit(v2)
assert is_unit(v3)
assert is_unit(v4)

print('OK')

1. Definir __abs__ torna Vector2d consistente-com SupportsAbs .

2. Parametrizar SupportsAbs com float assegura…​


3. …​que o Mypy aceite abs(v) como primeiro argumento para math.isclose .

4. Graças a @runtime_checkable na definição de SupportsAbs , essa é uma asserção válida durante a execução.

5. Todo o restante do código passa pelas verificações do Mypy e pelas asserções durante a execução.
6. O tipo int também é consistente-com SupportsAbs . De acordo com o typeshed (https://fpy.li/15-35), int.__abs__
devolve um int , o que é consistente-com o parametro de tipo float declarado na dica de tipo is_unit para o
argumento v .

De forma similar, podemos escrever uma versão genérica do protocolo RandomPicker , apresentado na seção Exemplo
18, que foi definido com um único método pick devolvendo Any .

O Exemplo 22 mostra como criar um RandomPicker genérico, covariante no tipo devolvido por pick .

Exemplo 22. generic_randompick.py: definição do RandomPicker genérico

PYTHON3
from typing import Protocol, runtime_checkable, TypeVar

T_co = TypeVar('T_co', covariant=True) # (1)

@runtime_checkable
class RandomPicker(Protocol[T_co]): # (2)
def pick(self) -> T_co: ... # (3)
1. Declara T_co como covariante .

2. Isso torna RandomPicker genérico, com um parâmetro de tipo formal covariante.


3. Usa T_co como tipo do valor devolvido.

O protocolo genérico RandomPicker pode ser covariante porque seu único parâmetro formal é usado em um tipo de
saída.

Com isso, podemos dizer que temos um capítulo.

15.9. Resumo do capítulo


Começamos com um exemplo simples de uso de @overload , seguido por um exemplo muito mais complexo, que
estudamos em detalhes: as assinaturas sobrecarregadas exigidas para anotar corretamente a função embutida max .

A seguir veio o artefato especial da linguagem typing.TypedDict . Escolhi tratar dele aqui e não no Capítulo 5, onde
vimos typing.NamedTuple , porque TypedDict não é uma fábrica de classes; ele é meramente uma forma de
acrescentar dicas de tipo a uma variável ou a um argumento que exige um dict com um conjunto específico de
chaves do tipo string, e tipos específicos para cada chave—algo que acontece quando usamos um dict como registro,
muitas vezes no contexto do tratamento de dados JSON. Aquela seção foi um pouco mais longa porque usar
TypedDict pode levar a um falso sentimento de segurança, e queria mostrar como as verificações durante a execução
e o tratamento de erros são inevitáveis quando tentamos criar registros estruturados estaticamente a partir de
mapeamentos, que por natureza são dinâmicos.

Então falamos sobre typing.cast , uma função projetada para nos permitir guiar o trabalho do verificador de tipos. É
importante considerar cuidadosamente quando usar cast , porque seu uso excessivo atrapalha o verificador de tipos.

O acesso a dicas de tipo durante a execução veio em seguida. O ponto principal era usar typing.​get_type_hints em
vez de ler o atributo __annotations__ diretamente. Entretanto, aquela função pode não ser confiável para algumas
anotações, e vimos que os desenvolvedores principais do Python ainda estão discutindo uma forma de tornar as dicas
de tipo usáveis durante a execução, e ao mesmo tempo reduzir seu impacto sobre o uso de CPU e memória.

A última seção foi sobre genéricos, começando com a classe genérica LottoBlower —que mais tarde aprendemos ser
uma classe genérica invariante. Aquele exemplo foi seguido pelas definições de quatro termos básicos: tipo genérico,
parâmetro de tipo formal, tipo parametrizado e parâmetro de tipo real.

Continuamos pelo grande tópico da variância, usando máquinas bebidas para uma cantina e latas de lixo como
exemplos da "vida real" para tipos genéricos invariantes, covariantes e contravariantes. Então revisamos,
formalizamos e aplicamos aqueles conceitos a exemplos na biblioteca padrão do Python.

Por fim, vimos como é definido um protocolo estático genérico, primeiro considerando o protocolo
typing.SupportsAbs , e então aplicando a mesma ideia ao exemplo do RandomPicker , tornando-o mais rigoroso que
o protocolo original do Capítulo 13.

O sistema de tipos do Python é um campo imenso e em rápida evolução. Este capítulo não é

✒️ NOTA abrangente. Escolhi me concentrar em tópicos que são ou amplamente aplicáveis, ou


particularmente complexos ou conceitualmente importantes, e que assim provavelmente se
manterão relevantes por um longo tempo.
15.10. Leitura complementar
O sistema de tipagem estática do Python já era complexo quando foi originalmente projetado, e tem se tornado mais
complexo a cada ano. A Tabela 16 lista todas as PEPs que encontrei até maio de 2021. Seria necessário um livro inteiro
para cobrir tudo.

Tabela 16. PEPs sobre dicas de tipo, com links nos títulos. PEPs com números marcados com * são importantes o
suficiente para serem mencionadas no parágrafo de abertura da documentação de typing
(https://docs.python.org/pt-br/3/library/typing.html). Pontos de interrogação na coluna Python indica PEPs em discussão ou
ainda não implementadas; "n/a" aparece em PEPs informacionais sem relação com uma versão específica do Python.
Todos os textos das PEPs estão em inglês. Dados coletados em maio 2021.
PEP Title Python Year

3107 Function Annotations 3.0 2006


(Anotações de Função)
(https://fpy.li/pep3107)

483* The Theory of Type Hints n/a 2014


(A Teoria das Dicas de
Tipo_) (https://fpy.li/pep483)

484* Type Hints (Dicas de Tipo) 3.5 2014


(https://fpy.li/pep484)

482 Literature Overview for n/a 2015


Type Hints (Revisão da
Literatura sobre Dicas de
Tipo) (https://fpy.li/pep482)

526* Syntax for Variable 3.6 2016


Annotations (Sintaxe para
Anotações de Variáveis)
(https://fpy.li/pep526)

544* Protocols: Structural 3.8 2017


subtyping (static duck
typing) (Protocolos:
subtipagem estrutural
(duck typing estático))
(https://fpy.li/pep544)

557 Data Classes (Classes de 3.7 2017


Dados) (https://fpy.li/pep557)

560 Core support for typing 3.7 2017


module and generic types
(Suporte nativo para
tipagem de módulos e tipos
genéricos)
(https://fpy.li/pep560)
PEP Title Python Year

561 Distributing and 3.7 2017


Packaging Type
Information (Distribuindo
e Empacotando
Informação de Tipo_)
(https://fpy.li/pep561)

563 Postponed Evaluation of 3.7 2017


Annotations (Avaliação
Adiada de Anotações)
(https://fpy.li/pep563)

586* Literal Types (Tipos 3.8 2018


Literais) (https://fpy.li/pep586)

585 Type Hinting Generics In 3.9 2019


Standard Collections
(Dicas de Tipo para
Genéricos nas Coleções
Padrão) (https://fpy.li/pep585)

589* TypedDict: Type Hints for 3.8 2019


Dictionaries with a Fixed
Set of Keys (TypedDict:
Dicas de Tipo para
Dicionários com um
Conjunto Fixo de Chaves)
(https://fpy.li/pep589)

591* Adding a final qualifier to 3.8 2019


typing (Acrescentando um
qualificador final à
tipagem)
(https://fpy.li/pep591)

593 Flexible function and ? 2019


variable annotations
(Anotações flexíveis para
funções e variáveis)
(https://fpy.li/pep593)

604 Allow writing union types 3.10 2019


as X | Y (Permitir a
definição de tipos de união
como X | Y )
(https://fpy.li/pep604)
PEP Title Python Year

612 Parameter Specification 3.10 2019


Variables (Variáveis de
Especificação de
Parâmetros)
(https://fpy.li/pep612)

613 Explicit Type Aliases 3.10 2020


(Aliases de Tipo Explícitos)
(https://fpy.li/pep613)

645 Allow writing optional ? 2020


types as x? (Permitir a
definição de tipos opcionais
como x? )
(https://fpy.li/pep645)

646 Variadic Generics ? 2020


(Genéricos Variádicos)
(https://fpy.li/pep646)

647 User-Defined Type Guards 3.10 2021


(Guardas de Tipos
Definidos pelo Usuário)
(https://fpy.li/pep647)

649 Deferred Evaluation Of ? 2021


Annotations Using
Descriptors (Avaliação
Adiada de Anotações
Usando Descritores)
(https://fpy.li/pep649)

655 Marking individual ? 2021


TypedDict items as
required or potentially-
missing (Marcando itens
TypedDict individuais
como obrigatórios ou
potencialmente ausentes)
(https://fpy.li/pep655)

A documentação oficial do Python mal consegue acompanhar tudo aquilo, então a documentação do Mypy
(https://fpy.li/mypy) (EN) é uma referência essencial. Robust Python (https://fpy.li/15-36) (EN), de Patrick Viafore (O’Reilly), é o
primeiro livro com um tratamento abrangente do sistema de tipagem estática do Python que conheço, publicado em
agosto de 2021. Você pode estar lendo o segundo livro sobre o assunto nesse exato instante.

O sutil tópico da variância tem sua própria seção na PEP 484 (https://fpy.li/15-37) (EN), e também é abordado na página
"Generics" (Genéricos) (https://fpy.li/15-38) (EN) do Mypy, bem como em sua inestimável página "Common Issues"
(Problemas Comuns) (https://fpy.li/15-39).
A PEP 362—Function Signature Object (O Objeto Assinatura de Função) (https://fpy.li/pep362) vale a pena ler se você
pretende usar o módulo inspect , que complementa a função typing.get_type_hints .

Se você estiver interessado na história do Python, pode gostar de saber que Guido van Rossum publicou "Adding
Optional Static Typing to Python" (Acrescentando Tipagem Estática Opcional ao Python) (https://fpy.li/15-40) em 23 de
dezembro de 2004.

"Python 3 Types in the Wild: A Tale of Two Type Systems" (Os Tipos do Python 3 na Natureza: Um Conto de Dois
Sistemas de Tipo) (https://fpy.li/15-41) (EN) é um artigo científico de Ingkarat Rak-amnouykit e outros, do Rensselaer
Polytechnic Institute e do IBM TJ Watson Research Center. O artigo avalia o uso de dicas de tipo em projetos de código
aberto no GitHub, mostrando que a maioria dos projetos não as usam , e também que a maioria dos projetos que
incluem dicas de tipo aparentemente não usam um verificador de tipos. Achei particularmente interessante a
discussão das semânticas diferentes do Mypy e do pytype do Google, onde os autores concluem que eles são
"essencialmente dois sistemas de tipos diferentes."

Dois artigos fundamentais sobre tipagem gradual são "Pluggable Type Systems" (Sistemas de Tipo Conectáveis)
(https://fpy.li/15-42) (EN), de Gilad Bracha, e "Static Typing Where Possible, Dynamic Typing When Needed: The End of the
Cold War Between Programming Languages" (Tipagem Estática Quando Possível, Tipagem Dinâmica Quando Necessário:
O Fim da Guerra Fria Entre Linguagens de Programação) (https://fpy.li/15-43) (EN), de Eric Meijer e Peter Drayton.[200]

Aprendi muito lendo as partes relevantes de alguns livros sobre outras linguagens que implementam algumas das
mesmas ideias:

Atomic Kotlin (https://fpy.li/15-44) (EN), de Bruce Eckel e Svetlana Isakova (Mindview)


Effective Java, 3rd ed., (https://fpy.li/15-45) (EN), de Joshua Bloch (Addison-Wesley)
Programming with Types: TypeScript Examples (https://fpy.li/15-46) (EN), de Vlad Riscutia (Manning)
Programming TypeScript (https://fpy.li/15-47) (EN), de Boris Cherny (O’Reilly)
The Dart Programming Language (https://fpy.li/15-48) (EN) de Gilad Bracha (Addison-Wesley).[201]

Para algumas visões críticas sobre os sistemas de tipagem, recomendo os posts de Victor Youdaiken "Bad ideas in type
theory" (Más ideias na teoria dos tipos) (https://fpy.li/15-49) (EN) e "Types considered harmful II" (Tipos considerados
nocivos II) (https://fpy.li/15-50) (EN).

Por fim, me surpreeendi ao encontrar "Generics Considered Harmful" (Genéricos Considerados Nocivos)
(https://fpy.li/15-51), de Ken Arnold, um desenvolvedor principal do Java desde o início, bem como co-autor das primeiras
quatro edições do livro oficial The Java Programming Language (Addison-Wesley)—junto com James Gosling, o
principal criador do Java.

Infelizmente, as críticas de Arnold também se aplicam ao sistema de tipagem estática do Python. Quando leio as muitas
regras e casos especiais das PEPs de tipagem, sou constantemente lembrado dessa passagem do post de Arnold:

“ Oordem
que nos traz ao problema que sempre cito para o C++: eu a chamo de "exceção de enésima
à regra de exceção". Ela soa assim: "Você pode fazer x, exceto no caso y, a menos que y
faça z, caso em que você pode se…​"

Felizmente, o Python tem uma vantagem crítica sobre o Java e o C++: um sistema de tipagem opcional. Podemos
silenciar os verificadores de tipo e omitir as dicas de tipo quando se tornam muito incômodos.
Ponto de Vista
As tocas de coelho da tipagem

Quando usamos um verificador de tipo, algumas vezes somos obrigados a descobrir e importar classes que não
precisávamos conhecer, e que nosso código não precisa usar—exceto para escrever dicas de tipo. Tais classes não
são documentadas, provavelmente porque são consideradas detalhes de implementação pelos autores dos
pacotes. Aqui estão dois exemplos da biblioteca padrão.

Tive que vasculhar a imensa documentação do asyncio, e depois navegar o código-fonte de vários módulos
daquele pacote para descobrir a classe não-documentada TransportSocket no módulo igualmente não
documentado asyncio.trsock só para usar cast() no exemplo do server.sockets , na seção Seção 15.4.
Usar socket.socket em vez de TransportSocket seria incorreto, pois esse último não é subtipo do primeiro,
como explicitado em uma docstring (https://fpy.li/15-52) (EN) no código-fonte.

Caí em uma toca de coelho similar quando acrescentei dicas de tipo ao Exemplo 13, uma demonstração simples
de multiprocessing . Aquele exemplo usa objetos SimpleQueue , obtidos invocando
multiprocessing.SimpleQueue() . Entretanto, não pude usar aquele nome em uma dica de tipo, porque
multiprocessing.SimpleQueue não é uma classe! É um método vinculado da classe não documentada
multiprocessing.BaseContext , que cria e devolve uma instância da classe SimpleQueue , definida no módulo
não-documentado multiprocessing.queues .

Em cada um desses casos, tive que gastar algumas horas até encontrar a classe não-documentada correta para
importar, só para escrever uma única dica de tipo. Esse tipo de pesquisa é parte do trabalho quando você está
escrevendo um livro. Mas se eu estivesse criando o código para uma aplicação, provavelmente evitaria tais caças
ao tesouro por causa de uma única linha, e simplesmente colocaria # type: ignore . Algumas vezes essa é a
única solução com custo-benefício positivo.

Notação de variância em outras linguagens

A variância é um tópico complicado, e a sintaxe das dicas de tipo do Python não é tão boa quanto poderia ser.
Essa citação direta da PEP 484 evidencia isso:

“ Covariância ou contravariância não são propriedaades de uma variável de tipo, mas sim
uma propriedade da classe genérica definida usando essa variável. [202]

Se esse é o caso, por que a covariância e a contravarância são declaradas com TypeVar e não na classe genérica?

Os autores da PEP 484 trabalharam sob a severa restrição auto-imposta de suportar dicas de tipo sem fazer
qualquer modificação no interpretador. Isso exigiu a introdução de TypeVar para definir variáveis de tipo, e
também levou ao abuso de [] para fornecer a sintaxe Klass[T] para genéricos—em vez da notação Klass<T>
usada em outras linguagens populares, incluindo C#, Java, Kotlin e TypeScript. Nenhuma dessas linguagens exige
que variáveis de tipo seja declaradas antes de serem usadas.

Além disso, a sintaxe do Kotlin e do C# torna claro se um parâmetro de tipo é covariante, contravariante ou
invariante exatamente onde isso faz sentido: na declaração de classe ou interface.

Em Kotlin, poderíamos declarar a BeverageDispenser assim:


KOTLIN
class BeverageDispenser<out T> {
// etc...
}

O modificador no parâmetro de tipo formal significa que


out T é um tipo de output (saída)), e portanto
BeverageDispenser é covariante.

Você provavelmente consegue adivinhar como TrashCan seria declarada:

KOTLIN
class TrashCan<in T> {
// etc...
}

Dado T como um parâmetro de tipo formal de input (entrada), segue que TrashCan é contravariante.

Se nem in nem out aparecem, então a classe é invariante naquele parâmetro.

É fácil lembrar das Seção 15.7.4.4 quando out e in são usado nos parâmetros de tipo formais.

Isso sugere que uma boa convenção para nomenclatura de variáveis de tipo covariante e contravariantes no
Python seria:

PYTHON3
T_out = TypeVar('T_out', covariant=True)
T_in = TypeVar('T_in', contravariant=True)

Aí poderíamos definir as classes assim:

PYTHON3
class BeverageDispenser(Generic[T_out]):
...

class TrashCan(Generic[T_in]):
...

Será que é tarde demais para modificar a convenção de nomenclatura definida na PEP 484?
16. Sobrecarga de operadores
“ Existem algumas coisas que me deixam meio dividido, como a sobrecarga de operadores. Deixei
a sobrecarga de operadores de fora em uma decisão bastante pessoal, pois tinha visto gente
demais abusar [desse recurso] no C++.[203]
— James Gosling
Criador do Java

Em Python, podemos calcular juros compostos usando uma fórmula escrita assim:

PYTHON3
interest = principal * ((1 + rate) ** periods - 1)

Operadores que aparecem entre operandos, como em 1 + rate , são operadores infixos. No Python, operadores infixos
podem lidar com qualquer tipo arbitrário. Assim, se você está trabalhando com dinheiro real, pode se assegurar que
principal , rate , e periods sejam números exatos—instâncias da classe decimal.Decimal do Python—e a
fórmula vai funcionar como está escrita, produzindo um resultado exato.

Mas em Java, se você mudar de float para BigDecimal , para obter resultados exatos, não é mais possível usar
operadores infixos, porque naquela linguagem eles só funcionam com tipos primitivos. Abaixo vemos a mesma
fórmula escrita em Java para funcionar com números BigDecimal :

JAVA
BigDecimal interest = principal.multiply(BigDecimal.ONE.add(rate)
.pow(periods).subtract(BigDecimal.ONE));

Está claro que operadores infixos tornam as fórmulas mais legíveis. A sobrecarga de operadores é necessária para
suportar a notação infixa de operadores com tipos definidos pelo usuário ou estendidos, tal como os arrays do NumPy.
Oferecer a sobrecarga de operadores em uma linguagem de alto nível e fácil de usar foi provavelmente uma das
principais razões do imenso sucesso do Python na ciência de dados, incluido as aplicações financeiras e científicas.

Na seção Seção 1.3.1 (do Capítulo 1) vimos algumas implementações triviais de operadores em uma classe básica
Vector . Os métodos __add__ e __mul__ no Exemplo 2 foram escritos para demonstrar como os métodos especiais
suportam a sobrecarga de operadores, mas deixamos passar problemas sutis naquelas implementações. Além disso, no
Exemplo 2 notamos que o método Vector2d.__eq__ considera True a seguinte expressão: Vector(3, 4) == [3, 4]
—algo que pode ou não fazer sentido. Nesse capítulo vamos cuidar desses problemas, e falaremos também de:

Como um método de operador infixo deveria indicar que não consegue tratar um operando
O uso de duck typing ou goose typing para lidar com operandos de vários tipos
O comportamento especial dos operadores de comparação cheia (e.g., == , > , ⇐ , etc.)

O tratamento default de operadores de atribuição aumentada tal como += , e como sobrecarregá-los

16.1. Novidades nesse capítulo


O goose typing é uma parte fundamental do Python, mas as ABCs numbers não são suportadas na tipagem estática.
Então modifiquei o Exemplo 11 para usar duck typing, em vez de uma verificação explícita usando isinstance contra
numbers.Real .[204]
Na primeira edição do Python Fluente, tratei do operador de multiplicação de matrizes @ como uma mudança futura,
quando o Python 3.5 ainda estava em sua versão alfa. Agora o @ está integrado ao fluxo do capítulo na seção Seção
16.6. Aproveitei o goose typing para tornar a implementação de __matmul__ aqui mais segura que a da primeira
edição, sem comprometer sua flexibilidade.

A seção Seção 16.11 agora inclui algumas novas referências—incluindo um post de blog de Guido van Rossum. Também
adicionei menções a duas bibliotecas que demonstram um uso efetivo da sobrecarga de operadores fora do domínio da
matemática: pathlib e Scapy .

16.2. Introdução à sobrecarga de operadores


A sobrecarga de operadores permite que objetos definidos pelo usuário interoperem com operadores infixos tais como
+ e | , ou com operadores unários como - e ~ . No Python, de uma perspectiva mais geral, a invocação de funções
( () ), o acesso a atributos ( . ) e o acesso a itens e o fatiamento ( [] ) também são operadores, mas este capítulo trata
dos operadores unários e infixos.

A sobrecarga de operadores tem má-fama em certos círculos. É um recurso da linguagem que pode ser (e tem sido)
abusado, resultando em programadores confusos, bugs, e gargalos de desempenho inesperados. Mas se bem utilizado,
ele gera APIs agradáveis de usar e código legível. O Python alcança um bom equilíbrio entre flexibilidade, usabilidade e
segurança, pela imposição de algumas limitações:

Não é permitido modificar o significado dos operadores para os tipos embutidos.


Não é permitido criar novos operadores, apenas sobrecarregar os existentes.
Alguns poucos operadores não podem ser sobrecarregados: is , and , or e not (mas os operadores binários &, |,
e ~ podem).

No Capítulo 12, na classe Vector , já apresentamos um operador infixo: == , suportado pelo método __eq__ . Nesse
capítulo, vamos melhorar a implementação de __eq__ para lidar melhor com operandos de outros tipos além de
Vector . Entretanto, os operadores de comparação cheia ( == , != , > , < , >= , ⇐ ) são casos especiais de sobrecarga de
operadores, então começaremos sobrecarregando quatro operadores aritméticos em Vector : os operadores unários
- e + , seguido pelos infixos + e * .

Vamos começar pelo tópico mais fácil: operadores unários.

16.3. Operadores unários


A seção "6.5. Unary arithmetic and bitwise operations" (Aritmética unária e operações binárias)
(https://docs.python.org/pt-br/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations) (EN), de A Referência da
Linguagem Python, elenca três operações unárias, listadas abaixo juntamente com seus métodos especiais associados:

- , implementado por __neg__

Negativo aritmético unário. Se x é -2 então -x == 2 .

+ , implementado por __pos__

Positivo aritmético unário. De forma geral, x == +x , mas há alguns poucos casos onde isso não é verdadeiro. Veja a
seção Quando x e +x não são iguais, se estiver curioso.

~ , implementado por __invert__

Negação binária, ou inversão binária de um inteiro, definida como ~x == -(x+1) . Se x é 2 então ~x == -3 .[205]
O capítulo "Modelo de Dados" (https://docs.python.org/pt-br/3/reference/datamodel.html#object.__neg__) de A Referência da
Linguagem Python também inclui a função embutida abs() como um operador unário. O método especial associado é
__abs__ , como já vimos.

É fácil suportar operadores unários. Basta implementar o método especial apropriado, que receberá apenas um
argumento: self . Use a lógica que fizer sentido na sua classe, mas se atenha à regra geral dos operadores: sempre
devolva um novo objeto. Em outras palavras, não modifique o destinatário ( self ), crie e devolva uma nova instância
do tipo adequado.

No caso de - e + , o resultado será provavelmente uma instância da mesma classe de self . Para o + unário, se o
destinatário for imutável você deveria devolver self ; caso contrário, devolva uma cópia de self . Para abs() , o
resultado deve ser um número escalar.

Já no caso de ~ , é difícil determinar o que seria um resultado razoável se você não estiver lidando com bits de um
número inteiro. No pacote de análise de dados pandas (https://fpy.li/pandas), o til nega condições booleanas de filtragem;
veja exemplos na documentação do pandas, em "Boolean indexing" (_Indexação booleana) (https://fpy.li/16-4) (EN).

Como prometido acima, vamos implementar vários novos operadores na classe Vector , do Capítulo 12. O Exemplo 1
mostra o método __abs__ , que já estava no Exemplo 16, e os novos métodos __neg__ e __pos__ para operadores
unários.

Exemplo 1. vector_v6.py: unary operators - and + added to Exemplo 16

PY
def __abs__(self):
return math.hypot(*self)

def __neg__(self):
return Vector(-x for x in self) # (1)

def __pos__(self):
return Vector(self) # (2)

1. Para computar -v , cria um novo Vector com a negação de cada componente de self .

2. Para computar +v , cria um novo Vector com cada componente de self .

Lembre-se que instâncias de Vector são iteráveis, e o Vector.__init__ recebe um argumento iterável, e daí as
implementações de __neg__ e __pos__ são curtas e rápidas.

Não vamos implementar __invert__ . Se um usuário tentar escrever ~v para uma instância de Vector , o Python vai
gerar um TypeError com uma mensagem clara: “bad operand type for unary ~: 'Vector' ” (_operando inválido para
o ~ unário: 'Vector' ).

O quadro a seguir trata de uma curiosidade que algum dia poderá ajudar você a ganhar uma aposta sobre o + unário .

Quando x e +x não são iguais


Todo mumdo espera que x == +x , e isso é verdade no Python quase todo o tempo, mas encontrei dois casos na
biblioteca padrão onde x != +x .

O primeiro caso envolve a classe decimal.Decimal . Você pode obter x != +x se x é uma instância de
Decimal , criada em um dado contexto aritmético e +x for então avaliada em um contexto com definições
diferentes. Por exemplo, x é calculado em um contexto com uma determinada precisão, mas a precisão do
contexto é modificada e daí +x é avaliado. Veja a uma demonstração no Exemplo 2.
Exemplo 2. Uma mudança na precisão do contexto aritmético pode fazer x se tornar diferente de +x

PY
>>> import decimal
>>> ctx = decimal.getcontext() # (1)
>>> ctx.prec = 40 # (2)
>>> one_third = decimal.Decimal('1') / decimal.Decimal('3') # (3)
>>> one_third # (4)
Decimal('0.3333333333333333333333333333333333333333')
>>> one_third == +one_third # (5)
True
>>> ctx.prec = 28 # (6)
>>> one_third == +one_third # (7)
False
>>> +one_third # (8)
Decimal('0.3333333333333333333333333333')

1. Obtém uma referência ao contexto aritmético global atual.


2. Define a precisão do contexto aritmético em 40 .

3. Computa 1/3 usando a precisão atual.


4. Inspeciona o resultado; há 40 dígitos após o ponto decimal.
5. one_third == +one_third é True .

6. Diminui a precisão para 28 —a precisão default para aritmética com Decimal .

7. Agora one_third == +one_third é False .

8. Inspeciona +one_third ; aqui há 28 dígitos após o '.' .

O fato é que cada ocorrência da expressão +one_third produz uma nova instância de Decimal a partir do
valor de one_third , mas usando a precisão do contexto aritmético atual.

Podemos encontrar o segundo caso onde x != +x na documentação


(https://docs.python.org/pt-br/3/library/collections.html#collections.Counter) de collections.Counter . A classe Counter
implementa vários operadores aritméticos, incluindo o + infixo, para somar a contagem de duas instâncias de
Counter . Entretanto, por razões práticas, a adição em Counter descarta do resultado qualquer item com
contagem negativa ou zero. E o prefixo + é um atalho para somar um Counter vazio, e portanto produz um
novo Counter , preservando apenas as contagens maiores que zero. Veja o Exemplo 3.

Exemplo 3. O + unário produz um novo `Counter`sem as contagens negativas ou zero

PYCON
>>> ct = Counter('abracadabra')
>>> ct
Counter({'a': 5, 'r': 2, 'b': 2, 'd': 1, 'c': 1})
>>> ct['r'] = -3
>>> ct['d'] = 0
>>> ct
Counter({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})
>>> +ct
Counter({'a': 5, 'b': 2, 'c': 1})

Como se vê, +ct devolve um contador onde todas as contagens são maiores que zero.

Agora voltamos à nossa programação normal.


16.4. Sobrecarregando + para adição de Vector
A classe Vector é um tipo sequência, e a seção "3.3.7. Emulando de tipos contêineres"
(https://docs.python.org/pt-br/3/reference/datamodel.html#emulating-container-types) do capítulo "Modelo de Dados", na
documentação oficial do Python, diz que sequências devem suportar o operador + para concatenação e o * para
repetição. Entretanto, aqui vamos implementar + e * como operações matemáticas de vetores, algo um pouco mais
complicado mas mais significativo para um tipo Vector .

Usuários que desejem concatenar ou repetir instâncias de Vector podem convertê-las para tuplas
ou listas, aplicar o operador e convertê-las de volta—graças ao fato de Vector ser iterável e poder
ser criado a partir de um iterável:
👉 DICA PYCON
>>> v_concatenated = Vector(list(v1) + list(v2))
>>> v_repeated = Vector(tuple(v1) * 5)

Somar dois vetores euclidianos resulta em um novo vetor no qual os componentes são as somas pareadas dos
componentes dos operandos. Ilustrando:

PYCON
>>> v1 = Vector([3, 4, 5])
>>> v2 = Vector([6, 7, 8])
>>> v1 + v2
Vector([9.0, 11.0, 13.0])
>>> v1 + v2 == Vector([3 + 6, 4 + 7, 5 + 8])
True

E o que acontece se tentarmos somar duas instâncias de Vector de tamanhos diferentes? Poderíamos gerar um erro,
mas considerando as aplicações práticas (tal como recuperação de informação), é melhor preencher o Vector menor
com zeros. Esse é o resultado que queremos:

PYCON
>>> v1 = Vector([3, 4, 5, 6])
>>> v3 = Vector([1, 2])
>>> v1 + v3
Vector([4.0, 6.0, 5.0, 6.0])

Dados esses requerimentos básicos, podemos implementar __add__ como no Exemplo 4.

Exemplo 4. Método Vector.__add__ , versão #1

PYTHON3
# inside the Vector class

def __add__(self, other):


pairs = itertools.zip_longest(self, other, fillvalue=0.0) # (1)
return Vector(a + b for a, b in pairs) # (2)

1. pairs é um gerador que produz tuplas (a, b) , onde a vem de self e b de other . Se self e other tiverem
tamanhos diferentes, fillvalue fornece os valores ausentes para o iterável mais curto.
2. Um novo Vector é criado a partir de uma expressão geradora, produzindo uma soma para cada (a, b) de
pairs .
Observe como __add__ devolve uma nova instância de Vector , sem modificar self ou other .

Métodos especiais implementando operadores unários ou infixos não devem nunca modificar o
⚠️ AVISO valor dos operandos. Se espera que expressões com tais operandos produzam resultados criando
novos objetos. Apenas operadores de atribuição aumentada podem modidifcar o primeiro operando
( self ), como discutido na seção Seção 16.9.

O Exemplo 4 permite somar um Vector a um Vector2d , e Vector a uma tupla ou qualquer iterável que produza
números, como prova o Exemplo 5.

Exemplo 5. Nossa versão #1 de Vector.__add__ também aceita objetos diferentes de Vector

PYCON
>>> v1 = Vector([3, 4, 5])
>>> v1 + (10, 20, 30)
Vector([13.0, 24.0, 35.0])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> v1 + v2d
Vector([4.0, 6.0, 5.0])

Os dois usos de + no Exemplo 5 funcionam porque __add__ usa zip_longest(…) , capaz de consumir qualquer
iterável, e a expressão geradora que cria um novo Vector simplemente efetua a operação a + b com os pares
produzidos por zip_longest(…) , então um iterável que produza quaisquer itens numéricos servirá.

Entretanto, se trocarmos a ordem dos operandos (no Exemplo 6), a soma de tipos diferentes falha.

Exemplo 6. A versão #1 de Vector.__add__ falha com se o operador da esquerda não for um `Vector

PYCON
>>> v1 = Vector([3, 4, 5])
>>> (10, 20, 30) + v1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "Vector") to tuple
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> v2d + v1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Vector2d' and 'Vector'

Para suportar operações envolvendo objetos de tipos diferentes, o Python implementa um mecanismo especial de
despacho para os métodos especiais de operadores infixos. Dada a expressão a + b , o interpretador vai executar as
seguintes etapas (veja também a Figura 1):

1. Se a implementa __add__ , invoca a.__add__(b) e devolve o resultado, a menos que seja NotImplemented .

2. Se a não implementa __add__ , ou a chamada devolve NotImplemented , verifica se b implementa __radd__ , e


então invoca b.__radd__(a) e devolve o resultado, a menos que seja NotImplemented .
3. Se b não implementa __radd__ , ou a chamada devolve NotImplemented , gera um TypeError com a mensagem
'unsupported operand types' (tipos de operandos não suportados).
👉 DICA O método __radd__ é chamado de variante "reversa" ou "refletida" de
geral "métodos especiais reversos".[206]
__add__ . Adotei o termo

Figura 1. Fluxograma para computar a + b com __add__ e __radd__ .

Assim, para fazer as somas de tipos diferentes no Exemplo 6 funcionarem, precisamos implementar o método
Vector.__radd__ , que o Python vai invocar como alternativa, se o operando à esquerda não implementar __add__ ,
ou se implementar mas devolver NotImplemented , indicando que não sabe como tratar o operando à direita.

Não confunda NotImplemented com NotImplementedError . O primeiro é um valor singleton


especial, que um método especial de operador infixo deve devolver para informar o interpretador
⚠️ AVISO que não consegue tratar um dado operando. NotImplementedError , por outro lado, é um exceção
que métodos stub em classes abstratas podem gerar, para avisar que subclasses devem implementar
tais métodos.

A implementação viável mais simples de __radd__ aparece no Exemplo 7.

Exemplo 7. Os métodos __add__ e __radd__ de Vector

PYTHON3
# inside the Vector class

def __add__(self, other): # (1)


pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)

def __radd__(self, other): # (2)


return self + other

1. Nenhuma mudança no __add__ do Exemplo 4; ele é listado aqui porque é usado por __radd__ .
2. __radd__ apenas delega para __add__ .
Muitas vezes, __radd__ pode ser simples assim: apenas a invocação do operador apropriado, delegando para
__add__ neste caso. Isso se aplica para qualquer operador comutativo; + é comutativo quando lida com números ou
com nossos vetores, mas não é comutativo ao concatenar sequências no Python.

Se __radd__ apenas invoca __add__ , aqui está outra forma de obter o mesmo efeito:

PYTHON3
def __add__(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)

__radd__ = __add__

Os métodos no Exemplo 7 funcionam com objetos Vector ou com qualquer iterável com itens numéricos, tal como um
Vector2d , uma tuple de inteiros ou um array de números de ponto flutuante. Mas se alimentado com um objeto
não-iterável, __add__ gera uma exceção com uma mensagem não muito útil, como no Exemplo 8.

Exemplo 8. O método Vector.__add__ precisa de operandos iteráveis

PYCON
>>> v1 + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "vector_v6.py", line 328, in __add__
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
TypeError: zip_longest argument #2 must support iteration

E pior ainda, recebemos uma mensagem enganosa se um operando for iterável mas seus itens não puderem ser
somados aos itens float no Vector . Veja o Exemplo 9.

Exemplo 9. O método Vector.__add__ precisa de um iterável com itens numéricos

PYCON
>>> v1 + 'ABC'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "vector_v6.py", line 329, in __add__
return Vector(a + b for a, b in pairs)
File "vector_v6.py", line 243, in __init__
self._components = array(self.typecode, components)
File "vector_v6.py", line 329, in <genexpr>
return Vector(a + b for a, b in pairs)
TypeError: unsupported operand type(s) for +: 'float' and 'str'

Tentei somar um Vector a uma str , mas a mensagem reclama de float e str .

Na verdade, os problemas no Exemplo 8 e no Exemplo 9 são mais profundos que meras mensagens de erro obscuras: se
um método especial de operando não é capaz de devolver um resultado válido por incompatibilidade de tipos, ele
deverua devolver NotImplemented e não gerar um TypeError . Ao devolver NotImplemented , a porta fica aberta
para a implementação do operando do outro tipo executar a operação, quando o Python tentar invocar o método
reverso.

No espírito do duck typing, vamos nos abster de testar o tipo do operando other ou o tipo de seus elementos. Vamos
capturar as exceções e devolver NotImplemented . Se o interpretador ainda não tiver invertido os operandos, tentará
isso agora. Se a invocação do método reverso devolver NotImplemented , então o Python irá gerar um TypeError com
uma mensagem de erro padrão "unsupported operand type(s) for +: 'Vector' and 'str'” (tipos de operandos não
suportados para +: Vector e `str`)
A implementação final dos métodos especiais de adição de Vector está no Exemplo 10.

Exemplo 10. vector_v6.py: métodos do operador + adicionados a vector_v5.py (no Exemplo 16)

PY
def __add__(self, other):
try:
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
return Vector(a + b for a, b in pairs)
except TypeError:
return NotImplemented

def __radd__(self, other):


return self + other

Observe que agora __add__ captura um TypeError e devolve NotImplemented .

Se um método de operador infixo gera uma exceção, ele interrompe o algoritmo de despacho do
operador. No caso específico de TypeError , geralmente é melhor capturar essa exceção e devolver
⚠️ AVISO NotImplemented . Isso permite que o interpretador tente chamar o método reverso do operador,
que pode tratar corretamente a operação com operadores invertidos, se eles forem de tipos
diferentes.

Agora que já sobrecarregamos o operador + com segurança, implementando __add__ e __radd__ , vamos enfrentar
outro operador infixo: * .

16.5. Sobrecarregando * para multiplicação escalar


O que significa Vector([1, 2, 3]) * x ? Se x é um número, isso seria um produto escalar, e o resultado seria um
novo Vector com cada componente multiplicado por x —também conhecida como multiplicação elemento a
elemento (elementwise multiplication):

PYCON
>>> v1 = Vector([1, 2, 3])
>>> v1 * 10
Vector([10.0, 20.0, 30.0])
>>> 11 * v1
Vector([11.0, 22.0, 33.0])

Outro tipo de produto envolvendo operandos de Vector seria o dot product (produto vetorial) de

✒️ NOTA dois vetores—ou multiplicação de matrizes, se tomarmos um vetor como uma matriz de 1 × N e o
outro como uma matriz de N × 1. Vamos implementar esse operador em nossa classe Vector na
seção Seção 16.6.

De volta a nosso produto escalar, começamos novamente com os métodos __mul__ e __rmul__ mais simples possíveis
que possam funcionar:
PYTHON3
# inside the Vector class

def __mul__(self, scalar):


return Vector(n * scalar for n in self)

def __rmul__(self, scalar):


return self * scalar

Esses métodos funcionam, exceto quando recebem operandos incompatíveis. O argumento scalar precisa ser um
número que, quando multiplicado por um float , produz outro float (porque nossa classe Vector usa,
internamente, um array de números de ponto flutuante). Então um número complex não serve, mas o escalar pode
ser um int , um bool (porque bool é subclasse de int ) ou mesmo uma instância de fractions.Fraction . No
Exemplo 11, o método __mul__ não faz qualquer verificação de tipo explícita com scalar . Em vez disso, o converte
em um float , e devolve NotImplemented se a conversão falha. Esse é um exemplo claro de duck typing.

Exemplo 11. vector_v7.py: métodos do operador * adicionados

PYTHON3
class Vector:
typecode = 'd'

def __init__(self, components):


self._components = array(self.typecode, components)

# many methods omitted in book listing, see vector_v7.py


# in https://github.com/fluentpython/example-code-2e

def __mul__(self, scalar):


try:
factor = float(scalar)
except TypeError: # (1)
return NotImplemented # (2)
return Vector(n * factor for n in self)

def __rmul__(self, scalar):


return self * scalar # (3)

1. Se scalar não pode ser convertido para float …​

2. …​não temos como lidar com ele, então devolvemos NotImplemented , para permitir ao Python tentar __rmul__ no
operando scalar .
3. Neste exemplo, __rmul__ funciona bem apenas executando self * scalar , que delega a operação para o
método __mul__ .

Com o Exemplo 11, é possível multiplicar um Vector por valores escalares de tipos numéricos comuns e não tão
comuns:

PYCON
>>> v1 = Vector([1.0, 2.0, 3.0])
>>> 14 * v1
Vector([14.0, 28.0, 42.0])
>>> v1 * True
Vector([1.0, 2.0, 3.0])
>>> from fractions import Fraction
>>> v1 * Fraction(1, 3)
Vector([0.3333333333333333, 0.6666666666666666, 1.0])
Agora que podemos multiplicar Vector por valores escalares, vamos ver como implementar o produto de um Vector
por outro Vector .

Na primeira edição de Python Fluente, usei goose typing no Exemplo 11: verificava o argumento
scalar de __mul__ com isinstance(scalar, numbers.Real) . Agora eu evito usar as ABCs de
numbers , por não serem suportadas pelas anotações de tipo introduzidas na PEP 484. Usar durante
a execução tipos que não podem ser também verificados de forma estática me parece uma má ideia.

✒️ NOTA Outra alternativa seria verificar com o protocolo typing.SupportsFloat , que vimos na seção
Seção 13.6.2. Escolhi usar duck typing naquele exemplo por achar que pythonistas fluentes devem se
sentir confortáveis com esse padrão de programação.

Mas __matmul__ , no Exemplo 12, que é novo e foi escrito para essa segunda edição, é um bom
exemplo de goose typing.

16.6. Usando @ como operador infixo


O símbolo @ é bastante conhecido como o prefixo de decoradores de função, mas desde 2015 ele também pode ser
usado como um operador infixo. Por anos, o produto escalar no NumPy foi escrito como numpy.dot(a, b) . A notação
de invocação de função faz com que fórmulas mais longas sejam difíceis de traduzir da notação matemática para o
Python,[207] então a comunidade de computação numérica fez campanha pela PEP 465—A dedicated infix operator for
matrix multiplication (Um operador infixo dedicado para multiplicação de matrizes) (https://fpy.li/pep465) (EN), que foi
implementada no Python 3.5. Hoje é possível escrever a @ b para computar o produto escalar de dois arrays do
NumPy.

O operador @ é suportado pelos métodos especiais __matmul__ , __rmatmul__ e __imatmul__ , cujos nomes derivam
de "matrix multiplication" (multiplicação de matrizes). Até o Python 3.10, esses métodos não são usados em lugar algum
na biblioteca padrão, mas eles são reconhecidos pelo interpretador desde o Python 3.5, então os desenvolvedores do
NumPy—​e o resto de nós—​podemos implementar o operador @ em nossas classes. O analisador sintático do Python
também foi modificado para aceitar o novo operador (no Python 3.4, a @ b era um erro de sintaxe).

Os testes simples abaixo mostram como @ deve funcionar com instâncias de Vector :

PYCON
>>> va = Vector([1, 2, 3])
>>> vz = Vector([5, 6, 7])
>>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7
True
>>> [10, 20, 30] @ vz
380.0
>>> va @ 3
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for @: 'Vector' and 'int'

O resultado de va @ vz no exemplo acima é o mesmo que obtemos no NumPy fazendo o produto escalar de arrays
com os mesmos valores:

PYCON
>>> import numpy as np
>>> np.array([1, 2, 3]) @ np.array([5, 6, 7])
38

O Exemplo 12 mostra o código dos métodos especiais relevantes na classe Vector .


Exemplo 12. vector_v7.py: operator @ methods

PYTHON3
class Vector:
# many methods omitted in book listing

def __matmul__(self, other):


if (isinstance(other, abc.Sized) and # (1)
isinstance(other, abc.Iterable)):
if len(self) == len(other): # (2)
return sum(a * b for a, b in zip(self, other)) # (3)
else:
raise ValueError('@ requires vectors of equal length.')
else:
return NotImplemented

def __rmatmul__(self, other):


return self @ other

1. Ambos os operandos precisam implementar __len__ e __iter__ …​

2. …​e ter o mesmo tamanho, para permitir…​


3. …​uma linda aplicação de sum , zip e uma expressão geradora.

O novo recurso de zip() no Python 3.10


Desde o Python 3.10, a função embutida zip aceita um argumento opcional apenas nomeado,
👉 DICA strict . Quando strict=True , a função gera um ValueError se os iteráveis tem tamanhos
diferentes. O default é False . Esse novo comportamento estrito se alinha à filosofia de falhar rápido
(https://fpy.li/16-8) do Python. No Exemplo 12, substituí o if interno por um try/except
ValueError e acrescentei strict=True à invocação de zip .

O Exemplo 12 é um bom exemplo prático de goose typing. Testar o operando other contra Vector negaria aos
usuários a flexibilidade de usar listas ou arrays como operandos de @ . Desde que um dos operandos seja um Vector ,
nossa implementação de @ suporta outros operandos que sejam instâncias de abc.Sized e abc.Iterable . Ambas as
ABCs implementam o __subclasshook__ , portanto qualquer objeto que forneça __len__ e __iter__ satisfaz nosso
teste—não há necessidade de criar subclasses concretas dessas ABCs ou sequer registrar-se com elas, como explicado
na seção Seção 13.5.8. Em especial, nossa classe Vector não é subclasse nem de abc.Sized nem de abc.Iterable ,
mas passa os testes de isinstance contra aquelas ABCs, pois implementa os métodos necessários.

Vamos revisar os operadores aritméticos suportados pelo Python antes de mergulhar na categoria especial dos Seção
16.8.

16.7. Resumindo os operadores aritméticos


Ao implementar + , * , e @ , vimos os padrões de programação mais comuns para operadores infixos. As técnicas
descritas são aplicáveis a todos os operadores listados na Tabela 17 (os operadores "no mesmo lugar" serão tratados em
Seção 16.9).

Tabela 17. Nomes dos métodos de operadores infixos (os operadores "no mesmo lugar" são usados para atribuição
aumentada; operadores de comparação estão na Tabela 18)
Operador Direto Reverso No mesmo lugar Descrição

+ __add__ __radd__ __iadd__ Adição ou


concatenação
Operador Direto Reverso No mesmo lugar Descrição

- __sub__ __rsub__ __isub__ Subtração

* __mul__ __rmul__ __imul__ Multiplicação ou


repetição

/ __truediv__ __rtruediv__ __itruediv__ Divisão exata (True


division)

// __floordiv__ __rfloordiv__ __ifloordiv__ Divisão inteira


(Floor division)

% __mod__ __rmod__ __imod__ Módulo

divmod() __divmod__ __rdivmod__ __idivmod__ Devolve uma tupla


com o quociente da
divisão inteira e o
módulo

** , pow() __pow__ __rpow__ __ipow__ Exponenciação[208]

@ __matmul__ __rmatmul__ __imatmul__ Multiplicação de


matrizes

& __and__ __rand__ __iand__ E binário (bit a bit)

| __or__ __ror__ __ior__ OU binário (bit a bit)

^ __xor__ __rxor__ __ixor__ XOR binário (bit a


bit)

<< __lshift__ __rlshift__ __ilshift__ Deslocamento de


bits para a esquerda

>> __rshift__ __rrshift__ __irshift__ Deslocamento de


bits para a direita

Operadores de comparação cheia usam um conjunto diferente de regras.

16.8. Operadores de comparação cheia


O tratamento dos operadores de comparação cheia == , != , > , < , >= e ⇐ pelo interpretador Python é similar ao
que já vimos, com duas importantes diferenças:

O mesmo conjunto de métodos é usado para invocações diretas ou reversas do operador. As regras estão resumidas
na Tabela 18. Por exemplo, no caso de == , tanto a chamada direta quanto a reversa invocam __eq__ , apenas
permutando os argumentos; e uma chamada direta a __gt__ é seguida de uma chamada reversa a __lt__ , com
os argumentos permutados.
Nos casos de == e != , se o métodos reverso estiver ausente, ou devolver NotImplemented , o Python vai comparar
os IDs dos objetos em vez de gerar um TypeError .
Tabela 18. Operadores de comparação cheia: métodos reversos invocados quando a chamada inicial ao método devolve
NotImplemented

Grupo Operador infixo Método de Método de Alternativa


invocação direta invocação reversa

Igualdade a == b a.__eq__(b) b.__eq__(a) Devolve id(a) ==


id(b)

a != b a.__ne__(b) b.__ne__(a) Devolve not (a ==


b)

Ordenação a > b a.__gt__(b) b.__lt__(a) Gera um TypeError

a < b a.__lt__(b) b.__gt__(a) Gera um TypeError

a >= b a.__ge__(b) b.__le__(a) Gera um TypeError

a ⇐b a.__le__(b) b.__ge__(a) Gera um TypeError

Dadas essas regras, vamos revisar e aperfeiçoar o comportamento do método Vector.__eq__ , que foi escrito assim no
vector_v5.py (Exemplo 16):

PYTHON3
class Vector:
# many lines omitted

def __eq__(self, other):


return (len(self) == len(other) and
all(a == b for a, b in zip(self, other)))

Eaae método produz os resultados do Exemplo 13.

Exemplo 13. Comparando um Vector a um Vector , a um Vector2d , e a uma tuple

PYCON
>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb # (1)
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d # (2)
True
>>> t3 = (1, 2, 3)
>>> va == t3 # (3)
True

1. Duas instâncias de Vector com componentes numéricos iguais são iguais.


2. Um Vector e um Vector2d também são iguais se seus componentes são iguais.
3. Um Vector também é considerado igual a uma tuple ou qualquer iterável com itens numéricos de valor igual.

O resultado no Exemplo 13 é provavelmente indesejável. Queremos mesmo que um Vector seja considerado igual a
uma tuple contendo os mesmos números? Não tenho uma regra fixa sobre isso; depende do contexto da aplicação. O
"Zen of Python" diz:


“ Em face da ambiguidade, rejeite a tentação de adivinhar.
Liberalidade excessiva na avaliação de operandos pode levar a resultados surpreendentes, e programadores odeiam
surpresas.

Buscando inspiração no próprio Python, vemos que [1,2] == (1, 2) é False . Então, vamos ser conservadores e
executar alguma verificação de tipos. Se o segundo operando for uma instância de Vector (ou uma instância de uma
subclasse de Vector ), então usaremos a mesma lógica do __eq__ atual. Caso contrário, devolvemos
NotImplemented e deixamos o Python cuidar do caso. Veja o Exemplo 14.

Exemplo 14. vector_v8.py: __eq__ aperfeiçoado na classe Vector

PY
def __eq__(self, other):
if isinstance(other, Vector): # (1)
return (len(self) == len(other) and
all(a == b for a, b in zip(self, other)))
else:
return NotImplemented # (2)

1. Se o operando other é uma instância de Vector (ou de uma subclasse de Vector ), executa a comparação como
antes.
2. Caso contrário, devolve NotImplemented .

Rodando os testes do Exemplo 13 com o novo Vector.__eq__ do Exemplo 14, obtemos os resultados que aparecem no
Exemplo 15.

Exemplo 15. Mesmas comparações do Exemplo 13: o último resultado mudou

PYCON
>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb # (1)
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d # (2)
True
>>> t3 = (1, 2, 3)
>>> va == t3 # (3)
False

1. Mesmo resultado de antes, como esperaado.


2. Mesmo resultado de antes, mas por que? Explicação a seguir.
3. Resultado diferente; era o que queríamos. Mas por que isso funciona? Continue lendo…​

Dos três resultados no Exemplo 15, o primeiro não é novidade, mas os dois últimos foram causados por __eq__
devolver NotImplemented no Exemplo 14. Eis o que acontece no exemplo com um Vector e um Vector2d , vc ==
v2d , passo a passo:

1. Para avaliar vc == v2d , o Python invoca Vector.eq(vc, v2d) .

2. Vector.__eq__(vc, v2d) verifica que v2d não é um Vector e devolve NotImplemented .


3. O Python recebe o resultado NotImplemented , então tenta Vector2d.__eq__(v2d, vc) .

4. Vector2d.__eq__(v2d, vc) transforma os dois operandos em tuplas e os compara: o resulltado é True (o código
de Vector2d.__eq__ está no Exemplo 11).
Já para a comparação va == t3 , entre Vector e tuple no Exemplo 15, os passos são:

1. Para avaliar va == t3 , o Python invoca Vector.__eq__(va, t3) .

2. Vector.__eq__(va, t3) verifica que t3 não é um Vector e devolve NotImplemented .

3. O Python recebe o resultado NotImplemented , e então tenta tuple.__eq__(t3, va) .

4. tuple.__eq__(t3, va) não tem a menor ideia do que seja um Vector , então devolve NotImplemented .

5. No caso especial de == , se a chamada reversa devolve NotImplemented , o Python compara os IDs dos objetos,
como último recurso.

Não precisamos implementar __ne__ para != , pois o comportamento alternativo do __ne__ herdado de object
nos serve: quando __eq__ é definido e não devolve NotImplemented , __ne__ devolve o mesmo resultado negado.

Em outras palavras, dados os mesmos objetos que usamos no Exemplo 15, os resultados para != são consistentes:

PYTHON3
>>> va != vb
False
>>> vc != v2d
False
>>> va != (1, 2, 3)
True

O __ne__ herdado de object funciona como o código abaixo—exceto pelo original estar escrito em C:[209]

PYTHON3
def __ne__(self, other):
eq_result = self == other
if eq_result is NotImplemented:
return NotImplemented
else:
return not eq_result

Vimos o básico da sobrecarga de operadores infixos.Vamos agora voltar nossa atenção para uma classe diferente de
operador: os operadores de atribuição aumentada.

16.9. Operadores de atribuição aumentada


Nossa classe Vector já suporta os operadores de atribuição aumentada += e *= . Isso se dá porque a atribuição
aumentada trabalha com recipientes imutáveis criando novas instâncias e re-vinculando a variável à esquerda do
operador.

O Exemplo 16 os mostra em ação.

Exemplo 16. Usando += e *= com instâncias de Vector


PYCON
>>> v1 = Vector([1, 2, 3])
>>> v1_alias = v1 # (1)
>>> id(v1) # (2)
4302860128
>>> v1 += Vector([4, 5, 6]) # (3)
>>> v1 # (4)
Vector([5.0, 7.0, 9.0])
>>> id(v1) # (5)
4302859904
>>> v1_alias # (6)
Vector([1.0, 2.0, 3.0])
>>> v1 *= 11 # (7)
>>> v1 # (8)
Vector([55.0, 77.0, 99.0])
>>> id(v1)
4302858336

1. Cria um alias, para podermos inspecionar o objeto Vector([1, 2, 3]) mais tarde.
2. Verifica o ID do Vector inicial, vinculado a v1 .

3. Executa a adição aumentada.


4. O resultado esperado…​
5. …​mas foi criado um novo Vector .

6. Inspeciona v1_alias para confirmar que o Vector original não foi alterado.
7. Executa a multiplicação aumentada.
8. Novamente, o resultado é o esperado, mas um novo Vector foi criado.

Se uma classe não implementa os operadores "no mesmo lugar" listados na Tabela 17, os operadores de atribuição
aumentada funcionam como açúcar sintático: a += b é avaliado exatamente como a = a + b . Esse é o
comportamento esperado para tipos imutáveis, e se você fornecer __add__ , então += funcionará sem qualquer
código adicional.

Entretanto, se você implementar um operador "no mesmo lugar" tal como __iadd__ , aquele método será chamado
para computar o resultado de a += b . Como indica seu nome, se espera que esses operadores modifiquem o operando
à esquerda do operador no mesmo lugar (NT: O "i" nos nomes desses operadores se refere a "in-place"), e não criem um
novo objeto como resultado.

⚠️ AVISO Os métodos especiais de atualização no mesmo lugar não devem nunca ser implementados para
tipos imutáveis como nossa classe Vector . Isso é bastante óbvio, mas vale a pena enfatizar.

Para mostrar o código de um operador de atualização no mesmo lugar, vamos estender a classe BingoCage do
Exemplo 9 para implementar __add__ e __iadd__ .

Vamos chamar a subclasse de AddableBingoCage . O Exemplo 17 mostra o comportamento esperado para o operador
+.

Exemplo 17. O operador + cria uma nova instância de AddableBingoCage


PY
>>> vowels = 'AEIOU'
>>> globe = AddableBingoCage(vowels) # (1)
>>> globe.inspect()
('A', 'E', 'I', 'O', 'U')
>>> globe.pick() in vowels # (2)
True
>>> len(globe.inspect()) # (3)
4
>>> globe2 = AddableBingoCage('XYZ') # (4)
>>> globe3 = globe + globe2
>>> len(globe3.inspect()) # (5)
7
>>> void = globe + [10, 20] # (6)
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'AddableBingoCage' and 'list'

1. Cria uma instância de globe com cinco itens (cada uma das vowels ).

2. Extrai um dos itens, e verifica que é uma das vowels .

3. Confirma que globe tem agora quatro itens.


4. Cria uma segunda instância, com três itens.
5. Cria uma terceira instância pela soma das duas anteriores. Essa instância tem sete itens.
6. Tentar adicionar uma AddableBingoCage a uma list falha com um TypeError . A mensagem de erro é
produzida pelo interpretador do Python quando nosso método __add__ devolve NotImplemented .

Como uma AddableBingoCage é mutável, o Exemplo 18 mostra como ela funcionará quando implementarmos
__iadd__ .

Exemplo 18. Uma AddableBingoCage existente pode ser carregada com += (continuando do Exemplo 17)

PY
>>> globe_orig = globe # (1)
>>> len(globe.inspect()) # (2)
4
>>> globe += globe2 # (3)
>>> len(globe.inspect())
7
>>> globe += ['M', 'N'] # (4)
>>> len(globe.inspect())
9
>>> globe is globe_orig # (5)
True
>>> globe += 1 # (6)
Traceback (most recent call last):
...
TypeError: right operand in += must be 'Tombola' or an iterable

1. Cria um alias para podermos verificar a identidade do objeto mais tarde.


2. globe tem quatro itens aqui.
3. Uma instância de AddableBingoCage pode receber itens de outra instância da mesma classe.
4. O operador à diretia de += também pode ser qualquer iterável.
5. Durante todo esse exemplo, globe sempre se refere ao mesmo objeto que globe_orig .
6. Tentar adicionar um não-iterável a uma AddableBingoCage falha com uma mensagem de erro apropriada.
Observe que o operador += é mais liberal que + quanto ao segundo operando. Com + , queremos que ambos os
operandos sejam do mesmo tipo (nesse caso, AddableBingoCage ), pois se aceitássemos tipos diferentes, isso poderia
causar confusão quanto ao tipo do resultado. Com o += , a situação é mais clara: o objeto à esquerda do operador é
atualizado no mesmo lugar, então não há dúvida quanto ao tipo do resultado.

Eu validei os comportamentos diversos de + e += observando como funciona o tipo embutido


list . Ao escrever my_list + x , você só pode concatenar uma list a outra list , mas se você
👉 DICA escrever my_list += x , você pode estender a lista da esquerda com itens de qualquer iterável x à
direita do operador. É assim que o método list.extend() funciona: ele aceita qualquer argumento
iterável.

Agora que esclarecemos o comportamento desejado para AddableBingoCage , podemos examinar sua implementação
no Exemplo 19. Lembre-se que BingoCage , do Exemplo 9, é uma subclasse concreta da ABC Tombola do Exemplo 7.

Exemplo 19. bingoaddable.py: AddableBingoCage estende BingoCage para suportar + e +=

PY
from tombola import Tombola
from bingo import BingoCage

class AddableBingoCage(BingoCage): # (1)

def __add__(self, other):


if isinstance(other, Tombola): # (2)
return AddableBingoCage(self.inspect() + other.inspect())
else:
return NotImplemented

def __iadd__(self, other):


if isinstance(other, Tombola):
other_iterable = other.inspect() # (3)
else:
try:
other_iterable = iter(other) # (4)
except TypeError: # (5)
msg = ('right operand in += must be '
"'Tombola' or an iterable")
raise TypeError(msg)
self.load(other_iterable) # (6)
return self # (7)

1. AddableBingoCage estende BingoCage .

2. Nosso __add__ só vai funcionar se o segundo operando for uma instância de Tombola .

3. Em __iadd__ , obtém os itens de other , se ele for uma instância de Tombola .

4. Caso contrário, tenta obter um iterador sobre other .[210]

5. Se aquilo falhar, gera uma exceção explicando o que o usuário deve fazer. Sempre que possível, mensagens de erro
devem guiar o usuário explicitamente para a solução.
6. Se chegamos até aqui, podemos carregar o other_iterable para self .

7. Muito importante: os métodos especiais de atribuição aumentada de objetos mutáveis devem devolver self . É o
que os usuários esperam.
Podemos resumir toda a ideia dos operadores de atualização no mesmo lugar comparando as instruções return que
produzem os resultados em __add__ e em __iadd__ no Exemplo 19:

__add__
O resultado é produzido chamando o construtor AddableBingoCage para criar uma nova instância.

__iadd__
O resultado é produzido devolvendo self , após ele ter sido modificado.

Para concluir esse exemplo, uma última observação sobre o Exemplo 19: propositalmente, nenhum método __radd__
foi incluido em AddableBingoCage , porque não há necessidade. O método direto __add__ só vai lidar com operandos
à direita do mesmo tipo, então se o Python tentar computar a + b , onde a é uma AddableBingoCage e b não,
devolvemos NotImplemented —talvez a classe de b possa fazer isso funcionar. Mas se a expressão for b + a e b não
for uma AddableBingoCage , e devolver NotImplemented , então é melhor deixar o Python desisitir e gerar um
TypeError , pois não temos como tratar b .

De modo geral, se um método de operador infixo direto (por exemplo __mul__ ) for projetado para

👉 DICA funcionar apenas com operandos do mesmo tipo de self , é inútil implementar o método reverso
correspondente (por exemplo, __rmul__ ) pois, por definição, esse método só será invocado quando
estivermos lidando com um operando de um tipo diferente.

Isso conclui nossa exploração de sobrecarga de operadores no Python.

16.10. Resumo do capítulo


Começamos o capítulo revisando algumas restrições impostas pelo Python à sobrecarga de operadores: é proibido
redefinir operadores nos próprios tipos embutidos, a sobrecarga está limitada aos operadores existentes, e alguns
operadores não podem ser sobrecarregados ( is , and , or , not ).

Colocamos a mão na massa com os operadores unários, implementando __neg__ e __pos__ . A seguir vieram os
operadores infixos, começando por + , suportado pelo método __add__ . Vimos que operadores unários e infixos
devem produzir resultados criando novos objetos, sem nunca modificar seus operandos. Para suportar operações com
outros tipos, devolvemos o valor especial NotImplemented —não uma exceção—permitindo ao interpretador tentar
novamente permutando os operandos e chamando o método especial reverso para aquele operador (por exemplo,
__radd__ ). O algoritmo usado pelo Python para tratar operadores infixos está resumido no fluxograma da Figura 1.

Misturar operandos de mais de um tipo exige detectar os operandos que não podemos tratar. Neste capitulo fizemos
isso de duas maneiras: ao modo do duck typing, apenas fomos em frente e tentamos a operação, capturando uma
exceção de TypeError se ela acontecesse; mais tarde, em __mul__ e __matmul__ , usamos um teste isinstance
explícito. Há prós e contras nas duas abordagens: duck typing é mais flexível, mas a verificação explícita de tipo é mais
previsível.

De modo geral, bibliotecas deveriam tirar proveito do duck typing--abrindo a porta para objetos independente de seus
tipos, desde que eles suportem as operações necessárias. Entretanto, o algoritmo de despacho de operadores do Python
pode produzir mensagens de erro enganosas ou resultados inesperados quando combinado com o duck typing. Por essa
razão, a disciplina da verificação de tipo com invocações de isinstance contra ABCs é muitas vezes útil quando
escrevemos métodos especiais para sobrecarga de operadores. Essa é a técnica batizada de goose typing por Alex
Martelli—como vimos na seção Seção 13.5. A goose typing é um bom compromisso entre a flexibilidade e a segurança,
porque os tipos definidos pelo usuário, existentes ou futuros, podem ser declarados como subclasses reais ou virtuais
de uma ABC. Além disso, se uma ABC implementa o __subclasshook__ , objetos podem então passar por verificações
com isinstance contra aquela ABC apenas fornecendo os métodos exigidos—​sem necessidade de ser uma subclasse
ou de se registrar com a ABC.
O próximo tópico tratado foram os operadores de comparação cheia. Implementamos == com __eq__ e descobrimos
que o Python oferece uma implementação conveniente de != no __ne__ herdado da classe base object . A forma
como o Python avalia esses operadores, bem como > , < , >= , e ⇐ , é um pouco diferente, com uma lógica especial
para a escolha do método reverso, e um tratamento alternativo para == e != que nunca gera erros, pois o Python
compara os IDs dos objetos como último recurso.

Na última seção, nos concentramos nos operadores de atribuição aumentada. Vimos que o Python os trata, por default,
como uma combinação do operador simples seguido de uma atribuição, isto é: a += b é avaliado exatamente como a
= a + b . Isso sempre cria um novo objeto, então funciona para tipos mutáveis ou imutáveis. Para objetos mutáveis,
podemos implementar métodos especiais de atualização no mesmo lugar, tal como __iadd__ para += , e alterar o
valor do operando à esquerda do operador. Para demonstrar isso na prática, deixamos para trás a classe imutável
Vector e trabalhamos na implementação de uma subclasse de BingoCage , suportando += para adicionar itens ao
reservatório de itens para sorteio, de modo similar à forma como o tipo embutido list suporta += como um atalho
para o método list.extend() . Enquanto fazíamos isso, discutimos como + tende a ser mais estrito que += em
relação aos tipos aceitos. Para tipos de sequências, + normalmente exige que ambos os operandos sejam do mesmo
tipo, enquanto += muitas vezes aceita qualquer iterável como o operando à direita do operador.

16.11. Leitura complementar


Guido van Rossum escreveu uma boa apologia da sobrecarga de operadores em "Why operators are useful" (Porque
operadores são úteis) (https://fpy.li/16-10) (EN). Trey Hunner postou "Tuple ordering and deep comparisons in Python"
(Ordenação de tuplas e comparações profundas em Python) (https://fpy.li/16-11) (EN), argumentando que os operadores de
comparação cheia do Python são mais flexíveis e poderosos do que os programadores vindos de outras linguagens
costumam pensar.

A sobrecarga de operadores é uma área da programação em Python onde testes com isinstance são comuns. A
melhor prática relacionada a tais testes é a goose typing, tratada na seção Seção 13.5. Se você pulou essa parte, se
assegure de voltar lá e ler aquela seção.

A principal referência para os métodos especiais de operadores é o capítulo "Modelos de Dados"


(https://docs.python.org/pt-br/3/reference/datamodel.html) na documentação do Python. Outra leitura relevante é
"Implementando as operações aritméticas"
(https://docs.python.org/pt-br/3/library/numbers.html#implementing-the-arithmetic-operations) no módulo numbers da Biblioteca
Padrão do Python.

Um exemplo brilhante de sobrecarga de operadores apareceu no pacote pathlib (https://fpy.li/16-13), adicionado no


Python 3.4. Sua classe Path sobrecarrega o operador / para construir caminhos do sistema de arquivos a partir de
strings, como mostra o exemplo abaixo, da documentação:

PYCON
>>> p = Path('/etc')
>>> q = p / 'init.d' / 'reboot'
>>> q
PosixPath('/etc/init.d/reboot')

Outro exemplo não aritmético de sobrecarga de operadores está na biblioteca Scapy (https://fpy.li/16-14), usada para
"enviar, farejar, dissecar e forjar pacotes de rede". Na Scapy, o operador / operator cria pacotes empilhando campos de
diferentes camadas da rede. Veja "Stacking layers" (_Empilhando camadas) (https://fpy.li/16-15) (EN) para mais detalhes.
Se você está prestes a implementar operadores de comparação, estude functools.total_ordering . Esse é um
decorador de classes que gera automaticamente os métodos para todos os operadores de comparação cheia em
qualquer classe que defina ao menos alguns deles. Veja a documentação do módulo functools
(https://docs.python.org/pt-br/3/library/functools.html#functools.total_ordering) (EN).

Se você tiver curiosidade sobre o despacho de métodos de operadores em linguagens com tipagem dinâmica, duas
leituras fundamentais são "A Simple Technique for Handling Multiple Polymorphism" (Uma Técnica Simples para
Tratar Polimorfismo Múltiplo) (https://fpy.li/16-17) (EN), de Dan Ingalls (membro da equipe original do Smalltalk), e
"Arithmetic and Double Dispatching in Smalltalk-80" (Aritmética e Despacho Duplo no Smalltalk-80) (https://fpy.li/16-18)
(EN), de Kurt J. Hebel e Ralph Johnson (Johnson ficou famoso como um dos autores do livro Padrões de Projetos
original). Os dois artigos fornecem discussões profundas sobre o poder do polimorfismo em linguagens com tipagem
dinâmica, como o Smalltalk, o Python e o Ruby. O Python não tem despacho duplo para tratar operadores, como
descrito naqueles artigos. O algoritmo do Python, usando operadores diretos e reversos, é mais fácil de suportar por
classes definidas pelo usuário que o despacho duplo, mas exige tratamento especial pelo interpretador. Por outro lado,
o despacho duplo clássico é uma técnica geral, que pode ser usada no Python ou em qualquer linguagem orientada a
objetos, para além do contexto específico de operadores infixos. E, de fato, Ingalls, Hebel e Johnson usam exemplos
muito diferentes para descrever essa técnica.

O artigo "The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling"(A Família de
Linguagens C: Entrevista com Dennis Ritchie, Bjarne Stroustrup, e James Gosling) (https://fpy.li/16-1) (EN), da onde tirei a
epígrafe desse capítulo, apareceu na Java Report, 5(7), julho de 2000, e na C++ Report, 12(7), julho/agosto de 2000,
juntamente com outros trechos que usei no "Ponto de Vista" deste capítulo (abaixo). Se você se interessa pelo projeto de
linguagens de programação, faça um favor a si mesmo e leia aquela entrevista.

Ponto de Vista
Sobrecarga de operadores: prós e contras

James Gosling, citado no início desse capítulo, tomou a decisão consciente de excluir a sobrecarga de operadores
quando projetou o Java. Naquela mesma entrevista ("The C Family of Languages: Interview with Dennis Ritchie,
Bjarne Stroustrup, and James Gosling" (A Família de Linguagens C: Entrevista com Dennis Ritchie, Bjarne
Stroustrup, e James Gosling) (https://fpy.li/16-20) (EN)) ele diz:

“ Provavelmente uns 20 a 30 porcento da população acha que sobrecarga de operadores é


uma criação demoníaca; alguém fez algo com sobrecarga de operadores que realmente os
tirou do sério, porque eles usaram algo como + para inserção em listas, e isso torna a vida
muito, muito confusa. Muito daquele problema vem do fato de existirem apenas uma meia
dúzia de operadores que podem ser sobrecarregados de forma razoável, mas existem
milhares ou milhões de operadores que as pessoas gostariam de definir—então é preciso
escolher, e muitas vezes as escolhas entram em conflito com a sua intuição.

Guido van Rossum escolheu o caminho do meio no suporte à sobrecarga de operadores: ele não deixou a porta
aberta para que os usuários criassem novos operadores arbitrários como <⇒ ou :-) , evitando uma Torre de
Babel de operadores personalizados, e permitindo ao analisador sintático do Python permanecer simples. O
Python também não permite a sobrecarga dos operadores de tipos embutidos, outra limitação que promove a
legibilidade e o desempenho previsível.

Gosling continua:


“ Esobrecarga
então há uma comunidade de aproximadamente 10 porcento que havia de fato usado a
de operadores de forma apropriada, e que realmente gostavam disso, e para
quem isso era realmente importante; essas são quase exclusivamente pessoas que fazem
trabalho numérico, onde a notação é muito importante para avivar a intuição [das
pessoas], porque elas chegam ali com uma intuição sobre o que + significa, e a capacidade
de dizer "a + b", onde a e b são números complexos ou matrizes ou alguma outra coisa,
realmente faz sentido.

Claro, há benefícios em não permitir a sobrecarga de operadores em uma linguagem. Já ouvi o argumento que C
é melhor que C++ para programação de sistemas, porque a sobrecarga de operadores em C++ pode fazer com que
operações dispendiosas pareçam triviais. Duas linguagens modernas bem sucedidas, que compilam para
executáveis binários, fizeram escolhas opostas: Go não tem sobrecarga de operadores, Rust tem (https://fpy.li/16-21).

Mas operadores sobrecarregados, quando usados de forma sensata, tornam o código mais fácil de ler e escrever. É
um ótimo recurso em uma linguagem de alto nível moderna.

Um vislumbre da avaliação preguiçosa

Se você olhar de perto o traceback no Exemplo 9, vai encontrar evidências da avaliação preguiçosa
(https://fpy.li/16-22) de expressões geradoras. O Exemplo 20 é o mesmo traceback, agora com explicações.

Exemplo 20. Mesmo que o Exemplo 9

PYCON
>>> v1 + 'ABC'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "vector_v6.py", line 329, in __add__
return Vector(a + b for a, b in pairs) # (1)
File "vector_v6.py", line 243, in __init__
self._components = array(self.typecode, components) # (2)
File "vector_v6.py", line 329, in <genexpr>
return Vector(a + b for a, b in pairs) # (3)
TypeError: unsupported operand type(s) for +: 'float' and 'str'

1. A chamada a Vector recebe uma expressão geradora como seu argumento components . Nenhum problema
nesse estágio.
2. A genexp components é passada para o construtor de array . Dentro do construtor de array , o Python
tenta iterar sobre a genexp, causando a avaliação do primeiro item a + b . É quando ocorre o TypeError .
3. A exceção se propaga para a chamada ao construtor de Vector , onde é relatada.

Isso mostra como a expressão geradora é avaliada no último instante possível, e não onde é definida no código-
fonte.

Se, por outro lado, o construtor de Vector fosse invocado como Vector([a + b for a, b in pairs]) , então a
exceção ocorreria bem ali, porque a compreensão de lista tentou criar uma list para ser passada como
argumento para a chamada a Vector() . O corpo de Vector.__init__ nunca seria alcançado.

O Capítulo 17 vai tratar das expressões geradoras em detalhes, mas não eu queria deixar essa demonstração
acidental de sua natureza preguiçosa passar desapercebida.
Parte IV: Controle de fluxo
17. Iteradores, geradores e corrotinas clássicas
“ Quando vejo padrões em meus programas, considero isso um mau sinal. A forma de um
programa deve refletir apenas o problema que ele precisa resolver. Qualquer outra regularidade
no código é, pelo menos para mim, um sinal que estou usando abstrações que não são
poderosas o suficiente—muitas vezes estou gerando à mão as expansões de alguma macro que
preciso escrever.[211]
— Paul Graham
hacker de Lisp e investidor

A iteração é fundamental para o processamento de dados: programas aplicam computações sobre séries de dados, de
pixels a nucleotídeos. Se os dados não cabem na memória, precisamos buscar esses itens de forma preguiçosa—um de
cada vez e sob demanda. É isso que um iterador faz. Este capítulo mostra como o padrão de projeto Iterator ("Iterador")
está embutido na linguagem Python, de modo que nunca será necessário programá-lo manualmente.

Todas as coleções padrão do Python são iteráveis. Um iterável é um objeto que fornece um iterador, que o Python usa
para suportar operações como:

loops for

Compreensões de lista, dict e set


Desempacotamento para atribuições
Criação de instâncias de coleções

Este capítulo cobre os seguintes tópicos:

Como o Python usa a função embutida iter() para lidar com objetos iteráveis
Como implementar o padrão Iterator clássico no Python
Como o padrão Iterator clássico pode ser substituído por uma função geradora ou por uma expressão geradora
Como funciona uma função geradora, em detalhes, com descrições linha a linha
Aproveitando o poder das funções geradoras de uso geral da biblioteca padrão
Usando expressões yield from para combinar geradoras
Porque geradoras e corrotinas clássicas se parecem, mas são usadas de formas muito diferentes e não devem ser
misturadas

17.1. Novidades nesse capítulo


A seção Seção 17.11 aumentou de uma para seis páginas. Ela agora inclui experimentos simples, demonstrando o
comportamento de geradoras com yield from , e um exemplo de código para percorrer uma árvore de dados,
desenvolvido passo a passo.

Novas seções explicam as dicas de tipo para os tipos Iterable , Iterator e Generator .

A última grande seção do capítulo, Seção 17.13, é agora uma introdução de 9 páginas a um tópico que ocupava um
capítulo de 40 páginas na primeira edição. Atualizei e transferi o capítulo Classic Coroutines (Corrotinas Clássicas)
(https://fpy.li/oldcoro) para um post no site que acompanha o livro (https://fpy.li/oldcoro), porque ele era o capítulo mais
difícil para os leitores, mas seu tema se tornou menos relevante após a introdução das corrotinas nativas no Python 3.5
(estudaremos as corrotinas nativas no Capítulo 21).
Vamos começar examinando como a função embutida iter() torna as sequências iteráveis.

17.2. Uma sequência de palavras


Vamos começar nossa exploração de iteráveis implementando uma classe Sentence : seu construtor recebe uma string
de texto e daí podemos iterar sobre a "sentença" palavra por palavra. A primeira versão vai implementar o protocolo
de sequência e será iterável, pois todas as sequências são iteráveis—como sabemos desde o Capítulo 1. Agora veremos
exatamente porque isso acontece.

O Exemplo 1 mostra uma classe Sentence que extrai palavras de um texto por índice.

Exemplo 1. sentence.py: uma Sentence como uma sequência de palavras

PY
import re
import reprlib

RE_WORD = re.compile(r'\w+')

class Sentence:

def __init__(self, text):


self.text = text
self.words = RE_WORD.findall(text) # (1)

def __getitem__(self, index):


return self.words[index] # (2)

def __len__(self): # (3)


return len(self.words)

def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text) # (4)

1. .findall devolve a lista com todos os trechos não sobrepostos correspondentes à expressão regular, como uma
lista de strings.
2. self.words mantém o resultado de .findall , então basta devolver a palavra em um dado índice.

3. Para completar o protocolo de sequência, implementamos __len__ , apesar dele não ser necessário para criar um
iterável.
4. reprlib.repr é uma função utilitária para gerar representações abreviadas, em forma de strings, de estruturas
de dados que podem ser muito grandes.[212]

Por default, reprlib.repr limita a string gerada a 30 caracteres. Veja como Sentence é usada na sessão de console
do Exemplo 2.

Exemplo 2. Testando a iteração em uma instância de Sentence


PYCON
>>> s = Sentence('"The time has come," the Walrus said,') # (1)
>>> s
Sentence('"The time ha... Walrus said,') # (2)
>>> for word in s: # (3)
... print(word)
The
time
has
come
the
Walrus
said
>>> list(s) # (4)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']

1. Uma sentença criada a partir de uma string.


2. Observe a saída de __repr__ gerada por reprlib.repr , usando …​.

3. Instâncias de Sentence são iteráveis; veremos a razão em seguida.


4. Sendo iteráveis, objetos Sentence podem ser usados como entrada para criar listas e outros tipos iteráveis.

Nas próximas páginas vamos desenvolver outras classes Sentence que passam nos testes do Exemplo 2. Entretanto, a
implementação no Exemplo 1 difere das outras por ser também uma sequência, e então é possível obter palavras
usando um índice:

PYCON
>>> s[0]
'The'
>>> s[5]
'Walrus'
>>> s[-1]
'said'

Programadores Python sabem que sequências são iteráveis. Agora vamos descobrir exatamente o porquê disso.

17.3. Porque sequências são iteráveis: a função iter


Sempre que o Python precisa iterar sobre um objeto x , ele automaticamente invoca iter(x) .

A função embutida iter :

1. Verifica se o objeto implementa o método __iter__ , e o invoca para obter um iterador.

2. Se __iter__ não for implementado, mas __getitem__ sim, então iter() cria um iterador que tenta buscar
itens pelo índice, começando de 0 (zero).
3. Se isso falhar, o Python gera um TypeError , normalmente dizendo 'C' object is not iterable (objeto 'C' não é
iterável), onde C é a classe do objeto alvo.

Por isso todas as sequências do Python são iteráveis: por definição, todas elas implementam __getitem__ . Na
verdade, todas as sequências padrão também implementam __iter__ , e as suas próprias sequências também deviam
implementar esse método, porque a iteração via __getitem__ existe para manter a compatibilidade retroativa, e pode
desaparecer em algum momento—apesar dela não ter sido descontinuada no Python 3.10, e eu duvidar que vá ser
removida algum dia.
Como mencionado na seção Seção 13.4.1, essa é uma forma extrema de duck typing: um objeto é considerado iterável
não apenas quando implementa o método especial __iter__ , mas também quando implementa __getitem__ . Veja
isso:

PYCON
>>> class Spam:
... def __getitem__(self, i):
... print('->', i)
... raise IndexError()
...
>>> spam_can = Spam()
>>> iter(spam_can)
<iterator object at 0x10a878f70>
>>> list(spam_can)
-> 0
[]
>>> from collections import abc
>>> isinstance(spam_can, abc.Iterable)
False

Se uma classe fornece __getitem__ , a função embutida iter() aceita uma instância daquela classe como iterável e
cria um iterador a partir da instância. A maquinaria de iteração do Python chamará __getitem__ com índices,
começando de 0, e entenderá um IndexError como sinal de que não há mais itens.

Observe que, apesar de spam_can ser iterável (seu método __getitem__ poderia fornecer itens), ela não é
reconhecida assim por uma chamada a isinstance contra abc.Iterable .

Na abordagem da goose typing, a definição para um iterável é mais simples, mas não tão flexível: um objeto é
considerado iterável se implementa o método __iter__ . Não é necessário ser subclasse ou se registar, pois
abc.Iterable implementa o __subclasshook__ , como visto na seção Seção 13.5.8. Eis uma demonstração:

PYCON
>>> class GooseSpam:
... def __iter__(self):
... pass
...
>>> from collections import abc
>>> issubclass(GooseSpam, abc.Iterable)
True
>>> goose_spam_can = GooseSpam()
>>> isinstance(goose_spam_can, abc.Iterable)
True

Desde o Python 3.10, a forma mais precisa de verificar se um objeto x é iterável é invocar iter(x)
👉 DICA e tratar a exceção TypeError se ele não for. Isso é mais preciso que usar isinstance(x,
abc.Iterable) , porque iter(x) também leva em consideração o método legado __getitem__ ,
enquanto a ABC Iterable não considera tal método.

Verificar explicitamente se um objeto é iterável pode não valer a pena, se você for iterar sobre o objeto logo após a
verificação. Afinal, quando se tenta iterar sobre um não-iterável, a exceção gerada pelo Python é bastante clara:
TypeError: 'C' object is not iterable (TypeError: o objeto 'C' não é iterável). Se você puder fazer algo mais além
de gerar um TypeError , então faça isso em um bloco try/except ao invés de realizar uma verificação explícita. A
verificação explícita pode fazer sentido se você estiver mantendo o objeto para iterar sobre ele mais tarde; nesse caso,
capturar o erro mais cedo torna a depuração mais fácil.

A função embutida iter() é usada mais frequentemente pelo Python que no nosso código. Há uma segunda maneira
de usá-la, mas não é muito conhecida.
17.3.1. Usando iter com um invocável
Podemos chamar iter() com dois argumentos, para criar um iterador a partir de uma função ou de qualquer objeto
invocável. Nessa forma de uso, o primeiro argumento deve ser um invocável que será chamado repetidamente (sem
argumentos) para produzir valores, e o segundo argumento é um valor sentinela (https://fpy.li/17-2) (EN): um marcador
que, quando devolvido por um invocável, faz o iterador gerar um StopIteration ao invés de produzir o valor
sentinela.

O exemplo a seguir mostra como usar iter para rolar um dado de seis faces até que o valor 1 seja sorteado:

PYCON
>>> def d6():
... return randint(1, 6)
...
>>> d6_iter = iter(d6, 1)
>>> d6_iter
<callable_iterator object at 0x10a245270>
>>> for roll in d6_iter:
... print(roll)
...
4
3
6
3

Observe que a função iter devolve um callable_iterator . O loop for no exemplo pode rodar por um longo
tempo, mas nunca vai devolver 1 , pois esse é o valor sentinela. Como é comum com iteradores, o objeto d6_iter se
torna inútil após ser exaurido. Para recomeçar, é necessário reconstruir o iterador, invocando novamente iter() .

A documentação de iter (https://docs.python.org/pt-br/3.10/library/functions.html#iter) inclui a seguinte explicação e código


de exemplo:

“ Uma aplicação útil da segunda forma de é para construir um bloco de leitura. Por
iter()
exemplo, ler blocos de comprimento fixo de um arquivo binário de banco de dados até que o
final do arquivo seja atingido:
PYTHON3
from functools import partial

with open('mydata.db', 'rb') as f:


read64 = partial(f.read, 64)
for block in iter(read64, b''):
process_block(block)

Para deixar o código mais claro, adicionei a atribuição read64 , que não está no exemplo original
(https://docs.python.org/pt-br/3.10/library/functions.html#iter). A função partial() é necessária porque o invocável passado
a iter() não pode requerer argumentos. No exemplo, um objeto bytes vazio é a sentinela, pois é isso que f.read
devolve quando não há mais bytes para ler.

A próxima seção detalha a relação entre iteráveis e iteradores.

17.4. Iteráveis versus iteradores


Da explicação na seção Seção 17.3 podemos extrapolar a seguinte definição:

iterável
Qualquer objeto a partir do qual a função embutida iter consegue obter um iterador. Objetos que implementam
um método __iter__ devolvendo um iterador são iteráveis. Sequências são sempre iteráveis, bem como objetos
que implementam um método __getitem__ que aceite índices iniciando em 0.
É importante deixar clara a relação entre iteráveis e iteradores: o Python obtém iteradores de iteráveis.

Aqui está um simples loop for iterando sobre uma str . A str 'ABC' é o iterável aqui. Você não vê, mas há um
iterador por trás das cortinas:

PYCON
>>> s = 'ABC'
>>> for char in s:
... print(char)
...
A
B
C

Se não existisse uma instrução for e fosse preciso emular o mecanismo do for à mão com um loop while , isso é o
que teríamos que escrever:

PYCON
>>> s = 'ABC'
>>> it = iter(s) # (1)
>>> while True:
... try:
... print(next(it)) # (2)
... except StopIteration: # (3)
... del it # (4)
... break # (5)
...
A
B
C

1. Cria um iterador it a partir de um iterável.


2. Chama next repetidamente com o iterador, para obter o item seguinte.
3. O iterador gera StopIteration quando não há mais itens.
4. Libera a referência a it —o obleto iterador é descartado.

5. Sai do loop.

StopIteration sinaliza que o iterador foi exaurido. Essa exceção é tratada internamente pela função embutida
iter() , que é parte da lógica dos loops for e de outros contextos de iteração, como compreensões de lista,
desempacotamento iterável, etc.

A interface padrão do Python para um iterador tem dois métodos:

__next__

Devolve o próximo item em uma série, gerando StopIteration se não há mais nenhum.

__iter__

Devolve self ; isso permite que iteradores sejam usado quando um iterável é esperado. Por exemplo, em um loop
for loop.

Essa interface está formalizada na ABC collections.abc.Iterator , que declara o método abstrato __next__ , e é
uma subclasse de Iterable—onde o método abstrato __iter__ é declarado. Veja a Figura 1.
Figura 1. As ABCs Iterable e Iterator . Métodos em itálico são abstratos. Um Iterable.__iter__ concreto deve
devolver uma nova instância de Iterator . Um Iterator concreto deve implementar __next__ . O método
Iterator.__iter__ apenas devolve a própria instância.

O código-fonte de collections.abc.Iterator aparece no Exemplo 3.

Exemplo 3. Classe abc.Iterator ; extraído de Lib/_collections_abc.py (https://fpy.li/17-5)

PYTHON3
class Iterator(Iterable):

__slots__ = ()

@abstractmethod
def __next__(self):
'Return the next item from the iterator. When exhausted, raise StopIteration'
raise StopIteration

def __iter__(self):
return self

@classmethod
def __subclasshook__(cls, C): # (1)
if cls is Iterator:
return _check_methods(C, '__iter__', '__next__') # (2)
return NotImplemented

1. __subclasshook__ suporta a verificação de tipo estrutural com isinstance e issubclass . Vimos isso na seção
Seção 13.5.8.
2. _check_methods percorre o parâmetro __mro__ da classe, para verificar se os métodos estão implementados em
sua classe base. Ele está definido no mesmo módulo, Lib/_collections_abc.py. Se os métodos estiverem
implementados, a classe C será reconhecida como uma subclasse virtual de Iterator . Em outras palavras,
issubclass(C, Iterable) devolverá True .

O método abstrato da ABC Iterator é it.__next__() no Python 3 e it.next() no Python 2.

⚠️ AVISO Como sempre, você deve evitar invocar métodos especiais diretamente. Use apenas next(it) : essa
função embutida faz a coisa certa no Python 2 e no 3—algo útil para quem está migrando bases de
código do 2 para o 3.
O código-fonte do módulo Lib/types.py (https://fpy.li/17-6) no Python 3.9 tem um comentário dizendo:

# Iteradores no Python não são uma questão de tipo, mas sim de protocolo. Um número
# grande e variável de tipos embutidos implementa *alguma* forma de
# iterador. Não verifique o tipo! Em vez disso, use `hasattr` para
# verificar [a existência] de ambos os atributos "__iter__" e "__next__".

E de fato, é exatamente o que o método __subclasshook__ da ABC abc.Iterator faz.

Dado o conselho de Lib/types.py e a lógica implementada em Lib/_collections_abc.py, a melhor forma

👉 DICA de verificar se um objeto x é um iterador é invocar isinstance(x, abc.Iterator) . Graças ao


Iterator.__subclasshook__ , esse teste funciona mesmo se a classe de x não for uma subclasse
real ou virtual de Iterator .

Voltando à nossa classe Sentence no Exemplo 1, usando o console do Python é possivel ver claramente como o
iterador é criado por iter() e consumido por next() :

PYCON
>>> s3 = Sentence('Life of Brian') # (1)
>>> it = iter(s3) # (2)
>>> it # doctest: +ELLIPSIS
<iterator object at 0x...>
>>> next(it) # (3)
'Life'
>>> next(it)
'of'
>>> next(it)
'Brian'
>>> next(it) # (4)
Traceback (most recent call last):
...
StopIteration
>>> list(it) # (5)
[]
>>> list(iter(s3)) # (6)
['Life', 'of', 'Brian']

1. Cria uma sentença s3 com três palavras.


2. Obtém um iterador a partir de s3 .

3. next(it) devolve a próxima palavra.


4. Não há mais palavras, então o iterador gera uma exceção StopIteration .

5. Uma vez exaurido, um itereador irá sempre gerar StopIteration , o que faz parecer que ele está vazio..

6. Para percorrer a sentença novamente é preciso criar um novo iterador.

Como os únicos métodos exigidos de um iterador são __next__ e __iter__ , não há como verificar se há itens
restantes, exceto invocando next() e capturando StopIteration . Além disso, não é possível "reiniciar" um iterador.
Se for necessário começar de novo, é preciso invocar iter() no iterável que criou o iterador original. Invocar iter()
no próprio iterador também não funciona, pois—como já mencionado—a implementação de Iterator.__iter__
apenas devolve self , e isso não reinicia um iterador exaurido.

Essa interface mínina é bastante razoável porque, na realidade, nem todos os itereadores são reiniciáveis. Por exemplo,
se um iterador está lendo pacotes da rede, não há como "rebobiná-lo".[213]
A primeira versão de Sentence , no Exemplo 1, era iterável graças ao tratamento especial dispensado pela função
embutida às sequências. A seguir vamos implementar variações de Sentence que implementam __iter__ para
devolver iteradores.

17.5. Classes Sentence com __iter__


As próximas variantes de Sentence implementam o protocolo iterável padrão, primeiro implementando o padrão de
projeto Iterable e depois com funções geradoras.

17.5.1. Sentence versão #2: um iterador clássico


A próxima implementação de Sentence segue a forma do padrão de projeto Iterator clássico, do livro Padrões de
Projeto. Observe que isso não é Python idiomático, como as refatorações seguintes deixarão claro. Mas é útil para
mostrar a distinção entre uma coleção iterável e um iterador que trabalha com ela.

A classe no Exemplo 4 é iterável por implementar o método especial


Sentence __iter__ , que cria e devolve um
SentenceIterator . É assim que um iterável e um iterador se relacionam.

Exemplo 4. sentence_iter.py: Sentence implementada usando o padrão Iterator

PY
import re
import reprlib

RE_WORD = re.compile(r'\w+')

class Sentence:

def __init__(self, text):


self.text = text
self.words = RE_WORD.findall(text)

def __repr__(self):
return f'Sentence({reprlib.repr(self.text)})'

def __iter__(self): # (1)


return SentenceIterator(self.words) # (2)

class SentenceIterator:

def __init__(self, words):


self.words = words # (3)
self.index = 0 # (4)

def __next__(self):
try:
word = self.words[self.index] # (5)
except IndexError:
raise StopIteration() # (6)
self.index += 1 # (7)
return word # (8)

def __iter__(self): # (9)


return self

1. O método __iter__ é o único acréscimo à implementação anterior de Sentence . Essa versão não inclui um
__getitem__ , para deixar claro que a classe é iterável por implementar __iter__ .

2. __iter__ atende ao protocolo iterável instanciando e devolvendo um iterador.


3. SentenceIterator mantém uma referência para a lista de palavras.
4. self.index determina a próxima palavra a ser recuperada.
5. Obtém a palavra em self.index .

6. Se não há palavra em self.index , gera uma StopIteration .

7. Incrementa self.index .

8. Devolve a palavra.
9. Implementa self.__iter__ .
O código do Exemplo 4 passa nos testes do Exemplo 2.

Veja que não é de fato necessário implementar __iter__ em SentenceIterator para esse exemplo funcionar, mas é
o correto a fazer: supõe-se que iteradores implementem tanto __next__ quanto __iter__ , e fazer isso permite ao
nosso iterador passar no teste issubclass(SentenceIterator, abc.Iterator) . Se tivéssemos tornado
SentenceIterator uma subclasse de abc.Iterator , teríamos herdado o método concreto
abc.Iterator.__iter__ .

É um bocado de trabalho (pelo menos para nós, programadores mimados pelo Python). Observe que a maior parte do
código em SentenceIterator serve para gerenciar o estado interno do iterador. Logo veremos como evitar essa
burocracia. Mas antes, um pequeno desvio para tratar de um atalho de implementação que pode parecer tentador, mas
é apenas errado.

17.5.2. Não torne o iterável também um iterador


Uma causa comum de erros na criação de iteráveis é confundir os dois. Para deixar claro: iteráveis tem um método
__iter__ que instancia um novo iterador a cada invocação. Iteradores implementam um método __next__ , que
devolve itens individuais, e um método __iter__ , que devolve self .

Assim, iteradores também são iteráveis, mas iteráveis não são iteradores.

Pode ser tentador implementar __next__ além de __iter__ na classe Sentence , tornando cada instância de
Sentence ao mesmo tempo um iterável e um iterador de si mesma. Mas raramente isso é uma boa ideia. Também é
um anti-padrão comum, de acordo com Alex Martelli, que possui vasta experiência revisando código no Google.

A seção "Aplicabilidade" do padrão de projeto Iterator no livro Padrões de Projeto diz:

“ Use o padrão Iterator


para acessar o conteúdo de um objeto agregado sem expor sua representação interna.
para suportar travessias múltiplas de objetos agregados.
para fornecer uma interface uniforme para atravessar diferentes estruturas agregadas (isto é,
para suportar iteração polimórfica).

Para "suportar travessias múltiplas", deve ser possível obter mútiplos iteradores independentes de uma mesma
instância iterável, e cada iterador deve manter seu próprio estado interno. Assim, uma implementação adequada do
padrão exige que cada invocação de iter(my_iterable) crie um novo iterador independente. É por essa razão que
precisamos da classe SentenceIterator neste exemplo.
Agora que demonstramos de forma apropriada o padrão Iterator clássico, vamos em frente. O Python incorporou a
instrução yield da linguagem CLU (https://fpy.li/17-7), de Barbara Liskov, para não termos que "escrever à mão" o código
implementando iteradores.

As próximas seções apresentam versões mais idiomáticas de Sentence .

17.5.3. Sentence versão #3: uma funcão geradora


Uma implementação pythônica da mesma funcionalidade usa uma geradora, evitando todo o trabalho para
implementar a classe SentenceIterator . A explicação completa da geradora está logo após o Exemplo 5.

Exemplo 5. sentence_gen.py: Sentence implementada usando uma geradora

PY
import re
import reprlib

RE_WORD = re.compile(r'\w+')

class Sentence:

def __init__(self, text):


self.text = text
self.words = RE_WORD.findall(text)

def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)

def __iter__(self):
for word in self.words: # (1)
yield word # (2)
# (3)

# done! (4)

1. Itera sobre self.words .

2. Produz a word atual.


3. Um return explícito não é necessário; a função pode apenas seguir em frente e retornar automaticamente. De
qualquer das formas, uma função geradora não gera StopIteration : ela simplesmente termina quando acaba de
produzir valores.[214]
4. Não há necessidade de uma classe iteradora separada!

Novamente temos aqui uma implementação diferente de Sentence que passa nos testes do Exemplo 2.

No código de Sentence do Exemplo 4, __iter__ chamava o construtor SentenceIterator para criar e devolver um
iterador. Agora o iterador do Exemplo 5 é na verdade um objeto gerador, criado automaticamente quando o método
__iter__ é invocado, porque aqui __iter__ é uma função geradora.

Segue abaixo uma explicação completa das geradoras.

17.5.4. Como funciona uma geradora


Qualquer função do Python contendo a instrução yield em seu corpo é uma função geradora: uma função que,
quando invocada, devolve um objeto gerador. Em outras palavras, um função geradora é uma fábrica de geradores.
O único elemento sintático distinguindo uma função comum de uma função geradora é o fato dessa
última conter a instrução yield em algum lugar de seu corpo. Alguns defenderam que uma nova
👉 DICA palavra reservada, algo como gen , deveria ser usada no lugar de def para declarar funções
geradoras, mas Guido não concordou. Seus argumentos estão na PEP 255 — Simple Generators
(Geradoras Simples) (https://fpy.li/pep255).[215]

O Exemplo 6 mostra o comportamento de uma função geradora simples.[216]

Exemplo 6. Uma função geradora que produz três números

>>> def gen_123():


... yield 1 # (1)
... yield 2
... yield 3
...
>>> gen_123 # doctest: +ELLIPSIS
<function gen_123 at 0x...> # (2)
>>> gen_123() # doctest: +ELLIPSIS
<generator object gen_123 at 0x...> # (3)
>>> for i in gen_123(): # (4)
... print(i)
1
2
3
>>> g = gen_123() # (5)
>>> next(g) # (6)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g) # (7)
Traceback (most recent call last):
...
StopIteration

1. O corpo de uma função geradora muitas vezes contém yield dentro de um loop, mas não necessariamente; aqui
eu apenas repeti yield três vezes.
2. Olhando mais de perto, vemos que gen_123 é um objeto função.
3. Mas quando invocado, gen_123() devolve um objeto gerador.
4. Objetos geradores implementam a interface Iterator , então são também iteráveis.

5. Atribuímos esse novo objeto gerador a g , para podermos experimentar seu funcionamento.

6. Como g é um iterador, chamar next(g) obtém o próximo item produzido por yield .

7. Quando a função geradora retorna, o objeto gerador gera uma StopIteration .

Uma função geradora cria um objeto gerador que encapsula o corpo da função. Quando invocamos next() no objeto
gerador, a execução avança para o próximo yield no corpo da função, e a chamada a next() resulta no valor
produzido quando o corpo da função é suspenso. Por fim, o objeto gerador externo criado pelo Python gera uma
StopIteration quando a função retorna, de acordo com o protocolo Iterator .
Acho útil ser rigoroso ao falar sobre valores obtidos a partir de um gerador. É confuso dizer que um
gerador "devolve" valores. Funções devolvem valores. A chamada a uma função geradora devolve
um gerador. Um gerador produz (yields) valores. Um gerador não "devolve" valores no sentido
👉 DICA comum do termo: a instrução return no corpo de uma função geradora faz com que uma
StopIteration seja criada pelo objeto gerador. Se você escrever return x na função geradora,
quem a chamou pode recuperar o valor de x na exceção StopIteration , mas normalmente isso é
feito automaticamente usando a sintaxe yield from , como veremos na seção Seção 17.13.2.

O Exemplo 7 torna a iteração entre um loop for e o corpo da função mais explícita.

Exemplo 7. Uma função geradora que exibe mensagens quando roda

PYCON
>>> def gen_AB():
... print('start')
... yield 'A' # (1)
... print('continue')
... yield 'B' # (2)
... print('end.') # (3)
...
>>> for c in gen_AB(): # (4)
... print('-->', c) # (5)
...
start (6)
--> A (7)
continue (8)
--> B (9)
end. (10)
>>> (11)

1. A primeira chamada implícita a next() no loop for em 4 vai exibir 'start' e parar no primeiro yield ,
produzindo o valor 'A' .
2. A segunda chamada implícita a next() no loop for vai exibir 'continue' e parar no segundo yield ,
produzindo o valor 'B' .
3. A terceira chamada a next() vai exibir 'end.' e continuar até o final do corpo da função, fazendo com que o
objeto gerador crie uma StopIteration .
4. Para iterar, o mecanismo do for faz o equivalente a g = iter(gen_AB()) para obter um objeto gerador, e daí
next(g) a cada iteração.

5. O loop exibe -→ e o valor devolvido por next(g) . Esse resultado só aparece após a saída das chamadas print
dentro da função geradora.
6. O texto start vem de print('start') no corpo da geradora.
7. yield 'A' no corpo da geradora produz o valor 'A' consumido pelo loop for , que é atribuído à variável c e
resulta na saída -→ A .
8. A iteração continua com a segunda chamada a next(g) , avançando no corpo da geradora de yield 'A' para
yield 'B' . O texto continue é gerado pelo segundo print no corpo da geradora.

9. yield 'B' produz o valor 'B' consumido pelo loop for , que é atribuído à variável c do loop, que então exibe -→
B.

10. A iteração continua com uma terceira chamada a next(it) , avançando para o final do corpo da função. O texto
end. é exibido por causa do terceiro print no corpo da geradora.
11. Quando a função geradora chega ao final, o objeto gerador cria uma StopIteration . O mecanismo do loop for
captura essa exceção, e o loop encerra naturalmente.
Espero agora ter deixado claro como Sentence.__iter__ no Exemplo 5 funciona: __iter__ é uma função geradora
que, quando chamada, cria um objeto gerador que implementa a interface Iterator , então a classe
SentenceIterator não é mais necessária.

A segunda versão de Sentence é mais concisa que a primeira, mas não é tão preguiçosa quanto poderia ser.
Atualmente, a preguiça é considerada uma virtude, pelo menos em linguagens de programação e APIs. Uma
implementação preguiçosa adia a produção de valores até o último momento possível. Isso economiza memória e
também pode evitar o desperdício de ciclos da CPU.

Vamos criar a seguir classes Sentence preguiçosas.

17.6. Sentenças preguiçosas


As últimas variações de Sentence são preguiçosas, se valendo de um função preguiçosa do módulo re .

17.6.1. Sentence versão #4: uma geradora preguiçosa


A interface Iterator foi projetada para ser preguiçosa: next(my_iterator) produz um item por vez. O oposto de
preguiçosa é ansiosa: avaliação preguiçosa e ansiosa são termos técnicos da teoria das linguagens de programação[217].

Até aqui, nossas implementações de Sentence não são preguiçosas, pois o __init__ cria ansiosamente uma lista com
todas as palavras no texto, vinculando-as ao atributo self.words . Isso exige o processamento do texto inteiro, e a lista
pode acabar usando tanta memória quanto o próprio texto (provavelmente mais: vai depender de quantos caracteres
que não fazem parte de palavras existirem no texto). A maior parte desse trabalho será inútil se o usuário iterar apenas
sobre as primeiras palavras. Se você está se perguntado se "Existiria uma forma preguiçosa de fazer isso em Python?",
a resposta muitas vezes é "Sim".

A função re.finditer é uma versão preguiçosa de re.findall . Em vez de uma lista, re.finditer devolve uma
geradora que produz instâncias de re.MatchObject sob demanda. Se existirem muitos itens, re.finditer
economiza muita memória. Com ela, nossa terceira versão de Sentence agora é preguiçosa: ela só lê a próxima
palavra do texto quando necessário. O código está no Exemplo 8.

Exemplo 8. sentence_gen2.py: Sentence implementada usando uma função geradora que invoca a função geradora
re.finditer

PY
import re
import reprlib

RE_WORD = re.compile(r'\w+')

class Sentence:

def __init__(self, text):


self.text = text # (1)

def __repr__(self):
return f'Sentence({reprlib.repr(self.text)})'

def __iter__(self):
for match in RE_WORD.finditer(self.text): # (2)
yield match.group() # (3)

1. Não é necessário manter uma lista words .


2. finditer cria um iterador sobre os termos encontrados com RE_WORD em self.text , produzindo instâncias de
MatchObject .

3. match.group() extraí o texto da instância de MatchObject .


Geradores são um ótimo atalho, mas o código pode ser ainda mais conciso com uma expressão geradora.

17.6.2. Sentence versão #5: Expressão geradora preguiçosa


Podemos substituir funções geradoras simples como aquela na última classe `Sentence (no Exemplo 8) por uma
expressão geradora. Assim como uma compreensão de lista cria listas, uma expressão geradora cria objetos geradores.
O Exemplo 9 compara o comportamento nos dois casos.

Exemplo 9. A função geradora gen_AB é usada primeiro por uma compreensão de lista, depois por uma expressão
geradora

PYCON
>>> def gen_AB(): # (1)
... print('start')
... yield 'A'
... print('continue')
... yield 'B'
... print('end.')
...
>>> res1 = [x*3 for x in gen_AB()] # (2)
start
continue
end.
>>> for i in res1: # (3)
... print('-->', i)
...
--> AAA
--> BBB
>>> res2 = (x*3 for x in gen_AB()) # (4)
>>> res2
<generator object <genexpr> at 0x10063c240>
>>> for i in res2: # (5)
... print('-->', i)
...
start # (6)
--> AAA
continue
--> BBB
end.

1. Está é a mesma função gen_AB do Exemplo 7.


2. A compreensão de lista itera ansiosamente sobre os itens produzidos pelo objeto gerador devolvido por gen_AB() :
'A' e 'B' . Observe a saída nas linhas seguintes: start , continue , end.

3. Esse loop for itera sobre a lista res1 criada pela compreensão de lista.
4. A expressão geradora devolve res2 , um objeto gerador. O gerador não é consumido aqui.

5. Este gerador obtém itens de gen_AB apenas quando o loop for itera sobre res2 . Cada iteração do loop for
invoca, implicitamente, next(res2) , que por sua vez invoca next() sobre o objeto gerador devolvido por
gen_AB() , fazendo este último avançar até o próximo yield .

6. Observe como a saída de gen_AB() se intercala com a saída do print no loop for .

Podemos usar uma expressão geradora para reduzir ainda mais o código na classe Sentence . Veja o Exemplo 10.

Exemplo 10. sentence_genexp.py: Sentence implementada usando uma expressão geradora


PY
import re
import reprlib

RE_WORD = re.compile(r'\w+')

class Sentence:

def __init__(self, text):


self.text = text

def __repr__(self):
return f'Sentence({reprlib.repr(self.text)})'

def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))

A única diferença com o Exemplo 8 é o método __iter__ , que aqui não é uma função geradora (ela não contém uma
instrução yield ) mas usa uma expressão geradora para criar um gerador e devolvê-lo. O resultado final é o mesmo:
quem invoca __iter__ recebe um objeto gerador.

Expressões geradoras são "açúcar sintático": elas pode sempre ser substituídas por funções geradoras, mas algumas
vezes são mais convenientes. A próxima seção trata do uso de expressões geradoras.

17.7. Quando usar expressões geradoras


Eu usei várias expressões geradoras quando implementamos a classe Vector no Exemplo 16. Cada um destes métodos
contém uma expressão geradora: __eq__ , __hash__ , __abs__ , angle , angles , format , __add__ , e __mul__ . Em
todos aqueles métodos, uma compreensão de lista também funcionaria, com um custo adicional de memória para
armazenar os valores da lista intermediária.

No Exemplo 10, vimos que uma expressão geradora é um atalho sintático para criar um gerador sem definir e invocar
uma função. Por outro lado, funções geradoras são mais flexíveis: podemos programar uma lógica complexa, com
múltiplos comandos, e podemos até usá-las como corrotinas, como veremos na seção Seção 17.13.

Nos casos mais simples, uma expressão geradora é mais fácil de ler de relance, como mostra o exemplo de Vector .

Minha regra básica para escolher qual sintaxe usar é simples: se a expressão geradora exige mais que um par de
linhas, prefiro escrever uma função geradora, em nome da legibilidade.

Dica de sintaxe
Quando uma expressão geradora é passada como único argumento a uma função ou a um
construtor, não é necessário escrever um conjunto de parênteses para a chamada da função e outro
par cercando a expressão geradora. Um único par é suficiente, como na chamada a Vector no
método __mul__ do Exemplo 16, reproduzido abaixo:

👉 DICA def __mul__(self, scalar):


if isinstance(scalar, numbers.Real):
PYTHON3

return Vector(n * scalar for n in self)


else:
return NotImplemented

Entretanto, se existirem mais argumentos para a função após a expressão geradora, é preciso cercar
a expressão com parênteses para evitar um SyntaxError .
Os exemplos de Sentence vistos até aqui mostram geradores fazendo o papel do padrão Iterator clássico: obter itens
de uma coleção. Mas podemos também usar geradores para produzir valores independente de uma fonte de dados. A
próxima seção mostra um exemplo.

Mas antes, um pequena discussão sonre os conceitos sobrepostos de iterador e gerador.

Comparando iteradores e geradores


Na documentação e na base de código oficiais do Python, a terminologia em torno de iteradores e geradores é
inconsistente e está em evolução. Adotei as seguintes definições:

iterador
Termo geral para qualquer objeto que implementa um método __next__ . Iteradores são projetados para
produzir dados a serem consumidos pelo código cliente, isto é, o código que controla o iterador através de um
loop for ou outro mecanismo de iteração, ou chamando next(it) explicitamente no iterador—apesar desse
uso explícito ser menos comum. Na prática, a maioria dos iteradores que usamos no Python são geradores.

gerador
Um iterador criado pelo compilador Python. Para criar um gerador, não implementamos __next__ . Em vez
disso, usamos a palavra reservada yield para criar uma função geradora, que é uma fábrica de objetos
geradores. Uma expressão geradora é outra maneira de criar um objeto gerador. Objetos geradores fornecem
__next__ , então são iteradores. Desde o Python 3.5, também temos geradores assíncronos, declarados com
async def . Vamos estudá-los no Capítulo 21.

O Glossário do Python (https://docs.python.org/pt-br/3/glossary.html) introduziu recentemente o termo iterador gerador


(https://docs.python.org/pt-br/3/glossary.html#term-generator-iterator) para se referir a objetos geradores criados por
funções geradoras, enquanto o verbete para expressão geradora
(https://docs.python.org/pt-br/3/glossary.html#term-generator-expression) diz que ela devolve um "iterador".

Mas, de acordo com o interpretador Python, os objetos devolvidos em ambos os casos são objetos geradores:

PYCON
>>> def g():
... yield 0
...
>>> g()
<generator object g at 0x10e6fb290>
>>> ge = (c for c in 'XYZ')
>>> ge
<generator object <genexpr> at 0x10e936ce0>
>>> type(g()), type(ge)
(<class 'generator'>, <class 'generator'>)

17.8. Um gerador de progressão aritmética


O padrão Iterator clássico está todo baseado em uma travessia: navegar por alguma estrutura de dados. Mas uma
interface padrão baseada em um método para obter o próximo item em uma série também é útil quando os itens são
produzidos sob demanda, ao invés de serem obtidos de uma coleção. Por exemplo, a função embutida range gera uma
progressão aritmética (PA) de inteiros delimitada. E se precisarmos gerar uma PA com números de qualquer tipo, não
apenas inteiros?
O Exemplo 11 mostra alguns testes no console com uma classe ArithmeticProgression , que vermos em breve. A
assinatura do construtor no Exemplo 11 é ArithmeticProgression(begin, step[, end]) . A assinatura completa da
função embutida range é range(start, stop[, step]) . Escolhi implementar uma assinatura diferente porque o
step é obrigatório, mas end é opcional em uma progressão aritmética. Também mudei os nomes dos argumentos de
start/stop para begin/end , para deixar claro que optei por uma assinatura diferente. Para cada teste no Exemplo
11, chamo list() com o resultado para inspecionar o valores gerados.

Exemplo 11. Demonstração de uma classe ArithmeticProgression

PY
>>> ap = ArithmeticProgression(0, 1, 3)
>>> list(ap)
[0, 1, 2]
>>> ap = ArithmeticProgression(1, .5, 3)
>>> list(ap)
[1.0, 1.5, 2.0, 2.5]
>>> ap = ArithmeticProgression(0, 1/3, 1)
>>> list(ap)
[0.0, 0.3333333333333333, 0.6666666666666666]
>>> from fractions import Fraction
>>> ap = ArithmeticProgression(0, Fraction(1, 3), 1)
>>> list(ap)
[Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)]
>>> from decimal import Decimal
>>> ap = ArithmeticProgression(0, Decimal('.1'), .3)
>>> list(ap)
[Decimal('0'), Decimal('0.1'), Decimal('0.2')]

Observe que o tipo dos números na progressão aritmética resultante segue o tipo de begin + step , de acordo com as
regras de coerção numérica da aritmética do Python. No Exemplo 11, você pode ver listas de números int , float ,
Fraction , e Decimal . O Exemplo 12 mostra a implementação da classe ArithmeticProgression .

Exemplo 12. A classe ArithmeticProgression

PY
class ArithmeticProgression:

def __init__(self, begin, step, end=None): # (1)


self.begin = begin
self.step = step
self.end = end # None -> "infinite" series

def __iter__(self):
result_type = type(self.begin + self.step) # (2)
result = result_type(self.begin) # (3)
forever = self.end is None # (4)
index = 0
while forever or result < self.end: # (5)
yield result # (6)
index += 1
result = self.begin + self.step * index # (7)

1. __init__ exige dois argumentos: begin e step ; end é opcional, se for None , a série será ilimitada.

2. Obtém o tipo somando self.begin e self.step . Por exemplo, se um for int e o outro float , o result_type
será float .
3. Essa linha cria um result com o mesmo valor numérico de self.begin , mas coagido para o tipo das somas
subsequentes.[218]
4. Para melhorar a legibilidade, o sinalizador forever será True se o atributo self.end for None , resultando em
uma série ilimitada.
5. Esse loop roda forever ou até o resultado ser igual ou maior que self.end . Quando esse loop termina, a função
retorna.
6. O result atual é produzido.
7. O próximo resultado em potencial é calculado. Ele pode nunca ser produzido, se o loop while terminar.
Na última linha do Exemplo 12, em vez de somar self.step ao result anterior a cada passagem do loop, optei por
ignorar o result existente: cada novo result é criado somando self.begin a self.step multiplicado por
index . Isso evita o efeito cumulativo de erros após a adição sucessiva de números de ponto flutuante. Alguns
experimentos simples tornam clara a diferença:

PYCON
>>> 100 * 1.1
110.00000000000001
>>> sum(1.1 for _ in range(100))
109.99999999999982
>>> 1000 * 1.1
1100.0
>>> sum(1.1 for _ in range(1000))
1100.0000000000086

A classe ArithmeticProgression do Exemplo 12 funciona como esperado, é outro exemplo do uso de uma função
geradora para implementar o método especial __iter__ . Entretanto, se o único objetivo de uma classe é criar um
gerador pela implementação de __iter__ , podemos substituir a classe por uma função geradora. Pois afinal, uma
função geradora é uma fábrica de geradores.

O Exemplo 13 mostra uma função geradora chamada aritprog_gen , que realiza a mesma tarefa da
ArithmeticProgression , mas com menos código. Se, em vez de chamar ArithmeticProgression , você chamar
aritprog_gen , os testes no Exemplo 11 são todos bem sucedidos.[219]

Exemplo 13. a função geradora aritprog_gen

PY
def aritprog_gen(begin, step, end=None):
result = type(begin + step)(begin)
forever = end is None
index = 0
while forever or result < end:
yield result
index += 1
result = begin + step * index

O Exemplo 13 é elegante, mas lembre-se sempre: há muitos geradores prontos para uso na biblioteca padrão, e a
próxima seção vai mostrar uma implementação mais curta, usando o módulo itertools .

17.8.1. Progressão aritmética com itertools


O módulo itertools no Python 3.10 contém 20 funções geradoras, que podem ser combinadas de várias maneiras
interessantes.

Por exemplo, a função itertools.count devolve um gerador que produz números. Sem argumentos, ele produz uma
série de inteiros começando de 0 . Mas você pode fornecer os valores opcionais start e step , para obter um
resultado similar ao das nossas funções aritprog_gen :
PYCON
>>> import itertools
>>> gen = itertools.count(1, .5)
>>> next(gen)
1
>>> next(gen)
1.5
>>> next(gen)
2.0
>>> next(gen)
2.5

itertools.count nunca para, então se você chamar list(count()) , o Python vai tentar criar
⚠️ AVISO uma list que preencheria todos os chips de memória já fabricados. Na prática, sua máquina vai
ficar muito mal-humorada bem antes da chamada fracassar.

Por outro lado, temos também a função itertools.takewhile : ela devolve um gerador que consome outro gerador e
para quando um dado predicado é avaliado como False . Então podemos combinar os dois e escrever o seguinte:

PYCON
>>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
>>> list(gen)
[1, 1.5, 2.0, 2.5]

Se valendo de takewhile e count , o Exemplo 14 é ainda mais conciso.

Exemplo 14. aritprog_v3.py: funciona como as funções aritprog_gen anteriores

PY
import itertools

def aritprog_gen(begin, step, end=None):


first = type(begin + step)(begin)
ap_gen = itertools.count(first, step)
if end is None:
return ap_gen
return itertools.takewhile(lambda n: n < end, ap_gen)

Observe que aritprog_gen no Exemplo 14 não é uma função geradora: não há um yield em seu corpo. Mas ela
devolve um gerador, exatamente como faz uma função geradora.

Entretanto, lembre-se que itertools.count soma o step repetidamente, então a série de números de ponto
flutuante que ela produz não é tão precisa quanto a do Exemplo 13.

O importante no Exemplo 14 é: ao implementar geradoras, olhe o que já está disponível na biblioteca padrão, caso
contrário você tem uma boa chance de reinventar a roda. Por isso a próxima seção trata de várias funções geradoras
prontas para usar.

17.9. Funções geradoras na biblioteca padrão


A biblioteca padrão oferece muitas geradoras, desde objetos de arquivo de texto forncendo iteração linha por linha até
a incrível função os.walk (https://fpy.li/17-12), que produz nomes de arquivos enquanto cruza uma árvore de diretórios,
tornando buscas recursivas no sistema de arquivos tão simples quanto um loop for .
A função geradora os.walk é impressionante, mas nesta seção quero me concentrar em funções genéricas que
recebem iteráveis arbitrários como argumento e devolvem geradores que produzem itens selecionados, calculados ou
reordenados. Nas tabelas a seguir, resumi duas dúzias delas, algumas embutidas, outras dos módulos itertools e
functools . Por conveniência, elas estão agrupadas por sua funcionalidade de alto nível, independente de onde são
definidas.

O primeiro grupo contém funções geradoras de filtragem: elas produzem um subconjunto dos itens produzidos pelo
iterável de entrada, sem mudar os itens em si. Como takewhile , a maioria das funções listadas na Tabela 19 recebe
um predicate , uma função booleana de um argumento que será aplicada a cada item no iterável de entrada, para
determinar se aquele item será incluído na saída.

Tabela 19. Funções geradoras de filtragem


Módulo Função Descrição

itertools compress(it, selector_it) Consome dois iteráveis em paralelo;


produz itens de it sempre que o
item correspondente em
selector_it é verdadeiro

itertools dropwhile(predicate, it) Consome it , pulando itens


enquanto predicate resutar
verdadeiro, e daí produz todos os
elementos restantes (nenhuma
verificação adicional é realizada)

(Embutida) filter(predicate, it) Aplica predicate para cada item


de iterable , produzindo o item se
predicate(item) for verdadeiro;
se predicate for None , apenas
itens verdadeiros serão produzidos

itertools filterfalse(predicate, it) Igual a filter , mas negando a


lógica de predicate : produz itens
sempre que predicate resultar
falso

itertools islice(it, stop) ou islice(it, Produz itens de uma fatia de it ,


start, stop, step=1) similar a s[:stop] ou
s[start:stop:step] , exceto por
it poder ser qualquer iterável e a
operação ser preguiçosa

itertools takewhile(predicate, it) Produz itens enquanto predicate


resultar verdadeiro, e daí para
(nenhuma verificação adicional é
realizada).

A seção de console no Exemplo 15 demonstra o uso de todas as funções na Tabela 19.

Exemplo 15. Exemplos de funções geradoras de filtragem


PYCON
>>> def vowel(c):
... return c.lower() in 'aeiou'
...
>>> list(filter(vowel, 'Aardvark'))
['A', 'a', 'a']
>>> import itertools
>>> list(itertools.filterfalse(vowel, 'Aardvark'))
['r', 'd', 'v', 'r', 'k']
>>> list(itertools.dropwhile(vowel, 'Aardvark'))
['r', 'd', 'v', 'a', 'r', 'k']
>>> list(itertools.takewhile(vowel, 'Aardvark'))
['A', 'a']
>>> list(itertools.compress('Aardvark', (1, 0, 1, 1, 0, 1)))
['A', 'r', 'd', 'a']
>>> list(itertools.islice('Aardvark', 4))
['A', 'a', 'r', 'd']
>>> list(itertools.islice('Aardvark', 4, 7))
['v', 'a', 'r']
>>> list(itertools.islice('Aardvark', 1, 7, 2))
['a', 'd', 'a']

O grupo seguinte contém os geradores de mapeamento: eles produzem itens computados a partir de cada item
individual no iterável de entrada—​ou iteráveis, nos casos de map e starmap .[220] As geradoras na Tabela 20
produzem um resultado por item dos iteráveis de entrada. Se a entrada vier de mais de um iterável, a saída para assim
que o primeiro iterável de entrada for exaurido.

Tabela 20. Funções geradoras de mapeamento


Módulo Função Descrição

itertools accumulate(it, [func]) Produz somas cumulativas; se func


for fornecida, produz o resultado da
aplicação de func ao primeiro par
de itens, depois ao primeiro
resultado e ao próximo item, etc.

(embutida) enumerate(iterable, start=0) Produz tuplas de dois itens na forma


(index, item) , onde index é
contado a partir de start , e item
é obtido do iterable

(embutida) map(func, it1, [it2, …, itN]) Aplica func a cada item de it ,


produzindo o resultado; se forem
fornecidos N iteráveis, func deve
aceitar N argumentos, e os iteráveis
serão consumidos em paralelo

itertools starmap(func, it) Aplica func a cada item de it ,


produzindo o resultado; o iterável
de entrada deve produzir itens
iteráveis iit , e func é aplicada na
forma func(*iit)

O Exemplo 16 demonstra alguns usos de itertools.accumulate .


Exemplo 16. Exemplos das funções geradoras de itertools.accumulate

PYCON
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> import itertools
>>> list(itertools.accumulate(sample)) # (1)
[5, 9, 11, 19, 26, 32, 35, 35, 44, 45]
>>> list(itertools.accumulate(sample, min)) # (2)
[5, 4, 2, 2, 2, 2, 2, 0, 0, 0]
>>> list(itertools.accumulate(sample, max)) # (3)
[5, 5, 5, 8, 8, 8, 8, 8, 9, 9]
>>> import operator
>>> list(itertools.accumulate(sample, operator.mul)) # (4)
[5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0]
>>> list(itertools.accumulate(range(1, 11), operator.mul))
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] # (5)

1. Soma acumulada.
2. Mínimo corrente.
3. Máximo corrente.
4. Produto acumulado.
5. Fatoriais de 1! a 10! .

As funções restantes da Tabela 20 são demonstradas no Exemplo 17.

Exemplo 17. Exemplos de funções geradoras de mapeamento

PYCON
>>> list(enumerate('albatroz', 1)) # (1)
[(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')]
>>> import operator
>>> list(map(operator.mul, range(11), range(11))) # (2)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> list(map(operator.mul, range(11), [2, 4, 8])) # (3)
[0, 4, 16]
>>> list(map(lambda a, b: (a, b), range(11), [2, 4, 8])) # (4)
[(0, 2), (1, 4), (2, 8)]
>>> import itertools
>>> list(itertools.starmap(operator.mul, enumerate('albatroz', 1))) # (5)
['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> list(itertools.starmap(lambda a, b: b / a,
... enumerate(itertools.accumulate(sample), 1))) # (6)
[5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333,
5.0, 4.375, 4.888888888888889, 4.5]

1. Número de letras na palavra, começando por 1.

2. Os quadrados dos inteiros de 0 a 10 .

3. Multiplicando os números de dois iteráveis em paralelo; os resultados cessam quando o iterável menor termina.
4. Isso é o que faz a função embutida zip .

5. Repete cada letra na palavra de acordo com a posição da letra na palavra, começando por 1.

6. Média corrente.
A seguir temos o grupo de geradores de fusão—todos eles produzem itens a partir de múltiplos iteráveis de entrada.
chain e chain.from_iterable consomem os iteráveis de entrada em sequência (um após o outro), enquanto
product , zip , e zip_longest consomem os iteráveis de entrada em paralelo. Veja a Tabela 21.

Tabela 21. Funções geradoras que fundem os iteráveis de entrada

Módulo Função Descrição

itertools chain(it1, …, itN) Produz todos os itens de it1 , a


seguir de it2 , etc., continuamente.

itertools chain.from_iterable(it) Produz todos os itens de cada


iterável produzido por it , um após
o outro, continuamente; it é um
iterável cujos itens também são
iteráveis, uma lista de tuplas, por
exemplo

itertools product(it1, …, itN, repeat=1) Produto cartesiano: produz tuplas


de N elementos criadas combinando
itens de cada iterável de entrada,
como loops for aninhados
produziriam; repeat permite que
os iteráveis de entrada sejam
consumidos mais de uma vez

(embutida) zip(it1, …, itN, strict=False) Produz tuplas de N elementos


criadas a partir de itens obtidos dos
iteráveis em paralelo, terminando
silenciosamente quando o menor
iterável é exaurido, a menos que
strict=True for passado[221]

itertools zip_longest(it1, …, itN, Produz tuplas de N elementos


fillvalue=None) criadas a partir de itens obtidos dos
iteráveis em paralelo, terminando
apenas quando o último iterável for
exaurido, preenchendo os itens
ausentes com o fillvalue

O Exemplo 18 demonstra o uso das funções geradoras itertools.chain e zip , e de suas pares. Lembre-se que o
nome da função zip vem do zíper ou fecho-éclair (nenhuma relação com a compreensão de dados). Tanto zip
quanto itertools.zip_longest foram apresentadas no O fantástico zip.

Exemplo 18. Exemplos de funções geradoras de fusão


PYCON
>>> list(itertools.chain('ABC', range(2))) # (1)
['A', 'B', 'C', 0, 1]
>>> list(itertools.chain(enumerate('ABC'))) # (2)
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> list(itertools.chain.from_iterable(enumerate('ABC'))) # (3)
[0, 'A', 1, 'B', 2, 'C']
>>> list(zip('ABC', range(5), [10, 20, 30, 40])) # (4)
[('A', 0, 10), ('B', 1, 20), ('C', 2, 30)]
>>> list(itertools.zip_longest('ABC', range(5))) # (5)
[('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
>>> list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # (6)
[('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]

1. chain é normalmente invocada com dois ou mais iteráveis.


2. chain não faz nada de útil se invocada com um único iterável.
3. Mas chain.from_iterable pega cada item do iterável e os encadeia em sequência, desde que cada item seja
também iterável.
4. Qualquer número de iteráveis pode ser consumido em paralelo por zip , mas a geradora sempre para assim que o
primeiro iterável acaba. No Python ≥ 3.10, se o argumento strict=True for passado e um iterável terminar antes
dos outros, um ValueError é gerado.
5. itertools.zip_longest funciona como zip , exceto por consumir todos os iteráveis de entrada, preenchendo as
tuplas de saída com None onde necessário.
6. O argumento nomeado fillvalue especifica um valor de preenchimento personalizado.

A geradora itertools.product é uma forma preguiçosa para calcular produtos cartesianos, que criamos usando
compreensões de lista com mais de uma instrução for na seção Seção 2.3.3. Expressões geradoras com múltiplas
instruções for também podem ser usadas para produzir produtos cartesianos de forma preguiçosa. O Exemplo 19
demonstra itertools.product .

Exemplo 19. Exemplo da função geradora itertools.product


PYCON
>>> list(itertools.product('ABC', range(2))) # (1)
[('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]
>>> suits = 'spades hearts diamonds clubs'.split()
>>> list(itertools.product('AK', suits)) # (2)
[('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'),
('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')]
>>> list(itertools.product('ABC')) # (3)
[('A',), ('B',), ('C',)]
>>> list(itertools.product('ABC', repeat=2)) # (4)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'),
('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
>>> list(itertools.product(range(2), repeat=3))
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0),
(1, 0, 1), (1, 1, 0), (1, 1, 1)]
>>> rows = itertools.product('AB', range(2), repeat=2)
>>> for row in rows: print(row)
...
('A', 0, 'A', 0)
('A', 0, 'A', 1)
('A', 0, 'B', 0)
('A', 0, 'B', 1)
('A', 1, 'A', 0)
('A', 1, 'A', 1)
('A', 1, 'B', 0)
('A', 1, 'B', 1)
('B', 0, 'A', 0)
('B', 0, 'A', 1)
('B', 0, 'B', 0)
('B', 0, 'B', 1)
('B', 1, 'A', 0)
('B', 1, 'A', 1)
('B', 1, 'B', 0)
('B', 1, 'B', 1)

1. O produto cartesiano de uma str com três caracteres e um range com dois inteiros produz seis tuplas (porque 3
* 2 é 6 ).

2. O produto de duas cartas altas ( 'AK' ) e quatro naipes é uma série de oito tuplas.
3. Dado um único iterável, product produz uma série de tuplas de um elemento—muito pouco útil.
4. O argumento nomeado repeat=N diz à função para consumir cada iterável de entrada N vezes.

Algumas funções geradoras expandem a entrada, produzindo mais de um valor por item de entrada. Elas estão listadas
na Tabela 22.

Tabela 22. Funções geradoras que expandem cada item de entrada em múltiplos itens de saída
Module Function Description

itertools combinations(it, out_len) Produz combinações de out_len


itens a partir dos itens produzidos
por it

itertools combinations_with_replacement(it, Produz combinações de out_len


out_len) itens a partir dos itens produzidos
por it , incluindo combinações
com itens repetidos
Module Function Description

itertools count(start=0, step=1) Produz números começando em


start e adicionando step para
obter o número seguinte,
indefinidamente

itertools cycle(it) Produz itens de it, armazenando


uma cópia de cada, e então produz
a sequência inteira repetida e
indefinidamente

itertools pairwise(it) Produz pares sobrepostos


sucessivos, obtidos do iterável de
entrada[222]

itertools permutations(it, out_len=None) Produz permutações de out_len


itens a partir dos itens produzidos
por it ; por default, out_len é
len(list(it))

itertools repeat(item, [times]) Produz um dado item


repetidamente e, a menos que um
número de times (vezes) seja
passado, indefinidamente

As funções count e repeat de itertools devolvem geradores que conjuram itens do nada: nenhum deles recebe
um iterável como parâmetro. Vimos itertools.count na seção Seção 17.8.1. O gerador cycle faz uma cópia do
iterável de entrada e produz seus itens repetidamente. O Exemplo 20 ilustra o uso de count , cycle , pairwise e
repeat .

Exemplo 20. count , cycle , pairwise , e repeat

PYCON
>>> ct = itertools.count() # (1)
>>> next(ct) # (2)
0
>>> next(ct), next(ct), next(ct) # (3)
(1, 2, 3)
>>> list(itertools.islice(itertools.count(1, .3), 3)) # (4)
[1, 1.3, 1.6]
>>> cy = itertools.cycle('ABC') # (5)
>>> next(cy)
'A'
>>> list(itertools.islice(cy, 7)) # (6)
['B', 'C', 'A', 'B', 'C', 'A', 'B']
>>> list(itertools.pairwise(range(7))) # (7)
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]
>>> rp = itertools.repeat(7) # (8)
>>> next(rp), next(rp)
(7, 7)
>>> list(itertools.repeat(8, 4)) # (9)
[8, 8, 8, 8]
>>> list(map(operator.mul, range(11), itertools.repeat(5))) # (10)
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
1. Cria ct , uma geradora count .

2. Obtém o primeiro item de ct .

3. Não posso criar uma list a partir de ct , pois ct nunca para. Então pego os próximos três itens.
4. Posso criar uma list de uma geradora count se ela for limitada por islice ou takewhile .

5. Cria uma geradora cycle a partir de 'ABC' , e obtem seu primeiro item, 'A' .

6. Uma list só pode ser criada se limitada por islice ; os próximos sete itens são obtidos aqui.

7. Para cada item na entrada, pairwise produz uma tupla de dois elementos com aquele item e o próximo—se
existir um próximo item. Disponível no Python ≥ 3.10.
8. Cria uma geradora repeat que vai produzir o número 7 para sempre.
9. Uma geradora repeat pode ser limitada passando o argumento times : aqui o número 8 será produzido 4
vezes.
10. Um uso comum de repeat : fornecer um argumento fixo em map ; aqui ela fornece o multiplicador 5 .

A funções geradoras combinations , combinations_with_replacement e permutations --juntamente com product


—são chamadas geradoras combinatórias na página de documentação do itertools
(https://docs.python.org/pt-br/3/library/itertools.html). Também há um relação muito próxima entre itertools.product e o
restante das funções combinatórias, como mostra o Exemplo 21.

Exemplo 21. Funções geradoras combinatórias produzem múltiplos valores para cada item de entrada

PYCON
>>> list(itertools.combinations('ABC', 2)) # (1)
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(itertools.combinations_with_replacement('ABC', 2)) # (2)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
>>> list(itertools.permutations('ABC', 2)) # (3)
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
>>> list(itertools.product('ABC', repeat=2)) # (4)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'),
('C', 'A'), ('C', 'B'), ('C', 'C')]

1. Todas as combinações com len()==2 a partir dos itens em 'ABC' ; a ordem dos itens nas tuplas geradas é
irrelevante (elas poderiam ser conjuntos).
2. Todas as combinação com len()==2 a partir dos itens em 'ABC' , incluindo combinações com itens repetidos.

3. Todas as permutações com len()==2 a partir dos itens em 'ABC' ; a ordem dos itens nas tuplas geradas é
relevante.
4. Produto cartesiano de 'ABC' e 'ABC' (esse é o efeito de repeat=2 ).

O último grupo de funções geradoras que vamos examinar nessa seção foram projetados para produzir todos os itens
dos iteráveis de entrada, mas rearranjados de alguma forma. Aqui estão duas funções que devolvem múltiplos
geradores: itertools.groupby e itertools.tee . A outra geradora nesse grupo, a função embutida reversed , é a
única geradora tratada nesse capítulo que não aceita qualquer iterável como entrada, apenas sequências. Faz sentido:
como reversed vai produzir os itens do último para o primeiro, só funciona com uma sequência de tamanho
conhecido. Mas ela evita o custo de criar uma cópia invertida da sequência produzindo cada item quando necessário.
Coloquei a função itertools.product junto com as geradoras de fusão, na Tabela 21, porque todas aquelas
consomem mais de um iterável, enquanto todas as geradoras na Tabela 23 aceitam no máximo um iterável como
entrada.
Tabela 23. Funcões geradoras de rearranjo
Módulo Função Descrição

itertools groupby(it, key=None) Produz tuplas de 2 elementos na


forma (key, group) , onde key é o
critério de agrupamento e group é
um gerador que produz os itens no
grupo

(embutida) reversed(seq) Produz os itens de seq na ordem


inversa, do último para o primeiro;
seq deve ser uma sequência ou
implementar o método especial
__reversed__

itertools tee(it, n=2) Produz uma tupla de n geradores,


cada um produzindo os itens do
iterável de entrada de forma
independente

O Exemplo 22 demonstra o uso de itertools.groupby e da função embutida reversed . Observe que


itertools.groupby assume que o iterável de entrada está ordenado pelo critério de agrupamento, ou que pelo
menos os itens estejam agrupados por aquele critério—mesmo que não estejam completamente ordenados. O revisor
técnico Miroslav Šedivý sugeriu esse caso de uso: você pode ordenar objetos datetime em ordem cronológica, e então
groupby por dia da semana, para obter o grupo com os dados de segunda-feira, seguidos pelos dados de terça, etc., e
então da segunda (da semana seguinte) novamente, e assim por diante.

Exemplo 22. itertools.groupby


PYCON
>>> list(itertools.groupby('LLLLAAGGG')) # (1)
[('L', <itertools._grouper object at 0x102227cc0>),
('A', <itertools._grouper object at 0x102227b38>),
('G', <itertools._grouper object at 0x102227b70>)]
>>> for char, group in itertools.groupby('LLLLAAAGG'): # (2)
... print(char, '->', list(group))
...
L -> ['L', 'L', 'L', 'L']
A -> ['A', 'A',]
G -> ['G', 'G', 'G']
>>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear',
... 'bat', 'dolphin', 'shark', 'lion']
>>> animals.sort(key=len) # (3)
>>> animals
['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark',
'giraffe', 'dolphin']
>>> for length, group in itertools.groupby(animals, len): # (4)
... print(length, '->', list(group))
...
3 -> ['rat', 'bat']
4 -> ['duck', 'bear', 'lion']
5 -> ['eagle', 'shark']
7 -> ['giraffe', 'dolphin']
>>> for length, group in itertools.groupby(reversed(animals), len): # (5)
... print(length, '->', list(group))
...
7 -> ['dolphin', 'giraffe']
5 -> ['shark', 'eagle']
4 -> ['lion', 'bear', 'duck']
3 -> ['bat', 'rat']
>>>

1. groupby produz tuplas de (key, group_generator) .

2. Tratar geradoras groupby envolve iteração aninhada: neste caso, o loop for externo e o construtor de list
interno.
3. Ordena animals por tamanho.
4. Novamente, um loop sobre o par key e group , para exibir key e expandir o group em uma list .

5. Aqui a geradora reverse itera sobre animals da direita para a esquerda.

A última das funções geradoras nesse grupo é iterator.tee , que apresenta um comportamento singular: ela produz
múltiplos geradores a partir de um único iterável de entrada, cada um deles produzindo todos os itens daquele iterável.
Esse geradores podem ser consumidos de forma independente, como mostra o Exemplo 23.

Exemplo 23. itertools.tee produz múltiplos geradores, cada um produzindo todos os itens do gerador de entrada
PYCON
>>> list(itertools.tee('ABC'))
[<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>]
>>> g1, g2 = itertools.tee('ABC')
>>> next(g1)
'A'
>>> next(g2)
'A'
>>> next(g2)
'B'
>>> list(g1)
['B', 'C']
>>> list(g2)
['C']
>>> list(zip(*itertools.tee('ABC')))
[('A', 'A'), ('B', 'B'), ('C', 'C')]

Observe que vários exemplos nesta seção usam combinações de funções geradoras. Essa é uma excelente característica
dessas funções: como recebem como argumentos e devolvem geradores, elas podem ser combinadas de muitas formas
diferentes.

Vamos agora revisar outro grupo de funções da biblioteca padrão que lidam com iteráveis.

17.10. Funções de redução de iteráveis


Todas as funções na Tabela 24 recebem um iterável e devolvem um resultado único. Elas são conhecidas como funções
de "redução", "dobra" (folding) ou "acumulação". Podemos implementar cada uma das funções embutidas listadas a
seguir com functools.reduce , mas elas existem embutidas por resolverem algums casos de uso comuns de forma
mais fácil. Já vimos uma explicação mais aprofundada sobre functools.reduce na seção Seção 12.7.

Nos casos de all e any , há uma importante otimização não suportada por functools.reduce : all e any
conseguem criar um curto-circuito—isto é, elas param de consumir o iterador assim que o resultado esteja
determinado. Veja o último teste com any no Exemplo 24.

Tabela 24. Funções embutidas que leem iteráveis e devolvem um único valor
Módulo Função Descrição

(embutida) all(it) Devolve True se todos os itens em


it forem verdadeiros, False em
caso contrário; all([]) devolve
True

(embutida) any(it) Devolve True se qualquer item em


it for verdadeiro, False em caso
contrário; any([]) devolve False

(embutida) max(it, [key=,] [default=]) Devolve o valor máximo entre os


itens de it ;[223] key é uma função
de ordenação, como em sorted ;
default é devolvido se o iterável
estiver vazio
Módulo Função Descrição

(embutida) min(it, [key=,] [default=]) Devolve o valor mínimo entre os


itens de it .[224] key é uma função
de ordenação, como em sorted ;
default é devolvido se o iterável
estiver vazio

functools reduce(func, it, [initial]) Devolve o resultado da aplicação de


func consecutivamente ao
primeiro par de itens, depois deste
último resultado e o terceito item, e
assim por diante; se initial for
passado, esse argumento formará o
par inicial com o primeiro item

(embutida) sum(it, start=0) A soma de todos os itens em it ,


acrescida do valor opcional start
(para uma precisão melhor na
adição de números de ponto
flutuante, use math.fsum )

O Exemplo 24 exemplifica a operação de all e de any .

Exemplo 24. Resultados de all e any para algumas sequências

PYCON
>>> all([1, 2, 3])
True
>>> all([1, 0, 3])
False
>>> all([])
True
>>> any([1, 2, 3])
True
>>> any([1, 0, 3])
True
>>> any([0, 0.0])
False
>>> any([])
False
>>> g = (n for n in [0, 0.0, 7, 8])
>>> any(g) # (1)
True
>>> next(g) # (2)
8

1. any iterou sobre g até g produzir 7 ; neste momento any parou e devolveu True .

2. É por isso que 8 ainda restava.

Outra função embutida que recebe um iterável e devolve outra coisa é sorted . Diferente de reversed , que é uma
função geradora, sorted cria e devolve uma nova list . Afinal, cada um dos itens no iterável de entrada precisa ser
lido para que todos possam ser ordenados, e a ordenação acontece em uma list ; sorted então apenas devolve
aquela list após terminar seu processamento. Menciono sorted aqui porque ela consome um iterável arbitrário.
Claro, sorted e as funções de redução só funcionam com iteráveis que terminam em algum momento. Caso contrário,
eles seguirão coletando itens e nunca devolverão um resultado.

Se você chegou até aqui, já viu o conteúdo mais importante e útil deste capítulo. As seções restantes
tratam de recursos avançados de geradores, que a maioria de nós não vê ou precisa com muita
✒️ NOTA frequência, tal como a instrução yield from e as corrotinas clássicas.

Há também seções sobre dicas de tipo para iteráveis, iteradores e corrotinas clássicas.

A sintaxe yield from fornece uma nova forma de combinar geradores. É nosso próximo assunto.

17.11. Subgeradoras com yield from


A sintaxe da expressão yield from foi introduzida no Python 3.3, para permitir que um gerador delegue tarefas a um
subgerador.

Antes da introdução de yield from , usávamos um loop for quando um gerador precisava produzir valores de outro
gerador:

PYCON
>>> def sub_gen():
... yield 1.1
... yield 1.2
...
>>> def gen():
... yield 1
... for i in sub_gen():
... yield i
... yield 2
...
>>> for x in gen():
... print(x)
...
1
1.1
1.2
2

Podemos obter o mesmo resultado usando yield from , como se vê no Exemplo 25.

Exemplo 25. Experimentando yield from

PYCON
>>> def sub_gen():
... yield 1.1
... yield 1.2
...
>>> def gen():
... yield 1
... yield from sub_gen()
... yield 2
...
>>> for x in gen():
... print(x)
...
1
1.1
1.2
2
No Exemplo 25, o loop for é o código cliente, gen é o gerador delegante e sub_gen é o subgerador. Observe que
yield from suspende gen , e sub_gen toma o controle até se exaurir. Os valores produzidos por sub_gen passam
através de gen diretamente para o loop for cliente. Enquanto isso, gen está suspenso e não pode ver os valores que
passam por ele. gen continua apenas quando sub_gen termina.

Quando o subgerador contém uma instrução return com um valor, aquele valor pode ser capturado pelo gerador
delegante, com o uso de yield from como parte de uma expressão. Veja a demonstração no Exemplo 26.

Exemplo 26. yield from recebe o valor devolvido pelo subgerador

PYCON
>>> def sub_gen():
... yield 1.1
... yield 1.2
... return 'Done!'
...
>>> def gen():
... yield 1
... result = yield from sub_gen()
... print('<--', result)
... yield 2
...
>>> for x in gen():
... print(x)
...
1
1.1
1.2
<-- Done!
2

Agora que já vimos o básico sobre yield from , vamos estudar alguns exemplos simples mas práticos de sua utilização.

17.11.1. Reinventando chain


Vimos na Tabela 21 que itertools fornece uma geradora chain , que produz itens a partir de vários iteráveis,
iterando sobre o primeiro, depois sobre o segundo, e assim por diante, até o último. Abaixo está uma implementação
caseira de chain , com loops for aninhados, em Python:[225]

PYCON
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = 'ABC'
>>> r = range(3)
>>> list(chain(s, r))
['A', 'B', 'C', 0, 1, 2]

A geradora chain , no código acima, está delegando para cada iterável it , controlando cada it no loop for interno.
Aquele loop interno pode ser substituído por uma expressão yield from , como mostra a seção de console a seguir:

PYCON
>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
O uso de yield from neste exemplo está correto, e o código é mais legível, mas parece açúcar sintático, com pouco
ganho real. Vamos então desenvolver um exemplo mais interessante.

17.11.2. Percorrendo uma árvore


Nessa seção, veremos yield from em um script para percorrer uma estrutura de árvore. Vou desenvolvê-lo bem
devagar.

A estrutura de árvore nesse exemplo é a hierarquia das exceções


(https://docs.python.org/pt-br/3/library/exceptions.html#exception-hierarchy) do Python. Mas o padrão pode ser adaptado para
exibir uma árvore de diretórios ou qualquer outra estrutura de árvore.

Começando de BaseException no nível zero, a hierarquia de exceções tem cinco níveis de profundidade no Python
3.10. Nosso primeiro pequeno passo será exibir o nível zero.

Dada uma classe raiz, a geradora tree no Exemplo 27 produz o nome dessa classe e para.

Exemplo 27. tree/step0/tree.py: produz o nome da classe raiz e para

PY
def tree(cls):
yield cls.__name__

def display(cls):
for cls_name in tree(cls):
print(cls_name)

if __name__ == '__main__':
display(BaseException)

A saída do Exemplo 27 tem apenas uma linha:

BASH
BaseException

O próximo pequeno passo nos leva ao nível 1. A geradora tree irá produzir o nome da classe raiz e os nomes de cada
subclasse direta. Os nomes das subclasses são indentados para explicitar a hierarquia. Esta é a saída que queremos:

BASH
$ python3 tree.py
BaseException
Exception
GeneratorExit
SystemExit
KeyboardInterrupt

O Exemplo 28 produz a saída acima.

Exemplo 28. tree/step1/tree.py: produz o nome da classe raiz e das subclasses diretas
PY
def tree(cls):
yield cls.__name__, 0 # (1)
for sub_cls in cls.__subclasses__(): # (2)
yield sub_cls.__name__, 1 # (3)

def display(cls):
for cls_name, level in tree(cls):
indent = ' ' * 4 * level # (4)
print(f'{indent}{cls_name}')

if __name__ == '__main__':
display(BaseException)

1. Para suportar a saída indentada, produz o nome da classe e seu nível na hierarquia.
2. Usa o método especial __subclasses__ para obter uma lista de subclasses.
3. Produz o nome da subclasse e o nível ( 1 ).
4. Cria a string de indentação de 4 espaços vezes o level . No nível zero, isso será uma string vazia.

No Exemplo 29, refatorei tree para separar o caso especial da classes raiz de suas subclasses, que agora são
processadas na geradora sub_tree . Em yield from , a geradora tree é suspensa, e sub_tree passa a produzir
valores.

Exemplo 29. tree/step2/tree.py: tree produz o nome da classe raiz, e entao delega para sub_tree

PY
def tree(cls):
yield cls.__name__, 0
yield from sub_tree(cls) # (1)

def sub_tree(cls):
for sub_cls in cls.__subclasses__():
yield sub_cls.__name__, 1 # (2)

def display(cls):
for cls_name, level in tree(cls): # (3)
indent = ' ' * 4 * level
print(f'{indent}{cls_name}')

if __name__ == '__main__':
display(BaseException)

1. Delega para sub_tree , para produzir os nomes das subclasses.

2. Produz o nome de cada subclasse e o nível ( 1 ). Por causa do yield from sub_tree(cls) dentro de tree , esses
valores escapam completamente à geradora tree …​
3. …​e são recebidos aqui diretamente.

Seguindo com nosso método de pequenos passos, vou escrever o código mais simples que consigo imaginar para
chegar ao nível 2. Para percorrer uma árvore primeiro em produndidade (depth-first)
(https://pt.wikipedia.org/wiki/Busca_em_profundidade), após produzir cada nó do nível 1, quero produzir os filhotes daquele
nó no nível 2 antes de voltar ao nível 1. Um loop for aninhado cuida disso, como no Exemplo 30.
Exemplo 30. tree/step3/tree.py: sub_tree percorre os níveis 1 e 2, primeiro em profundidade

PY
def tree(cls):
yield cls.__name__, 0
yield from sub_tree(cls)

def sub_tree(cls):
for sub_cls in cls.__subclasses__():
yield sub_cls.__name__, 1
for sub_sub_cls in sub_cls.__subclasses__():
yield sub_sub_cls.__name__, 2

def display(cls):
for cls_name, level in tree(cls):
indent = ' ' * 4 * level
print(f'{indent}{cls_name}')

if __name__ == '__main__':
display(BaseException)

Este é o resultado da execução de step3/tree.py, do Exemplo 30:

BASH
$ python3 tree.py
BaseException
Exception
TypeError
StopAsyncIteration
StopIteration
ImportError
OSError
EOFError
RuntimeError
NameError
AttributeError
SyntaxError
LookupError
ValueError
AssertionError
ArithmeticError
SystemError
ReferenceError
MemoryError
BufferError
Warning
GeneratorExit
SystemExit
KeyboardInterrupt

Você pode já ter percebido para onde isso segue, mas vou insistir mais uma vez nos pequenos passos: vamos atingir o
nível 3, acrescentando ainda outro loop for aninhado. Não há qualquer alteração no restante do programa, então o
Exemplo 31 mostra apenas a geradora sub_tree .

Exemplo 31. A geradora sub_tree de tree/step4/tree.py


PY
def sub_tree(cls):
for sub_cls in cls.__subclasses__():
yield sub_cls.__name__, 1
for sub_sub_cls in sub_cls.__subclasses__():
yield sub_sub_cls.__name__, 2
for sub_sub_sub_cls in sub_sub_cls.__subclasses__():
yield sub_sub_sub_cls.__name__, 3

Há um padrão claro no Exemplo 31. Entramos em um loop for para obter as subclasses do nível N. A cada passagem
do loop, produzimos uma subclasse do nível N, e então iniciamos outro loop for para visitar o nível N+1.

Na seção Seção 17.11.1, vimos como é possível substituir um loop for aninhado controlando uma geradora com
yield from sobre a mesma geradora. Podemos aplicar aquela ideia aqui, se fizermos sub_tree aceitar um
parâmetro level , usando yield from recursivamente e passando a subclasse atual como nova classe raiz com o
número do nível seguinte. Veja o Exemplo 32.

Exemplo 32. tree/step5/tree.py: a sub_tree recursiva vai tão longe quanto a memória permitir

PY
def tree(cls):
yield cls.__name__, 0
yield from sub_tree(cls, 1)

def sub_tree(cls, level):


for sub_cls in cls.__subclasses__():
yield sub_cls.__name__, level
yield from sub_tree(sub_cls, level+1)

def display(cls):
for cls_name, level in tree(cls):
indent = ' ' * 4 * level
print(f'{indent}{cls_name}')

if __name__ == '__main__':
display(BaseException)

O Exemplo 32 pode percorrer árvores de qualquer profundidade, limitado apenas pelo limite de recursão do Python. O
limite default permite 1.000 funções pendentes.

Qualquer bom tutorial sobre recursão enfatizará a importância de ter um caso base, para evitar uma recursão infinita.
Um caso base é um ramo condicional que retorna sem fazer uma chamada recursiva. O caso base é frequentemente
implementado com uma instrução if . No Exemplo 32, sub_tree não tem um if , mas há uma condicional implícita
no loop for : Se cls.subclasses() devolver uma lista vazia, o corpo do loop não é executado, e assim a chamada
recursiva não ocorre. O caso base ocorre quando a classe cls não tem subclasses. Nesse caso, sub_tree não produz
nada, apenas retorna.

O Exemplo 32 funciona como planejado, mas podemos fazê-la mais concisa recordando do padrão que observamos
quando alcançamos o nível 3 (no Exemplo 31): produzimos uma subclasse de nível N, e então iniciamos um loop for
aninhado para visitar o nível N+1. No Exemplo 32, substituímos o loop aninhado por yield from . Agora podemos
fundir tree e sub_tree em uma única geradora. O Exemplo 33 é o último passo deste exemplo.

Exemplo 33. tree/step6/tree.py: chamadas recursivas de tree passam um argumento level incrementado
PY
def tree(cls, level=0):
yield cls.__name__, level
for sub_cls in cls.__subclasses__():
yield from tree(sub_cls, level+1)

def display(cls):
for cls_name, level in tree(cls):
indent = ' ' * 4 * level
print(f'{indent}{cls_name}')

if __name__ == '__main__':
display(BaseException)

No início da seção Seção 17.11, vimos como yield from conecta a subgeradora diretamente ao código cliente,
escapando da geradora delegante. Aquela conexão se torna realmente importante quando geradoras são usadas como
corrotinas, e não apenas produzem mas também consomem valores do código cliente, como veremos na seção Seção
17.13.

Após esse primeiro encontro com yield from , vamos olhar as dicas de tipo para iteráveis e iteradores.

17.12. Tipos iteráveis genéricos


A bilbioteca padrão do Python contém muitas funções que aceitam argumentos iteráveis. Em seu código, tais funções
podem ser anotadas como a função zip_replace , vista no Exemplo 33, usando collections.abc.Iterable (ou
typing.Iterable , se você precisa suporta o Python 3.8 ou anterior, como explicado no Suporte a tipos de coleção
descontinuados). Veja o Exemplo 34.

Exemplo 34. replacer.py devolve um iterador de tuplas de strings

PY
from collections.abc import Iterable

FromTo = tuple[str, str] # (1)

def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # (2)


for from_, to in changes:
text = text.replace(from_, to)
return text

1. Define um apelido (alias) de tipo; isso não é obrigatório, mas torna a próxima dica de tipo mais legível. Desde o
Python 3.10, FromTo deve ter uma dica de tipo de typing.TypeAlias , para esclarecer a razão para essa linha:
FromTo: TypeAlias = tuple[str, str] .

2. Anota changes para aceitar um Iterable de tuplas FromTo .

Tipos Iterator não aparecem com a mesma frequência de tipos Iterable , mas eles também são simples de
escrever. O Exemplo 35 mostra a conhecida geradora Fibonacci, anotada.

Exemplo 35. fibo_gen.py: fibonacci devolve um gerador de inteiros


PY
from collections.abc import Iterator

def fibonacci() -> Iterator[int]:


a, b = 0, 1
while True:
yield a
a, b = b, a + b

Observe que o tipo Iterator é usado para geradoras programadas como funções com yield , bem como para
iteradores escritos "a mão", como classes que implementam __next__ . Há também o tipo
collections.abc.Generator (e o decontinuado typing.Generator correspondente) que podemos usar para anotar
objetos geradores, mas ele é verboso e redundane para geradoras usadas como iteradores, que não recebem valores via
.send() .

O Exemplo 36, quando verificado com o Mypy, revela que o tipo Iterator é, na verdade, um caso especial
simplificado do tipo Generator .

Exemplo 36. itergentype.py: duas formas de anotar iteradores

PY
from collections.abc import Iterator
from keyword import kwlist
from typing import TYPE_CHECKING

short_kw = (k for k in kwlist if len(k) < 5) # (1)

if TYPE_CHECKING:
reveal_type(short_kw) # (2)

long_kw: Iterator[str] = (k for k in kwlist if len(k) >= 4) # (3)

if TYPE_CHECKING: # (4)
reveal_type(long_kw)

1. Uma expressão geradora que produz palavras reservadas do Python com menos de 5 caracteres.
2. O Mypy infere: typing.Generator[builtins.str*, None, None] .[226]

3. Isso também produz strings, mas acrescentei uma dica de tipo explícita.
4. Tipo revelado: typing.Iterator[builtins.str] .

abc.Iterator[str] é consistente-com abc.Generator[str, None, None] , assim o Mypy não reporta erros na
verificação de tipos no Exemplo 36.

Iterator[T] é um atalho para Generator[T, None, None] . Ambas as anotações significam "uma geradora que
produz itens do tipo T , mas não consome ou devolve valores." Geradoras capazes de consumir e devolver valores são
corrotinas, nosso próximo tópico.
17.13. Corrotinas clássicas
A PEP 342—Coroutines via Enhanced Generators (Corrotinas via geradoras aprimoradas)
(https://fpy.li/pep342) introduziu .send() e outros recursos que tornaram possível usar geradoras
como corrotinas. A PEP 342 usa a palavra "corrotina" (coroutine) no mesmo sentido que estou
usando aqui. É lamentável que a documentação oficial do Python e da biblioteca padrão agora usem
uma terminologia inconsistente para se referir a geradoras usadas como corrotinas, me obrigando a
✒️ NOTA adotar o qualificador "corrotina clássica", para diferenciar estas últimas com os novos objetos
"corrotinas nativas".

Após o lançamento do Python 3.5, a tendência é usar "corrotina" como sinônimo de "corrotina
nativa". Mas a PEP 342 não está descontinuada, e as corrotinas clássicas ainda funcionam como
originalmente projetadas, apesar de não serem mais suportadas por asyncio .

Entender as corrotinas clássicas no Python é mais confuso porque elas são, na verdade, geradoras usadas de uma
forma diferente. Vamos então dar um passo atrás e examinar outro recurso do Python que pode ser usado de duas
maneiras.

Vimos na seção Seção 2.4 que é possível usar instâncias de tuple como registros ou como sequências imutáveis.
Quando usadas como um registro, se espera que uma tupla tenha um número específico de itens, e cada item pode ter
um tipo diferente. Quando usadas como listas imutáveis, uma tupla pode ter qualquer tamanho, e se espera que todos
os itens sejam do mesmo tipo. Por essa razão, há duas formas de anotar tuplas com dicas de tipo:

PY
# Um registro de cidade, como nome, país e população:
city: tuple[str, str, int]

# Uma sequência imutável de nomes de domínios:


domains: tuple[str, ...]

Algo similar ocorre com geradoras. Elas normalmente são usadas como iteradores, mas podem também ser usadas
como corrotinas. Na verdade, corrotina é uma função geradora, criada com a palavra-chave yield em seu corpo. E um
objeto corrotina é um objeto gerador, fisicamente. Apesar de compartilharem a mesma implementação subjacente em
C, os casos de uso de geradoras e corrotinas em Python são tão diferentes que há duas formas de escrever dicas de tipo
para elas:

PY
# A variável `readings` pode ser delimitada a um iterador
# ou a um objeto gerador que produz itens `float`:
readings: Iterator[float]

# A variável `sim_taxi` pode ser delimitada a uma corrotina


# representando um táxi em uma simulação de eventos discretos.
# Ela produz eventos, recebe um `float` de data/hora, e devolve
# o número de viagens realizadas durante a simulação:
sim_taxi: Generator[Event, float, int]

Para aumentar a confusão, os autores do módulo typing decidiram nomear aquele tipo Generator , quando ele de
fato descreve a API de um objeto gerador projetado para ser usado como uma corrotina, enquanto geradoras são mais
frequentemente usadas como iteradores simples.

A documentação do módulo typing (https://docs.python.org/pt-br/3/library/typing.html#typing.Generator) (EN) descreve assim


os parâmetros de tipo formais de Generator :
PY
Generator[YieldType, SendType, ReturnType]

O SendType só é relevante quando a geradora é usada como uma corrotina. Aquele parâmetro de tipo é o tipo de x na
chamada gen.send(x) . É um erro invocar .send() em uma geradora escrita para se comportar como um iterador
em vez de uma corrotina. Da mesma forma, ReturnType só faz sentido para anotar uma corrotina, pois iteradores não
devolvem valores como funções regulares. A única operação razoável em uma geradora usada como um iterador é
invocar next(it) direta ou indiretamente, via loops for e outras formas de iteração. O YieldType é o tipo do valor
devolvido em uma chamada a next(it) .

O tipo Generator tem os mesmo parâmetros de tipo de typing.Coroutine (https://fpy.li/typecoro):

PY
Coroutine[YieldType, SendType, ReturnType]

A documentação de typing.Coroutine (https://fpy.li/typecoro) diz literalmente: "A variância e a ordem das variáveis de
tipo correspondem às de Generator ." Mas typing.Coroutine (descontinuada) e collections.abc.Coroutine
(genérica a partir do Python 3.9) foram projetadas para anotar apenas corrotinas nativas, e não corrotinas clássicas. Se
você quiser usar dicas de tipo com corrotinas clássicas, vai sofrer com a confusão advinda de anotá-las como
Generator[YieldType, SendType, ReturnType] .

David Beazley criou algumas das melhores palestras e algumas das oficinas mais abrangentes sobre corrotinas
clássicas. No material de seu curso na PyCon 2009 (https://fpy.li/17-18) há um slide chamado "Keeping It Straight" (Cada
Coisa em Seu Lugar), onde se lê:

“ Geradoras produzem dados para iteração


Corrotinas são consumidoras de dados
Para evitar que seu cérebro exploda, não misture os dois conceitos
Corrotinas não tem relação com iteração
Nota: Há uma forma de fazer yield produzir um valor em uma corrotina, mas isso não
está ligado à iteração.[227]

Vamos ver agora como as corrotinas clássicas funcionam.

17.13.1. Exemplo: Corrotina para computar uma média móvel


Quando discutimos clausuras no Capítulo 9, estudamos objetos para computar uma média móvel. O Exemplo 7 mostra
uma classe e o Exemplo 13 apresenta uma função de ordem superior devolvendo uma função que mantem as variáveis
total e count entre invocações, em uma clausura. O Exemplo 37 mostra como fazer o mesmo com uma corrotina.
[228]

Exemplo 37. coroaverager.py: corrotina para computar uma média móvel


PY
from collections.abc import Generator

def averager() -> Generator[float, float, None]: # (1)


total = 0.0
count = 0
average = 0.0
while True: # (2)
term = yield average # (3)
total += term
count += 1
average = total/count

1. Essa função devolve uma geradora que produz valores float , aceita valores float via .send() , e não devolve
um valor útil.[229]
2. Esse loop infinito significa que a corrotina continuará produzindo médias enquanto o código cliente enviar valores.
3. O comando yield aqui suspende a corrotina, produz um resultado para o cliente e—mais tarde—recebe um valor
enviado pelo código de invocação para a corrotina, iniciando outra iteração do loop infinito.

Em uma corrotina, total e count podem ser variáveis locais: atributos de instância ou clasura não são necessários
para manter o contexto enquanto a corrotina está suspensa, esperando pelo próximo .send() . Por isso as corrotinas
são substitutas atraentes para callbacks em programação assíncrona—elas mantêm o estado local entre ativações.

O Exemplo 38 executa doctests mostrando a corrotina averager em operação.

Exemplo 38. coroaverager.py: doctest para a corrotina de média móvel do Exemplo 37

PY
>>> coro_avg = averager() # (1)
>>> next(coro_avg) # (2)
0.0
>>> coro_avg.send(10) # (3)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

1. Cria o objeto corrotina.


2. Inicializa a corrotina. Isso produz o valor inicial de average : 0.0.

3. Agora estamos conversando: cada chamada a .send() produz a média atual.

No Exemplo 38, a chamada next(coro_avg) faz a corrotina avançar até o yield , produzindo o valor inicial de
average . Também é possível inicializar a corrotina chamando coro_avg.send(None) —na verdade é isso que a
função embutida next() faz. Mas você não pode enviar qualquer valor diferente de None , pois a corrotina só pode
aceitar um valor enviado quando está suspensa, em uma linha de yield . Invocar next() ou .send(None) para
avançar até o primeiro yield é conhecido como "preparar (priming) a corrotina".

Após cada ativação, a corrotina é suspensa exatamente na palavra-chave yield , e espera que um valor seja enviado. A
linha coro_avg.send(10) fornece aquele valor, ativando a corrotina. A expressão yield se resolve para o valor 10,
que é atribuido à variável term . O restante do loop atualiza as variáveis total , count , e average . A próxima
iteração no loop while produz average , e a corrotina é novamente suspensa na palavra-chave yield .
O leitor atento pode estar ansioso para saber como a execução de uma instância de averager (por exemplo,
coro_avg ) pode ser encerrada, pois seu corpo é um loop infinito. Em geral, não precisamos encerrar uma geradora,
pois ela será coletada como lixo assim que não existirem mais referências válidas para ela. Se for necessário encerrá-la
explicitamente, use o método .close() , como mostra o Exemplo 39.

Exemplo 39. coroaverager.py: continuando de Exemplo 38

PY
>>> coro_avg.send(20) # (1)
16.25
>>> coro_avg.close() # (2)
>>> coro_avg.close() # (3)
>>> coro_avg.send(5) # (4)
Traceback (most recent call last):
...
StopIteration

1. coro_avg é a instância criada no Exemplo 38.


2. O método .close() gera uma exceção GeneratorExit na expressão yield suspensa. Se não for tratada na
função corrotina, a exceção a encerra. GeneratorExit é capturada pelo objeto gerador que encapsula a corrotina
—por isso não a vemos.
3. Invocar .close() em uma corrotina previamente encerrada não tem efeito.
4. Tentar usar .send() em uma corrotina encerrada gera uma StopIteration .

Além do método .send() , a PEP 342—Coroutines via Enhanced Generators (Corrotinas via geradoras aprimoradas)
(https://fpy.li/pep342) também introduziu uma forma de uma corrotina devolver um valor. A próxima seção mostra como
fazer isso.

17.13.2. Devolvendo um valor a partir de uma corrotina


Vamos agora estudar outra corrotina para computar uma média. Essa versão não vai produzir resultados parciais. Em
vez disso, ela devolve uma tupla com o número de termos e a média. Dividi a listagem em duas partes, no Exemplo 40 e
no Exemplo 41.

Exemplo 40. coroaverager2.py: a primeira parte do arquivo

PY
from collections.abc import Generator
from typing import Union, NamedTuple

class Result(NamedTuple): # (1)


count: int # type: ignore # (2)
average: float

class Sentinel: # (3)


def __repr__(self):
return f'<Sentinel>'

STOP = Sentinel() # (4)

SendType = Union[float, Sentinel] # (5)

1. A corrotina averager2 no Exemplo 41 vai devolver uma instância de Result .

2. Result é, na verdade, uma subclasse de tuple , que tem um método .count() , que não preciso aqui. O
comentário # type: ignore evita que o Mypy reclame sobre a existência do campo count .[230]
3. Uma classe para criar um valor sentinela com um __repr__ legível.
4. O valor sentinela que vou usar para fazer a corrotina parar de coletar dados e devolver uma resultado.
5. Vou usar esse apelido de tipo para o segundo parâmetro de tipo devolvido pela corrotina Generator , o parâmetro
SendType .
A definição de SendType também funciona no Python 3.10 mas, se não for necessário suportar versões mais antigas, é
melhor escrever a anotação assim, após importar TypeAlias de typing :

PY
SendType: TypeAlias = float | Sentinel

Usar | em vez de typing.Union é tão conciso e legível que eu provavelmente não criaria aquele apelido de tipo. Em
vez disso, escreveria a assinatura de averager2 assim:

PY
def averager2(verbose: bool=False) -> Generator[None, float | Sentinel, Result]:

Vamos agora estudar o código da corrotina em si (no Exemplo 41).

Exemplo 41. coroaverager2.py: uma corrotina que devolve um valor resultante

PY
def averager2(verbose: bool = False) -> Generator[None, SendType, Result]: # (1)
total = 0.0
count = 0
average = 0.0
while True:
term = yield # (2)
if verbose:
print('received:', term)
if isinstance(term, Sentinel): # (3)
break
total += term # (4)
count += 1
average = total / count
return Result(count, average) # (5)

1. Para essa corrotina, o tipo produzido é None , porque ela não produz dados. Ela recebe dados do tipo SendType e
devolve uma tupla Result quando termina o processamento.
2. Usar yield assim só faz sentido em corrotinas, que são projetadas para consumir dados. Isso produz None , mas
recebe um term de .send(term) .
3. Se term é um Sentinel , sai do loop. Graças a essa verificação com isinstance …​

4. …​Mypy me permite somar term a total sem sinalizar um erro (que eu não poderia somar um float a um
objeto que pode ser um float ou um Sentinel ).
5. Essa linha só será alcançada de um Sentinel for enviado para a corrotina.

Vamos ver agora como podemos usar essa corrotina, começando por um exemplo simples, que sequer produz um
resultado (no Exemplo 42).

Exemplo 42. coroaverager2.py: doctest mostrando .cancel()


PY
>>> coro_avg = averager2()
>>> next(coro_avg)
>>> coro_avg.send(10) # (1)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> coro_avg.close() # (2)

1. Lembre-se que averager2 não produz resultados parciais. Ela produz None , que o console do Python omite.

2. Invocar .close() nessa corrotina a faz parar, mas não devolve um resultado, pois a exceção GeneratorExit é
gerada na linha yield da corrotina, então a instrução return nunca é alcançada.

Vamos então fazê-la funcionar, no Exemplo 43.

Exemplo 43. coroaverager2.py: doctest mostrando StopIteration com um Result

PY
>>> coro_avg = averager2()
>>> next(coro_avg)
>>> coro_avg.send(10)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> try:
... coro_avg.send(STOP) # (1)
... except StopIteration as exc:
... result = exc.value # (2)
...
>>> result # (3)
Result(count=3, average=15.5)

1. Enviar o valor sentinela STOP faz a corrotina sair do loop e devolver um Result . O objeto gerador que encapsula
a corrotina gera então uma StopIteration .
2. A instância de StopIteration tem um atributo value vinculado ao valor do comando return que encerrou a
corrotina.
3. Acredite se quiser!

Essa ideia de "contrabandear" o valor devolvido para fora de uma corrotina dentro de uma exceção StopIteration é
um truque bizarro. Entretanto, esse truque é parte da PEP 342—Coroutines via Enhanced Generators (Corrotinas via
geradoras aprimoradas) (https://fpy.li/pep342) (EN), e está documentada com a exceção StopIteration
(https://docs.python.org/pt-br/3/library/exceptions.html#StopIteration) e na seção "Expressões yield"
(https://docs.python.org/pt-br/3/reference/expressions.html#yield-expressions) do capítulo 6 de A Referência da Linguagem
Python (https://fpy.li/17-24).

Uma geradora delegante pode obter o valor devolvido por uma corrotina diretamente, usando a sintaxe yield from ,
como demonstrado no Exemplo 44.

Exemplo 44. coroaverager2.py: doctest mostrando StopIteration com um Result


PY
>>> def compute():
... res = yield from averager2(True) # (1)
... print('computed:', res) # (2)
... return res # (3)
...
>>> comp = compute() # (4)
>>> for v in [None, 10, 20, 30, STOP]: # (5)
... try:
... comp.send(v) # (6)
... except StopIteration as exc: # (7)
... result = exc.value
received: 10
received: 20
received: 30
received: <Sentinel>
computed: Result(count=3, average=20.0)
>>> result # (8)
Result(count=3, average=20.0)

1. res vai coletar o valor devolvido por averager2 ; o mecanismo de yield from recupera o valor devolvido
quando trata a exceção StopIteration , que marca o encerramento da corrotina. Quando True , o parâmetro
verbose faz a corrotina exibir o valor recebido, tornando sua operação visível.

2. Preste atenção na saída desta linha quando a geradora for executada.


3. Devolve o resultado. Isso também estará encapsulado em StopIteration .

4. Cria o objeto corrotina delegante.


5. Esse loop vai controlar a corrotina delegante.
6. O primeiro valor enviado é None , para preparar a corrotina; o último é a sentinela, para pará-la.

7. Captura StopIteration para obter o valor devolvido por compute .

8. Após as linhas exibidas por averager2 e compute , reebemos a instância de Result .

Mesmo com esses exemplos aqui, que não fazem muita coisa, o código é difícl de entender. Controlar a corrotina com
chamadas .send() e recuperar os resultados é complicado, exceto com yield from —mas só podemos usar essa
sintaxe dentro de uma geradora/corrotina, que no fim precisa ser controlada por algum código não-trivial, como
mostra o Exemplo 44.

Os exemplos anteriores mostram que o uso direto de corrotinas é incômodo e confuso. Acrescente o tratamento de
exceções e o método de corrotina .throw() , e os exemplos ficam ainda mais complicados. Não vou tratar de
.throw() nesse livro porque—como .send() —ele só é útil para controlar corrotinas "manualmente", e não
recomendo fazer isso, a menos que você esteja criando uma nova framework baseada em corrotinas do zero .

Se você estiver interessado em um tratamento mais aprofundado de corrotinas clássicas—incluindo


o método .throw() —por favor veja "Classic Coroutines" (Corrotinas Clássicas) (https://fpy.li/oldcoro)

✒️ NOTA (EN) no site que acompanha o livro, fluentpython.com (http://fluentpython.com). Aquele texto inclui
pseudo-código similar ao Python detalhando como yield from controla geradoras e corrotinas,
bem como uma pequena simulação de eventos discretos, demonstrando uma forma de concorrência
usando corrotinas sem uma framework de programação assíncrona.

Na prática, realizar trabalho produtivo com corrotinas exige o suporte de uma framework especializada. É isso que
asyncio oferecia para corrotinas clássicas lá atrás, no Python 3.3. Com o advento das corrotinas nativas no Python 3.5,
os desenvolvedores principais do Python estão gradualmente eliminando o suporte a corrotinas clássicas no asyncio .
Mas os mecanismos subjacentes são muito similares. A sintaxe async def torna a corrotinas nativas mais fáceis de
identificar no código, um grande benefício por si só. Internamente, as corrotinas nativas usam await em vez de
yield from para delegar a outras corrotinas. O Capítulo 21 é todo sobre esse assunto.
Vamos agora encerrar o capítulo com uma seção alucinante sobre co-variância e contra-variância em dicas de tipo para
corrotinas.

17.13.3. Dicas de tipo genéricas para corrotinas clássicas


Anteriomente, na seção Seção 15.7.4.3, mencionei typing.Generator como um dos poucos tipos da biblioteca padrão
com um parâmetro de tipo contra-variante. Agora que estudamos as corrotinas clássicas, estamos prontos para
entender esse tipo genérico.

É assim que typing.Generator era declarado (https://fpy.li/17-25) no módulo typing.py do Python 3.6:[231]

PYTHON3
T_co = TypeVar('T_co', covariant=True)
V_co = TypeVar('V_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)

# muitas linhas omitidas

class Generator(Iterator[T_co], Generic[T_co, T_contra, V_co],


extra=_G_base):

Essa declaração de tipo genérico significa que uma dica de tipo de Generator requer aqueles três parâmetros de tipo
que vimos antes:

PYTHON3
my_coro : Generator[YieldType, SendType, ReturnType]

Pelas variáveis de tipo nos parâmetros formais, vemos que YieldType e ReturnType são covariantes, mas SendType
é contra-variante. Para entender a razão disso, considere que YieldType e ReturnType são tipos de "saída". Ambos
descrevem dados que saem do objeto corrotina—isto é, o objeto gerador quando usado como um objeto corrotina..

Faz sentido que esses parâmetros sejam covariantes, pois qualquer código esperando uma corrotina que produz
números de ponto flutuante pode usar uma corrotina que produz inteiros. Por isso Generator é covariante em seu
parâmetro YieldType . O mesmo raciocínio se aplica ao parâmetro ReturnType —também covariante.

Usando a notação introduzida na seção Seção 15.7.4.2, a covariância do primeiro e do terceiro parâmetros pode ser
expressa pelos símbolos :> apontando para a mesma direção:

float :> int


Generator[float, Any, float] :> Generator[int, Any, int]

YieldType e ReturnType são exemplos da primeira regra apresentada na seção Seção 15.7.4.4:

“ 1. Se um parâmetro de tipo formal define um tipo para dados que saem de um objeto, ele pode
ser covariante.

Por outro lado, é um parâmetro de "entrada": ele é o tipo do argumento value para o método
SendType
.send(value) do objeto corrotina. Código cliente que precise enviar números de ponto flutuante para uma corrotina
não consegue usar uma corrotina que receba int como o SendType , porque float não é um subtipo de int . Em
outras palavras, float não é consistente-com int . Mas o cliente pode usar uma corrotina que tenha complex como
SendType , pois float é um subtipo de complex , e portanto float é consistente-com complex .
A notação :> torna visível a contra-variância do segundo parâmetro:

float :> int


Generator[Any, float, Any] <: Generator[Any, int, Any]

Este é um exemplo da segunda regra geral da variância:

“ 2. Se um parâmetro de tipo formal define um tipo para dados que entram em um objeto após
sua construção inicial, ele pode ser contra-variante.

Essa alegre discussão sobre variância encerra o capítulo mais longo do livro.

17.14. Resumo do capítulo


A iteração está integrada tão profundamente à linguagem que eu gosto de dizer que o Python groks iteradores[232] A
integração do padrão Iterator na semântica do Python é um exemplo perfeito de como padrões de projeto não são
aplicáveis a todas as linguagens de programação. No Python, um Iterator clássico, implementado "à mão", como no
Exemplo 4, não tem qualquer função prática, exceto como exemplo didático.

Neste capítulo, criamos algumas versões de uma classe para iterar sobre palavras individuais em arquivos de texto
(que podem ser muito grandes). Vimos como o Python usa a função embutida iter() para criar iteradores a partir de
objetos similares a sequências. Criamos um iterador clássico como uma classe com __next__() , e então usamos
geradoras, para tornar cada refatoração sucessiva da classe Sentence mais concisa e legível.

Daí criamos uma geradora de progressões aritméticas, e mostramos como usar o módulo itertools para torná-la
mais simples. A isso se seguiu uma revisão da maioria das funções geradoras de uso geral na biblioteca padrão.

A seguir estudamos expressões yield from no contexto de geradoras simples, com os exemplos chain e tree .

A última seção de nota foi sobre corrotinas clássicas, um tópico de importância decrescente após a introducão das
corrotinas nativas, no Python 3.5. Apesar de difíceis de usar na prática, corrotinas clássicas são os alicerces das
corrotinas nativas, e a expressão yield from é uma precursora direta de await .

Dicas de tipo para os tipos Iterable , Iterator , e Generator também foram abordadas—com esse último
oferecendo um raro exemplo concreto de um parâmetro de tipo contra-variante.

17.15. Leitura complementar


Uma explicação técnica detalhada sobre geradoras aparece na A Referência da Linguagem Python, em "6.2.9. Expressões
yield" (https://docs.python.org/pt-br/3/reference/expressions.html#yieldexpr). A PEP onde as funções geradoras foram definidas
é a PEP 255—​Simple Generators (Geradoras Simples) (https://fpy.li/pep255).

A documentação do módulo itertools (https://docs.python.org/pt-br/3/library/itertools.html) é excelente, especialmente por


todos os exemplos incluídos. Apesar das funções daquele módulo serem implementadas em C, a documentação mostra
como algumas delas poderiam ser escritas em Python, frequentemente se valendo de outras funções no módulo. Os
exemplos de utilização também são ótimos; por exemplo, há um trecho mostrando como usar a função accumulate
para amortizar um empréstimo com juros, dada uma lista de pagamentos ao longo do tempo. Há também a seção
"Receitas com itertools" (https://docs.python.org/pt-br/3/library/itertools.html#itertools-recipes), com funções adicionais de alto
desempenho, usando as funções de itertools como base.

Além da bilbioteca padrão do Python, recomendo o pacote More Itertools (https://fpy.li/17-30), que continua a bela
tradição do itertools , oferecendo geradoras poderosas, acompanhadas de muitos exemplos e várias receitas úteis.
"Iterators and Generators" (Iteradores e Geradoras), o capítulo 4 de Python Cookbook, 3ª ed., de David Beazley e Brian K.
Jones (O’Reilly), traz 16 receitas sobre o assunto, de muitos ângulos diferentes, concentradas em aplicações práticas. O
capítulo contém algumas receitas esclarecedoras com yield from .

Sebastian Rittau—atualmente um dos principais colaboradores do typeshed—explica porque iteradores devem ser
iteráveis. Ele observou, em 2006, que "Java: Iterators are not Iterable" (Java:Iteradores não são Iteráveis)
(https://fpy.li/17-31).

A sintaxe de yield from é explicada, com exemplos, na seção "What’s New in Python 3.3" (Novidades no Python 3.3) da
PEP 380—​Syntax for Delegating to a Subgenerator (Sintaxe para Delegar para um Subgerador) (https://fpy.li/17-32). Meu
artigo "Classic Coroutines" (Corrotinas Clássicas) (https://fpy.li/oldcoro) (EN) no fluentpython.com (http://fluentpython.com)
explica yield from em profundidade, incluindo pseudo-código em Python de sua implementação (em C).

David Beazley é a autoridade final sobre geradoras e corrotinas no Python. O Python Cookbook (https://fpy.li/pycook3), 3ª
ed., (O’Reilly), que ele escreveu com Brian Jones, traz inúmeras receitas com corrotinas. Os tutoriais de Beazley sobre
esse tópico nas PyCon são famosos por sua profundidade e abrangência. O primeiro foi na PyCon US 2008: "Generator
Tricks for Systems Programmers" (Truques com Geradoras para Programadores de Sistemas) (https://fpy.li/17-33) (EN). A
PyCon US 2009 assisitiu ao lendário "A Curious Course on Coroutines and Concurrency" (Um Curioso Curso sobre
Corrotinas e Concorrência) (https://fpy.li/17-34) (EN) (links de vídeo difíceis de encontrar para todas as três partes: parte 1
(https://fpy.li/17-35), parte 2 (https://fpy.li/17-36), e parte 3 (https://fpy.li/17-37)). Seu tutorial na PyCon 2014 em Montreal foi
"Generators: The Final Frontier" (Geradoras: A Fronteira Final) (https://fpy.li/17-38), onde ele apresenta mais exemplos de
concorrência—então é, na verdade, mais relacionado aos tópicos do Capítulo 21. Dave não consegue deixar de explodir
cérebros em suas aulas, então, na última parte de "A Fronteira Final", corrotinas substituem o padrão clássico Visitor
em um analisador de expressões aritméticas.

Corrotinas permitem organizar o código de novas maneiras e, assim como a recursão e o polimorfismo (despacho
dinâmico), demora um certo tempo para se acostumar com suas possibilidades. Um exemplo interessante de um
algoritmo clássico reescrito com corrotinas aparece no post "Greedy algorithm with coroutines" (O Algoritmo guloso
com corrotinas) (https://fpy.li/17-39), de James Powell.

O Effective Python, 1ª ed. (https://fpy.li/17-40) (Addison-Wesley), de Brett Slatkin, tem um excelente capítulo curto chamado
"Consider Coroutines to Run Many Functions Concurrently" (Considere as Corrotinas para Executar Muitas Fun;cões de
Forma Concorrente). Esse capítulo não aparece na segunda edição de Effective Python, mas ainda está disponível online
como um capítulo de amostra (https://fpy.li/17-41) (EN). Slatkin apresenta o melhor exemplo que já vi do controle de
corrotinas com yield from : uma implementaçào do Jogo da Vida (https://pt.wikipedia.org/wiki/Jogo_da_vida), de John
Conway, no qual corrotinas gerenciam o estado de cada célula conforme o jogo avança. Refatorei o código do exemplo
do Jogo da Vida—separando funções e classes que implementam o jogo dos trechos de teste no código original de
Slatkin. Também reescrevi os testes como doctests, então você pode ver o resultados de várias corrotinas e classes sem
executar o script. The exemplo refatorado (https://fpy.li/17-43) está publicado como um GitHub gist (https://fpy.li/17-44).

Ponto de Vista
A interface Iterador minimalista do Python

Na seção "Implementação" do padrão Iterator,[233], a Guange dos Quatro escreveu:

“ A interface mínima de Iterator consiste das operações First, Next, IsDone, e CurrentItem.
Entretanto, essa mesma frase tem uma nota de rodapé, onde se lê:


“ Podemos tornar essa interface ainda menor, fundindo Next, IsDone, e CurrentItem em uma
única operação que avança para o próximo objeto e o devolve. Se a travessia estiver
encerrada, essa operação daí devolve um valor especial (0, por exemplo), que marca o final
da iteração.

Isso é próximo do que temos em Python: um único método __next__ , faz o serviço. Mas em vez de uma
sentinela, que poderia passar desapercebida por enganoo ou distração, a exceção StopIteration sinaliza o final
da iteração. Simples e correto: esse é o jeito do Python.

Geradoras conectáveis

Qualquer um que gerencie grandes conjuntos de dados encontra muitos usos para geradoras. Essa é a história da
primeira vez que criei uma solução prática baseada em geradoras.

Muitos anos atrás, eu trabalhava na BIREME, uma biblioteca digital operada pela OPAS/OMS (Organização Pan-
Americana da Saúde/Organização Mundial da Saúde) em São Paulo, Brasil. Entre os conjuntos de dados
bibliográficos criados pela BIREME estão o LILACS (Literatura Latino-Americana e do Caribe em Ciências da
Saúde) and SciELO (Scientific Electronic Library Online), dois bancos de dados abrangentes, indexando a
literatura de pesquisa em ciências da saúde produzida na região.

Desde o final dos anos 1980, o sistema de banco de dados usado para gerenciar o LILACS é o CDS/ISIS, um banco
de dados não-relacional de documentos, criado pela UNESCO. Uma de minhas tarefas era pesquisar alternativas
para uma possível migração do LILACS—​e depois do SciELO, muito maior—​para um banco de dados de
documentos moderno e de código aberto, tal como o CouchDB ou o MongoDB. Naquela época escrevi um artigo
explicando o modelo de dados semi-estruturado e as diferentes formas de representar dados CDS/ISIS com
registros do tipo JSON: "From ISIS to CouchDB: Databases and Data Models for Bibliographic Records" (Do ISIS ao
CouchDBL Bancos de Dados e Modelos de Dados para Registros Bibliográficos) (https://fpy.li/17-45) (EN).

Como parte daquela pesquisa, escrevi um script Python para ler um arquivo CDS/ISIS e escrever um arquivo
JSON adequado para importação pelo CouchDB ou pelo MongoDB. Inicialmente, o arquivo lia arquivos no
formato ISO-2709, exportados pelo CDS/ISIS. A leitura e a escrita tinham de ser feitas de forma incremental, pois
os conjuntos de dados completos eram muito maiores que a memória principal. Isso era bastante fácil: cada
iteração do loop for principal lia um registro do arquivo .iso, o manipulava e escrevia no arquivo de saída .json.

Entretanto, por razões operacionais, foi considerado necessário que o isis2json.py suportasse outro formato de
dados do CDS/ISIS: os arquivos binários .mst, usados em produção na BIREME—​para evitar uma exportação
dispendiosa para ISO-2709. Agora eu tinha um problema: as bibliotecas usadas para ler arquivos ISO-2709 e .mst
tinham APIs muito diferentes. E o loop de escrita JSON já era complicado, pois o script aceitava, na linha de
comando, muitas opções para reestruturar cada registro de saída. Ler dados usando duas APIs diferentes no
mesmo loop for onde o JSON era produzido seria muito difícil de manejar.

A solução foi isolar a lógica de leitura em um par de funções geradoras: uma para cada formato de entrada
suportado. No fim, dividi o script isis2json.py em quatro funções. Você pode ver o código-fonte em Python 2, com
suas dependências, no repositório fluentpython/isis2json (https://fpy.li/17-46) no GitHub.[234]

Aqui está uma visão geral em alto nível de como o script está estruturado:

main

A função main usa argparse para ler opções de linha de comando que configuram a estrutura dos registros
de saída. Baseado na extensão do nome do arquivo de entrada, uma função geradora é selecionada para ler os
dados e produzir os registros, um por vez.
iter_iso_records
Essa função geradora lê arquivos .iso (que se presume estarem no formato ISO-2709). Ela aceita dois
argumento: o nome do arquivo e isis_json_type , uma das opções relacionadas à estrutura do registro. Cada
iteração de seu loop for lê um registro, cria um dict vazio, o preenche com dados dos campos, e produz o
dict .

iter_mst_records

Essa outra função geradora lê arquivos .mst.[235] Se você examinar o código-fonte de isis2json.py, vai notar que
ela não é tão simples quanto iter_iso_records , mas sua interface e estrutura geral é a mesma: a função
recebe como argumentos um nome de arquivo e um isis_json_type , e entra em um loop for , que cria e
produz por iteração um dict , representando um único registro.

write_json
Essa função executa a escrita efetiva de registros JSON, um por vez. Ela recebe numerosos argumentos, mas o
primeiro—input_gen—é uma referência para uma função geradora: iter_iso_records ou
iter_mst_records . O loop for principal itera sobre os dicionários produzidos pela geradora input_gen
selecionada, os reestrutura de diferentes formas, determinadas pelas opções de linha de comando, e anexa o
registro JSON ao arquivo de saída.
Me aproveitando das funções geradoras, pude dissociar a leitura da escrita. Claro, a maneira mais simples de
dissociar as duas operações seria ler todos os registros para a memória e então escrevê-los no disco. Mas essa não
era uma opção viável, pelo tamanho dos conjuntos de dados. Usando geradoras, a leitura e a escrita são
intercaladas, então o script pode processar arquivos de qualquer tamanho. Além disso, a lógica especial para ler
um registro em formatos de entrada diferentes está isolada da lógica de reestruturação de cada registro para
escrita.

Agora, se precisarmos que isis2json.py suporte um formato de entrada adicional—digamos, MARCXML, uma DTD
(NT: sigle de Document Type Definition, Definição de Tipo de Documento) usada pela Biblioteca do Congresso
norte-americano para representar dados ISO-2709—será fácil acrescentar uma terceira função geradora para
implementar a lógica de leitura, sem mudar nada na complexa função write_json .

Nào é ciência de foguete, mas é um exemplo real onde as geradoras permitiram um solução eficiente e flexível
para processar bancos de dados como um fluxo de registros, mantendo o uso de memória baixo e independente
do tamanho do conjunto de dados.
18. Instruções with, match, e blocos else
“ Gerenciadores de contexto podem vir a ser quase tão importantes quanto a própria sub-rotina.
Só arranhamos a superfície das possibilidades. […​] Basic tem uma instrução , há with
instruções with em várias linguagens. Mas elas não fazem a mesma coisa, todas fazem algo
muito raso, economizam consultas a atributos com o operador ponto ( . ), elas não configuram e
desfazem ambientes. Não pense que é a mesma coisa só porque o nome é igual. A instrução
with é muito mais que isso.[236] (EN)

— Raymond Hettinger
um eloquente evangelista de Python

Este capítulo é sobre mecanismos de controle de fluxo não muito comuns em outras linguagens e que, por essa razão,
podem ser ignorados ou subutilizados em Python. São eles:

A instrução with e o protocolo de gerenciamento de contexto


A instrução match/case para pattern matching (casamento de padrões)
A cláusula else nas instruções for , while , e try

A instrução with cria um contexto temporário e o destrói com segurança, sob o controle de um objeto gerenciador de
contexto. Isso previne erros e reduz código repetitivo, tornando as APIs ao mesmo tempo mais seguras e mais fáceis de
usar. Programadores Python estão encontrando muitos usos para blocos with além do fechamento automático de
arquivos.

Já estudamos pattern matching em capítulos anteriores, mas aqui veremos como a gramática de uma linguagem de
programação pode ser expressa como padrões de sequências. Por isso match/case é uma ferramenta eficiente para
criar processadores de linguagem fáceis de entender e de estender. Vamos examinar um interpretador completo para
um pequeno (porém funcional) subconjunto da linguagem Scheme. As mesmas ideias poderiam ser aplicadas no
desenvolvimento de uma linguagem de templates ou uma DSL (Domain-Specific Language, literalmente Linguagem de
Domínio Específico) para codificar regras de negócio em um sistema maior.

A cláusula else não é grande coisa, mas ajuda a transmitir a intenção por trás do código quando usada corretamente
junto com for , while e try .

18.1. Novidades nesse capítulo


A seção Seção 18.3 é nova.

Também atualizei a seção Seção 18.2.1 para incluir alguns recursos do módulo contextlib adicionados desde o
Python 3.6, e os novos gerenciadores de contexto "parentizados", introduzidos no Python 3.10.

Vamos começar com a poderosa instrução with .

18.2. Gerenciadores de contexto e a instrução with


Objetos gerenciadores de contexto existem para controlar uma instrução with , da mesma forma que iteradores
existem para controlar uma instrução for .
A instrução with foi projetada para simplificar alguns usos comuns de try/finally , que garantem que alguma
operação seja realizada após um bloco de código, mesmo que o bloco termine com um return , uma exceção, ou uma
chamada sys.exit() . O código no bloco finally normalmente libera um recurso crítico ou restaura um estado
anterior que havia sido temporariamente modificado.

A comunidade Python está encontrando novos usos criativos para gerenciadores de contexto. Alguns exemplos, da
biblioteca padrão, são:

Gerenciar transações no módulo sqlite3 — veja "Usando a conexão como gerenciador de contexto"
(https://docs.python.org/3/library/sqlite3.html#using-the-connection-as-a-context-manager).

Manipular travas, condições e semáforos de forma segura—como descrito na documentação do módulo threading
(https://docs.python.org/pt-br/3/library/threading.html#using-locks-conditions-and-semaphores-in-the-with-statement) (EN).

Configurar ambientes personalizados para operações aritméticas com objetos Decimal —veja a documentação de
decimal.localcontext (https://docs.python.org/pt-br/3/library/decimal.html#decimal.localcontext) (EN).

Remendar (patch) objetos para testes—veja a função unittest.mock.patch


(https://docs.python.org/pt-br/3/library/unittest.mock.html#patch) (EN).

A interface gerenciador de contexto consiste dos métodos __enter__ and __exit__ . No topo do with , o Python
chama o método __enter__ do objeto gerenciador de contexto. Quando o bloco with encerra ou termina por
qualquer razão, o Python chama __exit__ no objeto gerenciador de contexto.

O exemplo mais comum é se assegurar que um objeto arquivo seja fechado. O Exemplo 1 é uma demonstração
detalhada do uso do with para fechar um arquivo.

Exemplo 1. Demonstração do uso de um objeto arquivo como gerenciador de contexto

PYCON
>>> with open('mirror.py') as fp: # (1)
... src = fp.read(60) # (2)
...
>>> len(src)
60
>>> fp # (3)
<_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'>
>>> fp.closed, fp.encoding # (4)
(True, 'UTF-8')
>>> fp.read(60) # (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

1. fp está vinculado ao arquivo de texto aberto, pois o método __enter__ do arquivo devolve self .

2. Lê 60 caracteres Unicode de fp .

3. A variável fp ainda está disponível—blocos with não definem um novo escopo, como fazem as funções.
4. Podemos ler os atributos do objeto fp .

5. Mas não podemos ler mais texto de fp pois, no final do bloco with , o método TextIOWrapper.__exit__ foi
chamado, e isso fechou o arquivo.

A primeira explicação no Exemplo 1 transmite uma informação sutil porém crucial: o objeto gerenciador de contexto é
o resultado da avaliação da expressão após o with , mas o valor vinculado à variável alvo (na cláusula as ) é o
resultado devolvido pelo método __enter__ do objeto gerenciador de contexto.
E acontece que a função open() devolve uma instância de TextIOWrapper , e o método __enter__ dessa classe
devolve self . Mas em uma classe diferente, o método __enter__ também pode devolver algum outro objeto em vez
do gerenciador de contexto.

Quando o fluxo de controle sai do bloco with de qualquer forma, o método __exit__ é invocado no objeto
gerenciador de contexto, e não no que quer que __enter__ tenha devolvido.

A cláusula as da instrução with é opcional. No caso de open , sempre precisamos obter uma referência para o
arquivo, para podermos chamar seus métodos. Mas alguns gerenciadores de contexto devolvem None , pois não tem
nenhum objeto útil para entregar ao usuário.

O Exemplo 2 mostra o funcionamento de um gerenciador de contexto perfeitamente frívolo, projetado para ressaltar a
diferença entre o gerenciador de contexto e o objeto devolvido por seu método __enter__ .

Exemplo 2. Testando a classe gerenciadora de contexto LookingGlass

PY
>>> from mirror import LookingGlass
>>> with LookingGlass() as what: # (1)
... print('Alice, Kitty and Snowdrop') # (2)
... print(what)
...
pordwonS dna yttiK ,ecilA
YKCOWREBBAJ
>>> what # (3)
'JABBERWOCKY'
>>> print('Back to normal.') # (4)
Back to normal.

1. O gerenciador de contexto é uma instância de LookingGlass ; o Python chama __enter__ no gerenciador de


contexto e o resultado é vinculado a what .
2. Exibe uma str , depois o valor da variável alvo what . A saída de cada print será invertida.
3. Agora o bloco with terminou. Podemos ver que o valor devolvido por __enter__ , armazenado em what , é a
string 'JABBERWOCKY' .
4. A saída do programa não está mais invertida.

O Exemplo 3 mostra a implementação de LookingGlass .

Exemplo 3. mirror.py: código da classe gerenciadora de contexto LookingGlass


PY
import sys

class LookingGlass:

def __enter__(self): # (1)


self.original_write = sys.stdout.write # (2)
sys.stdout.write = self.reverse_write # (3)
return 'JABBERWOCKY' # (4)

def reverse_write(self, text): # (5)


self.original_write(text[::-1])

def __exit__(self, exc_type, exc_value, traceback): # (6)


sys.stdout.write = self.original_write # (7)
if exc_type is ZeroDivisionError: # (8)
print('Please DO NOT divide by zero!')
return True # (9)
# (10)

1. O Python invoca __enter__ sem argumentos além de self .

2. Armazena o método sys.stdout.write original, para podermos restaurá-lo mais tarde.


3. Faz um monkey-patch em sys.stdout.write , substituindo-o com nosso próprio método.

4. Devolve a string 'JABBERWOCKY' , apenas para termos algo para colocar na variável alvo what .

5. Nosso substituto de sys.stdout.write inverte o argumento text e chama a implementação original.


6. Se tudo correu bem, o Python chama __exit__ com None, None, None ; se ocorreu uma exceção, os três
argumentos recebem dados da exceção, como descrito a seguir, logo após esse exemplo.
7. Restaura o método original em sys.stdout.write .

8. Se a exceção não é None e seu tipo é ZeroDivisionError , exibe uma mensagem…​

9. …​e devolve True , para informar o interpretador que a exceção foi tratada.

10. Se __exit__ devolve None ou qualquer valor falso, qualquer exceção levantada dentro do bloco with será
propagada.

Quando aplicações reais tomam o controle da saída padrão, elas frequentemente desejam substituir

👉 DICA sys.stdout com outro objeto similar a um arquivo por algum tempo, depois voltar ao original. O
gerenciador de contexto contextlib.redirect_stdout (https://fpy.li/18-6) faz exatamente isso: passe
a ele seu objeto similar a um arquivo que substituirá sys.stdout .

O interpretador chama o método __enter__ sem qualquer argumento—além do self implícito. Os três argumentos
passados a __exit__ são:

exc_type
A classe da exceção (por exemplo, ZeroDivisionError ).

exc_value
A instância da exceção. Algumas vezes, parâmetros passados para o construtor da exceção—tal como a mensagem de
erro—podem ser encontrados em exc_value.args .

traceback

Um objeto traceback .[237]


Para uma visão detalhada de como funciona um gerenciador de contexto, vejamos o Exemplo 4, onde LookingGlass é
usado fora de um bloco with , de forma que podemos chamar manualmente seus métodos __enter__ e __exit__ .

Exemplo 4. Exercitando o LookingGlass sem um bloco with

PY
>>> from mirror import LookingGlass
>>> manager = LookingGlass() # (1)
>>> manager # doctest: +ELLIPSIS
<mirror.LookingGlass object at 0x...>
>>> monster = manager.__enter__() # (2)
>>> monster == 'JABBERWOCKY' # (3)
eurT
>>> monster
'YKCOWREBBAJ'
>>> manager # doctest: +ELLIPSIS
>... ta tcejbo ssalGgnikooL.rorrim<
>>> manager.__exit__(None, None, None) # (4)
>>> monster
'JABBERWOCKY'

1. Instancia e inspeciona a instância de manager .

2. Chama o método __enter__ do manager e guarda o resultado em monster .

3. monster é a string 'JABBERWOCKY' . O identificador True aparece invertido, porque toda a saída via stdout
passa pelo método write , que modificamos em __enter__ .
4. Chama manager.__exit__ para restaurar o stdout.write original.

Gerenciadores de contexto entre parênteses


O Python 3.10 adotou um novo parser (https://fpy.li/pep617) (analisador sintático), mais poderoso que o
antigo parser LL(1) (https://fpy.li/18-8). Isso permitiu introduzir novas sintaxes que não eram viáveis
anteriormente. Uma melhoria na sintaxe foi permitir gerenciadores de contexto agrupados entre
parênteses, assim:

👉 DICA with (
PY

CtxManager1() as example1,
CtxManager2() as example2,
CtxManager3() as example3,
):
...

Antes do 3.10, as linhas acima teriam que ser escritas como blocos with aninhados.

A biblioteca padrão inclui o pacote contextlib , com funções, classe e decoradores muito convenientes para
desenvolver, combinar e usar gerenciadores de contexto.

18.2.1. Utilitários do contextlib


Antes de desenvolver suas próprias classes gerenciadoras de contexto, dê uma olhada em contextlib —"Utilities for
with-statement contexts" ("Utilitários para contextos da instrução with) (https://docs.python.org/pt-br/3/library/contextlib.html)
, na documentação do Python. Pode ser que você esteja prestes a escrever algo que já existe, ou talvez exista uma classe
ou algum invocável que tornará seu trabalho mais fácil.

Além do gerenciador de contexto redirect_stdout mencionado logo após o Exemplo 3, o redirect_stderr foi
acrescentado no Python 3.5—ele faz o mesmo que seu par mais antigo, mas com as saídas direcionadas para stderr .
O pacote contextlib também inclui:

closing

Uma função para criar gerenciadores de contexto a partir de objetos que forneçam um método close() mas não
implementam a interface __enter__/__exit__ .

suppress
Um gerenciador de contexto para ignorar temporariamente exceções passadas como parâmetros.

nullcontext
Um gerenciador de contexto que não faz nada, para simplificar a lógica condicional em torno de objetos que podem
não implementar um gerenciador de contexto adequado. Ele serve como um substituto quando o código condicional
antes do bloco with pode ou não fornecer um gerenciador de contexto para a instrução with . Adicionado no
Python 3.7.

O módulo contextlib fornece classes e um decorador que são mais largamente aplicáveis que os decoradores
mencionados acima:

@contextmanager

Um decorador que permite construir um gerenciador de contexto a partir de um simples função geradora, em vez de
criar uma classe e implementar a interface. Veja a seção Seção 18.2.2.

AbstractContextManager

Uma ABC que formaliza a interface gerenciador de contexto, e torna um pouco mais fácil criar classes gerenciadoras
de contexto, através de subclasses—adicionada no Python 3.6.

ContextDecorator
Uma classe base para definir gerenciadores de contexto baseados em classes que podem também ser usadas como
decoradores de função, rodando a função inteira dentro de um contexto gerenciado.

ExitStack

Um gerenciador de contexto que permite entrar em um número variável de gerenciadores de contexto. Quando o
bloco with termina, ExitStack chama os métodos __exit__ dos gerenciadores de contexto empilhados na ordem
LIFO (Last In, First Out, Último a Entrar, Primeiro a Sair). Use essa classe quando você não sabe de antemão em
quantos gerenciadores de contexto será necessário entrar no bloco with ; por exemplo, ao abrir ao mesmo tempo
todos os arquivos de uma lista arbitrária de arquivos.

Com o Python 3.7, contextlib acrescentou AbstractAsyncContextManager , @asynccontextmanager , e


AsyncExitStack . Eles são similares aos utilitários equivalentes sem a parte async no nome, mas projetados para uso
com a nova instrução async with , tratado no Capítulo 21.

Desses todos, o utilitário mais amplamente usado é o decorador @contextmanager , então ele merece mais atenção.
Esse decorador também é interessante por mostrar um uso não relacionado a iteração para a instrução yield .

18.2.2. Usando o @contextmanager


O decorador @contextmanager é uma ferramenta elegante e prática, que une três recursos distintos do Python: um
decorador de função, um gerador, e a instrução with .

Usar o @contextmanager reduz o código repetitivo na criação de um gerenciador de contexto: em vez de escrever toda
uma classe com métodos __enter__/__exit__ , você só precisa implementar um gerador com uma única instrução
yield , que deve produzir o que o método __enter__ deveria devolver.
Em um gerador decorado com @contextmanager , o yield divide o corpo da função em duas partes: tudo que vem
antes do yield será executado no início do bloco with , quando o interpretador chama __enter__ ; o código após o
yield será executado quando __exit__ é chamado, no final do bloco.

O Exemplo 5 substitui a classe LookingGlass do Exemplo 3 por uma função geradora.

Exemplo 5. mirror_gen.py: um gerenciador de contexto implementado com um gerador

PY
import contextlib
import sys

@contextlib.contextmanager # (1)
def looking_glass():
original_write = sys.stdout.write # (2)

def reverse_write(text): # (3)


original_write(text[::-1])

sys.stdout.write = reverse_write # (4)


yield 'JABBERWOCKY' # (5)
sys.stdout.write = original_write # (6)

1. Aplica o decorador contextmanager .

2. Preserva o método sys.stdout.write original.


3. reverse_write pode chamar original_write mais tarde, pois ele está disponível em sua clausura (closure).
4. Substitui sys.stdout.write por reverse_write .

5. Produz o valor que será vinculado à variável alvo na cláusula as da instrução with . O gerador se detem nesse
ponto, enquanto o corpo do with é executado.
6. Quando o fluxo de controle sai do bloco with , a execução continua após o yield ; neste ponto o
sys.stdout.write original é restaurado.

O Exemplo 6 mostra a função looking_glass em operação.

Exemplo 6. Testando a função gerenciadora de contexto looking_glass

PY
>>> from mirror_gen import looking_glass
>>> with looking_glass() as what: # (1)
... print('Alice, Kitty and Snowdrop')
... print(what)
...
pordwonS dna yttiK ,ecilA
YKCOWREBBAJ
>>> what
'JABBERWOCKY'
>>> print('back to normal')
back to normal

1. A única diferença do Exemplo 2 é o nome do gerenciador de contexto:`looking_glass` em vez de LookingGlass .

O decorador contextlib.contextmanager envolve a função em uma classe que implementa os métodos __enter__
e __exit__ .[238]
O método __enter__ daquela classe:

1. Chama a função geradora para obter um objeto gerador—vamos chamá-lo de gen .

2. Chama next(gen) para acionar com ele a palavra reservada yield .

3. Devolve o valor produzido por next(gen) , para permitir que o usuário o vincule a uma variável usando o formato
with/as .

Quando o bloco with termina, o método __exit__ :

1. Verifica se uma exceção foi passada como exc_type ; em caso afirmativo, gen.throw(exception) é invocado,
fazendo com que a exceção seja levantada na linha yield , dentro do corpo da função geradora.
2. Caso contrário, next(gen) é chamado, retomando a execução do corpo da função geradora após o yield .

O Exemplo 5 tem um defeito: Se uma exceção for levantada no corpo do bloco with , o interpretador Python vai
capturá-la e levantá-la novamente na expressão yield dentro de looking_glass . Mas não há tratamento de erro ali,
então o gerador looking_glass vai terminar sem nunca restaurar o método sys.stdout.write original, deixando o
sistema em um estado inconsistente.

O Exemplo 7 acrescenta o tratamento especial da exceção ZeroDivisionError , tornando esse gerenciador de contexto
funcionalmente equivalente ao Exemplo 3, baseado em uma classe.

Exemplo 7. mirror_gen_exc.py: gerenciador de contexto baseado em um gerador implementando tratamento de erro—


com o mesmo comportamento externo de Exemplo 3

PY
import contextlib
import sys

@contextlib.contextmanager
def looking_glass():
original_write = sys.stdout.write

def reverse_write(text):
original_write(text[::-1])

sys.stdout.write = reverse_write
msg = '' # (1)
try:
yield 'JABBERWOCKY'
except ZeroDivisionError: # (2)
msg = 'Please DO NOT divide by zero!'
finally:
sys.stdout.write = original_write # (3)
if msg:
print(msg) # (4)

1. Cria uma variável para uma possível mensagem de erro; essa é a primeira mudança em relação a Exemplo 5.
2. Trata ZeroDivisionError , fixando uma mensagem de erro.

3. Desfaz o monkey-patching de sys.stdout.write .

4. Mostra a mensagem de erro, se ela foi determinada.

Lembre-se que o método __exit__ diz ao interpretador que ele tratou a exceção ao devolver um valor verdadeiro;
nesse caso, o interpretador suprime a exceção.
Por outro lado, se __exit__ não devolver explicitamente um valor, o interpretador recebe o habitual None , e propaga
a exceção. Com o @contextmanager , o comportamento default é invertido: o método __exit__ fornecido pelo
decorador assume que qualquer exceção enviada para o gerador está tratada e deve ser suprimida.

Ter um try/finally (ou um bloco with ) em torno do yield é o preço inescapável do uso de
👉 DICA @contextmanager , porque você nunca sabe o que os usuários do seu gerenciador de contexto vão
fazer dentro do bloco with .[239]

Um recurso pouco conhecido do @contextmanager é que os geradores decorados com ele podem ser usados eles
mesmos como decoradores.[240] Isso ocorre porque @contextmanager é implementado com a classe
contextlib.ContextDecorator .

O Exemplo 8 mostra o gerenciador de contexto looking_glass do Exemplo 5 sendo usado como um decorador.

Exemplo 8. O gerenciador de contexto looking_glass também funciona como um decorador.

PY
>>> @looking_glass()
... def verse():
... print('The time has come')
...
>>> verse() # (1)
emoc sah emit ehT
>>> print('back to normal') # (2)
back to normal

1. looking_glass faz seu trabalho antes e depois do corpo de verse rodar.


2. Isso confirma que o sys.write original foi restaurado.

Compare o Exemplo 8 com o Exemplo 6, onde looking_glass é usado como um gerenciador de contexto.

Um interessante exemplo real do uso do @contextmanager fora da biblioteca padrão é a reescrita de arquivo no
mesmo lugar usando um gerenciador de contexto (https://fpy.li/18-11) de Martijn Pieters. O Exemplo 9 mostra como ele é
usado.

Exemplo 9. Um gerenciador de contexto para reescrever arquivos no lugar

PYTHON3
import csv

with inplace(csvfilename, 'r', newline='') as (infh, outfh):


reader = csv.reader(infh)
writer = csv.writer(outfh)

for row in reader:


row += ['new', 'columns']
writer.writerow(row)

A função inplace é um gerenciador de contexto que fornece a você dois identificadores—no exemplo, infh e outfh
—para o mesmo arquivo, permitindo que seu código leia e escreva ali ao mesmo tempo. Isso é mais fácil de usar que a
função fileinput.input (https://docs.python.org/pt-br/3/library/fileinput.html#fileinput.input) (EN) da biblioteca padrão (que,
por sinal, também fornece um gerenciador de contexto).
Se você quiser estudar o código-fonte do inplace de Martijn (listado no post (https://fpy.li/18-11)) (EN), encontre a
palavra reservada yield : tudo antes dela lida com configurar o contexto, que implica criar um arquivo de backup,
então abrir e produzir referências para os identificadores de arquivo de leitura e escrita que serão devolvidos pela
chamada a __enter__ . O processamento do __exit__ após o yield fecha os identificadores do arquivo e, se algo
deu errado, restaura o arquivo do backup.

Isso conclui nossa revisão da instrução with e dos gerenciadores de contexto. Vamos agora olhar o match/case , no
contexto de um exemplo completo.

18.3. Pattern matching no lis.py: um estudo de caso


Na seção Seção 2.6.1, vimos exemplos de sequências de padrões extraídos da funcão evaluate do interpretador lis.py
de Peter Norvig, portado para o Python 3.10. Nessa seção quero dar um visão geral do funcionamento do lis.py, e
também explorar todas as cláusulas case de evaluate , explicando não apenas os padrões mas também o que o
interpretador faz em cada case .

Além de mostrar mais pattern matching, escrevi essa seção por três razões:

1. O lis.py de Norvig é um lindo exemplo de código Python idiomático.


2. A simplicidade do Scheme é uma aula magna de design de linguagens.
3. Aprender como um interpretador funciona me deu um entendimento mais profundo sobre o Python e sobre
linguagens de programação em geral—interpretadas ou compiladas.

Antes de olhar o código Python, vamos ver um pouquinho de Scheme, para você poder entender este estudo de caso—
pensando em quem nunca viu Scheme e Lisp antes.

18.3.1. A sintaxe do Scheme


No Scheme não há distinção entre expressões e instruções, como temos em Python. Também não existem operadores
infixos. Todas as expressões usam a notação prefixa, como (+ x 13) em vez de x + 13 . A mesma notação prefixa é
usada para chamadas de função—por exemplo, (gcd x 13) —e formas especiais—por exemplo, (define x 13) , que
em Python escreveríamos como uma declaração de atribuição x = 13 .

A notação usada no Scheme e na maioria dos dialetos de Lisp é conhecida como S-expression (Expressão-S).[241]

O Exemplo 10 mostra um exemplo simples em Scheme.

Exemplo 10. Maior divisor comum em Scheme

SCHEME
(define (mod m n)
(- m (* n (quotient m n))))

(define (gcd m n)
(if (= n 0)
m
(gcd n (mod m n))))

(display (gcd 18 45))

O Exemplo 10 mostra três expressões em Scheme: duas definições de função— mod e gcd —e uma chamada a
display , que vai devolver 9, o resultado de (gcd 18 45) . O Exemplo 11 é o mesmo código em Python (menor que a
explicação em português do algoritmo recursivo de Euclides (https://pt.wikipedia.org/wiki/Algoritmo_de_Euclides)).
Exemplo 11. Igual ao Exemplo 10, mas escrito em Python

PY
def mod(m, n):
return m - (m // n * n)

def gcd(m, n):


if n == 0:
return m
else:
return gcd(n, mod(m, n))

print(gcd(18, 45))

Em Python idiomático, eu usaria o operador % em vez de reinventar mod , e seria mais eficiente usar um loop while
em vez de recursão. Mas queria mostrar duas definições de função, e fazer os exemplos o mais similares possível, para
ajudar você a ler o código Scheme.

O Scheme não tem instruções iterativas de controle de fluxo como while ou for . A iteração é feita com recursão.
Observe que não há atribuições nos exemplos em Python e Scheme. O uso extensivo de recursão e o uso mínimo de
atribuição são marcas registradas do estilo funcional de programação.[242]

Agora vamos revisar o código da versão Python 3.10 do lis.py. O código fonte completo, com testes, está no diretório 18-
with-match/lispy/py3.10/ (https://fpy.li/18-15), do repositório fluentpython/example-code-2e (https://fpy.li/code) no Github.

18.3.2. Importações e tipos


O Exemplo 12 mostra as primeiras linhas do lis.py. O uso do TypeAlias e do operador de união de tipos | exige o
Python 3.10.

Exemplo 12. lis.py: início do arquivo

PY
import math
import operator as op
from collections import ChainMap
from itertools import chain
from typing import Any, TypeAlias, NoReturn

Symbol: TypeAlias = str


Atom: TypeAlias = float | int | Symbol
Expression: TypeAlias = Atom | list

Os tipos definidos são:

Symbol

Só um alias para str . Em lis.py, Symbol é usado para identificadores; não há um tipo de dados string, com
operações como fatiamento (slicing), divisão (splitting), etc.[243]

Atom
Um elemento sintático simples, tal como um número ou um Symbol —ao contrário de uma estrutura complexa,
composta por vários elementos distintos, como uma lista.

Expression

Os componentes básicos de programas Scheme são expressões feitas de átomos e listas, possivelmente aninhados.
18.3.3. O parser
O parser (analisador sintático) de Norvig tem 36 linhas de código que exibem o poder do Python aplicado ao
tratamento da sintaxe recursiva simples das expressões-S—sem strings, comentários, macros e outros recursos que
tornam a análise sintática do Scheme padrão mais complicada (Exemplo 13).

Exemplo 13. lis.py: as principais funcões do analisador

PY
def parse(program: str) -> Expression:
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))

def tokenize(s: str) -> list[str]:


"Convert a string into a list of tokens."
return s.replace('(', ' ( ').replace(')', ' ) ').split()

def read_from_tokens(tokens: list[str]) -> Expression:


"Read an expression from a sequence of tokens."
# mais código do analisador omitido na listagem do livro

A principal função desse grupo é parse , que recebe uma expressão-S em forma de str e devolve um objeto
Expression , como definido no Exemplo 12: um Atom ou uma list que pode conter mais átomos e listas aninhadas.

Norvig usa um truque elegante em tokenize : ele acrescenta espaços antes e depois de cada parênteses na entrada, e
então a recorta, resultando em uma lista de símbolos sintáticos (tokens) com '(' e ')' como símbolos separados Esse
atalho funciona porque não há um tipo string no pequeno Scheme de lis.py, então todo '(' ou ')' é um delimitador
de expressão. O código recursivo do analisador está em read_from_tokens , uma função de 14 linhas que você pode
ler no repositório fluentpython/example-code-2e (https://fpy.li/18-17). Vou pular isso, pois quero me concentrar em outras
partes do interpretador.

Aqui estão alguns doctests estraídos do lispy/py3.10/examples_test.py (https://fpy.li/18-18):

PYCON
>>> from lis import parse
>>> parse('1.5')
1.5
>>> parse('ni!')
'ni!'
>>> parse('(gcd 18 45)')
['gcd', 18, 45]
>>> parse('''
... (define double
... (lambda (n)
... (* n 2)))
... ''')
['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]]

As regras de avaliação para esse subconjunto do Scheme são simples:

1. Um símbolo sintático que se pareça com um número é tratado como um float ou um int .

2. Todo o resto que não seja um '(' ou um ')' é considerado um Symbol —uma str , a ser usado como um
identificador. Isso inclui texto no código-fonte como + , set! , e make-counter , que são identificadores válidos em
Scheme, mas não em Python.
3. Expressões dentro de '(' e ')' são avaliadas recursivamente como listas contendo átomos ou listas aninhadas
que podem conter átomos ou mais listas aninhadas.
Usando a terminologia do interpretador Python, a saída de parse é uma AST (Abstract Syntax Tree—Árvore Sintática
Abstrata): uma representação conveniente de um programa Scheme como listas aninhadas formando uma estrutura
similar a uma árvore, onde a lista mais externa é o tronco, listas internas são os galhos, e os átomos são as folhas
(Figura 1).

Figura 1. Uma expressão lambda de Scheme, representada como código-fonte (sintaxe concreta de expressões-S), como
uma árvore, e como uma sequência de objetos Python (sintaxe abstrata).

18.3.4. O ambiente
A classe Environment estende collections.ChainMap , acrescentando o método change , para atualizar um valor
dentro de um dos dicts encadeados que as instâncias de ChainMap mantém em uma lista de mapeamentos: o atributo
self.maps . O método change é necessário para suportar a forma (set! …) do Scheme, descrita mais tarde; veja o
Exemplo 14.

Exemplo 14. lis.py: a classe Environment

PY
class Environment(ChainMap[Symbol, Any]):
"A ChainMap that allows changing an item in-place."

def change(self, key: Symbol, value: Any) -> None:


"Find where key is defined and change the value there."
for map in self.maps:
if key in map:
map[key] = value # type: ignore[index]
return
raise KeyError(key)
Observe que o método change só atualiza chaves existentes.[244] Tentar mudar uma chave não encontrada causa um
KeyError .

Esse doctest mostra como Environment funciona:

PYCON
>>> from lis import Environment
>>> inner_env = {'a': 2}
>>> outer_env = {'a': 0, 'b': 1}
>>> env = Environment(inner_env, outer_env)
>>> env['a'] # (1)
2
>>> env['a'] = 111 # (2)
>>> env['c'] = 222
>>> env
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1})
>>> env.change('b', 333) # (3)
>>> env
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333})

1. Ao ler os valores, Environment funciona como ChainMap : as chaves são procuradas nos mapeamentos aninhados
da esquerda para a direita. Por isso o valor de a no outer_env é encoberto pelo valor em inner_env .
2. Atribuir com [] sobrescreve ou insere novos itens, mas sempre no primeiro mapeamento, inner_env nesse
exemplo.
3. env.change('b', 333) busca a chave b e atribui a ela um novo valor no mesmo lugar, no outer_env

A seguir temos a função standard_env() , que constrói e devolve um Environment carregado com funções pré-
definidas, similar ao módulo __builtins__ do Python, que está sempre disponível (Exemplo 15).

Exemplo 15. lis.py: standard_env() constrói e devolve o ambiente global

PY
def standard_env() -> Environment:
"An environment with some Scheme standard procedures."
env = Environment()
env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+': op.add,
'-': op.sub,
'*': op.mul,
'/': op.truediv,
# omitted here: more operator definitions
'abs': abs,
'append': lambda *args: list(chain(*args)),
'apply': lambda proc, args: proc(*args),
'begin': lambda *x: x[-1],
'car': lambda x: x[0],
'cdr': lambda x: x[1:],
# omitted here: more function definitions
'number?': lambda x: isinstance(x, (int, float)),
'procedure?': callable,
'round': round,
'symbol?': lambda x: isinstance(x, Symbol),
})
return env

Resumindo, o mapeamento env é carregado com:

Todas as funções do módulo math do Python


Operadores selecionados do módulo op do Python
Funções simples porém poderosas construídas com o lambda do Python
Estruturas e entidades embutidas do Python, ou renomeadas, como callable para procedure? , ou mapeadas
diretamente, como round

18.3.5. O REPL
O REPL (read-eval-print-loop, loop-lê-calcula-imprime ) de Norvig é fácil de entender mas não é amigável ao usuário
(veja o Exemplo 16). Se nenhum argumento de linha de comando é passado a lis.py, a função repl() é invocada por
main() —definida no final do módulo. No prompt de lis.py> , devemos digitar expressões corretas e completas; se
esquecemos de fechar um só parênteses, lis.py se encerra.[245]

Exemplo 16. As funções do REPL

PYTHON3
def repl(prompt: str = 'lis.py> ') -> NoReturn:
"A prompt-read-eval-print loop."
global_env = Environment({}, standard_env())
while True:
ast = parse(input(prompt))
val = evaluate(ast, global_env)
if val is not None:
print(lispstr(val))

def lispstr(exp: object) -> str:


"Convert a Python object back into a Lisp-readable string."
if isinstance(exp, list):
return '(' + ' '.join(map(lispstr, exp)) + ')'
else:
return str(exp)

Segue uma breve explicação sobre essas duas funções:

repl(prompt: str = 'lis.py> ') → NoReturn


Chama standard_env() para provisionar as funções embutidas para o ambiente global, então entra em um loop
infinito, lendo e avaliando cada linha de entrada, calculando-a no ambiente global, e exibindo o resultado—a menos
que seja None . O global_env pode ser modificado por evaluate . Por exemplo, quando o usuário define uma nova
variável global ou uma função nomeada, ela é armazenada no primeiro mapeamento do ambiente—o dict vazio
na chamada ao construtor de Environment na primeira linha de repl .

lispstr(exp: object) → str


A função inversa de parse : dado um objeto Python representando uma expressão, lispstr devolve o código-fonte
para ela. Por exemplo, dado ['+', 2, 3] , o resultado é '(+ 2 3)' .

18.3.6. O avaliador de expressões


Agora podemos apreciar a beleza do avaliador de expressões de Norvig—tornado um pouco mais bonito com
match/case . A função evaluate no Exemplo 17 recebe uma Expression (construída por parse ) e um
Environment .

O corpo de evaluate é composto por uma única instrução match com uma expressão exp como sujeito. Os padrões
de case expressam a sintaxe e a semântica do Scheme com uma clareza impressionante.

Exemplo 17. evaluate recebe uma expressão e calcula seu valor


PYTHON3
KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']

def evaluate(exp: Expression, env: Environment) -> Any:


"Evaluate an expression in an environment."
match exp:
case int(x) | float(x):
return x
case Symbol(var):
return env[var]
case ['quote', x]:
return x
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
case ['lambda', [*parms], *body] if body:
return Procedure(parms, body, env)
case ['define', Symbol(name), value_exp]:
env[name] = evaluate(value_exp, env)
case ['define', [Symbol(name), *parms], *body] if body:
env[name] = Procedure(parms, body, env)
case ['set!', Symbol(name), value_exp]:
env.change(name, evaluate(value_exp, env))
case [func_exp, *args] if func_exp not in KEYWORDS:
proc = evaluate(func_exp, env)
values = [evaluate(arg, env) for arg in args]
return proc(*values)
case _:
raise SyntaxError(lispstr(exp))

Vamos estudar cada cláusula case e o que cada uma faz. Em algumas ocasiões eu acrescentei comentários, mostrando
uma expressão-S que casaria com padrão quando transformado em uma lista do Python. Os doctests extraídos de
examples_test.py (https://fpy.li/18-21) demonstram cada case .

avaliando números
PYTHON3
case int(x) | float(x):
return x

Padrão:
Instância de int ou float .

Ação:
Devolve o próprio valor.

Exemplo:
PYCON
>>> from lis import parse, evaluate, standard_env
>>> evaluate(parse('1.5'), {})
1.5

avaliando símbolos
PYTHON3
case Symbol(var):
return env[var]

Padrão:
Instância de Symbol , isto é, uma str usada como identificador.

Ação:
Consulta var em env e devolve seu valor.

Exemplos:
PYCON
>>> evaluate(parse('+'), standard_env())
<built-in function add>
>>> evaluate(parse('ni!'), standard_env())
Traceback (most recent call last):
...
KeyError: 'ni!'

(quote …)
A forma especial quote trata átomos e listas como dados em vez de expressões a serem avaliadas.

PYTHON3
# (quote (99 bottles of beer))
case ['quote', x]:
return x

Padrão:
Lista começando com o símbolo 'quote' , seguido de uma expressão x .

Ação:
Devolve x sem avaliá-la.

Exemplos:
PYCON
>>> evaluate(parse('(quote no-such-name)'), standard_env())
'no-such-name'
>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env())
[99, 'bottles', 'of', 'beer']
>>> evaluate(parse('(quote (/ 10 0))'), standard_env())
['/', 10, 0]

Sem quote , cada expressão no teste geraria um erro:

no-such-name seria buscado no ambiente, gerando um KeyError

(99 bottles of beer) não pode ser avaliado, pois o número 99 não é um Symbol nomeando uma forma especial,
um operador ou uma função
(/ 10 0) geraria um ZeroDivisionError

Por que linguagens tem palavras reservadas?


Apesar de ser simples, quote não pode ser implementada como uma função em Scheme. Seu poder especial é
impedir que o interpretador avalie (f 10) na expressão (quote (f 10)) : o resultado é apenas uma lista com
um Symbol e um int . Por outro lado, em uma chamada de função como (abs (f 10)) , o interpretador
primeiro calcula o resultado de (f 10) antes de invocar abs . Por isso quote é uma palavra reservada: ela
precisa ser tratada como uma forma especial.

De modo geral, palavras reservadas são necessárias para:


Introduzir regras especiais de avaliação, como quote e lambda —que não avaliam nenhuma de suas sub-
expressões
Mudar o fluxo de controle, como em if e chamadas de função—que também tem regras especiais de
avaliação
Para gerenciar o ambiente, como em define e set

Por isso também o Python, e linguagens de programação em geral, precisam de palavras reservadas. Pense em
def , if , yield , import , del , e o que elas fazem em Python.

(if …)
PYTHON3
# (if (< x 0) 0 x)
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)

Padrão:
Lista começando com 'if' seguida de três expressões: test , consequence , e alternative .

Ação:
Avalia test :

Se verdadeira, avalia consequence e devolve seu valor.


Caso contrário, avalia alternative e devolve seu valor.

Exemplos:
PYCON
>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env())
1
>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env())
0

Os ramos consequence e alternative devem ser expressões simples. Se mais de uma expressão for necessária em
um ramo, você pode combiná-las com (begin exp1 exp2…) , fornecida como uma função em lis.py—veja o Exemplo
15.

(lambda …)
A forma lambda do Scheme define funções anônimas. Ela não sofre das limitações da lambda do Python: qualquer
função que pode ser escrita em Scheme pode ser escrita usando a sintaxe (lambda …) .

PYTHON3
# (lambda (a b) (/ (+ a b) 2))
case ['lambda' [*parms], *body] if body:
return Procedure(parms, body, env)

Padrão:
Lista começando com 'lambda' , seguida de:

Lista de zero ou mais nomes de parâmetros


Uma ou mais expressões coletadas em body (a expressão guarda assegura que body não é vazio).
Ação:
Cria e devolve uma nova instância de Procedure com os nomes de parâmetros, a lista de expressões como o corpo
da função, e o ambiente atual.

Exemplo:
PYCON
>>> expr = '(lambda (a b) (* (/ a b) 100))'
>>> f = evaluate(parse(expr), standard_env())
>>> f # doctest: +ELLIPSIS
<lis.Procedure object at 0x...>
>>> f(15, 20)
75.0

A classe Procedure implementa o conceito de uma closure (clausura): um objeto invocável contendo nomes de
parâmetros, um corpo de função, e uma referência ao ambiente no qual a Procedure está sendo instanciada. Vamos
estudar o código de Procedure daqui a pouco.

(define …)
A palavra reservada define é usada de duas formas sintáticas diferentes. A mais simples é:

PYTHON3
# (define half (/ 1 2))
case ['define', Symbol(name), value_exp]:
env[name] = evaluate(value_exp, env)

Padrão:
Lista começando com 'define' , seguido de um Symbol e uma expressão.

Ação:
Avalia a expressão e coloca o valor resultante em env , usando name como chave.

Exemplo:
PYCON
>>> global_env = standard_env()
>>> evaluate(parse('(define answer (* 7 6))'), global_env)
>>> global_env['answer']
42

O doctest para esse case cria um global_env , para podermos verificar que evaluate coloca answer dentro
daquele Environment .

Podemos usar primeira forma de define para criar variáveis ou para vincular nomes a funções anônimas, usando
(lambda …) como o value_exp .

A segunda forma de define é um atalho para definir funções nomeadas.

PYTHON3
# (define (average a b) (/ (+ a b) 2))
case ['define', [Symbol(name), *parms], *body] if body:
env[name] = Procedure(parms, body, env)

Padrão:
Lista começando com 'define' , seguida de:

Uma lista começando com um Symbol(name) , seguida de zero ou mais itens agrupados em uma lista chamada
parms .
Uma ou mais expressões agrupadas em body (a expressão guarda garante que body não esteja vazio)

Ação:
Cria uma nova instância de Procedure com os nomes dos parâmetros, a lista de expressões como o corpo, e o
ambiente atual.
Insere a Procedure em env , usando name como chave.
O doctest no Exemplo 18 define e coloca no global_env uma função chamada % , que calcula uma porcentagem.

Exemplo 18. Definindo uma função chamada % , que calcula uma porcentagem

PYCON
>>> global_env = standard_env()
>>> percent = '(define (% a b) (* (/ a b) 100))'
>>> evaluate(parse(percent), global_env)
>>> global_env['%'] # doctest: +ELLIPSIS
<lis.Procedure object at 0x...>
>>> global_env['%'](170, 200)
85.0

Após chamar evaluate , verificamos que % está vinculada a uma Procedure que recebe dois argumentos numéricos
e devolve uma porcentagem.

O padrão para o segundo define não obriga os itens em parms a serem todos instâncias de Symbol . Eu teria que
verificar isso antes de criar a Procedure , mas não o fiz—para manter o código aqui tão fácil de acompanhar quanto o
de Norvig.

(set! …)
A forma set! muda o valor de uma variável previamente definida.[246]

PYTHON3
# (set! n (+ n 1))
case ['set!', Symbol(name), value_exp]:
env.change(name, evaluate(value_exp, env))

Padrão:
Lista começando com 'set!' , seguida de um Symbol e de uma expressão.

Ação:
Atualiza o valor de name em env com o resultado da avaliação da expressão.

O método Environment.change atravessa os ambientes encadeados de local para global, e atualiza a primeira
ocorrência de name com o novo valor. Se não estivéssemos implementando a palavra reservada 'set!' , esse
interpretador poderia usar apenas o ChainMap do Python para implementar env , sem precisar da nossa classe
Environment .

O nonlocal do Python e o set! do Scheme tratam da mesma questão


O uso da forma set! está relacionado ao uso da palavra reservada nonlocal em Python: declarar nonlocal x
permite a x = 10 atualizar uma variável x anteriormente definida fora do escopo local. Sem a declaração
nonlocal x , x = 10 vai sempre criar uma variável local em Python, como vimos na seção Seção 9.7.
De forma similar, (set! x 10) atualiza um x anteriormente definido que pode estar fora do ambiente local da
função. Por outro lado, a variável x em (define x 10) é sempre uma variável local, criada ou atualizada no
ambiente local.

Ambos, nonlocal e (set! …) , são necessários para atualizar o estados do programas mantidos em variáveis
dentro de uma clausura (closure). O Exemplo 13 demonstrou o uso de nonlocal para implementar uma função
que calcula uma média contínua, mantendo itens count e total em uma clausura. Aqui está a mesma ideia,
escrita no subconjunto de Scheme de lis.py:

SCHEME
(define (make-averager)
(define count 0)
(define total 0)
(lambda (new-value)
(set! count (+ count 1))
(set! total (+ total new-value))
(/ total count)
)
)
(define avg (make-averager)) # (1)
(avg 10) # (2)
(avg 11) # (3)
(avg 15) # (4)

1. Cria uma nova clausura com a função interna definida por lambda e as variáveis count e total ,
inicialziadas com 0; vincula a clausura a avg .
2. Devolve 10.0.
3. Devolve 10.5.
4. Devolve 12.0.

O código acima é um dos testes em lispy/py3.10/examples_test.py (https://fpy.li/18-18).

Agora chegamos a uma chamada de função.

Chamada de função
PYTHON3
# (gcd (* 2 105) 84)
case [func_exp, *args] if func_exp not in KEYWORDS:
proc = evaluate(func_exp, env)
values = [evaluate(arg, env) for arg in args]
return proc(*values)

Padrão:
Lista com um ou mais itens.

A expressão guarda garante que func_exp não é um de ['quote', 'if', 'define', 'lambda', 'set!'] —
listados logo antes de evaluate no Exemplo 17.

O padrão casa com qualquer lista com uma ou mais expressões, vinculando a primeira expressão a func_exp eo
restante a args como uma lista, que pode ser vazia.

Ação:
Avaliar func_exp para obter uma proc da função.
Avaliar cada item em args para criar uma lista de valores dos argumentos.
Chamar proc com os valores como argumentos separados, devolvendo o resultado.

Exemplo:
PYCON
>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env)
42.0

Esse doctest continua do Exemplo 18: ele assume que global_env contém uma função chamada % . Os argumentos
passados a % são expressões aritméticas, para enfatizar que eles são avaliados antes da função ser chamada.

A expressão guarda nesse case é necessária porque [func_exp, *args] casa com qualquer sequência sujeito com
um ou mais itens. Entretanto, se func_exp é uma palavra reservada e o sujeito não casou com nenhum dos case
anteriores, então isso é de fato um erro de sintaxe.

Capturar erros de sintaxe


Se o sujeito exp não casa com nenhum dos case anteriores, o case "pega tudo" gera um SyntaxError :

PYTHON3
case _:
raise SyntaxError(lispstr(exp))

Aqui está um exemplo de um (lambda …) malformado, identificado como um SyntaxError :

PYCON
>>> evaluate(parse('(lambda is not like this)'), standard_env())
Traceback (most recent call last):
...
SyntaxError: (lambda is not like this)

Se o para chamada de função não tivesse aquela expressão guarda rejeitando palavras reservadas, a expressão
case
(lambda is not like this) teria sido tratada como uma chamada de função, que geraria um KeyError , pois
'lambda' não é parte do ambiente—da mesma forma que lambda em Python não é uma função embutida.

18.3.7. Procedure: uma classe que implementa uma clausura


A classe Procedure poderia muito bem se chamar Closure , porque é isso que ela representa: uma definição de
função junto com um ambiente. A definição de função inclui o nome dos parâmetros e as expressões que compõe o
corpo da funcão. O ambiente é usado quando a função é chamada, para fornecer os valores das variáveis livres:
variáveis que aparecem no corpo da função mas não são parâmetros, variáveis locais ou variáveis globais. Vimos os
conceitos de clausura e de variáveis livres na seção Seção 9.6.

Aprendemos como usar clausuras em Python, mas agora podemos mergulhar mais fundo e ver como uma clausura é
implementada em lis.py:
PYTHON
class Procedure:
"A user-defined Scheme procedure."

def __init__( # (1)


self, parms: list[Symbol], body: list[Expression], env: Environment
):
self.parms = parms # (2)
self.body = body
self.env = env

def __call__(self, *args: Expression) -> Any: # (3)


local_env = dict(zip(self.parms, args)) # (4)
env = Environment(local_env, self.env) # (5)
for exp in self.body: # (6)
result = evaluate(exp, env)
return result # (7)

1. Chamada quando uma função é definida pelas formas lambda ou define .

2. Salva os nomes dos parâmetros, as expressões no corpo e o ambiente, para uso posterior.
3. Chamada por proc(*values) na última linha da cláusula case [func_exp, *args] .

4. Cria local_env , mapeando self.parms como nomes de variáveis locais e os args passados como valores.
5. Cria um novo env combinado, colocando local_env primeiro e então self.env —o ambiente que foi salvo
quando a função foi definida.
6. Itera sobre cada expressão em self.body , avaliando-as no env combinado.
7. Devolve o resultado da última expressão avaliada.

Há um par de funções simples após evaluate em lis.py (https://fpy.li/18-24): run lê um programa Scheme completo e o
executa, e main chama run ou repl , dependendo da linha de comando—parecido com o modo como o Python faz.
Não vou descrever essas funções, pois não há nada novo ali. Meus objetivos aqui eram compartilhar com vocês a
beleza do pequeno interpretador de Norvig, explicar melhor como as clausuras funcionam, e mostrar como
match/case foi uma ótima adição ao Python.

Para fechar essa seção estendida sobre pattern matching, vamos formalizar o conceito de um OR-pattern (padrão-OU).

18.3.8. Using padrões-OU


Uma série de padrões separados por | formam um OR-pattern (https://fpy.li/18-25) (EN): ele tem êxito se qualquer dos
sub-padrões tiver êxito. O padrão em Seção 18.3.6.1 é um OR-pattern:

PYTHON3
case int(x) | float(x):
return x

Todos os sub-padrões em um OR-pattern devem usar as mesmas variáveis. Essa restrição é necessária para garantir
que as variáveis estejam disponíveis para a expressão de guarda e para o corpo do case , independente de qual sub-
padrão tenha sido bem sucedido.

No contexto de uma cláusula case , o operador | tem um significado especial. Ele não aciona o
⚠️ AVISO método especial __or__ , que manipula expressões como a | b em outros contextos, onde ele é
sobrecarregado para realizar operações como união de conjuntos ou disjunção binária com inteiros
(o "ou binário"), dependendo dos operandos.
Um OR-pattern não está limitado a aparecer no nível superior de um padrão. | pode também ser usado em sub-
padrões. Por exemplo, se quiséssemos que o lis.py aceitasse a letra grega λ (lambda)[247] além da palavra reservada
lambda , poderíamos reescrever o padrão assim:

PYTHON3
# (λ (a b) (/ (+ a b) 2) )
case ['lambda' | 'λ', [*parms], *body] if body:
return Procedure(parms, body, env)

Agora podemos passar para o terceiro e último assunto deste capítulo: lugares incomuns onde a cláusula else pode
aparecer no Python.

18.4. Faça isso, então aquilo: os blocos else além do if


Isso não é segredo, mas é um recurso pouco conhecido em Python: a cláusula else pode ser usada não apenas com
instruções if , mas também com as instruções for , while , e try .

A semântica para for/else , while/else , e try/else é semelhante, mas é muito diferente do if/else . No início, a
palavra else na verdade atrapalhou meu entendimento desses recursos, mas no fim acabei me acostumando.

Aqui estão as regras:

for

O bloco else vai ser executado apenas se e quando o loop for rodar até o fim (isto é, não rodará se o for for
interrompido com um break ).

while

O bloco else vai ser executado apenas se e quando o loop while terminar pela condição se tornar falsa
(novamente, não rodará se o while for interrompido por um break )

try
O bloco else vai ser executado apenas se nenhuma exceção for gerada no bloco try . A documentação oficial
(https://docs.python.org/pt-br/3/reference/compound_stmts.html) também afirma: "Exceções na cláusula else não são
tratadas pela cláusula except precedente.""

Em todos os casos, a cláusula else também será ignorada se uma exceção ou uma instrução return , break ou
continue fizer com que o fluxo de controle saia do bloco principal da instrução composta.

Acho else uma escolha muito pobre de palavra reservada em todos os casos, exceto o if . Ela
implica em uma alternativa excludente, como em "Execute esse loop, caso contrário faça aquilo."

✒️ NOTA Mas a semântica para o else em loops é o oposto: "Execute esse loop, daí faça aquilo." Isso sugere
then como uma escolha melhor—que também faria sentido no contexto de um try : "Tente isso,
então faça aquilo." Entretanto, acrescentar uma palavra reservada é uma ruptura séria em uma
linguagem—uma decisão muito difícil.

Usar else com essas instruções muitas vezes torna o código mais fácil de ler e evita o transtorno de configurar flags
de controle ou acrescentar instruções if extras ao código.

O uso de else em loops em geral segue o padrão desse trecho:


PYTHON3
for item in my_list:
if item.flavor == 'banana':
break
else:
raise ValueError('No banana flavor found!')

No caso de blocos try/except , o else pode parecer redundante à primeira vista. Afinal, a after_call() no trecho
a seguir só será executado se a dangerous_call() não gerar uma exceção, correto?

PYTHON3
try:
dangerous_call()
after_call()
except OSError:
log('OSError...')

Entretanto, isso coloca a after_call() dentro do bloco try sem um bom motivo. Por clareza e correção, o corpo de
um bloco try deveria conter apenas instruções que podem gerar as exceções esperadas. Isso é melhor:

PYTHON3
try:
dangerous_call()
except OSError:
log('OSError...')
else:
after_call()

Agora fica claro que o bloco try está de guarda contra possíveis erros na dangerous_call() , e não em
after_call() . Também fica explícito que after_call() só será executada se nenhuma exceção for gerada no bloco
try .

Em Python, try/except é frequentemene usado para controle de fluxo, não apenas para tratamento de erro. Há
inclusive um acrônimo/slogan para isso, documentado no glossário oficial do Python
(https://docs.python.org/pt-br/3/glossary.html#term-eafp):

“ EAFP
Iniciais da expressão em inglês “easier to ask for forgiveness than permission” que significa “é
mais fácil pedir perdão que permissão”. Este estilo de codificação comum em Python assume a
existência de chaves ou atributos válidos e captura exceções caso essa premissa se prove
falsa. Este estilo limpo e rápido se caracteriza pela presença de várias instruções try e
except . A técnica diverge do estilo LBYL, comum em outras linguagens como C, por exemplo.

O glossário então define LBYL:


“ LBYL
Iniciais da expressão em inglês “look before you leap”, que significa algo como “olhe antes de
pisar”[NT: ou "olhe antes de pular"]. Este estilo de codificação testa as pré-condições
explicitamente antes de fazer chamadas ou buscas. Este estilo contrasta com a abordagem
EAFP e é caracterizada pela presença de muitas instruções if . Em um ambiente multithread,
a abordagem LBYL pode arriscar a introdução de uma condição de corrida entre “o olhar” e
“o pisar”. Por exemplo, o código if key in mapping: return mapping[key] pode falhar se
outra thread remover key do mapping após o teste, mas antes da olhada. Esse problema
pode ser resolvido com bloqueios [travas] ou usando a abordagem EAFP.

Dado o estilo EAFP, faz mais sentido conhecer e usar os blocos else corretamente nas instruções try/except .

Quando a [inclusão da] instrução match foi discutida, algumas pessoas (eu incluído) acharam que
✒️ NOTA ela também devia ter uma cláusula else . No fim ficou decidido que isso não era necessário, pois
case _: tem o mesmo efeito.[248]

Agora vamos resumir o capítulo.

18.5. Resumo do capítulo


Este capítulo começou com gerenciadores de contexto e o significado da instrução with , indo rapidamente além de
uso comum (o fechamento automático de arquivos abertos). Implementamos um gerenciador de contexto
personalizado: a classe LookingGlass , usando os métodos __enter__/__exit__ , e vimos como tratar exceções no
método __exit__ . Uma ideia fundamental apontada por Raymond Hettinger, na palestra de abertura da Pycon US
2013, é que with não serve apenas para gerenciamento de recursos; ele é uma ferramenta para fatorar código comum
de configuração e de finalização, ou qualquer par de operações que precisem ser executadas antes e depois de outro
procedimento.[249]

Revisamos funções no módulo contextlib da biblioteca padrão. Uma delas, o decorador @contextmanager , permite
implementar um gerenciador de contexto usando apenas um mero gerador com um yield—uma solução menos
trabalhosa que criar uma classe com pelo menos dois métodos. Reimplementamos a LookingGlass como uma função
geradora looking_glass , e discutimos como fazer tratamento de exceções usando o @contextmanager .

Nós então estudamos o elegante interpretador Scheme de Peter Norvig, o lis.py, escrito em Python idiomático e
refatorado para usar match/case em evaluate —a função central de qualquer interpretador. Entender o
funcionamenteo de evaluate exigiu revisar um pouco de Scheme, um parser para expressões-S, um REPL simples e a
construção de escopos aninhados através de Environment , uma subclasse de collection.ChainMap . No fim, lys.py se
tornou um instrumento para explorarmos muito mais que pattern matching. Ele mostra como diferentes partes de um
interpretador trabalham juntas, jogando luz sobre recursos fundamentais do próprio Python: porque palavras
reservadas são necessárias, como as regras de escopo funcionam, e como clausuras são criadas e usadas.

18.6. Para saber mais


O Capítulo 8, "Instruções Compostas," (https://docs.python.org/pt-br/3/reference/compound_stmts.html) em A Referência da
Linguagem Python diz praticamente tudo que há para dizer sobre cláusulas else em instruções if , for , while e
try . Sobre o uso pythônico de try/except , como ou sem else , Raymond Hettinger deu uma resposta brilhante para
a pergunta "Is it a good practice to use try-except-else in Python?" (É uma boa prática usar try-except-else em Python?)
(https://fpy.li/18-31) (EN) no StackOverflow. O Python in a Nutshell (https://fpy.li/pynut3), 3rd ed., by Martelli et al., tem um
capítulo sobre exceções com uma excelente discussão sobre o estilo EAFP, atribuindo à pioneira da computação Grace
Hopper a criação da frase "É mais fácil pedir perdão que pedir permissão."
O capítulo 4 de A Biblioteca Padrão do Python, "Tipos Embutidos", tem uma seção dedicada a "Tipos de Gerenciador de
Contexto" (https://docs.python.org/pt-br/3/library/stdtypes.html#typecontextmanager). Os métodos especiais
__enter__/__exit__ também estão documentados em A Referência da Linguagem Python, em "Gerenciadores de
Contexto da Instrução with" (https://docs.python.org/pt-br/3/reference/datamodel.html#with-statement-context-managers).[250] Os
gerenciadores de contexto foram introduzidos na PEP 343—The "with" Statement (https://fpy.li/pep343) (EN).

Raymond Hettinger apontou a instrução with como um "recurso maravilhoso da linguagem" em sua palestra de
abertura da PyCon US 2013 (https://fpy.li/18-29) (EN). Ele também mostrou alguns usos interessantes de gerenciadores de
contexto em sua apresentação "Transforming Code into Beautiful, Idiomatic Python" ("Transformando Código em Lindo
Python Idiomático") (https://fpy.li/18-35) (EN), na mesma conferência.

O post de Jeff Preshing em seu blog, "The Python 'with' Statement by Example" "A Instrução 'with' do Python através de
Exemplos" (https://fpy.li/18-36)(EN) é interessante pelos exemplos de uso de gerenciadores de contexto com a biblioteca
gráfica pycairo .

A classe contextlib.ExitStack foi baseada em uma ideia original de Nikolaus Rath, que escreveu um post curto
explicando porque ela é útil: "On the Beauty of Python’s ExitStack" "Sobre a Beleza do ExitStack do Python"
(https://fpy.li/18-37). No texto, Rath propõe que ExitStack é similar, mas mais flexível que a instrução defer em Go—
que acho uma das melhores ideias naquela linguagem.

Beazley and Jones desenvolveram gerenciadores de contexto para propósitos muito diferentes em seu livro, Python
Cookbook, (https://fpy.li/pycook3) (EN) 3rd ed. A "Recipe 8.3. Making Objects Support the Context-Management Protocol"
(Receita 8.3. Fazendo Objetos Suportarem o Protocolo Gerenciador de Contexto) implementa uma classe
LazyConnection , cujas instâncias são gerenciadores de contexto que abrem e fecham conexões de rede
automaticamente, em blocos with . A "Recipe 9.22. Defining Context Managers the Easy Way" (Receita 9.22. O Jeito Fácil
de Definir Gerenciadores de Contexto) introduz um gerenciador de contexto para código de cronometragem, e outro
para realizar mudanças transacionais em um objeto list : dentro do bloco with é criada um cópia funcional da
instância de list , e todas as mudanças são aplicadas àquela cópia funcional. Apenas quando o bloco with termina
sem uma exceção a cópia funcional substitui a original. Simples e genial.

Peter Norvig descreve seu pequeno interpretador Scheme nos posts "(How to Write a (Lisp) Interpreter (in Python))" "
(_Como Escrever um Interpretador (Lisp) (em Python))_" (https://fpy.li/18-38) (EN) e "(An ((Even Better) Lisp) Interpreter
(in Python))" "_(Um Interpretador (Lisp (Ainda Melhor)) (em Python))_" (https://fpy.li/18-39) (EN). O código-fonte de lis.py e
lispy.py está no repositório norvig/pytudes (https://fpy.li/18-40). Meu repositório, fluentpython/lispy (https://fpy.li/18-41), inclui
a versão mylis do lis.py, atualizado para o Python 3.10, com um REPL melhor, integraçào com a linha de comando,
exemplos, mais testes e referências para aprender mais sobre Scheme. O melhor ambiente e dialeto de Scheme para
aprender e experimentar é o Racket (https://fpy.li/18-42).

Ponto de vista
Fatorando o pão

Em sua palestra de abertura na PyCon US 2013, "What Makes Python Awesome" ("O que torna o Python incrível")
(https://fpy.li/18-1), Raymond Hettinger diz que quando viu a proposta da instrução with , pensou que era "um
pouquinho misteriosa." Inicialmente tive uma reação similar. As PEPs são muitas vezes difíceis de ler, e a PEP 343
é típica nesse sentido.
Mas aí—​nos contou Hettinger—​ele teve uma ideia: as sub-rotinas são a invenção mais importante na história das
linguagens de computador. Se você tem sequências de operações, como A;B;C e P;B;Q, você pode fatorar B em uma
sub-rotina. É como fatorar o recheio de um sanduíche: usar atum com tipos de diferentes de pão. Mas e se você
quiser fatorar o pão, para fazer sanduíches com pão de trigo integral usando recheios diferentes a cada vez? É
isso que a instrução with oferece. Ela é o complemento da sub-rotina. Hettinger continuou:

“ Aponta
instrução é algo muito importante. Encorajo vocês a irem lá e olharem para a
with
desse iceberg, e daí cavarem mais fundo. Provavelmente é possível fazer coisas muito
profundas com a instrução with . Seus melhores usos ainda estão por ser descobertos.
Espero que, se vocês fizerem bom uso dela, ela será copiada para outras linguagens, e todas
as linguagens futuras vão incluí-la. Vocês podem ser parte da descoberta de algo quase tão
profundo quanto a invenção da própria sub-rotina.

Hettinger admite que está tentando muito vender a instrução with . Mesmo assim, é um recurso bem útil.
Quando ele usou a analogia do sanduíche para explicar como with é o complemento da sub-rotina, muitas
possibilidades se abriram na minha mente.

Se você precisa convencer alguém que o Python é maravilhoso, assista a palestra de abertura de Hettinger. A
parte sobre gerenciadores de contexto fica entre 23:00 to 26:15. Mas a palestra inteira é excelente.

Recursão eficiente com chamadas de cauda apropriadas

As implementações padrão de Scheme são obrigadas a oferecer chamadas de cauda apropriadas (PTC, sigla em
inglês para proper tail calls), para tornar a iteração por recursão uma alternativa prática aos loops while das
linguagens imperativas. Alguns autores se referem às PTC como otimização de chamadas de cauda (TCO, sigla em
inglês para tail call optimization); para outros, TCO é uma coisa diferente. Para mais detalhes, leia "Chamadas
recursivas de cauda
(https://pt.wikipedia.org/wiki/Recursividade_(ci%C3%AAncia_da_computa%C3%A7%C3%A3o)#Fun%C3%A7%C3%B5es_recursivas
_em_cauda)
na Wikipedia em português e "Tail call" (https://fpy.li/18-44) (EN), mais aprofundado, na Wikipedia em inglês, e "Tail
call optimization in ECMAScript 6" (https://fpy.li/18-45) (EN).

Uma chamada de cauda é quando uma função devolve o resultado de uma chamada de função, que pode ou não
ser a ela mesma (a função que está devolvendo o resultado). Os exemplos gcd no Exemplo 10 e no Exemplo 11
fazem chamadas de cauda (recursivas) no lado falso do if .

Por outro lado, essa factorial não faz uma chamada de cauda:

PY
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)

A chamada para factorial na última linha não é uma chamada de cauda, pois o valor de return não é
somente o resultado de uma chamada recursiva: o resultado é multiplicado por n antes de ser devolvido.

Aqui está uma alternativa que usa uma chamada de cauda, e é portanto recursiva de cauda:
PY
def factorial_tc(n, product=1):
if n < 1:
return product
return factorial_tc(n - 1, product * n)

O Python não tem PTC então não há vantagem em escrever funções recursivas de cauda. Neste caso, a primeira
versão é, na minha opinião, mais curta e mais legível. Para usos na vida real, não se esqueça que o Python tem o
math.factorial , escrito em C sem recursão. O ponto é que, mesmo em linguagens que implementam PTC, isso
não beneficia toda função recursiva, apenas aquelas cuidadosamente escritas para fazerem chamadas de cauda.

Se PTC são suportadas pela linguagem, quando o interpretador vê uma chamada de cauda, ele pula para dentro
do corpo da função chamada sem criar um novo stack frame, economizando memória. Há também linguagens
compiladas que implementam PTC, por vezes como uma otimização que pode ser ligada e desligada.

Não existe um consenso universal sobre a definição de TCO ou sobre o valor das PTC em linguagens que não
foram projetadas como linguagens funcionais desde o início, como Python e Javascript. Em linguagens funcionais,
PTC é um recurso esperado, não apenas uma otimização boa de ter à mão. Se a linguagem não tem outro
mecanismo de iteração além da recursão, então PTC é necessário para tornar prático o uso da linguagem. O lis.py
(https://fpy.li/18-46) de Norvig não implementa PTC, mas seu interpretador mais elaborado, o lispy.py
(https://fpy.li/18-16), implementa.

Os argumentos contra chamadas de cauda apropriadas em Python e Javascript

O CPython não implementa PTC, e provavelmente nunca o fará. Guido van Rossum escreveu "Final Words on Tail
Calls" ("Últimas Palavras sobre Chamadas de Cauda") (https://fpy.li/18-48) para explicar o motivo. Resumindo, aqui
está uma passagem fundamental de seu post:

“ Pessoalmente, acho que é um bom recurso para algumas linguagens, mas não acho que se
encaixe no Python: a eliminação dos registros do stack para algumas chamadas mas não
para outras certamente confundiria muitos usuários, que não foram criados na religião das
chamadas de cauda, mas podem ter aprendido sobre a semântica das chamadas restreando
algumas chamadas em um depurador.

Em 2015, PTC foram incluídas no padrão ECMAScript 6 para JavaScript. Em outubro de 2021 o interpretador no
WebKit as implementa (https://fpy.li/18-49) (EN). O WebKit é usado pelo Safari. Os interpretadores JS em todos os
outros navegadores populares não tem PTC, assim como o Node.js, que depende da engine V8 que o Google
mantém para o Chrome. Transpiladores e polyfills (injetores de código) voltados para o JS, como o TypeScript, o
ClojureScript e o Babel, também não suportam PTC, de acordo com essa " Tabela de compatibilidade com
ECMAScript 6" (https://fpy.li/18-50) (EN).

Já vi várias explicações para a rejeição das PTC por parte dos implementadores, mas a mais comum é a mesma
que Guido van Rossum mencionou: PTC tornam a depuração mais difícil para todo mundo, e beneficiam apenas
uma minoria que prefere usar recursão para fazer iteração. Para mais detalhes, veja "What happened to proper
tail calls in JavaScript?" "O que aconteceu com as chamadas de cauda apropriadas em Javascript?" (https://fpy.li/18-51)
de Graham Marlow.

Há casos em que a recursão é a melhor solução, mesmo no Python sem PTC. Em um post anterior
(https://fpy.li/18-52) sobre o assunto, Guido escreveu:


“ […​
] uma implementação típica de Python permite 1000 recursões, o que é bastante para
código não-recursivo e para código que usa recursão para atravessar, por exemplo, um
árvore de parsing típica, mas não o bastante para um loop escrito de forma recursiva sobre
uma lista grande.

Concordo com Guido e com a maioria dos implementadores de Javascript. A falta de PTC é a maior restrição ao
desenvolvimento de programas Python em um estilo funcional—mais que a sintaxe limitada de lambda .

Se você estiver curioso em ver como PTC funciona em um interpretador com menos recursos (e menos código)
que o lispy.py de Norvig, veja o mylis_2 (https://fpy.li/18-53). O truque é iniciar com o loop infinito em evaluate e o
código no case para chamadas de função: essa combinação faz o interpretador pular para dentro do corpo da
próxima Procedure sem chamar evaluate recursivamente durante a chamada de cauda. Esses pequenos
interpretadores demonstram o poder da abstração: apesar do Python não implementar PTC, é possível e não
muito difícil escrever um interpretador, em Python, que implementa PTC. Aprendi a fazer isso lendo o código de
Peter Norvig. Obrigado por compartilhar, professor!

A opinião de Norvig sobre evaluate() com pattern matching

Eu compartilhei o código da versão Python 3.10 de lis.py com Peter Norvig. Ele gostou do exemplo usando pattern
matching, mas sugeriu uma solução diferente: em vez de usar os guardas que escrevi, ele teria exatamente um
case por palavra reservada, e teria testes dentro de cada case , para fornecer mensagens de SyntaxError
mais específicas—por exemplo, quando o corpo estiver vazio. Isso também tornaria o guarda em case
[func_exp, *args] if func_exp not in KEYWORDS: desnecessário, pois todas as palavras reservadas teriam
sido tratadas antes do case para chamadas de função.

Provavelmente seguirei o conselho do professor Norvig quando acrescentar mais funcionalidades ao mylis
(https://fpy.li/18-54). Mas a forma como estruturei evaluate no Exemplo 17 tem algumas vantagens didáticas nesse
livro: o exemplo é paralelo à implementação com if/elif/… (Exemplo 11), as cláusulas case demonstram mais
recursos de pattern matching e o código é mais conciso.
19. Modelos de concorrência em Python
“ Concorrência é lidar com muitas coisas ao mesmo tempo.
Paralelismo é fazer muitas coisas ao mesmo tempo.
Não são a mesma coisa, mas estão relacionados.
Uma é sobre estrutura, outro é sobre execução.
A concorrência fornece uma maneira de estruturar uma solução para resolver um problema que
pode (mas não necessariamente) ser paralelizado.[251]
— Rob Pike
Co-criador da linguagem Go

Este capítulo é sobre como fazer o Python "lidar com muitas coisas ao mesmo tempo." Isso pode envolver programação
concorrente ou paralela—e mesmo os acadêmicos rigorosos com terminologia discordam sobre o uso dessas palavras.
Vou adotar as definições informais de Rob Pike, na epígrafe desse capítulo, mas saiba que encontrei artigos e livros que
dizem ser sobre computação paralela mas são quase que inteiramente sobre concorrência.[252]

O paralelismo é, na perspectiva de Pike, um caso especial de concorrência. Todos sistema paralelo é concorrente, mas
nem todo sistema concorrente é paralelo. No início dos anos 2000, usávamos máquinas GNU Linux de um único núcleo,
que rodavam 100 processos ao mesmo tempo. Um laptop moderno com quatro núcleos de CPU rotineiramente está
executando mais de 200 processos a qualquer momento, sob uso normal, casual. Para executar 200 tarefas em paralelo,
você precisaria de 200 núcleos. Assim, na prática, a maior parte da computação é concorrente e não paralela. O SO
administra centenas de processos, assegurando que cada um tenha a oportunidade de progredir, mesmo se a CPU em si
não possa fazer mais que quatro coisas ao mesmo tempo.

Este capítulo não assume que você tenha qualquer conhecimento prévio de programação concorrente ou paralela.
Após uma breve introdução conceitual, vamos estudar exemplos simples, para apresentar e comparar os principais
pacotes da biblioteca padrão de Python dedicados a programação concorrente: threading , multiprocessing , e
asyncio .

O último terço do capítulo é uma revisão geral de ferramentas, servidores de aplicação e filas de tarefas distribuídas
(distributed task queues) de vários desenvolvedores, capazes de melhorar o desempenho e a escalabilidade de
aplicações Python. Todos esses são tópicos importantes, mas fogem do escopo de um livro focado nos recursos
fundamentais da linguagem Python. Mesmo assim, achei importante mencionar esses temas nessa segunda edição do
Python Fluente, porque a aptidão do Python para computação concorrente e paralela não está limitada ao que a
biblioteca padrão oferece. Por isso YouTube, DropBox, Instagram, Reddit e outros foram capazes de atingir alta
escalabilidade quando começaram, usando Python como sua linguagem primária—apesar das persistentes alegações
de que "O Python não escala."

19.1. Novidades nesse capítulo


Este capítulo é novo, escrito para a segunda edição do Python Fluente. Os exemplos com os caracteres giratórios no
Seção 19.4 antes estavam no capítulo sobre asyncio. Aqui eles foram revisados, e apresentam uma primeira ilustração
das três abordagens do Python à concorrência: threads, processos e corrotinas nativas.

O restante do conteúdo é novo, exceto por alguns parágrafos, que apareciam originalmente nos capítulos sobre
concurrent.futures e asyncio.
A Seção 19.7 é diferente do resto do livro: não há código exemplo. O objetivo ali é apresentar brevemente ferramentas
importantes, que você pode querer estudar para conseguir concorrência e paralelismo de alto desempenho, para além
do que é possível com a biblioteca padrão do Python.

19.2. A visão geral


Há muitos fatores que tornam a programação concorrente difícil, mas quero tocar no mais básico deles: iniciar threads
ou processos é fácil, mas como administrá-los?[253]

Quando você chama uma função, o código que origina a chamada fica bloqueado até que função retorne. Então você
sabe que a função terminou, e pode facilmente acessar o valor devolvido por ela. Se a função lançar uma exceção, o
código de origem pode cercar aquela chamada com um bloco try/except para tratar o erro.

Essas opções não existem quando você inicia threads ou um processo: você não sabe automaticamente quando eles
terminaram, e obter os resultados ou os erros requer criar algum canal de comunicação, tal como uma fila de
mensagens.

Além disso, criar uma thread ou um processo não é barato, você não quer iniciar uma delas apenas para executar uma
única computação e desaparecer. Muitas vezes queremos amortizar o custo de inicialização transformando cada
thread ou processo em um "worker" ou "unidade de trabalho", que entra em um loop e espera por dados para
processar. Isso complica ainda mais a comunicação e introduz mais perguntas. Como terminar um "worker" quando ele
não é mais necessário? E como fazer para encerrá-lo sem interromper uma tarefa inacabada, deixando dados
inconsistentes e recursos não liberados—tal como arquivos abertos? A resposta envolve novamente mensagens e filas.

Uma corrotina é fácil de iniciar. Se você inicia uma corrotina usando a palavra-chave await , é fácil obter o valor de
retorno e há um local óbvio para interceptar exceções. Mas corrotinas muitas vezes são iniciadas pela framework
assíncrona, e isso pode torná-las tão difíceis de monitorar quanto threads ou processos.

Por fim, as corrotinas e threads do Python não são adequadas para tarefas de uso intensivo da CPU, como veremos.

É por isso tudo que programação concorrente exige aprender novos conceitos e novos modelos de programação. Então
vamos primeiro garantir que estamos na mesma página em relação a alguns conceitos centrais.

19.3. Um pouco de jargão


Aqui estão alguns termos que vou usar pelo restante desse capítulo e nos dois seguintes:

Concorrência
A habilidade de lidar com múltiplas tarefas pendentes, fazendo progredir uma por vez ou várias em paralelo (se
possível), de forma que cada uma delas avance até terminar com sucesso ou falha. Uma CPU de núcleo único é capaz
de concorrência se rodar um "agendador" (scheduler) do sistema operacional, que intercale a execução das tarefas
pendentes. Também conhecida como multitarefa (multitasking).

Paralelismo
A habilidade de executar múltiplas operações computacionais ao mesmo tempo. Isso requer uma CPU com múltiplos
núcleos, múltiplas CPUs, uma GPU (https://fpy.li/19-2), ou múltiplos computadores em um cluster (agrupamento)).

Unidades de execução
Termo genérico para objetos que executam código de forma concorrente, cada um com um estado e uma pilha de
chamada independentes. O Python suporta de forma nativa três tipos de unidade de execução: processos, threads, e
corrotinas.

Processo
Uma instância de um programa de computador em execução, usando memória e uma fatia do tempo da CPU.
Sistemas operacionais modernos em nossos computadores e celulares rotineiramente mantém centenas de
processos de forma concorrente, cada um deles isolado em seu próprio espaço de memória privado. Processos se
comunicam via pipes, soquetes ou arquivos mapeados da memória. Todos esses métodos só comportam bytes puros.
Objetos Python precisam ser serializados (convertidos em sequências de bytes) para passarem de um processo a
outro. Isto é caro, e nem todos os objetos Python podem ser serializados. Um processo pode gerar subprocessos,
chamados "processos filhos". Estes também rodam isolados entre si e do processo original. Os processos permitem
multitarefa preemptiva: o agendador do sistema operacional exerce preempção—isto é, suspende cada processo em
execução periodicamente, para permitir que outro processos sejam executados. Isto significa que um processo
paralisado não pode paralisar todo o sistema—em teoria.

Thread
Uma unidade de execução dentro de um único processo. Quando um processo se inicia, ele tem uma única thread: a
thread principal. Um processo pode chamar APIs do sistema operacional para criar mais threads para operar de
forma concorrente. Threads dentro de um processo compartilham o mesmo espaço de memória, onde são mantidos
objetos Python "vivos" (não serializados). Isso facilita o compartilhamento de informações entre threads, mas pode
também levar a corrupção de dados, se mais de uma thread atualizar concorrentemente o mesmo objeto. Como os
processos, as threads também possibilitam a multitarefa preemptiva sob a supervisão do agendador do SO. Uma
thread consome menos recursos que um processo para realizar a mesma tarefa.

Corrotina
Uma função que pode suspender sua própria execução e continuar depois. Em Python, corrotinas clássicas são
criadas a partir de funções geradoras, e corrotinas nativas são definidas com async def . A Seção 17.13 introduziu o
conceito, e Capítulo 21 trata do uso de corrotinas nativas. As corrotinas do Python normalmente rodam dentro de
uma única thread, sob a supervisão de um loop de eventos, também na mesma thread. Frameworks de programação
assíncrona como a asyncio, a Curio, ou a Trio fornecem um loop de eventos e bibliotecas de apoio para E/S não-
bloqueante baseado em corrotinas. Corrotinas permitem multitarefa cooperativa: cada corrotina deve ceder
explicitamente o controle com as palavras-chave yield ou await , para que outra possa continuar de forma
concorrente (mas não em paralelo). Isso significa que qualquer código bloqueante em uma corrotina bloqueia a
execução do loop de eventos e de todas as outras corrotinas—ao contrário da multitarefa preemptiva suportada por
processos e threads. Por outro lado, cada corrotina consome menos recursos para executar o mesmo trabalho de
uma thread ou processo.

Fila (queue)
Uma estrutura de dados que nos permite adicionar e retirar itens, normalmente na ordem FIFO: o primeiro que
entra é o primeiro que sai.[254] Filas permitem que unidades de execução separadas troquem dados da aplicação e
mensagens de controle, tais como códigos de erro e sinais de término. A implementação de uma fila varia de acordo
com o modelo de concorrência subjacente: o pacote queue na biblioteca padrão do Python fornece classes de fila
para suportar threads, já os pacotes multiprocessing e asyncio implementam suas próprias classes de fila. Os
pacotes queue e asyncio também incluem filas não FIFO: LifoQueue e PriorityQueue .

Trava (lock)
Um objeto que as unidades de execução podem usar para sincronizar suas ações e evitar corrupção de dados. Ao
atualizar uma estrutura de dados compartilhada, o código em execução deve manter uma trava associada a tal
estrutura. Isso sinaliza a outras partes do programa que elas devem aguardar até que a trava seja liberada, antes de
acessar a mesma estrutura de dados. O tipo mais simples de trava é conhecida também como mutex (de mutual
exclusion, exclusão mútua). A implementação de uma trava depende do modelo de concorrência subjacente.

Contenda (contention)
Disputa por um recurso limitado. Contenda por recursos ocorre quando múltiplas unidades de execução tentam
acessar um recurso compartilhado — tal como uma trava ou o armazenamento. Há também contenda pela CPU,
quando processos ou threads de computação intensiva precisam aguardar até que o agendador do SO dê a eles uma
quota do tempo da CPU.
Agora vamos usar um pouco desse jargão para entender o suporte à concorrência no Python.

19.3.1. Processos, threads, e a infame GIL do Python


Veja como os conceitos que acabamos de tratar se aplicam ao Python, em dez pontos:

1. Cada instância do interpretador Python é um processo. Você pode iniciar processos Python adicionais usando as
bibliotecas multiprocessing ou concurrent.futures. A biblioteca subprocess do Python foi projetada para rodar
programas externos, independente das linguagens usadas para escrever tais programas.
2. O interpretador Python usa uma única thread para rodar o programa do usuário e o coletor de lixo da memória.
Você pode iniciar threads Python adicionais usando as bibliotecas threading ou concurrent.futures.
3. O acesso à contagem de referências a objetos e outros estados internos do interpretador é controlado por uma
trava, a Global Interpreter Lock (GIL) ou Trava Global do Interpretador. A qualquer dado momento, apenas uma
thread do Python pode reter a trava. Isso significa que apenas uma thread pode executar código Python a cada
momento, independente do número de núcleos da CPU.
4. Para evitar que uma thread do Python segure a GIL indefinidamente, o interpretador de bytecode do Python pausa
a thread Python corrente a cada 5ms por default,[255] liberando a GIL. A thread pode então tentar readquirir a GIL,
mas se existirem outras threads esperando, o agendador do SO pode escolher uma delas para continuar.
5. Quando escrevemos código Python, não temos controle sobre a GIL. Mas uma função embutida ou uma extensão
escrita em C—ou qualquer linguagem que trabalhe no nível da API Python/C—pode liberar a GIL enquanto estiver
rodando alguma tarefa longa.
6. Toda função na biblioteca padrão do Python que executa uma syscall[256] libera a GIL. Isso inclui todas as funções
que executam operações de escrita e leitura em disco, escrita e leitura na rede, e time.sleep() . Muitas funções de
uso intensivo da CPU nas bibliotecas NumPy/SciPy, bem como as funções de compressão e descompressão dos
módulos zlib and bz2 , também liberam a GIL.[257]
7. Extensões que se integram no nível da API Python/C também podem iniciar outras threads não-Python, que não são
afetadas pela GIL. Essas threads fora do controle da GIL normalmente não podem modificar objetos Python, mas
podem ler e escrever na memória usada por objetos que suportam o buffer protocol (https://fpy.li/pep3118) (EN), como
bytearray , array.array , e arrays do NumPy.

8. O efeito da GIL sobre a programação de redes com threads Python é relativamente pequeno, porque as funções de
E/S liberam a GIL, e ler e escrever na rede sempre implica em alta latência—comparado a ler e escrever na
memória. Consequentemente, cada thread individual já passa muito tempo esperando mesmo, então sua execução
pode ser intercalada sem maiores impactos no desempenho geral. Por isso David Beazley diz: "As threads do Python
são ótimas em fazer nada."[258]
9. As contendas pela GIL desaceleram as threads Python de processamento intensivo. Código sequencial de uma única
thread é mais simples e mais rápido para esse tipo de tarefa.
10. Para rodar código Python de uso intensivo da CPU em múltiplos núcleos, você tem que usar múltiplos processos
Python.

Aqui está um bom resumo, da documentação do módulo threading :[259]


“ apenas uma thread pode executar código :Python
Detalhe de implementação do CPython Em CPython, devido à Trava Global do Interpretador,
de cada vez (mas certas bibliotecas orientadas
ao desempenho podem superar essa limitação). Se você quer que sua aplicação faça melhor uso
dos recursos computacionais de máquinas com CPUs de múltiplos núcleos, aconselha-se usar
multiprocessing ou concurrent.futures.ProcessPoolExecutor .

Entretanto, threads ainda são o modelo adequado se você deseja rodar múltiplas tarefas ligadas
a E/S simultaneamente.

O parágrafo anterior começa com "Detalhe de implementação do CPython" porque a GIL não é parte da definição da
linguagem Python. As implementações Jython e o IronPython não tem uma GIL. Infelizmente, ambas estão ficando
para trás, ainda compatíveis apenas com Python 2.7 e 3.4, respectivamente. O interpretador de alto desempenho PyPy
(https://fpy.li/19-9) também tem uma GIL em suas versões 2.7, 3.8 e 3.9 (a mais recente em março de 2021).

Essa seção não mencionou corrotinas, pois por default elas compartilham a mesma thread Python
entre si e com o loop de eventos supervisor fornecido por uma framework assíncrona. Assim, a GIL
✒️ NOTA não as afeta. É possível usar múltiplas threads em um programa assíncrono, mas a melhor prática é
ter uma thread rodando o loop de eventos e todas as corrotinas, enquanto as threads adicionais
executam tarefas específicas. Isso será explicado na Seção 21.8.

Mas chega de conceitos por agora. Vamos ver algum código.

19.4. Um "Olá mundo" concorrente


Durante uma discussão sobre threads e sobre como evitar a GIL, o contribuidor do Python Michele Simionato postou
um exemplo (https://fpy.li/19-10) que é praticamente um "Olá Mundo" concorrente: o programa mais simples possível
mostrando como o Python pode "mascar chiclete e subir a escada ao mesmo tempo".

O programa de Simionato usa multiprocessing , mas eu o adaptei para apresentar também threading e asyncio .
Vamos começar com a versão threading , que pode parecer familiar se você já estudou threads em Java ou C.

19.4.1. Caracteres animados com threads


A ideia dos próximos exemplos é simples: iniciar uma função que pausa por 3 segundos enquanto anima caracteres no
terminal, para deixar o usuário saber que o programa está "pensando" e não congelado.

O script cria uma animação giratória e mostra em sequência cada caractere da string "\|/-" na mesma posição da
tela.[260] Quando a computação lenta termina, a linha com a animação é apagada e o resultado é apresentado:
Answer: 42 .

Figura 1 mostra a saída de duas versões do exemplo: primeiro com threads, depois com corrotinas. Se você estiver
longe do computador, imagine que o \ na última linha está girando.

Figura 1. Os scripts spinner_thread.py e spinner_async.py produzem um resultado similar: o repr do objeto spinner e o
texto "Answer: 42". Na captura de tela, spinner_async.py ainda está rodando, e a mensagem animada "/ thinking!" é
apresentada; aquela linha será substituída por "Answer: 42" após 3 segundos.

Vamos revisar o script spinner_thread.py primeiro. O Exemplo 1 lista as duas primeiras funções no script, e o Exemplo 2
mostra o restante.

Exemplo 1. spinner_thread.py: as funções spin e slow

PY
import itertools
import time
from threading import Thread, Event

def spin(msg: str, done: Event) -> None: # (1)


for char in itertools.cycle(r'\|/-'): # (2)
status = f'\r{char} {msg}' # (3)
print(status, end='', flush=True)
if done.wait(.1): # (4)
break # (5)
blanks = ' ' * len(status)
print(f'\r{blanks}\r', end='') # (6)

def slow() -> int:


time.sleep(3) # (7)
return 42

1. Essa função vai rodar em uma thread separada. O argumento done é uma instância de threading.Event , um
objeto simples para sincronizar threads.
2. Isso é um loop infinito, porque itertools.cycle produz um caractere por vez, circulando pela string para
sempre.
3. O truque para animação em modo texto: mova o cursor de volta para o início da linha com o caractere de controle
ASCII de retorno ( '\r' ).
4. O método Event.wait(timeout=None) retorna True quando o evento é acionado por outra thread; se o timeout
passou, ele retorna False . O tempo de 0,1s estabelece a "velocidade" da animação para 10 FPS. Se você quiser que
uma animação mais rápida, use um tempo menor aqui.
5. Sai do loop infinito.
6. Sobrescreve a linha de status com espaços para limpá-la e move o cursor de volta para o início.
7. slow() será chamada pela thread principal. Imagine que isso é uma chamada de API lenta, através da rede.
Chamar sleep bloqueia a thread principal, mas a GIL é liberada e a thread da animação pode continuar.

👉 DICA O primeiro detalhe importante deste exemplo é que time.sleep() bloqueia a thread que a chama,
mas libera a GIL, permitindo que outras threads Python rodem.

As funções spin e slow serão executadas de forma concorrente. A thread principal—a única thread quando o
programa é iniciado—vai iniciar uma nova thread para rodar spin e então chamará slow . Propositalmente, não há
qualquer API para terminar uma thread em Python. É preciso enviar uma mensagem para encerrar uma thread.

A classe threading.Event é o mecanismo de sinalização mais simples do Python para coordenar threads. Uma
instância de Event tem uma flag booleana interna que começa como False . Uma chamada a Event.set() muda a
flag para True . Enquanto a flag for falsa, se uma thread chamar Event.wait() , ela será bloqueada até que outra
thread chame Event.set() , quando então Event.wait() retorna True . Se um tempo de espera (timeout) em
segundos é passado para Event.wait(s) , essa chamada retorna False quando aquele tempo tiver passado, ou
retorna True assim que Event.set() é chamado por outra thread.
A função supervisor , que aparece no Exemplo 2, usa um Event para sinalizar para a função spin que ela deve
encerrar.

Exemplo 2. spinner_thread.py: as funções supervisor e main

PY
def supervisor() -> int: # (1)
done = Event() # (2)
spinner = Thread(target=spin, args=('thinking!', done)) # (3)
print(f'spinner object: {spinner}') # (4)
spinner.start() # (5)
result = slow() # (6)
done.set() # (7)
spinner.join() # (8)
return result

def main() -> None:


result = supervisor() # (9)
print(f'Answer: {result}')

if __name__ == '__main__':
main()

1. supervisor irá retornar o resultado de slow .

2. A instância de threading.Event é a chave para coordenar as atividades das threads main e spinner , como
explicado abaixo.
3. Para criar uma nova Thread , forneça uma função como argumento palavra-chave target , e argumentos
posicionais para a target como uma tupla passada via args .
4. Mostra o objeto spinner . A saída é <Thread(Thread-1, initial)> , onde initial é o estado da thread—
significando aqui que ela ainda não foi iniciada.
5. Inicia a thread spinner .

6. Chama slow , que bloqueia a thread principal. Enquanto isso, a thread secundária está rodando a animação.

7. Muda a flag de Event para True ; isso vai encerrar o loop for dentro da função spin .

8. Espera até que a thread spinner termine.


9. Roda a função supervisor . Escrevi main e supervisor como funções separadas para deixar esse exemplo mais
parecido com a versão asyncio no Exemplo 4.

Quando a thread main aciona o evento done , a thread spinner acabará notando e encerrando corretamente.

Agora vamos ver um exemplo similar usando o pacote multiprocessing .

19.4.2. Animação com processos


O pacote multiprocessing permite executar tarefas concorrentes em processos Python separados em vez de threads.
Quando você cria uma instância de multiprocessing.Process , todo um novo interpretador Python é iniciado como
um processo filho, em segundo plano. Como cada processo Python tem sua própria GIL, isto permite que seu programa
use todos os núcleos de CPU disponíveis—mas isso depende, em última instância, do agendador do sistema operacional.
Veremos os efeitos práticos em Seção 19.6, mas para esse programa simples não faz grande diferença.

O objetivo dessa seção é apresentar o multiprocessing e mostrar como sua API emula a API de threading ,
tornando fácil converter programas simples de threads para processos, como mostra o spinner_proc.py (Exemplo 3).

Exemplo 3. spinner_proc.py: apenas as partes modificadas são mostradas; todo o resto é idêntico a spinner_thread.py
PY
import itertools
import time
from multiprocessing import Process, Event # (1)
from multiprocessing import synchronize # (2)

def spin(msg: str, done: synchronize.Event) -> None: # (3)

# [snip] the rest of spin and slow functions are unchanged from spinner_thread.py

def supervisor() -> int:


done = Event()
spinner = Process(target=spin, # (4)
args=('thinking!', done))
print(f'spinner object: {spinner}') # (5)
spinner.start()
result = slow()
done.set()
spinner.join()
return result

# [snip] main function is unchanged as well

1. A API básica de multiprocessing imita a API de threading , mas as dicas de tipo e o Mypy mostram essa
diferença: multiprocessing.Event é uma função (e não uma classe como threading.Event ) que retorna uma
instância de synchronize.Event …​
2. …​nos obrigando a importar multiprocessing.synchronize …​

3. …​para escrever essa dica de tipo.


4. O uso básico da classe Process é similar ao da classe Thread .

5. O objeto spinner aparece como <Process name='Process-1' parent=14868 initial>`, onde 14868 é o ID do processo
da instância de Python que está executando o spinner_proc.py.

As APIs básicas de threading e multiprocessing são similares, mas sua implementação é muito diferente, e
multiprocessing tem uma API muito maior, para dar conta da complexidade adicional da programação
multiprocessos. Por exemplo, um dos desafios ao converter um programa de threads para processos é a comunicação
entre processos, que estão isolados pelo sistema operacional e não podem compartilhar objetos Python. Isso significa
que objetos cruzando fronteiras entre processos tem que ser serializados e deserializados, criando custos adicionais.
No Exemplo 3, o único dado que cruza a fronteira entre os processos é o estado de Event , que é implementado com
um semáforo de baixo nível do SO, no código em C sob o módulo multiprocessing .[261]

Desde o Python 3.8, há o pacote multiprocessing.shared_memory


(https://docs.python.org/pt-br/3/library/multiprocessing.shared_memory.html) (memória compartilhada para
acesso direto entre processos) na biblioteca padrão, mas ele não suporta instâncias de classes
definidas pelo usuário. Além bytes nus, o pacote permite que processos compartilhem uma
👉 DICA ShareableList , uma sequência mutável que pode manter um número fixo de itens dos tipos int ,
float , bool , e None , bem como str e bytes , até o limite de 10 MB por item. Veja a
documentação de ShareableList
(https://docs.python.org/pt-
br/3/library/multiprocessing.shared_memory.html#multiprocessing.shared_memory.ShareableList)
para mais detalhes.

Agora vamos ver como o mesmo comportamento pode ser obtido com corrotinas em vez de threads ou processos.

19.4.3. Animação com corrotinas


O Capítulo 21 é inteiramente dedicado à programação assíncrona com corrotinas. Essa seção é
✒️ NOTA apenas um introdução rápida, para contrastar essa abordagem com as threads e os processos. Assim,
vamos ignorar muitos detalhes.

Alocar tempo da CPU para a execução de threads e processos é trabalho dos agendadores do SO. As corrotinas, por
outro lado, são controladas por um loop de evento no nível da aplicação, que gerencia uma fila de corrotinas
pendentes, as executa uma por vez, monitora eventos disparados por operações de E/S iniciadas pelas corrotinas, e
passa o controle de volta para a corrotina correspondente quando cada evento acontece. O loop de eventos e as
corrotinas da biblioteca e as corrotinas do usuário todas rodam em uma única thread. Assim, o tempo gasto em uma
corrotina desacelera loop de eventos—e de todas as outras corrotinas.

A versão com corrotinas do programa de animação é mais fácil de entender se começarmos por uma função main , e
depois olharmos a supervisor . É isso que o Exemplo 4 mostra.

Exemplo 4. spinner_async.py: a função main e a corrotina supervisor

PY
def main() -> None: # (1)
result = asyncio.run(supervisor()) # (2)
print(f'Answer: {result}')

async def supervisor() -> int: # (3)


spinner = asyncio.create_task(spin('thinking!')) # (4)
print(f'spinner object: {spinner}') # (5)
result = await slow() # (6)
spinner.cancel() # (7)
return result

if __name__ == '__main__':
main()

1. main é a única função regular definida nesse programa—as outras são corrotinas.
2. A função`asyncio.run` inicia o loop de eventos para controlar a corrotina que irá em algum momento colocar as
outras corrotinas em movimento. A função main ficará bloqueada até que supervisor retorne. O valor de
retorno de supervisor será o valor de retorno de asyncio.run .
3. Corrotinas nativas são definidas com async def .

4. asyncio.create_task agenda a execução futura de spin , retornando imediatamente uma instância de


asyncio.Task .

5. O repr do objeto se parece com


spinner <Task pending name='Task-2' coro=<spin() running at
/path/to/spinner_async.py:11>> .

6. A palavra-chave await chama slow , bloqueando supervisor até que slow retorne. O valor de retorno de slow
será atribuído a result .
7. O método Task.cancel lança uma exceção CancelledError dentro da corrotina, como veremos no Exemplo 5.

O Exemplo 4 demonstra as três principais formas de rodar uma corrotina:

asyncio.run(coro())

É chamado a partir de uma função regular, para controlar o objeto corrotina, que é normalmente o ponto de entrada
para todo o código assíncrono no programa, como a supervisor nesse exemplo. Esta chamada bloqueia a função
até que coro retorne. O valor de retorno da chamada a run() é o que quer que coro retorne.
asyncio.create_task(coro())
É chamado de uma corrotina para agendar a execução futura de outra corrotina. Essa chamada não suspende a
corrotina atual. Ela retorna uma instância de Task , um objeto que contém o objeto corrotina e fornece métodos
para controlar e consultar seu estado.

await coro()
É chamado de uma corrotina para transferir o controle para o objeto corrotina retornado por coro() . Isso suspende
a corrotina atual até que coro retorne. O valor da expressão await será é o que quer que coro retorne.

✒️ NOTA Lembre-se: invocar uma corrotina como coro() retorna imediatamente um objeto corrotina, mas
não executa o corpo da função coro . Acionar o corpo de corrotinas é a função do loop de eventos.

Vamos estudar agora as corrotinas spin e slow no Exemplo 5.

Exemplo 5. spinner_async.py: as corrotinas spin e slow

PY
import asyncio
import itertools

async def spin(msg: str) -> None: # (1)


for char in itertools.cycle(r'\|/-'):
status = f'\r{char} {msg}'
print(status, flush=True, end='')
try:
await asyncio.sleep(.1) # (2)
except asyncio.CancelledError: # (3)
break
blanks = ' ' * len(status)
print(f'\r{blanks}\r', end='')

async def slow() -> int:


await asyncio.sleep(3) # (4)
return 42

1. Não precisamos do argumento Event , que era usado para sinalizar que slow havia terminado de rodar no
spinner_thread.py (Exemplo 1).
2. Use await asyncio.sleep(.1) em vez de time.sleep(.1) , para pausar sem bloquear outras corrotinas. Veja o
experimento após o exemplo.
3. asyncio.CancelledError é lançada quando o método cancel é chamado na Task que controla essa corrotina.
É hora de sair do loop.
4. A corrotina slow também usa await asyncio.sleep em vez de time.sleep .

Experimento: Estragando a animação para sublinhar um ponto


Aqui está um experimento que recomendo para entender como spinner_async.py funciona. Importe o módulo time ,
daí vá até a corrotina slow e substitua a linha await asyncio.sleep(3) por uma chamada a time.sleep(3) , como
no Exemplo 6.

Exemplo 6. spinner_async.py: substituindo await asyncio.sleep(3) por time.sleep(3)


PY
async def slow() -> int:
time.sleep(3)
return 42

Assistir o comportamento é mais memorável que ler sobre ele. Vai lá, eu espero.

Quando você roda o experimento, você vê isso:

1. O objeto spinner aparece: <Task pending name='Task-2' coro=<spin() running at


…/spinner_async.py:12>> .

2. A animação nunca aparece. O programa trava por 3 segundos.


3. Answer: 42 aparece e o programa termina.

Para entender o que está acontecendo, lembre-se que o código Python que está usando asyncio tem apenas um fluxo
de execução, a menos que você inicie explicitamente threads ou processos adicionais. Isso significa que apenas uma
corrotina é executada a qualquer dado momento. A concorrência é obtida controlando a passagem de uma corrotina a
outra. No Exemplo 7, vamos nos concentrar no que ocorre nas corrotinas supervisor e slow durante o experimento
proposto.

Exemplo 7. spinner_async_experiment.py: as corrotinas supervisor e slow

PY
async def slow() -> int:
time.sleep(3) # (4)
return 42

async def supervisor() -> int:


spinner = asyncio.create_task(spin('thinking!')) # (1)
print(f'spinner object: {spinner}') # (2)
result = await slow() # (3)
spinner.cancel() # (5)
return result

1. A tarefa spinner é criada para, no futuro, controlar a execução de spin .

2. O display mostra que Task está "pending"(em espera).


3. A expressão await transfere o controle para a corrotina slow .

4. time.sleep(3) bloqueia tudo por 3 segundos; nada pode acontecer no programa, porque a thread principal está
bloqueada—e ela é a única thread. O sistema operacional vai seguir com outras atividades. Após 3 segundos,
sleep desbloqueia, e slow retorna.

5. Logo após slow retornar, a tarefa spinner é cancelada. O fluxo de controle jamais chegou ao corpo da corrotina
spin .

O spinner_async_experiment.py ensina uma lição importante, como explicado no box abaixo.

Nunca use time.sleep(…) em corrotinas asyncio , a menos que você queira pausar o programa

⚠️ AVISO inteiro. Se uma corrotina precisa passar algum tempo sem fazer nada, ela deve await
asyncio.sleep(DELAY) . Isso devolve o controle para o loop de eventos de asyncio , que pode
acionar outras corrotinas em espera.
Greenlet e gevent
Ao discutir concorrência com corrotinas, é importante mencionar o pacote greenlet (https://fpy.li/19-14), que já existe
há muitos anos e é muito usado.[262] O pacote suporta multitarefa cooperativa através de corrotinas leves—
chamadas greenlets—que não exigem qualquer sintaxe especial tal como yield ou await , e assim são mais
fáceis de integrar a bases de código sequencial existentes. O SQL Alchemy 1.4 ORM (https://fpy.li/19-15) usa greenlets
internamente para implementar sua nova API assíncrona (https://fpy.li/19-16) compatível com asyncio.

A biblioteca de programação de redes gevent (https://fpy.li/19-17) modifica, através de monkey patches, o módulo
socket padrão do Python, tornando-o não-bloqueante, substituindo parte do código daquele módulo por
greenlets. Na maior parte dos casos, gevent é transparente para o código em seu entorno, tornando mais fácil
adaptar aplicações e bibliotecas sequenciais—tal como drivers de bancos de dados—para executar E/S de rede de
forma concorrente. Inúmeros projetos open source (https://fpy.li/19-18) usam gevent, incluindo o muito usado
Gunicorn (https://fpy.li/gunicorn)—mencionado em Seção 19.7.4.

19.4.4. Supervisores lado a lado


O número de linhas de spinner_thread.py e spinner_async.py é quase o mesmo. As funções supervisor são o núcleo
desses exemplos. Vamos compará-las mais detalhadamente. O Exemplo 8 mostra apenas a supervisor do Exemplo 2.

Exemplo 8. spinner_thread.py: a função supervisor com threads

PYTHON3
def supervisor() -> int:
done = Event()
spinner = Thread(target=spin,
args=('thinking!', done))
print('spinner object:', spinner)
spinner.start()
result = slow()
done.set()
spinner.join()
return result

Para comparar, o Exemplo 9 mostra a corrotina supervisor do Exemplo 4.

Exemplo 9. spinner_async.py: a corrotina assíncrona supervisor

PYTHON3
async def supervisor() -> int:
spinner = asyncio.create_task(spin('thinking!'))
print('spinner object:', spinner)
result = await slow()
spinner.cancel()
return result

Aqui está um resumo das diferenças e semelhanças notáveis entre as duas implementações de supervisor :

Uma asyncio.Task é aproximadamente equivalente a threading.Thread .

Uma Task aciona um objeto corrotina, e uma Thread invoca um callable.


Uma corrotina passa o controle explicitamente com a palavra-chave await

Você não instancia objetos Task diretamente, eles são obtidos passando uma corrotina para
asyncio.create_task(…) .
Quando asyncio.create_task(…) retorna um objeto Task , ele já esta agendado para rodar, mas uma instância
de Thread precisa ser iniciada explicitamente através de uma chamada a seu método start .
Na supervisor da versão com threads, slow é uma função comum e é invocada diretamente pela thread
principal. Na versão assíncrona da supervisor , slow é uma corrotina guiada por await .
Não há API para terminar uma thread externamente; em vez disso, é preciso enviar um sinal—como acionar o
done no objeto Event . Para objetos Task , há o método de instância Task.cancel() , que dispara um
CancelledError na expressão await na qual o corpo da corrotina está suspensa naquele momento.

A corrotina supervisor deve ser iniciada com asyncio.run na função main .


Essa comparação ajuda a entender como a concorrência é orquestrada com asyncio, em contraste com como isso é feito
com o módulo Threading , possivelmente mais familiar ao leitor.

Um último ponto relativo a threads versus corrotinas: quem já escreveu qualquer programa não-trivial com threads
sabe quão desafiador é estruturar o programa, porque o agendador pode interromper uma thread a qualquer
momento. É preciso lembrar de manter travas para proteger seções críticas do programa, para evitar ser interrompido
no meio de uma operação de muitas etapas—algo que poderia deixar dados em um estado inválido.

Com corrotinas, seu código está protegido de interrupções arbitrárias. É preciso chamar await explicitamente para
deixar o resto do programa rodar. Em vez de manter travas para sincronizar as operações de múltiplas threads,
corrotinas são "sincronizadas" por definição: apenas uma delas está rodando em qualquer momento. Para entregar o
controle, você usa await para passar o controle de volta ao agendador. Por isso é possível cancelar uma corrotina de
forma segura: por definição, uma corrotina só pode ser cancelada quando está suspensa em uma expressão await ,
então é possível realizar qualquer limpeza necessária capturando a exceção CancelledError .

A chamada time.sleep() bloqueia mas não faz nada. Vamos agora experimentar com uma chamada de uso intensivo
da CPU, para entender melhor a GIL, bem como o efeito de funções de processamento intensivo sobre código
assíncrono.

19.5. O real impacto da GIL


Na versão com threads(Exemplo 1), você pode substituir a chamada time.sleep(3) na função slow por um
requisição de cliente HTTP de sua biblioteca favorita, e a animação continuará girando. Isso acontece porque uma
biblioteca de programação para rede bem desenhada liberará a GIL enquanto estiver esperando uma resposta.

Você também pode substituir a expressão asyncio.sleep(3) na corrotina slow para que await espere pela
resposta de uma biblioteca bem desenhada de acesso assíncrono à rede, pois tais bibliotecas fornecem corrotinas que
devolvem o controle para o loop de eventos enquanto esperam por uma resposta da rede. Enquanto isso, a animação
seguirá girando.

Com código de uso intensivo da CPU, a história é outra. Considere a função is_prime no Exemplo 10, que retorna
True se o argumento for um número primo, False se não for.

Exemplo 10. primes.py: uma verificação de números primos fácil de entender, do exemplo em ProcessPool​Executor
na documentação do Python (https://docs.python.org/pt-br/3/library/concurrent.futures.html#processpoolexecutor-example)
PY
def is_prime(n: int) -> bool:
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False

root = math.isqrt(n)
for i in range(3, root + 1, 2):
if n % i == 0:
return False
return True

A chamada is_prime(5_000_111_000_222_021) leva cerca de 3.3s no laptop da empresa que estou usando agora.
[263]

19.5.1. Teste Rápido


Dado o que vimos até aqui, pare um instante para pensar sobre a seguinte questão, de três partes. Uma das partes da
resposta é um pouco mais complicada (pelo menos para mim foi).

“ O quê aconteceria à animação


5_000_111_000_222_021
se fossem feitas as seguintes modificações, presumindo que
—aquele mesmo número primo que minha máquina levou 3,3s para
n =

checar:

1. Em spinner_proc.py, substitua time.sleep(3) com uma chamada a is_prime(n) ?

2. Em spinner_thread.py, substitua time.sleep(3) com uma chamada a is_prime(n) ?

3. Em spinner_async.py, substitua await asyncio.sleep(3) com uma chamada a


is_prime(n) ?

Antes de executar o código ou continuar lendo, recomendo chegar as respostas por você mesmo. Depois, copie e
modifique os exemplos spinner_*.py como sugerido.

Agora as respostas, da mais fácil para a mais difícil.

1. Resposta para multiprocessamento


A animação é controlada por um processo filho, então continua girando enquanto o teste de números primos é
computado no processo raiz.[264]

2. Resposta para versão com threads


A animação é controlada por uma thread secundária, então continua girando enquanto o teste de número primo é
computado na thread principal.

Não acertei essa resposta inicialmente: Esperava que a animação congelasse, porque superestimei o impacto da GIL.

Nesse exemplo em particular, a animação segue girando porque o Python suspende a thread em execução a cada 5ms
(por default), tornando a GIL disponível para outras threads pendentes. Assim, a thread principal executando
is_prime é interrompida a cada 5ms, permitindo à thread secundária acordar e executar uma vez o loop for , até
chamar o método wait do evento done , quando então ela liberará a GIL. A thread principal então pegará a GIL, e o
cálculo de is_prime continuará por mais 5 ms.
Isso não tem um impacto visível no tempo de execução deste exemplo específico, porque a função spin rapidamente
realiza uma iteração e libera a GIL, enquanto espera pelo evento done , então não há muita disputa pela GIL. A thread
principal executando is_prime terá a GIL na maior parte do tempo.

Conseguimos nos safar usando threads para uma tarefa de processamento intensivo nesse experimento simples
porque só temos duas threads: uma ocupando a CPU, e a outra acordando apenas 10 vezes por segundo para atualizar
a animação.

Mas se você tiver duas ou mais threads disputando por mais tempo da CPU, seu programa será mais lento que um
programa sequencial.

3. Resposta para asyncio


Se você chamar is_prime(5_000_111_000_222_021) na corrotina slow do exemplo spinner_async.py, a animação
nunca vai aparecer. O efeito seria o mesmo que vimos no Exemplo 6, quando substituímos await asyncio.sleep(3)
por time.sleep(3) : nenhuma animação. O fluxo de controle vai passar da supervisor para slow , e então para
is_prime . Quando is_prime retornar, slow vai retornar também, e supervisor retomará a execução, cancelando
a tarefa spinner antes dela ser executada sequer uma vez. O programa parecerá congelado por aproximadamente 3s,
e então mostrará a resposta.

Soneca profunda com sleep(0)


Uma maneira de manter a animação funcionando é reescrever is_prime como uma corrotina, e periodicamente
chamar asyncio.sleep(0) em uma expressão await , para passar o controle de volta para o loop de eventos,
como no Exemplo 11.

Exemplo 11. spinner_async_nap.py: is_prime agora é uma corrotina

PY
async def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False

root = math.isqrt(n)
for i in range(3, root + 1, 2):
if n % i == 0:
return False
if i % 100_000 == 1:
await asyncio.sleep(0) # (1)
return True

1. Vai dormir a cada 50.000 iterações (porque o argumento step em range é 2).

O Issue #284 (https://fpy.li/19-20) (EN) no repositório do asyncio tem uma discussão informativa sobre o uso de
asyncio.sleep(0) .

Entretanto, observe que isso vai tornar is_prime mais lento, e—mais importante—vai também tornar o loop de
eventos e o seu programa inteiro mais lentos. Quando eu usei await asyncio.sleep(0) a cada 100.000
iterações, a animação foi suave mas o programa rodou por 4,9s na minha máquina, quase 50% a mais que a
função primes.is_prime rodando sozinha com o mesmo argumento ( 5_000_111_000_222_021 ).
Usar await asyncio.sleep(0) deve ser considerada uma medida paliativa até o código assíncrono ser
refatorado para delegar computações de uso intensivo da CPU para outro processo. Veremos uma forma de fazer
isso com o asyncio.loop.run_in_executor (https://fpy.li/19-21), abordado no Capítulo 21. Outra opção seria uma
fila de tarefas, que vamos discutir brevemente na Seção 19.7.5.

Até aqui experimentamos com uma única chamada para uma função de uso intensivo de CPU. A próxima seção
apresenta a execução concorrente de múltiplas chamadas de uso intensivo da CPU.

19.6. Um pool de processos caseiro


Escrevi essa seção para mostrar o uso de múltiplos processos em cenários de uso intensivo de CPU, e

✒️ NOTA o padrão comum de usar filas para distribuir tarefas e coletar resultados. O Capítulo 20 apresenta
uma forma mais simples de distribuir tarefas para processos: um ProcessPoolExecutor do pacote
concurrent.futures , que internamente usa filas.

Nessa seção vamos escrever programas para verificar se os números dentro de uma amostra de 20 inteiros são primos.
Os números variam de 2 até 9.999.999.999.999.999—isto é, 1016 – 1, ou mais de 253. A amostra inclui números primos
pequenos e grandes, bem como números compostos com fatores primos grandes e pequenos.

O programa sequential.py fornece a linha base de desempenho. Aqui está o resultado de uma execução de teste:

$ python3 sequential.py
2 P 0.000001s
142702110479723 P 0.568328s
299593572317531 P 0.796773s
3333333333333301 P 2.648625s
3333333333333333 0.000007s
3333335652092209 2.672323s
4444444444444423 P 3.052667s
4444444444444444 0.000001s
4444444488888889 3.061083s
5555553133149889 3.451833s
5555555555555503 P 3.556867s
5555555555555555 0.000007s
6666666666666666 0.000001s
6666666666666719 P 3.781064s
6666667141414921 3.778166s
7777777536340681 4.120069s
7777777777777753 P 4.141530s
7777777777777777 0.000007s
9999999999999917 P 4.678164s
9999999999999999 0.000007s
Total time: 40.31

Os resultados aparecem em três colunas:

O número a ser verificado.


P se é um número primo, caso contrária, vazia.
Tempo decorrido para verificar se aquele número específico é primo.

Neste exemplo, o tempo total é aproximadamente a soma do tempo de cada verificação, mas está computado
separadamente, como se vê no Exemplo 12.

Exemplo 12. sequential.py: verificação de números primos em um pequeno conjunto de dados


PY
#!/usr/bin/env python3

"""
sequential.py: baseline for comparing sequential, multiprocessing,
and threading code for CPU-intensive work.
"""

from time import perf_counter


from typing import NamedTuple

from primes import is_prime, NUMBERS

class Result(NamedTuple): # (1)


prime: bool
elapsed: float

def check(n: int) -> Result: # (2)


t0 = perf_counter()
prime = is_prime(n)
return Result(prime, perf_counter() - t0)

def main() -> None:


print(f'Checking {len(NUMBERS)} numbers sequentially:')
t0 = perf_counter()
for n in NUMBERS: # (3)
prime, elapsed = check(n)
label = 'P' if prime else ' '
print(f'{n:16} {label} {elapsed:9.6f}s')

elapsed = perf_counter() - t0 # (4)


print(f'Total time: {elapsed:.2f}s')

if __name__ == '__main__':
main()

1. A função check (na próxima chamada) retorna uma tupla Result com o valor booleano da chamada a is_prime
e o tempo decorrido.
2. check(n) chama is_prime(n) e calcula o tempo decorrido para retornar um Result .

3. Para cada número na amostra, chamamos check e apresentamos o resultado.


4. Calcula e mostra o tempo total decorrido.

19.6.1. Solução baseada em processos


O próximo exemplo, procs.py, mostra o uso de múltiplos processos para distribuir a verificação de números primos por
muitos núcleos da CPU. Esses são os tempos obtidos com procs.py:
$ python3 procs.py
Checking 20 numbers with 12 processes:
2 P 0.000002s
3333333333333333 0.000021s
4444444444444444 0.000002s
5555555555555555 0.000018s
6666666666666666 0.000002s
142702110479723 P 1.350982s
7777777777777777 0.000009s
299593572317531 P 1.981411s
9999999999999999 0.000008s
3333333333333301 P 6.328173s
3333335652092209 6.419249s
4444444488888889 7.051267s
4444444444444423 P 7.122004s
5555553133149889 7.412735s
5555555555555503 P 7.603327s
6666666666666719 P 7.934670s
6666667141414921 8.017599s
7777777536340681 8.339623s
7777777777777753 P 8.388859s
9999999999999917 P 8.117313s
20 checks in 9.58s

A última linha dos resultados mostra que procs.py foi 4,2 vezes mais rápido que sequential.py.

19.6.2. Entendendo os tempos decorridos


Observe que o tempo decorrido na primeira coluna é o tempo para verificar aquele número específico. Por exemplo,
is_prime(7777777777777753) demorou quase 8,4s para retornar True . Enquanto isso, outros processos estavam
verificando outros números em paralelo.

Há 20 números para serem verificados. Escrevi procs.py para iniciar um número de processos de trabalho igual ao
número de núcleos na CPU, como determinado por multiprocessing.cpu_count() .

O tempo total neste caso é muito menor que a soma dos tempos decorridos para cada verificação individual. Há algum
tempo gasto em iniciar processos e na comunicação entre processos, então o resultado final é que a versão
multiprocessos é apenas cerca de 4,2 vezes mais rápida que a sequencial. Isso é bom, mas um pouco desapontador,
considerando que o código inicia 12 processos, para usar todos os núcleos desse laptop.

A função multiprocessing.cpu_count() retorna 12 no MacBook Pro que estou usando para


escrever esse capítulo. Ele é na verdade um i7 com uma CPU de 6 núcleos, mas o SO informa 12 CPUs
devido ao hyperthreading, uma tecnologia da Intel que executa duas threads por núcleo. Entretanto,

✒️ NOTA hyperthreading funciona melhor quando uma das threads não está trabalhando tão pesado quanto a
outra thread no mesmo núcleo—talvez a primeira esteja parada, esperando por dados após uma
perda de cache, e a outra está mastigando números. De qualquer forma, não há almoço grátis: este
laptop tem o desempenho de uma máquina com 6 CPUs para atividades de processamento intensivo
com pouco uso de memória—como essa verificação simples de números primos.

19.6.3. Código para o verificador de números primos com múltiplos núcleos


Quando delegamos processamento para threads e processos, nosso código não chama a função de trabalho
diretamente, então não conseguimos simplesmente retornar um resultado. Em vez disso, a função de trabalho é guiada
pela biblioteca de threads ou processos, e por fim produz um resultado que precisa ser armazenado em algum lugar.
Coordenar threads ou processos de trabalho e coletar resultados são usos comuns de filas em programação
concorrente—e também em sistemas distribuídos.
Muito do código novo em procs.py se refere a configurar e usar filas. O início do arquivo está no Exemplo 13.

⚠️ AVISO SimpleQueue foi acrescentada a multiprocessing no Python 3.9. Se você estiver usando uma
versão anterior do Python, pode substituir SimpleQueue por Queue no Exemplo 13.

Exemplo 13. procs.py: verificação de primos com múltiplos processos; importações, tipos, e funções

PY
import sys
from time import perf_counter
from typing import NamedTuple
from multiprocessing import Process, SimpleQueue, cpu_count # (1)
from multiprocessing import queues # (2)

from primes import is_prime, NUMBERS

class PrimeResult(NamedTuple): # (3)


n: int
prime: bool
elapsed: float

JobQueue = queues.SimpleQueue[int] # (4)


ResultQueue = queues.SimpleQueue[PrimeResult] # (5)

def check(n: int) -> PrimeResult: # (6)


t0 = perf_counter()
res = is_prime(n)
return PrimeResult(n, res, perf_counter() - t0)

def worker(jobs: JobQueue, results: ResultQueue) -> None: # (7)


while n := jobs.get(): # (8)
results.put(check(n)) # (9)
results.put(PrimeResult(0, False, 0.0)) # (10)

def start_jobs(
procs: int, jobs: JobQueue, results: ResultQueue # (11)
) -> None:
for n in NUMBERS:
jobs.put(n) # (12)
for _ in range(procs):
proc = Process(target=worker, args=(jobs, results)) # (13)
proc.start() # (14)
jobs.put(0) # (15)

1. Na tentativa de emular threading , multiprocessing fornece multiprocessing.SimpleQueue , mas esse é um


método vinculado a uma instância pré-definida de uma classe de nível mais baixo, BaseContext . Temos que
chamar essa SimpleQueue para criar uma fila. Por outro lado, não podemos usá-la em dicas de tipo.
2. multiprocessing.queues contém a classe SimpleQueue que precisamos para dicas de tipo.
3. PrimeResult inclui o número verificado. Manter n junto com os outros campos do resultado simplifica a exibição
mais tarde.
4. Isso é um apelido de tipo para uma SimpleQueue que a função main (Exemplo 14) vai usar para enviar os
números para os processos que farão a verificação.
5. Apelido de tipo para uma segunda SimpleQueue que vai coletar os resultados em main . Os valores na fila serão
tuplas contendo o número a ser testado e uma tupla Result .
6. Isso é similar a sequential.py.
7. worker recebe uma fila com os números a serem verificados, e outra para colocar os resultados.
8. Nesse código, usei o número 0 como uma pílula venenosa: um sinal para que o processo encerre. Se n não é 0,
continue com o loop.[265]
9. Invoca a verificação de número primo e coloca o PrimeResult na fila.
10. Devolve um PrimeResult(0, False, 0.0) , para informar ao loop principal que esse processo terminou seu
trabalho.
11. procs é o número de processos que executarão a verificação de números primos em paralelo.
12. Coloca na fila jobs os números a serem verificados.
13. Cria um processo filho para cada worker . Cada um desses processos executará o loop dentro de sua própria
instância da função worker , até encontrar um 0 na fila jobs .
14. Inicia cada processo filho.
15. Coloca um 0 na fila de cada processo, para encerrá-los.

Loops, sentinelas e pílulas venenosas


A função worker no Exemplo 13 segue um modelo comum em programação concorrente: percorrer
indefinidamente um loop, pegando itens em um fila e processando cada um deles com uma função que realiza o
trabalho real. O loop termina quando a fila produz um valor sentinela. Nesse modelo, a sentinela que encerra o
processo é muitas vezes chamada de "pílula venenosa.

é bastante usado como valor sentinela, mas pode não ser adequado se existir a possibilidade dele aparecer
None
entre os dados. Chamar object() é uma maneira comum de obter um valor único para usar como sentinela.
Entretanto, isso não funciona entre processos, pois os objetos Python precisam ser serializados para comunicação
entre processos. Quando você pickle.dump e pickle.load uma instância de object , a instância recuperada
em pickle.load é diferentes da original: elas não serão iguais se comparadas. Uma boa alternativa a None é o
objeto embutido Ellipsis (também conhecido como …​), que sobrevive à serialização sem perder sua
identidade.[266]

A biblioteca padrão do Python usa muitos valores diferentes (https://fpy.li/19-22) (EN) como sentinelas. A PEP 661—
Sentinel Values (https://fpy.li/pep661) (EN) propõe um tipo sentinela padrão. Em março de 2023, é apenas um
rascunho.

Agora vamos estudar a função main de procs.py no Exemplo 14.

Exemplo 14. procs.py: verificação de números primos com múltiplos processos; função main
PY
def main() -> None:
if len(sys.argv) < 2: # (1)
procs = cpu_count()
else:
procs = int(sys.argv[1])

print(f'Checking {len(NUMBERS)} numbers with {procs} processes:')


t0 = perf_counter()
jobs: JobQueue = SimpleQueue() # (2)
results: ResultQueue = SimpleQueue()
start_jobs(procs, jobs, results) # (3)
checked = report(procs, results) # (4)
elapsed = perf_counter() - t0
print(f'{checked} checks in {elapsed:.2f}s') # (5)

def report(procs: int, results: ResultQueue) -> int: # (6)


checked = 0
procs_done = 0
while procs_done < procs: # (7)
n, prime, elapsed = results.get() # (8)
if n == 0: # (9)
procs_done += 1
else:
checked += 1 # (10)
label = 'P' if prime else ' '
print(f'{n:16} {label} {elapsed:9.6f}s')
return checked

if __name__ == '__main__':
main()

1. Se nenhum argumento é dado na linha de comando, define o número de processos como o número de núcleos na
CPU; caso contrário, cria quantos processos forem passados no primeiro argumento.
2. jobs e results são as filas descritas no Exemplo 13.
3. Inicia proc processos para consumir jobs e informar results .

4. Recupera e exibe os resultados; report está definido em 6.


5. Mostra quantos números foram verificados e o tempo total decorrido.
6. Os argumentos são o número de procs e a fila para armazenar os resultados.
7. Percorre o loop até que todos os processos terminem.
8. Obtém um PrimeResult . Chamar .get() em uma fila deixa o processamento bloqueado até que haja um item na
fila. Também é possível fazer isso de forma não-bloqueante ou estabelecer um timeout. Veja os detalhes na
documentação de SimpleQueue.get (https://docs.python.org/pt-br/3/library/queue.html#queue.SimpleQueue.get).
9. Se n é zero, então um processo terminou; incrementa o contador procs_done .

10. Senão, incrementa o contador checked (para acompanhar os números verificados) e mostra os resultados.

Os resultados não vão retornar na mesma ordem em que as tarefas foram submetidas. Por isso for necessário incluir n
em cada tupla PrimeResult . De outra forma eu não teria como saber que resultado corresponde a cada número.

Se o processo principal terminar antes que todos os subprocessos finalizem, podem surgir relatórios de rastreamento
(tracebacks) confusos, com referências a exceções de FileNotFoundError causados por uma trava interna em
multiprocessing . Depurar código concorrente é sempre difícil, e depurar código baseado no multiprocessing é
ainda mais difícil devido a toda a complexidade por trás da fachada emulando threads. Felizmente, o
ProcessPoolExecutor que veremos no Capítulo 20 é mais fácil de usar e mais robusto.
Agradeço ao leitor Michael Albert, que notou que o código que publiquei durante o pré-lançamento
tinha uma "condição de corrida" (race condition)
(https://pt.wikipedia.org/wiki/Condi%C3%A7%C3%A3o_de_corrida) no Exemplo 14. Uma condição de corrida
(ou de concorrência) é um bug que pode ou não aparecer, dependendo da ordem das ações
realizadas pelas unidades de execução concorrentes. Se "A" acontecer antes de "B", tudo segue
✒️ NOTA normal; mas se "B" acontecer antes, surge um erro. Essa é a corrida.

Se você estiver curiosa, esse diff mostra o bug e sua correção: example-code-2e/commit/2c123057
(https://fpy.li/19-25)—mas note que depois eu refatorei o exemplo para delegar partes de main para as
funções start_jobs e report . Há um arquivo README.md (https://fpy.li/19-26) na mesma pasta
explicando o problema e a solução.

19.6.4. Experimentando com mais ou menos processos


Você poderia tentar rodar procs.py, passando argumentos que modifiquem o número de processos filho. Por exemplo,
este comando…​

BASH
$ python3 procs.py 2

…​vai iniciar dois subprocessos, produzindo os resultados quase duas vezes mais rápido que sequential.py—se a sua
máquina tiver uma CPU com pelo menos dois núcleos e não estiver ocupada rodando outros programas.

Rodei procs.py 12 vezes, usando de 1 a 20 subprocessos, totalizando 240 execuções. Então calculei a mediana do tempo
para todas as execuções com o mesmo número de subprocessos, e desenhei a Figura 2.

Figura 2. Mediana dos tempos de execução para cada número de subprocessos de 1 a 20. O maior tempo mediano foi
40,81s, com 1 processo. O tempo mediano mais baixo foi 10,39s, com 6 processos, indicado pela linha pontilhada.

Neste laptop de 6 núcleos, o menor tempo mediano ocorreu com 6 processos:10.39s—marcado pela linha pontilhada na
Figura 2. Seria de se esperar que o tempo de execução aumentasse após 6 processos, devido à disputa pela CPU, e ele
atingiu um máximo local de 12.51s, com 10 processes. Eu não esperava e não sei explicar porque o desempenho
melhorou com 11 processos e permaneceu praticamente igual com 13 a 20 processos, com tempos medianos apenas
ligeiramente maiores que o menor tempo mediano com 6 processos.

19.6.5. Não-solução baseada em threads


Também escrevi threads.py, uma versão de procs.py usando threading em vez de multiprocessing . O código é
muito similar quando convertemos exemplo simples entre as duas APIs.[267] Devido à GIL e à natureza de
processamento intensivo de is_prime , a versão com threads é mais lenta que a versão sequencial do Exemplo 12, e
fica mais lenta conforme aumenta o número de threads, por causa da disputa pela CPU e o custo da mudança de
contexto. Para passar de uma thread para outra, o SO precisa salvar os registradores da CPU e atualizar o contador de
programas e o ponteiro do stack, disparando efeitos colaterais custosos, como invalidar os caches da CPU e talvez até
trocar páginas de memória. [268]

Os dois próximos capítulos tratam de mais temas ligados à programação concorrente em Python, usando a biblioteca
de alto nível concurrent.futures para gerenciar threads e processos (Capítulo 20) e a biblioteca asyncio para
programação assíncrona (Capítulo 21).

As demais seções nesse capítulo procuram responder à questão:

“ Dadas as limitações discutidas até aqui, como é possível que o Python seja tão bem-sucedido em
um mundo de CPUs com múltiplos núcleos?

19.7. Python no mundo multi-núcleo.


Considere a seguinte passagem, do artigo muito citado "The Free Lunch Is Over: A Fundamental Turn Toward
Concurrency in Software" (O Almoço Grátis Acabou: Uma Virada Fundamental do Software em Direção à Concorrência)
(EN) de Herb Sutter (https://fpy.li/19-29):

“ Ose omais importantes fabricantes e arquiteturas de processadores, da Intel e da AMD até a Sparc
PowerPC, esgotaram o potencial da maioria das abordagens tradicionais de aumento do
desempenho das CPUs. Ao invés de elevar a frequência do clock [dos processadores] e a taxa de
transferência das instruções encadeadas a níveis cada vez maiores, eles estão se voltando em
massa para o hyper-threading (hiperprocessamento) e para arquiteturas multi-núcleo. Março de
2005. [Disponível online].

O que Sutter chama de "almoço grátis" era a tendência do software ficar mais rápido sem qualquer esforço adicional
por parte dos desenvolvedores, porque as CPUs estavam executando código sequencial cada vez mais rápido, ano após
ano. Desde 2004 isso não é mais verdade: a frequência dos clocks das CPUs e as otimizações de execução atingiram um
platô, e agora qualquer melhoria significativa no desempenho precisa vir do aproveitamento de múltiplos núcleos ou
do hyperthreading, avanços que só beneficiam código escrito para execução concorrente.

A história do Python começa no início dos anos 1990, quando as CPUs ainda estavam ficando exponencialmente mais
rápidas na execução de código sequencial. Naquele tempo não se falava de CPUs com múltiplos núcleos, exceto para
supercomputadores. Assim, a decisão de ter uma GIL era óbvia. A GIL torna o interpretador rodando em um único
núcleo mais rápido, e simplifica sua implementação.[269] A GIL também torna mais fácil escrever extensões simples
com a API Python/C.
Escrevi "extensões simples" porque uma extensão não é obrigada a lidar com a GIL. Uma função
escrita em C ou Fortran pode ser centenas de vezes mais rápida que sua equivalente em Python.[270]
✒️ NOTA Assim, a complexidade adicional de liberar a GIL para tirar proveito de CPUs multi-núcleo pode, em
muitos casos, não ser necessária. Então podemos agradecer à GIL por muitas das extensões
disponíveis em Python—e isso é certamente uma das razões fundamentais da popularidade da
linguagem hoje.

Apesar da GIL, o Python está cada vez mais popular entre aplicações que exigem execução concorrente ou paralela,
graças a bibliotecas e arquiteturas de software que contornam as limitações do CPython.

Agora vamos discutir como o Python é usado em administração de sistemas, ciência de dados, e desenvolvimento de
aplicações para servidores no mundo do processamento distribuído e dos multi-núcleos de 2023.

19.7.1. Administração de sistemas


O Python é largamente utilizado para gerenciar grandes frotas de servidores, roteadores, balanceadores de carga e
armazenamento conectado à rede (network-attached storage ou NAS). Ele é também a opção preferencial para redes
definidas por software (SND, software-defined networking) e hacking ético. Os maiores provedores de serviços na
nuvem suportam Python através de bibliotecas e tutoriais de sua própria autoria ou da autoria de suas grande
comunidades de usuários da linguagem.

Nesse campo, scripts Python automatizam tarefas de configuração, emitindo comandos a serem executados pelas
máquinas remotas, então raramente há operações limitadas pela CPU. Threads ou corrotinas são bastante adequadas
para tais atividades. Em particular, o pacote concurrent.futures , que veremos no Capítulo 20, pode ser usado para
realizar as mesmas operações em muitas máquinas remotas ao mesmo tempo, sem grande complexidade.

Além da biblioteca padrão, há muito projetos populares baseados em Python para gerenciar clusters (agrupamentos) de
servidores: ferramentas como o Ansible (https://fpy.li/19-30) (EN) e o Salt (https://fpy.li/19-31) (EN), bem como bibliotecas
como a Fabric (https://fpy.li/19-32) (EN).

Há também um número crescente de bibliotecas para administração de sistemas que suportam corrotinas e asyncio .
Em 2016, a equipe de Engenharia de Produção (https://fpy.li/19-33) (EN) do Facebook relatou: "Estamos cada vez mais
confiantes no AsyncIO, introduzido no Python 3.4, e vendo ganhos de desempenho imensos conforme migramos as
bases de código do Python 2."

19.7.2. Ciência de dados


A ciência de dados—incluindo a inteligência artificial—e a computação científica estão muito bem servidas pelo
Python.

Aplicações nesses campos são de processamento intensivo, mas os usuários de Python se beneficiam de um vasto
ecossistema de bibliotecas de computação numérica, escritas em C, C++, Fortran, Cython, etc.—muitas das quais
capazes de aproveitar os benefícios de máquinas multi-núcleo, GPUs, e/ou computação paralela distribuída em clusters
heterogêneos.

Em 2021, o ecossistema de ciência de dados de Python já incluía algumas ferramentas impressionantes:

Project Jupyter (https://fpy.li/19-34)


Duas interfaces para navegadores—Jupyter Notebook e JupyterLab—que permitem aos usuários rodar e
documentar código analítico, potencialmente sendo executado através da rede em máquinas remotas. Ambas são
aplicações híbridas Python/Javascript, suportando kernels de processamento escritos em diferentes linguagens,
todos integrados via ZeroMQ—uma biblioteca de comunicação por mensagens assíncrona para aplicações
distribuídas. O nome Jupyter, inclusive, vem de Julia, Python, e R, as três primeiras linguagens suportadas pelo
Notebook. O rico ecossistema construído sobre as ferramentas Jupyter incluí o Bokeh (https://fpy.li/19-35), uma
poderosa biblioteca de visualização iterativa que permite aos usuários navegarem e interagirem com grandes
conjuntos de dados ou um fluxo de dados continuamente atualizado, graças ao desempenho dos navegadores
modernos e seus interpretadores JavaScript.

TensorFlow (https://fpy.li/19-36) e PyTorch (https://fpy.li/19-37)


Estas são as duas principais frameworks de aprendizagem profunda (deep learning), de acordo com o relatório de
Janeiro de 2021 da O’Reilly’s (https://fpy.li/19-38) (EN) medido pelo uso de seus recursos de aprendizagem durante 2020.
Os dois projetos são escritos em C++, e conseguem se beneficiar de múltiplos núcleos, GPUs e clusters. Eles também
suportam outras linguagens, mas o Python é seu maior foco e é usado pela maioria de seus usuários. O TensorFlow
foi criado e é usado internamente pelo Google; O Pythorch pelo Facebook.

Dask (https://fpy.li/dask)
Uma biblioteca de computação paralela que consegue delegar para processos locais ou um cluster de máquinas,
"testado em alguns dos maiores supercomputadores do mundo"—como seu site (https://fpy.li/dask) (EN) afirma. O Dask
oferece APIs que emulam muito bem o NumPy, o pandas, e o scikit-learn—hoje as mais populares bibliotecas em
ciência de dados e aprendizagem de máquina. O Dask pode ser usado a partir do JupyterLab ou do Jupyter Notebook,
e usa o Bokeh não apenas para visualização de dados mas também para um quadro interativo mostrando o fluxo de
dados e o processamento entre processos/máquinas quase em tempo real. O Dask é tão impressionante que
recomento assistir um vídeo tal como esse, 15-minute demo (https://fpy.li/19-39), onde Matthew Rocklin—um
mantenedor do projeto—mostra o Dask mastigando dados em 64 núcleos distribuídos por 8 máquinas EC2 na AWS.
Estes são apenas alguns exemplos para ilustrar como a comunidade de ciência de dados está criando soluções que
extraem o melhor do Python e superam as limitações do runtime do CPython.

19.7.3. Desenvolvimento de aplicações server-side para Web/Computação Móvel


O Python é largamente utilizado em aplicações Web e em APIs de apoio a aplicações para computação móvel no
servidor. Como o Google, o YouTube, o Dropbox, o Instagram, o Quora, e o Reddit—entre outros—conseguiram
desenvolver aplicações de servidor em Python que atendem centenas de milhões de usuários 24X7? Novamente a
resposta vai bem além do que o Python fornece "de fábrica". Antes de discutir as ferramentas necessárias para usar o
Python larga escala, preciso citar uma advertência da Technology Radar da Thoughtworks:

“ Inveja de alto desempenho/inveja de escala da web

Vemos muitas equipes se metendo em apuros por escolher ferramentas, frameworks ou


arquiteturas complexas, porque eles "talvez precisem de escalabilidade". Empresas como o
Twitter e a Netflix precisam aguentar cargas extremas, então precisam dessas arquiteturas, mas
elas também tem equipes de desenvolvimento extremamente habilitadas, capazes de lidar com a
complexidade. A maioria das situações não exige essas façanhas de engenharia; as equipes
devem manter sua inveja da escalabilidade na web sob controle, e preferir soluções simples que
ainda assim fazem o que precisa ser feito.[271]

Na escala da web, a chave é uma arquitetura que permita escalabilidade horizontal. Neste cenário, todos os sistemas
são sistemas distribuídos, e possivelmente nenhuma linguagem de programação será a única alternativa ideal para
todas as partes da solução.

Sistemas distribuídos são um campo da pesquisa acadêmica, mas felizmente alguns profissionais da área escreveram
livros acessíveis, baseados em pesquisas sólidas e experiência prática. Um deles é Martin Kleppmann, o autor de
Designing Data-Intensive Applications (Projetando Aplicações de Uso Intensivo de Dados) (O’Reilly).
Observe a Figura 3, o primeiro de muitos diagramas de arquitetura no livro de Kleppmann. Aqui há alguns
componentes que vi em muitos ambientes Python onde trabalhei ou que conheci pessoalmente:

Caches de aplicação:[272] memcached, Redis, Varnish


bancos de dados relacionais: PostgreSQL, MySQL
Bancos de documentos: Apache CouchDB, MongoDB
Full-text indexes (índices de texto integral): Elasticsearch, Apache Solr
Enfileiradores de mensagens: RabbitMQ, Redis

Figura 3. Uma arquitetura possível para um sistema, combinando diversos componentes.[273]

Há outros produtos de código aberto extremamente robustos em cada uma dessas categorias. Os grandes fornecedores
de serviços na nuvem também oferecem suas próprias alternativas proprietárias

O diagrama de Kleppmann é genérico e independente da linguagem—como seu livro. Para aplicações de servidor em
Python, dois componentes específicos são comumente utilizados:

Um servidor de aplicação, para distribuir a carga entre várias instâncias da aplicação Python. O servidor de
aplicação apareceria perto do topo na Figura 3, processando as requisições dos clientes antes delas chegaram ao
código da aplicação.
Uma fila de tarefas construída em torno da fila de mensagens no lado direito da Figura 3, oferecendo uma API de
alto nível e mais fácil de usar, para distribuir tarefas para processos rodando em outras máquinas.
As duas próximas seções exploram esses componentes, recomendados pelas boas práticas de implementações de
aplicações Python de servidor.

19.7.4. Servidores de aplicação WSGI


O WSGI— Web Server Gateway Interface (https://fpy.li/pep3333) (Interface de Gateway de Servidores Web)—é a API padrão
para uma aplicação ou um framework Python receber requisições de um servidor HTTP e enviar para ele as respostas.
[274] Servidores de aplicação WSGI gerenciam um ou mais processos rodando a sua aplicação, maximizando o uso das
CPUs disponíveis.

A Figura 4 ilustra uma instalação WSGI típica.

👉 DICA Se quiséssemos fundir os dois diagramas, o conteúdo do retângulo tracejado na Figura 4 substituiria
o retângulo sólido "Application code"(código da aplicação) no topo da Figura 3.

Os servidores de aplicação mais conhecidos em projeto web com Python são:

mod_wsgi (https://fpy.li/19-41)
uWSGI (https://fpy.li/19-42)[275]
Gunicorn (https://fpy.li/gunicorn)
NGINX Unit (https://fpy.li/19-43)

Para usuários do servidor HTTP Apache, mod_wsgi é a melhor opção. Ele é tão antigo com a própria WSGI, mas tem
manutenção ativa, e agora pode ser iniciado via linha de comando com o mod_wsgi-express , que o torna mais fácil de
configurar e mais apropriado para uso com containers Docker.
Figura 4. Clientes se conectam a um servidor HTTP que entrega arquivos estáticos e roteia outras requisições para o
servidor de aplicação, que inicia processo filhos para executar o código da aplicação, utilizando múltiplos núcleos de
CPU. A API WSGI é a ponte entre o servidor de aplicação e o código da aplicação Python.

O uWSGI e o Gunicorn são as escolhas mais populares entre os projetos recentes que conheço. Ambos são
frequentemente combinados com o servidor HTTP NGINX. uWSGI oferece muita funcionalidade adicional, incluindo
um cache de aplicação, uma fila de tarefas, tarefas periódicas estilo cron, e muitas outras. Por outro lado, o uWSGI é
muito mais difícil de configurar corretamente que o Gunicorn.[276]

Lançado em 2018, o NGINX Unit é um novo produto dos desenvolvedores do conhecido servidor HTTP e proxy reverso
NGINX.

O mod_wsgi e o Gunicorn só suportam apps web Python, enquanto o uWSGI e o NGINX Unit funcionam também com
outras linguagens. Para saber mais, consulte a documentação de cada um deles.

O ponto principal: todos esses servidores de aplicação podem, potencialmente, utilizar todos os núcleos de CPU no
servidor, criando múltiplos processos Python para executar apps web tradicionais escritas no bom e velho código
sequencial em Django, Flask, Pyramid, etc. Isso explica porque tem sido possível ganhar a vida como desenvolvedor
Python sem nunca ter estudado os módulos threading , multiprocessing , ou asyncio : o servidor de aplicação lida
de forma transparente com a concorrência.

ASGI—Asynchronous Server Gateway Interface


(Interface Assíncrona de Ponto de Entrada de Servidor)
A WSGI é uma API síncrona. Ela não suporta corrotinas com async/await —a forma mais eficiente
de implementar WebSockets or long pooling de HTTP em Python. A especificação da ASGI
✒️ NOTA (https://fpy.li/19-46) é a sucessora da WSGI, projetada para frameworks Python assíncronas para
programação web, tais como aiohttp, Sanic, FastAPI, etc., bem como Django e Flask, que estão
gradativamente acrescentando funcionalidade assíncrona.

Agora vamos examinar outra forma de evitar a GIL para obter um melhor desempenho em aplicações Python de
servidor.

19.7.5. Filas de tarefas distribuídas


Quando o servidor de aplicação entrega uma requisição a um dos processos Python rodando seu código, sua aplicação
precisa responder rápido: você quer que o processo esteja disponível para processar a requisição seguinte assim que
possível. Entretanto, algumas requisições exigem ações que podem demorar—por exemplo, enviar um email ou gerar
um PDF. As filas de tarefas distribuídas foram projetadas para resolver este problema.

A Celery (https://fpy.li/19-47) e a RQ (https://fpy.li/19-48) são as mais conhecidas filas de tarefas Open Source com uma API
para o Python. Provedores de serviços na nuvem também oferecem suas filas de tarefas proprietárias.

Esses produtos encapsulam filas de mensagens e oferecem uma API de alto nível para delegar tarefas a processos
executores, possivelmente rodando em máquinas diferentes.

No contexto de filas de tarefas, as palavras produtor e consumidor são usado no lugar da

✒️ NOTA terminologia tradicional de cliente/servidor. Por exemplo, para gerar documentos, um processador
de views do Django produz requisições de serviço, que são colocadas em uma fila para serem
consumidas por um ou mais processos renderizadores de PDFs.

Citando diretamente o FAQ (https://fpy.li/19-49) do Celery, eis alguns casos de uso:

“ Executar algo em segundo plano. Por exemplo, para encerrar uma requisição web o mais
rápido possível, e então atualizar a página do usuário de forma incremental. Isso dá ao
usuário a impressão de um bom desempenho e de "vivacidade", ainda que o trabalho real
possa na verdade demorar um pouco mais.
Executar algo após a requisição web ter terminado.
Se assegurar que algo seja feito, através de uma execução assíncrona e usando tentativas
repetidas.
Agendar tarefas periódicas.

Além de resolver esses problemas imediatos, as filas de tarefas suportam escalabilidade horizontal. Produtores e
consumidores são desacoplados: um produtor não precisa chamar um consumidor, ele coloca uma requisição em uma
fila. Consumidores não precisam saber nada sobre os produtores (mas a requisição pode incluir informações sobre o
produtor, se uma confirmação for necessária). Pode-se adicionar mais unidades de execução para consumir tarefas a
medida que a demanda cresce. Por isso o Celery e o RQ são chamados de filas de tarefas distribuídas.
Lembre-se que nosso simples procs.py (Exemplo 13) usava duas filas: uma para requisições de tarefas, outra para
coletar resultados. A arquitetura distribuída do Celery e do RQ usa um esquema similar. Ambos suportam o uso do
banco de dados NoSQL Redis (https://fpy.li/19-50) para armazenar as filas de mensagens e resultados. O Celery também
suporta outras filas de mensagens, como o RabbitMQ ou o Amazon SQS, bem como outros bancos de dados para
armazenamento de resultados.

Isso encerra nossa introdução à concorrência em Python. Os dois próximos capítulos continuam nesse tema, se
concentrando nos pacotes concurrent.futures e asyncio packages da biblioteca padrão.

19.8. Resumo do capítulo


Após um pouco de teoria, esse capítulo apresentou scripts da animação giratória, implementados em cada um dos três
modelos de programação de concorrência nativos do Python:

Threads, com o pacote threading

Processo, com multiprocessing

Corrotinas assíncronas com asyncio

Então exploramos o impacto real da GIL com um experimento: mudar os exemplos de animação para computar se um
inteiro grande era primo e observar o comportamento resultante. Isso demonstrou graficamente que funções de uso
intensivo da CPU devem ser evitadas em asyncio , pois elas bloqueiam o loop de eventos. A versão com threads do
experimento funcionou—apesar da GIL—porque o Python periodicamente interrompe as threads, e o exemplo usou
apenas duas threads: uma fazendo um trabalho de computação intensiva, a outra controlando a animação apenas 10
vezes por segundo. A variante com multiprocessing contornou a GIL, iniciando um novo processo só para a
animação, enquanto o processo principal calculava se o número era primo.

O exemplo seguinte, computando diversos números primos, destacou a diferença entre multiprocessing e
threading , provando que apenas processos permitem ao Python se beneficiar de CPUs com múltiplo núcleos. A GIL
do Python torna as threads piores que o código sequencial para processamento pesado.

A GIL domina as discussões sobre computação concorrente e paralela em Python, mas não devemos superestimar seu
impacto. Este foi o tema da Seção 19.7. Por exemplo, a GIL não afeta muitos dos casos de uso de Python em
administração de sistemas. Por outro lado, as comunidades de ciência de dados e de desenvolvimento para servidores
evitaram os problemas com a GIL usando soluções robustas, criadas sob medida para suas necessidades específicas. As
últimas duas seções mencionaram os dois elementos comuns que sustentam o uso de Python em aplicações de servidor
escaláveis: servidores de aplicação WSGI e filas de tarefas distribuídas.

19.9. Para saber mais


Este capítulo tem uma extensa lista de referências, então a dividi em subseções.

19.9.1. Concorrência com threads e processos


A biblioteca concurrent.futures, tratada no Capítulo 20, usa threads, processos, travas e filas debaixo dos panos, mas
você não vai ver as instâncias individuais desses elementos; eles são encapsulados e gerenciados por abstrações de um
nível mais alto: ThreadPoolExecutor ou ProcessPoolExecutor . Para aprender mais sobre a prática da programação
concorrente com aqueles objetos de baixo nível, "An Intro to Threading in Python" (https://fpy.li/19-51) (Uma Introdução [à
Programação com] Threads no Python) de Jim Anderson é uma boa primeira leitura. Doug Hellmann tem um capítulo
chamado "Concurrency with Processes, Threads, and Coroutines" (Concorrência com Processos, Threads, e Corrotinas)
em seus site (https://fpy.li/19-52) e livro, The Python 3 Standard Library by Example (https://fpy.li/19-53) (Addison-Wesley).
Effective Python (https://fpy.li/effectpy), 2nd ed. (Addison-Wesley), de Brett Slatkin, Python Essential Reference, 4th ed.
(Addison-Wesley), de David Beazley, e Python in a Nutshell, 3rd ed. (O’Reilly) de Martelli et al são outras referências
gerais de Python com uma cobertura significativa de threading e multiprocessing . A vasta documentação oficial
de multiprocessing inclui conselhos úteis em sua seção "Programming guidelines" (Diretrizes de programação)
(https://docs.python.org/pt-br/3/library/multiprocessing.html#programming-guidelines) (EN).

Jesse Noller e Richard Oudkerk contribuíram para o pacote multiprocessing , introduzido na PEP 371—​Addition of
the multiprocessing package to the standard library (https://fpy.li/pep371) (EN). A documentação oficial do pacote é um
arquivo de 93 KB .rst (https://docs.python.org/pt-br/3/library/multiprocessing.html)—são cerca de 63 páginas—tornando-o um
dos capítulos mais longos da biblioteca padrão do Python.

Em High Performance Python, 2nd ed., (https://fpy.li/19-56) (O’Reilly), os autores Micha Gorelick e Ian Ozsvald incluem um
capítulo sobre multiprocessing com um exemplo sobre verificação de números primos usando uma estratégia
diferente do nosso exemplo procs.py. Para cada número, eles dividem a faixa de fatores possíveis-de 2 a sqrt(n) —em
subfaixas, e fazem cada unidade de execução iterar sobre uma das subfaixas. Sua abordagem de dividir para
conquistar é típica de aplicações de computação científica, onde os conjuntos de dados são enormes, e as estações de
trabalho (ou clusters) tem mais núcleos de CPU que usuários. Em um sistema servidor, processando requisições de
muitos usuários, é mais simples e mais eficiente deixar cada processo realizar uma tarefa computacional do início ao
fim—reduzindo a sobrecarga de comunicação e coordenação entre processos. Além de multiprocessing , Gorelick e
Ozsvald apresentam muitas outras formas de desenvolver e implantar aplicações de ciência de dados de alto
desempenho, aproveitando múltiplos núcleos de CPU, GPUs, clusters, analisadores e compiladores como CYthon e
Numba. Seu capítulo final, "Lessons from the Field," (Lições da Vida Real) é uma valiosa coleção de estudos de caso
curtos, contribuição de outros praticantes de computação de alto desempenho em Python.

O Advanced Python Development (https://fpy.li/19-57), de Matthew Wilkes (Apress), é um dos raros livros a incluir
pequenos exemplos para explicar conceitos, mostrando ao mesmo tempo como desenvolver uma aplicação realista
pronta para implantação em produção: um agregador de dados, similar aos sistemas de monitoramento DevOps ou aos
coletores de dados para sensores distribuídos IoT. Dois capítulos no Advanced Python Development tratam de
programação concorrente com threading e asyncio .

O Parallel Programming with Python (https://fpy.li/19-58) (Packt, 2014), de Jan Palach, explica os principais conceitos por
trás da concorrência e do paralelismo, abarcando a biblioteca padrão do Python bem como o Celery.

"The Truth About Threads" (A Verdade Sobre as Threads) é o título do capítulo 2 de Using Asyncio in Python
(https://fpy.li/hattingh), de Caleb Hattingh (O’Reilly).[277] O capítulo trata dos benefícios e das desvantagens das threads—
com citações convincentes de várias fontes abalizadas—deixando claro que os desafios fundamentais das threads não
tem relação com o Python ou a GIL. Citando literalmente a página 14 de Using Asyncio in Python:

“ Esses temas se repetem com frequência:


Programação com threads torna o código difícil de analisar.
Programação com threads é um modelo ineficiente para concorrência em larga escala
(milhares de tarefas concorrentes).

Se você quiser aprender do jeito difícil como é complicado raciocinar sobre threads e travas—sem colocar seu emprego
em risco—tente resolver os problemas no livro de Allen Downey The Little Book of Semaphores (https://fpy.li/19-59) (Green
Tea Press). O livro inclui exercícios muito difíceis e até sem solução conhecida, mas mesmo os fáceis são desafiadores.

19.9.2. A GIL
Se você ficou curioso sobre a GIL, lembre-se que não temos qualquer controle sobre ela a partir do código em Python,
então a referência canônica é a documentação da C-API: Thread State and the Global Interpreter Lock (https://fpy.li/19-60)
(EN) (O Estado das Threads e a Trava Global do Interpretador). A resposta no FAQ Python Library and Extension (A
Biblioteca e as Extensões do Python): "Can’t we get rid of the Global Interpreter Lock?"
(https://docs.python.org/pt-br/3/faq/library.html#can-t-we-get-rid-of-the-global-interpreter-lock) (Não podemos remover o Bloqueio
Global do interpretador?). Também vale a pena ler os posts de Guido van Rossum e Jesse Noller (contribuidor do pacote
multiprocessing ), respectivamente: "It isn’t Easy to Remove the GIL" (https://fpy.li/19-62) (Não é Fácil Remover a GIL) e
"Python Threads and the Global Interpreter Lock" (https://fpy.li/19-63) (As Threads do Python e a Trava Global do
Interpretador).

CPython Internals (https://fpy.li/19-64), de Anthony Shaw (Real Python) explica a implementação do interpretador CPython
3 no nível da programação em C. O capítulo mais longo do livro é "Parallelism and Concurrency" (Paralelismo e
Concorrência): um mergulho profundo no suporte nativo do Python a threads e processos, incluindo o gerenciamento
da GIL por extensões usando a API C/Python.

Por fim, David Beazley apresentou uma exploração detalhada em "Understanding the Python GIL" (https://fpy.li/19-65)
(Entendendo a GIL do Python).[278] No slide 54 da apresentação (https://fpy.li/19-66), Beazley relata um aumento no tempo
de processamento de uma benchmark específica com o novo algoritmo da GIL, introduzido no Python 3.2. O problema
não tem importância com cargas de trabalho reais, de acordo com um comentário (https://fpy.li/19-67) de Antoine Pitrou—​
que implementou o novo algoritmo da GIL—​no relatório de bug submetido por Beazley: Python issue #7946
(https://fpy.li/19-68).

19.9.3. Concorrência além da biblioteca padrão


O Python Fluente se concentra nos recursos fundamentais da linguagem e nas partes centrais da biblioteca padrão. Full
Stack Python (https://fpy.li/19-69) é um ótimo complemento para esse livro: é sobre o ecossistema do Python, com seções
chamadas "Development Environments (Ambientes de Desenvolvimento)," "Data (Dados)," "Web Development
(Desenvolvimento Web)," e "DevOps," entre outros.

Já mencionei dois livros que abordam a concorrência usando a biblioteca padrão do Python e também incluem
conteúdo significativo sobre bibliotecas de terceiros e ferramentas:

High Performance Python, 2nd ed. (https://fpy.li/19-56) e Parallel Programming with Python (https://fpy.li/19-58). O Distributed
Computing with Python (https://fpy.li/19-72) de Francesco Pierfederici (Packt) cobre a biblioteca padrão e também
provedores de infraestrutura de nuvem e clusters HPC (High-Performance Computing, computação de alto
desempenho).

O "Python, Performance, and GPUs" (https://fpy.li/19-73) (EN) de Matthew Rocklin é uma atualização do status do uso de
aceleradores GPU com Python, publicado em junho de 2019.

"O Instagram hoje representa a maior instalação do mundo do framework web Django, que é escrito inteiramente em
Python." Essa é a linha de abertura do post "Web Service Efficiency at Instagram with Python" (https://fpy.li/19-74) (EN),
escrito por Min Ni—um engenheiro de software no Instagram. O post descreve as métricas e ferramentas usadas pelo
Instagram para otimizar a eficiência de sua base de código Python, bem como para detectar e diagnosticar regressões
de desempenho a cada uma das "30 a 50 vezes diárias" que o back-end é atualizado.

Architecture Patterns with Python: Enabling Test-Driven Development, Domain-Driven Design, and Event-Driven
Microservices (https://fpy.li/19-75), de Harry Percival e Bob Gregory (O’Reilly) apresenta modelos de arquitetura para
aplicações de servidor em Python. Os autores disponibilizaram o livro gratuitamente online em cosmicpython.com
(https://fpy.li/19-76) (EN).
Duas bibliotecas elegantes e fáceis de usar para tarefas de paralelização de processos são a lelo (https://fpy.li/19-77) de João
S. O. Bueno e a python-parallelize (https://fpy.li/19-78) de Nat Pryce. O pacote lelo define um decorador @parallel que
você pode aplicar a qualquer função para torná-la magicamente não-bloqueante: quando você chama uma função
decorada, sua execução é iniciada em outro processo. O pacote python-parallelize de Nat Pryce fornece um gerador
parallelize , que distribui a execução de um loop for por múltiplas CPUs. Ambos os pacotes são baseados na
biblioteca multiprocessing.

Eric Snow, um dos desenvolvedores oficiais do Python, mantém um wiki chamado Multicore Python (https://fpy.li/19-79),
com observações sobre os esforços dele e de outros para melhorar o suporte do Python a execução em paralelo. Snow é
o autor da PEP 554—​Multiple Interpreters in the Stdlib (https://fpy.li/pep554). Se aprovada e implementada, a PEP 554
assenta as bases para melhorias futuras, que podem um dia permitir que o Python use múltiplos núcleos sem as
sobrecargas do multiprocessing. Um dos grandes empecilhos é a iteração complexa entre múltiplos subinterpretadores
ativos e extensões que assumem a existência de um único interpretador.

Mark Shannon—também um mantenedor do Python—criou uma tabela útil (https://fpy.li/19-80) comparando os modelos
de concorrência em Python, referida em uma discussão sobre subinterpretadores entre ele, Eric Snow e outros
desenvolvedores na lista de discussão python-dev (https://fpy.li/19-81). Na tabela de Shannon, a coluna "Ideal CSP" se
refere ao modelo teórico de notação _Communicating Sequential Processes (https://fpy.li/19-82) (processos sequenciais
comunicantes) (EN), proposto por Tony Hoare em 1978. Go também permite objetos compartilhados, violando uma das
restrições essenciais do CSP: as unidades de execução devem se comunicar somente através de mensagens enviadas
através de canais.

O Stackless Python (https://fpy.li/19-83) (também conhecido como Stackless) é um fork do CPython que implementa
microthreads, que são threads leves no nível da aplicação—ao contrário das threads do SO. O jogo online multijogador
massivo EVE Online (https://fpy.li/19-84) foi desenvolvido com Stackless, e os engenheiros da desenvolvedora de jogos CCP
(https://fpy.li/19-85) foram mantenedores do Stackless (https://fpy.li/19-86) por algum tempo. Alguns recursos do Stackless
foram reimplementados no interpretador Pypy (https://fpy.li/19-87) e no pacote greenlet (https://fpy.li/19-14), a tecnologia
central da biblioteca de programação em rede gevent (https://fpy.li/19-17), que por sua vez é a fundação do servidor de
aplicação Gunicorn (https://fpy.li/gunicorn).

O modelo de atores (actor model) de programação concorrente está no centro das linguagens altamente escaláveis
Erlang e Elixir, e é também o modelo da framework Akka para Scala e Java. Se você quiser experimentar o modelo de
atores em Python, veja as bibliotecas Thespian (https://fpy.li/19-90) e Pykka (https://fpy.li/19-91).

Minhas recomendações restantes fazem pouca ou nenhuma menção ao Python, mas de toda forma são relevantes para
leitores interessados no tema do capítulo.

19.9.4. Concorrência e escalabilidade para além do Python


RabbitMQ in Action (https://fpy.li/19-92), de Alvaro Videla and Jason J. W. Williams (Manning), é uma introdução muito
bem escrita ao RabbitMQ e ao padrão AMQP (Advanced Message Queuing Protocol, Protocolo Avançado de
Enfileiramento de Mensagens), com exemplos em Python, PHP, e Ruby. Independente do resto de seu stack tecnológico,
e mesmo se você planeja usar Celery com RabbitMQ debaixo dos panos, recomendo esse livro por sua abordagem dos
conceitos, da motivação e dos modelos das filas de mensagem distribuídas, bem como a operação e configuração do
RabbitMQ em larga escala.

Aprendi muito lendo Seven Concurrency Models in Seven Weeks (https://fpy.li/19-93), de Paul Butcher (Pragmatic
Bookshelf), que traz o eloquente subtítulo When Threads Unravel.[279] O capítulo 1 do livro apresenta os conceitos
centrais e os desafios da programação com threads e travas em Java.[280] Os outros seis capítulos do livro são dedicados
ao que o autor considera as melhores alternativas para programação concorrente e paralela, e como funcionam com
diferentes linguagens, ferramentas e bibliotecas. Os exemplos usam Java, Clojure, Elixir, e C (no capítulo sobre
programação paralela com a framework OpenCL (https://fpy.li/19-94)). O modelo CSP é exemplificado com código Clojure,
apesar da linguagem Go merecer os créditos pela popularização daquela abordagem. Elixir é a linguagem dos
exemplos ilustrando o modelo de atores. Um capítulo bonus (https://fpy.li/19-95) alternativo (disponível online
gratuitamente) sobre atores usa Scala e a framework Akka. A menos que você já saiba Scala, Elixir é uma linguagem
mais acessível para aprender e experimentar o modelo de atores e plataforma de sistemas distribuídos Erlang/OTP.
Unmesh Joshi, da Thoughtworks contribuiu com várias páginas documentando os "Modelos de Sistemas Distribuídos"
no blog (https://fpy.li/19-96) de Martin Fowler. A página de abertura (https://fpy.li/19-97) é uma ótima introdução ao assunto,
com links para modelos individuais. Joshi está acrescentando modelos gradualmente, mas o que já está publicado
espelha anos de experiência adquirida a duras penas em sistema de missão crítica.

O Designing Data-Intensive Applications (https://fpy.li/19-98), de Martin Kleppmann (O’Reilly), é um dos raros livros
escritos por um profissional com vasta experiência na área e conhecimento acadêmico avançado. O autor trabalhou
com infraestrutura de dados em larga escala no LinkedIn e em duas startups, antes de se tornar um pesquisador de
sistemas distribuídos na Universidade de Cambridge. Cada capítulo do livro termina com uma extensa lista de
referências, incluindo resultados de pesquisas recentes. O livro também inclui vários diagramas esclarecedores e
lindos mapas conceituais.

Tive a sorte de estar na audiência do fantástico workshop de Francesco Cesarini sobre a arquitetura de sistemas
distribuídos confiáveis, na OSCON 2016: "Designing and architecting for scalability with Erlang/OTP" (Projetando e
estruturando para a escalabilidade com Erlang/OTP) (video (https://fpy.li/19-99) na O’Reilly Learning Platform). Apesar do
título, aos 9:35 no video, Cesarini explica:

“ Muito pouco do que vou dizer será específico de Erlang […]. Resta o fato de que o Erlang
remove muitas dificuldades acidentais no desenvolvimento de sistemas resilientes que nunca
falham, além serem escalonáveis. Então será mais fácil se vocês usarem Erlang ou uma
linguagem rodando na máquina virtual Erlang.

Aquele workshop foi baseado nos últimos quatro capítulos do Designing for Scalability with Erlang/OTP
(https://fpy.li/19-100) de Francesco Cesarini e Steve Vinoski (O’Reilly).

Desenvolver sistemas distribuídos é desafiador e excitante, mas cuidado com a inveja da escalabilidade na web
(https://fpy.li/19-40). O princípio KISS (https://fpy.li/19-102) (KISS é a sigla de Keep It Simple, Stupid: "Mantenha Isso Simples,
Idiota") continua sendo uma recomendação firme de engenharia.

Veja também o artigo "Scalability! But at what COST?" (https://fpy.li/19-103), de Frank McSherry, Michael Isard, e Derek G.
Murray. Os autores identificaram sistemas paralelos de processamento de grafos apresentados em simpósios
acadêmicos que precisavam de centenas de núcleos para superar "uma implementação competente com uma única
thread." Eles também encontraram sistemas que "tem desempenho pior que uma thread em todas as configurações
reportadas."

Essas descobertas me lembram uma piada hacker clássica:

“ Meu script Perl é mais rápido que seu cluster Hadoop.


Ponto de vista
Para gerenciar a complexidade, precisamos de restrições

Aprendi a programar em uma calculadora TI-58. Sua "linguagem" era similar ao assembler. Naquele nível, todas
as "variáveis" eram globais, e não havia o conforto dos comandos estruturados de controle de fluxo. Existiam
saltos condicionais: instruções que transferiam a execução diretamente para uma localização arbitrária—à frente
ou atrás do local atual—dependendo do valor de um registrador ou de uma flag na CPU.
É possível fazer basicamente qualquer coisa em assembler, e esse é o desafio: há muito poucas restrições para
evitar que você cometa erros, e para ajudar mantenedores a entender o código quando mudanças são
necessárias.

A segunda linguagem que aprendi foi o BASIC desestruturado que vinha nos computadores de 8 bits—nada
comparável ao Visual Basic, que surgiu muito mais tarde. Existiam os comandos FOR , GOSUB e RETURN , mas
ainda nenhum conceito de variáveis locais. O GOSUB não permitia passagem de parâmetros: era apenas um
GOTO mais chique, que inseria um número de linha de retorno em uma pilha, daí o RETURN tinha um local para
onde pular de volta. Subrrotinas podiam ler dados globais, e escrever sobre eles também. Era preciso improvisar
outras formas de controle de fluxo, com combinações de IF e GOTO —que, lembremos, permita pular para
qualquer linha do programa.

Após alguns anos programando com saltos e variáveis globais, lembro da batalha para reestruturar meu cérebro
para a "programação estruturada", quando aprendi Pascal. Agora precisava usar comandos de controle de fluxo
em torno de blocos de código que tinham um único ponto de entrada. Não podia mais saltar para qualquer
instrução que desejasse. Variáveis globais eram inevitáveis em BASIC, mas agora se tornaram tabu. Eu precisava
repensar o fluxo de dados e passar argumentos para funções explicitamente.

Meu próximo desafio foi aprender programação orientada a objetos. No fundo, programação orientada a objetos
é programação estruturada com mais restrições e polimorfismo. O ocultamento de informações força uma nova
perspectiva sobre onde os dados moram. Me lembro de mais de uma vez ficar frustrado por ter que refatorar
meu código, para que um método que estava escrevendo pudesse obter informações que estavam encapsuladas
em um objeto que aquele método não conseguia acessar.

Linguagens de programação funcionais acrescentam outras restrições, mas a imutabilidade é a mais difícil de
engolir, após décadas de programação imperativa e orientada a objetos. Após nos acostumarmos a tais restrições,
as vemos como bençãos. Elas fazem com que pensar sobre o código se torne muito mais simples.

A falta de restrições é o maior problema com o modelo de threads—e—travas de programação concorrente. Ao


resumir o capítulo 1 de Seven Concurrency Models in Seven Weeks, Paul Butcher escreveu:

“ Adifícil.
maior fraqueza da abordagem, entretanto, é que programação com threads—e—travas é
Pode ser fácil para um projetista de linguagens acrescentá-las a uma linguagem, mas
elas nos dão, a nós pobres programadores, muito pouca ajuda.

Alguns exemplos de comportamento sem restrições naquele modelo:

Threads podem compartilhar estruturas de dados mutáveis arbitrárias.


O agendador pode interromper uma thread em quase qualquer ponto, incluindo no meio de uma operação
simples, como a += 1 . Muito poucas operações são atômicas no nível das expressões do código-fonte.
Travas são, em geral, recomendações. Esse é um termo técnico, dizendo que você precisa lembrar de obter
explicitamente uma trava antes de atualizar uma estrutura de dados compartilhada. Se você esquecer de
obter a trava, nada impede seu código de bagunçar os dados enquanto outra thread, que obedientemente
detém a trava, está atualizando os mesmos dados.

Em comparação, considere algumas restrições impostas pelo modelo de atores, no qual a unidade de execução é
chamada de um actor ("ator"):[281]

Um ator pode manter um estado interno, mas não pode compartilhar esse estado com outros atores.
Atores só podem se comunicar enviando e recebendo mensagens.
Mensagens só contém cópias de dados, e não referências para dados mutáveis.
Um ator só processa uma mensagem de cada vez. Não há execução concorrente dentro de um único ator.
Claro, é possível adotar uma forma de programação ao estilo de ator para qualquer linguagem, seguindo essas
regras. Você também pode usar idiomas de programação orientada a objetos em C, e mesmo modelos de
programação estruturada em assembler. Mas fazer isso requer muita concordância e disciplina da parte de
qualquer um que mexa no código.

Gerenciar travas é desnecessário no modelo de atores, como implementado em Erlang e Elixir, onde todos os tipos
de dados são imutáveis.

Threads-e-travas não vão desaparecer. Eu só não acho que lidar com esse tipo de entidade básica seja um bom
uso de meu tempo quando escrevo aplicações—e não módulos do kernel, drivers de hardware, ou bancos de
dados.

Sempre me reservo o direito de mudar de opinião. Mas neste momento, estou convencido que o modelo de atores
é o modelo de programação concorrente mais sensato que existe. CSP (Communicating Sequential Processes)
também é sensato, mas sua implementação em Go deixa de fora algumas restrições. A ideia em CSP é que
corrotinas (ou goroutines em Go) trocam dados e se sincronizam usando filas (chamadas channels, "canais", em
Go). Mas Go também permite compartilhamento de memória e travas. Vi um livro sobre Go defende o uso de
memória compartilhada e travas em vez de canais—em nome do desempenho. É difícil abandonar velhos
hábitos.
20. Executores concorrentes
Quem fala mal de threads são tipicamente programadoras de sistemas, que tem em mente casos de uso que o
típico programador de aplicações nunca vai encontrar na vida.[...] Em 99% dos casos de uso que o programador
de aplicações vai encontrar, o modelo simples de gerar um monte de threads e coletar os resultados em uma fila é
tudo que se precisa saber.

Michele Simionato, profundo pensador do Python. Do post de Michele Simionato, "Threads, processes and
concurrency in Python: some thoughts" (_Threads, processos e concorrência em Python: algumas reflexões_)
(https://fpy.li/20-1), resumido assim: "Removendo exageros sobre a (não-)revolução dos múltiplos núcleos e alguns
comentários sensatos (oxalá) sobre threads e outras formas de concorrência."

Este capítulo se concentra nas classes do concurrent.futures.Executor , que encapsulam o modelo de "gerar um
monte de threads independentes e coletar os resultados em uma fila" descrito por Michele Simionato. Executores
concorrentes tornam o uso desse modelo quase trivial, não apenas com threads mas também com processos—úteis
para tarefas de processamento intensivo.

Também introduzo aqui o conceito de futures—objetos que representam a execução assíncrona de uma operação,
similares às promises do Javascript. Essa ideia básica é a fundação de concurrent.futures bem como do pacote
asyncio , assunto do Capítulo 21.

20.1. Novidades nesse capítulo


Renomeei este capítulo de "Concorrência com futures" para "Executores concorrentes", porque os executores são o
recurso de alto nível mais importante tratado aqui. Futures são objetos de baixo nível, tratados na seção Seção 20.2.3,
mas quase invisíveis no resto do capítulo.

Todos os exemplos de clientes HTTP agora usam a nova biblioteca HTTPX (https://fpy.li/httpx), que oferece APIs síncronas
e assíncronas.

A configuração para os experimentos na seção Seção 20.5 ficou mais simples, graças ao servidor de múltiplas threads
adicionado ao pacote http.server (https://fpy.li/20-2) no Python 3.7. Antes, a biblioteca padrão oferecia apenas o
BaseHttpServer de thread única, que não era adequado para experiências com clientes concorrentes, então na
primeira edição desse livro precisei usar um servidor externo.

A seção Seção 20.3 agora demonstra como um executor simplifica o código que vimos na Seção 19.6.3.

Por fim, movi a maior parte da teoria para o novo Capítulo 19.

20.2. Downloads concorrentes da web


A concorrência é essencial para uma comunicação eficiente via rede: em vez de esperar de braços cruzados por
respostas de máquinas remotas, a aplicação deveria fazer alguma outra coisa até a resposta chegar.[282]

Para demonstrar com código, escrevi três programas simples que baixam da web imagens de 20 bandeiras de países. O
primeiro, flags.py, roda sequencialmente: ele só requisita a imagem seguinte quando a anterior foi baixada e salva
localmente. Os outros dois scripts fazem downloads concorrentes: eles requisitam várias imagens quase ao mesmo
tempo, e as salvam conforme chegam. O script flags_threadpool.py usa o pacote concurrent.futures , enquanto
flags_asyncio.py usa asyncio .

O Exemplo 1 mostra o resultado da execução dos três scripts, três vezes cada um.
Os scripts baixam imagens de fluentpython.com (https://fluentpython.com), que usa uma CDN (Content Delivery Network,
Rede de Fornecimento de Conteúdo), então você pode notar os resultados mais lentos nas primeiras passagens. Os
resultados no Exemplo 1 foram obtidos após várias execuções, então o cache da CDN estava carregado.

Exemplo 1. Três execuções típicas dos scripts flags.py, flags_threadpool.py, e flags_asyncio.py

TEXT
$ python3 flags.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN (1)
20 flags downloaded in 7.26s (2)
$ python3 flags.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 flags downloaded in 7.20s
$ python3 flags.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 flags downloaded in 7.09s
$ python3 flags_threadpool.py
DE BD CN JP ID EG NG BR RU CD IR MX US PH FR PK VN IN ET TR
20 flags downloaded in 1.37s (3)
$ python3 flags_threadpool.py
EG BR FR IN BD JP DE RU PK PH CD MX ID US NG TR CN VN ET IR
20 flags downloaded in 1.60s
$ python3 flags_threadpool.py
BD DE EG CN ID RU IN VN ET MX FR CD NG US JP TR PK BR IR PH
20 flags downloaded in 1.22s
$ python3 flags_asyncio.py (4)
BD BR IN ID TR DE CN US IR PK PH FR RU NG VN ET MX EG JP CD
20 flags downloaded in 1.36s
$ python3 flags_asyncio.py
RU CN BR IN FR BD TR EG VN IR PH CD ET ID NG DE JP PK MX US
20 flags downloaded in 1.27s
$ python3 flags_asyncio.py
RU IN ID DE BR VN PK MX US IR ET EG NG BD FR CN JP PH CD TR (5)
20 flags downloaded in 1.42s

1. A saída de cada execução começa com os códigos dos países de cada bandeira a medida que as imagens são
baixadas, e termina com uma mensagem mostrando o tempo decorrido.
2. flags.py precisou em média de 7,18s para baixar 20 imagens.
3. A média para flags_threadpool.py foi 1,40s.
4. Já flags_asyncio.py, obteve um tempo médio de 1,35s.
5. Observe a ordem do códigos de país: nos scripts concorrentes, as imagens foram baixadas em um ordem diferente
a cada vez.

A diferença de desempenho entre os scripts concorrentes não é significativa, mas ambos são mais de cinco vezes mais
rápidos que o script sequencial—e isto apenas para a pequena tarefa de baixar 20 arquivos, cada um com uns poucos
kilobytes. Se você escalar a tarefa para centenas de downloads, os scripts concorrentes podem superar o código
sequencial por um fator de 20 ou mais.

Ao testar clientes HTTP concorrentes usando servidores web públicos, você pode inadvertidamente
lançar um ataque de negação de serviço (DoS, Denial of Service attack), ou se tornar suspeito de estar
⚠️ AVISO tentando um ataque. No caso do Exemplo 1 não há problema, pois aqueles scripts estão codificados
para realizar apenas 20 requisições. Mais adiante nesse capítulo usaremos o pacote http.server
do Python para executar nossos testes localmente.
Vamos agora estudar as implementações de dois dos scripts testados no Exemplo 1: flags.py e flags_threadpool.py. Vou
deixar o terceiro, flags_asyncio.py, para o Capítulo 21, mas queria demonstrar os três juntos para fazer duas
observações:

1. Independente dos elementos de concorrência que você use—threads ou corrotinas—haverá um ganho enorme de
desempenho sobre código sequencial em operações de E/S de rede, se o script for escrito corretamente.
2. Para clientes HTTP que podem controlar quantas requisições eles fazem, não há diferenças significativas de
desempenho entre threads e corrotinas.[283]

Vamos ver o código.

20.2.1. Um script de download sequencial


O Exemplo 2 contém a implementação de flags.py, o primeiro script que rodamos no Exemplo 1. Não é muito
interessante, mas vamos reutilizar a maior parte do código e das configurações para implementar os scripts
concorrentes, então ele merece alguma atenção.

Por clareza, não há qualquer tipo de tratamento de erro no Exemplo 2. Vamos lidar come exceções
✒️ NOTA mais tarde, mas aqui quero me concentrar na estrutura básica do código, para facilitar a
comparação deste script com os scripts que usam concorrência.

Exemplo 2. flags.py: script de download sequencial; algumas funções serão reutilizadas pelos outros scripts
PY
import time
from pathlib import Path
from typing import Callable

import httpx # (1)

POP20_CC = ('CN IN US ID BR PK NG BD RU JP '


'MX PH VN ET EG DE IR TR CD FR').split() # (2)

BASE_URL = 'https://www.fluentpython.com/data/flags' # (3)


DEST_DIR = Path('downloaded') # (4)

def save_flag(img: bytes, filename: str) -> None: # (5)


(DEST_DIR / filename).write_bytes(img)

def get_flag(cc: str) -> bytes: # (6)


url = f'{BASE_URL}/{cc}/{cc}.gif'.lower()
resp = httpx.get(url, timeout=6.1, # (7)
follow_redirects=True) # (8)
resp.raise_for_status() # (9)
return resp.content

def download_many(cc_list: list[str]) -> int: # (10)


for cc in sorted(cc_list): # (11)
image = get_flag(cc)
save_flag(image, f'{cc}.gif')
print(cc, end=' ', flush=True) # (12)
return len(cc_list)

def main(downloader: Callable[[list[str]], int]) -> None: # (13)


DEST_DIR.mkdir(exist_ok=True) # (14)
t0 = time.perf_counter() # (15)
count = downloader(POP20_CC)
elapsed = time.perf_counter() - t0
print(f'\n{count} downloads in {elapsed:.2f}s')

if __name__ == '__main__':
main(download_many) # (16)

1. Importa a biblioteca httpx . Ela não é parte da biblioteca padrão. Assim, por convenção, a importação aparece
após os módulos da biblioteca padrão e uma linha em branco.
2. Lista do código de país ISO 3166 para os 20 países mais populosos, em ordem decrescente de população.
3. O diretório com as imagens das bandeiras.[284]
4. Diretório local onde as imagens são salvas.
5. Salva os bytes de img para filename no DEST_DIR .

6. Dado um código de país, constrói a URL e baixa a imagem, retornando o conteúdo binário da resposta.
7. É uma boa prática adicionar um timeout razoável para operações de rede, para evitar ficar bloqueado sem motivo
por vários minutos.
8. Por default, o HTTPX não segue redirecionamentos.[285]
9. Não há tratamento de erros nesse script, mas esse método lança uma exceção se o status do HTTP não está na faixa
2XX—algo mutio recomendado para evitar falhas silenciosas.
10. download_many é a função chave para comparar com as implementações concorrentes.
11. Percorre a lista de códigos de país em ordem alfabética, para facilitar a confirmação de que a ordem é preservada
na saída; retorna o número de códigos de país baixados.
12. Mostra um código de país por vez na mesma linha, para vermos o progresso a cada download. O argumento end='
' substitui a costumeira quebra no final de cada linha escrita com um espaço, assim todos os códigos de país
aparecem progressivamente na mesma linha. O argumento flush=True é necessário porque, por default, a saída
do Python usa um buffer de linha, o que significa que o Python só mostraria os caracteres enviados após uma
quebra de linha.
13. main precisa ser chamada com a função que fará os downloads; dessa forma podemos usar main como uma
função de biblioteca com outras implementações de download_many nos exemplos de threadpool e ascyncio .
14. Cria o DEST_DIR se necessário; não acusa erro se o diretório existir.
15. Mede e apresenta o tempo decorrido após rodar a função downloader .

16. Chama main com a função download_many .

A biblioteca HTTPX (https://fpy.li/httpx) é inspirada no pacote pythônico requests (https://fpy.li/20-5), mas


foi desenvolvida sobre bases mais modernas. Especialmente, HTTPX tem APIs síncronas e
👉 DICA assíncronas, então podemos usá-la em todos os exemplos de clientes HTTP nesse capítulo e no
próximo. A biblioteca padrão do Python contém o módulo urllib.request , mas sua API é
exclusivamente síncrona, e não é muito amigável.

Não há mesmo nada de novo em flags.py. Ele serve de base para comparação com outros scripts, e o usei como uma
biblioteca, para evitar código redundante ao implementar aqueles scripts. Vamos ver agora uma reimplementação
usando concurrent.futures .

20.2.2. Download com concurrent.futures


Os principais recursos do pacote concurrent.futures são as classes ThreadPoolExecutor e
ProcessPoolExecutor , que implementam uma API para submissão de callables ("chamáveis") para execução em
diferentes threads ou processos, respectivamente. As classes gerenciam de forma transparente um grupo de threads ou
processos de trabalho, e filas para distribuição de tarefas e coleta de resultados. Mas a interface é de um nível muito
alto, e não precisamos saber nada sobre qualquer desses detalhes para um caso de uso simples como nossos downloads
de bandeiras.

O Exemplo 3 mostra a forma mais fácil de implementar os downloads de forma concorrente, usando o método
ThreadPoolExecutor.map .

Exemplo 3. flags_threadpool.py: script de download com threads, usando futures.ThreadPoolExecutor

PY
from concurrent import futures

from flags import save_flag, get_flag, main # (1)

def download_one(cc: str): # (2)


image = get_flag(cc)
save_flag(image, f'{cc}.gif')
print(cc, end=' ', flush=True)
return cc

def download_many(cc_list: list[str]) -> int:


with futures.ThreadPoolExecutor() as executor: # (3)
res = executor.map(download_one, sorted(cc_list)) # (4)

return len(list(res)) # (5)

if __name__ == '__main__':
main(download_many) # (6)
1. Reutiliza algumas funções do módulo flags (Exemplo 2).
2. Função para baixar uma única imagem; isso é o que cada thread de trabalho vai executar.
3. Instancia o como um gerenciador de contexto; o método executor​.__exit__ vai chamar
ThreadPoolExecutor
executor.shutdown(wait=True) , que vai bloquear até que todas as threads terminem de rodar.

4. O método map é similar ao map embutido, exceto que a função download_one será chamada de forma
concorrente por múltiplas threads; ele retorna um gerador que você pode iterar para recuperar o valor retornado
por cada chamada da função—nesse caso, cada chamada a download_one vai retornar um código de país.
5. Retorna o número de resultados obtidos. Se alguma das chamadas das threads levantar uma exceção, aquela
exceção será levantada aqui quando a chamada implícita next() , dentro do construtor de list , tentar recuperar
o valor de retorno correspondente, no iterador retornado por executor.map .
6. Chama a função main do módulo flags , passando a versão concorrente de download_many .

Observe que a função download_one do Exemplo 3 é essencialmente o corpo do loop for na função download_many
do Exemplo 2. Essa é uma refatoração comum quando se está escrevendo código concorrente: transformar o corpo de
um loop for sequencial em uma função a ser chamada de modo concorrente.

O Exemplo 3 é muito curto porque pude reutilizar a maior parte das funções do script sequencial
👉 DICA flags.py. Uma das melhores características do concurrent.futures é tornar simples a execução
concorrente de código sequencial legado.

O construtor de ThreadPoolExecutor recebe muitos argumentos além dos mostrados aqui, mas o primeiro e mais
importante é max_workers , definindo o número máximo de threads de trabalho a serem executadas. Quando
max_workers é None (o default), ThreadPool​
Executor decide seu valor usando, desde o Python 3.8, a seguinte
expressão:

PY
max_workers = min(32, os.cpu_count() + 4)

A justificativa é apresentada na documentação de ThreadPoolExecutor


(https://docs.python.org/pt-br/3.10/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor):

“ Esse valor default conserva pelo menos 5 threads de trabalho para tarefas de E/S. Ele utiliza no
máximo 32 núcleos da CPU para tarefas de processamento, o quê libera a GIL. E ele evita usar
recursos muitos grandes implicitamente em máquinas com muitos núcleos.

ThreadPoolExecutor agora também reutiliza threads de trabalho inativas antes iniciar [novas]
threads de trabalho de max_workers .

Concluindo: o valor default calculado de max_workers é razoável, e ThreadPoolExecutor evita iniciar novas threads
de trabalho desnecessariamente. Entender a lógica por trás de max_workers pode ajudar a decidir quando e como
estabelecer o valor em seu código.

A biblioteca se chama concurrency.futures, mas não há qualquer future à vista no Exemplo 3, então você pode estar se
perguntando onde estão eles. A próxima seção explica isso.

20.2.3. Onde estão os futures?


Os futures (literalmente "futuros") são componentes centrais de concurrent.futures e de asyncio , mas como
usuários dessas bibliotecas, raramente os vemos. O Exemplo 3 depende de futures por trás do palco, mas o código
apresentado não lida diretamente com objetos dessa classe. Essa seção apresenta uma visão geral dos futures, com um
exemplo mostrando-os em ação.

Desde o Python 3.4, há duas classes chamadas Future na biblioteca padrão: concurrent.futures.Future e
asyncio.Future . Elas tem o mesmo propósito: uma instância de qualquer das classes Future representa um
processamento adiado, que pode ou não ter sido completado. Isso é algo similar à classe Deferred no Twisted, a classe
Future no Tornado, e Promisse no Javascript moderno.

Os futures encapsulam operações pendentes de forma que possamos colocá-los em filas, verificar se terminaram, e
recuperar resultados (ou exceções) quando eles ficam disponíveis.

Uma coisa importante de saber sobre futures é eu e você, não devemos criá-los: eles são feitos para serem instanciados
exclusivamente pela framework de concorrência, seja ela a concurrent.futures ou a asyncio . O motivo é que um
Future representa algo que vai ser executado em algum momento, portanto precisa ser agendado para rodar, e quem
agenda tarefas é a framework.

Especificamente, instâncias concurrent.futures.Future são criadas apenas como resultado da submissão de um


objeto invocável (callable) para execução a uma subclasse de concurrent.futures.Executor . Por exemplo, o método
Executor.submit() recebe um invocável, agenda sua execução e retorna um Future .

O código da aplicação não deve mudar o estado de um future: a framework de concorrência muda o estado de um
future quando o processamento que ele representa termina, e não temos como controlar quando isso acontece.

Ambos os tipos de Future tem um método .done() não-bloqueante, que retorna um Boolean informando se o
invocável encapsulado por aquele future foi ou não executado. Entretanto, em vez de perguntar repetidamente se
um future terminou, o código cliente em geral pede para ser notificado. Por isso as duas classes Future tem um
método .add_done_callback() : você passa a ele um invocável e aquele invocável será invocado com o future como
único argumento, quando o future tiver terminado. Observe que aquele invocável de callback será invocado na mesma
thread ou processo de trabalho que rodou a função encapsulada no future.

Há também um método .result() , que funciona igual nas duas classes quando a execução do future termina: ele
retorna o resultado do invocável, ou relança qualquer exceção que possa ter aparecido quando o invocável foi
executado. Entretanto, quando o future não terminou, o comportamento do método result é bem diferente entre os
dois sabores de Future . Em uma instância de concurrency.futures.Future , invocar f.result() vai bloquear a
thread que chamou até o resultado ficar pronto. Um argumento timeout opcional pode ser passado, e se o future não
tiver terminado após aquele tempo, o método result gera um TimeoutError . O método asyncio.Future.result
não suporta um timeout, e await é a forma preferencial de obter o resultado de futures no asyncio —mas await não
funciona com instâncias de concurrency.futures.Future .

Várias funções em ambas as bibliotecas retornam futures; outras os usam em sua implementação de uma forma
transparente para o usuário. Um exemplo desse último caso é o Executor.map , que vimos no Exemplo 3: ele retorna
um iterador no qual __next__ chama o método result de cada future, então recebemos os resultados dos futures,
mas não os futures em si.

Para ver uma experiência prática com os futures, podemos reescrever o Exemplo 3 para usar a função
concurrent.futures.as_completed
(https://docs.python.org/pt-br/3/library/concurrent.futures.html#concurrent.futures.as_completed), que recebe um iterável de
futures e retorna um iterador que entrega futures quando cada um encerra sua execução.
Usar futures.as_completed exige mudanças apenas na função download_many . A chamada ao executor.map , de
alto nível, é substituída por dois loops for : um para criar e agendar os futures, o outro para recuperar seus resultados.
Já que estamos aqui, vamos acrescentar algumas chamadas a print para mostrar cada future antes e depois do
término de sua execução. O Exemplo 4 mostra o código da nova função download_many . O código de download_many
aumentou de 5 para 17 linhas, mas agora podemos inspecionar os misteriosos futures. As outras funções são idênticas
as do Exemplo 3.

Exemplo 4. flags_threadpool_futures.py: substitui executor.map por executor.submit e futures.as_completed na


função download_many

PY
def download_many(cc_list: list[str]) -> int:
cc_list = cc_list[:5] # (1)
with futures.ThreadPoolExecutor(max_workers=3) as executor: # (2)
to_do: list[futures.Future] = []
for cc in sorted(cc_list): # (3)
future = executor.submit(download_one, cc) # (4)
to_do.append(future) # (5)
print(f'Scheduled for {cc}: {future}') # (6)

for count, future in enumerate(futures.as_completed(to_do), 1): # (7)


res: str = future.result() # (8)
print(f'{future} result: {res!r}') # (9)

return count

1. Para essa demonstração, usa apenas os cinco países mais populosos.


2. Configura max_workers para 3 , para podermos ver os futures pendentes na saída.

3. Itera pelos códigos de país em ordem alfabética, para deixar claro que os resultados vão aparecer fora de ordem.
4. executor.submit agenda o invocável a ser executado, e retorna um future representando essa operação
pendente.
5. Armazena cada future , para podermos recuperá-los mais tarde com as_completed .

6. Mostra uma mensagem com o código do país e seu respectivo future .

7. as_completed entrega futures conforme eles terminam.


8. Recupera o resultado desse future .

9. Mostra o future e seu resultado.

Observe que a chamada a future.result() nunca bloqueará a thread nesse exemplo, pois future está vindo de
as_completed . O Exemplo 5 mostra a saída de uma execução do Exemplo 4.

Exemplo 5. Saída de flags_threadpool_futures.py


TEXT
$ python3 flags_threadpool_futures.py
Scheduled for BR: <Future at 0x100791518 state=running> (1)
Scheduled for CN: <Future at 0x100791710 state=running>
Scheduled for ID: <Future at 0x100791a90 state=running>
Scheduled for IN: <Future at 0x101807080 state=pending> (2)
Scheduled for US: <Future at 0x101807128 state=pending>
CN <Future at 0x100791710 state=finished returned str> result: 'CN' (3)
BR ID <Future at 0x100791518 state=finished returned str> result: 'BR' (4)
<Future at 0x100791a90 state=finished returned str> result: 'ID'
IN <Future at 0x101807080 state=finished returned str> result: 'IN'
US <Future at 0x101807128 state=finished returned str> result: 'US'

5 downloads in 0.70s

1. Os futures são agendados em ordem alfabética; o repr() de um future mostra seu estado: os três primeiros estão
running , pois há três threads de trabalho.

2. Os dois últimos futures estão pending ; esperando pelas threads de trabalho.

3. O primeiro CN aqui é a saída de download_one em uma thread de trabalho; o resto da linha é a saída de
download_many .

4. Aqui, duas threads retornam códigos antes que download_many na thread principal possa mostrar o resultado da
primeira thread.

Recomendo experimentar com flags_threadpool_futures.py. Se você o rodar várias vezes, vai ver a

👉 DICA ordem dos resultados variar. Aumentar max_workers para 5 vai aumentar a variação na ordem
dos resultados. Diminuindo aquele valor para 1 fará o script rodar de forma sequencial, e a ordem
dos resultados será sempre a ordem das chamadas a submit .

Vimos duas variantes do script de download usando concurrent.futures : uma no Exemplo 3 com
ThreadPoolExecutor.map e uma no Exemplo 4 com futures.as_completed . Se você está curioso sobre o código de
flags_asyncio.py, pode espiar o Exemplo 3 no Capítulo 21, onde ele é explicado.

Agora vamos dar uma olhada rápida em um modo simples de desviar da GIL para tarefas de uso intensivo de CPU,
usando concurrent.futures .

20.3. Iniciando processos com concurrent.futures


A página de documentação de concurrent.futures (https://docs.python.org/pt-br/3/library/concurrent.futures.html) tem por
subtítulo "Iniciando tarefas em paralelo." O pacote permite computação paralela em máquinas multi-núcleo porque
suporta a distribuição de trabalho entre múltiplos processos Python usando a classe ProcessPool​Executor .

Ambas as classes, ProcessPoolExecutor e ThreadPoolExecutor implementam a interface Executor


(https://fpy.li/20-9), então é fácil mudar de uma solução baseada em threads para uma baseada em processos usando
concurrent.futures .

Não há nenhuma vantagem em usar um ProcessPoolExecutor no exemplo de download de bandeiras ou em


qualquer tarefa concentrada em E/S. É fácil comprovar isso; apenas modifique as seguintes linhas no Exemplo 3:

PYTHON3
def download_many(cc_list: list[str]) -> int:
with futures.ThreadPoolExecutor() as executor:

para:
PYTHON3
def download_many(cc_list: list[str]) -> int:
with futures.ProcessPoolExecutor() as executor:

O construtor de ProcessPoolExecutor também tem um parâmetro max_workers , que por default é None . Nesse
caso, o executor limita o número de processos de trabalho ao número resultante de uma chamada a os.cpu_count() .

Processos usam mais memória e demoram mais para iniciar que threads, então o real valor de of
ProcessPoolExecutor é em tarefas de uso intensivo da CPU. Vamos voltar ao exemplo de teste de verificação de
números primos deSeção 19.6, e reescrevê-lo com concurrent.futures .

20.3.1. Verificador de primos multinúcleo redux


Na seção Seção 19.6.3, estudamos procs.py, um script que verificava se alguns números grandes eram primos usando
multiprocessing . No Exemplo 6 resolvemos o mesmo problema com o programa proc_pool.py, usando um
ProcessPool​ Executor . Do primeiro import até a chamada a main() no final, procs.py tem 43 linhas de código não-
vazias, e proc_pool.py tem 31—28% mais curto.

Exemplo 6. proc_pool.py: procs.py reescrito com ProcessPoolExecutor

PY
import sys
from concurrent import futures # (1)
from time import perf_counter
from typing import NamedTuple

from primes import is_prime, NUMBERS

class PrimeResult(NamedTuple): # (2)


n: int
flag: bool
elapsed: float

def check(n: int) -> PrimeResult:


t0 = perf_counter()
res = is_prime(n)
return PrimeResult(n, res, perf_counter() - t0)

def main() -> None:


if len(sys.argv) < 2:
workers = None # (3)
else:
workers = int(sys.argv[1])

executor = futures.ProcessPoolExecutor(workers) # (4)


actual_workers = executor._max_workers # type: ignore # (5)

print(f'Checking {len(NUMBERS)} numbers with {actual_workers} processes:')

t0 = perf_counter()

numbers = sorted(NUMBERS, reverse=True) # (6)


with executor: # (7)
for n, prime, elapsed in executor.map(check, numbers): # (8)
label = 'P' if prime else ' '
print(f'{n:16} {label} {elapsed:9.6f}s')

time = perf_counter() - t0
print(f'Total time: {time:.2f}s')

if __name__ == '__main__':
main()
1. Não há necessidade de importar multiprocessing , SimpleQueue etc.; concurrent.futures esconde tudo isso.
2. A tupla PrimeResult e a função check são as mesmas que vimos em procs.py, mas não precisamos mais das filas
nem da função worker .
3. Em vez de decidirmos por nós mesmos quantos processos de trabalho serão usados se um argumento não for
passado na linha de comando, atribuímos None a workers e deixamos o ProcessPoolExecutor decidir.
4. Aqui criei o ProcessPoolExecutor antes do bloco with em ➐, para poder mostrar o número real de processos na
próxima linha.
5. max_workers é um atributo de instância não-documentado de um ProcessPoolExecutor . Decidi usá-lo para
mostrar o número de processos de trabalho criados quando a variável workers é None . O Mypy corretamente
reclama quando eu acesso esse atributo, então coloquei o comentário type: ignore para silenciar a reclamação.
6. Ordena os números a serem verificados em ordem descendente. Isso vai mostrar a diferença no comportamento de
proc_pool.py quando comparado a procs.py. Veja a explicação após esse exemplo.
7. Usa o executor como um gerenciador de contexto.
8. A chamada a executor.map retorna as instâncias de PrimeResult retornadas por check na mesma ordem dos
argumentos numbers .

Se você rodar o Exemplo 6, verá os resultados aparecente em ordem rigorosamente descendente, como mostrado no
Exemplo 7. Por outro lado, a ordem da saída de procs.py (mostrado em Seção 19.6.1) é severamente influenciado pela
dificuldade em verificar se cada número é ou não primo. Por exemplo, procs.py mostra o resultado para
7777777777777777 próximo ao topo, pois ele tem um divisor pequeno, 7, então is_prime rapidamente determina que
ele não é primo.

Já o de 7777777536340681 is 881917092, então is_prime vai demorar muito mais para determinar que esse é um
número composto, e ainda mais para descobrir que 7777777777777753 é primo—assim, ambos esses números
aparecem próximo do final da saída de procs.py.

Ao rodar proc_pool.py, podemos observar não apenas a ordem descendente dos resultados, mas também que o
programa parece emperrar após mostrar o resultado para 9999999999999999.

Exemplo 7. Saída de proc_pool.py


TEXT
$ ./proc_pool.py
Checking 20 numbers with 12 processes:
9999999999999999 0.000024s # (1)
9999999999999917 P 9.500677s # (2)
7777777777777777 0.000022s # (3)
7777777777777753 P 8.976933s
7777777536340681 8.896149s
6666667141414921 8.537621s
6666666666666719 P 8.548641s
6666666666666666 0.000002s
5555555555555555 0.000017s
5555555555555503 P 8.214086s
5555553133149889 8.067247s
4444444488888889 7.546234s
4444444444444444 0.000002s
4444444444444423 P 7.622370s
3333335652092209 6.724649s
3333333333333333 0.000018s
3333333333333301 P 6.655039s
299593572317531 P 2.072723s
142702110479723 P 1.461840s
2 P 0.000001s
Total time: 9.65s

1. Essa linha aparece muito rápido.


2. Essa linha demora mais de 9,5s para aparecer.
3. Todas as linhas restantes aparecem quase imediatamente.

Aqui está o motivo para aquele comportamento de proc_pool.py:

Como mencionado antes, executor.map(check, numbers) retorna o resultado na mesma ordem em que numbers
é enviado.
Por default, proc_pool.py usa um número de processos de trabalho igual ao número de CPUs—isso é o que
ProcessPoolExecutor faz quando max_workers é None . Nesse laptop são então 12 processos.

Como estamos submetendo numbers em ordem descendente, o primeiro é 9999999999999999; com 9 como divisor,
ele retorna rapidamente.
O segundo número é 9999999999999917, o maior número primo na amostra. Ele vai demorar mais que todos os
outros para verificar.
Enquanto isso, os 11 processos restantes estarão verificando outros números, que são ou primos ou compostos com
fatores grandes ou compostos com fatores muito pequenos.
Quando o processo de trabalho encarregado de 9999999999999917 finalmente determina que ele é primo, todos os
outros processos já completaram suas últimas tarefas, então os resultados aparecem logo depois.

Apesar do progresso de proc_pool.py não ser tão visível quanto o de procs.py, o tempo total de
✒️ NOTA execução, para o mesmo número de processo de trabalho e de núcleos de CPU, é praticamente
idêntico, como retratado em Figura 2.

Entender como programas concorrentes se comportam não é um processo direto, então aqui está um segundo
experimento que pode ajudar a visualizar o funcionamento de Executor.map .
20.4. Experimentando com Executor.map
Vamos investigar Executor.map , agora usando um ThreadPoolExecutor com três threads de trabalho rodando cinco
chamáveis que retornam mensagens marcadas com data/hora. O código está no Exemplo 8, o resultado no Exemplo 9.

Exemplo 8. demo_executor_map.py: Uma demonstração simples do método map de ThreadPoolExecutor

PY
from time import sleep, strftime
from concurrent import futures

def display(*args): # (1)


print(strftime('[%H:%M:%S]'), end=' ')
print(*args)

def loiter(n): # (2)


msg = '{}loiter({}): doing nothing for {}s...'
display(msg.format('\t'*n, n, n))
sleep(n)
msg = '{}loiter({}): done.'
display(msg.format('\t'*n, n))
return n * 10 # (3)

def main():
display('Script starting.')
executor = futures.ThreadPoolExecutor(max_workers=3) # (4)
results = executor.map(loiter, range(5)) # (5)
display('results:', results) # (6)
display('Waiting for individual results:')
for i, result in enumerate(results): # (7)
display(f'result {i}: {result}')

if __name__ == '__main__':
main()

1. Essa função simplesmente exibe quaisquer argumentos recebidos, com momento da execução no formato
[HH:MM:SS] .

2. loiter não faz nada além mostrar uma mensagem quanto inicia, dormir por n segundos, e mostrar uma
mensagem quando termina; são usadas tabulações para indentar as mensagens de acordo com o valor de n .
3. loiter retorna n * 10 , então podemos ver como coletar resultados.

4. Cria um ThreadPoolExecutor com três threads.


5. Submete cinco tarefas para o executor . Já que há apenas três threads, apenas três daquelas tarefas vão iniciar
imediatamente: a chamadas loiter(0) , loiter(1) , e loiter(2) ; essa é uma chamada não-bloqueante.
6. Mostra imediatamente o results da invocação de executor.map : é um gerador, como se vê na saída no Exemplo
9.
7. A chamada enumerate no loop for vai invocar implicitamente next(results) , que por sua vez vai invocar
f.result() no future (interno) f , representando a primeira chamada, loiter(0) . O método result vai
bloquear a thread até que o future termine, portanto cada iteração nesse loop vai esperar até que o próximo
resultado esteja disponível.

Encorajo você a rodar o Exemplo 8 e ver o resultado sendo atualizado de forma incremental. Quando for fazer isso,
mexa no argumento max_workers do ThreadPool​Executor e com a função range , que produz os argumentos para
a chamada a executor.map —ou os substitua por listas com valores escolhidos, para criar intervalos diferentes.

O Exemplo 9 mostra uma execução típica do Exemplo 8.


Exemplo 9. Amostra da execução de demo_executor_map.py, do Exemplo 8

TEXT
$ python3 demo_executor_map.py
[15:56:50] Script starting. (1)
[15:56:50] loiter(0): doing nothing for 0s... (2)
[15:56:50] loiter(0): done.
[15:56:50] loiter(1): doing nothing for 1s... (3)
[15:56:50] loiter(2): doing nothing for 2s...
[15:56:50] results: <generator object result_iterator at 0x106517168> (4)
[15:56:50] loiter(3): doing nothing for 3s... (5)
[15:56:50] Waiting for individual results:
[15:56:50] result 0: 0 (6)
[15:56:51] loiter(1): done. (7)
[15:56:51] loiter(4): doing nothing for 4s...
[15:56:51] result 1: 10 (8)
[15:56:52] loiter(2): done. (9)
[15:56:52] result 2: 20
[15:56:53] loiter(3): done.
[15:56:53] result 3: 30
[15:56:55] loiter(4): done. (10)
[15:56:55] result 4: 40

1. Essa execução começou em 15:56:50.


2. A primeira thread executa loiter(0) , então vai dormir por 0s e retornar antes mesmo da segunda thread ter
chance de começar, mas YMMV.[286]
3. loiter(1) e loiter(2) começam imediatamente (como o pool de threads tem três threads de trabalho, é
possível rodar três funções de forma concorrente).
4. Isso mostra que o results retornado por executor.map é um gerador: nada até aqui é bloqueante, independente
do número de tarefas e do valor de max_workers .
5. Como loiter(0) terminou, a primeira thread de trabalho está disponível para iniciar a quarta thread para
loiter(3) .

6. Aqui é ponto a execução pode ser bloqueada, dependendo dos parâmetros passados nas chamadas a loiter : o
método __next__ do gerador results precisa esperar até o primeiro future estar completo. Neste caso, ele não
vai bloquear porque a chamada a loiter(0) terminou antes desse loop iniciar. Observe que tudo até aqui
aconteceu dentro do mesmo segundo: 15:56:50.
7. loiter(1) termina um segundo depois, em 15:56:51. A thread está livre para iniciar loiter(4) .

8. O resultado de loiter(1) é exibido: 10 . Agora o loop for ficará bloqueado, esperando o resultado de
loiter(2) .

9. O padrão se repete: loiter(2) terminou, seu resultado é exibido; o mesmo ocorre com loiter(3) .

10. Há um intervalo de 2s até loiter(4) terminar, porque ela começou em 15:56:51 e não fez nada por 4s.

A função Executor.map é fácil de usar, mas muitas vezes é preferível obter os resultados assim que estejam prontos,
independente da ordem em que foram submetidos. Para fazer isso, precisamos de uma combinação do método
Executor.submit e da função futures.as_completed como vimos no Exemplo 4. Vamos voltar a essa técnica na
seção Seção 20.5.2.
A combinação de Executor.submit e futures.as_completed é mais flexível que executor.map ,
pois você pode submit chamáveis e argumentos diferentes. Já executor.map é projetado para
👉 DICA rodar o mesmo invocável com argumentos diferentes. Além disso, o conjunto de futures que você
passa para futures.as_completed pode vir de mais de um executor—talvez alguns tenham sido
criados por uma instância de ThreadPoolExecutor enquanto outros vem de um
ProcessPoolExecutor .

Na próxima seção vamos retomar os exemplos de download de bandeiras com novos requerimentos que vão nos
obrigar a iterar sobre os resultados de futures.as_completed em vez de usar executor.map .

20.5. Download com exibição do progresso e tratamento de erro


Como mencionado, os scripts em Seção 20.2 não tem tratamento de erros, para torná-los mais fáceis de ler e para
comparar a estrutura das três abordagens: sequencial, com threads e assíncrona.

Para testar o tratamento de uma variedade de condições de erro, criei os exemplos flags2 :

flags2_common.py
Este módulo contém as funções e configurações comuns, usadas por todos os exemplos flags2 , incluindo a função
main , que cuida da interpretação da linha de comando, da medição de tempo e de mostrar os resultados. Isso é
código de apoio, sem relevância direta para o assunto desse capítulo, então não vou incluir o código-fonte aqui, mas
você pode vê-lo no fluentpython/example-code-2e (https://fpy.li/code) repositório: 20-executors/getflags/flags2_common.py
(https://fpy.li/20-10).

flags2_sequential.py
Um cliente HTTP sequencial com tratamento de erro correto e a exibição de uma barra de progresso. Sua função
download_one também é usada por flags2_threadpool.py .

flags2_threadpool.py
Cliente HTTP concorrente, baseado em futures.ThreadPoolExecutor , para demonstrar o tratamento de erros e a
integração da barra de progresso.

flags2_asyncio.py
Mesma funcionalidade do exemplo anterior, mas implementado com asyncio e httpx . Isso será tratado na seção
Seção 21.7, no capítulo Capítulo 21.

Tenha cuidado ao testar clientes concorrentes


Ao testar clientes HTTP concorrentes em servidores web públicos, você pode gerar muitas
⚠️ AVISO requisições por segundo, e é assim que ataques de negação de serviço (DoS, denial-of-service) são
feitos. Controle cuidadosamente seus clientes quando for usar servidores públicos. Para testar,
configure um servidor HTTP local. Veja o Configurando os servidores de teste para instruções.

A característica mais visível dos exemplos flags2 é sua barra de progresso animada em modo texto, implementada
com o pacote tqdm (https://fpy.li/20-11). Publiquei um vídeo de 108s no YouTube (https://fpy.li/20-12) mostrando a barra de
progresso e comparando a velocidade dos três scripts flags2 . No vídeo, começo com o download sequencial, mas
interrompo a execução após 32s. O script demoraria mais de 5 minutos para acessar 676 URLs e baixar 194 bandeiras.
Então rodo o script usando threads e o que usa asyncio , três vezes cada um, e todas as vezes eles completam a tarefa
em 6s ou menos (isto é mais de 60 vezes mais rápido). A Figura 1 mostra duas capturas de tela: durante e após a
execução de flags2_threadpool.py.
Figura 1. Acima, à esquerda: flags2_threadpool.py rodando com a barra de progresso em tempo real gerada pelo tqdm;
Abaixo, à direita: mesma janela do terminal após o script terminar de rodar.

O exemplo de uso mais simples do tqdm aparece em um .gif animado, no README.md (https://fpy.li/20-13) do projeto. Se
você digitar o código abaixo no console do Python após instalar o pacote tqdm, uma barra de progresso animada
aparecerá no lugar onde está o comentário:

PYCON
>>> import time
>>> from tqdm import tqdm
>>> for i in tqdm(range(1000)):
... time.sleep(.01)
...
>>> # -> progress bar will appear here <-

Além do efeito elegante, o tqdm também é conceitualmente interessante: ele consome qualquer iterável, e produz um
iterador que, enquanto é consumido, mostra a barra de progresso e estima o tempo restante para completar todas as
iterações. Para calcular aquela estimativa, o tqdm precisa receber um iterável que tenha um len , ou receber
adicionalmente o argumento total= com o número esperado de itens. Integrar o tqdm com nossos exemplos flags2
proporciona um oportunidade de observar mais profundamente o funcionamento real dos scripts concorrentes, pois
nos obriga a usar as funções futures.as_completed (https://fpy.li/20-7) e asyncio.as_completed (https://fpy.li/20-15),
para permitir que o tqdm mostre o progresso conforme cada future é termina sua execução.

A outra característica dos exemplos flags2 é a interface de linha de comando. Todos os três scripts aceitam as
mesmas opções, e você pode vê-las rodando qualquer um deles com a opção -h . O Exemplo 10 mostra o texto de ajuda.

Exemplo 10. Tela de ajuda dos scripts da série flags2


TEXT
$ python3 flags2_threadpool.py -h
usage: flags2_threadpool.py [-h] [-a] [-e] [-l N] [-m CONCURRENT] [-s LABEL]
[-v]
[CC [CC ...]]

Download flags for country codes. Default: top 20 countries by population.

positional arguments:
CC country code or 1st letter (eg. B for BA...BZ)

optional arguments:
-h, --help show this help message and exit
-a, --all get all available flags (AD to ZW)
-e, --every get flags for every possible code (AA...ZZ)
-l N, --limit N limit to N first codes
-m CONCURRENT, --max_req CONCURRENT
maximum concurrent requests (default=30)
-s LABEL, --server LABEL
Server to hit; one of DELAY, ERROR, LOCAL, REMOTE
(default=LOCAL)
-v, --verbose output detailed progress info

Todos os argumentos são opcionais. Mas o -s/--server é essencial para os testes: ele permite escolher qual servidor
HTTP e qual porta serão usados no teste. Passe um desses parâmetros (insensíveis a maiúsculas/minúsculas) para
determinar onde o script vai buscar as bandeiras:

LOCAL
Usa http://localhost:8000/flags ; esse é o default. Você deve configurar um servidor HTTP local, respondendo
na porta 8000. Veja as instruções na nota a seguir.

REMOTE

Usa http://fluentpython.com/data/flags ; este é meu site público, hospedado em um servidor compartilhado.


Por favor, não o martele com requisições concorrentes excessivas. O domínio fluentpython.com é gerenciado pela
CDN (Content Delivery Network, Rede de Fornecimento de Conteúdo) da Cloudflare (https://fpy.li/20-16), então você pode
notar que os primeiros downloads são mais lentos, mas ficam mais rápidos conforme o cache da CDN é carregado.

DELAY

Usa http://localhost:8001/flags ; um servidor atrasando as respostas HTTP deve responder na porta 8001.
Escrevi o slow_server.py para facilitar o experimento. Ele está no diretório 20-futures/getflags/ do repositório de
código do Python Fluente (https://fpy.li/code). Veja as instruções na nota a seguir.

ERROR

Usa http://localhost:8002/flags ; um servidor devolvendo alguns erros HTTP deve responder na porta 8002.
Instruções a seguir.
Configurando os servidores de teste
Se você não tem um servidor HTTP local para testes, escrevi instruções de configuração usando
apenas Python ≥ 3.9 (nenhuma biblioteca externa) em 20-executors/getflags/README.adoc
(https://fpy.li/20-17) no fluentpython/example-code-2e (https://fpy.li/code) repositório. Em resumo, o
README.adoc descreve como usar:

python3 -m http.server

✒️ NOTA O servidor LOCAL na porta 8000

python3 slow_server.py
O servidor DELAY na porta 8001, que acrescenta um atraso aleatório de 0,5s a 5s antes de cada
resposta

python3 slow_server.py 8002 --error-rate .25

O servidor ERROR na porta 8002, que além do atraso aleatório tem uma chance de 25% de
retornar um erro "418 I’m a teapot" (https://fpy.li/20-18) como resposta

Por default, cada script flags2*.py irá baixar as bandeiras dos 20 países mais populosos do servidor LOCAL
( http://localhost:8000/flags ), usando um número default de conexões concorrentes, que varia de script para
script. O Exemplo 11 mostra uma execução padrão do script flags2_sequential.py usando as configurações default. Para
rodá-lo, você precisa de um servidor local, como explicado em Tenha cuidado ao testar clientes concorrentes.

Exemplo 11. Rodando flags2_sequential.py com todos os defaults: site LOCAL , as 20 bandeiras dos países mais
populosos, 1 conexão concorrente

$ python3 flags2_sequential.py
LOCAL site: http://localhost:8000/flags
Searching for 20 flags: from BD to VN
1 concurrent connection will be used.
--------------------
20 flags downloaded.
Elapsed time: 0.10s

Você pode selecionar as bandeiras a serem baixadas de várias formas. O Exemplo 12 mostra como baixar todas as
bandeiras com códigos de país começando pelas letras A, B ou C.

Exemplo 12. Roda flags2_threadpool.py para obter do servidor DELAY todas as bandeiras com prefixos de códigos de
país A, B ou C

$ python3 flags2_threadpool.py -s DELAY a b c


DELAY site: http://localhost:8001/flags
Searching for 78 flags: from AA to CZ
30 concurrent connections will be used.
--------------------
43 flags downloaded.
35 not found.
Elapsed time: 1.72s

Independente de como os códigos de país são selecionados, o número de bandeiras a serem obtidas pode ser limitado
com a opção -l/--limit . O Exemplo 13 demonstra como fazer exatamente 100 requisições, combinando a opção -a
para obter todas as bandeiras com -l 100 .
Exemplo 13. Roda flags2_asyncio.py para baixar 100 bandeiras ( -al 100 ) do servidor ERROR , usando 100 requisições
concorrentes ( -m 100 )

$ python3 flags2_asyncio.py -s ERROR -al 100 -m 100


ERROR site: http://localhost:8002/flags
Searching for 100 flags: from AD to LK
100 concurrent connections will be used.
--------------------
73 flags downloaded.
27 errors.
Elapsed time: 0.64s

Essa é a interface de usuário dos exemplos flags2 . Vamos ver como eles estão implementados.

20.5.1. Tratamento de erros nos exemplos flags2


A estratégia comum em todos os três exemplos para lidar com erros HTTP é que erros 404 (not found) são tratados pela
função encarregada de baixar um único arquivo ( download_one ). Qualquer outra exceção propaga para ser tratada
pela função download_many ou pela corrotina supervisor —no exemplo de asyncio .

Vamos novamente começar estudando o código sequencial, que é mais fácil de compreender—e muito reutilizado pelo
script com um pool de threads. O Exemplo 14 mostra as funções que efetivamente fazer os downloads nos scripts
flags2_sequential.py e flags2_threadpool.py.

Exemplo 14. flags2_sequential.py: funções básicas encarregadas dos downloads; ambas são reutilizadas no
flags2_threadpool.py
PY
from collections import Counter
from http import HTTPStatus

import httpx
import tqdm # type: ignore # (1)

from flags2_common import main, save_flag, DownloadStatus # (2)

DEFAULT_CONCUR_REQ = 1
MAX_CONCUR_REQ = 1

def get_flag(base_url: str, cc: str) -> bytes:


url = f'{base_url}/{cc}/{cc}.gif'.lower()
resp = httpx.get(url, timeout=3.1, follow_redirects=True)
resp.raise_for_status() # (3)
return resp.content

def download_one(cc: str, base_url: str, verbose: bool = False) -> DownloadStatus:
try:
image = get_flag(base_url, cc)
except httpx.HTTPStatusError as exc: # (4)
res = exc.response
if res.status_code == HTTPStatus.NOT_FOUND:
status = DownloadStatus.NOT_FOUND # (5)
msg = f'not found: {res.url}'
else:
raise # (6)
else:
save_flag(image, f'{cc}.gif')
status = DownloadStatus.OK
msg = 'OK'

if verbose: # (7)
print(cc, msg)

return status

1. Importa a biblioteca de exibição de barra de progresso tqdm , e diz ao Mypy para não checá-la.[287]

2. Importa algumas funções e um Enum do módulo flags2_common .

3. Dispara um HTTPStatusError se o código de status do HTTP não está em range(200, 300) .

4. download_one trata o HTTPStatusError , especificamente para tratar o código HTTP 404…​

5. …​mudando seu status local para DownloadStatus.NOT_FOUND ; DownloadStatus é um Enum importado de


flags2_common.py.
6. Qualquer outra exceção de HTTPStatusError é re-emitida e propagada para quem chamou a função.
7. Se a opção de linha de comando -v/--verbose está vigente, o código do país e a mensagem de status são exibidos;
é assim que você verá o progresso no modo verbose .

O Exemplo 15 lista a versão sequencial da função download_many . O código é simples, mas vale a pena estudar para
compará-lo com as versões concorrentes que veremos a seguir. Se concentre em como ele informa o progresso, trata
erros e conta os downloads.

Exemplo 15. flags2_sequential.py: a implementação sequencial de download_many


PY
def download_many(cc_list: list[str],
base_url: str,
verbose: bool,
_unused_concur_req: int) -> Counter[DownloadStatus]:
counter: Counter[DownloadStatus] = Counter() # (1)
cc_iter = sorted(cc_list) # (2)
if not verbose:
cc_iter = tqdm.tqdm(cc_iter) # (3)
for cc in cc_iter:
try:
status = download_one(cc, base_url, verbose) # (4)
except httpx.HTTPStatusError as exc: # (5)
error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}'
error_msg = error_msg.format(resp=exc.response)
except httpx.RequestError as exc: # (6)
error_msg = f'{exc} {type(exc)}'.strip()
except KeyboardInterrupt: # (7)
break
else: # (8)
error_msg = ''

if error_msg:
status = DownloadStatus.ERROR # (9)
counter[status] += 1 # (10)
if verbose and error_msg: # (11)
print(f'{cc} error: {error_msg}')

return counter # (12)

1. Este Counter vai registrar os diferentes resultados possíveis dos downloads: DownloadStatus.OK ,
DownloadStatus.NOT_FOUND , ou DownloadStatus.ERROR .

2. cc_iter mantém a lista de códigos de país recebidos como argumentos, em ordem alfabética.
3. Se não estamos rodando em modo verbose , cc_iter é passado para o tqdm , que retorna um iterador que
produz os itens em cc_iter enquanto também anima a barra de progresso.
4. Faz chamadas sucessivas a download_one .

5. As exceções do código de status HTTP ocorridas em get_flag e não tratadas por download_one são tratadas aqui.
6. Outras exceções referentes à rede são tratadas aqui. Qualquer outra exceção vai interromper o script, porque a
função flags2_common.main , que chama download_many , não tem nenhum try/except .
7. Sai do loop se o usuário pressionar Ctrl-C.
8. Se nenhuma exceção saiu de download_one , limpa a mensagem de erro.

9. Se houve um erro, muda o status local de acordo com o erro.


10. Incrementa o contador para aquele status .

11. Se no modo verbose , mostra a mensagem de erro para o código de país atual, se houver.

12. Retorna counter para que main possa mostrar os números no relatório final.

Agora vamos estudar flags2_threadpool.py, o exemplo de pool de threads refatorado.

20.5.2. Usando futures.as_completed


Para integrar a barra de progresso do tqdm e tratar os erros a cada requisição, o script flags2_threadpool.py usa o
futures.ThreadPoolExecutor com a função, já vista anteriormente, futures.as_completed . O Exemplo 16 é a
listagem completa de flags2_threadpool.py. Apenas a função download_many é implementada; as outras funções são
reutilizadas de flags2_common.py e flags2_sequential.py.
Exemplo 16. flags2_threadpool.py: listagem completa

PY
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed

import httpx
import tqdm # type: ignore

from flags2_common import main, DownloadStatus


from flags2_sequential import download_one # (1)

DEFAULT_CONCUR_REQ = 30 # (2)
MAX_CONCUR_REQ = 1000 # (3)

def download_many(cc_list: list[str],


base_url: str,
verbose: bool,
concur_req: int) -> Counter[DownloadStatus]:
counter: Counter[DownloadStatus] = Counter()
with ThreadPoolExecutor(max_workers=concur_req) as executor: # (4)
to_do_map = {} # (5)
for cc in sorted(cc_list): # (6)
future = executor.submit(download_one, cc,
base_url, verbose) # (7)
to_do_map[future] = cc # (8)
done_iter = as_completed(to_do_map) # (9)
if not verbose:
done_iter = tqdm.tqdm(done_iter, total=len(cc_list)) # (10)
for future in done_iter: # (11)
try:
status = future.result() # (12)
except httpx.HTTPStatusError as exc: # (13)
error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}'
error_msg = error_msg.format(resp=exc.response)
except httpx.RequestError as exc:
error_msg = f'{exc} {type(exc)}'.strip()
except KeyboardInterrupt:
break
else:
error_msg = ''

if error_msg:
status = DownloadStatus.ERROR
counter[status] += 1
if verbose and error_msg:
cc = to_do_map[future] # (14)
print(f'{cc} error: {error_msg}')

return counter

if __name__ == '__main__':
main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)

1. Reutiliza download_one de flags2_sequential (Exemplo 14).


2. Se a opção de linha de comando -m/--max_req não é passada, este será o número máximo de requisições
concorrentes, implementado como o tamanho do poll de threads; o número real pode ser menor se o número de
bandeiras a serem baixadas for menor.
3. MAX_CONCUR_REQ limita o número máximo de requisições concorrentes independente do número de bandeiras a
serem baixadas ou da opção de linha de comando -m/--max_req . É uma medida de segurança, para evitar iniciar
threads demais, com seu uso significativo de memória.
4. Cria o executor com max_workers determinado por concur_req , calculado pela função main como o menor
de: MAX_CONCUR_REQ , o tamanho de cc_list , ou o valor da opção de linha de comando -m/--max_req . Isso evita
criar mais threads que o necessário.
5. Este dict vai mapear cada instância de Future —representando um download—com o respectivo código de país,
para exibição de erros.
6. Itera sobre a lista de códigos de país em ordem alfabética. A ordem dos resultados vai depender, mais do que de
qualquer outra coisa, do tempo das respostas HTTP; mas se o tamanho do pool de threads (dado por concur_req )
for muito menor que len(cc_list) , você poderá ver os downloads aparecendo em ordem alfabética.
7. Cada chamada a executor.submit agenda a execução de uma invocável e retorna uma instância de Future . O
primeiro argumento é a invocável, o restante são os argumentos que ela receberá.
8. Armazena o future e o código de país no dict .

9. futures.as_completed retorna um iterador que produz futures conforme cada tarefa é completada.
10. Se não estiver no modo verbose , passa o resultado de as_completed com a função tqdm , para mostrar a barra
de progresso; como done_iter não tem len , precisamos informar o tqdm qual o número de itens esperado com
o argumento total= , para que ele possa estimar o trabalho restante.
11. Itera sobre os futures conforme eles vão terminando.
12. Chamar o método result em um future retorna ou o valor retornado pela invocável ou dispara qualquer exceção
que tenha sido capturada quando a invocável foi executada. Esse método pode bloquear quem chama, esperando
por uma resolução. Mas não nesse exemplo, porque as_completed só retorna futures que terminaram sua
execução.
13. Trata exceções em potencial; o resto dessa função é idêntica à função download_many no Exemplo 15), exceto pela
observação a seguir.
14. Para dar contexto à mensagem de erro, recupera o código de país do to_do_map , usando o future atual como
chave. Isso não era necessário na versão sequencial, pois estávamos iterando sobre a lista de códigos de país, então
sabíamos qual era o cc atual; aqui estamos iterando sobre futures.
O Exemplo 16 usa um idioma que é muito útil com futures.as_completed : construir um dict

👉 DICA mapeando cada future a outros dados que podem ser úteis quando o future terminar de executar.
Aqui o to_do_map mapeia cada future ao código de país atribuído a ele. Isso torna fácil realizar o
pós-processamento com os resultados dos futures, apesar deles serem produzidos fora de ordem.

As threads do Python são bastante adequadas a aplicações de uso intensivo de E/S, e o pacote concurrent.futures as
torna relativamente simples de implementar em certos casos de uso. Com ProcessPoolExecutor você também pode
resolver problemas de uso intensivo de CPU em múltiplos núcleos—se o processamento for "embaraçosamente
paralelo" (https://fpy.li/20-19). Isso encerra nossa introdução básica a concurrent.futures .

20.6. Resumo do capítulo


Nós começamos o capítulo comparando dois clientes HTTP concorrentes com um sequencial, demonstrando que as
soluções concorrentes mostram um ganho significativo de desempenho sobre o script sequencial.

Após estudar o primeiro exemplo, baseado no concurrent.futures , olhamos mais de perto os objetos future,
instâncias de concurrent.futures.Future ou de asyncio​.Future , enfatizando as semelhanças entre essas classes
(suas diferenças serão examinadas no Capítulo 21). Vimos como criar futures chamando Executor.submit , e como
iterar sobre futures que terminaram sua execução com concurrent.futures.as_completed .
Então discutimos o uso de múltiplos processos com a classe concurrent.futures.ProcessPoolExecutor , para evitar
a GIL e usar múltiplos núcleos de CPU, simplificando o verificador de números primos multi-núcleo que vimos antes no
Capítulo 19.

Na seção seguinte vimos como funciona a concurrent.futures.ThreadPoolExecutor , com um exemplo didático,


iniciando tarefas que apenas não faziam nada por alguns segundos, exceto exibir seu status e a hora naquele instante.

Nós então voltamos para os exemplos de download de bandeiras. Melhorar aqueles exemplos com uma barra de
progresso e tratamento de erro adequado nos ajudou a explorar melhor a função geradora future.as_completed
mostrando um modelo comum: armazenar futures em um dict para anexar a eles informação adicional quando são
submetidos, para podermos usar aquela informação quando o future sai do iterador as_completed .

20.7. Para saber mais


O pacote concurrent.futures foi uma contribuição de Brian Quinlan, que o apresentou em uma palestra sensacional
intitulada "The Future Is Soon!" (https://fpy.li/20-20) (EN), na PyCon Australia 2010. A palestra de Quinlan não tinha slides;
ele mostra o que a biblioteca faz digitando código diretamente no console do Python. Como exemplo motivador, a
apresentação inclui um pequeno vídeo com o cartunista/programador do XKCD, Randall Munroe, executando um
ataque de negação de serviço (DoS) não-intencional contra o Google Maps, para criar um mapa colorido de tempos de
locomoção pela cidade. A introdução formal à biblioteca é a PEP 3148 - futures - execute computations
asynchronously (https://fpy.li/pep3148) (`futures` - executar processamento assíncrono) (EN). Na PEP, Quinlan escreveu que
a biblioteca concurrent.futures foi "muito influenciada pelo pacote java.util.concurrent do Java."

Para recursos adicionais falando do concurrent.futures , por favor consulte o Capítulo 19. Todas as referências que
tratam de threading e multiprocessing do Python na Seção 19.9.1 também tratam do concurrent.futures .

Ponto de vista
Evitando Threads

Concorrência: um dos tópicos mais difíceis na ciência da computação (normalmente é melhor evitá-lo).

David Beazley, educador Python e cientista louco—Slide #9 do tutorial "A Curious Course on Coroutines and
Concurrency" ("Um Curioso Curso sobre Corrotinas e Concorrência") (EN) (https://fpy.li/20-21), apresentado na
PyCon 2009.

Eu concordo com as citações aparentemente contraditórias de David Beazley e Michele Simionato no início desse
capítulo.

Assisti um curso de graduação sobre concorrência. Tudo o que vimos foi programação de threads POSIX
(https://pt.wikipedia.org/wiki/POSIX_Threads). O que aprendi: que não quero gerenciar threads e travas pessoalmente,
pela mesma razão que não quero gerenciar a alocação e desalocação de memória pessoalmente. Essas tarefas são
melhor desempenhadas por programadores de sistemas, que tem o conhecimento, a inclinação e o tempo para
fazê-las direito—ou assim esperamos. Sou pago para desenvolver aplicações, não sistemas operacionais. Não
preciso desse controle fino de threads, travas, malloc e free —veja "Alocação dinâmica de memória em C"
(https://pt.wikipedia.org/wiki/Aloca%C3%A7%C3%A3o_din%C3%A2mica_de_mem%C3%B3ria_em_C).

Por isso acho o pacote concurrent.futures interessante: ele trata threads, processos, e filas como
infraestrutura, algo a seu serviço, não algo que você precisa controlar diretamente. Claro, ele foi projetado
pensando em tarefas simples, os assim chamado problemas embaraçosamente paralelos—ao contrário de
sistemas operacionais ou servidores de banco de dados, como aponta Simionato naquela citação.
Para problemas de concorrência "não embaraçosos", threads e travas também não são a solução. Ao nível do
sistema operacional, as threads nunca vão desaparecer. Mas todas as linguagens de programação que achei
excitantes nos últimos muitos anos fornecem abstrações de alto nível para concorrência, como demonstra o
excelente livro de Paul Butcher, Seven Concurrency Models in Seven Weeks (https://fpy.li/20-24) (Sete Modelos de
Concorrência em Sete Semanas) (EN). Go, Elixir, e Clojure estão entre elas. Erlang—a linguagem de implementação
do Elixir—é um exemplo claro de uma linguagem projetada desde o início pensando em concorrência. Erlang não
me excita por uma razão simples: acho sua sintaxe feia. O Python me acostumou mal.

José Valim, antes um dos contribuidores centrais do Ruby on Rails, projetou o Elixir com uma sintaxe moderna e
agradável. Como Lisp e Clojure, o Elixir implementa macros sintáticas. Isso é uma faca de dois gumes. Macros
sintáticas permitem criar DSLs poderosas, mas a proliferação de sub-linguagens pode levar a bases de código
incompatíveis e à fragmentação da comunidade. O Lisp se afogou em um mar de macros, cada empresa e grupo
de desenvolvedores Lisp usando seu próprio dialeto arcano. A padronização em torno do Common Lisp resultou
em uma linguagem inchada. Espero que José Valim inspire a comunidade do Elixir a evitar um destino
semelhante. Até agora, o cenário parece bom. O invólucro de bancos de dados e gerador de queries Ecto
(https://fpy.li/20-25) é muito agradável de usar: um grande exemplo do uso de macros para criar uma DSL—sigla de
Domain-Specific Language, Linguagem de Domínio Específico—flexível mas amigável, para interagir com bancos
de dados relacionais e não-relacionais.

Como o Elixir, o Go é uma linguagem moderna com ideias novas. Mas, em alguns aspectos, é uma linguagem
conservadora, comparada ao Elixir. O Go não tem macros, e sua sintaxe é mais simples que a do Python. O Go não
suporta herança ou sobrecarga de operadores, e oferece menos oportunidades para metaprogramação que o
Python. Essas limitações são consideradas benéficas. Elas levam a comportamentos e desempenho mais
previsíveis. Isso é uma grande vantagem em ambientes de missão crítica altamente concorrentes, onde o Go
pretende substituir C++, Java e Python.

Enquanto Elixir e Go são competidores diretos no espaço da alta concorrência, seus projetos e filosofias atraem
públicos diferentes. Ambos tem boas chances de prosperar. Mas historicamente, as linguagens mais
conservadoras tendem a atrair mais programadores.
21. Programação assíncrona
O problema com as abordagens usuais da programação assíncrona é que elas são propostas do tipo "tudo ou
nada". Ou você reescreve todo o código, de forma que nada nele bloqueie [o processamento] ou você está só
perdendo tempo.

Alvaro Videla e Jason J. W. Williams, RabbitMQ in Action (RabbitMQ em Ação)Videla & Williams, RabbitMQ in
Action (RabbitMQ em Ação) (Manning), Capítulo 4, "Solving Problems with Rabbit: coding and patterns
(Resolvendo Problemas com Rabbit: programação e modelos)," p. 61.

Este capítulo trata de três grandes tópicos intimamente interligados:

Os elementos de linguagem async def , await , async with , e async for do Python;
Objetos que suportam tais elementos através de métodos especiais como __await__ , __aiter__ etc., tais como
corrotinas nativas e variantes assíncronas de gerenciadores de contexto, iteráveis, geradores e compreensões;
asyncio e outras bibliotecas assíncronas.

Este capítulo parte das ideias de iteráveis e geradores (Capítulo 17, em particular da Seção 17.13), gerenciadores de
contexto (no Capítulo 18), e conceitos gerais de programação concorrente (no Capítulo 19).

Vamos estudar clientes HTTP concorrentes similares aos vistos no Capítulo 20, reescritos com corrotinas nativas e
gerenciadores de contexto assíncronos, usando a mesma biblioteca HTTPX de antes, mas agora através de sua API
assíncrona. Veremos também como evitar o bloqueio do loop de eventos, delegando operações lentas para um executor
de threads ou processos.

Após os exemplos de clientes HTTP, teremos duas aplicações simples de servidor, uma delas usando a framework cada
vez mais popular FastAPI. A seguir tratamos de outros artefatos da linguagem viabilizados pelas palavras-chave
async/await : funções geradoras assíncronas, compreensões assíncronas, e expressões geradoras assíncronas. Para
realçar o fato daqueles recursos da linguagem não estarem limitados ao asyncio, veremos um exemplo reescrito para
usar a Curio—o elegante e inovador framework inventado por David Beazley.

Finalizando o capítulo, escrevi uma pequena seção sobre vantagens e armadilhas da programação assíncrona.

Há um longo caminho à nossa frente. Teremos espaço apenas para exemplos básicos, mas eles vão ilustrar as
características mais importantes de cada ideia.

A documentação do asyncio (https://fpy.li/21-1) melhorou muito após Yury Selivanov[288] reorganizá-la,


dando maior destaque às funções úteis para desenvolvedores de aplicações. A maior parte da API de
asyncio consiste em funções e classes voltadas para criadores de pacotes como frameworks web e
drivers de bancos de dados, ou seja, são necessários para criar bibliotecas assíncronas, mas não
👉 DICA aplicações.

Para mais profundidade sobre asyncio, recomendo Using Asyncio in Python ("Usando Asyncio em
Python") (https://fpy.li/hattingh) de Caleb Hattingh (O’Reilly). Política de transparência: Caleb é um dos
revisores técnicos deste livro.
21.1. Novidades nesse capítulo
Quando escrevi a primeira edição de Python Fluente, a biblioteca asyncio era provisória e as palavras-chave
async/await não existiam. Assim, todos os exemplos desse capítulo precisaram ser atualizados. Também criei novos
exemplos: scripts de sondagem de domínios, um serviço web com FastAPI, e experimentos com o novo modo
assíncrono do console do Python.

Novas seções tratam de recursos da linguagem inexistentes naquele momento, como corrotinas nativas, async with ,
async for , e os objetos que suportam essas instruções.

As ideias na seção Seção 21.13 refletem lições importantes tiradas da experiência prática, e a considero uma leitura
essencial para qualquer um trabalhando com programação assíncrona. Elas podem ajudar você a evitar muitos
problemas—seja no Python, seja no Node.js.

Por fim, removi vários parágrafos sobre asyncio.Futures , que agora considero parte das APIs de baixo nível do
asyncio.

21.2. Algumas definições.


No início da seção Seção 17.13, vimos que, desde o Python 3.5, a linguagem oferece três tipos de corrotinas:

Corrotina nativa
Uma função corrotina definida com async def . Você pode delegar de uma corrotina nativa para outra corrotina
nativa, usando a palavra-chave await , de forma similar àquela como as corrotinas clássicas usam yield from . O
comando async def sempre define uma corrotina nativa, mesmo se a palavra-chave await não seja usada em seu
corpo. A palavra-chave await não pode ser usada fora de uma corrotina nativa.[289]

Corrotina clássica
Uma função geradora que consome dados enviados a ela via chamadas a my_coro.send(data) , e que lê aqueles
dados usando yield em uma expressão. Corrotinas clássicas podem delegar para outras corrotinas clássicas usando
yield from . Corrotinas clássicas não podem ser controladas por await , e não são mais suportadas pelo asyncio.

Corrotinas baseadas em geradoras


Uma função geradora decorada com @types.coroutine —introduzido no Python 3.5. Esse decorador torna a
geradora compatível com a nova palavra-chave await .

Nesse capítulo vamos nos concentrar nas corrotinas nativas, bem como nas geradoras assíncronas:

Geradora assíncrona
Uma função geradora definida com async def que usa yield em seu corpo. Ela devolve um objeto gerador
assíncrono que oferece um __anext__ , um método corrotina para obter o próximo item.

@asyncio.coroutine Não Tem Futuro[290]


O decorador @asyncio.coroutine para corrotinas clássicas e corrotinas baseadas em gerador foi

⚠️ AVISO descontinuado no Python 3.8, e está previsto para ser removido no Python 3.11, de acordo com o
Issue 43216 (https://fpy.li/21-2). Por outro lado, @types.coroutine deve continuar existindo, como se
vê aqui: Issue 36921 (https://fpy.li/21-3). Esse decorador não é mais suportado pelo asyncio, mas é usado
em código interno nas frameworks assíncronas Curio e Trio.
21.3. Um exemplo de asyncio: sondando domínios
Imagine que você esteja prestes a lançar um novo blog sobre Python, e planeje registrar um domínio usando uma
palavra-chave do Python e o sufixo .DEV—por exemplo, AWAIT.DEV. O Exemplo 1 é um script usando asyncio que
verifica vários domínios de forma concorrente. Essa é saída produzida pelo script:

TEXT
$ python3 blogdom.py
with.dev
+ elif.dev
+ def.dev
from.dev
else.dev
or.dev
if.dev
del.dev
+ as.dev
none.dev
pass.dev
true.dev
+ in.dev
+ for.dev
+ is.dev
+ and.dev
+ try.dev
+ not.dev

Observe que os domínios aparecem fora de ordem. Se você rodar o script, os verá sendo exibidos um após o outro, a
intervalos variados. O sinal de + indica que sua máquina foi capaz de resolver o domínio via DNS. Caso contrário, o
domínio não foi resolvido e pode estar disponível.[291]

No blogdom.py, a sondagem de DNS é feita por objetos corrotinas nativas. Como as operações assíncronas são
intercaladas, o tempo necessário para verificar 18 domínios é bem menor que se eles fosse verificados
sequencialmente. Na verdade, o tempo total é quase o igual ao da resposta mais lenta, em vez da soma dos tempos de
todas as respostas do DNS.

O Exemplo 1 mostra o código dp blogdom.py.

Exemplo 1. blogdom.py: procura domínios para um blog sobre Python


PYTHON3
#!/usr/bin/env python3
import asyncio
import socket
from keyword import kwlist, softkwlist

MAX_KEYWORD_LEN = 4 # (1)
KEYWORDS = sorted(kwlist + softkwlist)

async def probe(domain: str) -> tuple[str, bool]: # (2)


loop = asyncio.get_running_loop() # (3)
try:
await loop.getaddrinfo(domain, None) # (4)
except socket.gaierror:
return (domain, False)
return (domain, True)

async def main() -> None: # (5)


names = (kw for kw in KEYWORDS if len(kw) <= MAX_KEYWORD_LEN) # (6)
domains = (f'{name}.dev'.lower() for name in names) # (7)
coros = [probe(domain) for domain in domains] # (8)
for coro in asyncio.as_completed(coros): # (9)
domain, found = await coro # (10)
mark = '+' if found else ' '
print(f'{mark} {domain}')

if __name__ == '__main__':
asyncio.run(main()) # (11)

1. Estabelece o comprimento máximo da palavra-chave para domínios, pois quanto menor, melhor.
2. probe devolve uma tupla com o nome do domínio e um valor booleano; True significa que o domínio foi
resolvido. Incluir o nome do domínio aqui facilita a exibição dos resultados.
3. Obtém uma referência para o loop de eventos do asyncio , para usá-la a seguir.

4. O método corrotina loop.getaddrinfo(…)


(https://docs.python.org/pt-br/3/library/asyncio-eventloop.html#asyncio.loop.getaddrinfo) devolve uma tupla de parâmetros
com cinco partes (https://docs.python.org/pt-br/3/library/socket.html#socket.getaddrinfo) para conectar ao endereço dado
usando um socket. Neste exemplo não precisamos do resultado. Se conseguirmos um resultado, o domínio foi
resolvido; caso contrário, não.
5. main tem que ser uma corrotina, para podemros usar await aqui.
6. Gerador para produzir palavras-chave com tamanho até MAX_KEYWORD_LEN .

7. Gerador para produzir nome de domínio com o sufixo .dev .

8. Cria uma lista de objetos corrotina, invocando a corrotina probe com cada argumento domain .

9. asyncio.as_completed é um gerador que produz corrotinas que devolvem os resultados das corrotinas passadas
a ele. Ele as produz na ordem em que elas terminam seu processamento, não na ordem em que foram submetidas.
É similar ao futures.as_completed , que vimos no Capítulo 20, Exemplo 4.
10. Nesse ponto, sabemos que a corrotina terminou, pois é assim que as_completed funciona. Portanto, a expressão
await não vai bloquear, mas precisamos dela para obter o resultado de coro . Se coro gerou uma exceção não
tratada, ela será gerada novamente aqui.
11. asyncio.run inicia o loop de eventos e retorna apenas quando o loop terminar. Esse é um modelo comum para
scripts usando asyncio : implementar main como uma corrotina e controlá-la com asyncio.run dentro do bloco
if name == 'main': .
A função asyncio.get_running_loop surgiu no Python 3.7, para uso dentro de corrotinas, como
visto em probe . Se não houver um loop em execução, asyncio.get_running_loop gera um
RuntimeError . Sua implementação é mais simples e mais rápida que a de
👉 DICA asyncio.get_event_loop , que pode iniciar um loop de eventos se necessário. Desde o Python 3.10,
asyncio.get_event_loop foi descontinuado
(https://docs.python.org/pt-br/3.10/library/asyncio-eventloop.html#asyncio.get_event_loop), e em algum
momento se tornará um alias para asyncio.get_running_loop .

21.3.1. O truque de Guido para ler código assíncrono


Há muitos conceitos novos para entender no asyncio, mas a lógica básica do Exemplo 1 é fácil de compreender se você
usar o truque sugerido pelo próprio Guido van Rossum: cerre os olhos e finja que as palavras-chave async e await
não estão ali. Fazendo isso, você vai perceber que as corrotinas podem ser lidas como as boas e velhas funções
sequenciais.

Por exemplo, imagine que o corpo dessa corrotina…​

PYTHON3
async def probe(domain: str) -> tuple[str, bool]:
loop = asyncio.get_running_loop()
try:
await loop.getaddrinfo(domain, None)
except socket.gaierror:
return (domain, False)
return (domain, True)

…​funciona como a função abaixo, exceto que, magicamente, ela nunca bloqueia a execução:

PYTHON3
def probe(domain: str) -> tuple[str, bool]: # no async
loop = asyncio.get_running_loop()
try:
loop.getaddrinfo(domain, None) # no await
except socket.gaierror:
return (domain, False)
return (domain, True)

Usar a sintaxe await loop.getaddrinfo(…​) evita o bloqueio, porque await suspende o objeto corrotina atual. Por
exemplo, durante a execução da corrotina probe('if.dev') , um novo objeto corrotina é criado por
getaddrinfo('if.dev', None) . Aplicar await sobre ele inicia a consulta de baixo nível addrinfo e devolve o
controle para o loop de eventos, não para a corrotina probe(‘if.dev’) , que está suspensa. O loop de eventos pode
então ativar outros objetos corrotina pendentes, tal como probe('or.dev') .

Quando o loop de eventos recebe uma resposta para a consulta getaddrinfo('if.dev', None) , aquele objeto
corrotina específico prossegue sua execução, e devolve o controle pra o probe('if.dev') —que estava suspenso no
await —e pode agora tratar alguma possível exceção e devolver a tupla com o resultado.

Até aqui, vimos asyncio.as_completed e await sendo aplicados apenas a corrotinas. Mas eles podem lidar com
qualquer objeto "esperável". Esse conceito será explicado a seguir.

21.4. Novo conceito: awaitable ou esperável


A palavra-chave for funciona com iteráveis. A palavra-chave await funciona com esperáveis (awaitable).

Como um usuário final do asyncio, esses são os esperáveis que você verá diariamente:
Um objeto corrotina nativa, que você obtém chamando uma função corrotina nativa
Uma asyncio.Task , que você normalmente obtém passando um objeto corrotina para asyncio.create_task()

Entretanto, o código do usuário final nem sempre precisa await por uma Task . Usamos
asyncio.create_task(one_coro()) para agendar one_coro para execução concorrente, sem esperar que retorne.
Foi o que fizemos com a corrotina spinner em spinner_async.py (no Exemplo 4). Criar a tarefa é o suficiente para
agendar a execução da corrotina.

Mesmo que você não precise cancelar a tarefa ou esperar por ela, é necessário preservar o objeto
Task devolvido por create_task , atribuindo ele a uma variável ou coleção que você controla. O
loop de eventos usa referências fracas para gerenciar as tarefas, o que significa que elas podem ser
⚠️ AVISO descartadas pelo coletor de lixo antes de executarem. Por isso você precisa criar referências fortes
para manter cada tarefa na memória. Veja a documentação de asyncio.create_task
(https://docs.python.org/pt-br/3/library/asyncio-task.html#asyncio.create_task). Sobre referências fracas,
escrevi o artigo "Weak References" (https://fpy.li/weakref) fluentpython.com (EN).[292]

Por outro lado, usamos await other_coro() para executar other_coro agora mesmo e esperar que ela termine,
porque precisamos do resultado para prosseguir. Em spinner_async.py, a corrotina supervisor usava res = await
slow() para executar slow e aguardar seu resultado..

Ao implementar bibliotecas assíncronas ou contribuir para o próprio asyncio, você pode também encontrar esse
esperáveis de baixo nível:

Um objeto com um método __await__ que devolve um iterador; por exemplo, uma instância de asyncio.Future
( asyncio.Task é uma subclasse de asyncio.Future )
Objetos escritos em outras linguagens usando a API Python/C, com uma função tp_as_async.am_await , que
devolvem um iterador (similar ao método __await__ )

As bases de código existentes podem também conter um tipo adicional de esperável: objetos corrotina baseados em
geradores, que estão no processo de serem descontinuados.

A PEP 492 afirma (https://fpy.li/21-7) (EN) que a expressão await "usa a [mesma] implementação de
yield from [mas] com um passo adicional de validação de seu argumento" e que “ await só aceita

✒️ NOTA um esperável.” A PEP não explica aquela implementação em detalhes, mas se refere à PEP 380
(https://fpy.li/pep380), que introduziu yield from . Eu postei uma explicação detalhada no texto "The
Meaning of yield from" (https://fpy.li/21-8) (EN) da seção "Classic Coroutines" (https://fpy.li/oldcoro) (EN) do
fluentpython.com (http://fluentpython.com).

Agora vamos estudar a versão asyncio de um script que baixa um conjunto fixo de imagens de bandeiras.

21.5. Downloads com asyncio e HTTPX


O script flags_asyncio.py baixa um conjunto fixo de 20 bandeiras de fluentpython.com. Nós já o mencionamos na Seção
20.2, mas agora vamos examiná-lo em detalhes, aplicando os conceitos que acabamos de ver.

A partir do Python 3.10, o asyncio só suporta TCP e UDP diretamente, e não há pacotes de cliente ou servidor HTTP
assíncronos na bilbioteca padrão. Estou usando o HTTPX (https://fpy.li/httpx) em todos os exemplos de cliente HTTP.

Vamos explorar o flags_asyncio.py de baixo para cima, isto é, olhando primeiro as função que configuram a ação no
Exemplo 2.
Para deixar o código mais fácil de ler, flags_asyncio.py não tem qualquer tratamento de erro. Nessa
introdução a async/await é útil se concentrar inicialmente no "caminho feliz", para entender como
funções regulares e corrotinas são dispostas em um programa. Começando na seção Seção 21.7, os
⚠️ AVISO exemplos incluem tratamento de erros e outros recursos.

Os exemplos de flags_.py aqui e no capítulo Capítulo 20 compartilham código e dados, então os


coloquei juntos no diretório example-code-2e/20-executors/getflags (https://fpy.li/21-9).

Exemplo 2. flags_asyncio.py: funções de inicialização

PY
def download_many(cc_list: list[str]) -> int: # (1)
return asyncio.run(supervisor(cc_list)) # (2)

async def supervisor(cc_list: list[str]) -> int:


async with AsyncClient() as client: # (3)
to_do = [download_one(client, cc)
for cc in sorted(cc_list)] # (4)
res = await asyncio.gather(*to_do) # (5)

return len(res) # (6)

if __name__ == '__main__':
main(download_many)

1. Essa precisa ser uma função comum—não uma corrotina—para poder ser passada para e chamada pela função
main do módulo flags.py (Exemplo 2).

2. Executa o loop de eventos, monitorando o objeto corrotina supervisor(cc_list) até que ele retorne. Isso vai
bloquear enquanto o loop de eventos roda. O resultado dessa linha é o que quer que supervisor devolver.
3. Operação de cliente HTTP assíncronas no httpx são métodos de AsyncClient , que também é um gerenciador de
contexto assíncrono: um gerenciador de contexto com métodos assíncronos de configuração e destruição (veremos
mais sobre isso na seção Seção 21.6).
4. Cria uma lista de objetos corrotina, chamando a corrotina download_one uma vez para cada bandeira a ser obtida.
5. Espera pela corrotina asyncio.gather , que aceita um ou mais argumentos esperáveis e aguarda até que todos
terminem, devolvendo uma lista de resultados para os esperáveis fornecidos, na ordem em que foram enviados.
6. supervisor devolve o tamanho da lista vinda de asyncio.gather .

Agora vamos revisar a parte superior de flags_asyncio.py (Exemplo 3). Reorganizei as corrotinas para podermos lê-las
na ordem em que são iniciadas pelo loop de eventos.

Exemplo 3. flags_asyncio.py: imports and download functions


PY
import asyncio

from httpx import AsyncClient # (1)

from flags import BASE_URL, save_flag, main # (2)

async def download_one(client: AsyncClient, cc: str): # (3)


image = await get_flag(client, cc)
save_flag(image, f'{cc}.gif')
print(cc, end=' ', flush=True)
return cc

async def get_flag(client: AsyncClient, cc: str) -> bytes: # (4)


url = f'{BASE_URL}/{cc}/{cc}.gif'.lower()
resp = await client.get(url, timeout=6.1,
follow_redirects=True) # (5)
return resp.read() # (6)

1. httpx precisa ser importado—não faz parte da biblioteca padrão


2. Reutiliza código de flags.py (Exemplo 2).
3. download_one tem que ser uma corrotina nativa, para poder await por get_flag —que executa a requisição
HTTP. Ela então mostra o código de país bandeira baixada, e salva a imagem.
4. get_flag precisa receber o AsyncClient para fazer a requisição.
5. O método get de uma instância de httpx.AsyncClient devolve um objeto ClientResponse , que também é um
gerenciador assíncrono de contexto.
6. Operações de E/S de rede são implementadas como métodos corrotina, então eles são controlados de forma
assíncrona pelo loop de eventos do asyncio .

Seria melhor, em termos de desempenho, que a chamada a save_flag dentro de get_flag fosse
assíncrona, evitando bloquear o loop de eventos. Entretanto, atualmente asyncio não oferece uma
✒️ NOTA API assíncrona de acesso ao sistema de arquivos—como faz o Node.js.

A seção Seção 21.7.1 vai mostrar como delegar save_flag para uma thread.

O seu código delega para as corrotinas do httpx explicitamente, usando await , ou implicitamente, usando os
métodos especiais dos gerenciadores de contexto assíncronos, tais como Async​Client e ClientResponse —como
veremos na seção Seção 21.6.

21.5.1. O segredo das corrotinas nativas: humildes geradores


A diferença fundamental entre os exemplos de corrotinas clássicas vistas nas seção Seção 17.13 e flags_asyncio.py é que
não há chamadas a .send() ou expressões yield visíveis nesse último. O seu código fica entre a biblioteca asyncio e
as bibliotecas assíncronas que você estiver usando, como por exemplo a HTTPX. Isso está ilustrado na Figura 1.
Figura 1. Em um programa assíncrono, uma função do usuário inicia o loop de eventos, agendando uma corrotina
inicial com asyncio.run . Cada corrotina do usuário aciona a seguinte com uma expressão await , formando um
canal que permite a comunicação entre uma biblioteca como a HTTPX e o loop de eventos.

Debaixo dos panos, o loop de eventos do asyncio faz as chamadas a .send que acionam as nossas corrotinas, e
nossas corrotinas await por outras corrotinas, incluindo corrotinas da biblioteca. Como já mencionado, a maior parte
da implementação de await vem de yield from , que também usa chamadas a .send para acionar corrotinas.

O canal await acaba por chegar a um esperável de baixo nível, que devolve um gerador que o loop de eventos pode
acionar em resposta a eventos tais com cronômetros ou E/S de rede. Os esperáveis e geradores no final desses canais
await estão implementados nas profundezas das bibliotecas, não são parte de suas APIs e podem ser extensões
Python/C.

Usando funções como asyncio.gather e asyncio.create_task , é possível iniciar múltiplos canais await
concorrentes, permitindo a execução concorrente de múltiplas operações de E/S acionadas por um único loop de
eventos, em uma única thread.

21.5.2. O problema do tudo ou nada


Observe que, no Exemplo 3, não pude reutilizar a função get_flag de flags.py (Exemplo 2). Tive que reescrevê-la
como uma corrotina para usar a API assíncrona do HTTPX. Para obter o melhor desempenho do asyncio, precisamos
substituir todas as funções que fazem E/S por uma versão assíncrona, que seja ativada com await ou
asyncio.create_task . Dessa forma o controle é devolvido ao loop de eventos enquanto a função aguarda pela
operação de entrada ou saída. Se você não puder reescrever a função bloqueante como uma corrotina, deveria
executá-la em uma thread ou um processo separados, como veremos na seção Seção 21.8.

Essa é a razão da escolha da epígrafe desse capítulo, que incluí o seguinte conselho: "[Ou] você reescreve todo o código,
de forma que nada nele bloqueie [o processamento] ou você está só perdendo tempo.""

Pela mesma razão, também não pude reutilizar a função download_one de flags_threadpool.py (Exemplo 3). O código
no Exemplo 3 aciona get_flag com await , então download_one precisa também ser uma corrotina. Para cada
requisição, um objeto corrotina download_one é criado em supervisor , e eles são todos acionados pela corrotina
asyncio.gather .

Vamos agora estudar o comando async with , que apareceu em supervisor (Exemplo 2) e get_flag (Exemplo 3).
21.6. Gerenciadores de contexto assíncronos
Na seção Seção 18.2, vimos como um objeto pode ser usado para executar código antes e depois do corpo de um bloco
with , se sua classe oferecer os métodos __enter__ e __exit__ .

Agora, considere o Exemplo 4, que usa o driver PostgreSQL asyncpg (https://fpy.li/21-10) compatível com o asyncio
(documentação do asyncpg sobre transações (https://fpy.li/21-11)).

Exemplo 4. Código exemplo da documentação do driver PostgreSQL asyncpg

PYTHON3
tr = connection.transaction()
await tr.start()
try:
await connection.execute("INSERT INTO mytable VALUES (1, 2, 3)")
except:
await tr.rollback()
raise
else:
await tr.commit()

Uma transação de banco de dados se presta naturalmente a protocolo do gerenciador de contexto: a transação precisa
ser iniciada, dados são modificados com connection.execute , e então um roolback (reversão) ou um commit
(confirmação) precisam acontecer, dependendo do resultado das mudanças.

Em um driver assíncrono como o asyncpg, a configuração e a execução precisam acontecer em corrotinas, para que
outras operações possam ocorrer de forma concorrente. Entretando, a implementação do comando with clássico não
suporta corrotinas na implementação dos métodos __enter__ ou __exit__ .

Por essa razão a PEP 492—Coroutines with async and await syntax (Corrotinas com async e await) (https://fpy.li/pep492)
(EN) introduziu o comando async with , que funciona com gerenciadores de contexto assíncronos: objetos
implementando os métodos __aenter__ e __aexit__ como corrotinas.

Com async with , o Exemplo 4 pode ser escrito como esse outro trecho da documentação do asyncpg (https://fpy.li/21-11):

PYTHON3
async with connection.transaction():
await connection.execute("INSERT INTO mytable VALUES (1, 2, 3)")

Na classe asyncpg.Transaction (https://fpy.li/21-13), o método corrotina __aenter__ executa await self.start() , e


a corrotina __aexit__ espera pelos métodos corrotina privados rollback ou commit , dependendo da ocorrência
ou não de uma exceção. Usar corrotinas para implementar Transaction como um gerenciador de contexto
assíncrono permite ao asyncpg controlar, de forma concorrente, muitas transações simultâneas.

Caleb Hattingh sobre o asyncpg


Outro detalhe fantástico sobre o asyncpg é que ele também contorna a falta de suporte à alta-
concorrência do PostgreSQL (que usa um processo servidor por conexão) implementando um pool
👉 DICA de conexões para conexões internas ao próprio Postgres.

Isso significa que você não precisa de ferramentas adicionais (por exemplo o pgbouncer), como
explicado na documentação (https://fpy.li/21-14) (EN) do asyncpg.[293]

Voltando ao flags_asyncio.py, a classe AsyncClient do httpx é um gerenciador de contexto assíncrono, então pode
usar esperáveis em seus métodos corrotina especiais __aenter__ e __aexit__ .
A seção Seção 21.10.1.3 mostra como usar a contextlib do Python para criar um gerenciador de
✒️ NOTA contexto assíncrono sem precisar escrever uma classe. Essa explicação aparece mais tarde nesse
capítulo por causa de um pré-requsito: a seção Seção 21.10.1.

Agora vamos melhorar o exemplo asyncio de download de bandeiras com uma barra de progresso, que nos levará a
explorar um pouco mais da API do asyncio.

21.7. Melhorando o download de bandeiras asyncio


Vamos recordar a seção Seção 20.5, na qual o conjunto de exemplos flags2 compartilhava a mesma interface de linha
de comando, e todos mostravam uma barra de progresso enquanto os downloads aconteciam. Eles também incluíam
tratamento de erros.

Encorajo você a brincar com os exemplos flags2 , para desenvolver uma intuição sobre o
funcionamento de clientes HTTP concorrentes. Use a opção -h para ver a tela de ajuda no Exemplo
10. Use as opções de linha de comando -a , -e , e -l para controlar o número de downloads, e a
👉 DICA opção -m para estabelecer o número de downloads concorrentes. Execute testes com os servidores
LOCAL , REMOTE , DELAY , e ERROR . Descubra o número ótimo de downloads concorrentes para
maximizar a taxa de transferência de cada servidor. Varie as opções dos servidores de teste, como
descrito no Configurando os servidores de teste.

Por exemplo, o Exemplo 5 mostra uma tentativa de obter 100 bandeiras ( -al 100 ) do servidor ERROR , usando 100
conexões concorrentes ( -m 100 ). Os 48 erros no resultado são ou HTTP 418 ou erros de tempo de espera excedido
(time-out)—o [mau]comportamento esperado do slow_server.py.

Exemplo 5. Running flags2_asyncio.py

$ python3 flags2_asyncio.py -s ERROR -al 100 -m 100


ERROR site: http://localhost:8002/flags
Searching for 100 flags: from AD to LK
100 concurrent connections will be used.
100%|█████████████████████████████████████████| 100/100 [00:03<00:00, 30.48it/s]
--------------------
52 flags downloaded.
48 errors.
Elapsed time: 3.31s

Aja de forma responsável ao testar clientes concorrentes


Mesmo que o tempo total de download não seja muito diferente entre os clientes HTTP na versão
⚠️ AVISO com threads e na versão asyncio HTTP , o asyncio é capaz de enviar requisições mais rápido, então
aumenta a probabilidade do servidor suspeitar de um ataque DoS. Para exercitar esses clientes
concorrentes em sua capacidade máxima, por favor use servidores HTTP locais em seus testes, como
explicado no Configurando os servidores de teste.

Agora vejamos como o flags2_asyncio.py é implementado.

21.7.1. Usando asyncio.as_completed e uma thread


No Exemplo 3, passamos várias corrotinas para asyncio.gather , que devolve uma lista com os resultados das
corrotinas na ordem em que foram submetidas. Isso significa que asyncio.gather só pode retornar quando todos os
esperáveis terminarem. Entretanto, para atualizar uma barra de progresso, precisamos receber cada um dos
resultados assim que eles estejam prontos.
Felizmente existe um equivalente asyncio da função geradora as_completed que usamos no exemplo de pool de
threads com a barra de progresso, (Exemplo 16).

O Exemplo 6 mostra o início do script flags2_asyncio.py, onde as corrotinas get_flag e download_one são definidas.
O Exemplo 7 lista o restante do código-fonte, com supervisor e download_many . O script é maior que
flags_asyncio.py por causa do tratamento de erros.

Exemplo 6. flags2_asyncio.py: parte superior (inicial) do script; o resto do código está no Exemplo 7

PY
import asyncio
from collections import Counter
from http import HTTPStatus
from pathlib import Path

import httpx
import tqdm # type: ignore

from flags2_common import main, DownloadStatus, save_flag

# low concurrency default to avoid errors from remote site,


# such as 503 - Service Temporarily Unavailable
DEFAULT_CONCUR_REQ = 5
MAX_CONCUR_REQ = 1000

async def get_flag(client: httpx.AsyncClient, # (1)


base_url: str,
cc: str) -> bytes:
url = f'{base_url}/{cc}/{cc}.gif'.lower()
resp = await client.get(url, timeout=3.1, follow_redirects=True) # (2)
resp.raise_for_status()
return resp.content

async def download_one(client: httpx.AsyncClient,


cc: str,
base_url: str,
semaphore: asyncio.Semaphore,
verbose: bool) -> DownloadStatus:
try:
async with semaphore: # (3)
image = await get_flag(client, base_url, cc)
except httpx.HTTPStatusError as exc: # (4)
res = exc.response
if res.status_code == HTTPStatus.NOT_FOUND:
status = DownloadStatus.NOT_FOUND
msg = f'not found: {res.url}'
else:
raise
else:
await asyncio.to_thread(save_flag, image, f'{cc}.gif') # (5)
status = DownloadStatus.OK
msg = 'OK'
if verbose and msg:
print(cc, msg)
return status

1. get_flag é muito similar à versão sequencial no Exemplo 14. Primeira diferença: ele exige o parâmetro client .

2. Segunda e terceira diferenças: .get é um método de AsyncClient , e é uma corrotina, então precisamos await
por ela.
3. Usa o semaphore como um gerenciador de contexto assíncrono, assim o programa como um todo não é bloqueado;
apenas essa corrotina é suspensa quando o contador do semáforo é zero. Veja mais sobre isso em Semáforos no
Python.
4. A lógica de tratamento de erro é idêntica à de download_one , do Exemplo 14.

5. Salvar a imagem é uma operação de E/S. Para não bloquear o loop de eventos, roda save_flag em uma thread.
No asyncio, toda a comunicação de rede é feita com corrotinas, mas não E/S de arquivos. Entretanto, E/S de arquivos
também é "bloqueante"—no sentido que ler/escrever arquivos é milhares de vezes mais demorado (https://fpy.li/21-15)
que ler/escrever na RAM. Se você estiver usando armazenamento conectado à rede
(https://pt.wikipedia.org/wiki/Armazenamento_conectado_%C3%A0_rede), isso pode até envolver E/S de rede internamente.

Desde o Python 3.9, a corrotina asyncio.to_thread facilitou delegar operações de arquivo para um pool de threads
fornecido pelo asyncio. Se você precisa suportar Python 3.7 ou 3.8, a seção Seção 21.8 mostra como fazer isso,
adicionando algumas linhas ao seu programa. Mas primeiro, vamos terminar nosso estudo do código do cliente HTTP.

21.7.2. Limitando as requisições com um semáforo


Clientes de rede como os que estamos estudando devem ser limitados ("throttled") (isto é, desacelerados) para que não
martelem o servidor com um número excessivo de requisições concorrentes.

Um semáforo (https://pt.wikipedia.org/wiki/Sem%C3%A1foro_(computa%C3%A7%C3%A3o)) é uma estrutura primitiva de


sincronização, mais flexível que uma trava. Um semáforo pode ser mantido por múltiplas corrotinas, com um número
máximo configurável. Isso o torna ideial para limitar o número de corrotinas concorrentes ativas. O Semáforos no
Python tem mais informações.

No flags2_threadpool.py (Exemplo 16), a limitação era obtida instanciando o ThreadPoolExecutor com o argumento
obrigatório max_workers fixado em concur_req na função download_many . Em flags2_asyncio.py, um
asyncio.Semaphore é criado pela função supervisor (mostrada no Exemplo 7) e passado como o argumento
semaphore para download_one no Exemplo 6.

Semáforos no Python
O cientista da computação Edsger W. Dijkstra inventou o semáforo
(https://pt.wikipedia.org/wiki/Sem%C3%A1foro_(computa%C3%A7%C3%A3o)) no início dos anos 1960. É uma ideia simples,
mas tão flexível que a maioria dos outros objetos de sincronização—tais como as travas e as barreiras—podem
ser construídas a partir de semáforos. Há três classes Semaphore na biblioteca padrão do Python: uma em
threading , outra em multiprocessing , e uma terceira em asyncio . Essas classes são parecidas, mas têm
implementações bem diferentes. Aqui vamos descrever a versão de asyncio .

Um asyncio.Semaphore tem um contador interno que é decrementado toda vez que usamos await no método
corrotina .acquire() , e incrementado quando chamamos o método .release() —que não é uma corrotina
porque nunca bloqueia. O valor inicial do contador é definido quando o Semaphore é instanciado:

PYTHON3
semaphore = asyncio.Semaphore(concur_req)

Invocar await em .acquire() não causa qualquer atraso quando o contador interno é maior que zero. Se o
contador for 0, entretanto, .acquire() suspende a a corrotina que chamou await até que alguma outra
corrotina chame .release() no mesmo Semaphore , incrementando assim o contador.

Em vez de usar esses métodos diretamente, é mais seguro usar o semaphore como um gerenciador de contexto
assíncrono, como fiz na função download_one em Exemplo 6:
PYTHON3
async with semaphore:
image = await get_flag(client, base_url, cc)

O método corrotina Semaphore.__aenter__ espera por .acquire() (usando await internamente), e seu
método corrotina __aexit__ chama .release() . Este async with garante que não mais que concur_req
instâncias de corrotinas get_flags estarão ativas a qualquer dado momento.

Cada uma das classes Semaphore na biblioteca padrão tem uma subclasse BoundedSemaphore , que impõe uma
restrição adicional: o contador interno não pode nunca ficar maior que o valor inicial, quando ocorrerem mais
operações .release() que .acquire() .[294]

Agora vamos olhar o resto do script em Exemplo 7.

Exemplo 7. flags2_asyncio.py: continuação de Exemplo 6


PY
async def supervisor(cc_list: list[str],
base_url: str,
verbose: bool,
concur_req: int) -> Counter[DownloadStatus]: # (1)
counter: Counter[DownloadStatus] = Counter()
semaphore = asyncio.Semaphore(concur_req) # (2)
async with httpx.AsyncClient() as client:
to_do = [download_one(client, cc, base_url, semaphore, verbose)
for cc in sorted(cc_list)] # (3)
to_do_iter = asyncio.as_completed(to_do) # (4)
if not verbose:
to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # (5)
error: httpx.HTTPError | None = None # (6)
for coro in to_do_iter: # (7)
try:
status = await coro # (8)
except httpx.HTTPStatusError as exc:
error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}'
error_msg = error_msg.format(resp=exc.response)
error = exc # (9)
except httpx.RequestError as exc:
error_msg = f'{exc} {type(exc)}'.strip()
error = exc # (10)
except KeyboardInterrupt:
break

if error:
status = DownloadStatus.ERROR # (11)
if verbose:
url = str(error.request.url) # (12)
cc = Path(url).stem.upper() # (13)
print(f'{cc} error: {error_msg}')
counter[status] += 1

return counter

def download_many(cc_list: list[str],


base_url: str,
verbose: bool,
concur_req: int) -> Counter[DownloadStatus]:
coro = supervisor(cc_list, base_url, verbose, concur_req)
counts = asyncio.run(coro) # (14)

return counts

if __name__ == '__main__':
main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)

1. supervisor recebe os mesmos argumentos que a função download_many , mas ele não pode ser invocado
diretamente de main , pois é uma corrotina e não uma função simples como download_many .
2. Cria um asyncio.Semaphore que não vai permitir mais que concur_req corrotinas ativas entre aquelas usando
este semáforo. O valor de concur_req é calculado pela função main de flags2_common.py, baseado nas opções de
linha de comando e nas constantes estabelecidas em cada exemplo.
3. Cria uma lista de objetos corrotina, um para cada chamada à corrotina download_one .

4. Obtém um iterador que vai devolver objetos corrotina quando eles terminarem sua execução. Não coloquei essa
chamada a as_completed diretamente no loop for abaixo porque posso precisar envolvê-la com o iterador
tqdm para a barra de progresso, dependendo da opção do usuário para verbosidade.

5. Envolve o iterador as_completed com a função geradora tqdm , para mostrar o progresso.
6. Declara e inicializa error com None ; essa variável será usada para manter uma exceção além do bloco
try/except , se alguma for levantada.

7. Itera pelos objetos corrotina que terminaram a execução; esse loop é similar ao de download_many em Exemplo
16.
8. await pela corrotina para obter seu resultado. Isso não bloqueia porque as_completed só produz corrotinas que
já terminaram.
9. Essa atribuição é necessária porque o escopo da variável exc é limitado a essa cláusula except , mas preciso
preservar o valor para uso posterior.
10. Mesmo que acima.
11. Se houve um erro, muda o status .

12. Se em modo verboso, extrai a URL da exceção que foi levantada…​


13. …​e extrai o nome do arquivo para mostrar o código do país em seguida.
14. download_many instancia o objeto corrotina supervisor e o passa para o loop de eventos com asyncio.run ,
coletando o contador que supervisor devolve quando o loop de eventos termina.
No Exemplo 7, não podíamos usar o mapeamento de futures para os códigos de país que vimos em Exemplo 16,
porque os esperáveis devolvidos por asyncio.as_completed são os mesmos esperáveis que passamos na chamada a
as_completed . Internamente, o mecanismo do asyncio pode substituir os esperáveis que fornecemos por outros que
irão, no fim, produzir os mesmos resultados.[295]

Já que não podia usar os esperáveis como chaves para recuperar os códigos de país de um dict em
caso de falha, tive que extrair o código de pais da exceção. Para fazer isso, mantive a exceção na
variável error , permitindo sua recuperação fora do bloco try/except . O Python não é uma
👉 DICA linguagem com escopo de bloco: comandos como loops e try/except não criam um escopo local
aos blocos que eles gerenciam. Mas se uma cláusula except vincula uma exceção a uma variável,
como as variáveis exc que acabamos de ver—aquele vínculo só existe no bloco dentro daquela
cláusula except específica.

Isso encerra nossa discussão da funcionalidade de um exemplo usando asyncio similar ao flags2_threadpool.py que
vimos antes.

O próximo exemplo demonstra um modelo simples de execução de uma tarefa assíncrona após outra usando
corrotinas. Isso merece nossa atenção porque qualquer um com experiência prévia em Javascript sabe que rodar um
função assíncrona após outra foi a razão para o padrão de codificação aninhado conhecido como pyramid of doom
(pirâmide da perdição) (https://fpy.li/21-20) (EN). A palavra-chave await desfaz a maldição. Por isso await agora é parte
do Python e do Javascript.

21.7.3. Fazendo múltiplas requisições para cada download


Suponha que você queira salvar cada bandeira com o nome e o código do país, em vez de apenas o código. Agora você
precisa fazer duas requisições HTTP por bandeira: uma para obter a imagem da bandeira propriamente dita, a outra
para obter o arquivo metadata.json, no mesmo diretório da imagem—é nesse arquivo que o nome do país está
registrado.

Coordenar múltiplas requisições na mesma tarefa é fácil no script com threads: basta fazer uma requisição depois a
outra, bloqueando a thread duas vezes, e mantendo os dois dados (código e nome do país) em variáveis locais, prontas
para serem usadas quando os arquivos forem salvo. Se você precisasse fazer o mesmo em um script assíncrono com
callbacks, você precisaria de funções aninhadas, de forma que o código e o nome do país estivessem disponíveis até o
momento em que fosse possível salvar o arquivo, pois cada callback roda em um escopo local diferente. A palavra-
chave await fornece um saída para esse problema, permitindo que você acione as requisições assíncronas uma após a
outra, compartilhando o escopo local da corrotina que dirige as ações.
Se você está trabalhando com programação de aplicações assíncronas no Python moderno e recorre
a uma grande quantidade de callbacks, provavelmente está aplicando modelos antigos, que não
fazem mais sentido no Python atual. Isso é justificável se você estiver escrevendo uma biblioteca que
👉 DICA se conecta a código legado ou a código de baixo nível, que não suportem corrotinas. De qualquer
forma, o Q&A do StackOverflow, "What is the use case for future.add_done_callback()?" (Qual o caso
de uso para future.add_done_callback()?) (https://fpy.li/21-21) (EN) explica porque callbacks são
necessários em código de baixo nível, mas não são muito úteis hoje em dia em código Python a nível
de aplicação.

A terceira variante do script asyncio de download de bandeiras traz algumas mudanças:

get_country
Essa nova corrotina baixa o arquivo metadata.json daquele código de país, e extrai dele o nome do país.

download_one
Essa corrotina agora usa await para delegar para get_flag e para a nova corrotina get_country , usando o
resultado dessa última para compor o nome do arquivo a ser salvo.

Vamos começar com o código de get_country (Exemplo 8). Observe que ele muito similar ao get_flag do Exemplo
6.

Exemplo 8. flags3_asyncio.py: corrotina get_country

PY
async def get_country(client: httpx.AsyncClient,
base_url: str,
cc: str) -> str: # (1)
url = f'{base_url}/{cc}/metadata.json'.lower()
resp = await client.get(url, timeout=3.1, follow_redirects=True)
resp.raise_for_status()
metadata = resp.json() # (2)
return metadata['country'] # (3)

1. Essa corrotina devolve uma string com o nome do país—se tudo correr bem.
2. metadata vai receber um dict Python construído a partir do conteúdo JSON da resposta.
3. Devolve o nome do país.

Agora vamos ver o download_one modificado do Exemplo 9, que tem apenas algumas linhas diferentes da corrotina
de mesmo nome do Exemplo 6.

Exemplo 9. flags3_asyncio.py: corrotina download_one


PY
async def download_one(client: httpx.AsyncClient,
cc: str,
base_url: str,
semaphore: asyncio.Semaphore,
verbose: bool) -> DownloadStatus:
try:
async with semaphore: # (1)
image = await get_flag(client, base_url, cc)
async with semaphore: # (2)
country = await get_country(client, base_url, cc)
except httpx.HTTPStatusError as exc:
res = exc.response
if res.status_code == HTTPStatus.NOT_FOUND:
status = DownloadStatus.NOT_FOUND
msg = f'not found: {res.url}'
else:
raise
else:
filename = country.replace(' ', '_') # (3)
await asyncio.to_thread(save_flag, image, f'{filename}.gif')
status = DownloadStatus.OK
msg = 'OK'
if verbose and msg:
print(cc, msg)
return status

1. Segura o semaphore para await por get_flag …​

2. …​e novamente por get_country .

3. Usa o nome do país para criar um nome de arquivo. Como usuário da linha de comando, não gosto de ver espaços
em nomes de arquivo.

Muito melhor que callbacks aninhados!

Coloquei as chamadas a get_flag e get_country em blocos with separados, controlados pelo semaphore porque é
uma boa prática manter semáforos e travas pelo menor tempo possível.

Eu poderia ter agendado ambos os scripts, get_flag e get_country , em paralelo, usando asyncio.gather , mas se
get_flag levantar uma exceção não haverá imagem para salvar, então seria inútil rodar get_country . Mas há casos
onde faz sentido usar asyncio.gather para acessar várias APIs simultaneamente, em vez de esperar por uma
resposta antes de fazer a próxima requisição

Em flags3_asyncio.py, a sintaxe await aparece seis vezes, e async with três vezes. Espero que você esteja pegando o
jeito da programação assíncrona em Python. Um desafio é saber quando você precisa usar await e quando você não
pode usá-la. A resposta, em princípio, é fácil: você await por corrotinas e outros esperáveis, tais como instâncias de
asyncio.Task . Mas algumas APIs são complexas, misturam corrotinas e funções normais de maneiras aparentemente
arbitrárias, como a classe StreamWriter que usaremos no Exemplo 14.

O Exemplo 9 encerra o grupo de exemplos flags. Vamos agora discutir o uso de executores de threads ou processos na
programação assíncrona.

21.8. Delegando tarefas a executores


Uma vantagem importante do Node.js sobre o Python para programação assíncrona é a biblioteca padrão do Node.js,
que inclui APIs assíncronas para toda a E/S—não apenas para E/S de rede. No Python, se você não for cuidadosa, a E/S
de arquivos pode degradar seriamente o desempenho de aplicações assíncronas, pois ler e escrever no
armazenamento desde a thread principal bloqueia o loop de eventos.
No corrotina download_one de Exemplo 6, usei a seguinte linha para salvar a imagem baixada para o disco:

PY
await asyncio.to_thread(save_flag, image, f'{cc}.gif')

Como mencionado antes, o asyncio.to_thread foi acrescentado no Python 3.9. Se você precisa suportar 3.7 ou 3.8,
substitua aquela linha pelas linhas em Exemplo 10.

Exemplo 10. Linhas para usar no lugar de await asyncio.to_thread

PY
loop = asyncio.get_running_loop() # (1)
loop.run_in_executor(None, save_flag, # (2)
image, f'{cc}.gif') # (3)

1. Obtém uma referência para o loop de eventos.


2. O primeiro argumento é o executor a ser utilizado; passar None seleciona o default, ThreadPoolExecutor , que
está sempre disponível no loop de eventos do asyncio .
3. Você pode passar argumentos posicionais para a função a ser executada, mas se você precisar passar argumentos
de palavra-chave, vai precisar recorrer a functool.partial , como descrito na documentação de
run_in_executor (https://docs.python.org/pt-br/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor).

A função mais recente asyncio.to_thread é mais fácil de usar e mais flexível, já que também aceita argumentos de
palavra-chave.

A própria implementação de asyncio usa run_in_executor debaixo dos panos em alguns pontos. Por exemplo, a
corrotina loop.getaddrinfo(…) , que vimos no Exemplo 1 é implementada chamando a função getaddrinfo do
módulo socket —uma função bloqueante que pode levar alguns segundos para retornar, pois depende de resolução
de DNS.

Um padrão comum em APIs assíncronas é encobrir chamadas bloqueantes que sejam detalhes de implementação nas
corrotinas usando run_in_executor internamente. Dessa forma, é possível apresentar uma interface consistente de
corrotinas a serem acionadas com await e esconder as threads que precisam ser usadas por razões pragmáticas. O
driver assíncrono para o MongoDB Motor (https://fpy.li/21-23) tem uma API compatível com async/await que na
verdade é uma fachada, encobrindo um núcleo de threads que conversa com o servidor de banco de dados. A. Jesse
Jiryu Davis, o principal desenvolvedor do Motor, explica suas razões em “Response to ‘Asynchronous Python and
Databases’” (“_Resposta a ‘O Python Assíncrono e os Bancos de Dados’”) (https://fpy.li/21-24). Spoiler: Davis descobriu que
um pool de threads tem melhor desempenho no caso de uso específico de um driver de banco de dados—apesar do
mito que abordagens assíncronas são sempre mais rápidas que threads para E/S de rede.

A principal razão para passar um Executor explícito para loop.run_in_executor é utilizar um


ProcessPoolExecutor , se a função a ser executada for de uso intensivo da CPU. Dessa forma ela rodará em um
processo Python diferente, evitando a disputa pela GIL. Por seu alto custo de inicialização, seria melhor iniciar o
ProcessPoolExecutor no supervisor , e passá-lo para as corrotinas que precisem utilizá-lo.

Caleb Hattingh—O autor de Using Asyncio in Python (https://fpy.li/hattingh) (O' Reilly)—é um dos revisores técnicos desse
livro, e sugeriu que eu acrescentasse o seguinte aviso sobre executores e o asyncio.
O aviso de Caleb sobre run_in_executors
Usar run_in_executor pode produzir problemas difíceis de depurar, já que o cancelamento não
funciona da forma que se esperaria. Corrotinas que usam executores apenas fingem terminar: a
thread subjacente (se for um ThreadPoolExecutor ) não tem um mecanismo de cancelamento. Por
⚠️ AVISO exemplo, uma thread de longa duração criada dentro de uma chamada a run_in_executor pode
impedir que seu programa asyncio encerre de forma limpa: asyncio.run vai esperar para retornar
até o executor terminar inteiramente, e vai esperar para sempre se os serviços iniciados pelo
executor não pararem sozinhos de alguma forma. Minha barba branca me inclina a desejar que
aquela função se chamasse run_in_executor_uncancellable .

Agora saímos de scripts cliente para escrever servidores com o asyncio .

21.9. Programando servidores asyncio


O exemplo clássico de um servidor TCP de brinquedo é um servidor eco
(https://docs.python.org/pt-br/3/library/asyncio-stream.html#tcp-echo-server-using-streams). Vamos escrever brinquedos um
pouco mais interessantes: utilitários de servidor para busca de caracteres Unicode, primeiro usando HTTP com a
FastAPI, depois usando TCP puro apenas com asyncio .

Esse servidores permitem que os usuários façam consultas sobre caracteres Unicode baseadas em palavras em seus
nomes padrão no módulo unicodedata que discutimos na seção Seção 4.9. A Figura 2 mostra uma sessão com o
web_mojifinder.py, o primeiro servidor que escreveremos.

Figura 2. Janela de navegador mostrando os resultados da busca por "mountain" no serviço web_mojifinder.py.

A lógica de busca no Unicode nesses exemplos é a classe InvertedIndex no módulo charindex.py no repositório de
código do Python Fluente (https://fpy.li/code). Não há nada concorrente naquele pequeno módulo, então vou dar apenas
um explicação breve sobre ele, no box opcional a seguir. Você pode pular para a implementação do servidor HTTP na
seção Seção 21.9.1.

Conhecendo o índice invertido


Um índice invertido normalmente mapeia palavras a documentos onde elas ocorrem. Nos exemplos mojifinder,
cada "documento" é o nome de um caractere Unicode. A classe charindex.InvertedIndex indexa cada palavra
que aparece no nome de cada caractere no banco de dados Unicode, e cria um índice invertido em um
defaultdict . Por exemplo, para indexar o caractere U+0037—DIGIT SEVEN—o construtor de InvertedIndex
anexa o caractere '7' aos registros sob as chaves 'DIGIT' e 'SEVEN' . Após indexar os dados do Unicode 13.0.0
incluídos no Python 3.10, 'DIGIT' será mapeado para 868 caracteres que tem essa palavra em seus nomes; e
'SEVEN' para 143, incluindo U+1F556—CLOCK FACE SEVEN OCLOCK e U+2790—DINGBAT NEGATIVE CIRCLED
SANS-SERIF DIGIT SEVEN.
Veja a Figura 3 para uma demonstração usando os registro para 'CAT' e 'FACE' .[296]

Figura 3. Explorando o atributo entries e o método search de InvertedIndex no console do Python

O método InvertedIndex.search quebra a consulta em palavras separadas, e devolve a intersecção dos


registros para cada palavra. É por isso que buscar por "face" encontra 171 resultados, "cat" encontra 14, mas "cat
face" apenas 10.

Essa é a bela ideia por trás dos índices invertidos: uma pedra fundamental da recuperação de informação—a
teoria por trás dos mecanismos de busca. Veja o artigo "Listas Invertidas"
(https://pt.wikipedia.org/wiki/Listas_invertidas) na Wikipedia para saber mais.

21.9.1. Um serviço web com FastAPI


Escrevi o próximo exemplo—web_mojifinder.py—usando a FastAPI (https://fpy.li/21-28): uma das frameworks ASGI de
desenvolvimento Web do Python, mencionada na ASGI—Asynchronous Server Gateway Interface. A Figura 2 é uma
captura de tela da interface de usuário. É uma aplicação muito simples, de uma página só (SPA, Single Page
Application): após o download inicial do HTML, a interface é atualizada via Javascript no cliente, em comunicação com
o servidor.

A FastAPI foi projetada para implementar o lado servidor de SPAs and apps móveis, que consistem principalmente de
pontos de acesso de APIs web, devolvendo respostas JSON em vez de HTML renderizado no servidor. A FastAPI se vale
de decoradores, dicas de tipo e introspecção de código para eliminar muito do código repetitivo das APIs web, e
também publica automaticamente uma documentação no padrão OpenAPI—a.k.a. Swagger (https://fpy.li/21-29)—para a
API que criamos. A Figura 4 mostra a página /docs para o web_mojifinder.py, gerada automaticamente.
Figura 4. Schema OpenAPI gerado automaticamente para o ponto de acesso /search .

O Exemplo 11 é o código do web_mojifinder.py, mas aquele é apenas o código do lado servidor. Quando você acessa a
URL raiz / , o servidor envia o arquivo form.html, que contém 81 linhas de código, incluindo 54 linhas de Javascript
para comunicação com o servidor e preenchimento de uma tabela com os resultados. Se você estiver interessado em
ler Javascript puro sem uso de frameworks, vá olhar o 21-async/mojifinder/static/form.html no repositório de código do
Python Fluente (https://fpy.li/code).

Para rodar o web_mojifinder.py, você precisa instalar dois pacotes e suas dependências: FastAPI e uvicorn.[297]

Este é o comando para executar o Exemplo 11 com uvicorn em modo de desenvolvimento:

SHELL
$ uvicorn web_mojifinder:app --reload

os parâmetros são:

web_mojifinder:app

O nome do pacote, dois pontos, e o nome da aplicação ASGI definida nele— app é o nome usado por convenção.
--reload
Faz o uvicorn monitorar mudanças no código-fonte da aplicação, e recarregá-la automaticamente. Útil apenas
durante o desenvolvimento.
Vamos agora olhar o código-fonte do web_mojifinder.py.

Exemplo 11. web_mojifinder.py: código-fonte completo

PY
from pathlib import Path
from unicodedata import name

from fastapi import FastAPI


from fastapi.responses import HTMLResponse
from pydantic import BaseModel

from charindex import InvertedIndex

STATIC_PATH = Path(__file__).parent.absolute() / 'static' # (1)

app = FastAPI( # (2)


title='Mojifinder Web',
description='Search for Unicode characters by name.',
)

class CharName(BaseModel): # (3)


char: str
name: str

def init(app): # (4)


app.state.index = InvertedIndex()
app.state.form = (STATIC_PATH / 'form.html').read_text()

init(app) # (5)

@app.get('/search', response_model=list[CharName]) # (6)


async def search(q: str): # (7)
chars = sorted(app.state.index.search(q))
return ({'char': c, 'name': name(c)} for c in chars) # (8)

@app.get('/', response_class=HTMLResponse, include_in_schema=False)


def form(): # (9)
return app.state.form

# no main funcion # (10)

1. Não relacionado ao tema desse capítulo, mas digno de nota: o uso elegante do operador / sobrecarregado por
pathlib .[298]

2. Essa linha define a app ASGI. Ela poderia ser tão simples como app = FastAPI() . Os parâmetros mostrados são
metadata para a documentação auto-gerada.
3. Um schema pydantic para uma resposta JSON, com campos char e name .[299]

4. Cria o index e carrega o formulário HTML estático, anexando ambos ao app.state para uso posterior.
5. Roda init quando esse módulo é carregado pelo servidor ASGI.
6. Rota para o ponto de acesso /search ; response_model usa aquele modelo CharName do pydantic para descrever
o formato da resposta.
7. A FastAPI assume que qualquer parâmetro que apareça na assinatura da função ou da corrotina e que não esteja
no caminho da rota será passado na string de consulta HTTP, isto é, /search?q=cat . Como q não tem default, a
FastAPI devolverá um status 422 (Unprocessable Entity, Entidade Não-Processável) se q não estiver presente na
string da consulta.
8. Devolver um iterável de dicts compatível com o schema response_model permite ao FastAPI criar uma resposta
JSON de acordo com o response_model no decorador @app.get ,
9. Funções regulares (isto é, não-assíncronas) também podem ser usadas para produzir respostas.
10. Este módulo não tem uma função principal. É carregado e acionado pelo servidor ASGI—neste exemplo, o uvicorn.
O Exemplo 11 não tem qualquer chamada direta ao asyncio . O FastAPI é construído sobre o tollkit ASGI Starlette, que
por sua vez usa o asyncio .

Observe também que o corpo de search não usa await , async with , ou async for , e assim poderia ser uma
função normal. Defini search como um corrotina apenas para mostrar que o FastAPI sabe como lidar com elas. Em
uma aplicação real, a maioria dos pontos de acesso serão consultas a bancos de dados ou acessos a outros servidores
remotos, então é uma vantagem crítica do FastAPI—e de frameworks ASGI em geral— suportarem corrotinas que
podem se valer de bibliotecas assíncronas para E/S de rede.

As funções init e form , escritas para carregar e entregar o formulário em HTML estático é uma
improvisação que escrevi para manter esse exemplo curto e fácil de executar. A melhor prática
recomendada é ter um proxy/balanceador de carga na frente do ASGI, para gerenciar todos os
recursos estáticos, e também usar uma CDN (Content Delivery Network, Rede de Entrega de
👉 DICA Conteúdo) quando possível. Um proxy/balanceador de carga desse tipo é o Traefik (https://fpy.li/21-32)
(EN), que se auto-descreve como um "roteador de ponta (edge router)", que "recebe requisições em
nome de seu sistema e descobre quais componentes são responsáveis por lidar com elas." O _FastAPI
tem scripts de geração de projeto (https://fastapi.tiangolo.com/pt/project-generation/) que preparam seu
código para fazer isso.

Os entusiastas pela tipagem podem ter notado que não há dicas de tipo para os resultados devolvidos por search e
form . Em vez disto, o FastAPI conta com o argumento de palavra-chave response_model= nos decoradores de rota. A
página "Modelo de Resposta" (https://fpy.li/21-34) (EN) na documentação do FastAPI explica:

“ Oresultado
modelo de resposta é declarado neste parâmetro em vez de como uma anotação de tipo de
devolvido por uma função, porque a função de rota pode não devolver aquele modelo
de resposta mas sim um dict , um objeto banco de dados ou algum outro modelo, e então usar
o response_model para realizar a limitação de campo e a serialização.

Por exemplo, em search , eu devolvi um gerador de itens dict e não uma lista de objetos CharName , mas isso é o
suficiente para o FastAPI e o pydantic validarem meus dados e construírem a resposta JSON apropriada, compatível
com response_model=list[CharName] .

Agora vamos nos concentrar no script tcp_mojifinder.py, que responde às consultas, na Figura 5.

21.9.2. Um servidor TCP asyncio


O programa tcp_mojifinder.py usa TCP puro para se comunicar com um cliente como o Telnet ou o Netcat, então pude
escrevê-lo usando asyncio sem dependências externas—e sem reinventar o HTTP. O Figura 5 mostra a interface de
texto do usuário.
Figura 5. Sessão de telnet com o servidor tcp_mojifinder.py: consultando "fire."

Este programa é duas vezes mais longo que o web_mojifinder.py, então dividi sua apresentação em três partes: Exemplo
12, Exemplo 14, e Exemplo 15. O início de tcp_mojifinder.py—incluindo os comandos import —está no Exemplo 14,mas
vou começar descrevendo a corrotina supervisor e a função main que controla o programa.

Exemplo 12. tcp_mojifinder.py: um servidor TCP simples; continua em Exemplo 14

PY
async def supervisor(index: InvertedIndex, host: str, port: int) -> None:
server = await asyncio.start_server( # (1)
functools.partial(finder, index), # (2)
host, port) # (3)

socket_list = cast(tuple[TransportSocket, ...], server.sockets) # (4)


addr = socket_list[0].getsockname()
print(f'Serving on {addr}. Hit CTRL-C to stop.') # (5)
await server.serve_forever() # (6)

def main(host: str = '127.0.0.1', port_arg: str = '2323'):


port = int(port_arg)
print('Building index.')
index = InvertedIndex() # (7)
try:
asyncio.run(supervisor(index, host, port)) # (8)
except KeyboardInterrupt: # (9)
print('\nServer shut down.')

if __name__ == '__main__':
main(*sys.argv[1:])

1. Este await rapidamente recebe um instância de asyncio.Server , um servidor TCP baseado em sockets. Por
default, start_server cria e inicia o servidor, então ele está pronto para receber conexões.
2. O primeiro argumento para start_server é client_connected_cb , um callback para ser executado quando a
conexão com um novo cliente se inicia. O callback pode ser uma função ou uma corrotina, mas precisa aceitar
exatamente dois argumentos: um asyncio.StreamReader e um asyncio.StreamWriter . Entretanto, minha
corrotina finder também precisa receber um index , então usei functools.partial para vincular aquele
parâmetro e obter um invocável que receber o leitor ( asyncio.StreamReader ) e o escritor
( asyncio.StreamWriter ). Adaptar funções do usuário a APIs de callback é o caso de uso mais comum de
functools.partial .
3. host e port são o segundo e o terceiro argumentos de start_server . Veja a assinatura completa na
documentação do asyncio (https://docs.python.org/pt-br/3/library/asyncio-stream.html#asyncio.start_server).
4. Este cast é necessário porque o typeshed tem uma dica de tipo desatualizada para a propriedade sockets da
classe Server —isso em maio de 2021. Veja Issue #5535 no typeshed (https://fpy.li/21-36).[300]
5. Mostra o endereço e a porta do primeiro socket do servidor.
6. Apesar de start_server já ter iniciado o servidor como uma tarefa concorrente, preciso usar o await no método
server_forever , para que meu supervisor seja suspenso aqui. Sem essa linha, o supervisor retornaria
imediatamente, encerrando o loop iniciado com asyncio.run(supervisor(…)) , e fechando o programa. A
documentação de Server.serve_forever
(https://docs.python.org/pt-br/3/library/asyncio-eventloop.html#asyncio.Server.serve_forever) diz: "Este método pode ser
chamado se o servidor já estiver aceitando conexões."
7. Constrói o índice invertido.[301]
8. Inicia o loop de eventos rodando supervisor .

9. Captura KeyboardInterrupt para evitar o traceback dispersivo quando encerro o servidor com Ctrl-C, no
terminal onde ele está rodando.
Pode ser mais fácil entender como o controle flui em tcp_mojifinder.py estudando a saída que ele gera no console do
servidor, listada em Exemplo 13.

Exemplo 13. tcp_mojifinder.py: isso é o lado servidor da sessão mostrada na Figura 5

TEXT
$ python3 tcp_mojifinder.py
Building index. # (1)
Serving on ('127.0.0.1', 2323). Hit Ctrl-C to stop. # (2)
From ('127.0.0.1', 58192): 'cat face' # (3)
To ('127.0.0.1', 58192): 10 results.
From ('127.0.0.1', 58192): 'fire' # (4)
To ('127.0.0.1', 58192): 11 results.
From ('127.0.0.1', 58192): '\x00' # (5)
Close ('127.0.0.1', 58192). # (6)
^C # (7)
Server shut down. # (8)
$

1. Saída de main . Antes da próxima linha surgir, vi um intervalo de 0,6s na minha máquina, enquanto o índice era
construído.
2. Saída de supervisor .

3. Primeira iteração de um loop while em finder . A pilha TCP/IP atribuiu a porta 58192 a meu cliente Telnet. Se
você conectar diversos clientes ao servidor, verá suas várias portas aparecerem na saída.
4. Segunda iteração do loop while em finder .

5. Eu apertei Ctrl-C no terminal cliente; o loop while em finder termina.


6. A corrotina finder mostra essa mensagem e então encerra. Enquanto isso o servidor continua rodando, pronto
para receber outro cliente.
7. Aperto Ctrl-C no terminal do servidor; server.serve_forever é cancelado, encerrando supervisor e o loop de
eventos.
8. Saída de main .
Após main construir o índice e iniciar o loop de eventos, supervisor rapidamente mostra a mensagem Serving
on… , e é suspenso na linha await server.serve_forever() . Nesse ponto o controle flui para dentro do loop de
eventos e lá permanece, voltando ocasionalmente para a corrotina finder , que devolve o controle de volta para o
loop de eventos sempre que precisa esperar que a rede envie ou receba dados.

Enquanto o loop de eventos estiver ativo, uma nova instância da corrotina finder será iniciada para cada cliente que
se conecte ao servidor. Dessa forma, múltiplos clientes podem ser atendidos de forma concorrente por esse servidor
simples. Isso segue até que ocorra um KeyboardInterrupt no servidor ou que seu processo seja eliminado pelo SO.

Agora vamos ver o início de tcp_mojifinder.py, com a corrotina finder .

Exemplo 14. tcp_mojifinder.py: continuação de Exemplo 12

PY
import asyncio
import functools
import sys
from asyncio.trsock import TransportSocket
from typing import cast

from charindex import InvertedIndex, format_results # (1)

CRLF = b'\r\n'
PROMPT = b'?> '

async def finder(index: InvertedIndex, # (2)


reader: asyncio.StreamReader,
writer: asyncio.StreamWriter) -> None:
client = writer.get_extra_info('peername') # (3)
while True: # (4)
writer.write(PROMPT) # can't await! # (5)
await writer.drain() # must await! # (6)
data = await reader.readline() # (7)
if not data: # (8)
break
try:
query = data.decode().strip() # (9)
except UnicodeDecodeError: # (10)
query = '\x00'
print(f' From {client}: {query!r}') # (11)
if query:
if ord(query[:1]) < 32: # (12)
break
results = await search(query, index, writer) # (13)
print(f' To {client}: {results} results.') # (14)

writer.close() # (15)
await writer.wait_closed() # (16)
print(f'Close {client}.') # (17)

1. format_results é útil para mostrar os resultado de InvertedIndex.search em uma interface de usuário


baseada em texto, como a linha de comando ou uma sessão Telnet.
2. Para passar finder para asyncio.start_server , a envolvi com functools.partial , porque o servidor espera
uma corrotina ou função que receba apenas os argumentos reader e writer .
3. Obtém o endereço do cliente remoto ao qual o socket está conectado.
4. Esse loop controla um diálogo que persiste até um caractere de controle ser recebido do cliente.
5. O método StreamWriter.write não é uma corrotina, é apenas um função normal; essa linha envia o prompt ?> .

6. O StreamWriter.drain esvazia o buffer de writer ; ela é uma corrotina, então precisa ser acionada com await .

7. StreamWriter.readline é um corrotina que devolve bytes .

8. Se nenhum byte foi recebido, o cliente fechou a conexão, então sai do loop.
9. Decodifica os bytes para str , usando a codificação UTF-8 como default.

10. Pode ocorrer um UnicodeDecodeError quando o usuário digita Ctrl-C e o cliente Telnet envia caracteres de
controle; se isso acontecer, substitui a consulta pelo caractere null, para simplificar.
11. Registra a consulta no console do servidor.
12. Sai do loop se um caractere de controle ou null foi recebido.
13. search realiza a busca efetiva; o código será apresentado a seguir.
14. Registra a resposta no console do servidor.
15. Fecha o StreamWriter .

16. Espera até StreamWriter fechar. Isso é recomendado na documentação do método .close()
(https://docs.python.org/pt-br/3/library/asyncio-stream.html#asyncio.StreamWriter.close).

17. Registra o final dessa sessão do cliente no console do servidor.


O último pedaço desse exemplo é a corrotina search , listada no Exemplo 15.

Exemplo 15. tcp_mojifinder.py: corrotina search

PY
async def search(query: str, # (1)
index: InvertedIndex,
writer: asyncio.StreamWriter) -> int:
chars = index.search(query) # (2)
lines = (line.encode() + CRLF for line # (3)
in format_results(chars))
writer.writelines(lines) # (4)
await writer.drain() # (5)
status_line = f'{"─" * 66} {len(chars)} found' # (6)
writer.write(status_line.encode() + CRLF)
await writer.drain()
return len(chars)

1. search tem que ser uma corrotina, pois escreve em um StreamWriter e precisa usar seu método corrotina
.drain() .

2. Consulta o índice invertido.


3. Essa expressão geradora vai produzir strings de bytes codificadas em UTF-8 com o ponto de código Unicode, o
caractere efetivo, seu nome e uma sequência CRLF (Return+Line Feed), isto é, b’U+0039\t9\tDIGIT NINE\r\n' .
4. Envia a lines . Surpreendentemente, writer.writelines não é uma corrotina.
5. Mas writer.drain() é uma corrotina. Não esqueça do await !

6. Cria depois envia uma linha de status.

Observe que toda a E/S de rede em tcp_mojifinder.py é feita em bytes ; precisamos decodificar os bytes recebidos da
rede, e codificar strings antes de enviá-las. No Python 3, a codificação default é UTF-8, e foi o que usei implicitamente
em todas as chamadas a encode e decode nesse exemplo.
Veja que alguns dos métodos de E/S são corrotinas, e precisam ser acionados com await , enquanto
outros são funções simples, Por exemplo, StreamWriter.write é uma função normal, porque
escreve em um buffer. Por outro lado, StreamWriter.drain —que esvazia o buffer e executa o E/S
⚠️ AVISO de rede—é uma corrotina, assim como StreamReader.readline —mas não
StreamWriter.writelines ! Enquanto estava escrevendo a primeira edição desse livro, a
documentação da API asyncio API docs foi melhorada pela indicação clara das corrotinas como tal
(https://docs.python.org/pt-br/3/library/asyncio-stream.html#streamwriter).

O código de tcp_mojifinder.py se vale da API Streams (https://fpy.li/21-40) de alto nível do asyncio , que fornece um
servidor pronto para ser usado, de forma que basta implemetar uma função de processamento, que pode ser um
callback simples ou uma corrotina. Há também uma API de Transportes e Protocolos
(https://docs.python.org/pt-br/3/library/asyncio-protocol.html) (EN) de baixo nível, inspirada pelas abstrações transporte e
protocolo da framework Twisted. Veja a documentação do asyncio para maiores informações, incluindo os servidores
echo e clientes TCP e UDP (https://docs.python.org/pt-br/3/library/asyncio-protocol.html#tcp-echo-server) implementados com
aquela API de nível mais baixo.

Nosso próximo tópico é async for e os objetos que a fazem funcionar.

21.10. Iteração assíncrona e iteráveis assíncronos


Na seção Seção 21.6 vimos como async with funciona com objetos que implementam os métodos __aenter__ and
__aexit__ , devolvendo esperáveis—normalmente na forma de objetos corrotina.

Se forma similar, async for funciona com iteráveis assíncronos: objetos que implementam __aiter__ . Entretanto,
__aiter__ precisa ser um método regular—não um método corrotina—e precisa devolver um iterador assíncrono.

Um iterador assíncrono fornece um método corrotina __anext__ que devolve um esperável—muitas vezes um objeto
corrotina. Também se espera que eles implementem __aiter__ , que normalmente devolve self . Isso espelha a
importante distinção entre iteráveis e iteradores que discutimos na seção Seção 17.5.2.

A documentação (https://fpy.li/21-43) (EN) do driver assíncrono de PostgreSQL aiopg traz um exemplo que ilustra o uso de
async for para iterar sobre as linhas de cursor de banco de dados.

PYTHON3
async def go():
pool = await aiopg.create_pool(dsn)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
ret = []
async for row in cur:
ret.append(row)
assert ret == [(1,)]

Nesse exemplo, a consulta vai devolver uma única linha, mas em um cenário realista é possível receber milhares de
linhas na resposta a um SELECT . Para respostas grandes, o cursor não será carregado com todas as linhas de uma vez
só. Assim é importante que async for row in cur: não bloqueie o loop de eventos enquanto o cursor pode estar
esperando por linhas adicionais. Ao implementar o cursor como um iterador assíncrono, aiopg pode devolver o
controle para o loop de eventos a cada chamada a __anext__ , e continuar mais tarde, quando mais linhas cheguem
do PostgreSQL.

21.10.1. Funções geradoras assíncronas


Você pode implementar um iterador assíncrono escrevendo uma classe com __anext__ e __aiter__ , mas há um
jeito mais simples: escreve uma função declarada com async def que use yield em seu corpo. Isso é paralelo à
forma como funções geradoras simplificam o modelo clássico do Iterador.

Vamos estudar um exemplo simples usando async for e implementando um gerador assíncrono. No Exemplo 1
vimos blogdom.py, um script que sondava nomes de domínio. Suponha agora que encontramos outros usos para a
corrotina probe definida ali, e decidimos colocá-la em um novo módulo—domainlib.py—junto com um novo gerador
assíncrono multi_probe , que recebe uma lista de nomes de domínio e produz resultados conforme eles são sondados.

Vamos ver a implementação de domainlib.py logo, mas primeiro examinaremos como ele é usado com o novo console
assíncrono do Python.

Experimentando com o console assíncrono do Python


Desde o Python 3.8 (https://fpy.li/21-44), é possível rodar o interpretador com a opção de linha de comando -m asyncio ,
para obter um "async REPL": um console de Python que importa asyncio , fornece um loop de eventos ativo, e aceita
await , async for , e async with no prompt principal—que em qualquer outro contexto são erros de sintaxe quando
usados fora de corrotinas nativas.[302]

Para experimentar com o domainlib.py, vá ao diretório 21-async/domains/asyncio/ na sua cópia local do repositório de
código do Python Fluente (https://fpy.li/code). Aí rode::

SHELL
$ python -m asyncio

Você verá o console iniciar, de forma similar a isso:

SHELL
asyncio REPL 3.9.1 (v3.9.1:1e5d33e9b9, Dec 7 2020, 12:10:52)
[Clang 6.0 (clang-600.0.57)] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>>

Veja como o cabeçalho diz que você pode usar await em vez de asyncio.run() —para acionar corrotinas e outros
esperáveis. E mais: eu não digitei import asyncio . O módulo asyncio é automaticamente importado e aquela linha
torna esse fato claro para o usuário.

Vamos agora importar domainlib.py e brincar com suas duas corrotinas: probe and multi_probe (Exemplo 16).

Exemplo 16. Experimentando com domainlib.py após executar python3 -m asyncio


PYCON
>>> await asyncio.sleep(3, 'Rise and shine!') # (1)
'Rise and shine!'
>>> from domainlib import *
>>> await probe('python.org') # (2)
Result(domain='python.org', found=True) # (3)
>>> names = 'python.org rust-lang.org golang.org no-lang.invalid'.split() # (4)
>>> async for result in multi_probe(names): # (5)
... print(*result, sep='\t')
...
golang.org True # (6)
no-lang.invalid False
python.org True
rust-lang.org True
>>>

1. Tente um simples await para ver o console assíncrono em ação. Dica: asyncio.sleep() pode receber um
segundo argumento opcional que é devolvido quando você usa await com ele.
2. Acione a corrotina probe .

3. A versão domainlib de probe devolve uma tupla nomeada Result .

4. Faça um lista de domínios. O domínio de nível superior .invalid é reservado para testes. Consultas ao DNS por
tais domínios sempre recebem uma resposta NXDOMAIN dos servidores DNS, que quer dizer "aquele domínio não
existe."[303]
5. Itera com async for sobre o gerador assíncrono multi_probe para mostrar os resultados.
6. Note que os resultados não estão na ordem em que os domínios foram enviados a multiprobe . Eles aparecem
quando cada resposta do DNS chega.

O Exemplo 16 mostra que multi_probe é um gerador assíncrono, pois ele é compatível com async for . Vamos
executar mais alguns experimentos, continuando com o Exemplo 17.

Exemplo 17. mais experimentos, continuando de Exemplo 16

PYCON
>>> probe('python.org') # (1)
<coroutine object probe at 0x10e313740>
>>> multi_probe(names) # (2)
<async_generator object multi_probe at 0x10e246b80>
>>> for r in multi_probe(names): # (3)
... print(r)
...
Traceback (most recent call last):
...
TypeError: 'async_generator' object is not iterable

1. Chamar uma corrotina nativa devolve um objeto corrotina.


2. Chamar um gerador assíncrono devolve um objeto async_generator .

3. Não podemos usar um loop for regular com geradores assíncronos, porque eles implementam __aiter__ em
vez de __iter__ .

Geradores assíncronos são acionados por async for , que pode ser um comando bloqueante (como visto em Exemplo
16), e também podem aparecer em compreensões assíncronas, que veremos mais tarde.

Implementando um gerador assíncrono


Vamos agora estudar o código do domainlib.py, com o gerador assíncrono multi_probe (Exemplo 18).

Exemplo 18. domainlib.py: funções para sondar domínios

PYTHON3
import asyncio
import socket
from collections.abc import Iterable, AsyncIterator
from typing import NamedTuple, Optional

class Result(NamedTuple): # (1)


domain: str
found: bool

OptionalLoop = Optional[asyncio.AbstractEventLoop] # (2)

async def probe(domain: str, loop: OptionalLoop = None) -> Result: # (3)
if loop is None:
loop = asyncio.get_running_loop()
try:
await loop.getaddrinfo(domain, None)
except socket.gaierror:
return Result(domain, False)
return Result(domain, True)

async def multi_probe(domains: Iterable[str]) -> AsyncIterator[Result]: # (4)


loop = asyncio.get_running_loop()
coros = [probe(domain, loop) for domain in domains] # (5)
for coro in asyncio.as_completed(coros): # (6)
result = await coro # (7)
yield result # (8)

1. A NamedTuple torna o resultado de probe mais fácil de ler e depurar.


2. Este apelido de tipo serve para evitar que a linha seguinte fique grande demais em uma listagem impressa em um
livro.
3. probe agora recebe um argumento opcional loop , para evitar chamadas repetidas a get_running_loop quando
essa corrotina é acionada por multi_probe .
4. Uma função geradora assíncrona produz um objeto gerador assíncrono, que pode ser anotado como
AsyncIterator[SomeType] .

5. Constrói uma lista de objetos corrotina probe , cada um com um domain diferente.
6. Isso não é async for porque asyncio.as_completed é um gerador clássico.
7. Espera pelo objeto corrotina para obter o resultado.
8. Produz result . Esta linha faz com que multi_probe seja um gerador assíncrono.
O loop for no Exemplo 18 poderia ser mais conciso:

PYTHON3
for coro in asyncio.as_completed(coros):
yield await coro

✒️ NOTA
O Python interpreta isso como yield (await coro) , então funciona.

Achei que poderia ser confuso usar esse atalho no primeiro exemplo de gerador assíncrono no livro,
então dividi em duas linhas.

Dado domainlib.py, podemos demonstrar o uso do gerador assíncrono multi_probe em domaincheck.py: um script
que recebe um sufixo de domínio e busca por domínios criados a partir de palavras-chave curtas do Python.

Aqui está uma amostra da saída de domaincheck.py:

TEXT
$ ./domaincheck.py net
FOUND NOT FOUND
===== =========
in.net
del.net
true.net
for.net
is.net
none.net
try.net
from.net
and.net
or.net
else.net
with.net
if.net
as.net
elif.net
pass.net
not.net
def.net

Graças à domainlib, o código de domaincheck.py é bastante direto, como se vê no Exemplo 19.

Exemplo 19. domaincheck.py: utilitário para sondar domínios usando domainlib


PYTHON3
#!/usr/bin/env python3
import asyncio
import sys
from keyword import kwlist

from domainlib import multi_probe

async def main(tld: str) -> None:


tld = tld.strip('.')
names = (kw for kw in kwlist if len(kw) <= 4) # (1)
domains = (f'{name}.{tld}'.lower() for name in names) # (2)
print('FOUND\t\tNOT FOUND') # (3)
print('=====\t\t=========')
async for domain, found in multi_probe(domains): # (4)
indent = '' if found else '\t\t' # (5)
print(f'{indent}{domain}')

if __name__ == '__main__':
if len(sys.argv) == 2:
asyncio.run(main(sys.argv[1])) # (6)
else:
print('Please provide a TLD.', f'Example: {sys.argv[0]} COM.BR')

1. Gera palavras-chave de tamanho até 4.

2. Gera nomes de domínio com o sufixo recebido como TLD (Top Level Domain, "Domínio de Topo").
3. Formata um cabeçalho para a saída tabular.
4. Itera de forma assíncrona sobre multi_probe(domains) .

5. Define indent como zero ou dois tabs, para colocar o resultado na coluna correta.
6. Roda a corrotina main com o argumento de linha de comando passado.

Geradores tem um uso adicional, não relacionado à iteração: ele podem ser usados como gerenciadores de contexto.
Isso também se aplica aos geradores assíncronos.

Geradores assíncronos como gerenciadores de contexto


Escrever nossos próprios gerenciadores de contexto assíncronos não é uma tarefa de programação frequente, mas se
você precisar escrever um, considere usar o decorador @asynccontextmanager
(https://docs.python.org/pt-br/3/library/contextlib.html#contextlib.asynccontextmanager) (EN), incluído no módulo contextlib
no Python 3.7. Ele é muito similar ao decorador @contextmanager que estudamos na seção Seção 18.2.2.

Um exemplo interessante da combinação de @asynccontextmanager com loop.run_in_executor aparece no livro


de Caleb Hattingh, Using Asyncio in Python (https://fpy.li/hattingh). O Exemplo 20 é o código de Caleb—com uma única
mudança e o acréscimo das explicações.

Exemplo 20. Exemplo usando @asynccontextmanager e loop.run_in_executor


PYTHON3
from contextlib import asynccontextmanager

@asynccontextmanager
async def web_page(url): # (1)
loop = asyncio.get_running_loop() # (2)
data = await loop.run_in_executor( # (3)
None, download_webpage, url)
yield data # (4)
await loop.run_in_executor(None, update_stats, url) # (5)

async with web_page('google.com') as data: # (6)


process(data)

1. A função decorada tem que ser um gerador assíncrono.


2. Uma pequena atualização no código de Caleb: usar o get_running_loop , mais leve, no lugar de get_event_loop .

3. Suponha que download_webpage é uma função bloqueante que usa a biblioteca requests; vamos rodá-la em uma
thread separada, para evitar o bloqueio do loop de eventos.
4. Todas as linhas antes dessa expressão yield vão se tornar o método corrotina __aenter__ do gerenciador de
contexto assíncrono criado pelo decorador. O valor de data será vinculado à variável data após a cláusula as no
comando async with abaixo.
5. As linhas após o yield se tornarão o método corrotina __aexit__ . Aqui outra chamada bloqueante é delegada
para um executor de threads.
6. Usa web_page com async with .

Isso é muito similar ao decorador sequencial @contextmanager . Por favor, consulte a seção Seção 18.2.2 para maiores
detalhes, inclusive o tratamento de erro na linha do yield . Para outro exemplo usando @asynccontextmanager , veja
a documentação do contextlib (https://docs.python.org/pt-br/3/library/contextlib.html#contextlib.asynccontextmanager).

Por fim, vamos terminar nossa jornada pelas funções geradoras assíncronas comparado-as com as corrotinas nativas.

Geradores assíncronos versus corrotinas nativas


Aqui estão algumas semelhanças e diferenças fundamentais entre uma corrotina nativa e uma função geradora
assíncrona:

Ambas são declaradas com async def .

Um gerador assíncrono sempre tem uma expressão yield em seu corpo—é isso que o torna um gerador. Uma
corrotina nativa nunca contém um yield .
Uma corrotina nativa pode return (devolver) algum valor diferente de None . Um gerador assíncrono só pode usar
comandos return vazios.
Corrotinas nativas são esperáveis: elas podem ser acionadas por expressões await ou passadas para alguma das
muitas funções do asyncio que aceitam argumentos esperáveis, tal como create_task . Geradores assíncronos
não são esperáveis. Eles são iteráveis assíncronos, acionados por async for ou por compreensões assíncronas.

É hora então de falar sobre as compreensões assíncronas.

21.10.2. Compreensões assíncronas e expressões geradoras assíncronas


A PEP 530—Asynchronous Comprehensions (https://fpy.li/pep530) (EN) introduziu o uso de async for e await na
sintaxe de compreensões e expressões geradoras, a partir do Python 3.6.
A única sintaxe definida na PEP 530 que pode aparecer fora do corpo de uma async def é uma expressão geradora
assíncrona.

Definindo e usando uma expressão geradora assíncrona


Dado o gerador assíncrono multi_probe do Exemplo 18, poderíamos escrever outro gerador assíncrono que
devolvesse apenas os nomes de domínios encontrados. Aqui está uma forma de fazer isso—novamente usando o
console assíncrono iniciado com -m asyncio :

PYCON
>>> from domainlib import multi_probe
>>> names = 'python.org rust-lang.org golang.org no-lang.invalid'.split()
>>> gen_found = (name async for name, found in multi_probe(names) if found) # (1)
>>> gen_found
<async_generator object <genexpr> at 0x10a8f9700> # (2)
>>> async for name in gen_found: # (3)
... print(name)
...
golang.org
python.org
rust-lang.org

1. O uso de async for torna isso uma expressão geradora assíncrona. Ela pode ser definida em qualquer lugar de
um módulo Python.
2. A expressão geradora assíncrona cria um objeto async_generator —exatamente o mesmo tipo de objeto
devolvido por uma função geradora assíncrona como multi_probe .
3. O objeto gerador assíncrono é acionado pelo comando async for , que por sua vez só pode aparecer dentro do
corpo de uma async def ou no console assíncrono mágico que eu usei nesse exemplo.

Resumindo: uma expressão geradora assíncrona pode ser definida em qualquer ponto do seu programa, mas só pode
ser consumida dentro de uma corrotina nativa ou de uma função geradora assíncrona.

As demais construções sintáticas introduzidos pela PEP 530 só podem ser definidos e usados dentro de corrotinas
nativas ou de funções geradoras assíncronas.

Compreeensões assíncronas
Yury Selivanov—autor da PEP 530—justifica a necessidade de compreensões assíncronas com três trechos curtos de
código, reproduzidos a seguir.

Podemos todos concordar que deveria ser possível reescrever esse código:

PY3
result = []
async for i in aiter():
if i % 2:
result.append(i)

assim:

PY3
result = [i async for i in aiter() if i % 2]

Além disso, dada uma corrotina nativa fun , deveria ser possível escrever isso:

PY3
result = [await fun() for fun in funcs]
Usar await em uma compreensão de lista é similar a usar asyncio.gather . Mas gather dá a
você um maior controle sobre o tratamento de exceções, graças ao seu argumento opcional
👉 DICA return_exceptions . Caleb Hattingh recomenda sempre definir return_exceptions=True (o
default é False ). Veja a documentação de asyncio.gather
(https://docs.python.org/pt-br/3/library/asyncio-task.html#asyncio.gather) para mais informações.

Voltemos ao console assíncrono mágico:

PYCON
>>> names = 'python.org rust-lang.org golang.org no-lang.invalid'.split()
>>> names = sorted(names)
>>> coros = [probe(name) for name in names]
>>> await asyncio.gather(*coros)
[Result(domain='golang.org', found=True),
Result(domain='no-lang.invalid', found=False),
Result(domain='python.org', found=True),
Result(domain='rust-lang.org', found=True)]
>>> [await probe(name) for name in names]
[Result(domain='golang.org', found=True),
Result(domain='no-lang.invalid', found=False),
Result(domain='python.org', found=True),
Result(domain='rust-lang.org', found=True)]
>>>

Observe que eu ordenei a lista de nomes, para mostrar que os resultados chegam na ordem em que foram enviados,
nos dois casos.

A PEP 530 permite o uso de async for e await em compreensões de lista, bem como em compreensões de dict e de
set . Por exemplo, aqui está uma compreensão de dict para armazenar os resultados de multi_probe no console
assíncrono:

PYCON
>>> {name: found async for name, found in multi_probe(names)}
{'golang.org': True, 'python.org': True, 'no-lang.invalid': False,
'rust-lang.org': True}

Podemos usar a palavra-chave await na expressão antes de cláusulas for ou de async for , e também na expressão
após a cláusula if . Aqui está uma compreensão de set no console assíncrono, coletando apenas os domínios
encontrados.

PYCON
>>> {name for name in names if (await probe(name)).found}
{'rust-lang.org', 'python.org', 'golang.org'}

Precisei colocar parênteses extras ao redor da expressão await devido à precedência mais alta do operador . (ponto)
de __getattr__ .

Repetindo, todas essas compreensões só podem aparecer no corpo de uma async def ou no console assíncrono
encantado.

Agora vamos discutir um recurso muito importante dos comandos async , das expressões async , e dos objetos que
eles criam. Esses artefatos são muitas vezes usados com o asyncio mas, na verdade, eles são independentes da
biblioteca.
21.11. Programação assíncrona além do asyncio: Curio
Os elementos da linguagem async/await do Python não estão presos a nenhum loop de eventos ou biblioteca
específicos.[304] Graças à API extensível fornecida por métodos especiais, qualquer um suficientemente motivado pode
escrever seu ambiente de runtime e sua framework assíncronos para acionar corrotinas nativas, geradores
assíncronos, etc.

Foi isso que David Beazley fez em seu projeto Curio (https://fpy.li/21-49). Ele estava interessado em repensar como esses
recursos da linguagem poderiam ser usados em uma framework desenvolvida do zero. Lembre-se que o asyncio foi
lançado no Python 3.4, e usava yield from em vez de await , então sua API não conseguia aproveitar gerenciadores
de contexto assíncronos, iteradores assíncronos e tudo o mais que as palavras-chave async/await tornaram possível.
O resultado é que o Curio tem uma API mais elegante e uma implementação mais simples quando comparado ao
asyncio .

O Exemplo 21 mostra o script blogdom.py (Exemplo 1) reescrito para usar o Curio.

Exemplo 21. blogdom.py: Exemplo 1, agora usando o Curio

PYTHON3
#!/usr/bin/env python3
from curio import run, TaskGroup
import curio.socket as socket
from keyword import kwlist

MAX_KEYWORD_LEN = 4

async def probe(domain: str) -> tuple[str, bool]: # (1)


try:
await socket.getaddrinfo(domain, None) # (2)
except socket.gaierror:
return (domain, False)
return (domain, True)

async def main() -> None:


names = (kw for kw in kwlist if len(kw) <= MAX_KEYWORD_LEN)
domains = (f'{name}.dev'.lower() for name in names)
async with TaskGroup() as group: # (3)
for domain in domains:
await group.spawn(probe, domain) # (4)
async for task in group: # (5)
domain, found = task.result
mark = '+' if found else ' '
print(f'{mark} {domain}')

if __name__ == '__main__':
run(main()) # (6)

1. probe não precisa obter o loop de eventos, porque…​


2. …​getaddrinfo é uma função nível superior de curio.socket , não um método de um objeto loop —como ele é
no asyncio .
3. Um TaskGroup é um conceito central no Curio, para monitorar e controlar várias corrotinas, e para garantir que
elas todas sejam executadas e terminadas corretamente.
4. TaskGroup.spawn é como você inicia uma corrotina, gerenciada por uma instância específica de TaskGroup . A
corrotina é envolvida em uma Task .
5. Iterar com async for sobre um TaskGroup produz instâncias de Task a medida que cada uma termina. Isso
corresponde à linha em Exemplo 1 que usa for … as_completed(…): .
6. O Curio foi pioneiro no uso dessa maneira sensata de iniciar um programa assíncrono em Python.
Para expandir esse último ponto: se você olhar nos exemplo de código de asyncio na primeira edição do Python
Fluente, verá linhas como as seguintes, repetidas várias vezes:

PYTHON3
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

Um TaskGroup do Curio é um gerenciador de contexto assíncrono que substitui várias APIs e padrões de codificação
ad hoc do asyncio . Acabamos de ver como iterar sobre um TaskGroup torna a função asyncio.as_completed(…)
desnecessária.

Outro exemplo: em vez da função especial gather , este trecho da documentação de "Task Groups" (https://fpy.li/21-50)
(EN) coleta os resultados de todas as tarefas no grupo:

PYTHON3
async with TaskGroup(wait=all) as g:
await g.spawn(coro1)
await g.spawn(coro2)
await g.spawn(coro3)
print('Results:', g.results)

Grupos de tarefas (task groups) suportam concorrência estruturada (https://fpy.li/21-51): uma forma de programação
concorrente que restringe todas a atividade de um grupo de tarefas assíncronas a um único ponto de entrada e saída.
Isso é análogo à programaçào estruturada, que eliminou o comando GOTO e introduziu os comandos de bloco para
limitar os pontos de entrada e saída de loops e sub-rotinas. Quando usado como um gerenciador de contexto
assíncrono, um TaskGroup garante que na saída do bloco, todas as tarefas criadas dentro dele estão ou finalizadas ou
canceladas e qualquer exceção foi levantada.

A concorrência estruturada vai provavelmente ser adotada pelo asyncio em versões futuras do
Python. Uma indicação forte disso aparece na PEP 654–Exception Groups and except*

✒️ NOTA (https://fpy.li/pep654) (EN), que foi aprovada para o Python 3.11 (https://fpy.li/21-52) (EN). A seção
"Motivation" (https://fpy.li/21-53)(EN) menciona as "creches" (nurseries) do _Trio, o nome que els dão
para grupos de tarefas: "Implementar uma API de geração de tarefas melhor no asyncio, inspirada
pelas creches do Trio, foi a principal motivação dessa PEP."

Outro importante recurso do Curio é um suporte melhor para programar com corrotinas e threads na mesma base de
código—uma necessidade de qualquer programa assíncrono não-trivial. Iniciar uma thread com await
spawn_thread(func, …) devolve um objeto AsyncThread com uma interface de Task . As threads podem chamar
corrotinas, graças à função especial AWAIT(coro) (https://fpy.li/21-54)—escrita inteiramente com maiúsculas porque
await agora é uma palavra-chave.

O Curio também oferece uma UniversalQueue que pode ser usada para coordenar o trabalho entre threads,
corrotinas Curio e corrotinas asyncio . Isso mesmo, o Curio tem recursos que permitem que ele rode em uma thread
junto com asyncio em outra thread, no mesmo processo, se comunicando através da UniversalQueue e de
UniversalEvent . A API para essas classes "universais" é a mesma dentro e fora de corrotinas, mas em uma corrotina
é preciso preceder as chamadas com await .
Em outubro de 2021, quando estou escrevendo esse capítulo, a HTTPX é a primeira biblioteca HTTP cliente compatível
com o Curio (https://fpy.li/21-55), mas não sei de nenhuma biblioteca assíncrona de banco de dados que o suporte nesse
momento. No repositório do Curio há um conjunto impressionante de exemplos de programação para rede
(https://fpy.li/21-56), incluindo um que utiliza WebSocket, e outro implementando o algoritmo concorrente RFC 8305—
Happy Eyeballs (https://fpy.li/21-57), para conexão com pontos de acesso IPv6 com rápido recuo para IPv4 se necessário.

O design do Curio tem tido grande influência. A framework Trio (https://fpy.li/21-58), iniciada por Nathaniel J. Smith, foi
muito inspirada pelo Curio. O Curio pode também ter alertado os contribuidores do Python a melhorarem a
usabilidade da API asyncio . Por exemplo, em suas primeiras versões, os usuários do asyncio muitas vezes eram
obrigados a obter e ficar passando um objeto loop , porque algumas funções essenciais eram ou métodos de loop ou
exigiam um argumento loop . Em versões mais recentes do Python, acesso direto ao loop não é mais tão necessário e,
de fato, várias funções que aceitavam um loop opcional estão agora descontinuando aquele argumento.

Anotações de tipo para tipos assíncronos é o nosso próximo tópico.

21.12. Dicas de tipo para objetos assíncronos


O tipo devolvido por uma corrotina nativa é o tipo do objeto que você obtém quando usa await naquela corrotina,
que é o tipo do objeto que aparece nos comandos return no corpo da função corrotina nativa.[305]

Nesse capítulo mostro muitos exemplos de corrotinas nativas anotadas, incluindo a probe do Exemplo 21:

PYTHON
async def probe(domain: str) -> tuple[str, bool]:
try:
await socket.getaddrinfo(domain, None)
except socket.gaierror:
return (domain, False)
return (domain, True)

Se você precisar anotar um parâmetro que recebe um objeto corrotina, então o tipo genérico é:

PYTHON
class typing.Coroutine(Awaitable[V_co], Generic[T_co, T_contra, V_co]):
...

Aquele tipo e os tipos seguintes foram introduzidos no Python 3.5 e 3.6 para anotar objetos assíncronos:

PYTHON
class typing.AsyncContextManager(Generic[T_co]):
...
class typing.AsyncIterable(Generic[T_co]):
...
class typing.AsyncIterator(AsyncIterable[T_co]):
...
class typing.AsyncGenerator(AsyncIterator[T_co], Generic[T_co, T_contra]):
...
class typing.Awaitable(Generic[T_co]):
...

Com o Python ≥ 3.9, use os equivalentes deles em collections.abc .

Quero destacar três aspectos desses tipos genéricos.

Primeiro: eles são todos covariantes do primeiro parâmetro de tipo, que é o tipo dos itens produzidos a partir desses
objetos. Lembre-se da regra #1 da Seção 15.7.4.4:


“ Secovariante.
um parâmetro de tipo formal define um tipo para um dado que sai do objeto, ele pode ser

Segundo: AsyncGenerator e Coroutine são contra-variantes do segundo ao último parâmetros. Aquele é o tipo do
argumento do método de baixo nível .send() , que o loop de eventos chama para acionar geradores assíncronos e
corrotinas. Dessa forma, é um tipo de "entrada". Assim, pode ser contra-variante, pelo Regra de Variância #2

“ Seconstrução
um parâmetro de tipo formal define um tipo para um dado que entra no objeto após sua
inicial, ele pode ser contra-variante.

Terceiro: AsyncGenerator não tem tipo de devolução, ao contrário de typing.Generator , que vimos na seção Seção
17.13.3. Devolver um valor levantando StopIteration(value) era uma das gambiarras que permitia a geradores
operarem como corrotinas e suportar yield from , como vimos na seção Seção 17.13. Não há tal sobreposição entre os
objetos assíncronos: objetos AsyncGenerator não devolvem valores e são completamente separados de objetos
corrotina, que são anotados com typing.Coroutine .

Por fim, vamos discutir rapidamente as vantagens e desafios da programaçào assíncrona.

21.13. Como a programação assíncrona funciona e como não funciona


As seções finais deste capítulo discutem as ideias de alto nível em torno da programação assíncrona, independente da
linguagem ou da biblioteca usadas.

Vamos começar explicando a razão número 1 pela qual a programação assíncrona é atrativa, seguido por um mito
popular e como lidar com ele.

21.13.1. Correndo em círculos em torno de chamadas bloqueantes


Ryan Dahl, o inventor do Node.js, introduz a filosofia por trás de seu projeto dizendo "Estamos fazendo E/S de forma
totalmente errada."[306] (EN). Ele define uma função bloqueante como uma função que faz E/S de arquivo ou rede, e
argumenta que elas não podem ser tratadas da mesma forma que tratamos funções não-bloqueantes. Para explicar a
razão disso, ele apresenta os números na segunda coluna da Tabela 25.

Tabela 25. Latência de computadores modernos para ler dados em diferentes dispositivos. A terceira coluna mostra os
tempos proporcionais em uma escala fácil de entender para nós, humanos vagarosos.

Dispositivo Ciclos de CPU Escala proporcional "humana"

L1 cache 3 3 segundos

L2 cache 14 14 segundos

RAM 250 250 segundos

disk 41.000.000 1,3 anos

network 240.000.000 7,6 anos

Para a Tabela 25 fazer sentido, tenha em mente que as CPUs modernas, com relógios funcionando em frequências na
casa dos GHz, rodam bilhões de ciclos por segundo. Vamos dizer que uma CPU rode exatamente 1 bilhão de ciclos por
segundo. Aquela CPU pode realizar mais de 333 milhões de leituras do cache L1 em 1 segundo, ou 4 (quatro!) leituras da
rede no mesmo tempo. A terceira coluna da Tabela 25 coloca os números em perspectiva, multiplicando a segunda
coluna por um fator constante. Então, em um universo alternativo, se uma leitura do cache L1 demorasse 3 segundos,
uma leitura da rede demoraria 7,6 anos!
A Tabela 25 explica porque uma abordagem disciplinada da programação assíncrona pode levar a servidores de alto
desempenho. O desafio é conseguir essa disciplina. O primeiro passo é reconhecer que um sistema limitado apenas por
E/S é uma fantasia.

21.13.2. O mito dos sistemas limitados por E/S


Um meme exaustivamente repetido é que programação assíncrona é boa para "sistemas limitados por E/S"—I/O bound
systems, ou seja, sistemas onde o gargalo é E/S, e não processamento de dados na CPU. Aprendi da forma mais difícil
que não existem "sistemas limitados por E/S." Você pode ter funções limitadas por E/S. Talvez a maioria das funções no
seu sistema sejam limitadas por E/S; isto é, elas passam mais tempo esperando por E/S do que realizando operações na
memória. Enquanto esperam, cedem o controle para o loop de eventos, que pode então acionar outras tarefas
pendentes. Mas, inevitavelmente, qualquer sistema não-trivial terá partes limitadas pela CPU. Mesmo sistemas triviais
revelam isso, sob stress. No Ponto de vista, conto a história de dois programas assíncronos sofrendo com funções
limitadas pela CPU freando loop de eventos, com severos impactos no desempenho do sistema como um todo.

Dado que qualquer sistema não-trivial terá funções limitadas pela CPU, lidar com elas é a chave do sucesso na
programação assíncrona.

21.13.3. Evitando as armadilhas do uso da CPU


Se você está usando Python em larga escala, deve ter testes automatizados projetados especificamente para detectar
regressões de desempenho assim que elas acontecem. Isso é de importância crítica com código assíncrono, mas é
relevante também para código Python baseado em threads—por causa da GIL. Se você esperar até a lentidão começar a
incomodar a equipe de desenvolvimento, será tarde demais. O conserto provavelmente vai exigir algumas mudanças
drásticas.

Aqui estão algumas opções para quando você identifica gargalos de uso da CPU:

Delegar a tarefa para um pool de processos Python.


Delegar a tarefa para um fila de tarefas externa.
Reescrever o código relevante em Cython, C, Rust ou alguma outra linguagem que compile para código de máquina
e faça interface com a API Python/C, de preferência liberando a GIL.
Decidir que você pode tolerar a perda de desempenho e deixar como está—mas registre essa decisão, para torná-la
mais fácil de reverter no futuro.

A fila de tarefas externa deveria ser escolhida e integrada o mais rápido possível, no início do projeto, para que
ninguém na equipe hesite em usá-la quando necessário.

A opção deixar como está entra na categoria de dívida tecnológica


(https://pt.wikipedia.org/wiki/D%C3%ADvida_tecnol%C3%B3gica).

Programação concorrente é um tópico fascinante, e eu gostaria de escrever muito mais sobre isso. Mas não é o foco
principal deste livro, e este já é um dos capítulos mais longos, então vamos encerrar por aqui.

21.14. Resumo do capítulo


“ Otipoproblema com as abordagens usuais da programação assíncrona é que elas são propostas do
"tudo ou nada". Ou você reescreve todo o código, de forma que nada nele bloqueie [o
processamento] ou você está só perdendo tempo.
— Alvaro Videla e Jason J. W. Williams
RabbitMQ in Action

Escolhi essa epígrafe para esse capítulo por duas razões. Em um nível mais alto, ela nos lembra de evitar o bloqueio do
loop de eventos, delegando tarefas lentas para uma unidade de processamento diferente, desde uma simples thread
indo até uma fila de tarefas distribuída. Em um nível mais baixo, ela também é um aviso: no momento em que você
escreve seu primeiro async def , seu programa vai inevitavelmente ver surgir mais e mais async def , await , async
with , e async for . E o uso de bibliotecas não-assíncronas subitamente se tornará um desafio.

Após os exemplos simples com o spinner no capítulo Capítulo 19, aqui nosso maior foco foi a programação assíncrona
com corrotinas nativas, começando com o exemplo de sondagem de DNS blogdom.py, seguido pelo conceito de
esperáveis. Lendo o código-fonte de flags_asyncio.py, descobrimos o primeiro exemplo de um gerenciador de contexto
assíncrono.

As variantes mais avançadas do programa de download de bandeiras introduziram duas funções poderosas: o gerador
asyncio.as_completed generator e a corrotina loop.run_in_executor . Nós também vimos o conceito e a aplicação
de um semáforo, para limitar o número de downloads concorrentes—como esperado de clientes HTTP bem
comportados.

A programaçãp assíncrona para servidores foi apresentada com os exemplos mojifinder: um serviço web usando a
FastAPI e o tcp_mojifinder.py—este último utilizando apenas o asyncio e o protocolo TCP..

A seguir, iteração assíncrona e iteráveis assíncronos foram o principal tópico, com seções sobre async for , o console
assíncrono do Python, geradores assíncronos expressões geradoras assíncronas e compreensões assíncronas.

O último exemplo do capítulo foi o blogdom.py reescrito com a framework Curio, demonstrando como os recursos de
programação assíncrona do Python não estão presos ao pacote asyncio . O Curio também demonstra o conceito de
concorrência estruturada, que pode vir a ter um grande impacto em toda a indústria de tecnologia, trazendo mais
clareza para o código concorrente.

Por fim, as seções sob o título Seção 21.13 discutiram o principal atrativo da programação assíncrona, a falácia dos
"sistemas limitados por E/S" e como lidar com as inevitáveis partes de uso intensivo de CPU em seu programa.

21.15. Para saber mais


A palestra de abertura da PyOhio 2016, de David Beazley, "Fear and Awaiting in Async" (https://fpy.li/21-61) (EN) é uma
fantástica introdução, com "código ao vivo", ao potencial dos recursos da linguagem tornados possíveis pela
contribuição de Yury Selivanov ao Python 3.5, as palavras-chave async/await . Em certo momento, Beazley reclama
que await não pode ser usada em compreensões de lista, mas isso foi resolvido por Selivanov na PEP 530—
Asynchronous Comprehensions (https://fpy.li/pep530) (EN), implementada mais tarde naquele mesmo ano, no Python 3.6.

Fora isso, todo o resto da palestra de Beazley é atemporal, pois ele demonstra como os objetos assíncronos vistos nesse
capítulo funcionam, sem ajuda de qualquer framework—com uma simples função run usando .send(None) para
acionar corrotinas. Apenas no final Beazley mostra o Curio (https://fpy.li/21-62), que ele havia começado a programar
naquele ano, como um experimento, para ver o quão longe era possível levar a programação assíncrona sem se basear
em callbacks ou futures, usando apenas corrotinas. Como se viu, dá para ir muito longe—como demonstra a evolução
do Curio e a criação posterior do Trio (https://fpy.li/21-58) por Nathaniel J. Smith. A documentação do Curio’s contém links
(https://fpy.li/21-64) para outras palestras de Beazley sobre o assunto.

Além criar o Trio, Nathaniel J. Smith escreveu dois post de blog muito profundos, que gostaria de recomendar: "Some
thoughts on asynchronous API design in a post-async/await world" (Algumas reflexões sobre o design de APIs
assíncronas em um mundo pós-async/await) (https://fpy.li/21-65) (EN), comparando os designs do Curio e do asyncio, e
"Notes on structured concurrency, or: Go statement considered harmful" (Notas sobre concorrência estruturada, ou: o
comando Go considerado nocivo) (https://fpy.li/21-66) (EN), sobre concorrência estruturada. Smith também deu uma longa
e informativa resposta à questão: "What is the core difference between asyncio and trio?" (Qual é a principal diferença
entre o asyncio e o trio?) (https://fpy.li/21-67) (EN) no StackOverflow.
Para aprender mais sobre o pacote asyncio, já mencionei os melhores recursos escritos que conheço no início do
capítulo: a documentação oficial (https://docs.python.org/pt-br/3/library/asyncio.html), após a fantástica reorganização
(https://fpy.li/21-69) (EN) iniciada por Yury Selivanov em 2018, e o livro de Caleb Hattingh, Using Asyncio in Python
(https://fpy.li/hattingh) (O’Reilly).

Na documentação oficial, não deixe de ler "Desenvolvendo com asyncio"


(https://docs.python.org/pt-br/3/library/asyncio-dev.html), que documenta o modo de depuração do asyncio e também discute
erros e armadilhas comuns, e como evitá-los.

Para uma introdução muito acessível, de 30 minutos, à programação assíncrona em geral e também ao asyncio, assista
a palestra "Asynchronous Python for the Complete Beginner" (Python Assíncrono para o Iniciante Completo)
(https://fpy.li/21-71) (EN), de Miguel Grinberg, apresentada na PyCon 2017. Outra ótima introdução é "Demystifying
Python’s Async and Await Keywords" (Desmistificando as Palavras-Chave Async e Await do Python) (https://fpy.li/21-72)
(EN), apresentada por Michael Kennedy, onde entre outras coisas aprendi sobre a bilblioteca unsync (https://fpy.li/21-73),
que oferece um decorador para delegar a execução de corrotinas, funções dedicadas a E/S e funções de uso intensivo de
CPU para asyncio , threading , ou multiprocessing , conforme a necessidade.

Na EuroPython 2019, Lynn Root—uma líder global da PyLadies (https://fpy.li/21-74)—apresentou a excelente "Advanced
asyncio: Solving Real-world Production Problems" (Asyncio Avançado: Resolvendo Problemas de Produção do Mundo
Real) (https://fpy.li/21-75) (EN), baseada na sua experiência usando Python como um engenheira do Spotify.

Em 2020, Łukasz Langa gravou um grande série de vídeos sobre o asyncio, começando com "Learn Python’s AsyncIO #1
—​The Async Ecosystem" (Aprenda o AsyncIO do Python—O Ecossistema Async) (https://fpy.li/21-76) (EN). Langa também
fez um vídeo muito bacana, "AsyncIO + Music" (https://fpy.li/21-77) (EN), para a PyCon 2020, que não apenas mostra o
asyncio aplicado a um domínio orientado a eventos muito concreto, como também explica essa aplicação do início ao
fim.

Outra área dominada por programaçao orientada a eventos são os sistemas embarcados. Por isso Damien George
adicionou o suporte a async/await em seu interpretador MicroPython (https://fpy.li/21-78) (EN) para microcontroladores
Na PyCon Australia 2018, Matt Trentini demonstrou a biblioteca uasyncio (https://fpy.li/21-79) (EN), um subconjunto do
asyncio que é parte da biblioteca padrão do MicroPython.

Para uma visão de mais alto nível sobre a programação assíncrona em Python, leia o post de blog "Python async
frameworks—Beyond developer tribalism" (Frameworks assíncronas do Python—Para além do tribalismo dos
desenvolvedores) (https://fpy.li/21-80) (EN), de Tom Christie.

Por fim, recomendo "What Color Is Your Function?" (Qual a Cor da Sua Função?) (https://fpy.li/21-81) de Bob Nystrom,
discutindo os modelos de execução incompatíveis de funções normais versus funções assíncronas—também
conhecidas como corrotinas—em Javascript, Python, C# e outras linguagens. Alerta de spoiler: A conclusão de Nystrom
é que a linguagem que acertou nessa área foi Go, onde todas as funções tem a mesma cor. Eu gosto disso no Go. Mas
também acho que Nathaniel J. Smith tem razão quando escreveu "Go statement considered harmful" (Comando Go
considerado nocivo) (https://fpy.li/21-66) (EN). Nada é perfeito, e programação concorrente é sempre difícil.

Ponto de vista
Como uma função lerda quase estragou as benchmarks do uvloop
Em 2016, Yury Selivanov lançou o uvloop (https://fpy.li/21-83), "um substituto rápido e direto para o loop de eventos
embutido do asyncio event loop." Os números de referência (benchmarks) apresentados no post de blog
(https://fpy.li/21-84) de Selivanov anunciando a biblioteca, em 2016, eram muito impressionantes. Ele escreveu: "ela
é pelo menos 2x mais rápida que o nodejs e gevent, bem como qualquer outra framework assíncrona do Python.
O desempenho do asyncio com o uvloop é próximo àquele de programas em Go."

Entretanto, o post revela que a uvloop é capaz de competir com o desempenho do Go sob duas condições:

1. Que o Go seja configurado para usar uma única thread. Isso faz o runtime do Go se comportar de forma
similar ao asyncio: a concorrência é alcançada através de múltiplas corrotinas acionadas por um loop de
eventos, todos na mesma thread.[307]
2. Que o código Python 3.5 use a biblioteca httptools (https://fpy.li/21-85) além do próprio uvloop.

Selivanov explica que escreveu httptools após testar o desempenho da uvloop com a aiohttp (https://fpy.li/21-86)—
uma das primeiras bibliotecas HTTP completas construídas sobre o asyncio :

“ Entretanto, o gargalo de desempenho no aiohttp estava em seu parser de HTTP, que era tão
lento que pouco importava a velocidade da biblioteca de E/S subjacente. Para tornar as
coisas mais interessantes, criamos uma biblioteca para Python usar a http-parser (a
biblioteca em C do parser do Node.js, originalmente desenvolvida para o NGINX). A
biblioteca é chamada httptools, e está disponível no Github e no PyPI.

Agora reflita sobre isso: os testes de desempenho HTTP de Selivanov consistiam de um simples servidor eco
escrito em diferentes linguagens e usando diferentes bibliotecas, testados pela ferramenta de benchmarking wrk
(https://fpy.li/21-87). A maioria dos desenvolvedores consideraria um simples servidor eco um "sistema limitado por
E/S", certo? Mas no fim, a análise de cabeçalhos HTTP é vinculada à CPU, e tinha uma implementação Python
lenta na aiohttp quando Selivanov realizou os testes, em 2016. Sempre que uma função escrita em Python estava
processando os cabeçalhos, o loop de eventos era bloqueado. O impacto foi tão significativo que Selivanov se deu
ao trabalho extra de escrever o httptools. Sem a otimização do código de uso intensivo de CPU, os ganhos de
desempenho de um loop de eventos mais rápido eram perdidos.

Morte por mil cortes

Em vez de um simples servidor eco, imagine um sistema Python complexo e em evolução, com milhares de linhas
de código assíncrono, e conectado a muitas bibliotexas externas. Anos atrás me pediram para ajudar a
diagnosticar problemas de desempenho em um sistema assim. Ele era escrito em Python 2.7, com a framework
Twisted (https://fpy.li/21-88)—uma biblioteca sólida e, de muitas maneiras, uma precursora do próprio asyncio.

Python era usado para construir uma fachada para a interface Web, integrando funcionalidades fornecidas por
biliotecas pré-existentes e ferramentas de linha de comando escritas em outras linguagens—mas não projetadas
para execução concorrente.

O projeto era ambicioso: já estava em desenvolvimento há mais de um ano, mas ainda não estava em produção.
[308] Com o tempo, os desenvolvedores notaram que o desempenho do sistema como um todo estava diminuindo,
e estavam tendo muita dificuldade em localizar os gargalos.

O que estava acontecendo: a cada nova funcionalidade, mais código intensivo em CPU desacelerava o loop de
eventos do Twisted. O papel do Python como um linguagem de "cola" significava que havia muita interpretação
de dados, serialização, desserialização, e muitas conversões entre formatos. Não havia um gargalo único: o
problema estava espalhado por incontáveis pequenas funções criadas ao longo de meses de desenvolvimento. O
conserto exigiria repensar a arquitetura do sistema, reescrever muito código, provavelmente usar um fila de
tarefas, e talvez usar microsserviços ou bibliotecas personalizadas, escritas em linguagens mais adequadas a
processamento concorrente intensivo em CPU. Os apoiadores internos não estavam preparados para fazer aquele
investimento adicional, e o projeto foi cancelado semanas depois deste diagnóstico.
Quando contei essa história para Glyph Lefkowitz—fundador do projeto Twisted—ele me disse que uma de suas
prioridades, no início de qualquer projeto envolvendo programação assíncrona, é decidir que ferramentas serão
usadas para executar de tarefas intensivas em CPU sem atrapalhar o loop de eventos. Essa conversa com Glyph
foi a inspiração para a seção Seção 21.13.3.
Parte V: Metaprogramação
22. Atributos dinâmicos e propriedades
“ Afatoimportância crucial das propriedades é que sua existência torna perfeitamente seguro, e de
aconselhável, expor atributos públicos de dados como parte da interface pública de sua
classe.[309]
— Martelli, Ravenscroft & Holden
Why properties are important (Porque propriedades são importantes)

No Python, atributos de dados e métodos são conhecidos conjuntamente como atributos . Um método é um atributo
invocável. Atributos dinâmicos apresentam a mesma interface que os atributos de dados—isto é, obj.attr —mas são
computados sob demanda. Isso atende ao Princípio de Acesso Uniforme de Bertrand Meyer:

“ Todos os serviços oferecidos por um módulo deveriam estar disponíveis através de uma notação
uniforme, que não revele se eles são implementados por armazenamento ou por computação.
[310]

— Bertrand Meyer
Object-Oriented Software Construction (Construção de Software Orientada a Objetos)

Há muitas formas de implementar atributos dinâmicos em Python. Este capítulo trata das mais simples delas: o
decorador @property e o método especial __getattr__ .

Uma classe definida pelo usuário que implemente __getattr__ pode implementar uma variação dos atributos
dinâmicos que chamo de atributos virtuais: atributos que não são declarados explicitamente em lugar algum no código-
fonte da classe, e que não estão presentes no __dict__ das instâncias, mas que podem ser obtidos de algum outro
lugar ou calculados em tempo real sempre que um usuário tenta ler um atributo inexistente tal como
obj.no_such_attr .

Programar atributos dinâmicos e virtuais é o tipo de metaprogramação que autores de frameworks fazem. Entretanto,
como as técnicas básicas no Python são simples, podemos usá-las nas tarefas cotidianas de processamento de dados. É
por aí que iniciaremos esse capítulo.

22.1. Novidades nesse capítulo


A maioria das atualizações deste capítulo foram motivadas pela discussão relativa a @functools.cached_property
(introduzido no Python 3.8), bem como pelo uso combinado de @property e @functools.cache (novo no 3.9). Isso
afetou o código das classes Record e Event , que aparecem na seção Seção 22.3. Também acrescentei uma refatoração
para aproveitar a otimização da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves)
(https://fpy.li/pep412).

Para enfatizar as características mais relevantes, e ao mesmo tempo manter os exemplos legíveis, removi algum código
não-essencial—fundindo a antiga classe DbRecord com Record , substituindo shelve.Shelve por um dict e
suprimindo a lógica para baixar o conjunto de dados da OSCON—que os exemplos agora leem de um arquivo local,
disponível no repositório de código do Python Fluente (https://fpy.li/code).

22.2. Processamento de dados com atributos dinâmicos


Nos próximos exemplos, vamos nos valer dos atributos dinâmicos para trabalhar com um conjunto de dados JSON
publicado pela O’Reilly, para a conferência OSCON 2014. O Exemplo 1 mostra quatro registros daquele conjunto de
dados.[311]
Exemplo 1. Amostra de registros do osconfeed.json; o conteúdo de alguns campos foi abreviado

JSON
{ "Schedule":
{ "conferences": [{"serial": 115 }],
"events": [
{ "serial": 34505,
"name": "Why Schools Don´t Use Open Source to Teach Programming",
"event_type": "40-minute conference session",
"time_start": "2014-07-23 11:30:00",
"time_stop": "2014-07-23 12:10:00",
"venue_serial": 1462,
"description": "Aside from the fact that high school programming...",
"website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
"speakers": [157509],
"categories": ["Education"] }
],
"speakers": [
{ "serial": 157509,
"name": "Robert Lefkowitz",
"photo": null,
"url": "http://sharewave.com/",
"position": "CTO",
"affiliation": "Sharewave",
"twitter": "sharewaveteam",
"bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..." }
],
"venues": [
{ "serial": 1462,
"name": "F151",
"category": "Conference Venues" }
]
}
}

O Exemplo 1 mostra 4 dos 895 registros no arquivo JSON. O conjunto dados total é um único objeto JSON, com a chave
"Schedule" (Agenda), e seu valor é outro mapeamento com quatro chaves: "conferences" (conferências), "events"
(eventos), "speakers" (palestrantes), e "venues" (locais). Cada uma dessas quatro últimas chaves aponta para uma
lista de registros. No conjunto de dados completo, as listas de "events" , "speakers" e "venues"`contêm dezenas
ou centenas de registros, ao passo que `"conferences" contém apenas aquele único registro exxibido no
Exemplo 1. Cada registro inclui um campo "serial" , que é um identificador único do registro dentro da lista.

Usei o console do Python para explorar o conjuntos de dados, como mostra o Exemplo 2.

Exemplo 2. Exploração interativa do osconfeed.json


PYCON
>>> import json
>>> with open('data/osconfeed.json') as fp:
... feed = json.load(fp) # (1)
>>> sorted(feed['Schedule'].keys()) # (2)
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed['Schedule'].items()):
... print(f'{len(value):3} {key}') # (3)
...
1 conferences
484 events
357 speakers
53 venues
>>> feed['Schedule']['speakers'][-1]['name'] # (4)
'Carina C. Zona'
>>> feed['Schedule']['speakers'][-1]['serial'] # (5)
141590
>>> feed['Schedule']['events'][40]['name']
'There *Will* Be Bugs'
>>> feed['Schedule']['events'][40]['speakers'] # (6)
[3471, 5199]

1. feed é um dict contendo dicts e listas aninhados, com valores string e inteiros.
2. Lista as quatro coleções de registros dentro de "Schedule" .

3. Exibe a contagem de registros para cada coleção.


4. Navega pelos dicts e listas aninhados para obter o nome da última palestrante ( speaker ).
5. Obtém o número de série para aquela mesma palestrante.
6. Cada evento tem uma lista 'speakers' , com o número de série de zero ou mais palestrantes.

22.2.1. Explorando dados JSON e similares com atributos dinâmicos


O Exemplo 2 é bastanre simples, mas a sintaxe feed['Schedule']['events'][40]['name'] é desajeitada. Em
JavaScript, é possível obter o mesmo valor escrevendo feed.Schedule.events[40].name . É fácil de implementar
uma classe parecida com um dict para fazer o mesmo em Python—​há inúmeras implementações na web.[312] Escrevi
FrozenJSON , que é mais simples que a maioria das receitas, pois suporta apenas leitura: ela serve apenas para
explorar os dados. FrozenJSON é também recursivo, lidando automaticamente com mapeamentos e listas aninhados.

O Exemplo 3 é uma demonstração da FrozenJSON , e o código-fonte aparece no Exemplo 4.

Exemplo 3. FrozenJSON , do Exemplo 4, permite ler atributos como name , e invocar métodos como .keys() e
.items()
PY
>>> import json
>>> raw_feed = json.load(open('data/osconfeed.json'))
>>> feed = FrozenJSON(raw_feed) # (1)
>>> len(feed.Schedule.speakers) # (2)
357
>>> feed.keys()
dict_keys(['Schedule'])
>>> sorted(feed.Schedule.keys()) # (3)
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed.Schedule.items()): # (4)
... print(f'{len(value):3} {key}')
...
1 conferences
484 events
357 speakers
53 venues
>>> feed.Schedule.speakers[-1].name # (5)
'Carina C. Zona'
>>> talk = feed.Schedule.events[40]
>>> type(talk) # (6)
<class 'explore0.FrozenJSON'>
>>> talk.name
'There *Will* Be Bugs'
>>> talk.speakers # (7)
[3471, 5199]
>>> talk.flavor # (8)
Traceback (most recent call last):
...
KeyError: 'flavor'

1. Cria uma instância de FrozenJSON a partir de raw_feed , feito de dicts e listas aninhados.

2. FrozenJSON permite navegar dicts aninhados usando a notação de atributos; aqui exibimos o tamanho da lista de
palestrantes.
3. Métodos dos dicts subjacentes também podem ser acessados; por exemplo, .keys() , para recuperar os nomes das
coleções de registros.
4. Usando items() , podemos buscar os nomes das coleções de registros e seus conteúdos, para exibir o len() de
cada um deles.
5. Uma list , tal como feed.Schedule.speakers , permanece uma lista, mas os itens dentro dela, se forem
mapeamentos, são convertidos em um FrozenJSON .
6. O item 40 na lista events era um objeto JSON; agora ele é uma instância de FrozenJSON .

7. Registros de eventos tem uma lista de speakers com os números de séries de palestrantes.
8. Tentar ler um atributo inexistente gera uma exceção KeyError , em vez da AttributeError usual.

A pedra angular da classe FrozenJSON é o metodo __getattr__ , que já usamos no exemplo Vector da seção Seção
12.6, para recuperar componentes de Vector por letra: v.x , v.y , v.z , etc. É essencial lembrar que o método
especial __getattr__ só é invocado pelo interpretador quando o processo habitual falha em recuperar um atributo
(isto é, quando o atributo nomeado não é encontrado na instância, nem na classe ou em suas superclasses).

A última linha do Exemplo 3 expõe um pequeno problema em meu código: tentar ler um atributo ausente deveria
produzir uma exceção AttributeError , e não a KeyError gerada. Quando implementei o tratamento de erro para
fazer isso, o método __getattr__ se tornou duas vezes mais longo, distraindo o leitor da lógica mais importante que
eu queria apresentar. Dado que os usuários saberiam que uma FrozenJSON é criada a partir de mapeamentos e listas,
acho que KeyError não é tão confuso assim.
Exemplo 4. explore0.py: transforma um conjunto de dados JSON em um FrozenJSON contendo objetos FrozenJSON
aninhados, listas e tipos simples

PY
from collections import abc

class FrozenJSON:
"""A read-only façade for navigating a JSON-like object
using attribute notation
"""

def __init__(self, mapping):


self.__data = dict(mapping) # (1)

def __getattr__(self, name): # (2)


try:
return getattr(self.__data, name) # (3)
except AttributeError:
return FrozenJSON.build(self.__data[name]) # (4)

def __dir__(self): # (5)


return self.__data.keys()

@classmethod
def build(cls, obj): # (6)
if isinstance(obj, abc.Mapping): # (7)
return cls(obj)
elif isinstance(obj, abc.MutableSequence): # (8)
return [cls.build(item) for item in obj]
else: # (9)
return obj

1. Cria um dict a partir do argumento mapping . Isso garante que teremos um mapeamento ou algo que poderá ser
convertido para isso. O prefixo de duplo sublinhado em __data o torna um atributo privado.
2. __getattr__ é invocado apenas quando não existe um atributo com aquele name .

3. Se name corresponde a um atributo da instância de dict __data , devolve aquele atributo. É assim que chamadas
como feed.keys() são tratadas: o método keys é um atributo do dict __data .
4. Caso contrário, obtém o item com a chave name de self.__data , e devolve o resultado da chamada
FrozenJSON.build() com aquele argumento.[313]

5. Implementar __dir__ suporta a função embutida dir() , que por sua vez suporta o preenchimento automático
(auto-complete) no console padrão do Python, bem como no IPython, no Jupyter Notebook, etc. Esse código simples
vai permitir preenchimento automático recursivo baseado nas chaves em self.__data , porque __getattr__
cria instâncias de FrozenJSON em tempo real—um recurso útil para a exploração interativa dos dados.
6. Este é um construtor alternativo, um uso comum para o decorador
7. Se obj é um mapeamento, cria um FrozenJSON com ele. Esse é um exmeplo de goose typing—veja a seção Seção
13.5 caso precise de uma revisão desse tópico.
8. Se for uma MutableSequence , tem que ser uma lista[314], então criamos uma list , passando recursivamente
cada item em obj para .build() .
9. Se não for um dict ou uma list , devolve o item com está.

Uma instância de contém um atributo de instância privado __data , armazenado sob o nome
FrozenJSON
_FrozenJSON__data , como explicado na seção Seção 11.10. Tentativas de recuperar atributos por outros nomes vão
disparar __getattr__ . Esse método irá primeiro olhar se o dict self.__data contém um atributo (não uma
chave!) com aquele nome; isso permite que instâncias de FrozenJSON tratem métodos de dict tal como items ,
delegando para self.__data.items() . Se self.__data não contiver uma atributo como o name dado,
__getattr__ usa name como chave para recuperar um item de self.__data , e passa aquele item para
FrozenJSON.build . Isso permite navegar por estruturas aninhadas nos dados JSON, já que cada mapeamento
aninhado é convertido para outra instância de FrozenJSON pelo método de classe build .
Observe que FrozenJSON não transforma ou armazena o conjunto de dados original. Conforme navegamos pelos
dados, __getattr__ cria continuamente instâncias de FrozenJSON . Isso é aceitável para um conjunto de dados deste
tamanho, e para um script que só será usado para explorar ou converter os dados.

Qualquer script que gera ou emula nomes de atributos dinâmicos a partir de fontes arbitrárias precisa lidar com uma
questão: as chaves nos dados originais podem não ser nomes adequados de atributos. A próxima seção fala disso.

22.2.2. O problema do nome de atributo inválido


O código de FrozenJSON não aceita com nomes de atributos que sejam palavras reservadas do Python. Por exemplo,
se você criar um objeto como esse

PYCON
>>> student = FrozenJSON({'name': 'Jim Bo', 'class': 1982})

não será possível ler student.class , porque class é uma palavra reservada no Python:

PYCON
>>> student.class
File "<stdin>", line 1
student.class
^
SyntaxError: invalid syntax

Claro, sempre é possível fazer assim:

PYCON
>>> getattr(student, 'class')
1982

Mas a ideia de FrozenJSON é oferecer acesso conveniente aos dados, então uma solução melhor é verificar se uma
chave no mapamento passado para FrozenJSON.__init__ é uma palavra reservada e, em caso positivo, anexar um
_ a ela, de forma que o atributo possa ser acessado assim:

PYCON
>>> student.class_
1982

Isso pode ser feito substituindo o __init__ de uma linha do Exemplo 4 pela versão no Exemplo 5.

Exemplo 5. explore1.py: anexa um _ a nomes de atributo que sejam palavraas reservadas do Python

PY
def __init__(self, mapping):
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key): # (1)
key += '_'
self.__data[key] = value

1. A função keyword.iskeyword(…) é exatamente o que precisamos; para usá-la, o módulo keyword precisa ser
importado; isso não aparece nesse trecho.
Um problema similar pode surgir se uma chave em um registro JSON não for um identificador válido em Python:

PYCON
>>> x = FrozenJSON({'2be':'or not'})
>>> x.2be
File "<stdin>", line 1
x.2be
^
SyntaxError: invalid syntax

Essas chaves problemáticas são fáceis de detectar no Python 3, porque a classe str oferece o método
s.isidentifier() , que informa se s é um identificador Python válido, de acordo com a gramática da linguagem.
Mas transformar uma chave que não seja um identificador válido em um nome de atributo válido não é trivial. Uma
solução seria implementar __getitem__ para permitir acesso a atributos usando uma notação como x['2be'] . Em
nome da simplicidade, não vou me preocucpar com esse problema.

Após essa pequena conversa sobre os nomes de atributos dinâmicos, vamos examinar outra característica essencial de
FrozenJSON : a lógica do método de classe build . Frozen.JSON.build é usado por __getattr__ para devolver um
tipo diferente de objeto, dependendo do valor do atributo que está sendo acessado: estruturas aninhadas são
convertidas para instâncias de FrozenJSON ou listas de instâncias de FrozenJSON .

Em vez de usar um método de classe, a mesma lógica poderia ser implementada com o método especial __new__ ,
como veremos a seguir.

22.2.3. Criação flexível de objetos com __new__


Muitas vezes nos referimos ao __init__ como o método construtor, mas isso é porque adotamos o jargão de outras
linguagens. No Python, __init__ recebe self como primeiro argumentos, portanto o objeto já existe quando
__init__ é invocado pelo interpretador. Além disso, __init__ não pode devolver nada. Então, na verdade, esse
método é um inicializador, não um construtor.

Quando uma classe é chamada para criar uma instância, o método especial chamado pelo Python naquela classe para
construir a instância é __new__ . É um método de classe, mas recebe tratamento especial, então o decorador
@classmethod não é aplicado a ele. O Python recebe a instância devolvida por __new__ , e daí a passa como o
primeiro argumento ( self ) para __init__ . Raramente precisamos escrever um __new__ , pois a implementação
herdada de object é suficiente na vasta maioria dos casos.

Se necessário, o método __new__ pode também devolver uma instância de uma classe diferente. Quando isso
acontece, o interpretador não invoca __init__ . Em outras palavras, a lógica do Python para criar um objeto é similar
a esse pseudo-código:

PYTHON3
# pseudocode for object construction
def make(the_class, some_arg):
new_object = the_class.__new__(some_arg)
if isinstance(new_object, the_class):
the_class.__init__(new_object, some_arg)
return new_object

# the following statements are roughly equivalent


x = Foo('bar')
x = make(Foo, 'bar')

O Exemplo 6 mostra uma variante de FrozenJSON onde a lógica da antiga classe build foi transferida para o método
__new__ .
Exemplo 6. explore2.py: usando __new__ em vez de build para criar novos objetos, que podem ou não ser instâncias
de FrozenJSON

PY
from collections import abc
import keyword

class FrozenJSON:
"""A read-only façade for navigating a JSON-like object
using attribute notation
"""

def __new__(cls, arg): # (1)


if isinstance(arg, abc.Mapping):
return super().__new__(cls) # (2)
elif isinstance(arg, abc.MutableSequence): # (3)
return [cls(item) for item in arg]
else:
return arg

def __init__(self, mapping):


self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self.__data[key] = value

def __getattr__(self, name):


try:
return getattr(self.__data, name)
except AttributeError:
return FrozenJSON(self.__data[name]) # (4)

def __dir__(self):
return self.__data.keys()

1. Como se trata de um método de classe, o primeiro argumento recebido por __new__ é a própria classe, e os
argumentos restantes são os mesmos recebido por __init__ , exceto por self .
2. O comportamento default é delegar para o __new__ de uma superclasse. Nesse caso, estamos invocando o
__new__ da classe base object , passando FrozenJSON como único argumento.

3. As linhas restantes de __new__ são exatamente as do antigo método build .

4. Era daqui que FrozenJSON.build era chamado antes; agora chamamos apenas a classe FrozenJSON , e o Python
trata essa chamada invocando FrozenJSON.__new__ .

O método __new__ recebe uma classe como primeiro argumento porque, normalmente, o objeto criado será uma
instância daquela classe. Então, em FrozenJSON.__new__ , quando a expressão super().__new__(cls) efetivamente
chama object.__new__(FrozenJSON) , a instância criada pela classe object é, na verdade, uma instância de
FrozenJSON . O atributo __class__ da nova instância vai manter uma referência para FrozenJSON, apesar da
construção concreta ser realizada por object.__new__ , implementado em C, nas entranhas do interpretador.

O conjunto de dados da OSCON está estruturado de uma forma pouco amigável à exploração interativa. Por exemplo, o
evento no índice 40 , chamado 'There Will Be Bugs' (Haverá Bugs) tem dois palestrantes, 3471 e 5199 . Encontrar
os nomes dos palestrantes é confuso, pois esses são números de série e a lista Schedule.speakers não está indexada
por eles. Para obter cada palestrante, precisamos iterar sobre a lista até encontrar um registro com o número de série
correspondente. Nossa próxima tarefa é reestruturar os dados para preparar a recuperação automática de registros
relacionados.
22.3. Propriedades computadas
Vimos inicialmente o decorador @property no Capítulo 11, na seção Seção 11.7. No Exemplo 7, usei duas propriedades
no Vector2d apenas para tornar os atributos x e y apenas para leitura. Aqui vamos ver propriedades que calculam
valores, levando a uma discussão sobre como armazernar tais valores.

Os registros na lista 'events' dos dados JSON da OSCON contêm números de série inteiros apontando para registros
nas listas 'speakers' e 'venues' . Por exemplo, esse é o registro de uma palestra (com a descrição parcial
terminando em reticências):

JSON
{ "serial": 33950,
"name": "There *Will* Be Bugs",
"event_type": "40-minute conference session",
"time_start": "2014-07-23 14:30:00",
"time_stop": "2014-07-23 15:10:00",
"venue_serial": 1449,
"description": "If you're pushing the envelope of programming...",
"website_url": "http://oscon.com/oscon2014/public/schedule/detail/33950",
"speakers": [3471, 5199],
"categories": ["Python"] }

Vamos implementar uma classe Event com propriedades venue e speakers , para devolver automaticamente os
dados relacionados—​em outras palavras, "derreferenciar" o número de série. Dada uma instância de Event , o
Exemplo 7 mostra o comportamento desejado.

Exemplo 7. Ler venue e speakers devolve objetos Record

PYCON
>>> event # (1)
<Event 'There *Will* Be Bugs'>
>>> event.venue # (2)
<Record serial=1449>
>>> event.venue.name # (3)
'Portland 251'
>>> for spkr in event.speakers: # (4)
... print(f'{spkr.serial}: {spkr.name}')
...
3471: Anna Martelli Ravenscroft
5199: Alex Martelli

1. Dada uma instância de Event ,…​

2. …​ler event.venue devolve um objeto Record em vez de um número de série.


3. Agora é fácil obter o nome do venue .

4. A propriedade event.speakers devolve uma lista de instâncias de Record .

Como sempre, vamos criar o código passo a passo, começando com a classe Record e uma função para ler dados JSON
e devolver um dict com instâncias de Record .

22.3.1. Passo 1: criação de atributos baseados em dados


O Exemplo 8 mostra o doctest para orientar esse primeiro passo.

Exemplo 8. Testando schedule_v1.py (do Exemplo 9)


PY
>>> records = load(JSON_PATH) # (1)
>>> speaker = records['speaker.3471'] # (2)
>>> speaker # (3)
<Record serial=3471>
>>> speaker.name, speaker.twitter # (4)
('Anna Martelli Ravenscroft', 'annaraven')

1. load um dict com os dados JSON.


2. As chaves em records são strings criadas a partir do tipo de registro e do número de série.
3. speaker é uma instância da classe Record , definida no Exemplo 9.

4. Campos do JSON original podem ser acessados como atributos de instância de Record .

O código de schedule_v1.py está no Exemplo 9.

Exemplo 9. schedule_v1.py: reorganizando os dados de agendamento da OSCON

PY
import json

JSON_PATH = 'data/osconfeed.json'

class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs) # (1)

def __repr__(self):
return f'<{self.__class__.__name__} serial={self.serial!r}>' # (2)

def load(path=JSON_PATH):
records = {} # (3)
with open(path) as fp:
raw_data = json.load(fp) # (4)
for collection, raw_records in raw_data['Schedule'].items(): # (5)
record_type = collection[:-1] # (6)
for raw_record in raw_records:
key = f'{record_type}.{raw_record["serial"]}' # (7)
records[key] = Record(**raw_record) # (8)
return records

1. Isso é um atalho comum para construir uma instância com atributos criados a partir de argumentos nomeados (a
explicação detalhada está abaixo) .
2. Usa o campo serial para criar a representação personalizada de Record exibida no Exemplo 8.
3. load vai por fim devolver um dict de instâncias de Record .

4. Analisa o JSON, devolvendo objetos Python nativos: listas, dicts, strings, números, etc.
5. Itera sobre as quatro listas principais, chamadas 'conferences' , 'events' , 'speakers' , e 'venues' .

6. record_type é o nome da lista sem o último caractere, então speakers se torna speaker . No Python ≥ 3.9,
podemos fazer isso de forma mais explícita com collection.removesuffix('s') —veja a PEP 616—String
methods to remove prefixes and suffixes (Métodos de string para remover prefixos e sufixos_) (https://fpy.li/pep616).
7. Cria a key no formato 'speaker.3471' .

8. Cria uma instância de Record e a armazena em records com a chave key .


O método Record.__init__ ilustra um antigo truque do Python. Lembre-se que o __dict__ de um objeto é onde são
mantidos seus atributos—​a menos que __slots__ seja declarado na classe, como vimos na seção Seção 11.11. Daí,
atualizar o __dict__ de uma instância é uma maneira fácil de criar um punhado (NT: a bunch no original—veja a
nota de rodapé) de atributos naquela instância.[315]

Dependendo da aplicação, a classe Record pode ter que lidar com chaves que não sejam nomes de
✒️ NOTA atributo válidos, como vimos na seção Seção 22.2.2. Tratar essa questão nos distrairia da ideia
principal desse exemplo, e não é um problema no conjunto de dados que estamos usando.

A definição de Record no Exemplo 9 é tão simples que você pode estar se perguntando porque não a usei antes, em
vez do mais complicado FrozenJSON . São duas razões. Primeiro, FrozenJSON funciona convertendo recursivamente
os mapeamentos aninhados e listas; Record não precisa fazer isso, pois nosso conjunto de dados convertido não
contém mapeamentos aninhados ou listas. Os registros contêm apenas strings, inteiros, listas de strings e listas de
inteiros. A segunda razão: FrozenJSON oferece acesso aos atributos no dict embutido __data —que usamos para
invocar métodos como .keys() —e agora também não precisamos mais dessa funcionalidade.

A biblioteca padrão do Python oferece classes similares a Record , onde cada instância tem um
conjunto arbitrário de atributos criados a partir de argumentos nomeados passados a __init__ :
types.SimpleNamespace (https://docs.python.org/pt-br/3/library/types.html#types.SimpleNamespace),

✒️ NOTA argparse.Namespace (https://docs.python.org/pt-br/3/library/argparse.html#argparse.Namespace) (EN), and


multiprocessing.managers.Namespace
(https://docs.python.org/pt-br/3/library/multiprocessing.html#multiprocessing.managers.Namespace) (EN).
Escrevi a classe Record , mais simples, para destacar a ideia essencial: __init__ atualizando o
__dict__ da instância.

Após reorganizar o conjunto de dados de agendamento, podemos aprimorar a classe Record para obter
automaticamente registros de venue e speaker referenciados em um registro event . Vamos utilizar propriedades
para fazer exatamente isso nos próximos exemplos.

22.3.2. Passo 2: Propriedades para recuperar um registro relacionado


O objetivo da próxima versão é: dado um registro event , ler sua propriedade venue vai devolver um Record . Isso é
similar ao que o ORM (Object Relational Mapping, Mapeamento Relacional de Objetos) do Django faz quando acessamos
um campo ForeignKey : em vez da chave, recebemos o modelo de objeto relacionado.

Vamos começar pela propriedade venue . Veja a interação parcial no Exemplo 10.

Exemplo 10. Extratos dos doctests de schedule_v2.py

PY
>>> event = Record.fetch('event.33950') # (1)
>>> event # (2)
<Event 'There *Will* Be Bugs'>
>>> event.venue # (3)
<Record serial=1449>
>>> event.venue.name # (4)
'Portland 251'
>>> event.venue_serial # (5)
1449

1. O método estático Record.fetch obtém um Record ou um Event do conjunto de dados.


2. Observe que event é uma instância da classe Event .
3. Acessar event.venue devolve uma instância de Record .

4. Agora é fácil encontrar o nome de um event.venue .

5. A instância de Event também tem um atributo venue_serial , vindo dos dados JSON.
Event é uma subclasse de Record , acrescentando um venue para obter os registros relacionados, e um método
__repr__ especializado.

O código dessa seção está no módulo schedule_v2.py (https://fpy.li/22-8), no repositório de código do Python Fluente
(https://fpy.li/code). O exemplo tem aproximadamente 50 linhas, então vou apresentá-lo em partes, começando pela
classe Record aperfeiçoada.

Exemplo 11. schedule_v2.py: a classe Record com um novo método fetch

PY
import inspect # (1)
import json

JSON_PATH = 'data/osconfeed.json'

class Record:

__index = None # (2)

def __init__(self, **kwargs):


self.__dict__.update(kwargs)

def __repr__(self):
return f'<{self.__class__.__name__} serial={self.serial!r}>'

@staticmethod # (3)
def fetch(key):
if Record.__index is None: # (4)
Record.__index = load()
return Record.__index[key] # (5)

1. inspect será usado em load , lista do no Exemplo 13.

2. No final, o atributo de classe privado __index manterá a referência ao dict devolvido por load .

3. fetch é um staticmethod , para deixar explícito que seu efeito não é influenciado pela classe ou pela instância
de onde ele é invocado.
4. Preenche o Record.__index , se necessário.

5. E o utiliza para obter um registro com uma dada key .

Esse é um exemplo onde o uso de staticmethod faz sentido. O método fetch sempre age sobre o

👉 DICA atributo de classe Record.__index , mesmo quando invocado desde uma subclasse, como
Event.fetch() —que exploraremos a seguir. Seria equivocado programá-lo como um método de
classe, pois o primeiro argumento, cls , nunca é usado.

Agora podemos usar a propriedade na classe Event , listada no Exemplo 12.

Exemplo 12. schedule_v2.py: a classe Event


PY
class Event(Record): # (1)

def __repr__(self):
try:
return f'<{self.__class__.__name__} {self.name!r}>' # (2)
except AttributeError:
return super().__repr__()

@property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key) # (3)

1. Event estende Record .

2. Se a instância tem um atributo name , esse atributo será usado para produzir uma representação personalizada.
Caso contrário, delega para o __repr__ de Record .
3. A propriedade venue cria uma key a partir do atributo venue_serial , e a passa para o método de classe fetch ,
herdado de Record (a razão para usar self.__class__ logo ficará clara).

A segunda linha do método venue no Exemplo 12 devolve self​.__class__.fetch(key) . Por que não podemos
simplesmente invocar self.fetch(key) ? A forma simples funciona com esse conjunto específico de dados da OSCON
porque não há registro de evento com uma chave 'fetch' . Mas, se um registro de evento possuísse uma chave
chamada 'fetch' , então dentro daquela instância específica de Event , a referência self.fetch apontaria para o
valor daquele campo, em vez do método de classe fetch que Event herda de Record . Esse é um bug sutil, e poderia
facilmente escapar aos testes, pois depende do conjunto de dados.

Ao criar nomes de atributos de instância a partir de dados, sempre existe o risco de bugs causados

⚠️ AVISO pelo ocultamento de atributos de classe—tais como métodos—ou pela perda de dados por
sobrescrita acidental de atributos de instância existentes. Esses problemas talvez expliquem, mais
que qualquer outra coisa, porque os dicts do Python não são como objetos Javascript.

Se a classe Record se comportasse mais como um mapeamento, implementando um __getitem__ dinâmico em vez
de um __getattr__ dinâmico, não haveria risco de bugs por ocultamento ou sobrescrita. Um mapeamento
personalizado seria provavelmente a forma pythônica de implementar Record . Mas se eu tivesse seguido por aquele
caminho, não estaríamos estudando os truques e as armadilhas da programação dinâmica de atributos.

A parte final deste exemplo é a função load revisada, no Exemplo 13.

Exemplo 13. schedule_v2.py: a função load


PY
def load(path=JSON_PATH):
records = {}
with open(path) as fp:
raw_data = json.load(fp)
for collection, raw_records in raw_data['Schedule'].items():
record_type = collection[:-1] # (1)
cls_name = record_type.capitalize() # (2)
cls = globals().get(cls_name, Record) # (3)
if inspect.isclass(cls) and issubclass(cls, Record): # (4)
factory = cls # (5)
else:
factory = Record # (6)
for raw_record in raw_records: # (7)
key = f'{record_type}.{raw_record["serial"]}'
records[key] = factory(**raw_record) # (8)
return records

1. Até aqui, nenhuma mudança em relação ao load em schedule_v1.py (do Exemplo 9).
2. Muda a primeira letra de record_type para maiúscula, para obter um possível nome de classe; por exemplo,
'event' se torna 'Event' .

3. Obtém um objeto com aquele nome do escopo global do módulo; se aquele objeto não existir, obtém a classe
Record .

4. Se o objeto recém-obtido é uma classe, e é uma subclasse de Record …​

5. …​vincula o nome factory a ele. Isso significa que factory pode ser qualquer subclasse de Record , dependendo
do record_type .
6. Caso contrário, vincula o nome factory a Record .

7. O loop for , que cria a key e armazena os registros, é o mesmo de antes, exceto que…​
8. …​o objeto armazenado em records é construído por factory , e pode ser Record ou uma subclasse, como
Event , selecionada de acordo com o record_type .

Observe que o único record_type que tem uma classe personalizada é Event , mas se classes chamadas Speaker ou
Venue existirem, load vai automaticamente usar aquelas classes ao criar e armazenar registros, em vez da classe
default Record .

Vamos agora aplicar a mesma ideia à nova propriedade speakers , na classe Events .

22.3.3. Passo 3: Uma propriedade sobrepondo um atributo existente


O nome da propriedade venue no Exemplo 12 não corresponde a um nome de campo nos registros da coleção
"events" . Seus dados vem de um nome de campo venue_serial . Por outro lado, cada registro na coleção events
tem um campo speakers , contendo uma lista de números de série. Queremos expor essa informação na forma de
uma propriedade speakers em instâncias de Event , que devolve um lista de instâncias de Record . Essa colisão de
nomes exige uma atenção especial, como revela o Exemplo 14.

Exemplo 14. schedule_v3.py: a propriedade speakers


PY
@property
def speakers(self):
spkr_serials = self.__dict__['speakers'] # (1)
fetch = self.__class__.fetch
return [fetch(f'speaker.{key}')
for key in spkr_serials] # (2)

1. Os dados que precisamos estão em um atributo speakers , mas precisamos obtê-los diretamente do __dict__ da
instância, para evitar uma chamada recursiva à propriedade speakers .
2. Devolve uma lista de todos os registros com chaves correspondendo aos números em spkr_serials .

Dentro do método speakers , tentar ler self.speakers irá invocar a própria propriedade, gerando rapidamente um
RecursionError . Entretanto, se lemos os mesmos dados via self.__dict__['speakers'] , o algoritmo normal do
Python para busca e recuperação de atributos é ignorado, a propriedade não é chamada e a recursão é evitada. Por
essa razão, ler ou escrever dados diretamente no __dict__ de um objeto é um truque comum em metaprogramação
no Python.

O interpretador avalia obj.my_attr olhando primeiro a classe de obj . Se a classe possuir uma
propriedade de nome my_attr , aquela propriedade oculta um atributo de instância com o mesmo
⚠️ AVISO nome. Isso será demonstrado por exemplos na seção Seção 22.5.1, e o [attribute_descriptors] vai
revelar que uma propriedade é implementada como um descritor—uma abstração mais geral e
poderosa.

Quando programava a compreensão de lista no Exemplo 14, meu cérebro réptil de programador pensou: "Isso talvez
seja custoso". Na verdade não é, porque os eventos no conjuntos de dados da OSCON contêm poucos palestrantes, então
programar algo mais complexo seria uma otimização prematura. Entretanto, criar um cache de uma propriedade é
uma necessidade comum—e há ressalvas. Vamos ver então, nos próximos exemplos, como fazer isso.

22.3.4. Passo 4: Um cache de propriedades sob medida


Fazer caching de propriedades é uma necessidade comum, pois há a expectativa de que uma expressão como
event.venue deveria ser pouco dispendiosa.[316] Alguma forma de caching poderia se tornar necessário caso o
método Record.fetch , subjacente às propriedades de Event , precise consultar um banco de dados ou uma API web.

Na primeira edição de Python Fluente, programei a lógica personalizada de caching para o método speakers , como
mostra o Exemplo 15.

Exemplo 15. A lógica de caching personalizada usando hasattr desabilita a otimização de compartilhamento de
chaves

PY
@property
def speakers(self):
if not hasattr(self, '__speaker_objs'): # (1)
spkr_serials = self.__dict__['speakers']
fetch = self.__class__.fetch
self.__speaker_objs = [fetch(f'speaker.{key}')
for key in spkr_serials]
return self.__speaker_objs # (2)

1. Se a instância não tem um atributo chamado __speaker_objs , obtém os objetos speaker e os armazena ali..
2. Devolve self.__speaker_objs .
O caching caseiro no Exemplo 15 é bastante direto, mas criar atributos após a inicialização da instância frustra a
otimização da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves) (https://fpy.li/pep412), como
explicado na seção Seção 3.9. Dependendo do tamanho da massa de dados, a diferença de uso de memória pode ser
importante.

Uma solução manual similar, que funciona bem com a otimização de compartilhamento de chaves, exige escrever um
__init__ para a classe Event , para criar o necessário __speaker_objs inicializado para None , e então usá-lo no
método speakers . Veja o Exemplo 16.

Exemplo 16. Armazenamento definido em __init__ para manter a otimização de compartilhamento de chaves

PY
class Event(Record):

def __init__(self, **kwargs):


self.__speaker_objs = None
super().__init__(**kwargs)

# 15 lines omitted...
@property
def speakers(self):
if self.__speaker_objs is None:
spkr_serials = self.__dict__['speakers']
fetch = self.__class__.fetch
self.__speaker_objs = [fetch(f'speaker.{key}')
for key in spkr_serials]
return self.__speaker_objs

O Exemplo 15 e o Exemplo 16 ilustram técnicas simples de caching bastante comuns em bases de código Python
legadas. Entretanto, em programas com múltiplas threads, caches manuais como aqueles introduzem condições de
concorrência (ou de corrida) que podem levar à corrupção de dados. Se duas threads estão lendo uma propriedade que
não foi armazenada no cache anteriormente, a primeira thread precisará computar os dados para o atributo de cache
( _speaker_objs nos exemplos) e a segunda thread corre o risco de ler um valor incompleto do _cache.

Felizmente, o Python 3.8 introduziu o decorador @functools.cached_property , que é seguro para uso com threads.
Infelizmente, ele vem com algumas ressalvas, discutidas a seguir.

22.3.5. Passo 5: Caching de propriedades com functools


O módulo functools oferece três decoradores para caching. Vimos @cache e @lru_cache na seção Seção 9.9.1 (no
Capítulo 9). O Python 3.8 introduziu @cached_property .

O decorador functools.cached_property faz cache do resultado de um método em uma variável de instância com o
mesmo nome.

Por exemplo, no Exemplo 17, o valor computado pelo método venue é armazenado em um atributo venue , em self .
Após isso, quando código cliente tenta ler venue , o recém-criado atributo de instância venue é usado, em vez do
método.

Exemplo 17. Uso simples de uma @cached_property

PY
@cached_property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key)
Na seção Seção 22.3.3, vimos que uma propriedade oculta um atributo de instância de mesmo nome. Se isso é verdade,
como @cached_property pode funcionar? Se a propriedade se sobrepõe ao atributo de instância, o atributo venue
será ignorado e o método venue será sempre chamado, computando a key e rodando fetch todas as vezes!

A resposta é um tanto triste: cached_property é um nome enganador. O decorador @cached_property não cria uma
propriedade completa, ele cria um descritor não dominante. Um descritor é um objeto que gerencia o acesso a um
atributo em outra classe. Vamos mergulhar nos descritores no [attribute_descriptors]. O decorador property é uma
API de alto nível para criar um descritor dominante. O [attribute_descriptors] inclui um explicação completa sobre
descritores dominantes e não dominantes.

Por hora, vamos deixar de lado a implementação subjacente e nos concentrar nas diferenças entre cached_property
e property do ponto de vista de um usuário. Raymond Hettinger os explica muito bem na Documentação do Python
(https://docs.python.org/pt-br/3/library/functools.html#functools.cached_property):

“ Aregular
mecânica de
bloqueia
é um tanto diferente da de property() . Uma propriedade
cached_property()
a escrita em atributos, a menos que um setter seja definido. Uma
cached_property , por outro lado, permite a escrita.

O decorador cached_property só funciona para consultas e apenas quando um atributo de


mesmo nome não existe. Quando funciona, cached_property escreve no atributo de mesmo
nome. Leituras e escritas subsequentes do/no atributo tem precedência sobre o método decorado
com cached_property e ele funciona como um atributo normal.

O valor em cache pode ser excluído apagando-se o atributo. Isso permite que o método
cached_property rode novamente.[317]

Voltando à nossa classe Event : o comportamento específico de @cached_property o torna inadequado para decorar
speakers , porque aquele método depende de um atributo existente também chamado speakers , contendo os
números de série dos palestrantes do evento.

@cached_property tem algumas importantes limitações:

Ele não pode ser usado como um substituto direto de @property se o método decorado já

⚠️ AVISO depender de um atributo de instância de mesmo nome.


Ele não pode ser usado em uma classe que defina __slots__ .

Ele impede a otimização de compartilhamento de chaves do __dict__ da instância, pois cria um


atributo de instância após o __init__ .

Apesar dessas limitações, @cached_property supre uma necessidade comum de uma maneira simples, e é seguro
para usar com threads. Seu código Python (https://fpy.li/22-13) é um exemplo do uso de uma trava recursiva (reentrant
lock) (https://docs.python.org/pt-br/3/library/threading.html#rlock-objects).

A documentação (https://docs.python.org/pt-br/3.10/library/functools.html#functools.cached_property) de @cached_property


recomenda uma solução altenativa que podemos usar com speakers : Empilhar decoradores @property e @cache ,
como exibido no Exemplo 18.

Exemplo 18. Stacking @property sobre @cache


PY
@property # (1)
@cache # (2)
def speakers(self):
spkr_serials = self.__dict__['speakers']
fetch = self.__class__.fetch
return [fetch(f'speaker.{key}')
for key in spkr_serials]

1. A ordem é importante: @property vai acima…​


2. …​de @cache .

Lembre-se do significado dessa sintaxe, comentada em Decoradore empilhados. A três primeiras linhas do Exemplo 18
são similares a :

PY
speakers = property(cache(speakers))

O @cache é aplicado a speakers , devolvendo uma nova função. Essa função é então decorada por @property , que a
substitui por uma propriedade recém-criada.

Isso encerra nossa discussão de propriedades somente para leitura e decoradores de caching, explorando o conjunto de
dados da OSCON.

Na próxima seção iniciamos uma nova série de exemplos, criando propriedades de leitura e escrita.

22.4. Usando uma propriedade para validação de atributos


Além de computar valores de atributos, as propriedades também são usadas para impor regras de negócio,
transformando um atributo público em um atributo protegido por um getter e um setter, sem afetar o código cliente.
Vamos explorar um exemplo estendido.

22.4.1. LineItem Versão #1: Um classe para um item em um pedido


Imagine uma aplicação para uma loja que vende comida orgânica a granel, onde os fregueses podem encomendar
nozes, frutas secas e cereais por peso. Nesse sistema, cada pedido mantém uma sequência de produtos, e cada produto
pode ser representado por uma instância de uma classe, como no Exemplo 19.

Exemplo 19. bulkfood_v1.py: a classe LineItem mais simples

PY
class LineItem:

def __init__(self, description, weight, price):


self.description = description
self.weight = weight
self.price = price

def subtotal(self):
return self.weight * self.price

Esse código é simples e agradável. Talvez simples demais. Exemplo 20 mostra um problema.

Exemplo 20. Um peso negativo resulta em um subtotal negativo


PY
>>> raisins = LineItem('Golden raisins', 10, 6.95)
>>> raisins.subtotal()
69.5
>>> raisins.weight = -20 # garbage in...
>>> raisins.subtotal() # garbage out...
-139.0

Apesar desse ser um exemplo inventado, não é tão fantasioso quanto se poderia imaginar. Aqui está uma história do
início da Amazon.com:

“ Descobrimos que os clientes podiam encomendar uma quantidade negativa de livros! E nós
creditariamos seus cartões de crédito com o preço e, suponho, esperaríamos que eles nos
enviassem os livros.[318]
— Jeff Bezos
fundador e CEO da Amazon.com

Como consertar isso? Poderíamos mudar a interface de LineItem para usar um getter e um setter para o atributo
weight . Esse seria o caminho do Java, e não está errado. Por outro lado, é natural poder determinar o weight (peso)
de um item apenas atribuindo um valor a ele; e talvez o sistema esteja em produção, com outras partes já acessando
item.weight diretamente. Nesse caso, o caminho do Python seria substituir o atributo de dados por uma propriedade.

22.4.2. LineItem versão #2: Uma propriedade de validação


Implementar uma propriedade nos permitirá usar um getter e um setter, mas a interface de LineItem não mudará
(isto é, definir o weight de um LineItem ainda será escrito no formato raisins.weight = 12 ).

O Exemplo 21 lista o código para uma propriedade de leitura e escrita de weight .

Exemplo 21. bulkfood_v2.py: um LineItem com uma propriedade weight

PY
class LineItem:

def __init__(self, description, weight, price):


self.description = description
self.weight = weight # (1)
self.price = price

def subtotal(self):
return self.weight * self.price

@property # (2)
def weight(self): # (3)
return self.__weight # (4)

@weight.setter # (5)
def weight(self, value):
if value > 0:
self.__weight = value # (6)
else:
raise ValueError('value must be > 0') # (7)

1. Aqui o setter da propriedade já está em uso, assegurando que nenhuma instância com peso negativo possa ser
criada.
2. @property decora o método getter.
3. Todos os métodos que implementam a propriedade compartilham o mesmo nome, do atributo público: weight .

4. O valor efetivo é armazenado em um atributo privado __weight .

5. O getter decorado tem um atributo .setter , que também é um decorador; isso conecta o getter e o setter.

6. Se o valor for maior que zero, definimos o __weight privado.


7. Caso contrário, uma ValueError é gerada.
Observe como agora não é possível criar uma LineItem com peso inválido:

PYCON
>>> walnuts = LineItem('walnuts', 0, 10.00)
Traceback (most recent call last):
...
ValueError: value must be > 0

Agora protegemos weight impedindo que usuários forneçam valores negativos. Apesar de compradores normalmente
não poderem definir o preço de um produto, um erro administrativo ou um bug poderiam criar um LineItem com
um price negativo. Para evitar isso, poderíamos também transformar price em uma propriedade, mas isso levaria a
alguma repetição no nosso código.

Lembre-se da citação de Paul Graham no Capítulo 17: "Quando vejo padrões em meus programas, considero isso um
mau sinal." A cura para a repetição é a abstração. Há duas maneiras de abstrair definições de propriedades: usar uma
fábrica de propriedades ou uma classe descritora. A abordagem via classe descritora é mais flexível, e dedicaremos o
[attribute_descriptors] a uma discussão completa desse recurso. Na verdade, propriedades são, elas mesmas,
implementadas como classes descritoras. Mas aqui vamos seguir com nossa exploração das propriedades,
implementando uma fábrica de propriedades em forma de função.

Mas antes de podermos implementar uma fábrica de propriedades, precisamos entender melhor as propriedades em
si.

22.5. Considerando as propriedades de forma adequada


Apesar de ser frequentemente usada como um decorador, property é na verdade uma classe embutida. No Python,
funções e classes são muitas vezes intercambiáveis, pois ambas são invocáveis e não há um operador new para
instanciação de objeto, então invocar um construtor não é diferente de invocar uma função fábrica. E ambas podem
ser usadas como decoradores, desde que elas devolvam um novo invocável, que seja um substituto adequado do
invocável decorado.

Essa é a assinatura completa do construtor de property :

PYTHON3
property(fget=None, fset=None, fdel=None, doc=None)

Todos os argumentos são opcionais, e se uma função não for fornecida para algum deles, a operação correspondente
não será permitida pelo objeto propriedade resultante.

O tipo property foi introduzido no Python 2.2, mas a sintaxe @ do decorador só surgiu no Python 2.4. Então, por
alguns anos, propriedades eram definidas passando as funções de acesso nos dois primeiros argumentos.

A sintaxe "clássica" para definir propriedades sem decoradores é ilustrada pelo Exemplo 22.

Exemplo 22. bulkfood_v2b.py: igual ao Exemplo 21, mas sem usar decoradores
PY
class LineItem:

def __init__(self, description, weight, price):


self.description = description
self.weight = weight
self.price = price

def subtotal(self):
return self.weight * self.price

def get_weight(self): # (1)


return self.__weight

def set_weight(self, value): # (2)


if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')

weight = property(get_weight, set_weight) # (3)

1. Um getter simples.
2. Um setter simples.
3. Cria a property e a vincula a um atributo de classe simples.

Em algumas situações, a forma clássica é melhor que a sintaxe do decorador; o código da fábrica de propriedade, que
discutiremos em breve, é um exemplo. Por outro lado, no corpo de uma classe com muitos métodos, os decoradores
tornam explícito quais são os getters e os setters, sem depender da convenção do uso dos prefixos get e set em seus
nomes.

A presença de uma propriedade em uma classe afeta como os atributos nas instâncias daquela classe podem ser
encontrados, de uma forma que à primeira vista pode ser surpreendente. A próxima seção explica isso.

22.5.1. Propriedades sobrepõe atributos de instância


Propriedades são sempre atributos de classe, mas elas na verdade geranciam o acesso a atributos nas instâncias da
classe.

Na seção Seção 11.12, vimos que quando uma instância e sua classe tem um atributo de dados com o mesmo nome, o
atributo de instância sobrepõe, ou oculta, o atributo da classe—ao menos quando lidos através daquela instância. O
Exemplo 23 ilustra esse ponto.

Exemplo 23. Atributo de instância oculta o atributo de classe data


PYCON
>>> class Class: # (1)
... data = 'the class data attr'
... @property
... def prop(self):
... return 'the prop value'
...
>>> obj = Class()
>>> vars(obj) # (2)
{}
>>> obj.data # (3)
'the class data attr'
>>> obj.data = 'bar' # (4)
>>> vars(obj) # (5)
{'data': 'bar'}
>>> obj.data # (6)
'bar'
>>> Class.data # (7)
'the class data attr'

1. Define Class com dois atributos de classe: o atributo data e a propriedade prop .

2. vars devolve o __dict__ de obj , mostrando que ele não tem atributos de instância.

3. Ler de obj.data obtém o valor de Class.data .

4. Escrever em obj.data cria um atributo de instância.


5. Inspeciona a instância, para ver o atributo de instância.
6. Ler agora de obj.data obtém o valor do atributo da instância. Quanto lido a partir da instância obj , o data da
instância oculta o data da classe.
7. O atributo Class.data está intacto.

Agora vamos tentar sobrepor o atributo prop na instância obj . Continuando a sessão de console anterior, temos o
Exemplo 24.

Exemplo 24. Um atributo de instância não oculta uma propriedade da classe (continuando do Exemplo 23)

PYCON
>>> Class.prop # (1)
<property object at 0x1072b7408>
>>> obj.prop # (2)
'the prop value'
>>> obj.prop = 'foo' # (3)
Traceback (most recent call last):
...
AttributeError: can't set attribute
>>> obj.__dict__['prop'] = 'foo' # (4)
>>> vars(obj) # (5)
{'data': 'bar', 'prop': 'foo'}
>>> obj.prop # (6)
'the prop value'
>>> Class.prop = 'baz' # (7)
>>> obj.prop # (8)
'foo'

1. Ler prop diretamente de Class obtém o próprio objeto propriedade, sem executar seu método getter.
2. Ler obj.prop executa o getter da propriedade.
3. Tentar definir um atributo prop na instância falha.
4. Inserir 'prop' diretamente em obj.__dict__ funciona.
5. Podemos ver que agora obj tem dois atributos de instância: data e prop .

6. Entretanto, ler obj.prop ainda executa o getter da propriedade. A propriedade não é ocultada pelo atributo de
instância.
7. Sobrescrever Class.prop destrói o objeto propriedade.
8. Agora obj.prop obtém o atributo de instância. Class.prop não é mais uma propriedade, então ela não mais
sobrepõe obj.prop .
Como uma demonstração final, vamos adicionar uma propriedade a Class , e vê-la sobrepor um atributo de instância.
O Exemplo 25 retoma a sessão onde Exemplo 24 parou.

Exemplo 25. Uma nova propriedade de classe oculta o atributo de instância existente (continuando do Exemplo 24)

PYCON
>>> obj.data # (1)
'bar'
>>> Class.data # (2)
'the class data attr'
>>> Class.data = property(lambda self: 'the "data" prop value') # (3)
>>> obj.data # (4)
'the "data" prop value'
>>> del Class.data # (5)
>>> obj.data # (6)
'bar'

1. obj.data obtém o atributo de instância data .

2. Class.data obtém o atributo de classe data .

3. Sobrescreve Class.data com uma nova propriedade.


4. obj.data está agora ocultado pela propriedade Class.data .

5. Apaga a propriedade .
6. obj.data agora lê novamente o atributo de instância data .

O ponto principal desta seção é que uma expressão como obj.data não começa a busca por data em obj . A busca
na verdade começa em obj.__class__ , e o Python só olha para a instância obj se não houver uma propriedade
chamada data na classe. Isso se aplica a descritores dominantes em geral, dos quais as propriedades são apenas um
exemplo. Mas um tratamento mais profundo de descritores vai ter que aguardar pelo [attribute_descriptors].

Voltemos às propriedades. Toda unidade de código do Python—módulos, funções, classes, métodos—pode conter uma
docstring. O próximo tópico mostra como anexar documentação às propriedades.

22.5.2. Documentação de propriedades


Quando ferramentas como a função help() do console ou IDEs precisam mostrar a documentação de uma
propriedade, elas extraem a informação do atributo __doc__ da propriedade.

Se usada com a sintaxe clássica de invocação, property pode receber a string de documentação no argumento doc :

PYTHON3
weight = property(get_weight, set_weight, doc='weight in kilograms')

A docstring do método getter—aquele que recebe o decorador @property —é usado como documentação da
propriedade toda. O Figura 1 mostra telas de ajuda geradas a partir do código no Exemplo 26.
Figura 1. Capturas de tela do console do Python para os comandos help(Foo.bar) e help(Foo) . O código-fonte está
no Exemplo 26.

Exemplo 26. Documentação para uma propriedade

PY
class Foo:

@property
def bar(self):
"""The bar attribute"""
return self.__dict__['bar']

@bar.setter
def bar(self, value):
self.__dict__['bar'] = value

Agora que cobrimos o essencial sobre as propriedades, vamos voltar para a questão de proteger os atributos weight e
price de LineItem , para que eles só aceitem valores maiores que zero—mas sem implementar manualmente dois
pares de getters/setters praticamente idênticos.

22.6. Criando uma fábrica de propriedades


Vamos programar uma fábrica para criar propriedades quantity (quantidade)--assim chamadas porque os atributos
gerenciados representam quantidades que não podem ser negativas ou zero na aplicação. O Exemplo 27 mostra a
aparência cristalina da classe LineItem usando duas instâncias de propriedades quantity : uma para gerenciar o
atributo weight , a outra para o price .

Exemplo 27. bulkfood_v2prop.py: a fábrica de propriedades quantity em ação

PY
class LineItem:
weight = quantity('weight') # (1)
price = quantity('price') # (2)

def __init__(self, description, weight, price):


self.description = description
self.weight = weight # (3)
self.price = price

def subtotal(self):
return self.weight * self.price # (4)
1. Usa a fábrica para definir a primeira propriedade personalizada, weight , como um atributo de classe.

2. Essa segunda chamada cria outra propriedade personalizada, price .

3. Aqui a propriedade já está ativa, assegurando que um peso negativo ou 0 seja rejeitado.
4. As propriedades também são usadas aqui, para recuperar os valores armazenados na instância.

Recorde que propriedades são atributos de classe. Ao criar cada propriedade quantity , precisamos passar o nome do
atributo de LineItem que será geranciado por aquela propriedade específica. Ter que digitar a palavra weight duas
vezes na linha abaixo é lamentável:

PYTHON3
weight = quantity('weight')

Mas evitar tal repetição é complicado, pois a propriedade não tem como saber qual nome de atributo será vinculado a
ela. Lembre-se: o lado direito de uma atribuição é avaliado primeiro, então quando quantity() é invocada, o atributo
de classe weight sequer existe.

Aperfeiçoar a propriedade quantity para que o usuário não precise redigitar o nome do atributo é
✒️ NOTA uma problema não-trivial de metaprogramação. Um problema que resolveremos no
[attribute_descriptors].

O Exemplo 28 apresenta a implementação da fábrica de propriedades quantity .[319]

Exemplo 28. bulkfood_v2prop.py: a fábrica de propriedades quantity

PY
def quantity(storage_name): # (1)

def qty_getter(instance): # (2)


return instance.__dict__[storage_name] # (3)

def qty_setter(instance, value): # (4)


if value > 0:
instance.__dict__[storage_name] = value # (5)
else:
raise ValueError('value must be > 0')

return property(qty_getter, qty_setter) # (6)

1. O argumento storage_name , onde os dados de cada propriedade são armazenados; para weight , o nome do
armazenamento será 'weight' .
2. O primeiro argumento do qty_getter poderia se chamar self , mas soaria estranho, pois isso não é o corpo de
uma classe; instance se refere à instância de LineItem onde o atributo será armazenado.
3. qty_getter se refere a storage_name , então ele será preservado na clausura desta função; o valor é obtido
diretamente de instance.__dict__ , para contornar a propriedade e evitar uma recursão infinita.
4. qty_setter é definido, e também recebe instance como primeiro argumento.
5. O value é armazenado diretamente no instance.__dict__ , novamente contornando a propriedade.

6. Cria e devolve um objeto propriedade personalizado.

As partes do Exemplo 28 que merecem um estudo mais cuidadoso giram em torno da variável storage_name .
Quando programamos um propriedade da maneira tradicional, o nome do atributo onde um valor será armazenado
está definido explicitamente nos métodos getter e setter. Mas aqui as funções qty_getter e qty_setter são
genéricas, e dependem da variável storage_name para saber onde ler/escrever o atributo gerenciado no __dict__
da instância. Cada vez que a fábrica quantity é chamada para criar uma propriedade, storage_name precisa ser
definida com um valor único.

As funções qty_getter e qty_setter serão encapsuladas pelo objeto property , criado na última linha da função
fábrica. Mais tarde, quando forem chamadas para cumprir seus papéis, essas funções lerão a storage_name de suas
clausuras para determinar de onde ler ou onde escrever os valores dos atributos gerenciados.

No Exemplo 29, criei e inspecionei uma instância de LineItem , expondo os atributos armazenados.

Exemplo 29. bulkfood_v2prop.py: explorando propriedades e atributos de armazenamento

PYCON
>>> nutmeg = LineItem('Moluccan nutmeg', 8, 13.95)
>>> nutmeg.weight, nutmeg.price # (1)
(8, 13.95)
>>> nutmeg.__dict__ # (2)
{'description': 'Moluccan nutmeg', 'weight': 8, 'price': 13.95}

1. Lendo o weight eo price através das propriedades que ocultam os atributos de instância de mesmo nome.
2. Usando vars para inspecionar a instância nutmeg : aqui vemos os reais atributos de instância usados para
armazenar os valores.

Observe como as propriedades criadas por nossa fábrica se valem do comportamento descrito na seção Seção 22.5.1: a
propriedade weight se sobrepõe ao atributo de instância weight , de forma que qualquer referência a self.weight
ou nutmeg.weight é tratada pelas funções da propriedade, e a única maneira de contornar a lógica da propriedade é
acessando diretamente o `__dict__`da instância.

O código no Exemplo 28 pode ser um pouco complicado, mas é conciso: seu tamanho é idêntico ao do par getter/setter
decorado que define apenas a propriedade weight no Exemplo 21. A definição de LineItem no Exemplo 27 parece
muito melhor sem o ruído de getters e setters.

Em um sistema real, o mesmo tipo de validação pode aparecer em muitos campos espalhados por várias classes, e a
fábrica quantity estaria em um módulo utilitário, para ser usada continuamente. Por fim, aquela fábrica simples
poderia ser refatorada em um classe descritora mais extensível, com subclasses especializadas realizando diferentes
validações. Faremos isso no [attribute_descriptors].

Vamos agora encerrar a discussão das propriedades com a questão da exclusão de atributos.

22.7. Tratando a exclusão de atributos


Podemos usar a instrução del para excluir não apenas variáveis, mas também atributos:
PYCON
>>> class Demo:
... pass
...
>>> d = Demo()
>>> d.color = 'green'
>>> d.color
'green'
>>> del d.color
>>> d.color
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Demo' object has no attribute 'color'

Na prática, a exclusão de atributos não é algo que se faça todo dia no Python, e a necessidade de lidar com isso no caso
de uma propriedade é ainda mais rara. Mas tal operação é suportada, e consigo pensar em um exemplo bobo para
demonstrá-la.

Em uma definição de propriedade, o decorador @my_property.deleter encapsula o método responsável por excluir
o atributo gerenciado pela propriedade. Como prometido, o tolo Exemplo 30 foi inspirado pela cena com o Cavaleiro
Negro, do filme Monty Python e o Cálice Sagrado.[320]

Exemplo 30. blackknight.py

PY
class BlackKnight:

def __init__(self):
self.phrases = [
('an arm', "'Tis but a scratch."),
('another arm', "It's just a flesh wound."),
('a leg', "I'm invincible!"),
('another leg', "All right, we'll call it a draw.")
]

@property
def member(self):
print('next member is:')
return self.phrases[0][0]

@member.deleter
def member(self):
member, text = self.phrases.pop(0)
print(f'BLACK KNIGHT (loses {member}) -- {text}')

Os doctests em blackknight.py estão no Exemplo 31.

Exemplo 31. blackknight.py: doctests para Exemplo 30 (o Cavaleiro Negro nunca reconhece a derrota)
PYCON
>>> knight = BlackKnight()
>>> knight.member
next member is:
'an arm'
>>> del knight.member
BLACK KNIGHT (loses an arm) -- 'Tis but a scratch.
>>> del knight.member
BLACK KNIGHT (loses another arm) -- It's just a flesh wound.
>>> del knight.member
BLACK KNIGHT (loses a leg) -- I'm invincible!
>>> del knight.member
BLACK KNIGHT (loses another leg) -- All right, we'll call it a draw.

Usando a sintaxe clássica de invocação em vez de decoradores, o argumento fdel configura a função de exclusão. Por
exemplo, a propriedade member seria escrita assim no corpo da classe BlackKnight :

PYTHON3
member = property(member_getter, fdel=member_deleter)

Se você não estiver usando uma propriedade, a exclusão de atributos pode ser tratada implementando o método
especial de nível mais baixo __delattr__ , apresentado na seção Seção 22.8.3. Programar um classe tola com
__delattr__ fica como exercício para a leitora que queira procrastinar.

Propriedades são recursos poderosos, mas algumas vezes alternativas mais simples ou de nível mais baixo são
preferíveis. Na seção final deste capítulo, vamos revisar algumas das APIs essenciais oferecidas pelo Python para
programação de atributos dinâmicos.

22.8. Atributos e funções essenciais para tratamento de atributos


Por todo este capítulo, e mesmo antes no livro, usamos algumas das funções embutidas e alguns dos métodos especiais
oferecidos pelo Python para lidar com atributos dinâmicos. Esta seção os reúne em um único lugar para uma visão
geral, pois sua documentação está espalhada na documentação oficial.

22.8.1. Atributos especiais que afetam o tratamento de atributos


O comportamento de muitas das funções e dos métodos especiais elencados nas próximas seções dependem de três
atributos especiais:

__class__

Uma referência à classe do objeto (isto é, obj.__class__ é o mesmo que type(obj) ). O Python procura por
métodos especiais tal como __getattr__ apenas na classe do objeto, e não nas instâncias em si.

__dict__

Um mapeamento que armazena os atributos passíveis de escrita de um objeto ou de uma classe. Um objeto que
tenha um __dict__ pode ter novos atributos arbitrários definidos a qualquer tempo. Se uma classe tem um
atributo __slots__ , então suas instâncias não podem ter um __dict__ . Veja __slots__ (abaixo).

__slots__
Um atributo que pode ser definido em uma classe para economizar memória. __slots__ é uma tuple de strings,
nomeando os atributos permitidos[321]. Se o nome '__dict__' não estiver em __slots__ , as instâncias daquela
classe então não terão um __dict__ próprio, e apenas os atributos listados em __slots__ serão permitidos
naquelas instâncias. Revise a seção Seção 11.11 para recordar esse tópico.

22.8.2. Funções embutidas para tratamento de atributos


Essas cinco funções embutidas executam leitura, escrita e introspecção de atributos de objetos:

dir([object])

Lista a maioria dis atributos de um objeto. A documentação oficial


(https://docs.python.org/pt-br/3/library/functions.html#dir) diz que o objetivo de dir é o uso interativo, então ele não
fornece uma lista completa de atributos, mas um conjunto de nomes "interessantes". dir pode inspecionar objetos
implementados com ou sem um __dict__ . O próprio atributo __dict__ não é exibido por dir , mas as chaves de
__dict__ são listadas. Vários atributos especiais de classes, tais como __mro__ , __bases__ e __name__ , também
não são exibidos por dir . Você pode personalziar a saída de dir implementando o método especial __dir__ ,
como vimos no Exemplo 4. Se o argumento opcional object não for passado, dir lista os nomes no escopo
corrente.

getattr(object, name[, default])


Devolve o atributo do object identificado pela string name . O principal caso de uso é obter atributos (ou métodos)
cujos nomes não sabemos de antemão. Essa função pode recuperar um atributo da classe do objeto ou de uma
superclasse. Se tal atributo não existir, getattr gera uma AttributeError ou devolve o valor default , se ele for
passado. Um ótimo exemplo de uso de gettatr aparece no método Cmd.onecmd (https://fpy.li/22-19), no pacote cmd
da biblioteca padrão, onde ela é usada para obter e executar um comando definido pelo usuário.

hasattr(object, name)

Devolve True se o atributo nomeado existir em object , ou puder ser obtido de alguma forma através dele (por
herança, por exemplo). A documentação (https://docs.python.org/pt-br/3/library/functions.html#hasattr) explica: "Isto é
implementado chamando getattr(object, name) e vendo se [isso] levanta um AttributeError ou não."

setattr(object, name, value)

Atribui o value ao atributo de object nomeado, se o object permitir essa operação. Isso pode criar um novo
atributo ou sobrescrever um atributo existente.

vars([object])
Devolve o __dict__ de object ; vars não funciona com instâncias de classes que definem __slots__ e não têm
um __dict__ (compare com dir , que aceita essas instâncias). Sem argumentos, vars() faz o mesmo que
locals() : devolve um dict representando o escopo local.

22.8.3. Métodos especiais para tratamento de atributos


Quando implementados em uma classe definida pelo usuário, os métodos especiais listados abaixo controlam a
recuperação, a atualização, a exclusão e a listagem de atributos.

Acessos a atributos, usando tanto a notação de ponto ou as funções embutidas getattr , hasattr e setattr
disparam os métodos especiais adequados, listados aqui. A leitura e escrita direta de atributos no __dict__ da
instância não dispara esses métodos especiais—​e essa é a forma habitual de evitá-los se isso for necessário.

A seção "3.3.11. Pesquisa de método especial"


(https://docs.python.org/pt-br/3.10/reference/datamodel.html#special-method-lookup) do capítulo "Modelo de dados" adverte:

“ Para classes personalizadas, as invocações implícitas de métodos especiais só têm garantia de


funcionar corretamente se definidas em um tipo de objeto, não no dicionário de instância do
objeto.

Em outras palavras, assuma que os métodos especiais serão acessados na própria classe, mesmo quando o alvo da ação
é uma instância. Por essa razão, métodos especiais não são ocultados por atributos de instância de mesmo nome.
Nos exemplos a seguir, assuma que há uma classe chamada Class , que obj é uma instância de Class , e que attr é
um atributo de obj .

Para cada um destes métodos especiais, não importa se o acesso ao atributo é feito usando a notação de ponto ou uma
das funções embutidas listadas acima, em Seção 22.8.2. Por exemplo, tanto obj.attr quanto getattr(obj, 'attr',
42) disparam Class.__getattribute__(obj, 'attr') .

__delattr__(self, name)

É sempre invocado quando ocorre uma tentativa de excluir um atributo usando a instrução del ; por exemplo, del
obj.attr dispara Class.__delattr__(obj, 'attr') . Se attr for uma propriedade, seu método de exclusão
nunca será invocado se a classe implementar __delattr__ .

__dir__(self)

Chamado quando dir é invocado sobre um objeto, para fornecer uma lista de atributos; por exemplo, dir(obj)
dispara Class.__dir__(obj) . Também usado pelo recurso de auto-completar em todos os consoles modernos do
Python.

__getattr__(self, name)

Chamado apenas quando uma tentativa de obter o atributo nomeado falha, após obj , Class e suas superclasses
serem pesquisadas. As expressões obj.no_such_attr , getattr(obj, 'no_such_attr') e hasattr(obj,
'no_such_attr') podem disparar Class.__getattr__(obj, 'no_such_attr') , mas apenas se um atributo com
aquele nome não for encontrado em obj ou em Class e suas superclasses.

__getattribute__(self, name)

Sempre chamado quando há uma tentativa de obter o atributo nomeado diretamente a partir de código Python (o
interpretador pode ignorar isso em alguns casos, por exemplo para obter o método __repr__ ). A notação de ponto e
as funções embutidas getattr e hasattr disparam esse método. __getattr__ só é invocado após
__getattribute__ , e apenas quando __getattribute__ gera uma AttributeError . Para acessar atributos da
instância obj sem entrar em uma recursão infinita, implementações de __getattribute__ devem usar
super().__getattribute__(obj, name) .

__setattr__(self, name, value)


Sempre chamado quando há uma tentativa de atribuir um valor ao atributo nomeado. A notação de ponto e a
função embutida setattr disparam esse método; por exemplo, tanto obj.attr = 42 quanto setattr(obj,
'attr', 42) disparam Class.__setattr__(obj, 'attr', 42) .

Na prática, como são chamados incondicionalmene e afetam praticamente todos os acessos a


⚠️ AVISO atributos, os métodos especiais __getattribute__ e __setattr__ são mais difíceis de usar
corretamente que __getattr__ , que só lida com nome de atributos não-existentes. Usar
propriedades ou descritores tende a causar menos erros que definir esses métodos especiais.

Isso conclui nosso mergulho nas propriedades, nos métodos especiais e nas outras técnicas de programação de
atributos dnâmicos.

22.9. Resumo do capítulo


Começamos nossa discussão dos atributos dinâmicos mostrando exemplos práticos de classes simples, que tornavam
mais fácil processar um conjunto de dados JSON. O primeiro exemplo foi a classe FrozenJSON , que converte listas e
dicts aninhados em instâncias aninhadas de FrozenJSON , e em listas de instâncias da mesma classe. O código de
FrozenJSON demonstrou o uso do método especial __getattr__ para converter estruturas de dados em tempo real,
sempre que seus atributos eram lidos. A última versão de FrozenJSON mostrou o uso do método construtor __new__
para transformar uma classe em uma fábrica flexível de objetos, não restrita a instâncias de si mesma.
Convertemos então o conjunto de dados JSON em um dict que armazena instâncias da classe Record . A primeira
versão de Record tinha apenas algumas linhas e introduziu o dialeto do "punhado" ("bunch"): usar
self.__dict__.update(**kwargs) para criar atributos arbitrários a partir de argumentos nomeados passados para
__init__ . A segunda passagem acrescentou a classe Event , implementando a recuperação automática de registros
relacionados através de propriedades. Valores calculados de propriedades algumas vezes exigem caching, e falamos de
algumas formas de fazer isso. Após descobrir que @functools.cached_property não é sempre aplicável,
aprendemos sobre uma alternativa: a combinação de @property acima de @functools.cache , nessa ordem.

A discussão sobre propriedades continuou com a classe LineItem , onde uma propriedade foi criada para proteger um
atributo weight de receber valores negativos ou zero, que não fazem sentido em termos do negócio. Após um
aprofundamento da sintaxe e da semântica das propriedades, criamos uma fábrica de propriedades para aplicar a
mesma validação a weight e a price , sem precisar escrever múltiplos getters e setters. A fábrica de propriedades se
apoiou em conceitos sutis—tais como clausuras e a sobreposição de atributos de instância por propriedades—​para
fornecer um solução genérica elegante, usando para isso o mesmo número de linhas que usamos antes para escrever
manualmente a definição de uma única propriedade.

Por fim, demos uma rápida passada pelo tratamento da exclusão de atributos com propriedades, seguida por um
resumo dos principais atributos especiais, funções embutidas e métodos especiais que suportam a metaprogramação
de atributos no núcleo da linguagem Python.

22.10. Leitura Complementar


A documentação oficial para as funções embutidas de tratamento de atributos e introspecção é o Capítulo 2, "Funções
embutidas" (https://docs.python.org/pt-br/3/library/functions.html) da Biblioteca Padrão do Python. Os métodos especiais
relacionados e o atributo especial __slots__ estão documentados em A Referência da Linguagem Python, em "3.3.2.
Personalizando o acesso aos atributos" (https://docs.python.org/pt-br/3/reference/datamodel.html#customizing-attribute-access). A
semântica de como métodos especiais são invocados ignorando as instâncias está explicada em "3.3.11. Pesquisa de
método especial" (https://docs.python.org/pt-br/3/reference/datamodel.html#special-method-lookup). No capítulo 4 da Biblioteca
Padrão do Python, "Tipos embutidos", "Atributos especiais"
(https://docs.python.org/pt-br/3/library/stdtypes.html#special-attributes) trata dos atributos __class__ e __dict__ .

O Python Cookbook (https://fpy.li/pycook3) (EN), 3ª ed., de David Beazley e Brian K. Jones (O’Reilly), tem várias receitas
relacionadas aos tópicos deste capítulo, mas eu destacaria três mais marcantes: A "Recipe 8.8. Extending a Property in a
Subclass" (Receita 8.8. Estendendo uma Propriedade em uma Subclasse) trata da espinhosa questão de sobrepor métodos
dentro de uma propriedade herdada de uma superclasse; a "Recipe 8.15. Delegating Attribute Access" (Receita 8.15.
Delegando o Acesso a Atributos) implementa uma classe proxy, demonstrando a maioria dos métodos especiais da
seção Seção 22.8.3 deste livro; e a fantástica "Recipe 9.21. Avoiding Repetitive Property Methods" (Receita 9.21. Evitando
Métodos de Propriedade Repetitivos), que foi a base da função fábrica de propriedades apresentada no Exemplo 28.

O Python in a Nutshell, (https://fpy.li/pynut3) 3ª ed., de Alex Martelli, Anna Ravenscroft e Steve Holden (O’Reilly), é rigoroso
e objetivo. Eles dedicam apenas três páginas a propriedades, mas isso se dá porque o livro segue um estilo de
apresentação axiomático: as 15 ou 16 páginas precedentes fornecem uma descrição minuciosa da semântica das
classes do Python, a partir do zero, incluindo descritores, que são como as propriedades são efetivamente
implementadas debaixo dos panos. Assim, quando Martelli et al. chegam à propriedades, eles concentram várias ideias
profundas naquelas três páginas—incluindo o trecho que selecionei para abrir este capítulo.
Bertrand Meyer—citado na definição do Princípio do Acesso Uniforme no início do capítulo—foi um pioneiro da
metodologia Programação por Contrato (Design by Contract), projetou a linguagem Eiffel e escreveu o excelente Object-
Oriented Software Construction, 2ª ed. (Pearson). Os primeiros seis capítulos fornecem uma das melhores introduções
conceituais à análise e design orientados a objetos que tenho notícia. O capítulo 11 apresenta a Programação por
Contrato, e o capítulo 35 traz as avaliações de Meyer de algumas das mais influentes linguagens orientadas a objetos:
Simula, Smalltalk, CLOS (the Common Lisp Object System), Objective-C, C++, e Java, com comentários curtos sobre
algumas outras. Apenas na última página do livro o autor revela que a "notação" extremamente legível usada como
pseudo-código no livro é Eiffel.

Ponto de Vista
O Princípio de Acesso Uniforme de Meyer é esteticamente atraente. Como um programador usando uma API, eu
não deveria ter de me preocupar se product.price simplesmente lê um atributo de dados ou executa uma
computação. Como um consumidor e um cidadão, eu me importo: no comércio online atual, o valor de
product.price muitas vezes depende de quem está perguntando, então ele certamente não é um mero atributo
de dados. Na verdade, é uma prática comum apresentar um preço mais baixo se a consulta vem de fora da loja—
por exemplo, de um mecanismo de comparação de preços. Isso efetivamente pune os fregueses fiéis, que gostam
de navegar dentro de uma loja específica. Mas estou divagando.

A digressão anterior toca um ponto relevante para programação: apesar do Princípio de Acesso Uniforme fazer
todo sentido em um mundo ideal, na realidade os usuários de uma API podem precisar saber se ler
product.price é potencialmente dispendioso ou demorado demais. Isso é um problema com abstrações de
programaçõa em geral: elas tornam difícil raciocinar sobre o custo da avaliação de uma expressão durante a
execução. Por outro lado, abstrações permitem aos usuários fazerem mais com menos código. É uma negociação.
Como de hábito em questões de engenharia de software, o wiki original (https://fpy.li/22-26) (EN) de Ward
Cunningham contém argumentos perspicazes sobre os méritos do Princípio de Acesso Uniforme (https://fpy.li/22-27)
(EN).

Em linguagens de programação orientadas a objetos, a aplicação ou violação do Princípio de Acesso Uniforme


muitas vezes gira em torno da sintaxe de leitura de atributos de dados públicos versus a invocação de métodos
getter/setter.

Smalltalk e Ruby resolvem essa questão de uma forma simples e elegante: elas não suportam nenhuma forma de
atributos de dados públicos. Todo atributo de instância nessas linguagens é privado, então qualquer acesso a eles
deve passar por métodos. Mas sua sintaxe torna isso indolor: em Ruby, product.price invoca o getter de
price ; em Smalltalk, ele é simplesmente product price .

Na outra ponta do espectro, a linguagem Java permite ao programador escolher entre quatro modificadores de
nível de acesso—incluindo o default sem nome que o Tutorial do Java (https://fpy.li/22-28) (EN) chama de "package-
private".

A prática geral, entretanto, não concorda com a sintaxe definida pelos projetistas do Java. Todos no campo do
Java concordam que atributos devem ser private , e é necessário escrever isso explicitamente todas as vezes,
porque não é o default. Quando todos os atributos são privados, todo acesso a eles de fora da classe precisa passar
por métodos de acesso. Os IDEs de Java incluem atalhos para gerar métodos de acesso automaticamente.
Infelizmente, o IDE não ajuda quando você precisa ler aquele código seis meses depois. É problema seu navegar
por um oceano de métodos de acesso que não fazem nada, para encontrar aqueles que adicionam valor,
implementando alguma lógica do negócio.

Alex Martelli fala pela maioria da comunidade Python quando chama métodos de acesso de "idiomas patéticos", e
então apresenta esse exemplos que parecem muito diferentes, mas fazem a mesma coisa: [322]
PYTHON3
someInstance.widgetCounter += 1
# rather than...
someInstance.setWidgetCounter(someInstance.getWidgetCounter() + 1)

Algumas vezes, ao projetar uma API, me pergunto se todo método que não recebe qualquer argumento (além de
self ), devolve um valor (diferente de None ) e é uma função pura (isto é, não tem efeitos colaterais) não deveria
ser substituído por uma propriedade somente de leitura. Nesse capítulo, o método LineItem.subtotal (no
Exemplo 27) seria um bom candidato a se tornar uma propriedade somente para leitura. Claro, isso exclui
métodos projetados para modificar o objeto, tal como my_list.clear() . Seria uma péssima ideia transformar
isso em uma propriedade, de tal forma que o mero acesso a my_list.clear apagaria o conteúdo da lista!

Na biblioteca GPIO Pingo (https://fpy.li/22-29) (mencionada na seção Seção 3.5.2), da qual sou co-autor, grande parte
da API do usuário está baseada em propriedades. Por exemplo, para ler o valor atual de uma porta analógica, o
usuário escreve pin.value , e definir o modo de uma porta digital é escrito pin.mode = OUT . Por trás da cortina,
ler o valor de uma porta analógica ou definir o modo de uma porta digital pode implicar em bastante código,
dependendo do driver específico da placa. Decidimos usar propriedades no Pingo porque queríamos que a API
fosse confortável de usar até mesmo em ambientes interativos como um Jupyter Notebook, e achamos que
pin.mode = OUT é mais fácil para os olhos e para os dedos que pin.set_mode(OUT) .

Apesar de achar a solução do Smalltalk e do Ruby mais limpa, acho que a abordagem do Python faz mais sentido
que a do Java. Podemos começar simples, programando elementos de dados como atributos públicos, pois
sabemos que eles sempre podem ser encapsulados por propriedades (ou descritores, dos quais falaremos no
próximo capítulo).

+__new__+ é melhor que new

Outro exemplo do Princípio de Acesso Uniforme (ou uma variante dele) é o fato de chamadas de função e
instanciação de objetos usarem a mesma sintaxe no Python: my_obj = foo() , onde foo pode ser uma classe ou
qualquer outro invocável.

Outras linguagens, influenciadas pela sintaxe do C++, tem um operador new que faz a instanciação parecer
diferente de uma invocação. Na maior parte do tempo, o usuário de uma API não se importa se foo é uma
função ou uma classe. Por anos tive a impressão que property era uma função. Para o uso normal, não faz
diferença.

Há muitas boas razões para substituir construtores por fábricas. [323] Um motivo popular é limitar o número de
instâncias, devolvendo objetos construídps anterioremente (como no padrão de projeto Singleton). Um uso
relacionado é fazer caching de uma construção de objeto dispendiosa. Além disso, às vezes é conveniente
devolver objetos de tipos diferentes, dependendo dos argumentos passados.

Programar um construtor é simples; fornecer uma fábrica aumenta a flexibilidade às custas de mais código. Em
linguagens com um operador new , o projetista de uma API precisa decidir a priori se vai se ater a um construtor
simples ou investir em uma fábrica. Se a escolha inicial estiver errada, a correção pode ser cara—tudo porque
new é um operador.

Algumas vezes pode ser conveniente pegar o caminho inverso, e substituir uma função simples por uma classe.

Em Python, classes e funções são muitas vezes intercambiáveis. Não apenas pela ausência de um operador new ,
mas também porque existe o método especial __new__ , que pode transformar uma classe em uma fábrica que
produz tipos diferentes de objetos (como vimos na seção Seção 22.2.3) ou devolver instâncias pré-fabricadas em
vez de criar uma nova todas as vezes.
Essa dualidade função-classe seria mais fácil de aproveitar se a PEP 8 — Style Guide for Python Code (Guia de
Estilo para Código Python) (https://fpy.li/22-31) não recomendasse CamelCase para nomes de classe. Por outro lado,
dezenas de classes na biblioteca padrão tem nomes apenas em minúsculas (por exemplo, property , str ,
defaultdict , etc.). Daí que talvez o uso de nomes de classe apenas com minúsculas seja um recurso, e não um
bug. Mas independente de como olhemos, essa inconsistência no uso de maiúsculas e minúsculas nos nomes de
classes na biblioteca padrão do Python nos coloca um problema de usabilidade.

Apesar da invocação de uma função não ser diferente da invocação de uma classe, é bom saber qual é qual, por
causa de outra coisa que podemos fazer com uma classe: criar uma subclasse. Então eu, pessoalmente, uso
CamelCase em todas as classes que escrevo, e gostaria que todas as classea na biblioteca padrão do Python
seguissem a mesma convenção. Estou olhando para vocês, collections.OrderedDict e
collections.defaultdict .
23. 🚧 Descritores de atributos
24. 🚧 Metaprogramação com classes
1. Uma struct do C é um tipo de registro com campos nomeados.
2. Agradeço à leitora Tina Lapine por apontar essa informação.
3. Operadores invertidos são explicados no capítulo Capítulo 16.
4. Também usado para sobrescrever uma sub-sequência. Veja a seção Seção 2.7.4.
5. Agradeço ao revisor técnico Leonardo Rochael por esse exemplo.
6. A tradução em português da documentação do Python adotou o termo "correspondência de padrões" no lugar de pattern
matching. Escolhemos manter o termo em inglês, pois é usado nas comunidades brasileiras de linguagens que implementam
pattern matching há muitos anos, como por exemplo Scala, Elixir e Haskell. Naturalmente mantivemos os títulos originais nos
links externos (NT).
7. Na minha opinião, uma sucessão if/elif/elif/…​/else funciona muito bem como no lugar de switch/case . E ela não sofre
dos problemas de fallthrough (cascateamento) (https://fpy.li/2-8) (EN) e de dangling else (o else errante) (https://fpy.li/2-9) (EN), que
alguns projetistas de linguagens copiaram irracionalmente do C—décadas após sabermos que tais problemas são a causa de
inúmeros bugs.
8. A última é chamada eval no código original; a renomeei para evitar que fosse confundida com a função embutida eval do
Python.
9. Na seção Seção 2.10.2 vamos mostrar que views da memória construídas de forma especial podem ter mais de uma dimensão.
10. Não, eu não escrevi ao contrário: o nome da classe ellipsis realmente se escreve só com minúsculas, e a instância é um objeto
embutido chamado Ellipsis , da mesma forma que bool é em minúsculas mas suas instâncias são True e False .
11. str é uma exceção a essa descrição. Como criar strings com += em loops é tão comum em bases de código reais, o CPython foi
otimizado para esse caso de uso. Instâncias de str são alocadas na memória com espaço extra, então a concatenação não exige a
cópia da string inteira a cada operação.
12. Agradeço a Leonardo Rochael e Cesar Kawakami por compartilharem esse enigma na Conferência PythonBrasil de 2013.
13. Alguns leitores sugeriram que a operação no exemplo pode ser realizada com t[2].extend([50,60]) , sem erros. Eu sei disso,
mas a intenção aqui é mostrar o comportamento estranho do operador += nesse caso.
14. Receptor (receiver) é o alvo de uma chamada a um método, o objeto vinculado a self no corpo do método.
15. O principal algoritmo de ordenação do Python se chama Timsort, em homenagem a seu criador, Tim Peters. Para curiosidades
sobre o Timsort, veja o Ponto de Vista.
16. As palavras nesse exemplo estão ordenadas em ordem alfabética porque são 100% constituídas de caracteres ASCII em letras
minúsculas. Veja o aviso após o exemplo.
17. Sigla em inglês para "First in, first out" (Primeiro a entrar, primeiro a sair—o comportamento padrão de filas.
18. Operadores invertidos são explicados no capítulo Capítulo 16.
19. Operadores invertidos são explicados no capítulo Capítulo 16.
20. a_list.pop(p) permite remover da posição p , mas deque não suporta essa opção.
21. Conceito da lei de copyright norte-americana que permite, em determinadas circunstâncias, o uso sem custo de partes da
propriedade intelectual de outros. Em geral traduzido como "uso razoável" ou "uso aceitável". Essa doutrina não faz parte da lei
brasileira.(NT)
22. Uma subclasse virtual é qualquer classe registrada com uma chamada ao método .register() de uma ABC, como explicado na
seção Seção 13.5.6. Um tipo implementado através da API Python/C também é aceitável, se um bit de marcação específico estiver
acionado. Veja Py_TPFLAGS_MAPPING (https://docs.python.org/pt-br/3.10/c-api/typeobj.html#Py_TPFLAGS_MAPPING) (EN).
23. O verbete para "hashable" no Glossário do Python (https://docs.python.org/pt-br/3/glossary.html#term-hashable) usa o termo "valor de
hash" em vez de código de hash. Prefiro código de hash porque "valor" é um conceito frequentemente usado no contexto de
mapeamentos, onde itens são compostos de chavas e valores. Então pode ser confuso se referir ao código de hash como um valor.
Nesse livro usarei apenas código de hash.
24. Veja a PEP 456—Secure and interchangeable hash algorithm (Algoritmo de hash seguro e intercambiável_) (https://fpy.li/pep456)
(EN) para saber mais sobre as implicações de segurança e as soluções adotadas.
25. default_factory não é um método, mas um atributo chamável definido pelo usuário quando um defaultdict é instanciado.
26. OrderedDict.popitem(last=False) remove o primeiro item inserido (FIFO). O argumento nomeado last não é suportado
por dict ou defaultdict , pelo menos até o Python 3.10b3.
27. Operadores invertidos são tratados no Capítulo 16.
28. O script original aparece no slide 41 da apresentação de Martelli, "Re-learning Python" (Reaprendendo Python) (https://fpy.li/3-5)
(EN). O script é, na verdade, uma demonstração de dict.setdefault , como visto no nosso Exemplo 5.
29. Isso é um exemplo do uso de um método como uma função de primeria classe, o assunto do Capítulo 7.
30. NT: "Missing" significa ausente, perdido ou desaparecido
31. Uma biblioteca dessas é a Pingo.io (https://fpy.li/3-6), que não está mais em desenvolvimento ativo.
32. NT: Least Recently Used, Menos Recentemente Usado, esquema de cache que descarta o item armazenado que esteja há mais
tempo sem requisições
33. NT: "to shelve" é "colocar na prateleira", "pickle" também significa "conserva" e "pickles" é literalmente picles. O trocadilho dos
desenvolvedores do Python é sobre colocar pickles em shelves .
34. O problema exato de se criar subclasses de dict e de outros tipos embutidos é tratado na seção Seção 14.3.
35. É assim que as tuplas são armazenadas.
36. A menos que classe tenha umm atributo __slots__ , como explicado na seção Seção 11.11.
37. Isso pode ser interessante, mas não é super importante. Essa diferença de velocidade vai ocorrer apenas quando um conjunto
literal for avaliado, e isso acontece no máximo uma vez por processo Python—quando um módulo é compilado pela primeira vez.
Se você estiver curiosa, importe a função dis do módulo dis , e a use para desmontar os bytecodes para um set literal—por
exemplo, dis('{1}') —e uma chamada a set — dis('set([1])')
38. NT: Na teoria dos conjuntos, A é um subconjunto próprio de B se A é subconjunto de B e A é diferente de B.
39. NT: Explicando o trocadilho intraduzível: "colon", em inglês, designa "a parte central do intestino grosso"; "semicolon", por outro
lado, é "ponto e vírgula". A frase diz, literalmente, "Açúcar sintático causa câncer no ponto e vírgula", que faz sentido em inglês
pela proximidade ortográfica das duas palavras.
40. Slide 12 da palestra "Character Encoding and Unicode in Python" (Codificação de Caracteres e Unicode no Python) na PyCon 2014
(slides (https://fpy.li/4-1) (EN), vídeo (https://fpy.li/4-2) (EN)).
41. O Python 2.6 e o 2.7 também tinham um bytes , mas ele era só um apelido (alias) para o tipo str .
42. Trívia: O caractere ASCII "aspas simples", que por default o Python usa como delimitador de strings, na verdade se chama
APOSTROPHE no padrão Unicode. As aspas simples reais são assimétricas: a da esquerda é U+2018 e a da direita U+2019.
43. Ele não funcionava do Python 3.0 ao 3.4, causando muitas dores de cabeça nos desenvolvedores que lidam com dados binários.
O retorno está documentado na PEP 461—​Adding % formatting to bytes and bytearray (Acrescentando formatação com % a bytes e
bytearray) (https://fpy.li/pep461). (EN)
44. A primeira vez que vi o termo "Unicode sandwich" (sanduíche de Unicode) foi na excelente apresentação de Ned Batchelder,
"Pragmatic Unicode" (Unicode pragmático) (EN) (https://fpy.li/4-10) na US PyCon 2012.
45. Fonte: "Windows Command-Line: Unicode and UTF-8 Output Text Buffer" (A Linha de Comando do Windows: O Buffer de Saída de
Texto para Unicode e UTF-8) (https://fpy.li/4-11).
46. Curiosamente, o símbolo de micro é considerado um "caractere de compatibilidade", mas o símbolo de ohm não. O resultado
disso é que a NFC não toca no símbolo de micro, mas muda o símbolo de ohm para ômega maiúsculo, ao passo que a NFKC e a
NFKD mudam tanto o ohm quanto o micro para caracteres gregos.
47. Sinais diacríticos afetam a ordenação apenas nos raros casos em que eles são a única diferennça entre duas palavras—nesse
caso, a palavra com o sinal diacrítico é colocada após a palavra sem o sinal na ordenação.
48. De novo, eu não consegui encontrar uma solução, mas encontrei outras pessoas relatando o mesmo problema. Alex Martelli, um
dos revisores técnicos, não teve problemas para usar setlocale e locale.strxfrm em seu Macintosh com o macOS 10.9. Em
resumo: cada caso é um caso.
49. Aquilo é uma imagem—não uma listagem de código—porque, no momento em que esse capítulo foi escrito, os emojis não tem
um bom suporte no sistema de publicação digital da O’Reilly.
50. Embora não tenha se saído melhor que o re para identificar dígitos nessa amostra em particular.
51. As metaclasses são um dos assuntos tratados no #class_metaprog.
52. Decoradores de classe são discutidos no [class_metaprog], na seção "Metaprogramação de classes", junto com as metaclasses.
Ambos são formas de personalizar o comportamento de uma classe além do que seria possível com herança.
53. Se você conhece Ruby, sabe que injetar métodos é uma técnica bastante conhecida, apesar de controversa, entre rubystas. Em
Python isso não é tão comum, pois não funciona com nenhum dos tipos embutidos— str , list , etc. Considero essa limitação do
Python uma benção.
54. No contexto das dicas de tipo, None não é o singleton NoneType , mas um apelido para o próprio NoneType . Se pararmos para
pensar, isso é estranho, mas agrada nossa intuição e torna as anotações de valores devolvidos por uma função mais fáceis de ler,
no caso comum de funções que devolvem None .
55. O conceito de undefined, um dos erros mais idiotas no design do Javascript, não existe no Python. Obrigado, Guido!
56. (NT) Um getter é um método que devolve o valor um atributo do objeto. Para propriedades mutáveis, o getter vem geralmente
acompanhado por um setter, que modifica a mesma propriedade. Os nomes derivam dos verbos em inglês get (obter, receber) e
set (definir, estabelecer).
57. Definir um atributo após o __init__ prejudica a otimização de uso de memória com o compartilhamento das chaves do
__dict__ , mencionada na seção Seção 3.9.
58. O @dataclass emula a imutabilidade criando um __setattr__ e um __delattr__ que geram um
dataclass.FrozenInstanceError —uma subclasse de AttributeError —quando o usuário tenta definir ou apagar o valor de
um campo.
59. dataclass._MISSING_TYPE é um valor sentinela, indicando que a opção não foi fornecida. Ele existe para que se possa definir
None como um valor default efetivo, um caso de uso comum.
60. A opção hash=None significa que o campo será usado em __hash__ apenas se compare=True .
61. Fonte: O artigo Dublin Core (https://pt.wikipedia.org/wiki/Dublin_Core) na Wikipedia.
62. (NT) Code smell em geral não é traduzido na bibliografia em português—uma tradução quase literal seria "fedor no código". Uma
tradução mais gentil pode ser "cheiro no código", adotado aqui (mais gentil e menos enviesada: um "cheiro no código" nem
sempre é indicação de um problema)
63. Eu tenho a felicidade de ter Martin Fowler como colega de trabalho na Thoughtworks, estão precisei de apenas 20 minutos para
obter sua permissão.
64. Trato desse conteúdo aqui por ser o primeiro capítulo sobre classes definidas pelo usuário, e acho que pattern matching com
classes é um assunto muito importante para esperar até a [function_objects_part] do livro. Minha filosofia: é mais importante
saber como usar classes que como definí-las.
65. Lynn Andrea Stein é uma aclamada educadora de ciências da computação. Ela atualmente leciona na Olin College of
Engineering (EN) (https://fpy.li/6-1).
66. Ao contrário de sequências simples de tipo único, como str , byte e array.array , que não contêm referências e sim seu
conteúdo — caracteres, bytes e números — armazenado em um espaço contíguo de memória.
67. Ver Principle of least astonishment (https://fpy.li/6-5) (EN).
68. Isso está claramente documentado. Digite help(tuple) no console do Python e leia: "Se o argumento é uma tupla, o valor de
retorno é o mesmo objeto." Pensei que sabia tudo sobre tuplas antes de escrever esse livro.
69. Essa mentirinha inofensiva, do método copy não copiar nada, é justificável pela compatibilidade da interface: torna
frozenset mais compatível com set . De qualquer forma, não faz nenhuma diferença para o usuário final se dois objetos
imutáveis idênticos são o mesmo ou são cópias.
70. Um péssimo uso dessas informações seria perguntar sobre elas quando entrevistando candidatos a emprego ou criando
perguntas para exames de "certificação". Há inúmeros fatos mais importantes e úteis para testar conhecimentos de Python.
71. Na verdade, o tipo de um objeto pode ser modificado, bastando para isso atribuir uma classe diferente ao atributo __class__
do objeto. Mas isso é uma perversão, e eu me arrependo de ter escrito essa nota de rodapé.
72. "Origins of Python’s 'Functional' Features" (As origens dos recursos 'funcionais' do Python—EN) (https://fpy.li/7-1), do blog The
History of Python (A História do Python) do próprio Guido.
73. "Benevolent Dictator For Life." - Ditador Benevolente Vitalício. Veja Guido van van Rossum em "Origin of BDFL" (A Origem do
BDFL) (https://fpy.li/bdfl) (EN).
74. Invocar uma classe normalmente cria uma instância daquela mesma classe, mas outros comportamentos são possíveis,
sobrepondo o __new__ . Veremos um exemplo disso na seção Seção 22.2.3.
75. Por que criar uma BingoCage quando já temos random.choice ? A função choice pode devolver o mesmo item múltiplas
vezes, pois o item escolhido não é removido da coleção usada. Invocações de BingoCage nunca devolvem um resultado
duplicado—desde que a instância tenha sido preeenchida com valores únicos.
76. O código fonte (https://fpy.li/7-9) de functools.py revela que functools.partial é implementada em C e é usada por default. Se ela
não estiver disponível, uma implementação em Python puro de partial está disponível desde o Python 3.4.
77. Há também o problema da perda de indentação quando colamos trechos de código em fóruns na Web, mas isso é outro assunto.
78. (NT) Sim, "diferir" também significa "adiar" em português!
79. Um compilador JIT ("just-in-time", compiladores que transformam o bytecode gerado pelo interpretador em código da máquina-
alvo no momento da execução) como o do PyPy tem informações muito melhores que as dicas de tipo: ele monitora o programa
Python durante a execução, detecta os tipos concretos em uso, e gera código de máquina otimizado para aqueles tipos concretos.
80. Em Python não há sintaxe para controlar o conjunto de possíveis valores de um tipo, exceto para tipos Enum . Por exemplo, não é
possível, usando dicas de tipo, definir Quantity como um número inteiro entre 1 e 10000, ou AirportCode como uma sequência
de 3 letras. O NumPy oferece uint8 , int16 , e outros tipos numéricos referentes à arquitetura do hardware, mas na biblioteca
padrão do Python nós encontramos apenas tipos com conjuntos muitos pequenos de valores ( NoneType , bool ) ou conjuntos
muito grandes ( float , int , str , todas as tuplas possíveis, etc.).
81. Duck typing é uma forma implícita de tipagem estrutural, que o Python passou a suportar após a versão 3.8, com a introdução de
typing.Protocol . Vamos falar disso mais adiante nesse capítulo - em Seção 8.5.10 — e com mais detalhes em Capítulo 13.
82. Muitas vezes a herança é sobreutilizada e difícil de justificar em exemplos que, apesar de realistas, são muito simples. Então por
favor aceite esse exemplo com animais como uma rápida ilustração de sub-tipagem.
83. Professora do MIT, designer de linguagens de programação e homenageada com o Turing Award em 2008. Wikipedia: Barbara
Liskov (https://pt.wikipedia.org/wiki/Barbara_Liskov).
84. Para ser mais preciso, ord só aceita str ou bytes com len(s) == 1 . Mas no momento o sistema de tipagem não consegue
expressar essa restrição.
85. Em ABC - a linguagem que mais influenciou o design inicial do Python - cada lista estava restrita a aceitar valores de um único
tipo: o tipo do primeiro item que você colocasse ali.
86. Uma de minhas contribuições para a documentação do módulo typing foi acrescentar dúzias de avisos de descontinuação,
enquanto eu reorganizava as entradas abaixo de "Conteúdo do Módulo"
(https://docs.python.org/pt-br/3/library/typing.html#module-contents) em subseções, sob a supervisão de Guido van Rossum.
87. Eu uso := quando faz sentido em alguns exemplos, mas não trato desse operador no livro. Veja PEP 572—Assignment
Expressions (https://fpy.li/pep572) para entender os detalhes dos operadores de atribuição.
88. Na verdade, dict é uma subclasse virtual de abc.MutableMapping . O conceito de subclasse virtual será explicado em Capítulo
13. Por hora, basta saber que issubclass(dict, abc.MutableMapping) é True , apesar de dict ser implementado em C e não
herdar nada de abc.MutableMapping , apenas de object .
89. A implementação aqui é mais simples que aquela do módulo statistics (https://fpy.li/8-29) na biblioteca padrão do Python
90. Eu contribui com essa solução para typeshed , e em 26 de maio de 2020 mode aparecia anotado assim em statistics.pyi
(https://fpy.li/8-32).
91. Não é maravilhoso poder abrir um console iterativo e contar com o duck typing para explorar recursos da linguagem, como
acabei de fazer? Eu sinto muita falta deste tipo de exploração quando uso linguagem que não tem esse recurso.
92. Sem essa dica de tipo, o Mypy inferiria o tipo de series como Generator[Tuple[builtins.int, builtins.str*], None,
None] , que é prolixo mas consistente-com Iterator[tuple[int, str]] , como veremos na Seção 17.12.
93. Eu não sei quem inventou a expressão duck tying estático, mas ela se tornou mais popular com a linguagem Go, que tem uma
semântica de interfaces que é mais parecida com os protocolos de Python que com as interfaces nominais de Java.
94. REPL significa Read-Eval-Print-Loop (Ler-Calcular-Imprimir-Recomeçar), o comportamento básico de interpretadores iterativos.
95. "Benevolent Dictator For Life." - Ditador Benevolente Vitalício. Veja Guido van van Rossum em "Origin of BDFL" (https://fpy.li/bdfl).
96. Do vídeo no Youtube, "Type Hints by Guido van Rossum (March 2015)" (https://fpy.li/8-39) (EN). A citação começa em 13'40"
(https://fpy.li/8-40). Editei levemente a transcrição para manter a clareza.
97. Fonte: "A Conversation with Alan Kay" (https://fpy.li/8-54).
98. GoF se refere ao livro Design Patterns (traduzido no Brasil como "Padrões de Projeto"), de 1985. Seus (quatro) autores ficaram
conhecidos como a "Gang of Four" (Gangue dos Quatro).
99. NT: Adotamos a tradução "clausura" para "closure". Alguns autores usam "fechamento". O termo em inglês é pronunciado
"clôujure", e o nome da linguagem Clojure brinca com esse fato. Gosto da palavra clausura por uma analogia cultural. Em
conventos, a clausura é um espaço fechado onde algumas freiras vivem isoladas. Suas memórias são seu único vínculo com o
exterior, mas elas refletem o mundo do passado. Em programação, uma clausura é um espaço isolado onde a função tem acesso a
variáveis que existiam quando a própria função foi criada, variáveis de um escopo que não existe mais, preservadas apenas na
memória clausura.
100. Se você substituir "função"por "classe" na sentença anterior, o resultado é uma descrição resumida do papel de um decorador
de classe. Decoradores de classe são tratadas no [class_metaprog].
101. Agradeço ao revisor técnico Leonardo Rochael por sugerir esse resumo.
102. O Python não tem um escopo global de programa, apenas escopos globais de módulos.
103. Esclarecendo, isso não é um erro de ortografia: memoization (https://fpy.li/9-2) é um termo da ciência da computação vagamente
relacionado a "memorização", mas não idêntico.
104. Infelizmente, o Mypy 0.770 reclama quando vê múltiplas funções com o mesmo nome.
105. Apesar do alerta em Seção 8.5.7.1, as ABCs de numbers não foram descontinuadas, e você as encontra em código de Python 3.
106. Talvez algum dia seja possível expressar isso com um único @htmlize.register sem parâmetros, e uma dica de tipo usando
Union . Mas quando tentei, o Python gerou um TypeError com uma mensagem dizendo que Union não é uma classe. Então,
apesar da sintaxe da PEP 484 ser suportada, a semântica ainda não chegou lá.
107. NumPy, por exemplo, implementa vários tipos de números inteiros e de ponto flutuante (https://fpy.li/9-3) (EN) em formatos
voltados para a arquitetura da máquina.
108. O revisor técnico Miroslav Šedivý observou: "Isso também quer dizer que analisadores de código-fonte (linters) vão reclamar
de variáveis não utilizadas, pois eles tendem a ignorar o uso de locals() ." Sim, esse é mais um exemplo de como ferramentas
estáticas de verificação desencorajam o uso dos recursos dinâmicos do Python que primeiro me atraíram (e a incontáveis outros
programadores) na linguagem. Para fazer o linter feliz, eu poderia escrever cada variável local duas vezes na chamada:
fmt.format(elapsed=​ elapsed, name=name, args=args, result=result) . Prefiro não fazer isso. Se você usa ferramentas
estáticas de verificação, é importante saber quando ignorá-las.
109. Como queria manter o código o mais simples possível, não segui o excelente conselho de Slatkin em todos os exemplos.
110. De um slide na palestra "Root Cause Analysis of Some Faults in Design Patterns," (Análise das Causas Básicas de Alguns Defeitos
em Padrões de Projetos), apresentada por Ralph Johnson no IME/CCSL da Universidade de São Paulo, em 15 de novembro de 2014.
111. Visitor, Citado da página 4 da edição em inglês de Padrões de Projeto.
112. Precisei reimplementar Order com @dataclass devido a um bug no Mypy. Você pode ignorar esse detalhe, pois essa classe
funciona também com NamedTuple , exatamente como no Exemplo 1. Quando Order é uma NamedTuple , o Mypy 0.910 encerra
com erro ao verificar a dica de tipo para promotion . Tentei acrescentar # type ignore àquela linha específica, mas o erro
persistia. Entretanto, se Order for criada com @dataclass , o Mypy trata corretamente a mesma dica de tipo. O Issue #9397
(https://fpy.li/10-3) não havia sido resolvido em 19 de julho de 2021, quando essa nota foi escrita. Espero que o problema tenha sido
solucionado quando você estiver lendo isso. (NT: Aparentemente foi resolvido. O Issue #9397 gerou o Issue #12629
(https://github.com/python/mypy/issues/12629), fechado com indicação de solucionado em agosto de 2022, o último comentário
indicando que a opção de linha de comando --enable-recursive-aliases do Mypy evita os erros relatados).
113. veja a página 323 da edição em inglês de Padrões de Projetos.
114. Ibid., p. 196.
115. Tanto o flake8 quanto o VS Code reclamam que esses nomes são importados mas não são usados. Por definição, ferramentas de
análise estática não conseguem entender a natureza dinâmica do Python. Se seguirmos todos os conselhos dessas ferramentas,
logo estaremos escrevendo programas auteros e prolixos similares aos do Java, mas com a sintaxe do Python.
116. "Root Cause Analysis of Some Faults in Design Patterns" (Análise das Causas Básicas de Alguns Defeitos em Padrões de Projetos),
palestra apresentada por Johnson no IME/CCSL da Universidade de São Paulo, em 15 de novembro de 2014.
117. (NT) Literalmente "Tartarugas até embaixo" ou algo como "Tartarugas até onde a vista alcança" ou "Uma torre infinita de
tartarugas". Curiosamente, um livro com esse nome foi publicado no Brasil com o título "Mil vezes adeus", na tradição brasileira
(especialmente para filmes) de traduzir nomes de obras de forma preguiçosa ou aleatória.
118. Do post no blog de Faassen intitulado What is Pythonic? (O que é Pythônico?) (https://fpy.li/11-1)
119. Usei eval para clonar o objeto aqui apenas para mostrar uma característica de repr ; para clonar uma instância, a função
copy.copy é mais segura e rápida.
120. Essa linha também poderia ser escrita assim: yield self.x; yield.self.y . Terei muito mais a dizer sobre o método especial
__iter__ , sobre expressões geradoras e sobre a palavra reservada yield no Capítulo 17.
121. Tivemos uma pequena introdução a memoryview e explicamos seu método .cast na seção Seção 2.10.2.
122. Leonardo Rochael, um dos revisores técnicos deste livro, discorda de minha opinião desabonadora sobre o staticmethod , e
recomenda como contra-argumento o post de blog "The Definitive Guide on How to Use Static, Class or Abstract Methods in
Python" (O Guia Definitivo sobre Como Usar Métodos Estáticos, de Classe ou Abstratos em Python) (https://fpy.li/11-2) (EN), de Julien
Danjou. O post de Danjou é muito bom; recomendo sua leitura. Mas ele não foi suficiente para mudar meu ponto de vista sobre
staticmethod . Você terá que decidir por si mesmo.
123. Os prós e contras dos atributos privados são assunto da seção Seção 11.10, mais adiante.
124. Do "Paste Style Guide" (Guia de Estilo do Paste) (https://fpy.li/11-8).
125. Em módulos, um único _ no início de um nome de nível superior tem sim um efeito: se você escrever from mymod import * ,
os nomes com um prefixo _ não são importados de mymod . Entretanto, ainda é possível escrever from mymod import
_privatefunc . Isso é explicado no Tutorial do Python, seção 6.1., "Mais sobre módulos"
(https://docs.python.org/pt-br/3/tutorial/modules.html#more-on-modules).
126. Um exemplo é a documentação do módulo gettext (https://docs.python.org/pt-br/3/library/gettext.html#gettext.NullTranslations).
127. Se você acha este estado de coisas deprimente e desejaria que o Python fosse mais parecido com o Java nesse aspecto, nem leia
minha discussão sobre a força relativa do modificador private do Java no Ponto de Vista.
128. Veja "Simplest Thing that Could Possibly Work: A Conversation with Ward Cunningham, Part V" (A Coisa Mais Simples que
Poderia Funcionar: Uma Conversa com Ward Cunningham, Parte V). (https://fpy.li/11-14)
129. A função iter() é tratada no Capítulo 17, juntamente com o método __iter__ .
130. A pesquisa de atributos é mais complicada que isso; veremos todos detalhes macabros desse processo no [metaprog_part]. Por
ora, essa explicação simplificada nos serve.
131. Apesar de __match_args__ existir para suportar pattern matching desde Python 3.10, definir este atributo em versões
anteriores da linguagem é inofensivo. Na primeira edição chamei este atributo de shortcut_names . Com o novo nome, ele
cumpre dois papéis: suportar padrões posicionais em instruções case e manter os nomes dos atributos dinâmicos suportados
por uma lógica especial em __getattr__ e __setattr__ .
132. sum , any , e all cobrem a maioria dos casos de uso comuns de reduce . Veja a discussão na seção Seção 7.3.1.
133. Vamos considerar seriamente o caso de Vector([1, 2]) == (1, 2) na seção Seção 16.2.
134. O website Wolfram Mathworld tem um artigo sobre hypersphere (hiperesfera) (https://fpy.li/12-4) (EN); na Wikipedia,
"hypersphere" redireciona para a página “n-sphere” (https://fpy.li/nsphere) (EN) (NT: A Wikipedia tem uma página em português, "N-
esfera" (https://pt.wikipedia.org/wiki/N-esfera). Entretanto, enquanto a versão em inglês traz uma extensa explicação matemática,
dividida em 12 seções e inúmeras subseções, a versão em português se resume a um parágrafo curto. Preferimos então manter o
link para a versão mais completa.).
135. Adaptei o código apresentado aqui: em 2003, reduce era uma função embutida, mas no Python 3 precisamos importá-la;
também substitui os nomes x e y por my_list e sub (para sub-lista).
136. NT: Aqui Martelli está se referindo à linguagem APL (https://pt.wikipedia.org/wiki/APL_(linguagem_de_programa%C3%A7%C3%A3o)
137. NT:E aqui à linguagem FP (https://pt.wikipedia.org/wiki/FP_(linguagem_de_programa%C3%A7%C3%A3o)
138. O artigo "Monkey patch" (https://fpy.li/13-4) (EN) na Wikipedia tem um exemplo engraçado em Python.
139. Por isso a necessidade de testes automatizados.
140. Consultada em 3 de março de 2023.
141. 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
142. 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.
143. Talvez o cliente precise auditar o randomizador ou a agência queira fornecer um randomizador "viciado". Nunca se sabe…​
144. 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."
145. A árvore completa está na seção "5.4. Exception hierarchy" da documentação da _Biblioteca Padrão do Python.
146. O verbete @abc.abstractmethod (https://docs.python.org/pt-br/dev/library/abc.html#abc.abstractmethod) na documentação do módulo
abc (https://docs.python.org/pt-br/dev/library/abc.html).
147. Seção 6.5.2 em Capítulo 6 foi dedicado à questão de apelidamento que acabamos de evitar aqui.
148. 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" (https://docs.python.org/pt-br/3/library/stdtypes.html#truth) no capítulo "Tipos Embutidos" da documentação do
Python.
149. Há toda uma explicação sobre o atributo de classe __mro__ na Seção 14.4. Por agora, essas informações básicas são o
suficiente.
150. O conceito de consistência de tipo é explicado na Seção 8.5.1.1.
151. 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 ?)" (https://fpy.li/shed4051)
aproveitava um protocolo _SupportsLessThan , que usei para melhorar as anotações de max , min , sorted , e list.sort .
152. O atributo __slots__ é irrelevante para nossa discussão aqui - é uma otimização sobre a qual falamos na Seção 11.11.
153. Agradeço a Ivan Levkivskyi, co-autor da PEP 544 (https://fpy.li/pep544) (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.
154. Leia a decisão (https://fpy.li/13-32) (EN) do Python Steering Council no python-dev.
155. "papel" aqui é usado no sentido de incorporação de um personagem (NT)
156. 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.
157. Para detalhes e justificativa, veja por favor a seção sobre @runtime_checkable (https://fpy.li/13-37) (EN) na PEP 544—Protocols:
Structural subtyping (static duck typing).
158. Novamente, leia por favor "Merging and extending protocols" (https://fpy.li/13-38) (EN) na PEP 544 para os detalhes e justificativas.
159. ver Issue #41974—Remove complex.__float__ , complex.__floordiv__ , etc (https://fpy.li/13-41).
160. Eu não testei todas as outras variantes de float e integer que o NumPy oferece.
161. Os tipos numéricos do NumPy são todos registrados com as ABCs apropriadas de numbers , que o Mypy ignora.
162. 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__ .
163. Agradeço ao revisor técnico Jürgen Gmach por ter recomentado o post "Interfaces and Protocols".
164. Alan Kay, "The Early History of Smalltalk" (Os Primórdios do Smalltalk), na SIGPLAN Not. 28, 3 (março de 1993), 69–95. Também
disponível online (https://fpy.li/14-1) (EN). Agradeço a meu amigo Christiano Anderson, por compartilhar essa referência quando eu
estava escrevendo este capítulo.
165. Modifiquei apenas a docstring do exemplo, porque a original está errada. Ela diz: "Armazena itens na ordem das chaves
adicionadas por último" ("Store items in the order the keys were last added"), mas não é isso o que faz a classe claramente batizada
LastUpdatedOrderedDict .
166. Também é possível passar apenas o primeiro argumento, mas isso não é útil e pode logo ser descontinuado, com as bênçãos de
Guido van Rossum, o próprio criador de super() . Veja a discussão em "Is it time to deprecate unbound super methods?" (É hora
de descontinuar métodos "super" não vinculados?) (https://fpy.li/14-4).
167. É interessante observar que o C++ diferencia métodos virtuais e não-virtuais. Métodos virtuais tem vinculação tardia, enquanto
os métodos não-virtuais são vinculados na compilação. Apesar de todos os métodos que podemos escrever em Python serem de
vinculação tardia, como um método virtual, objetos embutidos escritos em C parecem ter métodos não-virtuais por default, pelo
menos no CPython.
168. Se você tiver curiosidade, o experimento está no arquivo 14-inheritance/strkeydict_dictsub.py (https://fpy.li/14-7) do repositório
fluentpython/example-code-2e (https://fpy.li/code).
169. Aliás, nesse mesmo tópico, o PyPy se comporta de forma mais "correta" que o CPython, às custas de introduzir uma pequena
incompatibilidade. Veja os detalhes em "Differences between PyPy and CPython" (Diferenças entre o PyPy e o CPython)
(https://fpy.li/14-5) (EN).
170. Classes também têm um método .mro() , mas este é um recurso avançado de programaçõa de metaclasses, mencionado no
[anatomy_of_classes]. Durante o uso normal de uma classe, apenas o conteúdo do atributo __mro__ importa.
171. Erich Gamma, Richard Helm, Ralph Johnson, e John Vlissides, Padrões de Projetos: Soluções Reutilizáveis de Software Orientados
a Objetos (Bookman).
172. Como já mencionado, o Java 8 permite que interfaces também forneçam implementações de métodos. Esse novo recurso é
chamado "Default Methods" (Métodos Default) (https://fpy.li/14-12) (EN) no Tutorial oficial do Java.
173. Os programadores Django sabem que o método de classe as_view é a parte mais visível da interface View , mas isso não é
relevante para nós aqui.
174. Se você gosta de padrões de projetos, note que o mecanismo de despacho do Django é uma variação dinâmica do padrão
Template Method (Método Template (https://pt.wikipedia.org/wiki/Template_Method). Ele é dinâmico porque a classe View não obriga
subclasses a implementarem todos os métodos de tratamento, mas dispatch verifica, durante a execução, se um método de
tratamento concreto está disponível para cada requisição específica.
175. NT: Literalmente "nos trilhos", mas obviamente uma referência à popular framework web baseada na linguagem Ruby, a Ruby
on Rails
176. Esse princípio aparece na página 20 da introdução, na edição em inglês do livro.
177. Grady Booch et al., "Object-Oriented Analysis and Design with Applications" (Análise e Projeto Orientados a Objetos, com
Aplicações), 3ª ed. (Addison-Wesley), p. 109.
178. NT: a doctring diz "Renderiza alguma lista de objetos, definida por self.model ou self.queryset . self.queryset na
verdade pode ser qualquer iterável de itens, não apenas um queryset."
179. A PEP 591 também introduz uma anotação Final (https://docs.python.org/pt-br/3/library/typing.html#typing.Final) para variáveis e
atributos que não devem ser reatribuidos ou sobrepostos.
180. NT: O nome da seção é uma referência ao filme "The Good, the Bad and the Ugly", um clássico do spaghetti western de 1966,
lançado no Brasil com o título "Três Homens em Conflito".
181. Alan Kay, "The Early History of Smalltalk" (Os Promórdios do Smalltalk), na SIGPLAN Not. 28, 3 (março de 1993), 69–95. Também
disponível online (https://fpy.li/14-1) (EN).
182. Meu amigo e revisor técnico Leonardo Rochael explica isso melhor do que eu poderia: "A existência continuada junto com o
persistente adiamento da chegada do Perl 6 estava drenando a força de vontade da evolução do próprio Perl. Agora o Perl
continua a ser desenvolvido como uma linguagem separada (está na versão 5.34), sem a ameaça de ser descontinuada pela
linguagem antes conhecida como Perl 6."
183. De um vídeo no YouTube da A Language Creators' Conversation: Guido van Rossum, James Gosling, Larry Wall & Anders
Hejlsberg ("Uma Conversa entre Criadores de Linguagens: Guido van Rossum, James Gosling, Larry Wall & Anders Hejlsberg),
transmitido em 2 de abril de 2019. A citação (editada por brevidade) começa em 1:32:05 (https://fpy.li/15-1). A transcrição completa
está disponível em https://github.com/fluentpython/language-creators (EN).
184. (NT) Texto original em inglês: "Return the sum of a 'start' value (default: 0) plus an iterable of numbers When the iterable is
empty, return the start value. This function is intended specifically for use with numeric values and may reject non-numeric
types"
185. Agradeço a Jelle Zijlstra—um mantenedor do typeshed—que me ensinou várias coisas, incluindo como reduzir minhas nove
sobreposições originais para seis.
186. Em maio de 2020, o pytype ainda permite isso. Mas seu FAQ (https://fpy.li/15-6) (EN) diz que tal operação será proibida no futuro.
Veja a pergunta "Why didn’t pytype catch that I changed the type of an annotated variable?" (Por que o pytype não avisou quando
eu mudei o tipo de uma variável anotada?) no FAQ (https://fpy.li/15-6) (EN) do pytype.
187. Prefiro usar o pacote lxml (https://fpy.li/15-8) (EN) para gerar e interpretar XML: ele é fácil de começar a usar, completo e rápido.
Infelizmente, nem o lxml nem o ElementTree (https://docs.python.org/pt-br/3/library/xml.etree.elementtree.html) do próprio Python cabem
na RAM limitada de meu microcontrolador hipotético.
188. A documentação do Mypy discute isso na seção "Types of empty collections" (Tipos de coleções vazias) (https://fpy.li/15-11) (EN) da
página "Common issues and solutions" (Problemas comuns e suas soluções) (https://fpy.li/15-10) (EN).
189. Brett Cannon, Guido van Rossum e outros vem discutindo como escrever dicas de tipo para json.loads() desde 2016, em
Mypy issue #182: Define a JSON type (Definir um tipo JSON) (https://fpy.li/15-12) (EN).
190. O uso de enumerate no exemplo serve para confundir intencionalmente o verificador de tipo. Uma implementação mais
simples, produzindo strings diretamente, sem passar pelo índice de enumerate , seria corretamente analisada pelo Mypy, e o
cast() não seria necessário.
191. Relatei o problema em issue #5535 (https://fpy.li/15-17) no typeshed, "Dica de tipo errada para o atributo sockets em
asyncio.base_events.Server sockets attribute.", e ele foi rapidamente resolvido por Sebastian Rittau. Mas decidi manter o exemplo,
pois ele ilustra um caso de uso comum para cast , e o cast que escrevi é inofensivo.
192. Para ser franco, originalmente eu anexei um comentário # type: ignore às linhas com `server.sockets[0]` porque, após
pesquisar um pouco, encontrei linhas similares na documentação
(https://docs.python.org/pt-br/3/library/asyncio-stream.html#tcp-echo-server-using-streams) do asyncio e em um caso de teste
(https://fpy.li/15-19) (EN), e aí comecei a suspeitar que o problema não estava em meu código.
193. Mensagem de 18 de maio de 2020 (https://fpy.li/15-21) para a lista de email typing-sig.
194. A sintaxe `# type: ignore[code]` permite especificar qual erro do Mypy está sendo silenciado, mas os códigos nem sempre são
fáceis de interpretar. Veja a página "Error codes" (https://fpy.li/15-22) na documentação do Mypy.
195. Não vou entrar nos detalhes da implementação de clip , mas se você tiver curiosidade, pode ler o módulo completo em
clip_annot.py (https://fpy.li/15-23).
196. Mensagem "PEP 563 in light of PEP 649" (PEP 563 à luz da PEP 649) (https://fpy.li/15-27), publicado em 16 de abril de 2021.
197. Os termos são do livro clássico de Joshua Bloch, Java Efetivo, 3rd ed. (Alta Books). As definições e exemplos são meus.
198. A primeira vez que vi a analogia da cantina para variância foi no prefácio de Erik Meijer para o livro The Dart Programming
Language ("A Linguagem de Programação Dart"), de Gilad Bracha (Addison-Wesley).
199. Muito melhor que banir livros!
200. O leitor de notas de rodapé se lembrará que dei o crédito a Erik Meijer pela analogia da cantina para explicar variância.
201. Esse livro foi escrito para o Dart 1. Há mudanças significativas no Dart 2, inclusive no sistema de tipos. Mesmo assim, Bracha é
um pesquisador importante na área de design de linguagens de programação, e achei o livro valioso por sya perspectiva sobre o
design do Dart.
202. Veja o último parágrafo da seção "Covariance and Contravariance" (Covariância e Contravariância) (https://fpy.li/15-37) (EN) na
PEP 484.
203. Fonte: "The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling" (A Família de
Linguagens C: Entrevista com Dennis Ritchie, Bjarne Stroustrup, e James Gosling) (https://fpy.li/16-1) (EN).
204. O restante das ABCs na biblioteca padrão do Python ainda são valiosas para o goose typing e a tipagem estática. O problema
com as ABCs numbers é explicado na seção Seção 13.6.8.
205. Veja Lógica Binária - NOT (https://pt.wikipedia.org/wiki/L%C3%B3gica_bin%C3%A1ria#NOT) para uma explicação da negação binária.
206. A documentação do Python usa os dois termos. O capítulo "Modelo de Dados" (https://fpy.li/dtmodel) usa "refletido", mas em
"9.1.2.2. Implementando operações aritméticas" (https://fpy.li/16-7), a documentação do módulo menciona métodos de "adiante"
(forward) e "reverso" (reverse), uma terminologia que considero melhor, pois "adiante" e "reverso" são claramente sentidos
opostos, mas o oposto de "refletido" não é tão evidente.
207. Veja o Ponto de Vista para uma discussão desse problema.
208. pow pode receber um terceiro argumento opcional, modulo : pow(a, b, modulo) , também suportado pelos métodos especiais
quando invocados diretamente (por exemplo, a.__pow__(b, modulo) ).
209. A lógica para object.__eq__ e object.__ne__ está na função object_richcompare em Objects/typeobject.c (https://fpy.li/16-9),
no código-fonte do CPython.
210. A função embutida iter será tratada no próximo capítulo. Eu poderia ter usado tuple(other) aqui, e isso funcionaria, ao
custo de criar uma nova tuple quando tudo que o método .load(…) precisa é iterar sobre seu argumento.
211. De "Revenge of the Nerds" (A Revanche dos Nerds) (https://fpy.li/17-1), um post de blog.
212. Já usamos reprlib na seção Seção 12.3.
213. Agradeço ao revisor técnico Leonardo Rochael por esse ótimo exemplo.
214. Ao revisar esse código, Alex Martelli sugeriu que o corpo deste método poderia ser simplesmente return iter(self.words) .
Ele está certo: o resultado da invocação de self.words.iter() também seria um iterador, como deve ser. Entretanto, usei um
loop for com yield aqui para introduzir a sintaxe de uma função geradora, que exige a instrução yield , como veremos na
próxima seção. Durante a revisão da segunda edição deste livro, Leonardo Rochael sugeriu ainda outro atalho para o corpo de
__iter__ : yield from self.words . Também vamos falar de yield from mais adiante neste mesmo capítulo.
215. Eu algumas vezes acrescento um prefixo ou sufixo gen ao nomear funções geradoras, mas essa não é uma prática comum. E
claro que não é possível fazer isso ao implementar um iterável: o método especial obrigatório deve se chamar __iter__ .
216. Agradeço a David Kwast por sugerir esse exemplo.
217. (NT) Os termos em inglês são lazy (preguiçosa) e eager (ansiosa). Em português essas traduções aparecem, mas a literatura usa
também avaliação estrita e avaliação não estrita. Optamos pelos termos "preguiçosa" e "ansiosa", que parecem mais claros.
218. No Python 2, havia uma função embutida coerce() , mas ela não existe mais no Python 3. Foi considerada desnecessária, pois
as regras de coerção numérica estão implícitas nos métodos dos operadores aritméticos. Então, a melhor forma que pude
imaginar para forçar o valor inicial para o mesmo tipo do restante da série foi realizar a adição e usar seu tipo para converter o
resultado. Perguntei sobre isso na Python-list e recebi uma excelente resposta de Steven D’Aprano (https://fpy.li/17-11) (EN).
219. O diretório 17-it-generator/ no repositório de código do Python Fluente (https://fpy.li/code) inclui doctests e um script,
aritprog_runner.py, que roda os testes contra todas as variações dos scripts aritprog*.py.
220. O termo "mapeamento" aqui não está relacionado a dicionários, mas com a função embutida map .
221. O argumento apenas nomeado strict é novo, surgiu no Python 3.10. Quando strict=True , um ValueError é gerado se
qualquer iterável tiver um tamanho diferente. O default é False , para manter a compatibilidade retroativa.
222. itertools.pairwise foi introduzido no Python 3.10.
223. Pode também ser invocado na forma max(arg1, arg2, …, [key=?]) , devolvendo então o valor máximo entre os argumentos
passados.
224. Pode também ser invocado na forma min(arg1, arg2, …, [key=?]) , devolvendo então o valor mínimo entre os argumentos
passados.
225. chain e a maioria das funções de itertools são escritas em C.
226. Na versão 0.910 (NT: a última versão disponível quando este capítulo foi escrito), o Mypy ainda utiliza os tipos descontinuados
de typing .
227. Slide 33, "Keeping It Straight" (Cada Coisa em seu Lugar) em "A Curious Course on Coroutines and Concurrency" (Um Curioso
Curso sobre Corrotinas e Concorrência) (https://fpy.li/17-18).
228. Este exemplo foi inspirado por um trecho enviado por Jacob Holm à lista Python-ideas, em uma mensagem intitulada "Yield-
From: Finalization guarantees" (Yield-From: Garantias de finalização) (https://fpy.li/17-20) (EN). Algumas variantes aparecem mais
tarde na mesma thread, e Holm dá mais explicações sobre suas ideia em message 003912 (https://fpy.li/17-21) (EN).
229. Na verdade, ela nunca retorna, a menos que uma exceção interrompa o loop. O Mypy 0.910 aceita tanto None quanto typing​
.NoReturn como parâmetro de tipo devolvido pela geradora—mas ele também aceita str naquela posição, então aparentemente
o Mypy não consegue, neste momento, analisar completamente o código da corrotina.
230. Considerei renomear o campo, mas count é o melhor nome para a variável local na corrotina, e é o nome que usei para essa
variável em exemplos similares ao longo do livro, então faz sentido usar o mesmo nome no campo de Result . Não hesitei em
usar # type: ignore para evitar as limitações e os aborrecimentos de verificadores de tipo estáticos, quando se submeteer à
ferramenta tornaria o código pior ou desnecessariamente complicado.
231. Desde o Python 3.7, typing.Generator e outros tipos que correspondem a ABCs em`collections.abc` foram refatorados e
encapsulads em torno da ABC correspondente, então seus parâmetros genéricos não são visíveis no código-fonte de typing.py . Por
isso estou fazendo referência ao código-fonte do Python 3.6 aqui.
232. De acordo com o Jargon file (https://fpy.li/17-26) (EN), to grok não é meramente aprender algo, mas absorver de uma forma que
"aquilo se torna parte de você, parte de sua identidade".
233. Gamma et. al., Design Patterns: Elements of Reusable Object-Oriented Software, p. 261.
234. O código está em Python 2 porque uma de suas dependências opcionais é uma biblioteca Java chamada Bruma, que podemos
importar quando executamos o script com o Jython—que ainda não suporta o Python 3.
235. A biblioteca usada para ler o complexo arquivo binário .mst é na verdade escrita em Java, então essa funcionalidade só está
disponível quando isis2json.py é executado com o interpretador Jython, versão 2.5 ou superior. Para maiores detalhes, veja o
arquivo README.rst (https://fpy.li/17-47) (EN)no repositório. As dependências são importadas dentro das funções geradoras que
precisam delas, então o script pode rodar mesmo se apenas uma das bibliotecas externas esteja disponível.
236. Palestra de abertura da PyCon US 2013: "What Makes Python Awesome" ("O que torna o Python incrível") (https://fpy.li/18-1); a
parte sobre with começa em 23:00 e termina em 26:15.
237. Os três argumentos recebidos por self são exatamente o que você obtém se chama sys.exc_info() (https://fpy.li/18-7) no bloco
finally de uma instrução try/finally . Isso faz sentido, considerando que a instrução with tem por objetivo substituir a
maioria dos usos de try/finally , e chamar sys.exc_info() é muitas vezes necessário para determinar que ação de limpeza é
necessária.
238. A classe real se chama _GeneratorContextManager . Se você quiser saber exatamente como ela funciona, leia seu código fonte
(https://fpy.li/18-10) na Lib/contextlib.py do Python 3.10.
239. Essa dica é uma citação literal de um comentário de Leonardo Rochael, um do revisores técnicos desse livro. Muito bem dito,
Leo!
240. "Pouco conhecido" porque pelo menos eu e os outros revisores técnicos não sabíamos disso até Caleb Hattingh nos contar.
Obrigado, Caleb!
241. As pessoas reclamam sobre o excesso de parênteses no Lisp, mas uma indentação bem pensada e um bom editor praticamente
resolvem essa questão. O maior problema de legibilidade é o uso da mesma notação (f …​) para chamadas de função e formas
especiais como (define …​) , (if …​) e (quote …​) , que de forma alguma se comportam como chamadas de função
242. Para tornar a iteração por recursão prática e eficiente, o Scheme e outras linguagens funcionais implementam chamadas de
cauda apropriadas (ou otimizadas). Para ler mais sobre isso, veja o Ponto de vista.
243. Mas o segundo interpretador de Norvig, lispy.py (https://fpy.li/18-16), suporta strings como um tipo de dado, e também traz
recursos avançados como macros sintáticas, continuações, e chamadas de cauda otimizadas. Entretanto, o lispy.py é quase três
vezes maior que o lis.py—é muito mais difícil de entender.
244. O comentário # type: ignore[index] está ali por causa do issue #6042 (https://fpy.li/18-19) no typeshed, que segue sem
resolução quando esse capítulo está sendo revisado. ChainMap é anotado como MutableMapping , mas a dica de tipo no atributo
maps diz que ele é uma lista de Mapping , indiretamente tornando todo o ChainMap imutável até onde o Mypy entende.
245. Enquanto estudava o lis.py e o lispy.py de Norvig, comecei uma versão chamada mylis (https://fpy.li/18-20), que acrescenta alguns
recursos, incluindo um REPL que aceita expressões-S parciais e espera a continuação, como o REPL do Python sabe que não
terminamos e apresenta um prompt secundário ( …​) até entrarmos uma expressão ou instrução completa, que possa ser analisada
e avaliada. O mylis também trata alguns erros de forma graciosa, mas ele ainda é fácil de quebrar. Não é nem de longe tão robusto
quanto o REPL do Python.
246. A atribuição é um dos primeiros recursos ensinados em muitos tutoriais de programacão, mas set! só aparece na página 220
do mais conhecido livro de Scheme, Structure and Interpretation of Computer Programs (A Estrutura e a Interpretação de
Programas de Computador), 2nd ed., (https://fpy.li/18-22) de Abelson et al. (MIT Press), também conhecido como SICP ou "Wizard
Book" (Livro do Mago). Programas em estilo funcional podem nos levar muito longe sem as mudanças de estado típicas da
programação imperativa e da programação orientada a objetos.
247. O nome Unicode oficial para λ (U+03BB) é GREEK SMALL LETTER LAMDA. Isso não é um erro ortográfico: o caractere é
chamado "lamda" sem o "b" no banco de dados do Unicode. De acordo com o artigo "Lambda" (https://fpy.li/18-26) (EN) da Wikipedia
em inglês, o Unicode Consortium adotou essa forma em função de "preferências expressas pela Autoridade Nacional Grega."
248. Acompanhando a discussão na lista python-dev, achei que uma razão para a rejeição do else foi a falta de consenso sobre
como indentá-lo dentro do match : o else deveria ser indentedo no mesmo nível do match ou no mesmo nível do case ?
249. Veja slide 21 em "Python is Awesome" ("O Python é Incrível") (https://fpy.li/18-29) (EN).
250. (NT)No momento em que essa tradução é feita, o título dessa seção na documentação diz "Com gerenciadores de contexto de
instruções", uma frase que sequer faz sentido. Foi aberto um issue sobre isso.
251. Slide 8 of the talk Concurrency Is Not Parallelism (https://fpy.li/19-1).
252. Estudei e trabalhei com o Prof. Imre Simon, que gostava de dizer que há dois grandes pecados na ciência: usar palavras
diferentes para significar a mesma coisa e usar uma palavra para significar coisas diferentes. Imre Simon (1942-2009) foi um
pioneiro da ciência da computação no Brasil, com contribuições seminais para a Teoria dos Autômatos. Ele fundou o campo da
Matemática Tropical e foi também um defensor do software livre, da cultura livre, e da Wikipédia.
253. Essa seção foi sugerida por meu amigo Bruce Eckel—autor de livros sobre Kotlin, Scala, Java, e C++.
254. "FIFO" é a sigla em inglês para "first in, first out" (NT).
255. Chame sys.getswitchinterval() (https://docs.python.org/pt-br/3/library/sys.html#sys.getswitchinterval) para obter o intervalo; ele
pode ser modificado com sys.setswitchinterval(s) (https://docs.python.org/pt-br/3/library/sys.html#sys.setswitchinterval).
256. Uma syscall é uma chamada a partir do código do usuário para uma função do núcleo (kernel) do sistema operacional. E/S,
temporizadores e travas são alguns dos serviços do núcleo do SO disponíveis através de syscalls. Para aprender mais sobre esse
tópico, leia o artigo "Chamada de sistema" (https://pt.wikipedia.org/wiki/Chamada_de_sistema) na Wikipedia.
257. Os módulos zlib e bz2 são mencionados nominalmente em uma mensagem de Antoine Pitrou na python-dev (https://fpy.li/19-6)
(EN). Pitrou contribuiu para a lógica da divisão de tempo da GIL no Python 3.2.
258. Fonte: slide 106 do tutorial de Beazley, "Generators: The Final Frontier" (EN) (https://fpy.li/19-7).
259. Fonte: início do capítulo "threading — Paralelismo baseado em Thread" (https://docs.python.org/pt-br/3/library/threading.html#) (EN).
260. O Unicode tem muitos caracteres úteis para animações simples, como por exemplo os padrões Braille (https://fpy.li/19-11). Usei os
caracteres ASCII "\|/-" para simplificar os exemplos do livro.
261. O semáforo é um bloco fundamental que pode ser usado para implementar outros mecanismos de sincronização. O Python
fornece diferentes classes de semáforos para uso com threads, processos e corrotinas. Veremos o asyncio.Semaphore na Seção
21.7.1 (no Capítulo 21).
262. Agradeço aos revisores técnicos Caleb Hattingh e Jürgen Gmach, que não me deixaram esquecer de greenlet e gevent.
263. É um MacBook Pro 15” de 2018, com uma CPU Intel Core i7 2.2 GHz de 6 núcleos.
264. Isso é verdade hoje porque você provavelmente está usando um SO moderno, com multitarefa preemptiva. O Windows antes da
era NT e o MacOS antes da era OSX não eram "preemptivos", então qualquer processo podia tomar 100% da CPU e paralisar o
sistema inteiro. Não estamos inteiramente livres desse tipo de problema hoje, mas confie na minha barba branca: esse tipo de
coisa assombrava todos os usuários nos anos 1990, e a única cura era um reset de hardware.
265. Nesse exemplo, 0 é uma sentinela conveniente. None também é comumente usado para essa finalidade, mas usar 0
simplifica a dica de tipo para PrimeResult e a implementação de worker .
266. Sobreviver à serialização sem perder nossa identidade é um ótimo objetivo de vida.
267. See 19-concurrency/primes/threads.py (https://fpy.li/19-27) no repositório de código do Fluent Python (https://fpy.li/code).
268. Para saber mais, consulte "Troca de contexto" (https://pt.wikipedia.org/wiki/Troca_de_contexto) na Wikipedia.
269. Provavelmente foram essas mesmas razões que levaram o criador do Ruby, Yukihiro Matsumoto, a também usar uma GIL no
seu interpretador.
270. Na faculdade, como exercício, tive que implementar o algorítimo de compressão LZW em C. Mas antes escrevi o código em
Python, para verificar meu entendimento da especificação. A versão C foi cerca de 900 vezes mais rápida.
271. Fonte: Thoughtworks Technology Advisory Board, Technology Radar—November 2015 (https://fpy.li/19-40) (EN).
272. Compare os caches de aplicação—usados diretamente pelo código de sua aplicação—com caches HTTP, que estariam no limite
superior da Figura 3, servindo recursos estáticos como imagens e arquivos CSS ou JS. Redes de Fornecimento de Conteúdo (CDNs
de Content Delivery Networks) oferecem outro tipo de cache HTTP, instalados em datacenters próximos aos usuários finais de sua
aplicação.
273. Diagrama adaptado da Figure 1-1, Designing Data-Intensive Applications de Martin Kleppmann (O’Reilly).
274. Alguns palestrantes soletram a sigla WSGI, enquanto outros a pronunciam como uma palavra rimando com "whisky."
275. uWSGI é escrito com um "u" minúsculo, mas pronunciado como a letra grega "µ," então o nome completo soa como "micro-
whisky", mas com um "g" no lugar do "k."
276. Os engenheiros da Bloomberg Peter Sperl and Ben Green escreveram "Configuring uWSGI for Production Deployment"
(Configurando o uWSGI para Implantação em Produção) (https://fpy.li/19-44) (EN), explicando como muitas das configurações default
do uWSGI não são adequadas para cenários comuns de implantação. Sperl apresentou um resumo de suas recomendações na
EuroPython 2019 (https://fpy.li/19-45). Muito recomendado para usuários de uWSGI.
277. Caleb é um dos revisores técnicos dessa edição de Python Fluente.
278. Agradeço a Lucas Brunialti por me enviar um link para essa palestra.
279. NT: Trocadilho intraduzível com thread no sentido de "fio" ou "linha", algo como "Quando as linhas desfiam."
280. As APIs Python threading e concurrent.futures foram fortemente influenciadas pela biblioteca padrão do Java.
281. A comunidade Erlang usa o termo "processo" para se referir a atores. Em Erlang, cada processo é uma função em seu próprio
loop, que então são muito leves, tornando viável ter milhões deles ativos ao mesmo tempo em uma única máquina—nenhuma
relação com os pesados processo do SO, dos quais falamos em outros pontos desse capitulo. Então temos aqui exemplos dos dois
pecados descritos pelo Prof. Simon: usar palavras diferentes para se referir à mesma coisa, e usar uma palavra para se referir a
coisas diferentes.
282. Especialmente se o seu provedor de serviços na nuvem aluga máquinas por tempo de uso, independente de quão ocupada
esteja a CPU.
283. Para servidores que podem ser acessados por muitos clientes, há uma diferença: as corrotinas escalam melhor, pois usam
menos memória que as threads, e também reduzem o custo das mudanças de contexto, que mencionei na seção Seção 19.6.5.
284. As imagens são originalmente do CIA World Factbook (https://fpy.li/20-4), uma publicação de domínio público do governo norte-
americano. Copiei as imagens para o meu site, para evitar o risco de lançar um ataque de DoS contra cia.gov.
285. Definir follow_redirects=True não é necessário nesse exemplo, mas eu queria destacar essa importante diferença entre
HTTPX e requests. Além disso, definir follow_redirects=True nesse exemplo me dá a flexibilidade de armazenar os arquivos de
imagem em outro lugar no futuro. Acho sensata a configuração default do HTTPX, follow_redirects​=False , pois
redirecionamentos inesperados podem mascarar requisições desnecessárias e complicar o diagnóstico de erro.
286. Acrônimo de your mileage may vary, (NT: algo como sua quilometragem pode variar, querendo dizer "seu caso pode ser
diferente"): com threads, você nunca sabe a sequência exata de eventos que deveriam acontecer quase ao mesmo tempo; é
possível que, em outra máquina, se veja loiter(1) começar antes de loiter(0) terminar, especialmente porque sleep
sempre libera a GIL, então o Python pode mudar para outra thread mesmo se você dormir por 0s.
287. Em setembro de 2021 não havia dicas de tipo na versão (então) atual do tqdm . Tudo bem. O mundo não vai acabar por causa
disso. Obrigado, Guido, pela tipagem opcional!
288. Selivanov implementou async/await no Python, e escreveu as PEPs relacionadas: 492 (https://fpy.li/pep492), 525
(https://fpy.li/pep525), e 530 (https://fpy.li/pep530).
289. Há uma exceção a essa regra: se você iniciar o Python com a opção -m asyncio , pode então usar await diretamente no
prompt >>> para controlar uma corrotina nativa. Isso é explicado na seção Seção 21.10.1.1.
290. Desculpem, não consegui resistir.
291. true.dev está disponível por US$ 360,00 ao ano no momento em que escrevi isso. Também notei que for.dev está registrado,
mas seu DNS não está configurado.
292. Agradeço ao leitor Samuel Woodward por ter reportado esse erro para a O’Reilly em fevereiro de 2023
293. Essa dica é uma citação literal de um comentário do revisor técnico Caleb Hattingh. Obrigado, Caleb!
294. Agradeço a Guto Maia, que notou que conceito de um semáforo não era explicado, quando leu o primeiro rascunho deste
capítulo.
295. Um discussão detalhada sobre esse tópico pode era encontrada em uma thread de discussão que iniciei no grupo python-tulip,
intitulada "Which other futures may come out of asyncio.as_completed?" (Que outros futures podem sair de asyncio.as_completed?
) (https://fpy.li/21-19). Guido responde e fornece detalhes sobre a implementação de as_completed , bem como sobre a relação
próxima entre futures e corrotinas no asyncio.
296. O ponto de interrogação encaixotado na captura de tela não é um defeito do livro ou do ebook que você está lendo. É o
caractere U+101EC—PHAISTOS DISC SIGN CAT, que não existe na fonte do terminal que usei. O Disco de Festo
(https://pt.wikipedia.org/wiki/Disco_de_Festo) é um artefato antigo inscrito com pictogramas, descoberto na ilha de Creta.
297. Você pode usar outro servidor ASGI no lugar do uvicorn, tais como o hypercorn ou o Daphne. Veja na documentação oficial do
ASGI a página sobre implementações (https://fpy.li/21-30) (EN) para maiores informações.
298. Agradeço o revisor técnico Miroslav Šedivý por apontar bons lugares para usar pathlib nos exemplo de código.
299. Como mencionado no Capítulo 8, o pydantic (https://fpy.li/21-31) aplica dicas de tipo durante a execução, para validação de dados.
300. O issue #5535 está fechado desde outubro de 2021, mas o Mypy não lançou uma nova versão desde então, daí o erro
permanece.
301. O revisor técnico Leonardo Rochael apontou que a construção do índice poderia ser delegada a outra thread, usando
loop.run_with_executor() na corrotina supervisor . Dessa forma o servidor estaria pronto para receber requisições
imediatamente, enquanto o índice é construído. Isso é verdade, mas como consultar o índice é a única coisa que esse servidor faz,
isso não seria uma grande vantagem nesse exemplo.
302. Isso é ótimo para experimentação, como o console do Node.js. Agradeço a Yury Selivanov por mais essa excelente contribuição
para o Python assíncrono.
303. Veja RFC 6761—Special-Use Domain Names (https://fpy.li/21-45).
304. Em contraste com o Javascript, onde async/await são atrelados ao loop de eventos que é inseparável do ambiente de runtime,
isto é, um navegador, o Node.js ou o Deno.
305. Isso difere das anotações de corrotinas clássicas, discutidas na seção Seção 17.13.3.
306. Vídeo: "Introduction to Node.js" (Introdução ao Node.js) (https://fpy.li/21-59), em 4:55.
307. Usar uma única thread era o default até o lançamento do Go 1.5. Anos antes, o Go já tinha ganho uma merecida reputação por
permitir a criação de sistemas em rede de alta concorrência. Mais uma evidência que a concorrência não exige múltiplas threads
ou múltiplos núcleos de CPU.
308. Independente de escolhas técnicas, esse foi talvez o maior erro daquela projeto: as partes interessadas não forçaram uma
abordagem MVP—entregar o "Mínimo Produto Viável" o mais rápido possível e acrescentar novos recursos em um ritmo estável.
309. Alex Martelli, Anna Ravenscroft & Steve Holden, Python in a Nutshell, Third Edition (https://fpy.li/pynut3) (EN) (O’Reilly), p. 123.
310. Bertrand Meyer, Object-Oriented Software Construction, 2nd ed. (Pearson), p. 57. (EN)
311. A OSCON—O’Reilly Open Source Conference (Conferência O’Reilly de Código Aberto)—foi uma vítima da pandemia de COVID-19.
O arquivo JSON original de 744 KB, que usei para esses exemplos, não está mais disponível online hoje (10 de janeiro de 2021).
Você pode obter uma cópia do osconfeed.json (https://fpy.li/22-1) no repositório de exemplos do livro.
312. Dois exemplos são AttrDict (https://fpy.li/22-2) e addict (https://fpy.li/22-3).
313. A expressão self.__data[name] é onde a exceção KeyError pode acontecer. Idealmente, ela deveria ser tratada, e uma
AttributeError deveria ser gerada em seu lugar, pois é isso que se espera de __getattr__ . O leitor mais diligente está
convidado a programar o tratamento de erro, como um exercício.
314. A fonte dos dados é JSON e os únicos tipos de coleção em dados JSON são dict e list .
315. Aliás, Bunch é o nome da classe usada por Alex Martelli para compartilahr essa dica em uma receita de 2001 intitulada "The
simple but handy ‘collector of a bunch of named stuff’ class" (Uma classe simples mas prática 'coletora de um punhado de coisas
nomeadas') (https://fpy.li/22-4).
316. Isso é, na verdade, uma desvantagem do Princípio de Acesso Uniforme de Meyer, mencionada no início deste capítulo. Quem
tiver interesse nessa discussão pode ler o Ponto de Vista opcional.
317. Fonte: documentação de @functools.cached_property (https://docs.python.org/pt-br/3/library/functools.html#functools.cached_property).
Sei que autor dessa explicação é Raymond Hettinger porque ele a escreveu em resposta a um problema que eu mesmo reportei:
bpo42781—functools.cached_property docs should explain that it is non-overriding (a documentação de functools.cached_property
deveria explicar que ele é não-dominante) (https://fpy.li/22-11) (EN). Hettinger é um grande colaborador da documentação oficial do
Python e da biblioteca padrão. Ele também escreveu o excelente Descriptor HowTo Guide (Guia de Utilização de Descritores)
(https://fpy.li/22-12) (EN), um recurso fundamental para o [attribute_descriptors].
318. Citação direta de Jeff Bezos no artigo do Wall Street Journal "Birth of a Salesman" (O Nascimento de um Vendedor)
(https://fpy.li/22-16) (EN) (15 de outubro de 2011). Pelo menos até 2023, é necessario ser assinante para ler o artigo.
319. Esse código foi adaptado da "Recipe 9.21. Avoiding Repetitive Property Methods" (Receita 9.21. Evitando Métodos Repetitivos de
Propriedades) do Python Cookbook (https://fpy.li/pycook3) (EN), 3ª ed., de David Beazley e Brian K. Jones (O’Reilly).
320. Aquela cena sangrenta está disponível no Youtube (https://fpy.li/22-17) (EN) quando reviso essa seção, em outubro de 2021.
321. Alex Martelli assinala que, apesar de __slots__ poder ser definido como uma list , é melhor ser explícito e sempre usar
uma tuple , pois modificar a lista em __slots__ após o processamento do corpo da classe não tem qualquer efeito. Assim, seria
equivocado usar uma sequência mutável ali.
322. Alex Martelli, Python in a Nutshell, 2ª ed. (O’Reilly), p. 101.
323. As razões que menciono foram apresentadas no artigo intitulado "Java’s new Considered Harmful" (new do Java considerado
nocivo) (https://fpy.li/22-30), de Jonathan Amsterdam, publicado na Dr. Dobbs Journal, e no "Consider static factory methods instead
of constructors" (Considere substituir construtores por métodos estáticos de fábrica), que é o Item 1 do premiado livro Effective
Java, 3ª ed., de Joshua Bloch (Addison-Wesley).

HTML gerado em 2023-08-26 17:14:46 -0300

Você também pode gostar