Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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.
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.
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 )
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.
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)
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 .
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')
...
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')
...
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 .
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:
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)
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__ .
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 __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))
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.
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
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.
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.
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.
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:
Na verdade, o Python não exige que classes concretas herdem de qualquer dessas ABCs. Qualquer classe que
implemente __len__ satisfaz a interface Sized .
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.
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).
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.
Comparação rica < <= == != > >= __lt__ __le__ __eq__ __ne__
__gt__ __ge__
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 .
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 .
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.
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.
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).
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:
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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 .
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 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.
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
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 .
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.
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
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.
s.__add__(s2) ● ● s + s2—concatenação
s.__iadd__(s2) ● s += s2—concatenação no
mesmo lugar
s.__contains__(e) ● ● e in s
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
Vamos agora examinar um tópico importante para a programação Python idiomática: tuplas, listas e
desempacotamento iterável.
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.
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)
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.
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()
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.
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.
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:
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 .
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:
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
...
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:
name 'Shanghai'
lat 31.1
lon 121.3
Variável Valor atribuído
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))]:
👉 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.
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.
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.
LISP
(lambda (parms…) body1 body2…)
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.
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.
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.
(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].
É 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.
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.
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.
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.
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.
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.
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)
PYTHON3
>>> board = []
>>> for i in range(3):
... row = ['_'] * 3 # (1)
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board # (2)
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]
👉 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.
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.
PYCON
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
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.
Esse exemplo é um caso raro—em meus 20 anos usando Python, nunca vi esse comportamento estranho estragar o dia
de alguém.
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.
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)
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.
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.
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
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.__contains__(e) ● ● e in s
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]
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:
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.
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])
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 .
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' ).
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.
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
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])
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).
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 .
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.__contains__(e) ● e in s
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]
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).
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.
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.
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 .
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']
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.
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
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 é
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.
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.
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.
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 .
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 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.
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.
“ 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 .
d.__contains__(k) ● ● ● k in d.
d.__copy__() ● Suporte a
copy.copy(d).
d.__len__() ● ● ● len(d)—número de
itens.
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.__reversed__() ● ● ● Suporte a
reverse(d)—
devolve um iterador
de chaves, da última
para a primeira a
serem inseridas.
d.setdefault(k, ● ● ● Se k in d , devolve
[default]) d[k] ; senão, atribui
d[k] = default e
devolve isso.
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.
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)
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)
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.
PYTHON3
my_dict.setdefault(key, []).append(new_value)
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.
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:
O chamável que produz os valores default é mantido em um atributo de instância chamado default_factory .
PY
"""Build an index mapping word -> list of occurrences"""
import collections
import re
import sys
WORD_RE = re.compile(r'\w+')
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.
⚠️ 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
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.get('2')
'two'
>>> d.get(4)
'four'
>>> d.get(1, 'N/A')
'N/A'
>>> 2 in d
True
>>> 1 in d
False
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)
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.
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) .
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.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:
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:
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.
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.
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
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)).
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.
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'
>>>
A seguir veremos views—que permitem operações de alto desempenho em um dict , sem cópias desnecessárias dos
dados.
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
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.
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).
✒️ 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:
>>> 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.
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.
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)
{'§', '=', '¢', '#', '¤', '<', '¥', 'µ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}
A ordem da saída muda a cada processo Python, devido ao hash "salgado", mencionado na seção Seção 3.4.1.
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.
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.intersection(it, …) Intersecção de s e
todos os conjuntos
construídos a partir de
iteráveis it , etc.
z | s s.__ror__(z) | invertido
A Tabela 9 lista predicados de conjuntos: operadores e métodos que devolvem True ou False .
s.issubset(it) s é um subconjunto do
conjunto criado a partir do
iterável it
s.issuperset(it) s é um superconjunto do
conjunto criado a partir do
iterável it
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.
s.__len__() ● ● len(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 .
s.__contains__() ● ● ● e in s
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.__len__() ● ● ● len(s)
s.__or__(z) ● ● ● s | z (união de
s e z)
s.__ror__() ● ● ● Operador |
invertido
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.__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ê!
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 .
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."
— 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]
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.
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.
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.
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é'
👉 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.
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.
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.
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:
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.
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.
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:
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.
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.
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ã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 .
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 .
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'
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.
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.
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.
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.
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.
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.
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é' .
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.
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()
"""
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'
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'
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.
BASH
Z:\>python default_encodings.py > encodings.log
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.
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
]
'㊷' CIRCLED NUMBER FORTY TWO —não existe nem no CP 1252 nem no CP 437.
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.
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:
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.
“ 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. […]
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.
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]
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.
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.
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.
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1 == s2
False
>>> nfc_equal(s1, s2)
True
>>> nfc_equal('A', 'a')
False
>>> 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
"""
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.
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.
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)
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)
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)
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.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 .
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)
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.
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."
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.
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 .
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.
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 .
👉 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.
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.
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
def main(words):
if words:
find(*words)
else:
print('Please provide words to find.')
if __name__ == '__main__':
main(sys.argv[1:])
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.
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'
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.
Exemplo 23. ramanujan.py: compara o comportamento de expressões regulares simples como str e como bytes
PY
import re
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)
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 .
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 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.
PYCON
>>> os.listdir('.') # (1)
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.') # (2)
[b'abc.txt', b'digits-of-\xcf\x80.txt']
O Unicode é um buraco de coelho bem fundo. É hora de encerrar nossa exploração de str e bytes .
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.
"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 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?
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.
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": 文字化け.
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
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.
Vamos começar por uma visão geral, por alto, das três fábricas de classes.
Exemplo 1. class/coordinates.py
PY3
class Coordinate:
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
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:
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:
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 .
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
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.
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 .
👉 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.
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() .
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 .
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.
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
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.
Exemplo 8. typing_namedtuple/coordinates2.py
PY
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float # (1)
lon: float
reference: str = 'WGS84' # (2)
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.
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.
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.
PY
>>> import typing
>>> class Coordinate(typing.NamedTuple):
... lat: float
... lon: float
...
>>> trash = Coordinate('Ni!', None)
>>> print(trash)
Coordinate(lat='Ni!', lon=None) # (1)
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]
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:
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
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 .
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 .
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.
PYTHON3
import typing
class DemoNTClass(typing.NamedTuple):
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)
3. c é só um bom e velho atributo de classe comum; não será mencionado em nenhuma anotação.
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' .
PYCON
>>> DemoNTClass.__doc__
'DemoNTClass(a, b)'
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.
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 .
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'
PYCON
>>> dc = DemoDataClass(9)
>>> dc.a
9
>>> dc.b
1.1
>>> dc.c
'spam'
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'
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.
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
order=True
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.
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.
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.
PY3
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list[str] = field(default_factory=list) # (1)
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.
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)
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.
PY3
"""
``HackerClubMember`` objects accept an optional ``handle`` argument::
If ``handle`` is omitted, it's set to the first part of the member's name::
Members must have a unique handle. The following ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::
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.
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__ .
@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)
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.
$ 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.
PY3
@dataclass
class C:
i: int
j: int = None
database: InitVar[DatabaseType] = None
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.
“ 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
@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 .
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.
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.
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.
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.
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.
Há três variantes de padrões de classes: simples, nomeado e posicional. Vamos estudá-las nessa ordem.
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)
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.
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.
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.
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.
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.
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.
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.
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:
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.
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
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 = ''
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’.”
“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.
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.
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]
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']
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.
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}
Figura 2. charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor
igual.
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.
“ 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.
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 .
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
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.
⚠️ 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
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] .
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.
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)
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.
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.
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.
PY
class Bus:
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.
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)
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.
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))
Outra questão relacionada a parâmetros de função é o uso de valores mutáveis como defaults, discutida a seguir.
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.
PY
class HauntedBus:
"""A bus model haunted by ghost passengers"""
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 .
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']
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.
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.
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']
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. Uma classe simples mostrando os perigos de mudar argumentos recebidos
PY
class TwilightBus:
"""A bus model that makes passengers vanish"""
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)
3. Apaga a referência a.
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
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 .
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).
✒️ 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
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]
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
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:
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 .
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.
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.
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:
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.
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.
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.
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.
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.
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]
>>>
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).
PYCON
>>> from functools import reduce (1)
>>> from operator import add (2)
>>> reduce(add, range(100)) (3)
4950
>>> sum(range(100)) (4)
4950
>>>
✒️ 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.
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.
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.
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 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() :
Vamos agora criar instâncias de classes que funcionam como objetos invocáveis.
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:
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.
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 .
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.
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.
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.
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.
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)
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.
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.
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'}
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.
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 .
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:
“ 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.
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 .
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.
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.
É 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.
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.
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'
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'
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.
PY
from pytest import mark
@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'
Usando --disallow-untyped-defs com o arquivo de teste produz três erros e uma observação:
--disallow-incomplete-defs .
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:
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:
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'
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)
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}'
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]:
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.
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.
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.
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:
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.
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
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:
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]
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.
PY
from birds import *
daffy = Duck()
alert(daffy) # (1)
alert_duck(daffy) # (2)
alert_bird(daffy) # (3)
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:
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.
PY
from birds import *
woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)
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
Vamos falar de um de cada vez, começando por um tipo que é estranho, aparentemente inútil, mas de uma
importância fundamental.
PYTHON
def double(x):
return x * 2
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.
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 .
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:
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.
PYTHON
class T1:
...
class T2(T1):
...
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()
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 .
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) #
f1(o4) #
f2(o4) # tudo certo: regra #3
f3(o4) #
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.
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.
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 .
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
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 .
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.
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] :
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.
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.
PYTHON
from __future__ import annotations
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.
PYTHON
from typing import List
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.
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.
PY
from geolib import geohash as gh # type: ignore # (1)
PRECISION = 9
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.
PY
from typing import NamedTuple
PRECISION = 9
class Coordinate(NamedTuple):
lat: float
lon: float
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}'
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
PY
list[tuple[str, ...]]
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)]
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.
PY
import sys
import re
import unicodedata
from collections.abc import Iterator
RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1
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
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
PY3
from collections.abc import Mapping
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 .
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()
“ 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
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.
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.
PYTHON
def fsum(__seq: Iterable[float]) -> float:
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'
PY
from collections.abc import Iterable
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.
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.
PY
from collections.abc import Sequence
from random import shuffle
from typing import TypeVar
T = TypeVar('T')
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] .
Outro exemplo é a função statistics.mode da biblioteca padrão, que retorna o ponto de dado mais comum de uma
série.
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
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')
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
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.
PYCON
>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'
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.
PYTHON3
from collections.abc import Iterable, 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
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.
O construtor de typing.TypeVar tem outros parâmetros opcionais - covariant e contravariant — que veremos
em Capítulo 15, Seção 15.7.
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.
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')]
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.
PY
from typing import Protocol, Any
2. O corpo do protocolo tem uma ou mais definições de método, com …em seus corpos.
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
LT = TypeVar('LT', bound=SupportsLessThan)
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 .
PY
from collections.abc import Iterator
from typing import TYPE_CHECKING # (1)
import pytest
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)
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]
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 .
PYTHON3
def input(__prompt: Any = ...) -> str: ...
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.
PY
from collections.abc import Callable
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.
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.
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] .
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
Para encerrar esse capítulo, vamos considerar brevemente os limites das dicas de tipo e do sistema de tipagem estática
que elas suportam.
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.
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.
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
“ 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 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
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.
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.
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.
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]]
]
]
]
]
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.
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.
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 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]
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:
Após criar essa base, poderemos então enfrentar os outros tópicos relativos aos decoradores:
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.
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.
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]
PYTHON3
@decorate
def target():
print('running target()')
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.
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>
PY
registry = [] # (1)
@register # (6)
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
if __name__ == '__main__':
main() # (9)
5. Devolve func : precisamos devolver uma função; aqui devolvemos a mesma função recebida como argumento.
7. f3 não é decorada.
8. main mostra registry , depois chama f1() , f2() , e f3() .
$ 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 .
PYCON
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)
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.
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.
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
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.
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
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.
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
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.
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.
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?
PYTHON3
class Averager():
def __init__(self):
self.series = []
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.
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.
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.
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
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.
Tendo visto as clausuras do Python, podemos agora de fato implementar decoradores com funções aninhadas.
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 .
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))
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
PYTHON3
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
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:
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.
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.
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.
PYTHON3
import functools
@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():
...
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.
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):
...
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.
PYTHON3
@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
...
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?).
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><built-in function abs></pre>'
>>> htmlize('Heimlich & Co.\n- a game') # (2)
'<p>Heimlich & 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.
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>'
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.
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()
Exemplo 22. Para aceitar parâmetros, o novo decorador register precisa ser invocado como uma função
PY
registry = set() # (1)
@register(active=False) # (8)
def f1():
print('running f1()')
@register() # (9)
def f2():
print('running f2()')
def f3():
print('running f3()')
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.
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 .
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>}
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.
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.
PY
import time
if __name__ == '__main__':
@clock() # (11)
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
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 .
11. Nesse auto-teste, clock() é chamado sem argumentos, então o decorador aplicado usará o formato default, str .
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.
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)
BASH
$ python3 clockdeco_param_demo1.py
snooze: 0.12414693832397461s
snooze: 0.1241159439086914s
snooze: 0.12412118911743164s
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)
BASH
$ python3 clockdeco_param_demo2.py
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
✒️ 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.
Exemplo 27. Módulo clockdeco_cls.py: decorador parametrizado clock , implementado como uma classe
PY
import time
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 .
Isso encerra nossa exploração dos decoradores de função. Veremos os decoradores de classe no [class_metaprog].
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.
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.
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"
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.
Figura 1. Diagrama de classes UML para o processamento de descontos em um pedido, implementado com o padrão de
projeto Estratégia.
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.
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 .
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.
class Customer(NamedTuple):
name: str
fidelity: int
class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
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.
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>
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.
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 __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# (3)
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.
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.
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>
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.
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>
3. Encerrando a compra com um carrinho simples, best_promo deu à cliente fiel ann o desconto da
fidelity_promo .
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)
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 .
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)
)
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.
PY
from decimal import Decimal
import inspect
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.
PY
Promotion = Callable[[Order], Decimal]
@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)
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.
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 __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__ .
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++.
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'
— 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.)
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 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.
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 ).
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.
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.
class Vector2d:
typecode = 'd' # (1)
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 __abs__(self):
return math.hypot(self.x, self.y) # (9)
def __bool__(self):
return bool(abs(self)) # (10)
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() .
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.
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',)
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.
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
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)'
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)'
PYTHON3
# inside the Vector2d class
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)' .
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
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.
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)
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 .
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
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'
@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)
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.
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.
PYTHON3
# inside class Vector2d:
def __hash__(self):
return hash((self.x, self.y))
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)}
👉 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.
PY
case Vector2d(_, 0):
print(f'{v!r} is horizontal')
o resultado é esse:
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 .
>>> 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)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
Tests of hashing:
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> len({v1, v2})
2
"""
class Vector2d:
__match_args__ = ('x', 'y')
typecode = 'd'
@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 __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 .
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").
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__ .
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__ .
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__ .
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.
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__ .
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 .
É 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 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.
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'
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 .
3. Verifica o repr de sv .
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.
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:
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.
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
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.
“ 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.
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)
PYTHON3
>>> my_object.foo += 1
“ 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 {
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
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:
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
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.
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.
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 .
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 __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 __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 .
4. Remove o prefixo array('d', eo ) final, antes de inserir a string em uma chamada ao construtor de Vector .
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.
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.
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)
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__ .
👉 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.
def __len__(self):
return len(self._components)
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__(...) .
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) .
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']
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) :
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)
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__ .
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)
3. …invoca a classe para criar outra instância de Vector a partir de uma fatia do array _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 TypeError 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 .
4. Vector não suporta indexação multidimensional, então tuplas de índices ou de fatias geram um erro.
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.
1. Define __match_args__ para permitir pattern matching posicional sobre os atributos dinâmicos suportados por
__getattr__ .[131]
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)
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.
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)
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 .
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.
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 .
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
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'
def __hash__(self):
hashes = (hash(x) for x in self._components) # (4)
return functools.reduce(operator.xor, hashes, 0) # (5)
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)
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 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.
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' .
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
>>> v7 = Vector(range(7))
>>> v7
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
>>> abs(v7) # doctest:+ELLIPSIS
9.53939201...
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
>>> v7 = Vector(range(10))
>>> v7.x
0.0
>>> v7.y, v7.z, v7.t
(1.0, 2.0, 3.0)
>>> 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::
Most hash codes of non-integers vary from a 32-bit to 64-bit CPython build::
class Vector:
typecode = 'd'
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 __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)
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv)
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 > ).
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.
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.
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.
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.
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.
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
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?
“ 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:
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.
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.
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.
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):
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.
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.
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.
PYTHON3
import collections
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)
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.
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."
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.
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.
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.
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.
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.
“ 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!
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ē !
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
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.
PYTHON3
from collections import namedtuple, abc
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)
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.
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 é
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]
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 .
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
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.
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:
“ 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)
.load(…)
.pick()
.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).
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.
Exemplo 7. tombola.py: Tombola é uma ABC com dois métodos abstratos e dois métodos concretos.
PY
import abc
@abc.abstractmethod
def load(self, iterable): # (2)
"""Add items from an iterable."""
@abc.abstractmethod
def pick(self): # (3)
"""Remove item at random, returning it.
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)
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]
➋ 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.
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
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.
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:
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.
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 .
PY
import random
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
class LottoBlower(Tombola):
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)
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 .
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
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 .
@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')
def loaded(self):
return bool(self) # (6)
def inspect(self):
return tuple(self)
# Tombola.register(TomboList) # (7)
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.
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.
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.
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.
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.
✒️ 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.
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.
T = TypeVar('T') # (1)
class Repeatable(Protocol):
def __mul__(self: T, repeat_count: int) -> T: ... # (2)
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__ .
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() .
@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.
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)
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 .
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')
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'")
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.
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 .
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.
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.
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 .
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.
@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.
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.
@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 .
PYTHON3
import random
from typing import Any, Iterable, TYPE_CHECKING
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.
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
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.
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 .
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__ .
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.
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.
Ó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").
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.
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.
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):
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()
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.
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.
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"""
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!"""
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"""
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.
“ 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.
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).
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.
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?
Figura 9. Esquerda: Sequência de ativação para a chamada leaf1.ping() . Direita: Sequência de ativação para a
chamada leaf1.pong() .
def pong(self):
print(f'{self}.pong() in Root')
def __repr__(self):
cls_name = type(self).__name__
return f'<instance of {cls_name}>'
def pong(self):
print(f'{self}.pong() in A')
super().pong()
def pong(self):
print(f'{self}.pong() in B')
Vejamos agora o efeito da invocação dos métodos ping e pong em uma instância de Leaf (Exemplo 25).
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
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.
PYTHON3
from diamond import A # (1)
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' .
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 .
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
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.
PYTHON3
import collections
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.
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 .
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']
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.
PYTHON
class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
PYTHON3
class ThreadingMixIn:
"""Mixin class to handle each request in a new thread."""
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).
✒️ 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].
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.
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.
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.
Vamos agora discutir algumas boas práticas de herança múltipla e examinar se o Tkinter as segue.
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.
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.
No Python, não há uma maneira formal de declarar uma classe como mixin. Assim, é fortemente recomendado que
seus nomes incluam o sufixo Mixin .
“
“ Uma classe construída principalmente herdando de mixins,
comportamento próprios, é chamada de classe agregada. [177]
sem adicionar estrutura ou
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).
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.
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.
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.
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:
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.
>>> 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
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.
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)
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 .
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:
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.
MISSING = object()
EMPTY_MSG = 'max() arg is an empty sequence'
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.
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 .
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.
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.
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.
PYTHON3
max(1, 2, -3, key=abs) # returns -3
max(['Go', 'Python', 'Rust'], key=len) # returns 'Python'
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 .
PYTHON3
max([1, 2, -3], default=0) # returns 2
max([], default=None) # returns None
As entradas são:
Invocável que recebe um argumento do tipo T e devolve um valor do tipo LT , que implementa
SupportsLessThan
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
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.
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.
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.
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 .
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.
if __name__ == '__main__':
demo()
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.
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 .
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]
PY
AUTHOR_ELEMENT = '<AUTHOR>{}</AUTHOR>'
O Exemplo 9 mostra uma função que interpreta uma str JSON e devolve um BookDict .
PY
def from_json(data: str) -> BookDict:
whatever = json.loads(data) # (1)
return whatever # (2)
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 :
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.
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.
print(not_book) # (3)
print(not_book['flavor']) # (4)
if __name__ == '__main__':
demo()
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:
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 .
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
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()
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
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:
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.
PY
def clip(text: str, max_len: int = 80) -> str:
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.
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:
[…] 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 . […]
⚠️ 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.
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.
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 inspect.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 ...
⚠️ 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.
PY
from generic_lotto import LottoBlower
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 …
Além disso, o Mypy aponta violações do tipo parametrizado com mensagens úteis, como ilustrado no Exemplo 16.
PY
from generic_lotto import LottoBlower
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.
PY
import random
T = TypeVar('T')
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] .
Agora que vimos como implementar um classe genérica, vamos definir a terminologia para falar sobre tipos genéricos.
Tipo genérico
Um tipo declarado com uma ou mais variáveis de tipo.
Exemplos: LottoBlower[T] , abc.Mapping[KT, VT]
Tipo parametrizado
Um tipo declarado com os parâmetros de tipo reais.
Exemplos: LottoBlower[int] , abc.Mapping[str, float]
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]
class Juice(Beverage):
"""Any fruit juice."""
class OrangeJuice(Juice):
"""Delicious juice from Brazilian oranges."""
T = TypeVar('T') # (2)
PY
juice_dispenser = BeverageDispenser(Juice())
install(juice_dispenser)
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 .
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.
PY
T_co = TypeVar('T_co', covariant=True) # (1)
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 .
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)
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:
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.
PY
from typing import TypeVar, Generic
class Biodegradable(Refuse):
"""Biodegradable refuse."""
class Compostable(Biodegradable):
"""Compostable 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] .
PY
bio_can: TrashCan[Biodegradable] = TrashCan()
deploy(bio_can)
PY
compost_can: TrashCan[Compostable] = TrashCan()
deploy(compost_can)
## mypy: Argument 1 to "deploy" has
## incompatible type "TrashCan[Compostable]"
## expected "TrashCan[Biodegradable]"
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.
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]):
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 .
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.
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.
A seguir, vamos ver como definir protocolos estáticos genéricos, aplicando a ideia de covariância a alguns novos
exemplos.
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
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.
class Vector2d(NamedTuple):
x: float
y: float
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')
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 .
PYTHON3
from typing import Protocol, runtime_checkable, TypeVar
@runtime_checkable
class RandomPicker(Protocol[T_co]): # (2)
def pick(self) -> T_co: ... # (3)
1. Declara T_co como covariante .
O protocolo genérico RandomPicker pode ser covariante porque seu único parâmetro formal é usado em um tipo de
saída.
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 é
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
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:
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.
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.
KOTLIN
class TrashCan<in T> {
// etc...
}
Dado T como um parâmetro de tipo formal de input (entrada), segue que TrashCan é contravariante.
É 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)
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.)
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 .
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:
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 * .
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.
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.
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 .
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 .
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')
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.
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.
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])
PYTHON3
# inside the Vector class
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.
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 .
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.
PYTHON3
# inside the Vector class
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.
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.
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
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: * .
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
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.
PYTHON3
class Vector:
typecode = 'd'
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.
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
PYTHON3
class Vector:
# many methods omitted in book listing
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.
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
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
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
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
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.
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.
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
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:
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:
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.
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 .
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
+.
1. Cria uma instância de globe com cinco itens (cada uma das vowels ).
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
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.
PY
from tombola import Tombola
from bingo import BingoCage
2. Nosso __add__ só vai funcionar se o segundo operando for uma instância de Tombola .
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.
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.
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.
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:
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.
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.
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
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
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.
O Exemplo 1 mostra uma classe Sentence que extrai palavras de um texto por índice.
PY
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
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.
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.
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() .
“ 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
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.
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
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.
__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.
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 .
⚠️ 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__".
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']
5. Uma vez exaurido, um itereador irá sempre gerar StopIteration , o que faz parecer que ele está vazio..
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.
PY
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __repr__(self):
return f'Sentence({reprlib.repr(self.text)})'
class SentenceIterator:
def __next__(self):
try:
word = self.words[self.index] # (5)
except IndexError:
raise StopIteration() # (6)
self.index += 1 # (7)
return word # (8)
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__ .
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.
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.
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.
PY
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
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)
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.
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 .
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.
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.
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 __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)
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.
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.
RE_WORD = re.compile(r'\w+')
class Sentence:
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.
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:
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.
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.
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'>)
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 .
PY
class ArithmeticProgression:
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]
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 .
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]
PY
import itertools
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.
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.
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.
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! .
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]
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.
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.
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 .
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
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 .
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 .
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 .
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
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 .
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.
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
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 .
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.
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.
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.
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.
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.
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.
PY
def tree(cls):
yield cls.__name__
def display(cls):
for cls_name in tree(cls):
print(cls_name)
if __name__ == '__main__':
display(BaseException)
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
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)
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)
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 .
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 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.
PY
from collections.abc import Iterable
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] .
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.
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 .
PY
from collections.abc import Iterator
from keyword import kwlist
from typing import TYPE_CHECKING
if TYPE_CHECKING:
reveal_type(short_kw) # (2)
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]
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]
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.
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) .
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ê:
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.
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
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.
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
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.
PY
from collections.abc import Generator
from typing import Union, NamedTuple
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]:
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).
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.
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.
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.
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 .
✒️ 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.
É 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)
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:
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:
“ 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.
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.
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
“ 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 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 .
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.
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).
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.
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__ .
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.
class LookingGlass:
4. Devolve a string 'JABBERWOCKY' , apenas para termos algo para colocar na variável alvo what .
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
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'
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.
👉 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.
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.
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 .
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.
PY
import contextlib
import sys
@contextlib.contextmanager # (1)
def looking_glass():
original_write = sys.stdout.write # (2)
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.
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
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:
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 .
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.
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.
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.
PY
>>> @looking_glass()
... def verse():
... print('The time has come')
...
>>> verse() # (1)
emoc sah emit ehT
>>> print('back to normal') # (2)
back to normal
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.
PYTHON3
import csv
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.
Além de mostrar mais pattern matching, escrevi essa seção por três razões:
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.
A notação usada no Scheme e na maioria dos dialetos de Lisp é conhecida como S-expression (Expressão-S).[241]
SCHEME
(define (mod m n)
(- m (* n (quotient m n))))
(define (gcd m n)
(if (= n 0)
m
(gcd n (mod m n))))
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)
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.
PY
import math
import operator as op
from collections import ChainMap
from itertools import chain
from typing import Any, TypeAlias, NoReturn
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).
PY
def parse(program: str) -> Expression:
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))
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.
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]]]
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.
PY
class Environment(ChainMap[Symbol, Any]):
"A ChainMap that allows changing an item in-place."
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).
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
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]
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))
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.
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]
(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 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 :
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:
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 .
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 .
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.
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.
PYTHON3
case _:
raise SyntaxError(lispstr(exp))
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.
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."
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).
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.
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.
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.
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.
“
“ 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]
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.
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.
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.
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!
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."
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.
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.
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.
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.
“
“ 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.
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.
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.
PY
import itertools
import time
from threading import Thread, Event
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.
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
if __name__ == '__main__':
main()
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 .
Quando a thread main aciona o evento done , a thread spinner acabará notando e encerrando corretamente.
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)
# [snip] the rest of spin and slow functions are unchanged from spinner_thread.py
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 …
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]
Agora vamos ver como o mesmo comportamento pode ser obtido com corrotinas em vez de threads ou processos.
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.
PY
def main() -> None: # (1)
result = asyncio.run(supervisor()) # (2)
print(f'Answer: {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 .
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.
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.
PY
import asyncio
import itertools
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 .
Assistir o comportamento é mais memorável que ler sobre ele. Vai lá, eu espero.
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.
PY
async def slow() -> int:
time.sleep(3) # (4)
return 42
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 .
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.
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
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 :
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.
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.
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 ProcessPoolExecutor
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]
checar:
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.
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.
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.
✒️ 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
Neste exemplo, o tempo total é aproximadamente a soma do tempo de cada verificação, mas está computado
separadamente, como se vê no Exemplo 12.
"""
sequential.py: baseline for comparing sequential, multiprocessing,
and threading code for CPU-intensive work.
"""
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 .
A última linha dos resultados mostra que procs.py foi 4,2 vezes mais rápido que sequential.py.
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.
✒️ 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.
⚠️ 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)
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)
é 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.
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])
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 .
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.
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.
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).
“ 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?
“ 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.
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."
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.
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.
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:
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.
👉 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.
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.
Agora vamos examinar outra forma de evitar a GIL para obter um melhor desempenho em aplicações Python de
servidor.
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.
✒️ 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.
“ 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.
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.
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:
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).
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.
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."
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.
“ 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.
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.
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.
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.
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]
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
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 .
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 .
O Exemplo 3 mostra a forma mais fácil de implementar os downloads de forma concorrente, usando o método
ThreadPoolExecutor.map .
PY
from concurrent import futures
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)
“ 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.
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.
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.
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)
return count
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 .
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.
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.
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 .
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 .
PY
import sys
from concurrent import futures # (1)
from time import perf_counter
from typing import NamedTuple
t0 = perf_counter()
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.
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.
PY
from time import sleep, strftime
from concurrent import futures
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.
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 ThreadPoolExecutor 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.
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
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 .
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.
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.
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
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
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
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
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 )
Essa é a interface de usuário dos exemplos flags2 . Vamos ver como eles estão implementados.
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)
DEFAULT_CONCUR_REQ = 1
MAX_CONCUR_REQ = 1
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]
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.
if error_msg:
status = DownloadStatus.ERROR # (9)
counter[status] += 1 # (10)
if verbose and error_msg: # (11)
print(f'{cc} error: {error_msg}')
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.
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.
PY
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import httpx
import tqdm # type: ignore
DEFAULT_CONCUR_REQ = 30 # (2)
MAX_CONCUR_REQ = 1000 # (3)
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)
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 .
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.
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 .
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.
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.
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.
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.
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.
⚠️ 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.
MAX_KEYWORD_LEN = 4 # (1)
KEYWORDS = sorted(kwlist + softkwlist)
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.
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 .
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.
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.
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.
PY
def download_many(cc_list: list[str]) -> int: # (1)
return asyncio.run(supervisor(cc_list)) # (2)
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.
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 AsyncClient e ClientResponse —como
veremos na seção Seção 21.6.
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.
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)).
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)")
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.
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.
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
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.
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]
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
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 .
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.
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.
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.
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.
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.
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.
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.
PY
loop = asyncio.get_running_loop() # (1)
loop.run_in_executor(None, save_flag, # (2)
image, f'{cc}.gif') # (3)
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.
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 .
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.
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.
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]
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.
PY
from pathlib import Path
from unicodedata import name
init(app) # (5)
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.
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.
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)
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.
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 .
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.
PY
import asyncio
import functools
import sys
from asyncio.trsock import TransportSocket
from typing import cast
CRLF = b'\r\n'
PROMPT = b'?> '
writer.close() # (15)
await writer.wait_closed() # (16)
print(f'Close {client}.') # (17)
6. O StreamWriter.drain esvazia o buffer de writer ; ela é uma corrotina, então precisa ser acionada com await .
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).
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() .
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.
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.
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.
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
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).
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 .
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.
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
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.
PYTHON3
import asyncio
import socket
from collections.abc import Iterable, AsyncIterator
from typing import NamedTuple, Optional
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)
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.
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
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')
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.
@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)
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.
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.
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.
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 .
PYTHON3
#!/usr/bin/env python3
from curio import run, TaskGroup
import curio.socket as socket
from keyword import kwlist
MAX_KEYWORD_LEN = 4
if __name__ == '__main__':
run(main()) # (6)
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.
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]):
...
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 .
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.
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.
L1 cache 3 3 segundos
L2 cache 14 14 segundos
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.
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.
Aqui estão algumas opções para quando você identifica gargalos de uso da CPU:
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.
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.
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.
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).
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.
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.
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).
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.
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" .
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
"""
@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.
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
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.
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
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 __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.
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.
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
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 .
4. Campos do JSON original podem ser acessados como atributos de instância de Record .
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' .
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),
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.
Vamos começar pela propriedade venue . Veja a interação parcial no Exemplo 10.
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
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.
PY
import inspect # (1)
import json
JSON_PATH = 'data/osconfeed.json'
class Record:
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)
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.
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.
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)
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.
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 .
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 .
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.
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):
# 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.
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.
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 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.
Ele não pode ser usado como um substituto direto de @property se o método decorado já
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).
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.
PY
class LineItem:
def subtotal(self):
return self.weight * self.price
Esse código é simples e agradável. Talvez simples demais. Exemplo 20 mostra um problema.
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.
PY
class LineItem:
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 .
5. O getter decorado tem um atributo .setter , que também é um decorador; isso conecta o getter e o setter.
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.
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 subtotal(self):
return self.weight * self.price
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.
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.
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.
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'
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.
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.
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.
PY
class LineItem:
weight = quantity('weight') # (1)
price = quantity('price') # (2)
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.
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].
PY
def quantity(storage_name): # (1)
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.
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.
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.
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]
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}')
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.
__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.
dir([object])
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."
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.
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.
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) .
Isso conclui nosso mergulho nas propriedades, nos métodos especiais e nas outras técnicas de programação de
atributos dnâmicos.
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.
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).
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).
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).