Para Marta, com todo o meu amor.

Prefácio

Eis um plano: se uma pessoa usar um recurso que você não entende, mate-a. É mais fácil que aprender algo novo, e em pouco tempo os únicos programadores sobreviventes usarão apenas um subconjunto minúsculo e fácil de entender de Python 0.9.6 <piscadela marota>.[1]

— Tim Peters
lendário colaborador do CPython e autor do Zen de Python

"Python é uma linguagem fácil de aprender e poderosa." Essas são as primeiras palavras do tutorial oficial de Python 3.10. Isso é verdade, mas há uma pegadinha: como a linguagem é fácil de entender e de começar a usar, muitos programadores praticantes de Python se contentam apenas com uma fração de seus poderosos recursos.

Uma programadora experiente pode começar a escrever código Python útil em questão de horas. Conforme as primeiras horas produtivas se tornam semanas e meses, muitos desenvolvedores continuam escrevendo código Python com um forte sotaque das linguagens que aprenderam antes. Mesmo se Python for sua primeira linguagem, muitas vezes ela é apresentada nas universidades e em livros introdutórios evitando deliberadamente os recursos específicos da linguagem.

Como professor, ensinando Python para programadores experientes em outras linguagens, vejo outro problema: só sentimos falta daquilo que conhecemos. Vindo de outra linguagem, qualquer um é capaz de imaginar que Python suporta expressões regulares, e procurar esse tema na documentação. Mas se você nunca viu desempacotamento de tuplas ou descritores de atributos, talvez nunca procure por eles, e pode acabar não usando esses recursos, só por que são novos para você.

Este livro não é uma referência exaustiva de Python de A a Z. A ênfase está em recursos da linguagem característicos de Python ou incomuns em outras linguagens populares. Vamos nos concentrar principalmente nos aspectos centrais da linguagem e pacotes essenciais da biblioteca padrão. Apenas alguns exemplos mostram o uso de pacotes externos como FastAPI, httpx, e Curio.

Para quem é esse livro

Escrevi este livro para programadores que já usam Python e desejem se tornar fluentes em Python 3 moderno. Testei os exemplos em Python 3.10—e a maioria também em Python 3.9 e 3.8. Os exemplos que exigem especificamente Python 3.10 estão indicados.

Caso não tenha certeza se conhece Python o suficiente para acompanhar o livro, revise o tutorial oficial de Python. Tópicos tratados no tutorial não serão explicados aqui, exceto por alguns recursos mais novos.

Para quem esse livro não é

Se está começando a estudar Python, poderá achar difícil acompanhar este livro. Mais ainda, se você o ler muito cedo em sua jornada pela linguagem, pode ficar com a impressão que todo script Python precisa se valer de métodos especiais e truques de metaprogramação. Abstração prematura é tão ruim quanto otimização prematura.

Para quem está aprendendo a programar, recomendo o livro Pense em Python de Allen Downey, disponível na Web.

Se já sabe programar e está aprendendo Python, o tutorial oficial de Python foi traduzido pela comunidade Python brasileira.

Como ler este livro

Recomendo que todos leiam o Capítulo 1. Após a leitura do capítulo "O modelo de dados de Python", o público principal deste livro não terá problema em pular diretamente para qualquer outra parte, mas muitas vezes assumo que você leu os capítulos precedentes de cada parte específica. Pense nas partes Parte I: Estruturas de dados até a Parte V: Metaprogramação como cinco livros dentro do livro.

Tentei enfatizar o uso de classes e módulos que já existem antes de discutir como criar seus próprios. Por exemplo, na Parte I: Estruturas de dados, o Capítulo 2 trata dos tipos de sequências que estão prontas para serem usadas, incluindo algumas que não recebem muita atenção, como collections.deque. Criar sequências definidas pelo usuário só é discutido na Parte III: Classes e protocolos, onde também vemos como usar as classes base abstratas (ABCs) de collections.abc. Criar suas próprias ABCs é discutido ainda mais tarde, na Parte III: Classes e protocolos, pois acredito na importância de estar confortável usando uma ABC antes de escrever uma.

Essa abordagem tem algumas vantagens. Primeiro, saber o que está disponivel para uso imediato pode evitar que você reinvente a roda. Usamos as classes de coleções existentes com mais frequência que implementamos nossas próprias coleções, e podemos prestar mais atenção ao uso avançado de ferramentas prontas adiando a discussão sobre a criação de novas ferramentas. Também é mais provável herdamos de ABCs existentes que criar uma nova ABC do zero. E, finalmente, acredito ser mais fácil entender as abstrações após vê-las em ação.

A desvantagem dessa estratégia são as referências a pontos futuros espalhadas pelo livro. Acredito que isso é mais fácil de tolerar agora que você sabe porque escolhi esse caminho.

Cinco livros em um

Aqui estão os principais tópicos de cada parte do livro:

Parte I: Estruturas de dados

O Capítulo 1 introduz o Modelo de Dados de Python e explica porque os métodos especiais (por exemplo, __repr__) são a chave do comportamento consistente de objetos de todos os tipos. Os métodos especiais são tratados em maiores detalhes ao longo do livro. Os capítulos restantes dessa parte cobrem o uso de tipos coleção: sequências, mapeamentos e conjuntos, bem como a separação de str e bytes--causa de muitas celebrações entre usuários de Python 3, e de muita dor para usuários de Python 2 obrigados a migrar suas bases de código. Também são abordadas as fábricas de classe de alto nível na biblioteca padrão: fábricas de tuplas nomeadas e o decorador @dataclass. Pattern matching ("casamento de padrões")—novidade no Python 3.10—é tratada em seções do Capítulo 2, do Capítulo 3 e do Capítulo 5, que discutem padrões para sequências, padrões para mapeamentos e padrões para instâncias de classes. O último capítulo na Parte I: Estruturas de dados versa sobre o ciclo de vida dos objetos: referências, mutabilidade e coleta de lixo (garbage collection).

Parte II: Funções como objetos

Aqui falamos sobre funções como objetos de primeira classe na linguagem: o significado disso, como isso afeta alguns padrões de projetos populares e como aproveitar as clausuras para implementar decoradores de função. Também são vistos aqui o conceito geral de invocáveis no Python, atributos de função, introspecção, anotação de parâmetros e a nova declaração nonlocal no Python 3. O Capítulo 8 introduz um novo tópico importante, dicas de tipo em assinaturas de função.

Parte III: Classes e protocolos

Agora o foco se volta para a criação "manual" de classes—em contraste com o uso de fábricas de classe vistas no Capítulo 5. Como qualquer linguagem orientada a objetos, Python tem seu conjunto particular de recursos que podem ou não estar presentes na linguagem na qual você ou eu aprendemos programação baseada em classes. Os capítulos explicam como criar suas próprias coleções, classes base abstratas (ABCs) e protocolos, bem como as formas de lidar com herança múltipla e como implementar a sobrecarga de operadores, quando fizer sentido. O Capítulo 15 continua a conversa sobre dicas de tipo.

Parte IV: Controle de fluxo

Nesta parte são tratados os mecanismos da linguagem e as bibliotecas que vão além do controle de fluxo tradicional, com condicionais, laços e sub-rotinas. Começamos com os geradores, visitamos a seguir os gerenciadores de contexto e as corrotinas, incluindo a desafiadora mas poderosa sintaxe do yield from. O Capítulo 18 inclui um exemplo significativo, usando pattern matching em um interpretador de linguagem simples mas funcional. O Capítulo 19 é novo, apresentando uma visão geral das alternativas para processamento concorrente e paralelo no Python, suas limitações, e como a arquitetura de software permite ao Python operar na escala da Web. Reescrevi o capítulo sobre programação assíncrona, para enfatizar os recursos centrais da linguagem—por exemplo, await, async def, async for e async with, e mostrar como eles são usados com asyncio e outros frameworks.

Parte V: Metaprogramação

Essa parte começa com uma revisão de técnicas para criação de classes com atributos criados dinamicamente para lidar com dados semi-estruturados, tal como conjuntos de dados JSON. A seguir tratamos do mecanismo familiar das propriedades, antes de mergulhar no funcionamento do acesso a atributos de objetos no Python em um nível mais baixo, usando descritores. A relação entre funções, métodos e descritores é explicada. Por toda a Parte V: Metaprogramação, a implementação passo a passo de uma biblioteca de validação de campos revela questões sutis, levando às ferramentas avançadas do capítulo final: decoradores de classes e metaclasses.

Abordagem "mão na massa"

Frequentemente usaremos o console interativo de Python para explorar a linguagem e as bibliotecas. Acho isso importante para enfatizar o poder dessa ferramenta de aprendizagem, especialmente para quem teve mais experiência com linguagens estáticas compiladas, que não oferecem um REPL.[2]

Um dos pacotes padrão de testagem de Python, o doctest, funciona simulando sessões de console e verificando se as expressões resultam nas resposta exibidas. Usei doctest para verificar a maior parte do código desse livro, incluindo as listagens do console. Não é necessário usar ou sequer saber da existência do doctest para acompanhar o texto: a principal característica dos doctests é que eles imitam transcrições de sessões interativas no console de Python, assim qualquer pessoa pode reproduzir as demonstrações facilmente.

Algumas vezes vou explicar o que queremos realizar mostrando um doctest antes do código que implementa a solução. Estabelecer precisamente o quê deve ser feito, antes de pensar sobre como fazer, ajuda a focalizar nosso esforço de codificação. Escrever os testes previamente é a base de desenvolvimento dirigido por testes (TDD, test-driven development), e também acho essa técnica útil para ensinar.

Também escrevi testes de unidade para alguns dos exemplos maiores usando pytest—que acho mais fácil de usar e mais poderoso que o módulo unittest da bibliotexa padrão. Você vai descobrir que pode verificar a maior parte do código do livro digitando python3 -m doctest example_script.py ou pytest no console de seu sistema operacional. A configuração do pytest.ini, na raiz do repositório do código de exemplo, assegura que doctests são coletados e executados pelo comando pytest.

Ponto de vista: minha perspectiva pessoal

Venho usando, ensinando e debatendo Python desde 1998, e gosto de estudar e comparar linguagens de programação, seus projetos e a teoria por trás delas. Ao final de alguns capítulos acrescentei uma seção "Ponto de vista", apresentando minha perspectiva sobre Python e outras linguagens. Você pode pular essas partes, se não tiver interesse em tais discussões. Seu conteúdo é inteiramente opcional.

Conteúdo na na Web

Criei dois sites para este livro:

https://pythonfluente.com

O texto integral em português traduzido por Paulo Candido de Oliveira Filho. É que você está lendo agora.

https://fluentpython.com

Contém textos em inglês para ambas edições do livro, além de um glossário. É um material que eu cortei para não ultrapassar o limite de 1.000 páginas.

O repositório de exemplos de código está no GitHub.

Convenções usadas no livro

As seguintes convenções tipográficas são usadas neste livro:

Itálico

Indica novos termos, URLs, endereços de email, nomes e extensões de arquivos [3].

Espaçamento constante

Usado para listagens de programas, bem como dentro de parágrafos para indicar elementos programáticos tais como nomes de variáveis ou funções, bancos de dados, tipos de dados, variáveis do ambiente, instruções e palavras-chave.

Observe que quando uma quebra de linha cai dentro de um termo de espaçamento constante, o hífen não é utilizado—​pois ele poderia ser erroneamente entendido como parte do termo.

Espaçamento constante em negrito

Mostra comandos ou outro texto que devem ser digitados literalmente pelo usuário.

Espaçamento constante em itálico

Mostra texto que deve ser substituído por valores fornecidos pelo usuário ou por valores determinados pelo contexto.

👉 Dica

Esse elemento é uma dica ou sugestão.

✒️ Nota

Este elemento é uma nota ou observação.

⚠️ Aviso

Este elemento é um aviso ou alerta.

Usando os exemplos de código

Todos os scripts e a maior parte dos trechos de código que aparecem no livro estão disponíveis no repositório de código de Python Fluente, no GitHub.

Se você tiver uma questão técnica ou algum problema para usar o código, por favor mande um email para .

Esse livro existe para ajudar você a fazer seu trabalho. Em geral, se o código exemplo está no livro, você pode usá-lo em seus programas e na sua documentação. Não é necessário nos contactar para pedir permissão, a menos que você queira reproduzir uma parte significativa do código. Por exemplo, escrever um programa usando vários pedaços de código deste livro não exige permissão. Vender ou distribuir exemplos de livros da O’Reilly exige permissão. Responder uma pergunta citando este livro e código exemplo daqui não exige permissão. Incorporar uma parte significativa do código exemplo do livro na documentação de seu produto exige permissão.

Gostamos, mas em geral não exigimos, atribuição da fonte. Isto normalmente inclui o título, o autor, a editora e o ISBN. Por exemplo, “Python Fluente, 2ª ed., de Luciano Ramalho. Copyright 2022 Luciano Ramalho, 978-1-492-05635-5.”

Se você achar que seu uso dos exemplo de código está fora daquilo previsto na lei ou das permissões dadas acima, por favor entre em contato com .

O’Reilly Online Learning

✒️ Nota

Por mais de 40 anos, O’Reilly Media tem oferecido treinamento, conhecimento e ideias sobre tecnologia e negócios, ajudando empresas serem bem sucedidas.

Nossa rede sem igual de especialistas e inovadores compartilha conhecimento e sabedoria através de livros, artigos e de nossa plataforma online de aprendizagem. A plataforma de aprendizagem online da O’Reilly’s oferece acesso sob demanda a treinamentos ao vivo, trilhas de aprendizagem profunda, ambientes interativos de programação e uma imensa coleção de textos e vídeos da O’Reilly e de mais de 200 outras editoras. Para maiores informações, visite http://oreilly.com.

Como entrar em contato

Por favor, envie comentários e perguntas sobre esse livro para o editor:

  • O’Reilly Media, Inc.
  • 1005 Gravenstein Highway North
  • Sebastopol, CA 95472
  • 800-998-9938 (in the United States or Canada)
  • 707-829-0515 (international or local)
  • 707-829-0104 (fax)

Há uma página online para este livro, com erratas, exemplos e informação adicional, que pode ser acessada aqui: https://fpy.li/p-4.

Envie email para , com comentários ou dúvidas técnicas sobre o livro.

Novidades e informações sobre nossos livros e cursos podem ser encontradas em http://oreilly.com.

Agradecimentos

Eu não esperava que atualizar um livro de Python cinco anos depois fosse um empreendimento de tal magnitude. Mas foi. Marta Mello, minha amada esposa, sempre esteve ao meu lado quando precisei. Meu querido amigo Leonardo Rochael me ajudou desde os primeiros rascunhos até a revisão técnica final, incluindo consolidar e revisar as sugestões dos outros revisores técnicos, de leitores e de editores. Honestamente, não sei se teria conseguido sem seu apoio, Marta e Leo. Muito, muito grato!

Jürgen Gmach, Caleb Hattingh, Jess Males, Leonardo Rochael e Miroslav Šedivý formaram a fantástica equipe de revisores técnicos da segunda edição. Eles revisaram o livro inteiro. Bill Behrman, Bruce Eckel, Renato Oliveira e Rodrigo Bernardo Pimentel revisaram capítulos específicos. Suas inúmeras sugestões, vindas de diferentes perspectivas, tornaram o livro muito melhor.

Muitos leitores me enviaram correções ou fizeram outras contribuições durante o pré-lançamento, incluindo: Guilherme Alves, Christiano Anderson, Konstantin Baikov, K. Alex Birch, Michael Boesl, Lucas Brunialti, Sergio Cortez, Gino Crecco, Chukwuerika Dike, Juan Esteras, Federico Fissore, Will Frey, Tim Gates, Alexander Hagerman, Chen Hanxiao, Sam Hyeong, Simon Ilincev, Parag Kalra, Tim King, David Kwast, Tina Lapine, Wanpeng Li, Guto Maia, Scott Martindale, Mark Meyer, Andy McFarland, Chad McIntire, Diego Rabatone Oliveira, Francesco Piccoli, Meredith Rawls, Michael Robinson, Federico Tula Rovaletti, Tushar Sadhwani, Arthur Constantino Scardua, Randal L. Schwartz, Avichai Sefati, Guannan Shen, William Simpson, Vivek Vashist, Jerry Zhang, Paul Zuradzki—e outros que pediram para não ter seus nomes mencionados, enviaram correções após a entrega da versão inicial ou foram omitidos porque eu não registrei seus nomes—mil desculpas.

Durante minha pesquisa, aprendi sobre tipagem, concorrência, pattern matching e metaprogramação interagindo com Michael Albert, Pablo Aguilar, Kaleb Barrett, David Beazley, J. S. O. Bueno, Bruce Eckel, Martin Fowler, Ivan Levkivskyi, Alex Martelli, Peter Norvig, Sebastian Rittau, Guido van Rossum, Carol Willing e Jelle Zijlstra.

Os editores da O’Reilly Jeff Bleiel, Jill Leonard e Amelia Blevins fizeram sugestões que melhoraram o fluxo do texto em muitas partes. Jeff Bleiel e o editor de produção Danny Elfanbaum me apoiaram durante essa longa maratona.

As ideias e sugestões de cada um deles tornaram o livro melhor e mais preciso. Inevitavelmente, vão restar erros de minha própria criação no produto final. Me desculpo antecipadamente.

Por fim gostaria de estender meus sinceros agradecimento a meus colegas na Thoughtworks Brasil—e especialmente a meu mentor, Alexey Bôas—que apoiou este projeto de muitas formas até o fim.

Claro, todos os que me ajudaram a entender Python e a escrever a primeira edição merecem agora agradecimentos em dobro. Não haveria segunda edição sem o sucesso da primeira.

Agradecimentos da primeira edição

O tabuleiro e as peças de xadrez Bauhaus, criadas por Josef Hartwig, são um exemplo de um excelente design: belo, simples e claro. Guido van Rossum, filho de um arquiteto e irmão de projetista de fonte magistral, criou um obra prima de design de linguagens. Adoro ensinar Python porque ele é belo, simples e claro.

Alex Martelli e Anna Ravenscroft foram os primeiros a verem o esquema desse livro, e me encorajaram a submetê-lo à O’Reilly para publicação. Seus livros me ensinaram Python idiomático e são modelos de clareza, precisão e profundidade em escrita técnica. Os 6,200+ posts de Alex no Stack Overflow (EN) são uma fonte de boas ideias sobre a linguagem e seu uso apropriado.

Martelli e Ravenscroft foram também revisores técnicos deste livro, juntamente com Lennart Regebro e Leonardo Rochael. Todos nesta proeminente equipe de revisão técnica têm pelo menos 15 anos de experiência com Python, com muitas contribuições a projetos Python de alto impacto, em contato constante com outros desenvolvedores da comunidade. Em conjunto, eles me enviaram centenas de correções, sugestões, questões e opiniões, acrescentando imenso valor ao livro. Victor Stinner gentilmente revisou o Capítulo 21, trazendo seu conhecimento especializado, como um dos mantenedores do asyncio, para a equipe de revisão técnica. Foi um grande privilégio e um prazer colaborar com eles por estes muitos meses.

A editora Meghan Blanchette foi uma fantástica mentora, e me ajudou a melhorar a organização e o fluxo do texto do livro, me mostrando que partes estavam monótonas e evitando que eu atrasasse o projeto ainda mais. Brian MacDonald editou os capítulo na Parte II: Funções como objetos quando Meghan estava ausente. Adorei trabalhar com eles e com todos na O’Reilly, incluindo a equipe de suporte e desenvolvimento do Atlas (Atlas é a plataforma de publicação de livros da O’Reilly, que eu tive a felicidade de usar para escrever esse livro).

Mario Domenech Goulart deu sugestões numerosas e detalhadas, desde a primeira versão do livro. Também recebi muitas sugestões e comentários de Dave Pawson, Elias Dorneles, Leonardo Alexandre Ferreira Leite, Bruce Eckel, J. S. Bueno, Rafael Gonçalves, Alex Chiaranda, Guto Maia, Lucas Vido e Lucas Brunialti.

Ao longo dos anos, muitas pessoas me encorajaram a me tornar um autor, mas os mais persuasivos foram Rubens Prates, Aurelio Jargas, Rudá Moura e Rubens Altimari. Mauricio Bussab me abriu muitas portas, incluindo minha primeira experiência real na escrita de um livro. Renzo Nuccitelli apoiou este projeto de escrita o tempo todo, mesmo quando significou iniciar mais lentamente nossa parceria no python.pro.br.

A maravilhosa comunidade brasileira de Python é inteligente, generosa e divertida. O The Python Brasil group tem milhares de membros, e nossas conferências nacionais reúnem centenas de pessoas. Mas os mais influemtes em minha jornada como pythonista foram Leonardo Rochael, Adriano Petrich, Daniel Vainsencher, Rodrigo RBP Pimentel, Bruno Gola, Leonardo Santagada, Jean Ferri, Rodrigo Senra, J. S. Bueno, David Kwast, Luiz Irber, Osvaldo Santana, Fernando Masanori, Henrique Bastos, Gustavo Niemayer, Pedro Werneck, Gustavo Barbieri, Lalo Martins, Danilo Bellini, e Pedro Kroger.

Dorneles Tremea foi um grande amigo, (e incrivelmente generoso com seu tempo e seu conhecimento), um hacker fantástico e o mais inspirador líder da Associação Python Brasil. Ele nos deixou cedo demais.

Meus estudantes, ao longo desses anos, me ensinaram muito através de suas perguntas, ideias, feedbacks e soluções criativas para problemas. Érico Andrei e a Simples Consultoria tornaram possível que eu me concentrasse em ser um professor de Python pela primeira vez.

Martijn Faassen foi meu mentor de Grok e compartilhou ideias valiosas sobre Python e os neandertais. Seu trabalho e o de Paul Everitt, Chris McDonough, Tres Seaver, Jim Fulton, Shane Hathaway, Lennart Regebro, Alan Runyan, Alexander Limi, Martijn Pieters, Godefroid Chapelle e outros, dos planetas Zope, Plone e Pyramid, foram decisivos para minha carreira. Graças ao Zope e a surfar na primeira onda da web, pude começar a ganhar a vida com Python em 1998. José Octavio Castro Neves foi meu sócio na primeira software house baseada em Python do Brasil.

Tenho gurus demais na comunidade Python como um todo para listar todos aqui, mas além daqueles já mencionados, eu tenho uma dívida com Steve Holden, Raymond Hettinger, A.M. Kuchling, David Beazley, Fredrik Lundh, Doug Hellmann, Nick Coghlan, Mark Pilgrim, Martijn Pieters, Bruce Eckel, Michele Simionato, Wesley Chun, Brandon Craig Rhodes, Philip Guo, Daniel Greenfeld, Audrey Roy e Brett Slatkin, por me ensinarem novas e melhores formas de ensinar Python.

A maior parte dessas páginas foi escrita no meu home office e em dois laboratórios: o CoffeeLab e o Garoa Hacker Clube. O CoffeeLab é o quartel general dos geeks cafeinados na Vila Madalena, em São Paulo, Brasil. O Garoa Hacker Clube é um espaço hacker aberto a todos: um laboratório comunitário onde qualquer um é livre para tentar novas ideias.

A comunidade Garoa me forneceu inspiração, infraestrutura e distração. Acho que Aleph gostaria desse liro.

Minha mãe, Maria Lucia, e meu pai, Jairo, sempre me apoiaram de todas as formas. Gostaria que ele estivesse aqui para ver esse livro; e fico feliz de poder compartilhá-lo com ela.

Minha esposa, Marta Mello, suportou 15 meses de um marido que estava sempre trabalhando, mas continuou me apoiando e me guiando através dos momentos mais críticos do projeto, quando temi que poderia abandonar a maratona.

Agradeço a todos vocês, por tudo.

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. Os arquivos-fonte em formato Asciidoc estão no repositório público https://github.com/pythonfluente/pythonfluente2e.

Enquanto publicávamos a tradução ao longo de 2023, muitas correções foram enviadas por leitores como issues (defeitos) ou pull requests (correções) no repositório. Agradeceço a todas as pessoas que colaboraram!

✒️ Nota

Correções e sugestões de melhorias são bem vindas! Para contribuir, veja os issues no repositório https://github.com/pythonfluente/pythonfluente2e.

Contamos com sua colaboração. 🙏

Histórico das traduções

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

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

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

  1. inglês,

  2. português brasileiro,

  3. chinês simplificado (China),

  4. chinês tradicional (Taiwan),

  5. japonês,

  6. coreano,

  7. russo,

  8. francês,

  9. polonês.

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

Entre 2020 e 2022, atualizei e expandi bastante o livro para a segunda edição. Sou muito grato à liderança da Thoughtworks Brasil por terem me apoiado enquanto passei a maior parte de 2020 e 2021 pesquisando, escrevendo, e revisando esta edição.

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. 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 e publicando em https://PythonFluente.com. Estamos trabalhando diretamente a partir do Fluent Python, Second Edition da O’Reilly, sem aproveitar a tradução da primeira edição, cujo copyright pertence à Novatec.

O copyright desta tradução pertence a mim.

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

Parte I: Estruturas de dados

1. O modelo de dados de Python

O senso estético de Guido para o design de linguagens é incrível. Conheci muitos projetistas capazes de criar linguagens teoricamente lindas, que ninguém jamais usaria. Mas Guido é uma daquelas raras pessoas capaz de criar uma linguagem só um pouco menos teoricamente linda que, por isso mesmo, é uma delícia para programar. [4]

— Jim Hugunin
criador do Jython, co-criador do AspectJ, e arquiteto do .Net DLR—Dynamic Language Runtime

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

Entretanto, se você aprendeu outra linguagem orientada a objetos antes de Python, pode achar estranho usar len(collection) em vez de collection.len(). Essa aparente esquisitice é a ponta de um iceberg que, quando bem compreendido, é a chave para tudo aquilo que chamamos de pythônico. O iceberg se chama o Modelo de Dados de 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 de Python na forma de um 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 um framework, passamos um bom tempo programando métodos que são chamados pelo framework, e não pelas nossas classes. O mesmo acontece quando nos valemos do Modelo de Dados de Python para criar novas classes. O interpretador de Python invoca métodos especiais para realizar operações básicas sobre os objetos, muitas vezes acionadas por uma sintaxe especial. Os nomes dos métodos especiais são sempre precedidos e seguidos de dois sublinhados. Por exemplo, a sintaxe obj[key] está amparada no método especial __getitem__. Para resolver my_collection[key], o interpretador chama my_collection.__getitem__(key).

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

  • Coleções

  • Acesso a atributos

  • Iteração (incluindo iteração assíncrona com async for)

  • Sobrecarga (overloading) de operadores

  • Invocação de funções e métodos

  • Representação e formatação de strings

  • Programação assíncrona usando await

  • Criação e destruição de objetos

  • Contextos gerenciados usando as instruções with ou async with

✒️ Nota
Mágica e o "dunder"

O termo método mágico é uma gíria usada para se referir aos métodos especiais, mas como falamos de um método específico, por exemplo __getitem__? Aprendi a dizer "dunder-getitem" com o autor e professor Steve Holden. "Dunder" é uma contração da frase em inglês "double underscore before and after" (sublinhado duplo antes e depois). Por isso os métodos especiais são também conhecidos como métodos dunder. O capítulo "Análise Léxica" de A Referência da Linguagem Python adverte: "Qualquer uso de nomes no formato __*__ que não siga explicitamente o uso documentado, em qualquer contexto, está sujeito a quebra sem aviso prévio."

1.1. Novidades nesse capítulo

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

  • Métodos especiais que suportam programação assíncrona e outras novas funcionalidades foram acrescentados às tabelas em Seção 1.4.

  • A Figura 2, mostrando o uso de métodos especiais em Seção 1.3.4, incluindo a classe base abstrata collections.abc.Collection, introduzida n Python 3.6.

Além disso, aqui e por toda essa segunda edição, adotei a sintaxe f-string, introduzida n 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 %.

👉 Dica

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 exemplo, quando my_fmt tem múltiplas linhas e é melhor definida em uma constante, ou quando tem de vir de um arquivo de configuração ou de um banco de dados. Essas são necessidades reais, mas não acontecem com frequência.

1.2. Um baralho pythônico

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

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

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

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

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

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

    def __getitem__(self, position):
        return self._cards[position]

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

>>> 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 de Python, uma instância de FrenchDeck responde à função len(), devolvendo o número de cartas naquele baralho:

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

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

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

  • Os usuários de suas classes não precisam memorizar nomes arbitrários de métodos para operações comuns ("Como obter o número de itens? É .size(), .length() ou outra coisa?")

  • É mais fácil de aproveitar a rica biblioteca padrão de Python e evitar reinventar a roda, como no caso da função random.choice.

Mas não é só isso.

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 baralho, e depois pegar só os ases, iniciando com o índice 12 e pulando 13 cartas por vez:

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

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

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

>>> for card in reversed(deck):  # doctest: +ELLIPSIS
...   print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...
✒️ Nota
Reticências nos doctests

Sempre que possível, extraí as listagens do console de Python usadas neste livro com o doctest, para garantir a precisão. Quando a saída era longa demais, a parte omitida está marcada por reticências (…​), como na última linha do trecho de código anterior.

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

A iteração muitas vezes é implícita. Python invoca o método __contains__ da coleção para tratar o operador in: student in team. Mas se a coleção não fornece 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:

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

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:

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

✒️ Nota
E como embaralhar as cartas?

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

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

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

Mas o interpretador pega um atalho quando está lidando com um tipo embutido como list, str, bytearray, ou extensões compiladas como os arrays do NumPy. As coleções de tamanho variável de Python escritas em C incluem uma struct[5] chamada PyVarObject, com um campo ob_size que mantém o número de itens na coleção. Então, se my_object é uma instância de algum daqueles tipos embutidos, len(my_object) lê o valor do campo ob_size, e isso é muito mais rápido que chamar um método.

Na maior parte das vezes, a chamada a um método especial é implícita. Por exemplo, o comando for i in x: na verdade gera uma invocação de iter(x), que por sua vez pode chamar x.__iter__() se esse método estiver disponível, ou usar x.__getitem__(), como no exemplo do FrenchDeck.

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

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

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

  • Emular tipos numéricos

  • Representar objetos na forma de strings

  • Determinar o valor booleano de um objeto

  • Implementar coleções

1.3.1. Emulando tipos numéricos

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

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

👉 Dica

O tipo embutido complex pode ser usado para representar vetores bi-dimensionais, mas nossa classe pode ser estendida para representar vetores n-dimensionais. Faremos isso em Capítulo 17.

vetores 2D
Figura 1. Soma de vetores bi-dimensionais; Vector(2, 4) + Vector(2, 1) devolve 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:

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

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

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

>>> 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):

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

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

Exemplo 2. Uma classe simples para representar um vetor 2D.
"""
vector2d.py: a simplistic class demonstrating some special methods

It is simplistic for didactic reasons. It lacks proper error handling,
especially in the ``__add__`` and ``__mul__`` methods.

This example is greatly expanded later in the book.

Addition::

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

Absolute value::

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

Scalar multiplication::

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

"""


import math

class Vector:

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

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

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

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

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

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

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

O Exemplo 2 implementa dois operadores: + e *, para demonstrar o uso básico de __add__ e __mul__. No dois casos, os métodos criam e devolvem uma nova instância de Vector, e não modificam nenhum dos operandos: self e other são apenas lidos. Esse é o comportamento esperado de operadores infixos: criar novos objetos e não tocar em seus operandos. Vou falar muito mais sobre esse tópico no Capítulo 16.

⚠️ Aviso

Da forma como está implementado, o Exemplo 2 permite multiplicar um Vector por um número, 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 16.

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

1.3.2. Representação de strings

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

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

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

  • Pelo sinalizador de conversão !r na nova sintaxe de strings de formato usada nas f-strings e no método str.format. Ex: f'{my_obj!r}'

Note que a f-string no nosso __repr__ usa !r para obter a representação padrão dos atributos a serem exibidos. Isso é uma boa prática, pois durante uma seção de depuração podemos ver a diferença entre Vector(1, 2) e Vector('1', '2'). Este segundo objeto não funcionaria no contexto desse exemplo, porque o construtor espera que os argumentos 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 de __str__ herdada da classe object já invoca __repr__. O Exemplo 66 é um dos muitos exemplos neste livro com um __str__ personalizado.

👉 Dica

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

"What is the difference between __str__ and __repr__ in Python?" (Qual a diferença entre __str__ e __repr__ em Python?) (EN) é uma questão no Stack Overflow com excelentes contribuições dos pythonistas Alex Martelli e Martijn Pieters.

1.3.3. O valor booleano de um tipo personalizado

Apesar de Python ter um tipo bool, ele aceita qualquer objeto em um contexto booleano, tal como as expressões controlando instruções if ou while, ou como operandos de and, or e not. Para determinar se um valor x é verdadeiro ou falso, Python invoca bool(x), que devolve somente True ou False.

Por default, instâncias de classes definidas pelo usuário são consideradas verdadeiras, a menos que __bool__ ou __len__ estejam implementadas. Basicamente, bool(x) chama x.__bool__() e usa o resultado. Se __bool__ não está implementado, Python tenta invocar x.__len__(), e se esse último devolver zero, bool devolve False. Caso contrário, bool devolve True.

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

Observe que o método especial __bool__ permite que seus objetos sigam as regras de teste do valor verdade definidas no capítulo "Tipos Embutidos" da documentação da Biblioteca Padrão de Python.

✒️ Nota

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

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

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

1.3.4. A API de Collection

A Figura 2 documenta as interfaces dos tipos de coleções essenciais na linguagem. Todas as classes no diagrama são ABCs—classes base abstratas (ABC é sigla para Abstract Base Class). As ABCs e o módulo collections.abc são tratados no Capítulo 13. O objetivo dessa pequena seção é dar uma visão panorâmica das interfaces das coleções mais importantes de Python, mostrando como elas são criadas a partir de métodos especiais.

Diagram de classes UML com todas as superclasses e algumas subclasses de `abc.Collection`
Figura 2. Diagrama de classes UML com os tipos fundamentais de coleções. Métodos como nome em itálico são abstratos, então precisam ser implementados pelas subclasses concretas, tais como list e dict. O restante dos métodos tem implementações concretas, então as subclasses podem herdá-los.

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

  • Iterable, para suportar for, desempacotamento, e outras formas de iteração

  • Sized para suportar a função embutida len

  • Container para suportar o operador in

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

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

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

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

  • Set, a interface dos tipos embutidos set e frozenset

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

✒️ Nota

Desde Python 3.7, o tipo dict é oficialmente "ordenado", mas isso só quer dizer que a ordem de 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 de Python.

1.4. Visão geral dos métodos especiais

O capítulo "Modelo de Dados" de A Referência da Linguagem Python lista mais de 80 nomes de métodos especiais. Mais da metade deles implementa operadores aritméticos, de comparação, ou bit-a-bit. Para ter uma visão geral do que está disponível, veja tabelas a seguir.

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

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

Representação de string/bytes

__repr__ __str__ __format__ __bytes__ __fspath__

Conversão para número

__bool__ __complex__ __int__ __float__ __hash__ __index__

Emulação de coleções

__len__ __getitem__ __setitem__ __delitem__ __contains__

Iteração

__iter__ __aiter__ __next__ __anext__ __reversed__

Execução de chamável ou corrotina

__call__ __await__

Gerenciamento de contexto

__enter__ __exit__ __aexit__ __aenter__

Criação e destruição de instâncias

__new__ __init__ __del__

Gerenciamento de atributos

__getattr__ __getattribute__ __setattr__ __delattr__ __dir__

Descritores de atributos

__get__ __set__ __delete__ __set_name__

Classes base abstratas

__instancecheck__ __subclasscheck__

Metaprogramação de classes

__prepare__ __init_subclass__ __class_getitem__ __mro_entries__

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

Tabela 2. Nomes e símbolos de métodos especiais para operadores
Categoria do operador Símbolos Nomes de métodos

Unário numérico

- + abs()

__neg__ __pos__ __abs__

Comparação rica

< <= == != > >=

__lt__ __le__ __eq__ __ne__ __gt__ __ge__

Aritmético

+ - * / // % @ divmod() round() ** pow()

__add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __matmul__ __divmod__ __round__ __pow__

Aritmética reversa

operadores aritméticos com operandos invertidos)

__radd__ __rsub__ __rmul__ __rtruediv__ __rfloordiv__ __rmod__ __rmatmul__ __rdivmod__ __rpow__

Atribuição aritmética aumentada

+= -= *= /= //= %= @= **=

__iadd__ __isub__ __imul__ __itruediv__ __ifloordiv__ __imod__ __imatmul__ __ipow__

Bit a bit

& | ^ << >> ~

__and__ __or__ __xor__ __lshift__ __rshift__ __invert__

Bit a bit reversa

(operadores bit a bit com os operandos invertidos)

__rand__ __ror__ __rxor__ __rlshift__ __rrshift__

Atribuição bit a bit aumentada

&= |= ^= <⇐ >>=

__iand__ __ior__ __ixor__ __ilshift__ __irshift__

✒️ Nota
 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 atalho combinando um operador infixo com uma atribuição de variável, por exemplo `a += b`.

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

1.5. Por que len não é um método?

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

Em outras palavras, len não é chamado como um método porque recebe um tratamento especial como parte do Modelo de Dados de 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 de Python": "Casos especiais não são especiais o bastante para quebrar as regras."

✒️ Nota

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 orientada a objetos. De fato, a linguagem ABC—uma ancestral direta de Python, que antecipou muitas das funcionalidades desta última—tinha o operador #, que era o equivalente de len (se escrevia #s). Quando usado como operador infixo, x#s contava as ocorrências de x em s, que em Python obtemos com s.count(x), para qualquer sequência s.

1.6. Resumo do capítulo

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

Uma exigência básica para um objeto em 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 2. Como implementar suas próprias sequências será visto na Capítulo 12, onde criaremos uma extensão multidimensional da classe Vector.

Graças à sobrecarga de operadores, 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 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 de Python ao longo deste livro.

1.7. Para saber mais

O capítulo "Modelo de Dados" em A Referência da Linguagem Python é a fonte canônica para o assunto desse capítulo e de uma boa parte deste livro.

Python in a Nutshell, 3rd ed. (EN), de Alex Martelli, Anna Ravenscroft, e Steve Holden (O’Reilly) tem uma excelente cobertura do modelo de dados. Sua descrição da mecânica de acesso a atributos é a mais competente que já vi, perdendo apenas para o próprio código-fonte em C do CPython. Martelli também é um contribuidor prolífico do Stack Overflow, com mais de 6200 respostas publicadas. Veja seu perfil de usuário no Stack Overflow.

David Beazley tem dois livros tratando do modelo de dados em detalhes, no contexto de Python 3: Python Essential Reference (EN), 4th ed. (Addison-Wesley), e Python Cookbook, 3rd ed (EN) (O’Reilly), colaborando com Brian K. Jones.

The Art of the 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 de Python é um exemplo.

Ponto de Vista

Modelo de dados ou modelo de objetos?

Aquilo que a documentação de Python chama de "Modelo de Dados de Python", a maioria dos autores diria que é o "Modelo de objetos de 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 de Python, mas se referem a ele como o "modelo de objetos." Na Wikipedia, a primeira definição de "modelo de objetos" (EN) é: "as propriedades dos objetos em geral em uma linguagem de programação de computadores específica." É disso que o Modelo de Dados de Python trata. Neste livro, usarei "modelo de dados" porque esse é o título do capítulo de A Referência da Linguagem Python mais relevante para nossas discussões.

Métodos de "trouxas"

The Original Hacker’s Dictionary (Dicionário Hacker Original) (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."

Ruby tem o equivalente aos métodos especiais, chamados de métodos mágicos naquela comunidade. Alguns na comunidade Python também adotam esse termo. Acredito que os métodos especiais são o contrário de mágica. Python e 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 mantenedores que escrevem os interpretadores daquelas linguagens.

Por outro lado, pense em 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 de 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, no ano 2021 Go não tem ainda o conceito de uma interface iterável ou um objeto iterador acessível ao 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 de 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 de 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 é mais fácil de implementar em uma linguagem dinâmica com Python, e alguns frameworks fazem exatamente isso. O exemplo mais importante é a zope.interface (EN), parte do framework sobre a qual o sistema de gerenciamento de conteúdo Plone é 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 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 "pythônicas": operações genéricas com diferentes tipos de sequências, tipos tupla e mapeamento embutidos, estrutura de blocos por indentação, tipagem forte sem declaração de variáveis, entre outras. Python não é assim tão amigável por acidente.

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 aproveitem bem os tipos de sequências existentes e futuras.

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

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

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

  • O uso de tuplas como registros versus o uso de tuplas como listas imutáveis

  • Desempacotamento de sequências e padrões de sequências.

  • Lendo de fatias e escrevendo em fatias

  • Tipos especializados de sequências, tais como arrays e filas

2.1. Novidades neste capítulo

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

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

  • Um novo diagrama e uma nova descrição do funcionamento interno das sequências, contrastando contêineres e sequências planas.

  • Uma comparação entre list e tuple quanto ao desempenho e ao armazenamento.

  • Ressalvas sobre tuplas com elementos mutáveis, e como detectá-los se necessário.

Movi a discussão sobre tuplas nomeadas para a Seção 5.3 no Capítulo 5, onde elas são comparadas com typing.NamedTuple e @dataclass.

✒️ Nota

Para abrir espaço para conteúdo novo mantendo o número de páginas dentro do razoável, a seção "Managing Ordered Sequences with Bisect" ("Gerenciando sequências ordenadas com bisect") da primeira edição agora é um artigo (EN) no site que complementa o livro, fluentpython.com.

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

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

Sequências contêiner

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

Sequências planas

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

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

Diagrama de memória simplificado de um `array` e de uma `tuple`
Figura 3. 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.

✒️ Nota

Todo objeto Python na memória tem um cabeçalho com metadados. O objeto Python mais simples, um float, tem um campo de valor e dois campos de metadados:

  • ob_refcnt: a contagem de referências ao objeto

  • ob_type: um ponteiro para o tipo do objeto

  • ob_fval: um double de C mantendo o valor do float

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

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

Sequências mutáveis

Por exemplo, list, bytearray, array.array e collections.deque.

Sequências imutáveis

Por exemplo, tuple, str, e bytes.

A Figura 4 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 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:

>>> from collections import abc
>>> issubclass(tuple, abc.Sequence)
True
>>> issubclass(list, abc.MutableSequence)
True
Diagrama de classe UML para `Sequence` e `MutableSequence`
Figura 4. Diagrama de classe UML simplificado para algumas classes de collections.abc (as superclasses estão à esquerda; as setas de herança apontam das subclasses para as superclasses; nomes em itálico indicam classes e métodos abstratos).

Lembre-se dessas características básicas: mutável versus imutável; contêiner versus plana. Elas ajudam a extrapolar o que se sabe sobre um tipo de sequência para outros tipos.

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

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

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

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

👉 Dica

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

2.3.1. Compreensões de lista e legibilidade

Em sua opinião qual desses exemplos é mais fácil de ler, o Exemplo 3 ou o Exemplo 4?

Exemplo 3. Cria uma lista de pontos de código Unicode a partir de uma string
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
...     codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
Exemplo 4. Cria uma lista de pontos de código Unicode a partir de uma string, usando uma listcomp
>>> 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 3. Entretanto, após aprender sobre as listcomps, acho o Exemplo 4 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 3 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
Dica de sintaxe

Em 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 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, é uma gentileza deixar uma vírgula após o último item. Isso torna um pouco mais fácil ao próximo programador acrescentar mais um item àquela lista, e reduz o ruído quando se lê os diffs.

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

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

>>> x = 'ABC'
>>> codes = [ord(x) for x in x]
>>> x  (1)
'ABC'
>>> codes
[65, 66, 67]
>>> codes = [last := ord(c) for c in x]
>>> last  (2)
67
>>> c  (3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined
  1. x não foi sobrescrito: continua vinculado a 'ABC'.

  2. last permanece.

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

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

2.3.2. Listcomps versus map e filter

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

Considere o Exemplo 5.

Exemplo 5. A mesma lista, criada por uma listcomp e por uma composição de map/filter
>>> 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 no repositório de código de Python Fluente é um teste de desempenho simples, comparando listcomp com filter/map.

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

2.3.3. Produtos cartesianos

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

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

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

Exemplo 6. Produto cartesiano usando uma compreensão de lista
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]  (1)
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
 ('white', 'M'), ('white', 'L')]
>>> for color in colors:  (2)
...     for size in sizes:
...         print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes      (3)
...                          for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
 ('black', 'L'), ('white', 'L')]
  1. Isso gera uma lista de tuplas ordenadas por cor, depois por tamanho.

  2. Observe que a lista resultante é ordenada como se os loops for estivessem aninhados na mesma ordem que eles aparecem na listcomp.

  3. Para ter os itens ordenados por tamanho e então por cor, apenas rearranje as cláusulas for; adicionar uma quebra de linha listcomp torna mais fácil ver como o resultado será ordenado.

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

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

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

2.3.4. Expressões geradoras

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

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

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

Exemplo 7. Inicializando uma tupla e um array a partir de uma expressão geradora
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols)  (1)
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols))  (2)
array('I', [36, 162, 163, 165, 8364, 164])
  1. Se a expressão geradora é o único argumento em uma chamada de função, não há necessidade de duplicar os parênteses circundantes.

  2. O construtor de array espera dois argumentos, então os parênteses em torno da expressão geradora são obrigatórios. O primeiro argumento do construtor de array define o tipo de armazenamento usado para os números no array, como veremos na Seção 2.10.1.

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

Exemplo 8. Produto cartesiano em uma expressão geradora
>>> 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 é criada neste exemplo.

✒️ Nota

O Capítulo 17 explica em detalhes o funcionamento de geradoras. A ideia aqui é apenas mostrar o uso de expressões geradores para inicializar sequências diferentes de listas, ou produzir uma saída que não precise ser mantida na memória.

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

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

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

2.4.1. Tuplas como registros

Tuplas podem conter registros: cada item na tupla contém os dados de um campo, e a posição do item indica seu significado.

Se você pensar em uma tupla apenas como uma lista imutável, a quantidade e a ordem dos elementos pode ser importante ou não, dependendo do contexto. Mas quando usamos uma tupla como uma coleção de campos, o número de itens em geral é fixo e sua ordem é sempre importante.

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

Exemplo 9. Tuplas usadas como registros
>>> lax_coordinates = (33.9425, -118.408056)  (1)
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)  (2)
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),  (3)
...     ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids):  (4)
...     print('%s/%s' % passport)   (5)
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids:  (6)
...     print(country)
...
USA
BRA
ESP
  1. Latitude e longitude do Aeroporto Internacional de Los Angeles.

  2. Dados sobre Tóquio: nome, ano, população (em milhares), crescimento populacional (%) e área (km²).

  3. Uma lista de tuplas no formato (código_de_país, número_do_passaporte).

  4. Iterando sobre a lista, passport é vinculado a cada tupla.

  5. O operador de formatação % entende as tuplas e trata cada item como um campo separado.

  6. O loop for sabe como recuperar separadamente os itens de uma tupla—isso é chamado "desempacotamento" ("unpacking"). Aqui não estamos interessados no segundo item, então o atribuímos a _, uma variável descartável, usada apenas para coletar valores que não serão usados.

👉 Dica

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 coringa que corresponde a qualquer valor, mas não está vinculado a um valor. Veja a Seção 2.6. E no console de 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 9, 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.

✒️ Nota

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 3132 — Extended Iterable Unpacking (Desempacotamento Estendido de Iteráveis).

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

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

2.4.2. Tuplas como listas imutáveis

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

Clareza

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

Desempenho

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

Entretanto, lembre-se que a imutabilidade de uma tuple só se aplica às referências ali contidas. Referências em uma 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 6 representa a disposição inicial da tupla b na memória.

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

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

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

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

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

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

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

Apesar dessa ressalva, as tuplas são frequentemente usadas como listas imutáveis. Elas oferecem algumas vantagens de desempenho, explicadas por uma dos desenvolvedores principais de 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?) 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 na pilha, 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 reservam 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, Python precisa realocar o array de referências para criar espaço. A indireção adicional torna o cache da CPU menos eficiente.

2.4.3. Comparando os métodos de tuplas e listas

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

Tabela 3. Métodos e atributos encontrados em list ou tuple (os métodos implementados por object foram omitidos para economizar espaço)
list tuple  

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

s += s2—concatenação no mesmo lugar

s.append(e)

Acrescenta um elemento após o último

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.count(e)

Conta as ocorrências de um elemento

s.__delitem__(p)

Remove o item na posição p

s.extend(it)

Acrescenta itens do iterável it

s.__getitem__(p)

s[p]—obtém o item na posição p

s.__getnewargs__()

Suporte a serialização otimizada com pickle

s.index(e)

Encontra a posição da primeira ocorrência de e

s.insert(p, e)

Insere elemento e antes do item na posição p

s.__iter__()

Obtém o iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

s *= n—concatenação repetida no mesmo lugar

s.__rmul__(n)

n * s—concatenação repetida inversa[7]

s.pop([p])

Remove e devolve o último item ou o item na posição opcional p

s.remove(e)

Remove a primeira ocorrência do elemento e, por valor

s.reverse()

Reverte, no lugar, a ordem dos itens

s.__reversed__()

Obtém iterador para examinar itens, do último para o primeiro

s.__setitem__(p, e)

s[p] = e—coloca e na posição p, sobrescrevendo o item existente[8]

s.sort([key], [reverse])

Ordena os itens no lugar, com os argumentos nomeados opcionais key e reverse

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

2.5. Desempacotando sequências e iteráveis

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

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

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

>>> b, a = a, b

Outro exemplo de desempacotamento é prefixar um argumento com * ao chamar uma função:

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

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

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

2.5.1. Usando * para recolher itens em excesso

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

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

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

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

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

A PEP 448—Additional Unpacking Generalizations (Generalizações adicionais de desempacotamento) (EN) introduziu uma sintaxe mais flexível para desempacotamento iterável, melhor resumida em "O que há de novo no Python 3.5" (EN).

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

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

O * pode também ser usado na definição de literais list, tuple, ou set, como visto nesses exemplos de "O que há de novo no Python 3.5" (EN):

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

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

2.5.3. Desempacotamento aninhado

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

Exemplo 10. Desempacotando tuplas aninhadas para acessar a longitude
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 10 é:

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

>>> [record] = query_returning_single_row()

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

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

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

2.6. Pattern matching com sequências

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

✒️ Nota

Carol Willing, uma das desenvolvedoras principais de Python, escreveu uma excelente introdução ao pattern matching na seção "Casamento de padrão estrutural"[10] em "O que há de novo no Python 3.10". Você pode querer ler aquela revisão rápida. Neste livro, dividi o tratamento do casamento de padrões em diferentes capítulos, dependendo dos tipos de padrão: Na Seção 3.3 e na Seção 5.8. E há um exemplo mais longo na Seção 18.3.

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

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

Exemplo 11. Método de uma classe Robot imaginária
    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 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, 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.[11] 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 de 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 12 mostra parte do Exemplo 10 reescrito com match/case.

Exemplo 12. Desestruturando tuplas aninhadas—requer Python ≥ 3.10
metro_areas = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

def main():
    print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
    for record in metro_areas:
        match record:  # (1)
            case [name, _, _, (lat, lon)] if lon <= 0:  # (2)
                print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
  1. O sujeito desse match é record—isto é, cada uma das tuplas em metro_areas.

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

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

  1. O sujeito é uma sequência, e

  2. O sujeito e o padrão tem o mesmo número de itens, e

  3. Cada item correspondente casa, incluindo os itens aninhados.

Por exemplo, o padrão [name, _, _, (lat, lon)] no Exemplo 12 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 12.

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.

⚠️ Aviso

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:

    match tuple(phone):
        case ['1', *rest]:  # North America and Caribbean
            ...
        case ['2', *rest]:  # Africa and some territories
            ...
        case ['3' | '4', *rest]:  # Europe
            ...

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

list     memoryview    array.array
tuple    range         collections.deque

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

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

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

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

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

Variável Valor atribuído

name

'Shanghai'

lat

31.1

lon

121.3

coord

(31.1, 121.3)

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

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

As expressões str(name) e float(lat) se parecem com chamadas a construtores, que usaríamos para converter name e lat para str e float. Mas no contexto de um padrão, aquela sintaxe faz uma verificação de tipo durante a execução do programa: o padrão acima vai casar com uma 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 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:

        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 12:

        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.

👉 Dica

A desestruturação com padrões é tão expressiva que, algumas vezes, um match com um único 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) (EN).

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

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

Peter Norvig, da Universidade de Stanford, escreveu o lis.py: um interpretador de um subconjunto do dialeto Scheme da linguagem de programação Lisp, em 132 belas linhas de código Python legível. Peguei o código fonte de Norvig (publicado sob a licença MIT) e o atualizei para Python 3.10, para exemplificar o pattern matching. Nessa seção, vamos comparar uma parte fundamental do código de Norvig—que usa if/elif e desempacotamento—com uma nova versão usando match/case.

As duas funções principais do lis.py são parse e evaluate.[12] O parser (analisador sintático) recebe as expressões entre parênteses do Scheme e devolve listas Python. Aqui estão dois exemplos:

>>> 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 18.3 para aprender mais sobre o funcionamento do lis.py.

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

Exemplo 13. Casando padrões sem match/case
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 14.

Exemplo 14. Pattern matching com match/case—requer Python ≥ 3.10
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 se 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 14 é mais curto e mais seguro que o Exemplo 13.

2.6.1.1. Padrões alternativos para lambda

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

(lambda (parms…) body1 body2…)

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

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

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

['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() de Python.

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

        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.

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

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

(define (name parm…) body1 body2…)

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

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

        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 14. 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 13. A instrução match faz muito mais que o switch das linguagens similares ao C.

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

Tabela 4. Algumas formas sintáticas do Scheme e os padrões de case para tratá-las
Sintaxe do Scheme Padrão de sequência

(quote exp)

['quote', exp]

(if test conseq alt)

['if', test, conseq, alt]

(lambda (parms…) body1 body2…)

['lambda', [*parms], *body] if body

(define name exp)

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

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

✒️ Nota

Veremos mais do lis.py na Seção 18.3, quando vamos revisar o exemplo completo de 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)).

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 Parte III: Classes e protocolos.

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

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

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

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

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

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

Agora vamos olhar mais de perto a forma como Python interpreta a notação de fatiamento.

2.7.2. Objetos fatia

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

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

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

>>> 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 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 15. Em vez de encher seu código de fatias explícitas fixas, você pode nomeá-las. Veja como isso torna legível o loop for no final do exemplo.

Exemplo 15. Itens de um arquivo tabular de fatura
>>> 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 12.5. Enquanto isso, do ponto de vista do usuário, o fatiamento tem recursos adicionais, tais como fatias multidimensionais e a notação de reticências (...). Siga comigo.

2.7.3. Fatiamento multidimensional e reticências

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

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

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

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 de Python. Esse símbolo é um apelido para o objeto Ellipsis, a única instância da classe ellipsis.[14] 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" (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 de Python. Se você souber de algum, me avise. Esses recursos sintáticos existem para suportar tipos definidos pelo usuário ou extensões como o NumPy.

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

2.7.4. Atribuindo a fatias

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

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

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

2.8. Usando + e * com sequências

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

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

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

⚠️ Aviso

Tenha cuidado com expressões como a * n quando a é uma sequência contendo itens mutáveis, pois o resultado pode ser surpreendente. Por exemplo, tentar inicializar uma lista de listas como my_list = [[]] * 3 vai resultar em uma lista com três referências para a mesma lista interna, que provavelmente não é o quê você quer.

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

2.8.1. Criando uma lista de listas

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

Exemplo 16. Uma lista com três listas de tamanho 3 pode representar um tabuleiro de jogo da velha
>>> 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 17.

Exemplo 17. Uma lista com três referências para a mesma lista é inútil
>>> 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 17 é que ele se comporta, essencialmente, como o código abaixo:

row = ['_'] * 3
board = []
for i in range(3):
    board.append(row)  (1)
  1. A mesma row é anexada três vezes ao board.

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

>>> board = []
>>> for i in range(3):
...     row = ['_'] * 3  # (1)
...     board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board  # (2)
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]
  1. Cada iteração cria uma nova row e a acrescenta ao board.

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

👉 Dica

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

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

2.8.2. Atribuição aumentada com sequências

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

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

Entretanto, se __iadd__ não estiver implementado, Python chama __add__ como fallback. Considere essa expressão simples:

>>> a += b

Se a implementar __iadd__, esse método será chamado. No caso de sequências mutáveis (por exemplo, list, 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:

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

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

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

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

Exemplo 18. Um enigma
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

O que acontece a seguir? Escolha a melhor alternativa:

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

  2. É gerado um TypeError com a mensagem 'tuple' object does not support item assignment (o objeto tupla não suporta atribuição de itens).

  3. Nenhuma das alternativas acima..

  4. Ambas as alternativas, A e B.

Quando vi isso, tinha certeza que a resposta era B, mas, na verdade é D, "Ambas as alternativas, A e B"! O Exemplo 19 é a saída real em um console rodando Python 3.10.[17]

Exemplo 19. O resultado inesperado: o item t2 é modificado e uma exceção é gerada
>>> 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 (EN) é uma ferramenta online fantástica para visualizar em detalhes o funcionamento de Python. A Figura 7 é uma composição de duas capturas de tela, mostrando os estados inicial e final da tupla t do Exemplo 19.

Diagrama de referência
Figura 7. 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 20), fica claro como isso acontece.

Exemplo 20. Bytecode para a expressão s[a] += b
>>> dis.dis('s[a] += b')
  1           0 LOAD_NAME                0 (s)
              3 LOAD_NAME                1 (a)
              6 DUP_TOP_TWO
              7 BINARY_SUBSCR                      (1)
              8 LOAD_NAME                2 (b)
             11 INPLACE_ADD                        (2)
             12 ROT_THREE
             13 STORE_SUBSCR                       (3)
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE
  1. Coloca o valor de s[a] no TOS (Top Of Stack, topo da pilha de execução_).

  2. Executa TOS += b. Isso é bem sucedido se TOS se refere a um objeto mutável (no Exemplo 19 é uma lista).

  3. Atribui s[a] = TOS. Isso falha se s é imutável (a tupla t no Exemplo 19).

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

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

  • Evite colocar objetos mutáveis em tuplas.

  • A atribuição aumentada não é uma operação atômica—acabamos de vê-la gerar uma exceção após executar parte de seu trabalho.

  • Inspecionar o bytecode de Python não é muito difícil, e pode ajudar a ver o que está acontecendo por debaixo dos panos.

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

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

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

✒️ Nota

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 objetos (todos os métodos de str, por exemplo) podem ser cascateados no estilo de uma interface fluente. Veja o artigo "Fluent interface" (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).

👉 Dica

Também se pode usar o parâmetro de palavra-chave opcional key com as funções embutidas 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 de Python é estável (isto é, ele preserva a ordem relativa de itens que resultam iguais na comparação):[19]

>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry']  (1)
>>> fruits
['grape', 'raspberry', 'apple', 'banana']  (2)
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple']  (3)
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']  (4)
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple']  (5)
>>> fruits
['grape', 'raspberry', 'apple', 'banana']  (6)
>>> fruits.sort()                          (7)
>>> fruits
['apple', 'banana', 'grape', 'raspberry']  (8)
  1. Isso produz uma lista de strings ordenadas alfabeticamente.[20]

  2. Inspecionando a lista original, vemos que ela não mudou.

  3. Isso é a ordenação "alfabética" anterior, invertida.

  4. Uma nova lista de strings, agora ordenada por tamanho. Como o algoritmo de ordenação é estável, "grape" e "apple," ambas com tamanho 5, estão em sua ordem original.

  5. Essas são strings ordenadas por tamanho em ordem descendente. Não é o inverso do resultado anterior porque a ordenação é estável e então, novamente, "grape" aparece antes de "apple."

  6. Até aqui, a ordenação da lista fruits original não mudou.

  7. Isso ordena a lista no mesmo lugar, devolvendo None (que o console omite).

  8. Agora fruits está ordenada.

⚠️ Aviso

Por default, Python ordena as strings lexicograficamente por código de caractere. Isso quer dizer 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 4.8 trata de maneiras corretas de ordenar texto da forma esperada por seres humanos.

Uma vez ordenadas, podemos realizar buscas em nossas sequências de forma muito eficiente. Um algoritmo de busca binária já é fornecido no módulo bisect da biblioteca padrão de 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) (EN) em fluentpython.com, o website que complementa este livro.

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

2.10. Quando uma lista não é a resposta

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

👉 Dica

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 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 de Python quase tão enxuto quanto um array do C. Como mostrado na Figura 3, 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 Python não permite que você insira qualquer número que não corresponda ao tipo do array.

O Exemplo 21 mostra a criação, o armazenamento e o carregamento de um array de 10 milhões de números de ponto flutuante aleatórios.

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

  2. Cria um array de números de ponto flutuante de dupla precisão (código de tipo 'd') a partir de qualquer objeto iterável—nesse caso, uma expressão geradora.

  3. Inspeciona o último número no array.

  4. Salva o array em um arquivo binário.

  5. Cria um array vazio de números de ponto flutuante de dupla precisão

  6. Lê 10 milhões de números do arquivo binário.

  7. Inspeciona o último número no array.

  8. Verifica a igualdade do conteúdo dos arrays

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

Para o caso específico de arrays numéricas representando dados binários, tal como bitmaps de imagens, Python tem os tipos bytes e bytearray, discutidos na 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

s.append(e)

Acrescenta um elemento após o último

s.byteswap()

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

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.__copy__()

Suporte a copy.copy

s.count(e)

Conta as ocorrências de um elemento

s.__deepcopy__()

Suporte otimizado a copy.deepcopy

s.__delitem__(p)

Remove item na posição p

s.extend(it)

Acrescenta itens a partir do iterável it

s.frombytes(b)

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

s.fromfile(f, n)

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

s.fromlist(l)

Acrescenta itens de lista; se um deles causar um TypeError, nenhum item é acrescentado

s.__getitem__(p)

s[p]—obtém o item ou fatia na posição

s.index(e)

Encontra a posição da primeira ocorrência de e

s.insert(p, e)

Insere elemento e antes do item na posição p

s.itemsize

Tamanho em bytes de cada item do array

s.__iter__()

Obtém iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

s *= n—concatenação repetida no mesmo lugar

s.__rmul__(n)

n * s—concatenação repetida invertida[22]

s.pop([p])

Remove e devolve o item na posição p (default: o último)

s.remove(e)

Remove a primeira ocorrência do elemento e por valor

s.reverse()

Reverte a ordem dos itens no mesmo lugar

s.__reversed__()

Obtém iterador para percorrer itens do último até o primeiro

s.__setitem__(p, e)

s[p] = e—coloca e na posição p, sobrescrevendo item ou fatia existente

s.sort([key], [reverse])

Ordena itens no mesmo lugar, com os argumentos de palavra-chave opcionais key e reverse

s.tobytes()

Devolve itens como pacotes de valores em código de máquina em um objeto bytes

s.tofile(f)

Grava itens como pacotes de valores em código de máquina no arquivo binário f

s.tolist()

Devolve os itens como objetos numéricos em uma list

s.typecode

String de um caractere identificando o tipo em C dos itens

👉 Dica

Até 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:

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

Para manter a ordem de um array ordenado ao acrescentar novos itens, use a função bisect.insort.

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

2.10.2. Views de memória

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

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

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

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

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

  2. Cria uma memoryview a partir daquele array, e a exporta como uma lista.

  3. Cria uma nova memoryview a partir da anterior, mas com 2 linhas e 3 colunas.

  4. Ainda outra memoryview, agora com 3 linhas e 2 colunas.

  5. Sobrescreve o byte em m2, na linha 1, coluna 1 com 22.

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

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

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

Exemplo 23. Mudando o valor de um item em um array de inteiros de 16 bits trocando apenas o valor de um de seus bytes
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)  (1)
>>> len(memv)
5
>>> memv[0]  (2)
-2
>>> memv_oct = memv.cast('B')  (3)
>>> memv_oct.tolist()  (4)
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4  (5)
>>> numbers
array('h', [-2, -1, 1024, 1, 2])  (6)
  1. Cria uma memoryview a partir de um array de 5 inteiros com sinal de 16 bits (código de tipo 'h').

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

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

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

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

  6. Observe a mudança em numbers: um 4 no byte mais significativo de um inteiro de 2 bytes sem sinal é 1024.

✒️ Nota

Você pode ver um exemplo de inspeção de uma memoryview com o pacote struct em fluentpython.com: "Parsing binary records with struct" Analisando registros binários com struct (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 de 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 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. Em outras palavras, a SciPy dá a cientistas o melhor de dois mundos: um prompt iterativo e as APIs de alto nível de Python, junto com funções estáveis e de eficiência comprovada para processamento de números, otimizadas em C e Fortran

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

Exemplo 24. Operações básicas com linhas e colunas em uma numpy.ndarray
>>> 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 de Python). Por convenção, numpy é importada como np.

  2. Cria e inspeciona uma numpy.ndarray com inteiros de 0 a 11.

  3. Inspeciona as dimensões do array: essa é um array com uma dimensão e 12 elementos.

  4. Muda o formato do array, acrescentando uma dimensão e depois inspecionando o resultado.

  5. Obtém a linha no índice 2

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

  7. Obtém a coluna no índice 1

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

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

>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt')  (1)
>>> floats[-3:]  (2)
array([ 3016362.69195522,   535281.10514262,  4566560.44373946])
>>> floats *= .5  (3)
>>> floats[-3:]
array([ 1508181.34597761,   267640.55257131,  2283280.22186973])
>>> from time import perf_counter as pc (4)
>>> t0 = pc(); floats /= 3; pc() - t0 (5)
0.03690556302899495
>>> numpy.save('floats-10M', floats)  (6)
>>> floats2 = numpy.load('floats-10M.npy', 'r+')  (7)
>>> floats2 *= 6
>>> floats2[-3:]  (8)
memmap([ 3016362.69195522,   535281.10514262,  4566560.44373946])
  1. Carrega 10 milhões de números de ponto flutuante de um arquivo de texto.

  2. Usa a notação de fatiamento de sequência para inspecionar os três últimos números.

  3. Multiplica cada elemento no array floats por .5 e inspeciona novamente os três últimos elementos.

  4. Importa o cronômetro de medida de tempo em alta resolução (disponível desde Python 3.3).

  5. Divide cada elemento por 3; o tempo decorrido para dividir os 10 milhões de números de ponto flutuante é menos de 40 milissegundos.

  6. Salva o array em um arquivo binário .npy.

  7. Carrega os dados como um arquivo mapeado na memória em outro array; isso permite o processamento eficiente de fatias do array, mesmo que ele não caiba inteiro na memória.

  8. Inspeciona os três últimos elementos após multiplicar cada elemento por 6.

Mas isso foi apenas um aperitivo.

A NumPy e a SciPy são bibliotecas formidáveis, e estão na base de outras ferramentas fantásticas, como a Pandas (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 (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) de Python. O projeto Dask suporta a paralelização do processamento da NumPy, da Pandas e da scikit-learn para grupos (clusters) de máquinas. Esses pacotes merecem livros inteiros. Este não é um desses livros, mas nenhuma revisão das sequências de Python estaria completa sem pelo menos uma breve passagem pelos arrays da NumPy.

Tendo olhado as sequências planas—arrays padrão e arrays da NumPy—vamos agora nos voltar para um grupo completamente diferentes de substitutos para a boa e velha list: filas (queues).

2.10.4. Deques e outras filas

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

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

Exemplo 25. Usando um deque
>>> 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 deque implementa a maioria dos métodos de list, acrescentando alguns específicos ao seu modelo, como popleft e rotate. Mas há um custo oculto: remover itens do meio de um deque não é rápido. A estrutura é realmente otimizada para acréscimos e remoções pelas pontas.

As operações append e popleft são atômicas, então deque pode ser usado de forma segura como uma fila FIFO em aplicações multithread sem a necessidade de travas.

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

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

s += s2—concatenação no mesmo lugar

s.append(e)

Acrescenta um elemento à direita (após o último)

s.appendleft(e)

Acrescenta um elemento à esquerda (antes do primeiro)

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.__copy__()

Suporte a copy.copy (cópia rasa)

s.count(e)

Conta ocorrências de um elemento

s.__delitem__(p)

Remove item na posição p

s.extend(i)

Acrescenta item do iterável i pela direita

s.extendleft(i)

Acrescenta item do iterável i pela esquerda

s.__getitem__(p)

s[p]—obtém item ou fatia na posição

s.index(e)

Encontra a primeira ocorrência de e

s.insert(p, e)

Insere elemento e antes do item na posição p

s.__iter__()

Obtém iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

s *= n—concatenação repetida no mesmo lugar

s.__rmul__(n)

n * s—concatenação repetida invertida[23]

s.pop()

Remove e devolve último item[24]

s.popleft()

Remove e devolve primeiro item

s.remove(e)

Remove primeira ocorrência do elemento e por valor

s.reverse()

Inverte a ordem do itens no mesmo lugar

s.__reversed__()

Obtém iterador para percorrer itens, do último para o primeiro

s.rotate(n)

Move n itens de um lado para o outro

s.__setitem__(p, e)

s[p] = e—coloca e na posição p, sobrescrevendo item ou fatia existentes

s.sort([key], [reverse])

Ordena os itens no mesmo lugar, com os argumentos de palavra-chave opcionais key e reverse

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

queue

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

multiprocessing

Implementa sua própria SimpleQueue, não-delimitada, e Queue, delimitada, muito similares àquelas no pacote queue, mas projetadas para comunicação entre processos. Uma fila especializada, multiprocessing.JoinableQueue, é disponibilizada para gerenciamento de tarefas.

asyncio

Fornece Queue, LifoQueue, PriorityQueue, e JoinableQueue com APIs inspiradas pelas classes nos módulos queue e multiprocessing, mas adaptadas para gerenciar tarefas em programação assíncrona.

heapq

Diferente do últimos três módulos, heapq não implementa a classe queue, mas oferece funções como heappush e heappop, que permitem o uso de uma sequência mutável como uma fila do tipo heap ou como uma fila de prioridade.

Aqui termina nossa revisão das alternativas ao tipo list, e também nossa exploração dos tipos sequência em geral—exceto pelas especificidades de str e das sequências binárias, que tem seu próprio capítulo (Capítulo 4).

2.11. Resumo do capítulo

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

As sequências de 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 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) (EN). Python 3.10 introduziu o casamento de padrões com match/case, suportando um tipo de desempacotamento mais poderoso, conhecido como desestruturação.

Fatiamento de sequências é um dos recursos de sintaxe preferidos de 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 de Python oferece array.array. Apesar da NumPy e da SciPy não serem parte da biblioteca padrão, se você faz qualquer tipo de processamento numérico em grandes conjuntos de dados, estudar mesmo uma pequena parte dessas bibliotecas pode levar você muito longe.

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

2.12. Leitura complementar

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

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, honestamente acho que vale a pena ter à mão as duas edições do Python Cookbook.

O "HowTo - Ordenação" oficial de Python tem vários exemplos de técnicas avançadas de uso de sorted e list.sort.

A PEP 3132—​Extended Iterable Unpacking (Desempacotamento Iterável Estendido) (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 de Python, "Missing *-unpacking generalizations" (As generalizações esquecidas de * no desempacotamento) (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) (EN) foi o resultado de discussões ocorridas naquele tópico.

Como mencionei na Seção 2.6, o texto introdutório "Casamento de padrão estrutural", de Carol Willing, no "O que há de novo no Python 3.10", é 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) (EN) também é boa, mas mais longa. A mesma PEP 636 inclui o "Appendix A—Quick Intro" (Apêndice A-Introdução Rápida) (EN). Ele é menor que a introdução de Willing, porque omite as considerações gerais sobre os motivos pelos quais o casamento de padrões é útil. Se você precisar de mais argumentos para se convencer ou convencer outros que o casamento de padrões foi 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) (EN).

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, escrito por Jake VanderPlas e de acesso aberto, e a segunda edição do Python for Data Analysis, de Wes McKinney.

"A Numpy é toda sobre vetorização". Essa é a frase de abertura do livro de acesso aberto From Python to NumPy, 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", na documentação de Python.

A melhor defesa da convenção de Python de excluir o último item range e fatias foi escrita pelo grande Edsger W. Dijkstra, em uma nota curta intitulada "Why Numbering Should Start at Zero" (Porque a Numeração Deve Começar em Zero). O assunto da nota é notação matemática, mas ela é relevante para 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 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 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 de 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 Python mais prático e mais bem sucedido que a ABC. Da perspectiva de um implementador de linguagens, fazer as tuplas se comportarem como sequências custa pouco. Como resultado, o principal caso de uso de tuplas como registros não é tão óbvio, mas ganhamos listas imutáveis—mesmo que seu tipo não seja tão claramente nomeado como o de frozenlist.

Sequências planas versus sequências contêineres

Para realçar os diferentes modelos de memória dos tipos de sequências usei os termos sequência contêiner e sequência plana. A palavra "contêiner" vem da própria documentação do "Modelo de Dados":

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:

>>> 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 de 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, 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:

>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
>>> sorted(l, key=int)
[0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
>>> sorted(l, key=str)
[0, '1', 14, 19, '23', 28, '28', 5, 6, '9']

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

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

O Timsort foi usado no CPython pela primeira vez em 2002. Desde 2009, o Timsort também é usado para ordenar arrays tanto em 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 (EN) de 2012. Em 2021, a Suprema Corte dos Estados Unidos decidiu que o uso do código de Java pelo Google é "fair use"[25]

O Timsort foi inventado por Tim Peters, um dos desenvolvedores principais de 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" (EN). Tim também escreveu "The Zen of Python": import this.

3. Dicionários e conjuntos

Python é feito basicamente de dicionários cobertos por muitas camadas de açúcar 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 de 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 de 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 de 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 de 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 de Python implementam todas as operações fundamentais da teoria dos conjuntos, como união, intersecção, testes de subconjuntos, etc. Com eles, podemos expressar algoritmos de forma mais declarativa, evitando o excesso de loops e condicionais aninhados.

Aqui está um breve esquema do capítulo:

  • A sintaxe moderna para criar e manipular dicts e mapeamentos, incluindo desempacotamento aumentado e pattern matching (casamento de padrões)

  • Métodos comuns dos tipos de mapeamentos

  • Tratamento especial para chaves ausentes

  • Variantes de dict na biblioteca padrão

  • Os tipos set e frozenset

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

3.1. Novidades nesse capítulo

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

  • A Seção 3.2 fala da sintaxe aperfeiçoada de desempacotamento e de diferentes maneiras de mesclar mapeamentos—incluindo os operadores | e |=, suportados pelos dicts desde Python 3.9.

  • A Seção 3.3 ilustra o manuseio de mapeamentos com match/case, recurso que surgiu no Python 3.10.

  • A Seção 3.6.1 agora se concentra nas pequenas mas ainda relevantes diferenças entre dict e OrderedDict—levando em conta que, desde 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.

✒️ Nota

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) (EN) para o fluentpython.com, o site que complementa o livro. O post de 18 páginas (EN) foi atualizado e expandido, e inclui explicações e diagramas sobre:

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

  • A otimização de memória que preserva a ordem de inserção de chaves em instâncias de dict (desde Python 3.6) .

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

3.2. A sintaxe moderna dos dicts

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

3.2.1. Compreensões de dict

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

Exemplo 26. Exemplos de compreensões de dict
>>> dial_codes = [                                                  # (1)
...     (880, 'Bangladesh'),
...     (55,  'Brazil'),
...     (86,  'China'),
...     (91,  'India'),
...     (62,  'Indonesia'),
...     (81,  'Japan'),
...     (234, 'Nigeria'),
...     (92,  'Pakistan'),
...     (7,   'Russia'),
...     (1,   'United States'),
... ]
>>> country_dial = {country: code for code, country in dial_codes}  # (2)
>>> country_dial
{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62,
'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
>>> {code: country.upper()                                          # (3)
...     for country, code in sorted(country_dial.items())
...     if code < 70}
{55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA', 1: 'UNITED STATES'}
  1. Um iterável de pares chave-valor como dial_codes pode ser passado diretamente para o construtor de dict, mas…​

  2. …​aqui permutamos os pares: country é a chave, e code é o valor.

  3. Ordenando country_dial por nome, revertendo novamente os pares, colocando os valores em maiúsculas e filtrando os itens com code < 70.

Se você já está acostumada com as listcomps, as dictcomps são um próximo passo natural. Caso contrário, a propagação da sintaxe de compreensão mostra que agora é mais valioso que nunca se tornar fluente nessa técnica.

3.2.2. Desempacotando mapeamentos

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

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

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

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

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

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

3.2.3. Fundindo mapeamentos com |

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

O operador | cria um novo mapeamento:

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

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

Se você precisa manter código rodando no Python 3.8 ou anterior, a seção "Motivation" (Motivação) (EN) da PEP 584—Add Union Operators To dict (Acrescentar Operadores de União a dict) (EN) inclui um bom resumo das outras formas de mesclar mapeamentos.

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

3.3. Pattern matching com mapeamentos

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

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 27 demonstra isso.

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

Exemplo 27. creator.py: get_creators() extrai o nome dos criadores em registros de mídia
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 27 mostra algumas práticas úteis para lidar com dados semi-estruturados, tais como registros JSON:

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

  • Incluir um campo identificando a versão do schema (por exemplo, 'api': 2'), para permitir evoluções futuras das APIs públicas.

  • Ter cláusulas case para processar registros inválidos de um tipo específico (por exemplo, 'book'), bem como um case final para capturar tudo que tenha passado pelas condições anteriores.

Agora vamos ver como get_creators se comporta com alguns doctests concretos:

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

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

👉 Dica

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

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

3.4. A API padrão dos tipos de mapeamentos

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

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:

>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
>>> isinstance(my_dict, abc.MutableMapping)
True
👉 Dica

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

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

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

3.4.1. O que é hashable?

Aqui está parte da definição de hashable, adaptado do Glossário de Python:

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

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:

>>> 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 de Python, da arquitetura da máquina, e pelo sal acrescentado ao cálculo do hash por razões de segurança.[28] O código de hash de um objeto corretamente implementado tem a garantia de ser constante apenas dentro de um processo Python.

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

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

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

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

Tabela 7. Métodos do tipos de mapeamento dict, collections.defaultdict, e collections.OrderedDict (métodos comuns de object omitidos por concisão); argumentos opcionais então entre […]
dict defaultdict OrderedDict  

d.clear()

Remove todos os itens.

d.__contains__(k)

k in d.

d.copy()

Cópia rasa.

d.__copy__()

Suporte a copy.copy(d).

d.default_factory

Chamável invocado por __missing__ para definir valores ausentes.[29]

d.__delitem__(k)

del d[k]—remove item com chave k

d.fromkeys(it, [initial])

Novo mapeamento com chaves no iterável it, com um valor inicial opcional (o default é None).

d.get(k, [default])

Obtém item com chave k, devolve default ou None se k não existir.

d.__getitem__(k)

d[k]—obtém item com chave k.

d.items()

Obtém uma view dos itens—pares (chave, valor).

d.__iter__()

Obtém iterador das chaves.

d.keys()

Obtém view das chaves.

d.__len__()

len(d)—número de itens.

d.__missing__(k)

Chamado quando __getitem__ não consegue encontrar a chave.

d.move_to_end(k, [last])

Move k para primeira ou última posição (last é True por default).

d.__or__(other)

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

d.__ior__(other)

Suporte a d1 |= d2 para atualizar d1 com d2 (Python ≥ 3.9).

d.pop(k, [default])

Remove e devolve valor em k, ou default ou None, se k não existir.

d.popitem()

Remove e devolve, na forma (chave, valor), o último item inserido.[30]

d.__reversed__()

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

d.__ror__(other)

Suporte a other | dd—operador de união invertido (Python ≥ 3.9)[31]

d.setdefault(k, [default])

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

d.__setitem__(k, v)

d[k] = v—coloca v em k

d.update(m, [**kwargs])

Atualiza d com itens de um mapeamento ou iterável de pares (chave, valor).

d.values()

Obtém uma view dos valores.

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

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

3.4.3. Inserindo ou atualizando valores mutáveis

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

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

Exemplo 28. Saída parcial do Exemplo 29 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)
$ 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 29 é 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.[32]

Exemplo 29. 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 30)
"""Build an index mapping word -> list of occurrences"""

import re
import sys

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

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

# display in alphabetical order
for word in sorted(index, key=str.upper):      # (4)
    print(word, index[word])
  1. Obtém a lista de ocorrências de word, ou [] se a palavra não for encontrada.

  2. Acrescenta uma nova localização a occurrences.

  3. Coloca a occurrences modificada no dict index; isso exige uma segunda busca em index.

  4. Não estou chamando str.upper no argumento key= de sorted, apenas passando uma referência àquele método, para que a função sorted possa usá-lo para normalizar as palavras antes de ordená-las.[33]

As três linhas tratando de occurrences no Exemplo 29 podem ser substituídas por uma única linha usando dict.setdefault. O Exemplo 30 fica mais próximo do código apresentado por Alex Martelli.

Exemplo 30. 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 29
"""Build an index mapping word -> list of occurrences"""

import re
import sys

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

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

# display in alphabetical order
for word in sorted(index, key=str.upper):
    print(word, index[word])
  1. Obtém a lista de ocorrências de word, ou a define como [], se não for encontrada; setdefault devolve o valor, então ele pode ser atualizado sem uma segunda busca.

Em outras palavras, o resultado final desta linha…​

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

…​é o mesmo que executar…​

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

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

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

3.5. Tratamento automático de chaves ausentes

Algumas vezes é conveniente que os mapeamentos devolvam algum valor padronizado quando se busca por uma chave ausente. Há duas abordagem principais para esse fim: uma é usar um defaultdict em vez de um dict simples. A outra é criar uma subclasse de dict ou de qualquer outro tipo de mapeamento e acrescentar um método __missing__. Vamos ver as duas soluções a seguir.

3.5.1. defaultdict: outra perspectiva sobre as chaves ausentes

Uma instância de collections.defaultdict cria itens com um valor default sob demanda, sempre que uma chave ausente é buscada usando a sintaxe d[k]. O Exemplo 31 usa defaultdict para fornecer outra solução elegante para o índice de palavras do Exemplo 30.

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

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

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

  2. Insere a lista em dd usando 'new-key' como chave.

  3. Devolve uma referência para aquela lista.

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

Exemplo 31. index_default.py: usando um defaultdict em vez do método setdefault
"""Build an index mapping word -> list of occurrences"""

import collections
import re
import sys

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

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

# display in alphabetical order
for word in sorted(index, key=str.upper):
    print(word, index[word])
  1. Cria um defaultdict com o construtor de list como default_factory.

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

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

⚠️ Aviso

O default_factory de um defaultdict só é invocado para fornecer valores default para chamadas a __getitem__, não para outros métodos. Por exemplo, se dd é um defaultdict e k uma chave ausente, dd[k] chamará default_factory para criar um valor default, mas dd.get(k) vai devolver None e k in dd é False.

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

3.5.2. O método __missing__

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

Suponha que você queira um mapeamento onde as chaves são convertidas para str quando são procuradas. Um caso de uso concreto seria uma biblioteca para dispositivos IoT (Internet of Things, Internet das Coisas)[35], 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 32 mostra como tal mapeamento funcionaria.

Exemplo 32. Ao buscar por uma chave não-string, StrKeyDict0 a converte para str quando ela não é encontrada
Tests for item retrieval using `d[key]` notation::

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

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

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


Tests for the `in` operator::

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

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

👉 Dica

Uma forma melhor de criar uma mapeamento definido pelo usuário é criar uma subclasse de collections.UserDict em vez de dict (como faremos no Exemplo 34). Aqui criamos uma subclasse de dict apenas para mostrar que __missing__ é suportado pelo método embutido dict.__getitem__.

Exemplo 33. StrKeyDict0 converte chaves não-string para string no momento da consulta (vejas os testes no Exemplo 32)
class StrKeyDict0(dict):  # (1)

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

    def get(self, key, default=None):
        try:
            return self[key]  # (4)
        except KeyError:
            return default  # (5)

    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()  # (6)
  1. StrKeyDict0 herda de dict.

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

  3. Cria uma str de key e a procura.

  4. O método get delega para __getitem__ usando a notação self[key]; isso dá oportunidade para nosso __missing__ agir.

  5. Se um KeyError foi gerado, __missing__ já falhou, então devolvemos o default.

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

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

Sem aquele teste, nosso método __missing__ funcionaria bem com qualquer chave kstr 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 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 33. 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.

⚠️ Aviso

Classes definidas pelo usuário derivadas de mapeamentos da biblioteca padrão podem ou não usar __missing__ como alternativa em sua implementação de __getitem__, get, ou __contains__, como explicado na próxima seção.

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

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

subclasse de dict

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

subclasse de collections.UserDict

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

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

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

subclasse de abc.Mapping com __getitem__ chamando __missing__

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

Veja missing.py 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õem 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 3.6.5.

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

3.6. Variações de dict

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

3.6.1. collections.OrderedDict

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

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

  • O método popitem() de OrderedDict tem uma assinatura diferente, que aceita um argumento opcional especificando qual item será devolvido.

  • OrderedDict tem um método move_to_end(), que reposiciona de um elemento para uma ponta do dicionário de forma eficiente.

  • O dict comum foi projetado para ser muito bom nas operações de mapeamento. Monitorar a ordem de inserção era uma preocupação secundária.

  • OrderedDict foi projetado para ser bom em operações de reordenamento. Eficiência espacial, velocidade de iteração e o desempenho de operações de atualização eram preocupações secundárias.

  • Em termos do algoritmo, um OrderedDict lida melhor que um dict com operações frequentes de reordenamento. Isso o torna adequado para monitorar acessos recentes (em um cache LRU[36], por exemplo).

3.6.2. collections.ChainMap

Uma instância de ChainMap mantém uma lista de mapeamentos que podem ser consultados como se fossem um mapeamento único. A busca é realizada em cada mapa incluído, na ordem em que eles aparecem na chamada ao construtor, e é bem sucedida assim que a chave é encontrada em um daqueles mapeamentos. Por exemplo:

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

>>> 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", 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:

import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))

O Exemplo 350 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.

Aqui temos um Counter usado para contar as letras em palavras:

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

A função de módulo shelve.open devolve uma instância de shelve.Shelf—um banco de dados DBM simples de chave-valor, baseado no módulo dbm, com as seguintes características:

  • shelve.Shelf é uma subclasse de abc.MutableMapping, então fornece os métodos essenciais esperados de um tipo mapeamento.

  • Além disso, shelve.Shelf fornece alguns outros métodos de gerenciamento de E/S, como sync e close.

  • Uma instância de Shelf é um gerenciador de contexto, então é possível usar um bloco with para garantir que ela seja fechada após o uso.

  • Chaves e valores são salvos sempre que um novo valor é atribuído a uma chave.

  • As chaves devem ser strings.

  • Os valores devem ser objetos que o módulo pickle possa serializar.

A documentação para os módulos shelve, dbm (EN), e pickle traz mais detalhes e também algumas ressalvas.

⚠️ Aviso

O pickle de Python é fácil de usar nos caso mais simples, mas tem vários inconvenientes. Leia o "Pickle’s nine flaws", de Ned Batchelder, antes de adotar qualquer solução envolvendo pickle. Em seu post, Ned menciona outros formatos de serialização que podem ser considerados como alternativas.

As classes OrderedDict, ChainMap, Counter, e Shelf podem ser usadas diretamente, mas também podem ser personalizadas por subclasses. UserDict, por outro lado, foi planejada apenas como uma classe base a ser estendida.

3.6.5. Criando subclasses de UserDict em vez de dict

É melhor criar um novo tipo de mapeamento estendendo collections.UserDict em vez de dict. Percebemos isso quando tentamos estender nosso StrKeyDict0 do Exemplo 33 para assegurar que qualquer chave adicionada ao mapeamento seja armazenada como str.

A principal razão pela qual é melhor criar uma subclasse de UserDict em vez de dict é que o tipo embutido tem alguns atalhos de implementação, que acabam nos obrigando a sobrepor métodos que poderíamos apenas herdar de UserDict sem maiores problemas.[38]

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

Graças a UserDict, o StrKeyDict (Exemplo 34) é mais conciso que o StrKeyDict0 (Exemplo 33), 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 34. StrKeyDict sempre converte chaves que não sejam strings para str na inserção, atualização e busca
import collections


class StrKeyDict(collections.UserDict):  # (1)

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

    def __contains__(self, key):
        return str(key) in self.data  # (3)

    def __setitem__(self, key, item):
        self.data[str(key)] = item   # (4)
  1. StrKeyDict estende UserDict.

  2. __missing__ é exatamente igual ao do Exemplo 33.

  3. __contains__ é mais simples: podemos assumir que todas as chaves armazenadas são str, e podemos operar sobre self.data em vez de invocar self.keys(), como fizemos em StrKeyDict0.

  4. __setitem__ converte qualquer key para uma str. Esse método é mais fácil de sobrepor quando podemos delegar para o atributo self.data.

Como UserDict estende abc.MutableMapping, o restante dos métodos que fazem de StrKeyDict uma mapeamento completo são herdados de UserDict, MutableMapping, ou Mapping. Estes últimos contém vários métodos concretos úteis, apesar de serem classes base abstratas (ABCs). Os seguinte métodos são dignos de nota:

MutableMapping.update

Esse método poderoso pode ser chamado diretamente, mas também é usado por __init__ para criar a instância a partir de outros mapeamentos, de iteráveis de pares (chave, valor), e de argumentos nomeados. Como usa self[chave] = valor para adicionar itens, ele termina por invocar nossa implementação de __setitem__.

Mapping.get

No StrKeyDict0 (Exemplo 33), precisamos codificar nosso próprio get para devolver os mesmos resultados de __getitem__, mas no Exemplo 34 herdamos Mapping.get, que é implementado exatamente como StrKeyDict0.get (consulte o código-fonte de Python).

👉 Dica

Antoine Pitrou escreveu a PEP 455—​Adding a key-transforming dictionary to collections (Acrescentando um dicionário com transformação de chaves a collections) (EN) e um patch para aperfeiçoar o módulo collections com uma classe TransformDict, que é mais genérico que StrKeyDict e preserva as chaves como fornecidas antes de aplicar a transformação. A PEP 455 foi rejeitada em maio de 2015—veja a mensagem de rejeição (EN) de Raymond Hettinger. Para experimentar com a TransformDict, extraí o patch de Pitrou do issue18986 (EN) para um módulo independente (03-dict-set/transformdict.py disponível no repositório de código da segunda edição do Fluent Python).

Sabemos que existem tipos de sequências imutáveis, mas e mapeamentos imutáveis? Bem, não há um tipo real desses na biblioteca padrão, mas um substituto está disponível. É o que vem a seguir.

3.7. Mapeamentos imutáveis

Os tipos de mapeamentos disponíveis na biblioteca padrão são todos mutáveis, mas pode ser necessário impedir que os usuários mudem um mapeamento por acidente. Um caso de uso concreto pode ser encontrado, novamente, em uma biblioteca de programação de hardware como a Pingo, mencionada na Seção 3.5.2: o mapeamento board.pins representa as portas de GPIO (General Purpose Input/Output, Entrada/Saída Genérica) em um dispositivo. Dessa forma, seria útil evitar atualizações descuidadas de board.pins, pois o hardware não pode ser modificado via software: qualquer mudança no mapeamento o tornaria inconsistente com a realidade física do dispositivo.

O módulo types oferece uma classe invólucro (wrapper) chamada MappingProxyType que, dado um mapeamento, devolve uma instância de mappingproxy, que é um proxy somente para leitura (mas dinâmico) do mapeamento original. Isso significa que atualizações ao mapeamento original são refletidas no mappingproxy, mas nenhuma mudança pode ser feita através desse último. Veja uma breve demonstração no Exemplo 35.

Exemplo 35. MappingProxyType cria uma instância somente de leitura de mappingproxy a partir de um dict
>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1]  (1)
'A'
>>> d_proxy[2] = 'x'  (2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d[2] = 'B'
>>> d_proxy  (3)
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>
  1. Os items em d podem ser vistos através de d_proxy.

  2. Não é possível fazer modificações através de d_proxy.

  3. d_proxy é dinâmica: qualquer mudança em d é refletida ali.

Isso pode ser usado assim na prática, no cenário da programação de hardware: o construtor em uma subclasse concreta Board preencheria um mapeamento privado com os objetos porta, e o exporia aos clientes da API via um atributo público .portas, implementado como um mappingproxy. Dessa forma os clientes não poderiam acrescentar, remover ou modificar as portas por acidente.

A seguir veremos views—que permitem operações de alto desempenho em um dict, sem cópias desnecessárias dos dados.

3.8. Views de dicionários

Os métodos de instância de dict .keys(), .values(), e .items() devolvem instâncias de classes chamadas dict_keys, dict_values, e dict_items, respectivamente. Essas views de dicionário são projeções somente para leitura de estruturas de dados internas usadas na implemetação de dict. Elas evitam o uso de memória adicional dos métodos equivalentes no Python 2, que devolviam listas, duplicando dados já presentes no dict alvo. E também substituem os métodos antigos que devolviam iteradores.

O Exemplo 36 mostra algumas operações básicas suportadas por todas as views de dicionários.

Exemplo 36. O método .values() devolve uma view dos valores em um dict
>>> d = dict(a=10, b=20, c=30)
>>> values = d.values()
>>> values
dict_values([10, 20, 30])  (1)
>>> len(values)  (2)
3
>>> list(values)  (3)
[10, 20, 30]
>>> reversed(values)  (4)
<dict_reversevalueiterator object at 0x10e9e7310>
>>> values[0] (5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict_values' object is not subscriptable
  1. O repr de um objeto view mostra seu conteúdo.

  2. Podemos consultar a len de uma view.

  3. Views são iteráveis, então é fácil criar listas a partir delas.

  4. Views implementam __reversed__, devolvendo um iterador personalizado.

  5. Não é possível usar [] para obter itens individuais de uma view.

Um objeto view é um proxy dinâmico. Se o dict fonte é atualizado, as mudanças podem ser vistas imediatamente através de uma view existente. Continuando do Exemplo 36:

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

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

Agora vamos ver algumas regras e dicas baseadas na forma como dict é implementado debaixo dos panos.

3.9. Consequências práticas da forma como dict funciona

A implementação da tabela de hash do dict de Python é muito eficiente, mas é importante entender os efeitos práticos desse design:

  • Chaves devem ser objetos hashable. Eles devem implementar métodos __hash__ e __eq__ apropriados, como descrito na Seção 3.4.1.

  • O acesso aos itens através da chave é muito rápido. Mesmo que um dict tenha milhões de chaves, 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.[39] Comparado a isso, uma tabela de hash precisa armazenar mais dados para cada entrada e, para manter a eficiência, 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 de Python, de armazenar atributos de instância em um atributo __dict__ especial, que é um dict vinculado a cada instância.[40] Desde a implementação da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves) (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 Python a criar uma nova tabela de hash só para o __dict__ daquela instância (que era o comportamento default antes de 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" (EN) em fluentpython.com.

Agora vamos estudar conjuntos(sets).

3.10. Teoria dos conjuntos

Conjuntos não são novidade no Python, mais ainda são um tanto subutilizados. O tipo set e seu irmão imutável, frozenset, surgiram inicialmente como módulos na biblioteca padrão de Python 2.3, e foram promovidos a tipos embutidos no Python 2.6.

✒️ Nota

Nesse livro, uso a palavra "conjunto" para me referir tanto a set quanto a frozenset.

Um conjunto é uma coleção de objetos únicos. Uma grande utilidade dos conjuntos é descartar itens duplicados:

>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
>>> set(l)
{'eggs', 'spam', 'bacon'}
>>> list(set(l))
['eggs', 'spam', 'bacon']
👉 Dica

Para remover elementos duplicados preservando a ordem da primeira ocorrência de cada item, você pode fazer isso com um dict simples, assim:

>>> dict.fromkeys(l).keys()
dict_keys(['spam', 'eggs', 'bacon'])
>>> 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 instâncias de 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. Quando bem utilizadas, 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—pela remoção de loops e 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 codar isso em uma expressão simples (veja o Exemplo 37).

Exemplo 37. Conta as ocorrências de agulhas (needles) em um palheiro (haystack), ambos do tipo set
found = len(needles & haystack)

Sem o operador de intersecção, seria necessário escrever o Exemplo 38 para realizar a mesma tarefa executa pelo Exemplo 37.

Exemplo 38. Conta as ocorrências de agulhas (needles) em um palheiro (haystack); mesmo resultado final do Exemplo 37
found = 0
for n in needles:
    if n in haystack:
        found += 1

O Exemplo 37 é um pouco mais rápido que o Exemplo 38. Por outro lado, o Exemplo 38 funciona para quaisquer objetos iteráveis needles e haystack, enquanto o Exemplo 37 exige que ambos sejam conjuntos. Mas se você não tem conjuntos à mão, pode sempre criá-los na hora, como mostra o Exemplo 39.

Exemplo 39. Conta as ocorrências de agulhas (needles) em um palheiro (haystack); essas linhas funcionam para qualquer tipo iterável
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 39, mas se ou as needles ou o haystack já forem um set, a alternativa no Exemplo 39 pode ser mais barata que o Exemplo 38.

Qualquer dos exemplos acima é capaz de buscar 1000 elementos em um haystack de 10 milhões de itens em cerca de 0,3 milisegundos—isso é cerca de 0,3 microsegundos por elemento.

Além do teste de existência extremamente rápido (graças à tabela de hash subjacente), os tipos embutidos set e frozenset oferecem uma rica API para criar novos conjuntos ou, no caso de set, para modificar conjuntos existentes. Vamos discutir essas operações em breve, após uma observação sobre sintaxe.

3.10.1. Sets literais

A sintaxe de literais set{1}, {1, 2}, etc.—parece muito com a notação matemática, mas tem uma importante exceção: não há notação literal para o set vazio, então precisamos nos lembrar de escrever set().

⚠️ Aviso
Peculiaridade sintática

Para criar um set vazio, usamos 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:

>>> s = {1}
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s.pop()
1
>>> s
set()

A sintaxe do set literal, 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, 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.[41]

Não há sintaxe especial para representar literais frozenset—eles só podem ser criados chamando seu construtor. Sua representação padrão como string no Python 3 se parece com uma chamada ao construtor de frozenset com um argumento set`. Observe a saída na sessão de console a seguir:

>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

E por falar em sintaxe, a ideia das listcomps foi adaptada para criar conjuntos também.

3.10.2. Compreensões de conjuntos

Compreensões de conjuntos (setcomps) apareceram há bastante tempo, no Python 2.7, junto com as dictcomps que vimos na Seção 3.2.1. O Exemplo 40 mostra procedimento.

Exemplo 40. Cria um conjunto de caracteres Latin-1 que tenham a palavra "SIGN" em seus nomes Unicode
>>> from unicodedata import name  (1)
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')}  (2)
{'§', '=', '¢', '#', '¤', '<', '¥', 'µ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}
  1. Importa a função name de unicodedata para obter os nomes dos caracteres.

  2. Cria um conjunto de caracteres com códigos entre 32 e 255 que contenham a palavra 'SIGN' em seus nomes.

A ordem da saída muda a cada processo Python, devido ao hash "salgado", mencionado na Seção 3.4.1.

Questões de sintaxe à parte, vamos considerar agora o comportamento dos conjuntos.

3.11. Consequências práticas da forma de funcionamento dos conjuntos

Os tipos set e frozenset são ambos implementados com um tabela de hash. Isso tem os seguintes efeitos:

  • Elementos de conjuntos tem que ser objetos hashable. Eles precisam implementar métodos __hash__ e __eq__ adequados, como descrido na Seção 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 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" (EN) no fluentpython.com para maiores detalhes.

Agora vamos revisar a vasta seleção de operações oferecidas pelos conjuntos.

3.11.1. Operações de conjuntos

A Figura 9 dá uma visão geral dos métodos disponíveis em conjuntos mutáveis e imutáveis. Muitos deles são métodos especiais que sobrecarregam operadores, tais como & and >=. A Tabela 8 mostra os operadores matemáticos de conjuntos que tem operadores ou métodos correspondentes no Python. Note que alguns operadores e métodos realizam mudanças no mesmo lugar sobre o conjunto alvo (por exemplo, &=, difference_update, etc.). Tais operações não fazem sentido no mundo ideal dos conjuntos matemáticos, e também não são implementadas em frozenset.

👉 Dica

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

Diagrama de classe UML para `Set` e `MutableSet`
Figura 9. Diagrama de classes UML simplificado para MutableSet e suas superclasses em collections.abc (nomes em itálico são classes e métodos abstratos; métodos de operadores reversos foram omitidos por concisão).
Tabela 8. Operações matemáticas com conjuntos: esses métodos ou produzem um novo conjunto ou atualizam o conjunto alvo no mesmo lugar, se ele for mutável
Math symbol Python operator Method Description

S ∩ Z

s & z

s.__and__(z)

Intersecção de s e z

z & s

s.__rand__(z)

Operador & invertido

s.intersection(it, …)

Intersecção de s e todos os conjuntos construídos a partir de iteráveis it, etc.

s &= z

s.__iand__(z)

s atualizado com a intersecção de s e z

s.intersection_update(it, …)

s atualizado com a intersecção de s e todos os conjuntos construídos a partir de iteráveis it, etc.

S ∪ Z

s | z

s.__or__(z)

União de s e z

z | s

s.__ror__(z)

| invertido

s.union(it, …)

União de s e todos os conjuntos construídos a partir de iteráveis it, etc.

s |= z

s.__ior__(z)

s atualizado com a união de s e z

s.update(it, …)

s atualizado com a união de s e todos os conjuntos construídos a partir de iteráveis it, etc.

S \ Z

s - z

s.__sub__(z)

Complemento relativo ou diferença entre s e z

z - s

s.__rsub__(z)

Operador - invertido

s.difference(it, …)

Diferença entre s e todos os conjuntos construídos a partir de iteráveis it, etc.

s -= z

s.__isub__(z)

s atualizado com a diferença entre s e z

s.difference_update(it, …)

s atualizado com a diferença entre s e todos os conjuntos construídos a partir de iteráveis it, etc.

S ∆ Z

s ^ z

s.__xor__(z)

Diferença simétrica (o complemento da intersecção s & z)

z ^ s

s.__rxor__(z)

Operador ^ invertido

s.symmetric_difference(it)

Complemento de s & set(it)

s ^= z

s.__ixor__(z)

s atualizado com a diferença simétrica de s e z

s.symmetric_difference_update(it, …)

s atualizado com a diferença simétrica de s e todos os conjuntos construídos a partir de iteráveis it, etc.

A Tabela 9 lista predicados de conjuntos: operadores e métodos que devolvem True ou False.

Tabela 9. Operadores e métodos de comparação de conjuntos que devolvem um booleano
Math symbol Python operator Method Description

S ∩ Z = ∅

s.isdisjoint(z)

s e z são disjuntos (não tem elementos em comum)

e ∈ S

e in s

s.__contains__(e)

Elemento e é membro de s

S ⊆ Z

s <= z

s.__le__(z)

s é um subconjunto do conjunto z

s.issubset(it)

s é um subconjunto do conjunto criado a partir do iterável it

S ⊂ Z

s < z

s.__lt__(z)

s é um subconjunto próprio[42] do conjunto z

S ⊇ Z

s >= z

s.__ge__(z)

s é um superconjunto do conjunto z

s.issuperset(it)

s é um superconjunto do conjunto criado a partir do iterável it

S ⊃ Z

s > z

s.__gt__(z)

s é um superconjunto próprio do conjunto z

Além de operadores e métodos derivados da teoria matemática dos conjuntos, os tipos conjunto implementam outros métodos para tornar seu uso prático, resumidos na Tabela 10.

Tabela 10. Métodos adicionais de conjuntos
set frozenset  

s.add(e)

Adiciona elemento e a s

s.clear()

Remove todos os elementos de s

s.copy()

Cópia rasa de s

s.discard(e)

Remove elemento e de s, se existir

s.__iter__()

Obtém iterador de s

s.__len__()

len(s)

s.pop()

Remove e devolve um elemento de s, gerando um KeyError se s estiver vazio

s.remove(e)

Remove elemento e de s, gerando um KeyError se e não existir em s

Isso encerra nossa visão geral dos recursos dos conjuntos. Como prometido na Seção 3.8, vamos agora ver como dois dos tipos de views de dicionários se comportam de forma muito similar a um frozenset.

3.12. Operações de conjuntos em views de dict

A Tabela 11 mostra como os objetos view devolvidos pelos métodos .keys() e .items() de dict são notavelmente similares a um frozenset.

Tabela 11. Métodos implementados por frozenset, dict_keys, e dict_items
frozenset dict_keys dict_items Description

s.__and__(z)

s & z (interseção de s e z)

s.__rand__(z)

operador & invertido

s.__contains__()

e in s

s.copy()

Cópia rasa de s

s.difference(it, …)

Diferença entre s e os iteráveis it, etc.

s.intersection(it, …)

Intersecção de s e dos iteráveis it, etc.

s.isdisjoint(z)

s e z são disjuntos (não tem elementos em comum)

s.issubset(it)

s é um subconjunto do iterável it

s.issuperset(it)

s é um superconjunto do iterável it

s.__iter__()

obtém iterador para s

s.__len__()

len(s)

s.__or__(z)

s | z (união de s e z)

s.__ror__()

Operador | invertido

s.__reversed__()

Obtém iterador para s com a ordem invertida

s.__rsub__(z)

Operador - invertido

s.__sub__(z)

s - z (diferença entre s e z)

s.symmetric_difference(it)

Complemento de s & set(it)

s.union(it, …)

União de s e dos iteráveis`it`, etc.

s.__xor__()

s ^ z (diferença simétrica de s e z)

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:

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

>>> s = {'a', 'e', 'i'}
>>> d1.keys() & s
{'a'}
>>> d1.keys() | s
{'a', 'c', 'b', 'd', 'i', 'e'}
⚠️ Aviso

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 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 implementação de Python em C trabalhar para você!

Com isso, encerramos esse capítulo.

3.13. Resumo do capítulo

Dicionários são a pedra fundamental de Python. Ao longo dos anos, a sintaxe literal familiar, {k1: v1, k2: v2}, foi aperfeiçoada para suportar desempacotamento com ** e pattern matching, bem como com compreensões de dict.

Além do dict básico, a biblioteca padrão oferece mapeamentos práticos prontos para serem usados, como o defaultdict, o ChainMap, e o Counter, todos definidos no módulo collections. Com a nova implementação de dict, o OrderedDict não é mais tão útil quanto antes, mas deve permanecer na biblioteca 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 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() de Python 2, que criavam listas duplicando os dados na instância alvo de dict. Além disso, as classes dict_keys e dict_items suportam os operadores e métodos mais úteis de frozenset.

3.14. Leitura complementar

Na documentação da Biblioteca Padrão de Python, a seção "collections—Tipos de dados de contêineres" inclui exemplos e receitas práticas para vários tipos de mapeamentos. O código-fonte de Python para o módulo, Lib/collections/__init__.py, é uma excelente referência para qualquer um que deseje criar novos tipos de mapeamentos ou entender a lógica dos tipos existentes. O capítulo 1 do Python Cookbook, 3rd ed. (O’Reilly), de David Beazley e Brian K. Jones traz 20 receitas práticas e perpicazes usando estruturas de dados—a maioria mostrando formas inteligentes de usar dict.

Greg Gandenberger defende a continuidade do uso de collections.OrderedDict, com os argumentos de que "explícito é melhor que implícito," compatibilidade retroativa, e o fato de algumas ferramentas e bibliotecas presumirem que a ordenação das chaves de um dict é irrelevante—nesse post: "Python Dictionaries Are Now Ordered. Keep Using OrderedDict" (Os dicionários de Python agora são ordenados. Continue a usar OrderedDict) (EN).

A PEP 3106—​Revamping dict.keys(), .values() and .items() (Renovando dict.keys(), .values() e .items()) (EN) foi onde Guido van Rossum apresentou o recurso de views de dicionário para Python 3. No resumo, ele afirma que a ideia veio da Java Collections Framework.

O PyPy 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) (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 de PHP) (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) (EN), uma continuação de sua apresentação animada clássica "The Mighty Dictionary" (O poderoso dicionário) (EN)—incluindo colisões de hash animadas! Outro vídeo atual mas mais aprofundado sobre o funcionamento interno do dict de Python é "Modern Dictionaries" (Dicionários modernos) (EN) de Raymond Hettinger, onde ele nos diz que após inicialmente fracassar em convencer os desenvolvedores principais de Python sobre os dicts compactos, ele persuadiu a equipe do PyPy, eles os adotaram, a ideia ganhou força, e finalmente foi adicionada 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 (EN) e no documento de design em Objects/dictnotes.txt (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). Quando a PEP 218 foi aprovada, nenhuma sintaxe literal especial foi adotada para conjuntos. Os literais set foram criados para Python 3 e implementados retroativamente no Python 2.7, assim como as compreensões de dict e set. Na PyCon 2019, apresentei "Set Practice: learning from Python’s set types" (A Prática dos Conjuntos: aprendendo com os tipos conjunto de Python) (EN), descrevendo casos de uso de conjuntos em programas reais, falando sobre o design de sua API, e sobre a implementação da uintset, 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) (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).

Ponto de Vista

Açúcar sintático

Meu amigo Geraldo Cohen certa vez observou que Python é "simples e correto."

Puristas de linguagens de programação gostam de desprezar a sintaxe como algo desimportante.

Syntactic sugar causes cancer of the semicolon.[43]

— 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 Python, fiz um pouco de programação para a web usando Perl e PHP. A sintaxe para mapeamentos nessas linguagens é muito útil, e eu tenho muita 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) e se tornou um imenso sucesso, substituindo XML em vários contextos. Uma sintaxe concisa para listas e dicionários resulta em um excelente formato para troca de dados.

PHP e Ruby imitaram a sintaxe de hash do Perl, usando => para ligar chaves a valores. JavaScript usa : como Python. Por que usar dois caracteres, quando um já é legível o bastante?

O JSON veio de JavaScript, mas por acaso também é quase um subconjunto exato da sintaxe de Python. O JSON é compatível com Python, exceto por usar true, false, e null em vez de True, False, e None.

Armin Ronacher tuitou que gosta de brincar com o espaço de nomes global de Python, para acrescentar apelidos compatíveis com o JSON para o True, o False, e o None de Python, pois daí ele pode colar trechos de JSON diretamente no console. Sua ideia básica:

>>> 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 de 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.[44]

— Esther Nam e Travis Fischer

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 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" de Python 2 não oferecia.

Nesse capítulo, veremos os seguintes tópicos:

  • Caracteres, pontos de código e representações binárias

  • Recursos exclusivos das sequências binárias: bytes, bytearray, e memoryview

  • Codificando para o Unicode completo e para conjuntos de caracteres legados

  • Evitando e tratando erros de codificação

  • Melhores práticas para lidar com arquivos de texto

  • A armadilha da codificação default e questões de E/S padrão

  • Comparações seguras de texto Unicode com normalização

  • Funções utilitárias para normalização, case folding (equiparação maiúsculas/minúsculas) e remoção de sinais diacríticos por força bruta

  • Ordenação correta de texto Unicode com locale e a biblioteca pyuca

  • Metadados de caracteres do banco de dados Unicode

  • APIs duais, que processam str e bytes

4.1. Novidades nesse capítulo

O suporte ao Unicode no Python 3 sempre foi muito completo e estável, então o acréscimo mais notável é a Seção 4.9.1, descrevendo um utilitário de linha de comando para busca no banco de dados Unicode—uma forma de encontrar gatinhos sorridentes ou hieróglifos do Egito antigo.

Vale a pena mencionar que o suporte a Unicode no Windows ficou melhor e mais simples desde Python 3.6, como veremos na 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.

✒️ Nota

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), (EN) no fluentpython.com, o website que complementa o livro.

Lá você também vai encontrar o "Building Multi-character Emojis" (Criando emojis multi-caractere) (EN), descrevendo como combinar caracteres Unicode para criar bandeiras de países, bandeiras de arco-íris, pessoas com tonalidades de pele diferentes e ícones de diferentes tipos de famílias.

4.2. Questões de caracteres

O conceito de "string" é bem simples: uma string é uma sequência de caracteres. O problema está na definição de "caractere".

Em 2023, a melhor definição de "caractere" que temos é um caractere Unicode. Consequentemente, os itens que compõe um str de 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 de 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 41.

Exemplo 41. Codificando e decodificando
>>> s = 'café'
>>> len(s)  # (1)
4
>>> b = s.encode('utf8')  # (2)
>>> b
b'caf\xc3\xa9'  # (3)
>>> len(b)  # (4)
5
>>> b.decode('utf8')  # (5)
'café'
  1. A str 'café' tem quatro caracteres Unicode.

  2. Codifica str para bytes usando a codificação UTF-8.

  3. bytes literais são prefixados com um b.

  4. bytes b tem cinco bytes (o ponto de código para "é" é codificado com dois bytes em UTF-8).

  5. Decodifica bytes para str usando a codificação UTF-8.

👉 Dica

Um jeito fácil de sempre lembrar a distinção entre .decode() e .encode() é se convencer que 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 de Python 3 ser quase o tipo unicode de Python 2 com um novo nome, o bytes de Python 3 não é meramente o velho str renomeado, e há também o tipo estreitamente relacionado bytearray. Então vale a pena examinar os tipos de sequências binárias antes de avançar para questões de codificação/decodificação.

4.3. Os fundamentos do byte

Os novos tipos de sequências binárias são diferentes do str de Python 2 em vários aspectos. A primeira coisa importante é que existem dois tipos embutidos básicos de sequências binárias: o tipo imutável bytes, introduzido no Python 3, e o tipo mutável bytearray, introduzido há tempos, no Python 2.6[45]. A documentação de Python algumas vezes usa o termo genérico "byte string" (string de bytes, na documentação em português) para se referir a bytes e bytearray.

Cada item em bytes ou bytearray é um inteiro entre 0 e 255, e não uma string de um caractere, como no str de 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 42.

Exemplo 42. Uma sequência de cinco bytes, como bytes e como `bytearray
>>> cafe = bytes('café', encoding='utf_8')  (1)
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0]  (2)
99
>>> cafe[:1]  (3)
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr  (4)
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:]  (5)
bytearray(b'\xa9')
  1. bytes pode ser criado a partir de uma str, dada uma codificação.

  2. Cada item é um inteiro em range(256).

  3. Fatias de bytes também são bytes—mesmo fatias de um único byte.

  4. Não há uma sintaxe literal para bytearray: elas aparecem como bytearray() com um literal bytes como argumento.

  5. Uma fatia de bytearray também é uma bytearray.

⚠️ Aviso

O fato de my_bytes[0] obter um int mas my_bytes[:1] devolver uma sequência de bytes de tamanho 1 só é surpreeendente porque estamos acostumados com o tipo str de 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 \'.[46]

  • Para qualquer outro valor do byte, é usada uma sequência de escape hexadecimal (por exemplo, \x00 é o byte nulo).

É por isso que no Exemplo 42 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 a 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 Python 3.5, o operador % voltou a funcionar com sequências binárias.[47]

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:

>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'

As outras formas de criar instâncias de bytes ou bytearray são chamadas a seus construtores com:

  • Uma str e um argumento nomeado encoding

  • Um iterável que forneça itens com valores entre 0 e 255

  • Um objeto que implemente o protocolo de buffer (por exemplo bytes, bytearray, memoryview, array.array), que copia os bytes do objeto fonte para a recém-criada sequência binária

⚠️ Aviso

Até Python 3.5, era possível chamar bytes ou bytearray com um único inteiro, para criar uma 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).

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

Exemplo 43. Inicializando bytes a partir de dados brutos de um array
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])  (1)
>>> octets = bytes(numbers)  (2)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'  (3)
  1. O typecode 'h' cria um array de short integers (inteiros de 16 bits).

  2. octets mantém uma cópia dos bytes que compõem numbers.

  3. Esses são os 10 bytes que representam os 5 inteiros pequenos.

Criar um objeto bytes ou bytearray a partir de qualquer fonte tipo buffer vai sempre copiar os bytes. Já objetos memoryview permitem compartilhar memória entre estruturas de dados binários, como vimos na Seção 2.10.2.

Após essa exploração básica dos tipos de sequências de bytes de Python, vamos ver como eles são convertidos de e para strings.

4.4. Codificadores/Decodificadores básicos

A distribuição de Python inclui mais de 100 codecs (encoders/decoders, _codificadores/decodificadores) para conversão de texto para bytes e vice-versa. Cada codec tem um nome, como 'utf_8', e muitas vezes apelidos, tais como 'utf8', 'utf-8', e 'U8', que você pode usar como o argumento de codificação em funções como open(), str.encode(), bytes.decode(), e assim por diante. O Exemplo 44 mostra o mesmo texto codificado como três sequências de bytes diferentes.

Exemplo 44. A string "El Niño" codificada com três codecs, gerando sequências de bytes muito diferentes
>>> 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 10 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.

Tabela de demonstração de codificações
Figura 10. 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 10 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 10 foram escolhidas para montar uma amostra representativa:

latin1 a.k.a. iso8859_1

Importante por ser a base de outras codificações,tal como a cp1252 e o próprio Unicode (observe que os valores binários do latin1 aparecem nos bytes do cp1252 e até nos pontos de código).

cp1252

Um superconjunto útil de latin1, criado pela Microsoft, acrescentando símbolos convenientes como as aspas curvas e o € (euro); alguns aplicativos de Windows chamam essa codificação de "ANSI", mas ela nunca foi um padrão ANSI real.

cp437

O conjunto de caracteres original do IBM PC, com caracteres de desenho de caixas. Incompatível com o latin1, que surgiu depois.

gb2312

Padrão antigo para codificar ideogramas chineses simplificados usados na República da China; uma das várias codificações muito populares para línguas asiáticas.

utf-8

De longe a codificação de 8 bits mais comum na web. Em julho de 2021, o "W3Techs: Usage statistics of character encodings for websites" 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".

⚠️ Aviso

A UTF-16 sucedeu a codificação de 16 bits original do Unicode 1.0—a UCS-2—há muito tempo, em 1996. A UCS-2 ainda é usada em muitos sistemas, apesar de ter sido descontinuada ainda no século passado, por suportar apenas ponto de código até U+FFFF. Em 2021, mas de 57% dos pontos de código alocados estava acima de U+FFFF, incluindo os importantíssimos emojis.

Após completar essa revisão das codificações mais comuns, vamos agora tratar das questões relativas a operações de codificação e decodificação.

4.5. Entendendo os problemas de codificação/decodificação

Apesar de existir uma exceção genérica, UnicodeError, o erro relatado pelo Python em geral é mais específico: ou é um UnicodeEncodeError (ao converter uma str para sequências binárias) ou é um UnicodeDecodeError (ao ler uma sequência binária para uma str). Carregar módulos de Python também pode geram um SyntaxError, quando a codificação da fonte for inesperada. Vamos ver como tratar todos esses erros nas próximas seções.

👉 Dica

A primeira coisa a observar quando aparece um erro de Unicode é o tipo exato da exceção. É um UnicodeEncodeError, um UnicodeDecodeError, ou algum outro erro (por exemplo, SyntaxError) mencionando um problema de codificação? Para resolver o problema, você primeiro precisa entendê-lo.

4.5.1. Tratando o UnicodeEncodeError

A maioria dos codecs não-UTF entendem apenas um pequeno subconjunto dos caracteres Unicode. Ao converter texto para bytes, um UnicodeEncodeError será gerado se um caractere não estiver definido na codificação alvo, a menos que seja fornecido um tratamento especial, passando um argumento errors para o método ou função de codificação. O comportamento para tratamento de erro é apresentado no Exemplo 45.

Exemplo 45. Encoding to bytes: success and error handling
>>> city = 'São Paulo'
>>> city.encode('utf_8')  (1)
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1')  (2)
b'S\xe3o Paulo'
>>> city.encode('cp437')  (3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore')  (4)
b'So Paulo'
>>> city.encode('cp437', errors='replace')  (5)
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace')  (6)
b'S&#227;o Paulo'
  1. As codificações UTF lidam com qualquer str

  2. iso8859_1 também funciona com a string 'São Paulo'.

  3. cp437 não consegue codificar o 'ã' ("a" com til). O método default de tratamento de erro, — 'strict'—gera um UnicodeEncodeError.

  4. O método de tratamento errors='ignore' pula os caracteres que não podem ser codificados; isso normalmente é uma péssima ideia, levando a perda silenciosa de informação.

  5. Ao codificar, errors='replace' substitui os caracteres não-codificáveis por um '?'; aqui também há perda de informação, mas os usuários recebem um alerta de que algo está faltando.

  6. 'xmlcharrefreplace' substitui os caracteres não-codificáveis por uma entidade XML. Se você não pode usar UTF e não pode perder informação, essa é a única opção.

✒️ Nota

O tratamento de erros de codecs é extensível. Você pode registrar novas strings para o argumento 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 (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. Python 3.7 trouxe um novo método booleano, str.isascii(), para verificar se seu texto Unicode é 100% ASCII. Se for, você deve ser capaz de codificá-lo para bytes em qualquer codificação sem gerar um UnicodeEncodeError.

4.5.2. Tratando o UnicodeDecodeError

Nem todo byte contém um caractere ASCII válido, e nem toda sequência de bytes é um texto codificado em UTF-8 ou UTF-16 válidos; assim, se você presumir uma dessas codificações ao converter um sequência binária para texto, pode receber um UnicodeDecodeError, se bytes inesperados forem encontrados.

Por outro lado, várias codificações de 8 bits antigas, como a 'cp1252', a 'iso8859_1' e a 'koi8_r' são capazes de decodificar qualquer série de bytes, incluindo ruído aleatório, sem reportar qualquer erro. Portanto, se seu programa presumir a codificação de 8 bits errada, ele vai decodificar lixo silenciosamente.

👉 Dica

Caracteres truncados ou distorcidos são conhecidos como "gremlins" ou "mojibake" (文字化け—"texto modificado" em japonês).

O Exemplo 46 ilustra a forma como o uso do codec errado pode produzir gremlins ou um UnicodeDecodeError.

Exemplo 46. Decodificando de str para bytes: sucesso e tratamento de erro
>>> octets = b'Montr\xe9al'  (1)
>>> octets.decode('cp1252')  (2)
'Montréal'
>>> octets.decode('iso8859_7')  (3)
'Montrιal'
>>> octets.decode('koi8_r')  (4)
'MontrИal'
>>> octets.decode('utf_8')  (5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace')  (6)
'Montr�al'
  1. A palavra "Montréal" codificada em latin1; '\xe9' é o byte para "é".

  2. Decodificar com Windows 1252 funciona, pois esse codec é um superconjunto de latin1.

  3. ISO-8859-7 foi projetado para a língua grega, então o byte '\xe9' é interpretado de forma incorreta, e nenhum erro é gerado.

  4. KOI8-R é foi projetado para o russo. Agora '\xe9' significa a letra "И" do alfabeto cirílico.

  5. O codec 'utf_8' detecta que octets não é UTF-8 válido, e gera um UnicodeDecodeError.

  6. Usando 'replace' para tratamento de erro, o \xe9 é substituído por "�" (ponto de código #U+FFFD), o caractere oficial do Unicode chamado REPLACEMENT CHARACTER, criado exatamente para representar caracteres desconhecidos.

4.5.3. O SyntaxError ao carregar módulos com codificação inesperada

UTF-8 é a codificação default para fontes no Python 3, da mesma forma que ASCII era o default no Python 2. Se você carregar um módulo .py contendo dados que não estejam em UTF-8, sem declaração codificação, receberá uma mensagem como essa:

SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line
  1, but no encoding declared; see https://python.org/dev/peps/pep-0263/
  for details

Como o UTF-8 está amplamente instalado em sistemas GNU/Linux e macOS, um cenário onde isso tem mais chance de ocorrer é na abertura de um arquivo .py criado no Windows, com cp1252. Observe que esse erro ocorre mesmo no Python para Windows, pois a codificação default para fontes de Python 3 é UTF-8 em todas as plataformas.

Para resolver esse problema, acrescente o comentário mágico coding no início do arquivo, como no Exemplo 47.

Exemplo 47. 'ola.py': um "Hello, World!" em português
# coding: cp1252

print('Olá, Mundo!')
👉 Dica

Agora que o código fonte de Python 3 não está mais limitado ao ASCII, e por default usa a excelente codificação UTF-8, a melhor "solução" para código fonte em codificações antigas como 'cp1252' é converter tudo para UTF-8 de uma vez, e não se preocupar com os comentários coding. E se seu editor não suporta UTF-8, é hora de trocar de editor.

Suponha que você tem um arquivo de texto, seja ele código-fonte ou poesia, mas não sabe qual codificação foi usada. Como detectar a codificação correta? Respostas na próxima seção.

4.5.4. Como descobrir a codificação de uma sequência de bytes

Como descobrir a codificação de uma sequência de bytes? Resposta curta: não é possível. Você precisa ser informado.

Alguns protocolos de comunicação e formatos de arquivo, como o HTTP e o XML, contêm cabeçalhos que nos dizem explicitamente como o conteúdo está codificado. Você pode ter certeza que algumas sequências de bytes não estão em ASCII, pois elas contêm bytes com valores acima de 127, e o modo como o UTF-8 e o UTF-16 são construídos também limita as sequências de bytes possíveis.

O hack do Leo para adivinhar uma decodificação UTF-8

(Os próximos parágrafos vieram de uma nota escrita pelo revisor técnico Leonardo Rochael no rascunho desse livro.)

Pela forma como o UTF-8 foi projetado, é quase impossível que uma sequência aleatória de bytes, ou mesmo uma sequência não-aleatória de bytes de uma codificação diferente do UTF-8, seja acidentalmente decodificada como lixo no UTF-8, ao invés de gerar um UnicodeDecodeError.

As razões para isso são que as sequências de escape do UTF-8 nunca usam caracteres ASCII, e tais sequências de escape tem padrões de bits que tornam muito difícil que dados aleatórioas sejam UTF-8 válido por acidente.

Portanto, se você consegue decodificar alguns bytes contendo códigos > 127 como UTF-8, a maior probabilidade é de sequência estar em UTF-8.

Trabalhando com os serviços online brasileiros, alguns dos quais alicerçados em back-ends antigos, ocasionalmente precisei implementar uma estratégia de decodificação que tentava decodificar via UTF-8, e tratava um UnicodeDecodeError decodificando via cp1252. Uma estratégia feia, mas efetiva.

Entretanto, considerando que as linguagens humanas também tem suas regras e restrições, uma vez que você supõe que uma série de bytes é um texto humano simples, pode ser possível intuir sua codificação usando heurística e estatística. Por exemplo, se bytes com valor b'\x00' bytes forem comuns, é provável que seja uma codificação de 16 ou 32 bits, e não um esquema de 8 bits, pois caracteres nulos em texto simples são erros. Quando a sequência de bytes `b'\x20\x00'` aparece com frequência, é mais provável que esse seja o caractere de espaço (U+0020) na codificação UTF-16LE, e não o obscuro caractere U+2000 (EN QUAD)—seja lá o que for isso.

É assim que o pacote "Chardet—​The Universal Character Encoding Detector (Chardet—O Detector Universal de Codificações de Caracteres)" 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:

$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99

Apesar de sequências binárias de texto codificado normalmente não trazerem dicas sobre sua codificação, os formatos UTF podem preceder o conteúdo textual por um marcador de ordem dos bytes. Isso é explicado a seguir.

4.5.5. BOM: um gremlin útil

No Exemplo 44, você pode ter notado um par de bytes extra no início de uma sequência codificada em UTF-16. Aqui estão eles novamente:

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

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

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

👉 Dica
A dica de Caleb sobre o UTF-8-SIG

Caleb Hattingh—um dos revisores técnicos—sugere sempre usar o codec UTF-8-SIG para ler arquivos UTF-8. Isso é inofensivo, pois o UTF-8-SIG lê corretamente arquivos com ou sem um BOM, e não devolve o BOM propriamente dito. Para escrever arquivos, recomendo usar UTF-8, para interoperabilidade integral. Por exemplo, scripts Python podem ser tornados executáveis em sistemas Unix, se começarem com o comentário: #!/usr/bin/env python3. Os dois primeiros bytes do arquivo precisam ser b'#!' para isso funcionar, mas o BOM rompe essa convenção. Se você tem o requerimento específico de exportar dados para aplicativos que precisam do BOM, use o UTF-8-SIG, mas esteja ciente do que diz a documentação sobre codecs (EN) de Python: "No UTF-8, o uso do BOM é desencorajado e, em geral, deve ser evitado."

Vamos agora ver como tratar arquivos de texto no Python 3.

4.6. Processando arquivos de texto

A melhor prática para lidar com E/S de texto é o "Sanduíche de Unicode" (Unicode sandwich) (Figura 11).[48] Isso significa que os bytes devem ser decodificados para str o mais cedo possível na entrada (por exemplo, ao abrir um arquivo para leitura). O "recheio" do sanduíche é a lógica do negócio de seu programa, onde o tratamento do texto é realizado exclusivamente sobre objetos str. Você nunca deveria codificar ou decodificar no meio de outro processamento. Na saída, as str são codificadas para bytes o mais tarde possível. A maioria dos frameworks web funciona assim, e raramente tocamos em bytes ao usá-los. No Django, por exemplo, suas views devem produzir str em Unicode; o próprio Django se encarrega de codificar a resposta para bytes, usando UTF-8 como default.

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.

Diagrama do sanduíche de Unicode
Figura 11. O sanduíche de Unicode: melhores práticas atuais para processamento de texto.

Observe a sessão de console no Exemplo 48. Você consegue ver o erro?

Exemplo 48. Uma questão de plataforma na codificação (você pode ou não ver o problema se tentar isso na sua máquina)
>>> 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 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 48 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.

👉 Dica

Código que precisa rodar em múltiplas máquinas ou múltiplas ocasiões não deveria jamais 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 48 é 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 49 é uma versão estendida do Exemplo 48, e explica esse e outros detalhes.

Exemplo 49. Uma inspeção mais atenta do Exemplo 48 rodando no Windows revela o bug e a solução do problema
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp  # (1)
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café')  # (2)
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size  # (3)
5
>>> fp2 = open('cafe.txt')
>>> fp2  # (4)
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding  # (5)
'cp1252'
>>> fp2.read() # (6)
'café'
>>> fp3 = open('cafe.txt', encoding='utf_8')  # (7)
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read() # (8)
'café'
>>> fp4 = open('cafe.txt', 'rb')  # (9)
>>> fp4                           # (10)
<_io.BufferedReader name='cafe.txt'>
>>> fp4.read()  # (11)
b'caf\xc3\xa9'
  1. Por default, open usa o modo texto e devolve um objeto TextIOWrapper com uma codificação específica.

  2. O método write de um TextIOWrapper devolve o número de caracteres Unicode escritos.

  3. os.stat diz que o arquivo tem 5 bytes; o UTF-8 codifica 'é' com 2 bytes, 0xc3 e 0xa9.

  4. Abrir um arquivo de texto sem uma codificação explícita devolve um TextIOWrapper com a codificação configurada para um default do locale.

  5. Um objeto TextIOWrapper tem um atributo de codificação que pode ser inspecionado: neste caso, cp1252.

  6. Na codificação cp1252 do Windows, o byte 0xc3 é um "Ã" (A maiúsculo com til), e 0xa9 é o símbolo de copyright.

  7. Abrindo o mesmo arquivo com a codificação correta.

  8. O resultado esperado: os mesmo quatro caracteres Unicode para 'café'.

  9. A flag 'rb' abre um arquivo para leitura em modo binário.

  10. O objeto devolvido é um BufferedReader, e não um TextIOWrapper.

  11. Ler do arquivo obtém bytes, como esperado.

👉 Dica

Não abra arquivos de texto no modo binário, a menos que seja necessário analisar o conteúdo do arquivo para determinar sua codificação—e mesmo assim, você deveria estar usando o Chardet em vez de reinventar a roda (veja a Seção 4.5.4).

Programas comuns só deveriam usar o modo binário para abrir arquivos binários, como arquivos de imagens raster ou bitmaps.

O problema no Exemplo 49 vem de se confiar numa configuração default ao se abrir um arquivo de texto. Há várias fontes de tais defaults, como mostra a próxima seção.

4.6.1. Cuidado com os defaults de codificação

Várias configurações afetam os defaults de codificação para E/S no Python. Veja o script default_encodings.py script no Exemplo 50.

Exemplo 50. Explorando os defaults de codificação
import locale
import sys

expressions = """
        locale.getpreferredencoding()
        type(my_file)
        my_file.encoding
        sys.stdout.isatty()
        sys.stdout.encoding
        sys.stdin.isatty()
        sys.stdin.encoding
        sys.stderr.isatty()
        sys.stderr.encoding
        sys.getdefaultencoding()
        sys.getfilesystemencoding()
    """

my_file = open('dummy', 'w')

for expression in expressions.split():
    value = eval(expression)
    print(f'{expression:>30} -> {value!r}')

A saída do Exemplo 50 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:

$ python3 default_encodings.py
 locale.getpreferredencoding() -> 'UTF-8'
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'UTF-8'
           sys.stdout.isatty() -> True
           sys.stdout.encoding -> 'utf-8'
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'utf-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'utf-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'

No Windows, porém, a saída é o Exemplo 51.

Exemplo 51. Codificações default, no PowerShell do Windows 10 (a saída é a mesma no cmd.exe)
> chcp  (1)
Active code page: 437
> python default_encodings.py  (2)
 locale.getpreferredencoding() -> 'cp1252'  (3)
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'cp1252'  (4)
           sys.stdout.isatty() -> True      (5)
           sys.stdout.encoding -> 'utf-8'   (6)
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'utf-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'utf-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'
  1. chcp mostra a página de código ativa para o console: 437.

  2. Executando default_encodings.py, com a saída direcionada para o console.

  3. locale.getpreferredencoding() é a configuração mais importante.

  4. Arquivos de texto usam`locale.getpreferredencoding()` por default.

  5. A saída está direcionada para o console, então sys.stdout.isatty() é True.

  6. Agora, sys.stdout.encoding não é a mesma que a página de código informada por chcp!

O suporte a Unicode no próprio Windows e no Python para Windows melhorou desde que escrevi a primeira edição deste livro. O Exemplo 51 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) (EN), implementada no Python 3.6, e ao suporte a Unicode no PowerShell do cmd.exe (desde o Windows 1809, de outubro de 2018).[49] É 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), também implementada no Python 3.6, que modificou a codificação do sistema de arquivos (usada para representar nomes de diretórios e de arquivos), da codificação proprietária MBCS da Microsoft para UTF-8.

Entretanto, se a saída do Exemplo 50 for redirecionada para um arquivo, assim…​

Z:\>python default_encodings.py > encodings.log

…​aí o valor de sys.stdout.isatty() se torna False, e sys.stdout.encoding é determinado por locale.getpreferredencoding(), 'cp1252' naquela máquina—mas sys.stdin.encoding e sys.stderr.encoding seguem como utf-8.

👉 Dica

No Exemplo 52, usei a expressão de escape '\N{}' para literais Unicode, escrevendo o nome oficial do caractere dentro do \N{}. Isso é bastante prolixo, mas explícito e seguro: Python gera um SyntaxError se o nome não existir—bem melhor que escrever um número hexadecimal que pode estar errado, mas isso só será descoberto muito mais tarde. De qualquer forma, você provavelmente vai querer escrever um comentário explicando os códigos dos caracteres, então a verbosidade do \N{} é fácil de aceitar.

Isso significa que um script como o Exemplo 52 funciona quando está escrevendo no console, mas pode falhar quando a saída é redirecionada para um arquivo.

Exemplo 52. stdout_check.py
import sys
from unicodedata import name

print(sys.version)
print()
print('sys.stdout.isatty():', sys.stdout.isatty())
print('sys.stdout.encoding:', sys.stdout.encoding)
print()

test_chars = [
    '\N{HORIZONTAL ELLIPSIS}',       # exists in cp1252, not in cp437
    '\N{INFINITY}',                  # exists in cp437, not in cp1252
    '\N{CIRCLED NUMBER FORTY TWO}',  # not in cp437 or in cp1252
]

for char in test_chars:
    print(f'Trying to output {name(char)}:')
    print(char)

O Exemplo 52 mostra o resultado de uma chamada a sys.stdout.isatty(), o valor de sys.​stdout.encoding, e esses três caracteres:

  • '…' HORIZONTAL ELLIPSIS—existe no CP 1252 mas não no CP 437.

  • '∞' INFINITY—existe no CP 437 mas não no CP 1252.

  • '㊷' CIRCLED NUMBER FORTY TWO—não existe nem no CP 1252 nem no CP 437.

Quando executo o stdout_check.py no PowerShell ou no cmd.exe, funciona como visto na Figura 12.

Captura de tela do `stdout_check.py` no PowerShell
Figura 12. Executando stdout_check.py no PowerShell.

Apesar de chcp informar o código ativo como 437, sys.stdout.encoding é UTF-8, então tanto HORIZONTAL ELLIPSIS quanto INFINITY são escritos corretamente. O CIRCLED NUMBER FORTY TWO é substituído por um retângulo, mas nenhum erro é gerado. Presume-se que ele seja reconhecido como um caractere válido, mas a fonte do console não tem o glifo para mostrá-lo.

Entretanto, quando redireciono a saída de stdout_check.py para um arquivo, o resultado é o da Figura 13.

Captura de teal do `stdout_check.py` no PowerShell, redirecionando a saída
Figura 13. Executanto stdout_check.py no PowerShell, redirecionando a saída.

O primeiro problema demonstrado pela Figura 13 é o UnicodeEncodeError mencionando o caractere '\u221e', porque sys.stdout.encoding é 'cp1252'—uma página de código que não tem o caractere INFINITY.

Lendo out.txt com o comando type—ou um editor de Windows como o VS Code ou o Sublime Text—mostra que, ao invés do HORIZONTAL ELLIPSIS, consegui um 'à' (LATIN SMALL LETTER A WITH GRAVE). Acontece que o valor binário 0x85 no CP 1252 significa '…', mas no CP 437 o mesmo valor binário representa o 'à'. Então, pelo visto, a página de código ativa tem alguma importância, não de uma forma razoável ou útil, mas como uma explicação parcial para uma experiência ruim com o Unicode.

✒️ Nota

Para realizar esses experimentos, usei um laptop configurado para o mercado norte-americano, 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 51:

  • Se você omitir o argumento encoding ao abrir um arquivo, o default é dado por locale.getpreferredencoding() ('cp1252' no Exemplo 51).

  • Antes de Python 3.6, a codificação de sys.stdout|stdin|stderr costumava ser determinada pela variável do ambiente PYTHONIOENCODING—agora essa variável é ignorada, a menos que PYTHONLEGACYWINDOWSSTDIO seja definida como uma string não-vazia. Caso contrário, a codificação da E/S padrão será UTF-8 para E/S interativa, ou definida por locale.getpreferredencoding(), se a entrada e a saída forem redirecionadas para ou de um arquivo.

  • sys.getdefaultencoding() é usado internamente pelo Python em conversões implícitas de dados binários de ou para str. Não há suporte para mudar essa configuração.

  • sys.getfilesystemencoding() é usado para codificar/decodificar nomes de arquivo (mas não o conteúdo dos arquivos). Ele é usado quando open() recebe um argumento str para um nome de arquivo; se o nome do arquivo é passado como um argumento bytes, ele é entregue sem modificação para a API do sistema operacional.

✒️ Nota

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 codificações diferentes são usadas no mesmo sistema, elas também são, normalmente, páginas de código como 'cp850' ou 'cp1252', que suportam só o ASCII com 127 caracteres adicionais (que por sua vez são diferentes de uma codificação para a outra). Assim, usuários de Windows tem muito mais chances de cometer erros de codificação, a menos que sejam muito cuidadosos.

Resumindo, a configuração de codificação mais importante devolvida por locale.getpreferredencoding() é a default para abrir arquivos de texto e para sys.stdout/stdin/stderr, quando eles são redirecionados para arquivos. Entretanto, a documentação diz (em parte):

locale.getpreferredencoding(do_setlocale=True)

Retorna a codificação da localidade usada para dados de texto, de acordo com as preferências do usuário. As preferências do usuário são expressas de maneira diferente em sistemas diferentes e podem não estar disponíveis programaticamente em alguns sistemas, portanto, essa função retorna apenas uma estimativa. […​]

Assim, o melhor conselho sobre defaults de codificação é: não confie neles.

Você evitará muitas dores de cabeça se seguir o conselho do sanduíche de Unicode, e sempre tratar codificações de forma explícita em seus programas. Infelizmente, o Unicode é trabalhoso mesmo se você converter seus bytes para str corretamente. As duas próximas seções tratam de assuntos que são simples no reino do ASCII, mas ficam muito complexos no planeta Unicode: normalização de texto (isto é, transformar o texto em uma representação uniforme para comparações) e ordenação.

4.7. Normalizando o Unicode para comparações confiáveis

Comparações de strings são dificultadas pelo fato do Unicode ter combinações de caracteres: sinais diacríticos e outras marcações que são anexadas aos caractere anterior, ambos aparecendo juntos como um só caractere quando impressos.

Por exemplo, a palavra "café" pode ser composta de duas formas, usando quatro ou cinco pontos de código, mas o resultado parece exatamente o mesmo:

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

>>> 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: Casamento de Strings e Busca) (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:

>>> 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).[50]

É assim que a NFKC funciona na prática:

>>> 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 [51], nosso próximo assunto.

4.7.1. Case Folding

Case folding é essencialmente a conversão de todo o texto para minúsculas, com algumas transformações adicionais. A operação é suportada pelo método str.casefold().

Para qualquer string s contendo apenas caracteres latin1, s.casefold() produz o mesmo resultado de s.lower(), com apenas duas exceções—o símbolo de micro, 'µ', é trocado pela letra grega mu minúscula (que é exatamente igual na maioria das fontes) e a letra alemã Eszett (ß), também chamada "s agudo" (scharfes S) se torna "ss":

>>> 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 de Python fez um grande esforço para apresentar uma solução que, espera-se, funcione para a maioria dos usuários.

Nas próximas seções vamos colocar nosso conhecimento sobre normalização para trabalhar, desenvolvendo algumas funções utilitárias.

4.7.2. Funções utilitárias para casamento de texto normalizado

Como vimos, é seguro usar a NFC e a NFD, e ambas permitem comparações razoáveis entre strings Unicode. A NFC é a melhor forma normalizada para a maioria das aplicações, e str.casefold() é a opção certa para comparações indiferentes a maiúsculas/minúsculas.

Se você precisa lidar com texto em muitas línguas diferentes, seria muito útil acrescentar às suas ferramentas de trabalho um par de funções como nfc_equal e fold_equal, do Exemplo 53.

Exemplo 53. normeq.py: normalized Unicode string comparison
"""
Utility functions for normalized Unicode string comparison.

Using Normal Form C, case sensitive:

    >>> s1 = 'café'
    >>> s2 = 'cafe\u0301'
    >>> s1 == s2
    False
    >>> nfc_equal(s1, s2)
    True
    >>> nfc_equal('A', 'a')
    False

Using Normal Form C with case folding:

    >>> s3 = 'Straße'
    >>> s4 = 'strasse'
    >>> s3 == s4
    False
    >>> nfc_equal(s3, s4)
    False
    >>> fold_equal(s3, s4)
    True
    >>> fold_equal(s1, s2)
    True
    >>> fold_equal('A', 'a')
    True

"""

from unicodedata import normalize

def nfc_equal(str1, str2):
    return normalize('NFC', str1) == normalize('NFC', str2)

def fold_equal(str1, str2):
    return (normalize('NFC', str1).casefold() ==
            normalize('NFC', str2).casefold())

Além da normalização e do case folding do Unicode—ambos partes desse padrão—algumas vezes faz sentido aplicar transformações mais profundas, como por exemplo mudar 'café' para 'cafe'. Vamos ver quando e como na próxima seção.

4.7.3. "Normalização" extrema: removendo sinais diacríticos

O tempero secreto da busca do Google inclui muitos truques, mas um deles aparentemente é ignorar sinais diacríticos (acentos e cedilhas, por exemplo), pelo menos em alguns contextos. Remover sinais diacríticos não é uma forma regular de normalização, pois muitas vezes muda o sentido das palavras e pode produzir falsos positivos em uma busca. Mas ajuda a lidar com alguns fatos da vida: as pessoas às vezes são preguiçosas ou desconhecem o uso correto dos sinais diacríticos, e regras de ortografia mudam com o tempo, levando acentos a desaparecerem e reaparecerem nas línguas vivas.

Além do caso da busca, eliminar os acentos torna as URLs mais legíveis, pelo menos nas línguas latinas. Veja a URL do artigo da Wikipedia sobre a cidade de São Paulo:

https://en.wikipedia.org/wiki/S%C3%A3o_Paulo

O trecho %C3%A3 é a renderização em UTF-8 de uma única letra, o "ã" ("a" com til). A forma a seguir é muito mais fácil de reconhecer, mesmo com a ortografia incorreta:

https://en.wikipedia.org/wiki/Sao_Paulo

Para remover todos os sinais diacríticos de uma str, você pode usar uma função como a do Exemplo 54.

Exemplo 54. simplify.py: função para remover todas as marcações combinadas
import unicodedata
import string


def shave_marks(txt):
    """Remove all diacritic marks"""
    norm_txt = unicodedata.normalize('NFD', txt)  # (1)
    shaved = ''.join(c for c in norm_txt
                     if not unicodedata.combining(c))  # (2)
    return unicodedata.normalize('NFC', shaved)  # (3)
  1. Decompõe todos os caracteres em caracteres base e marcações combinadas.

  2. Filtra e retira todas as marcações combinadas.

  3. Recompõe todos os caracteres.

Exemplo 55 mostra alguns usos para shave_marks.

Exemplo 55. Dois exemplos de uso da shave_marks do Exemplo 54
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> shave_marks(order)
'“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”'  (1)
>>> Greek = 'Ζέφυρος, Zéfiro'
>>> shave_marks(Greek)
'Ζεφυρος, Zefiro'  (2)
  1. Apenas as letras "è", "ç", e "í" foram substituídas.

  2. Tanto "έ" quando "é" foram substituídas.

A função shave_marks do Exemplo 54 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 56 faz.

Exemplo 56. 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 54)
def shave_marks_latin(txt):
    """Remove all diacritic marks from Latin base characters"""
    norm_txt = unicodedata.normalize('NFD', txt)  # (1)
    latin_base = False
    preserve = []
    for c in norm_txt:
        if unicodedata.combining(c) and latin_base:   # (2)
            continue  # ignore diacritic on Latin base char
        preserve.append(c)                            # (3)
        # if it isn't a combining char, it's a new base char
        if not unicodedata.combining(c):              # (4)
            latin_base = c in string.ascii_letters
    shaved = ''.join(preserve)
    return unicodedata.normalize('NFC', shaved)   # (5)
  1. Decompõe todos os caracteres em caracteres base e marcações combinadas.

  2. Pula as marcações combinadas quando o caractere base é latino.

  3. Caso contrário, mantém o caractere original.

  4. Detecta um novo caractere base e determina se ele é latino.

  5. Recompõe todos os caracteres.

Um passo ainda mais radical substituiria os símbolos comuns em textos de línguas ocidentais (por exemplo, aspas curvas, travessões, os círculos de bullet points, etc) em seus equivalentes ASCII. É isso que a função asciize faz no Exemplo 57.

Exemplo 57. Transforma alguns símbolos tipográficos ocidentais em ASCII (este trecho também é parte do simplify.py do Exemplo 54)
single_map = str.maketrans("""‚ƒ„ˆ‹‘’“”•–—˜›""",  # (1)
                           """'f"^<''""---~>""")

multi_map = str.maketrans({  # (2)
    '€': 'EUR',
    '…': '...',
    'Æ': 'AE',
    'æ': 'ae',
    'Œ': 'OE',
    'œ': 'oe',
    '™': '(TM)',
    '‰': '<per mille>',
    '†': '**',
    '‡': '***',
})

multi_map.update(single_map)  # (3)


def dewinize(txt):
    """Replace Win1252 symbols with ASCII chars or sequences"""
    return txt.translate(multi_map)  # (4)


def asciize(txt):
    no_marks = shave_marks_latin(dewinize(txt))     # (5)
    no_marks = no_marks.replace('ß', 'ss')          # (6)
    return unicodedata.normalize('NFKC', no_marks)  # (7)
  1. Cria uma tabela de mapeamento para substituição de caractere para caractere.

  2. Cria uma tabela de mapeamento para substituição de string para caractere.

  3. Funde as tabelas de mapeamento.

  4. dewinize não afeta texto em ASCII ou latin1, apenas os acréscimos da Microsoft ao latin1 no cp1252.

  5. Aplica dewinize e remove as marcações de sinais diacríticos.

  6. Substitui o Eszett por "ss" (não estamos usando case folding aqui, pois queremos preservar maiúsculas e minúsculas).

  7. Aplica a normalização NFKC para compor os caracteres com seus pontos de código de compatibilidade.

O Exemplo 58 mostra a asciize em ação.

Exemplo 58. Dois exemplos usando asciize, do Exemplo 57
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> dewinize(order)
'"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."'  (1)
>>> asciize(order)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."'  (2)
  1. dewinize substitui as aspas curvas, os bullets, e o ™ (símbolo de marca registrada).

  2. asciize aplica dewinize, remove os sinais diacríticos e substitui o 'ß'.

⚠️ Aviso

Cada língua tem suas próprias regras para remoção de sinais diacríticos. Por exemplo, os alemães 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 uma cirurgia profunda no texto, com boas chances de mudar seu sentido. Só você pode decidir se deve ir tão longe, conhecendo a língua alvo, os seus usuários e a forma como o texto transformado será utilizado.

Isso conclui nossa discussão sobre normalização de texto Unicode.

Vamos agora ordenar nossos pensamentos sobre ordenação no Unicode.

4.8. Ordenando texto Unicode

Python ordena sequências de qualquer tipo comparando um por um os itens em cada sequência. Para strings, isso significa comparar pontos de código. Infelizmente, isso produz resultados inaceitáveis para qualquer um que use caracteres não-ASCII.

Considere ordenar uma lista de frutas cultivadas no Brazil:

>>> 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.[52] Então "cajá" é lido como "caja," e deve vir antes de "caju."

A lista fruits ordenada deveria ser:

['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, "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 59 pode funcionar para você.

Exemplo 59. locale_sort.py: Usando a função locale.strxfrm como chave de ornenamento
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 59 no GNU/Linux (Ubuntu 19.10) com o locale pt_BR.UTF-8 instalado, consigo o resultado correto:

'pt_BR.UTF-8'
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Portanto, você precisa chamar setlocale(LC_COLLATE, «your_locale») antes de usar locale.strxfrm como a chave de ordenação.

Porém, aqui vão algumas ressalvas:

  • Como as configurações de locale são globais, não é recomendado chamar setlocale em uma biblioteca. Sua aplicação ou framework deveria definir o locale no início do processo, e não mudá-lo mais depois disso.

  • O locale desejado deve estar instalado no SO, caso contrário setlocale gera uma exceção de locale.Error: unsupported locale setting.

  • Você tem que saber como escrever corretamente o nome do locale.

  • O locale precisa ser corretamente implementado pelos desenvolvedores do SO. Tive sucesso com o Ubuntu 19.10, mas não no macOS 10.14. No macOS, a chamada setlocale(LC_COLLATE, 'pt_BR.UTF-8') devolve a string 'pt_BR.UTF-8' sem qualquer reclamação. Mas sorted(fruits, key=locale.strxfrm) produz o mesmo resultado incorreto de sorted(fruits). Também tentei os locales fr_FR, es_ES, e de_DE no macOS, mas locale.strxfrm nunca fez seu trabalho direito.[53]

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, disponível no PyPI.

4.8.1. Ordenando com o Algoritmo de Ordenação do Unicode

James Tauber, contribuidor muito ativo do Django, deve ter sentido essa nossa mesma dor, e criou a pyuca, uma implementação integralmente em Python do Algoritmo de Ordenação do Unicode (UCA, sigla em inglês para Unicode Collation Algorithm). O Exemplo 60 mostra como ela é fácil de usar.

Exemplo 60. Utilizando o método pyuca.Collator.sort_key
>>> 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, incluído no projeto. Esse arquivo é apenas uma cópia da Default Unicode Collation Element Table (Tabela Default de Ordenação de Elementos Unicode) do Unicode.org .

👉 Dica
PyICU: A recomendação do Miro para ordenação com Unicode

(O revisor técnico Miroslav Šedivý é um poliglota e um especialista em Unicode. Eis o que ele escreveu sobre a pyuca.)

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, que funciona como locale sem modificar o locale do processo. Ela também é necessária se você quiser mudar a capitalização de iİ/ıI em turco. A PyICU inclui uma extensão que precisa ser compilada, então pode ser mais difícil de instalar em alguns sistemas que a pyuca, que é toda feita em Python.

E por sinal, aquela tabela de ordenação é um dos muitos arquivos de dados que formam o banco de dados do Unicode, nosso próximo assunto.

4.9. O banco de dados do Unicode

O padrão Unicode fornece todo um banco de dados—na forma de vários arquivos de texto estruturados—que inclui não apenas a tabela mapeando pontos de código para nomes de caracteres, mas também metadados sobre os caracteres individuais e como eles se relacionam. Por exemplo, o banco de dados do Unicode registra se um caractere pode ser impresso, se é uma letra, um dígito decimal ou algum outro símbolo numérico. É assim que os métodos de str isalpha, isprintable, isdecimal e isnumeric funcionam. str.casefold também usa informação de uma tabela do Unicode.

✒️ Nota

A função unicodedata.category(char) devolve uma categoria de char com duas letras, do banco de dados do Unicode. Os métodos de alto nível de str são mais fáceis de usar. Por exemplo, label.isalpha() devolve True se todos os caracteres em label pertencerem a uma das seguintes categorias: Lm, Lt, Lu, Ll, or Lo. Para descobrir o que esses códigos significam, veja "General Category" (EN) no artigo "Unicode character property" (EN) da Wikipedia em inglês.

4.9.1. Encontrando caracteres por nome

O módulo unicodedata tem funções para obter os metadados de caracteres, incluindo unicodedata.name(), que devolve o nome oficial do caractere no padrão. A Figura 14 demonstra essa função.[54]

Explorando `unicodedata.name()` no console de Python
Figura 14. Explorando unicodedata.name() no console de Python.

Você pode usar a função name() para criar aplicações que permitem aos usuários buscarem caracteres por nome. A Figura 15 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 61.

Usando _cf.py_ para encontrar gatos sorridentes.
Figura 15. Usando cf.py para encontrar gatos sorridentes.
⚠️ Aviso

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 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, 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 61, 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 de Python, não precisamos de um loop for aninhado e de outro if para implementar essa verificação

Exemplo 61. cf.py: o utilitário de busca de caracteres
#!/usr/bin/env python3
import sys
import unicodedata

START, END = ord(' '), sys.maxunicode + 1           # (1)

def find(*query_words, start=START, end=END):       # (2)
    query = {w.upper() for w in query_words}        # (3)
    for code in range(start, end):
        char = chr(code)                            # (4)
        name = unicodedata.name(char, None)         # (5)
        if name and query.issubset(name.split()):   # (6)
            print(f'U+{code:04X}\t{char}\t{name}')  # (7)

def main(words):
    if words:
        find(*words)
    else:
        print('Please provide words to find.')

if __name__ == '__main__':
    main(sys.argv[1:])
  1. Configura os defaults para a faixa de pontos de código da busca.

  2. find aceita query_words e somente argumentos nomeados (opcionais) para limitar a faixa da busca, facilitando os testes.

  3. Converte query_words em um conjunto de strings capitalizadas.

  4. Obtém o caractere Unicode para code.

  5. Obtém o nome do caractere, ou None se o ponto de código não estiver atribuído a um caractere.

  6. Se há um nome, separa esse nome em uma lista de palavras, então verifica se o conjunto query é um subconjunto daquela lista.

  7. Mostra uma linha com o ponto de código no formato U+9999, o caractere e seu nome.

O módulo unicodedata tem outras funções interessantes. A seguir veremos algumas delas, relacionadas a obter informação de caracteres com sentido numérico.

4.9.2. O sentido numérico de caracteres

O módulo unicodedata inclui funções para determinar se um caractere Unicode representa um número e, se for esse o caso, seu valor numérico em termos humanos—em contraste com o número de seu ponto de código.

O Exemplo 62 demonstra o uso de unicodedata.name() e unicodedata.numeric(), junto com os métodos .isdecimal() e .isnumeric() de str.

Exemplo 62. Demo do banco de dados Unicode de metadados de caracteres numéricos (as notas explicativas descrevem cada coluna da saída)
import unicodedata
import re

re_digit = re.compile(r'\d')

sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'

for char in sample:
    print(f'U+{ord(char):04x}',                       # (1)
          char.center(6),                             # (2)
          're_dig' if re_digit.match(char) else '-',  # (3)
          'isdig' if char.isdigit() else '-',         # (4)
          'isnum' if char.isnumeric() else '-',       # (5)
          f'{unicodedata.numeric(char):5.2f}',        # (6)
          unicodedata.name(char),                     # (7)
          sep='\t')
  1. Ponto de código no formato U+0000.

  2. O caractere, centralizado em uma str de tamanho 6.

  3. Mostra re_dig se o caractere casa com a regex r'\d'.

  4. Mostra isdig se char.isdigit() é True.

  5. Mostra isnum se char.isnumeric() é True.

  6. Valor numérico formatado com tamanho 5 e duas casa decimais.

  7. O nome Unicode do caractere.

Executar o Exemplo 62 gera a Figura 16, se a fonte do seu terminal incluir todos aqueles símbolos.

Captura de tela de caracteres numéricos
Figura 16. 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 16 é 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 16 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.[55] 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.

A seguir vamos dar uma rápida passada pelas APIs de modo dual, com funções que aceitam argumentos str ou bytes e dão a eles tratamento especial dependendo do tipo.

4.10. APIs de modo dual para str e bytes

A biblioteca padrão de Python tem funções que aceitam argumentos str ou bytes e se comportam de forma diferente dependendo do tipo recebido. Alguns exemplos podem ser encontrados nos módulos re e os.

4.10.1. str versus bytes em expressões regulares

Se você criar uma expressão regular com bytes, padrões tal como \d e \w vão casar apenas com caracteres ASCII; por outro lado, se esses padrões forem passados como str, eles vão casar com dígitos Unicode ou letras além do ASCII. O Exemplo 63 e a Figura 17 comparam como letras, dígitos ASCII, superescritos e dígitos tamil casam em padrões str e bytes.

Exemplo 63. ramanujan.py: compara o comportamento de expressões regulares simples como str e como bytes
import re

re_numbers_str = re.compile(r'\d+')     # (1)
re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+')  # (2)
re_words_bytes = re.compile(rb'\w+')

text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef"  # (3)
            " as 1729 = 1³ + 12³ = 9³ + 10³.")        # (4)

text_bytes = text_str.encode('utf_8')  # (5)

print(f'Text\n  {text_str!r}')
print('Numbers')
print('  str  :', re_numbers_str.findall(text_str))      # (6)
print('  bytes:', re_numbers_bytes.findall(text_bytes))  # (7)
print('Words')
print('  str  :', re_words_str.findall(text_str))        # (8)
print('  bytes:', re_words_bytes.findall(text_bytes))    # (9)
  1. As duas primeiras expressões regulares são do tipo str.

  2. As duas últimas são do tipo bytes.

  3. Texto Unicode para ser usado na busca, contendo os dígitos tamil para 1729 (a linha lógica continua até o símbolo de fechamento de parênteses).

  4. Essa string é unida à anterior no momento da compilação (veja "2.4.2. String literal concatenation" (Concatenação de strings literais) em A Referência da Linguagem Python).

  5. Uma string bytes é necessária para a busca com as expressões regulares bytes.

  6. O padrão str r'\d+' casa com os dígitos ASCII e tamil.

  7. O padrão bytes rb'\d+' casa apenas com os bytes ASCII para dígitos.

  8. O padrão str r'\w+' casa com letras, superescritos e dígitos tamil e ASCII.

  9. O padrão bytes rb'\w+' casa apenas com bytes ASCII para letras e dígitos.

Saída de ramanujan.py
Figura 17. Captura de tela da execução de ramanujan.py do Exemplo 63.

O Exemplo 63 é um exemplo trivial para destacar um ponto: você pode usar expressões regulares com str ou bytes, mas nesse último caso os bytes fora da faixa do ASCII são tratados como caracteres que não representam dígitos nem palavras.

Para expressões regulares str, há uma marcação re.ASCII, que faz \w, \W, \b, \B, \d, \D, \s, e \S executarem um casamento apenas com ASCII. Veja a documentaçào do módulo re para maiores detalhes.

Outro módulo importante é o os.

4.10.2. str versus bytes nas funções de os

O kernel do GNU/Linux não conhece Unicode então, no mundo real, você pode encontrar nomes de arquivo compostos de sequências de bytes que não são válidas em nenhum esquema razoável de codificação, e não podem ser decodificados para str. Servidores de arquivo com clientes usando uma variedade de diferentes SOs são particularmente inclinados a apresentar esse cenário.

Para mitigar esse problema, todas as funções do módulo os que aceitam nomes de arquivo ou caminhos podem receber seus argumentos como str ou bytes. Se uma dessas funções é chamada com um argumento str, o argumento será automaticamente convertido usando o codec informado por sys.getfilesystemencoding(), e a resposta do SO será decodificada com o mesmo codec. Isso é quase sempre o que se deseja, mantendo a melhor prática do sanduíche de Unicode.

Mas se você precisa lidar com (e provavelmente corrigir) nomes de arquivo que não podem ser processados daquela forma, você pode passar argumentos bytes para as funções de os, e receber bytes de volta. Esse recurso permite que você processe qualquer nome de arquivo ou caminho, independende de quantos gremlins encontrar. Veja o Exemplo 64.

Exemplo 64. listdir com argumentos str e bytes, e os resultados
>>> os.listdir('.')  # (1)
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.')  # (2)
[b'abc.txt', b'digits-of-\xcf\x80.txt']
  1. O segundo nome de arquivo é "digits-of-π.txt" (com a letra grega pi).

  2. Dado um argumento byte, listdir devolve nomes de arquivos como bytes: b'\xcf\x80' é a codificação UTF-8 para a letra grega pi.

Para ajudar no processamento manual de sequências str ou bytes que são nomes de arquivos ou caminhos, o módulo os fornece funções especiais de codificação e decodificação, os.fsencode(name_or_path) e os.fsdecode(name_or_path). Ambas as funções aceitam argumentos dos tipos str, bytes ou, desde Python 3.6, um objeto que implemente a interface os.PathLike.

O Unicode é um buraco de coelho bem fundo. É hora de encerrar nossa exploração de str e bytes.

4.11. Resumo do capítulo

Começamos o capítulo descartando a noção de que 1 caractere == 1 byte. A medida que o mundo adota o Unicode, precisamos manter o conceito de strings de texto separado das sequências binárias que as representam em arquivos, e Python 3 aplica essa separação.

Após uma breve passada pelos tipos de dados sequências binárias—bytes, bytearray, e memoryview—, mergulhamos na codificação e na decodificação, com uma amostragem dos codecs 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 de 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 adaptadas 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 linhas de código, graças ao poder de Python. Demos uma olhada em outros metadados do Unicode, e vimos rapidamente as APIs de modo dual, onde algumas funções podem ser chamadas com argumentos str ou bytes, produzindo resultados diferentes.

4.12. Leitura complementar

A palestra de Ned Batchelder na PyCon US 2012, "Pragmatic Unicode, or, How Do I Stop the Pain?" (Unicode Pragmático, ou, Como Eu Fiz a Dor Sumir?) (EN), foi marcante. Ned é tão profissional que forneceu uma transcrição completa da palestra, além dos slides e do vídeo.

"Character encoding and Unicode in Python: How to (╯°□°)╯︵ ┻━┻ with dignity" (Codificação de caracteres e o Unicode no Python: como (╯°□°)╯︵ ┻━┻ com dignidade) (slides, vídeo) (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)" (Modelo Mental Útil do Unicode) em um post curto, "Unconfusing Unicode: What Is Unicode?" (Desconfundindo o Unicode: O Que É O Unicode?) (EN). O Unicode é um padrão complexo, então o UMMU de Lennart é realmente um ponto de partida útil.

O "Unicode HOWTO" oficial na documentação de Python aborda o assunto por vários ângulos diferentes, de uma boa introdução histórica a detalhes de sintaxe, codecs, expressões regulares, nomes de arquivo, e boas práticas para E/S sensível ao Unicode (isto é, o sanduíche de Unicode), com vários links adicionais de referências em cada seção.

O Chapter 4, "Strings" (Capítulo 4, "Strings"), do maravilhosos livro Dive into Python 3 (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 descreve como a biblioteca Chardet foi portada de Python 2 para 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) (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, é bastante profundo e realça algumas das armadilhas do Unicode no Python (Armin não é um grande fã de Python 3).

O capítulo 2 ("Strings and Text" Strings e Texto) do Python Cookbook, 3rd ed. (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) (EN) e "Processing Text Files in Python 3" (Processando Arquivos de Texto em Python 3) (EN). Fortemente recomendado.

Uma lista de codificações suportadas pelo Python está disponível em "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, que acompanha o código-fonte do CPython.

Os livros Unicode Explained (Unicode Explicado) (EN), de Jukka K. Korpela (O’Reilly) e Unicode Demystified (Unicode Desmistificado), de Richard Gillam (Addison-Wesley) não são específicos sobre Python, nas foram muito úteis para meu estudo dos conceitos do Unicode. Programming with Unicode (Programando com Unicode), 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) (EN) e "Character Model for the World Wide Web: String Matching" (O Modelo de Caracteres para a World Wide Web: Casamento de Strings) (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) (EN). A seção "Frequently Asked Questions, Normalization" (Perguntas Frequentes, Normalização) (EN) do Unicode.org é mais fácil de ler, bem como o "NFC FAQ" (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 (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 (EN) publicou o artigo "Correcting the Record on the First Emoji Set" (Corrigindo o Registro [Histórico] sobre o Primeiro Conjunto de Emojis) (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, 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?

Python 3 permite identificadores não-ASCII no código-fonte:

>>> 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 Python pode interpretar nomes em Unicode, e que o UTF-8 é a codificação padrão para código-fonte, não vejo motivo para codificar identificadores em português sem acentos, como fazíamos no Python 2, por necessidade—a menos que seu código tenha que rodar também no Python 2. Se os nomes estão em português, excluir os acentos não vai tornar o código mais legível para ninguém.

Esse é meu ponto de vista como um brasileiro falante de português, mas acredito que se aplica além de fronteiras e a outras culturas: escolha a linguagem humana que torna o código mais legível para sua equipe, e então use todos os caracteres necessários para a ortografia correta.

O que é "texto puro"?

Para qualquer um que lide diariamente com texto em línguas diferentes do inglês, "texto puro" não significa "ASCII". O Glossário do Unicode (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, que é parte do conjunto de ferramentas do excelente Atlas book publishing platform (plataforma de publicação de livros Atlas) da O’Reilly. Os arquivos fonte de AsciiDoc são texto puro, mas são UTF-8, e não ASCII. Se fosse o contrário, escrever esse capítulo teria sido realmente doloroso. Apesar do nome, o AsciiDoc é muito bom.

O mundo do Unicode está em constante expansão e, nas margens, as ferramentas de apoio nem sempre existem. Nem todos os caracteres que eu queria exibir estavam disponíveis nas fontes usadas para renderizar o livro. Por isso tive que usar imagens em vez de listagens em vários exemplos desse capítulo. Por outro lado, os terminais do Ubuntu e do macOS exibem a maioria do texto Unicode muito bem—incluindo os caracteres japoneses para a palavra "mojibake": 文字化け.

Como os ponto de código numa str são representados na RAM?

A documentação oficial de 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, 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 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) (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 de 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).

Python oferece algumas formas de criar uma classe simples, apenas uma coleção de campos, com pouca ou nenhuma funcionalidade adicional. Esse padrão é conhecido como "classe de dados"—e dataclasses é um dos pacotes que suporta tal modelo. Este capítulo trata de três diferentes fábricas de classes que podem ser utilizadas como atalhos para escrever classes de dados:

collections.namedtuple

A forma mais simples—disponível desde Python 2.6.

typing.NamedTuple

Uma alternativa que requer dicas de tipo nos campos—desde 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 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.

✒️ Nota

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 (EN) de Python 3.11.

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 15.3 do Capítulo 15.

5.1. Novidades nesse capítulo

Este capítulo é novo, aparece nessa segunda edição do Python Fluente. A Seção 5.3 era parte do capítulo 2 da primeira edição, mas o restante do capítulo é inteiramente inédito.

Vamos começar por uma visão geral, por alto, das três fábricas de classes.

5.2. Visão geral das fábricas de classes de dados

Considere uma classe simples, representando um par de coordenadas geográficas, como aquela no Exemplo 65.

Exemplo 65. class/coordinates.py
class Coordinate:

    def __init__(self, lat, lon):
        self.lat = lat
        self.lon = lon

A tarefa da classe Coordinate é manter os atributos latitude e longitude. Escrever o __init__ padrão fica cansativo muito rápido, especialmente se sua classe tiver mais que alguns poucos atributos: cada um deles é mencionado três vezes! E aquele código repetitivo não nos dá sequer os recursos básicos que esperamos de um objeto Python:

>>> from coordinates import Coordinate
>>> moscow = Coordinate(55.76, 37.62)
>>> moscow
<coordinates.Coordinate object at 0x107142f10>  (1)
>>> location = Coordinate(55.76, 37.62)
>>> location == moscow  (2)
False
>>> (location.lat, location.lon) == (moscow.lat, moscow.lon)  (3)
True
  1. O __repr__ herdado de object não é muito útil.

  2. O == não faz sentido; o método __eq__ herdado de object compara os IDs dos objetos.

  3. Comparar duas coordenadas exige a comparação explícita de cada atributo.

As fábricas de classes de dados tratadas nesse capítulo fornecem automaticamente os métodos __init__, __repr__, e __eq__ necessários, além alguns outros recursos úteis.

✒️ Nota

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 @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:

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

>>> 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'>}
👉 Dica

Uma tupla nomeada e com dicas de tipo pode também ser construída passando os campos como argumentos nomeados, assim:

Coordinate = typing.NamedTuple('Coordinate', lat=float, lon=float)

Além de ser mais legível, essa forma permite fornecer o mapeamento de campos e tipos como **fields_and_types.

Desde 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) (EN). É muito mais legível, e torna fácil sobrepor métodos ou acrescentar métodos novos. O Exemplo 66 é 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 66. typing_namedtuple/coordinates.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}'
⚠️ Aviso

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[56] para personalizar a criação da classe do usuário. Veja isso:

>>> issubclass(Coordinate, typing.NamedTuple)
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 (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 67.

Exemplo 67. dataclass/coordinates.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 66 e no Exemplo 67 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.[57] A classe Coordinate no Exemplo 67 é uma subclasse de object.

5.2.1. Principais recursos

As diferentes fábricas de classes de dados tem muito em comum, como resume a Tabela 12.

Tabela 12. Recursos selecionados, comparando as três fábricas de classes de dados; x é uma instância de uma classe de dados daquele tipo
namedtuple NamedTuple dataclass

instâncias mutáveis

NÃO

NÃO

SIM

sintaxe de declaração de classe

NÃO

SIM

SIM

criar um dict

x._asdict()

x._asdict()

dataclasses.asdict(x)

obter nomes dos campos

x._fields

x._fields

[f.name for f in dataclasses.fields(x)]

obter defaults

x._field_defaults

x._field_defaults

[f.default for f in dataclasses.fields(x)]

obter tipos dos campos

N/A

x.__annotations__

x.__annotations__

nova instância com modificações

x._replace(…)

x._replace(…)

dataclasses.replace(x, …)

nova classe durante a execução

namedtuple(…)

NamedTuple(…)

dataclasses.make_dataclass(…)

⚠️ Aviso

As classes criadas por typing.NamedTuple e @dataclass tem um atributo __annotations__, contendo as dicas de tipo para os campos. Entretanto, ler diretamente de __annotations__ não é recomendado. Em vez disso, a melhor prática recomendada para obter tal informação é chamar inspect.get_annotations(MyClass) (a partir de Python 3.10—EN) ou typing.​get_​type_​hints(MyClass) (Python 3.5 a 3.9—EN). Isso porque tais funções fornecem serviços adicionais, como a resolução de referências futuras nas dicas de tipo. Voltaremos a isso bem mais tarde neste livro, na Seção 15.5.1.

Vamos agora detalhar aqueles recursos principais.

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

5.2.1.2. Sintaxe de declaração de classe

Apenas typing.NamedTuple e dataclass suportam a sintaxe de declaração de class regular, tornando mais fácil acrescentar métodos e docstrings à classe que está sendo criada.

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

5.2.1.4. Obter nomes dos campos e valores default

Todas as três fábricas de classes permitem que você obtenha os nomes dos campos e os valores default (que podem ser configurados para cada campo). Nas classes de tuplas nomeadas, aqueles metadados estão nos atributos de classe ._fields e ._fields_defaults. Você pode obter os mesmos metadados em uma classe decorada com dataclass usando a função fields do módulo dataclasses. Ele devolve uma tupla de objetos Field com vários atributos, incluindo name e default.

5.2.1.5. Obter os tipos dos campos

Classes definidas com a ajuda de typing.NamedTuple e @dataclass contêm um mapeamento dos nomes dos campos para seus tipos, o atributo de classe __annotations__. Como já mencionado, use a função typing.get_type_hints em vez de ler diretamente de __annotations__.

5.2.1.6. Nova instância com modificações

Dada uma instância de tupla nomeada x, a chamada x._replace(**kwargs) devolve uma nova instância com os valores de alguns atributos modificados, de acordo com os argumentos nomeados incluídos na chamada. A função de módulo dataclasses.replace(x, **kwargs) faz o mesmo para uma instância de uma classe decorada com dataclass.

5.2.1.7. Nova classe durante a execução

Apesar da sintaxe de declaração de classe ser mais legível, ela é fixa no código. Um framework pode ter a necessidade de criar classes de dados durante a execução. Para tanto, podemos usar a sintaxe default de chamada de função de collections.namedtuple, que também é suportada por typing.NamedTuple. O módulo dataclasses oferece a função make_dataclass, com o mesmo propósito.

Após essa visão geral dos principais recursos das fábricas de classes de dados, vamos examinar cada uma delas mais de perto, começando pela mais simples.

5.3. Tuplas nomeadas clássicas

A função collections.namedtuple é uma fábrica que cria subclasses de tuple, acrescidas de nomes de campos, um nome de classe, e um __repr__ informativo. Classes criadas com namedtuple podem ser usadas onde quer que uma tupla seja necessária. Na verdade, muitas funções da biblioteca padrão, que antes devolviam tuplas agora devolvem, por conveniência, tuplas nomeadas, sem afetar de forma alguma o código do usuário.

👉 Dica

Cada instância de uma classe criada por namedtuple usa exatamente a mesma quantidade de memória usada por uma tupla, pois os nomes dos campos são armazenados na classe.

O Exemplo 68 mostra como poderíamos definir uma tupla nomeada para manter informações sobre uma cidade.

Exemplo 68. Definindo e usando um tipo tupla nomeada
>>> 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ção 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 69 demonstra os mais úteis dentre eles: o atributo de classe _fields, o método de classe _make(iterable), e o método de instância _asdict().

Exemplo 69. Atributos e métodos das tuplas nomeadas (continuando do exenplo anterior)
>>> City._fields  (1)
('name', 'country', 'population', 'location')
>>> Coordinate = namedtuple('Coordinate', 'lat lon')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, Coordinate(28.613889, 77.208889))
>>> delhi = City._make(delhi_data)  (2)
>>> delhi._asdict()  (3)
{'name': 'Delhi NCR', 'country': 'IN', 'population': 21.935,
'location': Coordinate(lat=28.613889, lon=77.208889)}
>>> import json
>>> json.dumps(delhi._asdict())  (4)
'{"name": "Delhi NCR", "country": "IN", "population": 21.935,
"location": [28.613889, 77.208889]}'
  1. ._fields é uma tupla com os nomes dos campos da classe.

  2. ._make() cria uma City a partir de um iterável; City(*delhi_data) faria o mesmo.

  3. ._asdict() devolve um dict criado a partir da instância de tupla nomeada.

  4. ._asdict() é útil para serializar os dados no formato JSON, por exemplo.

⚠️ Aviso

Até Python 3.7, o método _asdict devolvia um OrderedDict. Desde Python 3.8, ele devolve um dict simples—o que não causa qualquer problema, agora que podemos confiar na ordem de inserção das chaves. Se você precisar de um OrderedDict, a documentação do _asdict (EN) recomenda criar um com o resultado: OrderedDict(x._asdict()).

Desde 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 70 mostra como definir uma tupla nomeada Coordinate com um valor default para o campo reference.

Exemplo 70. Atributos e métodos das tuplas nomeadas, continuando do Exemplo 69
>>> 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 5.2.1.2, mencionei que é mais fácil programar métodos com a sintaxe de classe suportada por typing.NamedTuple and @dataclass. Você também pode acrescentar métodos a uma namedtuple, mas é um remendo. Pule a próxima caixinha se você não estiver interessada em gambiarras.

Remendando uma tupla nomeada para injetar um método

Lembre como criamos a classe Card class no Exemplo 1 do Capítulo 1:

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 71 mostra como isso é feito:

Exemplo 71. frenchdeck.doctest: Acrescentando um atributo de classe e um método a Card, a namedtuple da Seção 1.2
>>> Card.suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)  # (1)
>>> def spades_high(card):                                            # (2)
...     rank_value = FrenchDeck.ranks.index(card.rank)                
...     suit_value = card.suit_values[card.suit]
...     return rank_value * len(card.suit_values) + suit_value
...
>>> Card.overall_rank = spades_high                                   # (3)
>>> lowest_card = Card('2', 'clubs')
>>> highest_card = Card('A', 'spades')
>>> lowest_card.overall_rank()                                        # (4)
0
>>> highest_card.overall_rank()
51
  1. Acrescenta um atributo de classe com valores para cada naipe.

  2. A função spades_high vai se tornar um método; o primeiro argumento não precisa ser chamado de self. Como um método, ela de qualquer forma terá acesso à instância que recebe a chamada.

  3. Anexa a função à classe Card como um método chamado overall_rank.

  4. Funciona!

Para uma melhor legibilidade e para ajudar na manutenção futura, é muito melhor programar métodos dentro de uma declaração class. Mas é bom saber que essa gambiarra é possível, pois às vezes pode ser útil.[58]

Isso foi apenas um pequeno desvio para demonstrar o poder de uma linguagem dinâmica.

Agora vamos ver a variante typing.NamedTuple.

5.4. Tuplas nomeadas com tipo

A classe Coordinate com um campo default, do Exemplo 70, pode ser escrita usando typing.NamedTuple, como se vê no Exemplo 72.

Exemplo 72. typing_namedtuple/coordinates2.py
from typing import NamedTuple

class Coordinate(NamedTuple):
    lat: float                # (1)
    lon: float
    reference: str = 'WGS84'  # (2)
  1. Todo campo de instância precisa ter uma anotação de tipo.

  2. O campo de instância reference é anotado com um tipo e um valor default.

As classes criadas por typing.NamedTuple não tem qualquer método além daqueles que collections.namedtuple também gera—e aquele herdados de tuple. A única diferença é a presença do atributo de classe __annotations__—que Python ignora completamente durante a execução do programa.

Dado que o principal recurso de typing.NamedTuple são as anotações de tipo, vamos dar uma rápida olhada nisso antes de continuar nossa exploração das fábricas de classes de dados.

5.5. Introdução às dicas de tipo

Dicas de tipo—também chamadas anotações de tipo—são formas de declarar o tipo esperado dos argumentos, dos valores devolvidos, das variáveis e dos atributos de funções.

A primeira coisa que você precisa saber sobre dicas de tipo é que elas não são impostas de forma alguma pelo compilador de bytecode ou pelo interpretador de Python.

✒️ Nota

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 sentido. Vamos trata de anotações de tipo nas assinaturas de função no Capítulo 8 e de anotações mais avançadas no Capítulo 15. Aqui vamos ver principalmente dicas com tipos embutidos simples, tais como str, int, e float, que são provavelmente os tipos mais comuns usados para anotar campos em classes de dados.

5.5.1. Nenhum efeito durante a execução

Pense nas dicas de tipo de Python como "documentação que pode ser verificada por IDEs e verificadores de tipo".

Isso porque as dicas de tipo não tem qualquer impacto sobre o comportamento de programas em Python durante a execução. Veja o Exemplo 73.

Exemplo 73. Python não exige dicas de tipo durante a execução de um programa
>>> import typing
>>> class Coordinate(typing.NamedTuple):
...     lat: float
...     lon: float
...
>>> trash = Coordinate('Ni!', None)
>>> print(trash)
Coordinate(lat='Ni!', lon=None)    # (1)
  1. Eu avisei: não há verificação de tipo durante a execução!

Se você incluir o código do Exemplo 73 em um módulo de Python, ela vai rodar e exibir uma Coordinate sem sentido, e sem gerar qualquer erro ou aviso:

$ 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 ou o verificador de tipo embutido do PyCharm IDE. 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:

$ 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.[59]

Vamos falar agora sobre a sintaxe e o significado das dicas de tipo.

5.5.2. Sintaxe de anotação de variáveis

Tanto typing.NamedTuple quanto @dataclass usam a sintaxe de anotações de variáveis definida na PEP 526 (EN). Vamos ver aqui uma pequena introdução àquela sintaxe, no contexto da definição de atributos em declarações class.

A sintaxe básica da anotação de variáveis é :

var_name: some_type

A seção "Acceptable type hints" (_Dicas de tipo aceitáveis), na PEP 484, explica o que são tipo aceitáveis. Porém, no contexto da definição de uma classe de dados, os tipos mais úteis geralmente serão os seguintes:

  • Uma classe concreta, por exemplo str ou FrenchDeck.

  • Um tipo de coleção parametrizada, como list[int], tuple[str, float], etc.

  • typing.Optional, por exemplo Optional[str]—para declarar um campo que pode ser uma str ou None.

Você também pode inicializar uma variável com um valor. Em uma declaração de typing.NamedTuple ou @dataclass, aquele valor se tornará o default daquele atributo quando o argumento correspondente for omitido na chamada de inicialização:

var_name: some_type = a_value

5.5.3. O significado das anotações de variáveis

Vimos, no tópico Seção 5.5.1, que dicas de tipo não tem qualquer efeito durante a execução de um programa. Mas no momento da importação—quando um módulo é carregado—o Python as lê para construir o dicionário __annotations__, que typing.NamedTuple e @dataclass então usam para aprimorar a classe.

Vamos começar essa exploração no Exemplo 74, com uma classe simples, para mais tarde ver que recursos adicionais são acrescentados por typing.NamedTuple e @dataclass.

Exemplo 74. meaning/demo_plain.py: uma classe básica com dicas de tipo
class DemoPlainClass:
    a: int           # (1)
    b: float = 1.1   # (2)
    c = 'spam'       # (3)
  1. a se torna um registro em __annotations__, mas é então descartada: nenhum atributo chamado a é criado na classe.

  2. b é salvo como uma anotação, e também se torna um atributo de classe com o valor 1.1.

  3. c é só um bom e velho atributo de classe básico, sem uma anotação.

Podemos checar isso no console, primeiro lendo o __annotations__ da DemoPlainClass, e daí tentando obter os atributos chamados a, b, e c:

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

5.5.3.1. Inspecionando uma typing.NamedTuple

Agora vamos examinar uma classe criada com typing.NamedTuple (Exemplo 75), usando os mesmos atributos e anotações da DemoPlainClass do Exemplo 74.

Exemplo 75. meaning/demo_nt.py: uma classe criada com typing.NamedTuple
import typing

class DemoNTClass(typing.NamedTuple):
    a: int           # (1)
    b: float = 1.1   # (2)
    c = 'spam'       # (3)
  1. a se torna uma anotação e também um atributo de instância.

  2. b é outra anotação, mas também se torna um atributo de instância com o valor default 1.1.

  3. c é só um bom e velho atributo de classe comum; não será mencionado em nenhuma anotação.

Inspecionando a DemoNTClass, temos o seguinte:

>>> from demo_nt import DemoNTClass
>>> DemoNTClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoNTClass.a
<_collections._tuplegetter object at 0x101f0f940>
>>> DemoNTClass.b
<_collections._tuplegetter object at 0x101f0f8b0>
>>> DemoNTClass.c
'spam'

Aqui vemos as mesmas anotações para a e b que vimos no Exemplo 74. Mas typing.NamedTuple cria os atributos de classe a e b. O atributo c é apenas um atributo de classe simples, com o valor 'spam'.

Os atributos de classe a e b são descritores (descriptors)—um recurso avançado tratado no Capítulo 23. Por ora, pense neles como similares a um getter de propriedades do objeto[61]: métodos que não exigem o operador explícito de chamada () para obter um atributo de instância. Na prática, isso significa que a e b vão funcionar como atributos de instância somente para leitura—o que faz sentido, se lembrarmos que instâncias de DemoNTClass são apenas tuplas chiques, e tuplas são imutáveis.

A DemoNTClass também recebe uma docstring personalizada:

>>> DemoNTClass.__doc__
'DemoNTClass(a, b)'

Vamos examinar uma instância de DemoNTClass:

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

5.5.3.2. Inspecionando uma classe decorada com dataclass

Vamos agora examinar o Exemplo 76.

Exemplo 76. meaning/demo_dc.py: uma classe decorada com @dataclass
from dataclasses import dataclass

@dataclass
class DemoDataClass:
    a: int           # (1)
    b: float = 1.1   # (2)
    c = 'spam'       # (3)
  1. a se torna uma anotação, e também um atributo de instância controlado por um descritor.

  2. b é outra anotação, e também se torna um atributo de instância com um descritor e um valor default de 1.1.

  3. c é apenas um atributo de classe comum; nenhuma anotação se refere a ele.

Podemos então verificar o __annotations__, o __doc__, e os atributos a, b, c no Demo​DataClass:

>>> from demo_dc import DemoDataClass
>>> DemoDataClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoDataClass.__doc__
'DemoDataClass(a: int, b: float = 1.1)'
>>> DemoDataClass.a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'DemoDataClass' has no attribute 'a'
>>> DemoDataClass.b
1.1
>>> DemoDataClass.c
'spam'

O __annotations__ e o __doc__ não guardam surpresas. Entretanto, não há um atributo chamado a em DemoDataClass—diferente do que ocorre na DemoNTClass do Exemplo 75, que inclui um descritor para obter a das instâncias da classe, como atributos somente para leitura (aquele misterioso <_collections.tuplegetter>). Isso ocorre porque o atributo a só existirá nas instâncias de DemoDataClass. Será um atributo público, que poderemos obter e definir, a menos que a classe seja frozen. Mas b e c existem como atributos de classe, com b contendo o valor default para o atributo de instância b, enquanto c é apenas um atributo de classe que não será vinculado a instâncias.

Vejamos como se parece uma instância de DemoDataClass:

>>> dc = DemoDataClass(9)
>>> dc.a
9
>>> dc.b
1.1
>>> dc.c
'spam'

Novamente, a e b são atributos de instância, e c é um atributo de classe obtido através da instância.

Como mencionado, instâncias de DemoDataClass são mutáveis—e nenhuma verificação de tipo é realizada durante a execução:

>>> dc.a = 10
>>> dc.b = 'oops'

Podemos fazer atribuições ainda mais ridículas:

>>> 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 de Python: instâncias regulares podem ter seus próprios atributos, que não aparecem na classe.[62]

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:

@dataclass(*, init=True, repr=True, eq=True, order=False,
              unsafe_hash=False, frozen=False)

O * na primeira posição significa que os parâmetros restantes são todos parâmetros nomeados. A Tabela 13 os descreve.

Tabela 13. Parâmetros nomeados aceitos pelo decorador @dataclass
Option Meaning Default Notes

init

Gera o __init__

True

Ignorado se o __init__ for implementado pelo usuário.

repr

Gera o __repr__

True

Ignorado se o __repr__ for implementado pelo usuário.

eq

Gera o __eq__

True

Ignorado se o __eq__ for implementado pelo usuário.

order

Gera __lt__, __le__, __gt__, __ge__

False

Se True, causa uma exceção se eq=False, ou se qualquer dos métodos de comparação que seriam gerados estiver definido ou for herdado.

unsafe_hash

Gera o __hash__

False

Semântica complexa e várias restrições—veja a: documentação de dataclass.

frozen

Cria instâncias "imutáveis"

False

As instâncias estarão razoavelmente protegidas contra mudanças acidentais, mas não serão realmente imutáveis.[63]

Os defaults são, de fato, as configurações mais úteis para os casos de uso mais comuns. As opções mais prováveis de serem modificadas de seus defaults são:

frozen=True

Protege as instâncias da classe de modificações acidentais.

order=True

Permite ordenar as instâncias da classe de dados.

Dada a natureza dinâmica de objetos Python, não é muito difícil para um programador curioso contornar a proteção oferecida por frozen=True. Mas os truques necessários são fáceis de perceber em uma revisão do código.

Se tanto o argumento eq quanto o frozen forem True, @dataclass produz um método __hash__ adequado, e daí as instâncias serão hashable. O __hash__ gerado usará dados de todos os campos que não forem individualmente excluídos usando uma opção de campo, que veremos na Seção 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) (EN) diz o seguinte sobre unsafe_hash:

Apesar de não ser recomendado, você pode forçar Classes de Dados a criarem um método __hash__ com unsafe_hash=True. Pode ser esse o caso, se sua classe for logicamente imutável e mesmo assim possa ser modificada. Este é um caso de uso especializado e deve ser considerado com cuidado.

Deixo o unsafe_hash por aqui. Se você achar que precisa usar essa opção, leia a documentação de dataclasses.dataclass.

Outras personalizações da classe de dados gerada podem ser feitas no nível dos campos.

5.6.1. Opções de campo

Já vimos a opção de campo mais básica: fornecer (ou não) um valor default junto com a dica de tipo. Os campos de instância declarados se tornarão parâmetros no __init__ gerado. Python não permite parâmetros sem um default após parâmetros com defaults. Então, após declarar um campo com um valor default, cada um dos campos seguintes deve também ter um default.

Valores default mutáveis são a fonte mais comum de bugs entre desenvolvedores Python iniciantes. Em definições de função, um valor default mutável é facilmente corrompido, quando uma invocação da função modifica o default, mudando o comportamento nas invocações posteriores—um tópico que vamos explorar na Seção 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 77.

Exemplo 77. dataclass/club_wrong.py: essa classe gera um ValueError
@dataclass
class ClubMember:
    name: str
    guests: list = []

Se você carregar o módulo com aquela classe ClubMember, o resultado será esse:

$ 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 78 mostra como corrigir a ClubMember.

Exemplo 78. dataclass/club.py: essa definição de ClubMember funciona
from dataclasses import dataclass, field

@dataclass
class ClubMember:
    name: str
    guests: list = field(default_factory=list)

No campo guests do Exemplo 78, em vez de uma lista literal, o valor default é definido chamando a função 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.

⚠️ Aviso

É 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. Outros valores mutáveis usados como default não serão apontados por @dataclass. É sua responsabilidade entender o problema e se lembrar de usar uma factory default para definir valores default mutáveis.

Se você estudar a documentação do módulo dataclasses, verá um campo list definido com uma sintaxe nova, como no Exemplo 79.

Exemplo 79. dataclass/club_generic.py: essa definição de ClubMember é mais precisa
from dataclasses import dataclass, field

@dataclass
class ClubMember:
    name: str
    guests: list[str] = field(default_factory=list)  # (1)
  1. list[str] significa "uma lista de str."

A nova sintaxe list[str] é um tipo genérico parametrizado: desde Python 3.9, o tipo embutido list aceita aquela notação com colchetes para especificar o tipo dos itens da lista.

⚠️ Aviso

Antes de 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 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 78 e o Exemplo 79 estão ambos corretos, e que o verificador de tipagem Mypy não reclama de nenhuma das duas definições de classe.

A diferença é que aquele guests: list significa que guests pode ser uma list de objetos de qualquer natureza, enquanto guests: list[str] diz que guests deve ser uma list na qual cada item é uma str. Isso permite que o verificador de tipos encontre (alguns) bugs em código que insira itens inválidos na lista, ou que leia itens dali.

A default_factory é possivelmente a opção mais comum da função field, mas há várias outras, listadas na Tabela 14.

Tabela 14. Argumentos nomeados aceitos pela função field
Option Meaning Default

default

Valor default para o campo

_MISSING_TYPE [64]

default_factory

função com 0 parâmetros usada para produzir um valor default

_MISSING_TYPE

init

Incluir o campo nos parâmetros de __init__

True

repr

Incluir o campo em __repr__

True

compare

Usar o campo nos métodos de comparação __eq__, __lt__, etc.

True

hash

Incluir o campo no cálculo de __hash__

None[65]

metadata

Mapeamento com dados definidos pelo usuário; ignorado por @dataclass

None

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:

@dataclass
class ClubMember:
    name: str
    guests: list = field(default_factory=list)
    athlete: bool = field(default=False, repr=False)

5.6.2. Processamento pós-inicialização

O método __init__ gerado por @dataclass apenas recebe os argumentos passados e os atribui—ou seus valores default, se o argumento não estiver presente—aos atributos de instância, que são campos da instância. Mas pode ser necessário fazer mais que isso para inicializar a instância. Se for esse o caso, você pode fornecer um método __post_init__. Quando esse método existir, @dataclass acrescentará código ao __init__ gerado para invocar __post_init__ como o último passo da inicialização.

Casos de uso comuns para __post_init__ são validação e o cálculo de valores de campos baseado em outros campos. Vamos estudar um exemplo simples, que usa __post_init__ pelos dois motivos.

Primeiro, dê uma olhada no comportamento esperado de uma subclasse de ClubMember, chamada HackerClubMember, como descrito por doctests no Exemplo 80.

Exemplo 80. dataclass/hackerclub.py: doctests para HackerClubMember
"""
``HackerClubMember`` objects accept an optional ``handle`` argument::

    >>> anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')
    >>> anna
    HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')

If ``handle`` is omitted, it's set to the first part of the member's name::

    >>> leo = HackerClubMember('Leo Rochael')
    >>> leo
    HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')

Members must have a unique handle. The following ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::

    >>> leo2 = HackerClubMember('Leo DaVinci')
    Traceback (most recent call last):
      ...
    ValueError: handle 'Leo' already exists.

To fix, ``leo2`` must be created with an explicit ``handle``::

    >>> leo2 = HackerClubMember('Leo DaVinci', handle='Neo')
    >>> leo2
    HackerClubMember(name='Leo DaVinci', guests=[], handle='Neo')
"""

Observe que precisamos fornecer handle como um argumento nomeado, pois HackerClubMember herda name e guests de ClubMember, e acrescenta o campo handle. A docstring gerada para HackerClubMember mostra a ordem dos campos na chamada de inicialização:

>>> HackerClubMember.__doc__
"HackerClubMember(name: str, guests: list = <factory>, handle: str = '')"

Aqui <factory> é um caminho mais curto para dizer que algum invocável vai produzir o valor default para guests (no nosso caso, a fábrica é a classe list). O ponto é o seguinte: para fornecer um handle mas não um guests, precisamos passar handle como um argumento nomeado.

A seção "Herança na documentação do módulo dataclasses explica como a ordem dos campos é analisada quando existem vários níveis de herança.

✒️ Nota

No ch_inheritance vamos falar sobre o uso indevido da herança, especialmente quando as superclasses 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 81 mais curto, e permitir que nos concentrássemos na declaração do campo handle e na validação com __post_init__.

O Exemplo 81 mostra a implementação.

Exemplo 81. dataclass/hackerclub.py: código para HackerClubMember
from dataclasses import dataclass
from club import ClubMember

@dataclass
class HackerClubMember(ClubMember):                         # (1)
    all_handles = set()                                     # (2)
    handle: str = ''                                        # (3)

    def __post_init__(self):
        cls = self.__class__                                # (4)
        if self.handle == '':                               # (5)
            self.handle = self.name.split()[0]
        if self.handle in cls.all_handles:                  # (6)
            msg = f'handle {self.handle!r} already exists.'
            raise ValueError(msg)
        cls.all_handles.add(self.handle)                    # (7)
  1. HackerClubMember estende ClubMember.

  2. all_handles é um atributo de classe.

  3. handle é um campo de instância do tipo str, com uma string vazia como valor default; isso o torna opcional.

  4. Obtém a classe da instância.

  5. Se self.handle é a string vazia, a define como a primeira parte de name.

  6. Se self.handle está em cls.all_handles, gera um ValueError.

  7. Insere o novo handle em cls.all_handles.

O Exemplo 81 funciona como esperado, mas não é satisfatório pra um verificador estático de tipos. A seguir veremos a razão disso, e como resolver o problema.

5.6.3. Atributos de classe tipados

Se verificarmos os tipos de Exemplo 81 com o Mypy, seremos repreendidos:

$ mypy hackerclub.py
hackerclub.py:37: error: Need type annotation for "all_handles"
(hint: "all_handles: Set[<type>] = ...")
Found 1 error in 1 file (checked 1 source file)

Infelizmente, a dica fornecida pelo Mypy (versão 0.910 quando essa seção foi revisada) não é muito útil no contexto do uso de @dataclass. Primeiro, ele sugere usar Set, mas desde 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 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) (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 81 assim:

    all_handles: ClassVar[set[str]] = set()

Aquela dica de tipo está dizendo o seguinte:

all_handles é um atributo de classe do tipo set-de-str, com um set vazio como valor default.

Para escrever aquela anotação precisamos também importar ClassVar do módulo typing.

O decorador @dataclass não se importa com os tipos nas anotações, exceto em dois casos, e este é um deles: se o tipo for ClassVar, um campo de instância não será gerado para aquele atributo.

O outro caso onde o tipo de um campo é relevante para @dataclass é quando declaramos variáveis apenas de inicialização, nosso próximo tópico.

5.6.4. Variáveis de inicialização que não são campos

Algumas vezes pode ser necessário passar para __init__ argumentos que não são campos de instância. Tais argumentos são chamados "argumentos apenas de inicialização" (init-only variables) pela documentação de dataclasses. Para declarar um argumento desses, o módulo dataclasses oferece o pseudo-tipo InitVar, que usa a mesma sintaxe de typing.ClassVar. O exemplo dados na documentação é uma classe de dados com um campo inicializado a partir de um banco de dados, e o objeto banco de dados precisa ser passado para o __init__.

O Exemplo 82 mostra o código que ilustra a seção "Variáveis de inicialização apenas".

Exemplo 82. Exemplo da documentação do módulo dataclasses
@dataclass
class C:
    i: int
    j: int | None = None
    database: InitVar[DatabaseType | None] = None

    def __post_init__(self, database):
        if self.j is None and database is not None:
            self.j = database.lookup('j')

c = C(10, database=my_database)

Veja como o atributo database é declarado. InitVar vai evitar que @dataclass trate database como um campo regular. Ele não será definido como um atributo de instância, e a função dataclasses.fields não vai listá-lo. Entretanto, database será um dos argumentos aceitos pelo __init__ gerado, e também será passado para o __post_init__. Ao escrever aquele método é preciso adicionar o argumento correspondente à sua assinatura, como mostra o Exemplo 82.

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 e a PEP 526—​Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (EN) têm todos os detalhes.

Na próxima seção apresento um exemplo mais completo com o @dataclass.

5.6.5. Exemplo de @dataclass: o registro de recursos do Dublin Core

Frequentemente as classes criadas com o @dataclass vão ter mais campos que os exemplos muito curtos apresentados até aqui. O Dublin Core (EN) oferece a fundação para um exemplo mais típico de @dataclass.

O Dublin Core é um esquema de metadados que visa descrever objetos digitais, tais como, videos, sons, imagens, textos e sites na web. Aplicações de Dublin Core utilizam XML e o RDF (Resource Description Framework).[66]

— Dublin Core na Wikipedia

O padrão define 15 campos opcionais; a classe Resource, no Exemplo 83, usa 8 deles.

Exemplo 83. dataclass/resource.py: código de Resource, uma classe baseada nos termos do Dublin Core
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum, auto
from datetime import date


class ResourceType(Enum):  # (1)
    BOOK = auto()
    EBOOK = auto()
    VIDEO = auto()


@dataclass
class Resource:
    """Media resource description."""
    identifier: str                                    # (2)
    title: str = '<untitled>'                          # (3)
    creators: list[str] = field(default_factory=list)
    date: Optional[date] = None                        # (4)
    type: ResourceType = ResourceType.BOOK             # (5)
    description: str = ''
    language: str = ''
    subjects: list[str] = field(default_factory=list)
  1. Esse Enum vai fornecer valores de um tipo seguro para o campo Resource.type.

  2. identifier é o único campo obrigatório.

  3. title é o primeiro campo com um default. Isso obriga todos os campos abaixo dele a fornecerem defaults.

  4. O valor de date pode ser uma instância de datetime.date ou None.

  5. O default do campo type é ResourceType.BOOK.

O Exemplo 84 mostra um doctest, para demonstrar como um registro Resource aparece no código.

Exemplo 84. dataclass/resource.py: código de Resource, uma classe baseada nos termos do Dublin Core
    >>> 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):

    >>> 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 85 é 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 85. dataclass/resource_repr.py: código para o método __repr__, implementado na classe Resource do Exemplo 83
    def __repr__(self):
        cls = self.__class__
        cls_name = cls.__name__
        indent = ' ' * 4
        res = [f'{cls_name}(']                            # (1)
        for f in fields(cls):                             # (2)
            value = getattr(self, f.name)                 # (3)
            res.append(f'{indent}{f.name} = {value!r},')  # (4)

        res.append(')')                                   # (5)
        return '\n'.join(res)                             # (6)
  1. Dá início à lista res, para criar a string de saída com o nome da classe e o parênteses abrindo.

  2. Para cada campo f na classe…​

  3. …​obtém o atributo nomeado da instância.

  4. Anexa uma linha indentada com o nome do campo e repr(value)—é isso que o !r faz.

  5. Acrescenta um parênteses fechando.

  6. Cria uma string de múltiplas linhas a partir de res, e devolve essa string.

Com esse exemplo, inspirado pelo espírito de Dublin, Ohio, concluímos nosso passeio pelas fábricas de classes de dados de Python.

Classes de dados são úteis, mas podem estar sendo usadas de forma excessiva em seu projeto. A próxima seção explica isso.

5.7. A classe de dados como cheiro no código

Independente de você implementar uma classe de dados escrevendo todo o código ou aproveitando as facilidades oferecidas por alguma das fábricas de classes descritas nesse capítulo, fique alerta: isso pode sinalizar um problema em seu design.

No Refactoring: Improving the Design of Existing Code (Refatorando: Melhorando o Design de Código Existente), 2nd ed. (Addison-Wesley), Martin Fowler e Kent Beck apresentam um catálogo de "cheiros no código"[67]—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) (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.[68]

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.

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 corrigi-lo. Soube de um líder de uma equipe de desenvolvimento que elege um "cheiro da semana", e pede às pessoas que procurem aquele cheiro e o apresentem para colegas mais experientes. Fazer isso com um cheiro por vez é uma ótima maneira de ensinar gradualmente os membros da equipe a serem programadores melhores.

A principal ideia da programação orientada a objetos é manter o comportamento e os dados juntos, na mesma unidade de código: uma classe. Se uma classe é largamente utilizada mas não tem qualquer comportamento próprio significativo, é bem provável que o código que interage com as instâncias dessa classe esteja espalhado (ou mesmo duplicado) em métodos e funções ao longo de todo o sistema—uma receita para dores de cabeça na manutenção. Por isso, as refatorações de Fowler para lidar com uma classe de dados envolvem trazer responsabilidades de volta para a classe.

Levando o que foi dito acima em consideração, há alguns cenários comuns onde faz sentido ter um classe de dados com pouco ou nenhum comportamento.

5.7.1. A classe de dados como um esboço

Nesse cenário, a classe de dados é uma implementação simplista inicial de uma classe, para dar início a um novo projeto ou módulo. Com o tempo, a classe deve ganhar seus próprios métodos, deixando de depender de métodos de outras classes para operar sobre suas instâncias. O esboço é temporário; ao final do processo, sua classe pode se tornar totalmente independente da fábrica usada inicialmente para criá-la.

Python também é muito usado para resolução rápida de problemas e para experimentaçào, e nesses casos é aceitável deixar o esboço pronto para uso.

5.7.2. A classe de dados como representação intermediária

Uma classe de dados pode ser útil para criar registros que serão exportados para o JSON ou algum outro formato de intercomunicação, ou para manter dados que acabaram de ser importados, cruzando alguma fronteira do sistema. Todas as fábricas de classes de dados de Python oferecem um método ou uma função para converter uma instância em um dict simples, e você sempre pode invocar o construtor com um dict, usado para passar argumentos nomeados expandidos com **. Um dict desses é muito similar a um registro JSON.

Nesse cenário, as instâncias da classe de dados devem ser tratadas como objetos imutáveis—mesmo que os campos sejam mutáveis, não deveriam ser modificados nessa forma intermediária. Mudá-los significa perder o principal benefício de manter os dados e o comportamento próximos. Quando o processo de importação/exportação exigir mudança nos valores, você deve implementar seus próprios métodos de fábrica, em vez de usar os métodos "as dict" existentes ou os construtores padrão.

Vamos agora mudar de assunto e aprender como escrever padrões que "casam" com instâncias de classes arbitrárias, não apenas com as sequências e mapeamentos que vimos nas seções Seção 2.6 e Seção 3.3.

5.8. Pattern Matching com instâncias de classes

Padrões de classe são projetados para "casar" com instâncias de classes por tipo e—opcionalmente—por atributos. O sujeito de um padrão de classe pode ser uma instância de qualquer classe, não apenas instâncias de classes de dados.[69]

Há três variantes de padrões de classes: simples, nomeado e posicional. Vamos estudá-las nessa ordem.

5.8.1. Padrões de classe simples

Já vimos um exemplo de padrões de classe simples usados como sub-padrões na Seção 2.6:

        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 e o ú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):

    match x:
        case float():
            do_something_with(x)

Mas isso aqui possivelmente será um bug no seu código:

    match x:
        case float:  # DANGER!!!
            do_something_with(x)

No exemplo anterior, case float: "casa" com qualquer sujeito, pois 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) (EN) da PEP 634—Structural Pattern Matching: Specification ((Pattern Matching Estrutural: Especificação):

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:

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

Se a classe não de um daqueles onze tipos embutidos "abençoados", então essas variáveis parecidas com argumentos representam padrões a serem testados com atributos de uma instância daquela classe.

5.8.2. Padrões de classe nomeados

Para entender como usar padrões de classe nomeados, observe a classe City e suas cinco instâncias no Exemplo 86, abaixo.

Exemplo 86. A classe City e algumas instâncias
import typing

class City(typing.NamedTuple):
    continent: str
    name: str
    country: str


cities = [
    City('Asia', 'Tokyo', 'JP'),
    City('Asia', 'Delhi', 'IN'),
    City('North America', 'Mexico City', 'MX'),
    City('North America', 'New York', 'US'),
    City('South America', 'São Paulo', 'BR'),
]

Dadas essas definições, a seguinte função devolve uma lista de cidades asiáticas:

def match_asian_cities():
    results = []
    for city in cities:
        match city:
            case City(continent='Asia'):
                results.append(city)
    return results

O padrão City(continent='Asia') encontra qualquer instância de City onde o atributo continent seja igual a 'Asia', independente do valor dos outros atributos.

Para coletar o valor do atributo country, você poderia escrever:

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:

        match city:
            case City(continent='Asia', country=country):
                results.append(country)

Padrões de classe nomeados são bastante legíveis, e funcionam com qualquer classe que possua atributos de instância públicos. Mas eles são um tanto prolixos.

Padrões de classe posicionais são mais convenientes em alguns casos, mas exigem suporte explícito da classe do sujeito, como veremos a seguir.

5.8.3. Padrões de classe posicionais

Dadas as definições do Exemplo 86, a seguinte função devolveria uma lista de cidades asiáticas, usando um padrão de classe posicional:

def match_asian_cities_pos():
    results = []
    for city in cities:
        match city:
            case City('Asia'):
                results.append(city)
    return results

O padrão City('Asia') encontra qualquer instância de City na qual o valor do primeiro atributo seja Asia, independente do valor dos outros atributos.

Se você quiser obter o valor do atributo country, poderia escrever:

def match_asian_countries_pos():
    results = []
    for city in cities:
        match city:
            case City('Asia', _, country):
                results.append(country)
    return results

O padrão City('Asia', _, country) encontra as mesmas cidades de antes, mas agora variável country está vinculada ao terceiro atributo da instância.

Eu falei do "primeiro" ou do "terceiro" atributos, mas o quê isso realmente significa?

City (ou qualquer classe) funciona com padrões posicionais graças a um atributo de classe especial chamado __match_args__, que as fábricas de classe vistas nesse capítulo criam automaticamente. Esse é o valor de __match_args__ na classe City:

>>> 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 11.8 vamos escrever código para definir __match_args__ em uma classe que criaremos sem a ajuda de uma fábrica de classes.

👉 Dica

Você pode combinar argumentos nomeados e posicionais em um padrão. Alguns, mas não todos, os atributos de instância disponíveis para o match podem estar listados no __match_args__. Dessa forma, algumas vezes pode ser necessário usar argumentos nomeados em um padrão, além dos argumentos posicionais.

Hora de um resumo de capítulo.

5.9. Resumo do Capítulo

O tópico principal desse capítulo foram as fábricas de classes de dados collections.namedtuple, typing.NamedTuple, e dataclasses.dataclass. Vimos como cada uma delas gera classes de dados a partir de descrições, fornecidas como argumentos a uma função fábrica ou, no caso das duas últimas, a partir de uma declaração class com dicas de tipo. Especificamente, ambas as variantes de tupla produzem subclasses de tuple, acrescentando apenas a capacidade de acessar os campos por nome, e criando também um atributo de classe _fields, que lista os nomes dos campos na forma de uma tupla de strings.

A seguir colocamos lado a lado os principais recursos de cada uma das três fábricas de classes, incluindo como extrair dados da instância como um dict, como obter os nomes e valores default dos campos, e como criar uma nova instância a partir de uma instância existente.

Isso levou ao nosso primeiro contato com dicas de tipo, especialmente aquelas usadas para anotar atributos em uma declaração class, usando a notação introduzida no Python 3.6 com a PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (EN). O aspecto provavelmente mais surpreeendente das dicas de tipo em geral é o fato delas não terem qualquer efeito durante a execução. Python continua sendo uma linguagem dinâmica. Ferramentas externas, como o Mypy, são necessárias para aproveitar a informação de tipagem na detecção de erros via análise estática do código-fonte. Após um resumo básico da sintaxe da PEP 526, estudamos os efeitos das anotações em uma classe simples e em classes criadas por typing.NamedTuple e por @dataclass.

A seguir falamos sobre os recursos mais usados dentre os oferecidos por @dataclass, e sobre a opção default_factory da função dataclasses.field. Também demos uma olhada nas dicas de pseudo-tipo especiais typing.ClassVar e dataclasses.InitVar, importantes no contexto das classes de dados. Esse tópico central foi concluído com um exemplo baseado no schema Dublin Core, ilustrando como usar dataclasses.fields para iterar sobre os atributos de uma instância de Resource em um __repr__ personalizado.

Então alertamos contra os possíveis usos abusivos das classes de dados, frustrando um princípio básico da programação orientada a objetos: os dados e as funções que acessam os dados devem estar juntos na mesma classe. Classes sem uma lógica podem ser um sinal de uma lógica fora de lugar.

Na última seção, vimos como o pattern matching funciona com instâncias de qualquer classe como sujeitos—e não apenas das classes criadas com as fábricas apresentadas nesse capítulo.

5.10. Leitura complementar

A documentação padrão de Python para as fábricas de classes de dados vistas aqui é muito boa, e inclui muitos pequenos exemplos.

Em especial para @dataclass, a maior parte da PEP 557—Data Classes (Classes de Dados) (EN) foi copiada para a documentação do módulo dataclasses . Entretanto, algumas seções informativas da PEP 557 não foram copiadas, incluindo "Why not just use namedtuple?" (Por que simplesmente não usar namedtuple?), "Why not just use typing.NamedTuple?" (Por que simplesmente não usar typing.NamedTuple?), e a seção "Rationale" (Justificativa), que termina com a seguinte Q&A:

Quando não é apropriado usar Classes de Dados?

Quando for exigida compatibilidade da API com tuplas de dicts. Quando for exigida validação de tipo além daquela oferecida pelas PEPs 484 e 526 , ou quando for exigida validação ou conversão de valores.

— Eric V. Smith
PEP 557 "Justificativa"

Para mais recursos e funcionalidade avançada, incluindo validação, o projeto attrs (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) (EN). A documentação da attrs inclui uma discussão aobre alternativas.

O autor de livros, instrutor e cientista maluco da computação Dave Beazley escreveu o cluegen, 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 de 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 abordagem é 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 de Python que de Java—a linguagem usada na primeira edição.

Ponto de vista

O verbete para "Guido" no "The Jargon File" (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 de 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.

Python sempre teve uma forma fácil de declarar um atributo de classe, se ele tiver um valor inicial. Mas atributos de instância são muito mais comuns, e os programadores Python tem sido obrigados a olhar dentro do método __init__ para encontrá-los, sempre temerosos que podem existir atributos de instância sendo criados em outro lugar na classe—ou mesmo por funções e métodos de outras classes.

Agora temos o @dataclass, viva!

Mas ele traz seus próprios problemas

Primeiro, quando você usa @dataclass, dicas de tipo não são opcionais. Pelos últimos sete anos, desde a PEP 484—Type Hints (Dicas de Tipo) (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 no lugar do @dataclass.

Em segundo lugar, a sintaxe da PEP 526 (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:

    @dataclass
    class Spam:
        repeat: int  # instance attribute

Aqui, repeat também é um atributo de instância:

    @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:

    @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:

    @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), mas aqui está uma sintaxe alternativa que eu gostaria de ver:

@dataclass
class HackerClubMember:
    .name: str                                   # (1)
    .guests: list = field(default_factory=list)
    .handle: str = ''

    all_handles = set()                          # (2)
  1. Atributos de instância devem ser declarados com um prefixo ..

  2. Qualquer nome de atributo que não tenha um prefixo . é um atributo de classe (como sempre foram).

A gramática da linguagem teria que mudar para acomodar isso. Mas acho essa forma muito legível, e ela evita o problema da exceção-da-exceção.

Queria poder pegar a máquina do tempo de Gudo emprestada e voltar a 2017, para convencer os desenvolvedores principais a aceitarem essa ideia.

6. Referências, mutabilidade, e memória

“Você está triste,” disse o Cavaleiro em um tom de voz ansioso: “deixe eu cantar para você uma canção reconfortante. […] O nome da canção se chama ‘OLHOS DE HADOQUE’.”

“Oh, esse é o nome da canção?,” disse Alice, tentando parecer interessada.

“Não, você não entendeu,” retorquiu o Cavaleiro, um pouco irritado. “É assim que o nome É CHAMADO. O nome na verdade é ‘O ENVELHECIDO HOMEM VELHO.‘”

— Adaptado de “Alice Através do Espelho e o que Ela Encontrou Lá”
de Lewis Caroll

Alice e o Cavaleiro dão o tom do que veremos nesse capítulo. O tema é a distinção entre objetos e seus nomes; um nome não é o objeto; o nome é algo diferente.

Começamos o capítulo apresentando uma metáfora para variáveis em Python: variáveis são rótulos, não caixas. Se variáveis de referência não são novidade para você, a analogia pode ainda ser útil para ilustrar questões de aliasing (“apelidamento”) para alguém.

Depois discutimos os conceitos de identidade, valor e apelidamento de objetos. Uma característica surpreendente das tuplas é revelada: elas são imutáveis, mas seus valores podem mudar. Isso leva a uma discussão sobre cópias rasas e profundas. Referências e parâmetros de funções são o tema seguinte: o problema com parâmetros mutáveis por default e formas seguras de lidar com argumentos mutáveis passados para nossas funções por clientes.

As últimas seções do capítulo tratam de coleta de lixo (“garbage collection”), o comando del e de algumas peças que Python prega com objetos imutáveis.

É um capítulo bastante árido, mas os tópicos tratados podem causar muitos bugs sutis em programas reais em Python.

6.1. Novidades nesse capítulo

Os tópicos tratados aqui são muito estáveis e fundamentais. Não foi introduzida nenhuma mudança digna de nota nesta segunda edição.

Acrescentei um exemplo usando is para testar a existência de um objeto sentinela, e um aviso sobre o mau uso do operador is no final de Seção 6.3.1.

Este capítulo estava na Parte IV, mas decidi abordar esses temas mais cedo, pois eles funcionam melhor como o encerramento da Parte II, “Estruturas de Dados”, que como abertura de “Práticas de Orientação a Objetos"

✒️ Nota

A seção sobre “Referências Fracas” da primeira edição deste livro agora é um post em fluentpython.com.

Vamos começar desaprendendo que uma variável é como uma caixa onde você guarda dados.

6.2. Variáveis não são caixas

Em 1997, fiz um curso de verão sobre Java no MIT. A professora, Lynn Stein [70] , apontou que a metáfora comum, de “variáveis como caixas”, na verdade, atrapalha o entendimento de variáveis de referência em linguagens orientadas a objetos. As variáveis em Python são como variáveis de referência em Java; uma metáfora melhor é pensar em uma variável como um rótulo (ou etiqueta) que associa um nome a um objeto. O exemplo e a figura a seguir ajudam a entender o motivo disso.

Exemplo 87 é uma interação simples que não pode ser explicada por “variáveis como caixas”. A Figura 18 ilustra o motivo de metáfora da caixa estar errada em Python, enquanto etiquetas apresentam uma imagem mais útil para entender como variáveis funcionam.

Exemplo 87. As variáveis a e b mantém referências para a mesma lista, não cópias da lista.
>>> a = [1, 2, 3]  (1)
>>> b = a          (2)
>>> a.append(4)    (3)
>>> b              (4)
[1, 2, 3, 4]
  1. Cria uma lista [1, 2, 3] e a vincula à variável a.

  2. Vincula a variável b ao mesmo valor referenciado por a.

  3. Modifica a lista referenciada por a, anexando um novo item.

  4. É possível ver o efeito através da variável b. Se você pensar em b como uma caixa que guardava uma cópia de [1, 2, 3] da caixa a, este comportamento não faz sentido.

Boxes and labels diagram
Figura 18. 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 87 é 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 88 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 88.

Exemplo 88. Variáveis são vinculadas a objetos somente após os objetos serem criados
>>> class Gizmo:
...    def __init__(self):
...         print(f'Gizmo id: {id(self)}')
...
>>> x = Gizmo()
Gizmo id: 4301489152  (1)
>>> y = Gizmo() * 10  (2)
Gizmo id: 4301489432  (3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>>
>>> dir()  (4)
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']
  1. A saída Gizmo id: … é um efeito colateral da criação de uma instância de Gizmo.

  2. Multiplicar uma instância de Gizmo levanta uma exceção.

  3. Aqui está a prova que um segundo Gizmo foi de fato instanciado antes que a multiplicação fosse tentada.

  4. Mas a variável y nunca foi criada, porque a exceção aconteceu enquanto a parte direita da atribuição estava sendo executada.

👉 Dica

Para entender uma atribuição em Python, leia primeiro o lado direito: é ali que o objeto é criado ou recuperado. Depois disso, a variável do lado esquerdo é vinculada ao objeto, como uma etiqueta colada a ele. Esqueça as caixas.

Como variáveis são apenas meras etiquetas, nada impede que um objeto tenha várias etiquetas vinculadas a si. Quando isso acontece, você tem apelidos (aliases), nosso próximo tópico.

6.3. Identidade, igualdade e apelidos

Lewis Carroll é o pseudônimo literário do Prof. Charles Lutwidge Dodgson. O Sr. Carroll não é apenas igual ao Prof. Dodgson, eles são exatamente a mesma pessoa. Exemplo 89 expressa essa ideia em Python.

Exemplo 89. charles e lewis se referem ao mesmo objeto
>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
>>> lewis = charles  (1)
>>> lewis is charles
True
>>> id(charles), id(lewis)  (2)
(4300473992, 4300473992)
>>> lewis['balance'] = 950  (3)
>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
  1. lewis é um apelido para charles.

  2. O operador is e a função id confirmam essa afirmação.

  3. Adicionar um item a lewis é o mesmo que adicionar um item a charles.

Entretanto, suponha que um impostor—vamos chamá-lo de Dr. Alexander Pedachenko—diga que é o verdadeiro Charles L. Dodgson, nascido em 1832. Suas credenciais podem ser as mesmas, mas o Dr. Pedachenko não é o Prof. Dodgson. Figura 19 ilustra esse cenário.

Alias x copy diagram
Figura 19. charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor igual.

Exemplo 90 implementa e testa o objeto alex como apresentado em Figura 19.

Exemplo 90. alex e charles são iguais quando comparados, mas alex não é charles
>>> 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 89 é um exemplo de apelidamento (aliasing). Naquele código, lewis e charles são apelidos: duas variáveis vinculadas ao mesmo objeto. Por outro lado, alex não é um apelido para charles: essas variáveis estão vinculadas a objetos diferentes. Os objetos vinculados a alex e charles tem o mesmo valor  — é isso que == compara — mas tem identidades diferentes.

Na The Python Language Reference (Referência da Linguagem Python), https://docs.python.org/pt-br/3/reference/datamodel.html#objects-values-and-types está escrito:

A identidade de um objeto nunca muda após ele ter sido criado; você pode pensar nela como o endereço do objeto na memória. O operador is compara a identidade de dois objetos; a função id() retorna um inteiro representando essa identidade.

O verdadeiro significado do ID de um objeto depende da implementação da linguagem. Em CPython, id() retorna o endereço de memória do objeto, mas outro interpretador Python pode retornar algo diferente. O ponto fundamental é que o ID será sempre um valor numérico único, e ele jamais mudará durante a vida do objeto.

Na prática, nós raramente usamos a função id() quando programamos. A verificação de identidade é feita, na maior parte das vezes, com o operador is, que compara os IDs dos objetos, então nosso código não precisa chamar id() explicitamente. A seguir vamos falar sobre is versus ==.

👉 Dica

Para o revisor técnico Leonardo Rochael, o uso mais frequente de id() ocorre durante o processo de debugging, quando o repr() de dois objetos são semelhantes, mas você precisa saber se duas referências são apelidos ou apontam para objetos diferentes. Se as referências estão em contextos diferentes — por exemplo, em stack frames diferentes — pode não ser viável usar is.

6.3.1. Escolhendo Entre == e is

O operador == compara os valores de objetos (os dados que eles contêm), enquanto is compara suas identidades.

Quando estamos programando, em geral, nos preocupamos mais com os valores que com as identidades dos objetos, então == aparece com mais frequência que is em programas Python.

Entretanto, se você estiver comparando uma variável com um singleton (um objeto único) faz mais sentido usar is. O caso mais comum, de longe, é verificar se a variável está vinculada a None. Esta é a forma recomendada de fazer isso:

x is None

E a forma apropriada de escrever sua negação é:

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:

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

Normalmente estamos mais interessados na igualdade que na identidade de objetos. Checar se o 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.[71]

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 91 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 91. t1 e t2 inicialmente são iguais, mas a mudança em um item mutável dentro da tupla t1 as torna diferentes
>>> t1 = (1, 2, [30, 40])  (1)
>>> t2 = (1, 2, [30, 40])  (2)
>>> t1 == t2  (3)
True
>>> id(t1[-1])  (4)
4302515784
>>> t1[-1].append(99)  (5)
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1])  (6)
4302515784
>>> t1 == t2  (7)
False
  1. t1 é imutável, mas t1[-1] é mutável.

  2. Cria a tupla t2, cujos itens são iguais àqueles de t1.

  3. Apesar de serem objetos distintos, quando comparados t1 e t2 são iguais, como esperado.

  4. Obtém o ID da lista na posição t1[-1].

  5. Modifica diretamente a lista t1[-1].

  6. O ID de t1[-1] não mudou, apenas seu valor.

  7. t1 e t2 agora são diferentes

Essa imutabilidade relativa das tuplas está por trás do enigma Seção 2.8.3. Essa também é razão pela qual não é possível gerar o hash de algumas tuplas, como vimos em Seção 3.4.1.

A distinção entre igualdade e identidade tem outras implicações quando você precisa copiar um objeto. Uma cópia é um objeto igual com um ID diferente. Mas se um objeto contém outros objetos, é preciso que a cópia duplique os objetos internos ou eles podem ser compartilhados? Não há uma resposta única. A seguir discutimos esse ponto.

6.4. A princípio, cópias são rasas

A forma mais fácil de copiar uma lista (ou a maioria das coleções mutáveis nativas) é usando o construtor padrão do próprio tipo. Por exemplo:

>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1)  (1)
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1  (2)
True
>>> l2 is l1  (3)
False
  1. list(l1) cria uma cópia de l1.

  2. As cópias são iguais…​

  3. …​mas se referem a dois objetos diferentes.

Para listas e outras sequências mutáveis, o atalho l2 = l1[:] também cria uma cópia.

Contudo, tanto o construtor quanto [:] produzem uma cópia rasa (shallow copy). Isto é, o contêiner externo é duplicado, mas a cópia é preenchida com referências para os mesmos itens contidos no contêiner original. Isso economiza memória e não causa qualquer problema se todos os itens forem imutáveis. Mas se existirem itens mutáveis, isso pode gerar surpresas desagradáveis.

Em Exemplo 92 criamos uma lista contendo outra lista e uma tupla, e então fazemos algumas mudanças para ver como isso afeta os objetos referenciados.

👉 Dica

Se você tem um computador conectado à internet disponível, recomendo fortemente que você assista à animação interativa do Exemplo 92 em Online Python Tutor. No momento em 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 92. 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
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)      # (1)
l1.append(100)     # (2)
l1[1].remove(55)   # (3)
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22]  # (4)
l2[2] += (10, 11)  # (5)
print('l1:', l1)
print('l2:', l2)
  1. l2 é uma cópia rasa de l1. Este estado está representado em Figura 20.

  2. Concatenar 100 a l1 não tem qualquer efeito sobre l2.

  3. Aqui removemos 55 da lista interna l1[1]. Isso afeta l2, pois l2[1] está associado à mesma lista em l1[1].

  4. Para um objeto mutável como a lista referida por l2[1], o operador += altera a lista diretamente. Essa mudança é visível em l1[1], que é um apelido para l2[1].

  5. += em uma tupla cria uma nova tupla e reassocia a variável l2[2] a ela. Isso é equivalente a fazer l2[2] = l2[2] + (10, 11). Agora as tuplas na última posição de l1 e l2 não são mais o mesmo objeto. Veja Figura 21.

References diagram
Figura 20. Estado do programa imediatamente após a atribuição l2 = list(l1) em Exemplo 92. 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 92 é Exemplo 93, e o estado final dos objetos está representado em Figura 21.

Exemplo 93. Saída de Exemplo 92
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)]
References diagram
Figura 21. Estado final de l1 e l2: elas ainda compartilham referências para o mesmo objeto lista, que agora contém [66, 44, 33, 22], mas a operação l2[2] += (10, 11) criou uma nova tupla com conteúdo (7, 8, 9, 10, 11), sem relação com a tupla (7, 8, 9) referenciada por l1[2]. (Diagram generated by the Online Python Tutor.)

Já deve estar claro que cópias rasas são fáceis de criar, mas podem ou não ser o que você quer. Nosso próximo tópico é a criação de cópias profundas.

6.4.1. Cópias profundas e cópias rasas

Trabalhar com cópias rasas nem sempre é um problema, mas algumas vezes você vai precisar criar cópias profundas (isto é, cópias que não compartilham referências de objetos internos). O módulo copy oferece as funções deepcopy e copy, que retornam cópias profundas e rasas de objetos arbitrários.

Para ilustrar o uso de copy() e deepcopy(), Exemplo 94 define uma classe simples, Bus, representando um ônibus escolar que é carregado com passageiros, e então pega ou deixa passageiros ao longo de sua rota.

Exemplo 94. Bus pega ou deixa passageiros
class Bus:

    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

Agora, no Exemplo 95 interativo, vamos criar um objeto bus (bus1) e dois clones—uma cópia rasa (bus2) e uma cópia profunda (bus3)—para ver o que acontece quando bus1 deixa um passageiro.

Exemplo 95. Os efeitos do uso de copy versus deepcopy
>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752)  (1)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David']          (2)
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800)  (3)
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']  (4)
  1. Usando copy e deepcopy, criamos três instâncias distintas de Bus.

  2. Após bus1 deixar 'Bill', ele também desaparece de bus2.

  3. A inspeção do atributo dos passengers mostra que bus1 e bus2 compartilham o mesmo objeto lista, pois bus2 é uma cópia rasa de bus1.

  4. bus3 é uma cópia profunda de bus1, então seu atributo passengers se refere a outra lista.

Observe que, em geral, criar cópias profundas não é uma questão simples. Objetos podem conter referências cíclicas que fariam um algoritmo ingênuo entrar em um loop infinito. A função 'deepcopy' lembra dos objetos já copiados, de forma a tratar referências cíclicas de modo elegante. Isso é demonstrado em Exemplo 96.

Exemplo 96. Referências cíclicas: b tem uma referência para a e então é concatenado a a; ainda assim, deepcopy consegue copiar a.
>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]

Além disso, algumas vezes uma cópia profunda pode ser profunda demais. Por exemplo, objetos podem ter referências para recursos externos ou para singletons (objetos únicos) que não devem ser copiados. Você pode controlar o comportamento de copy e de deepcopy implementando os métodos especiais __copy__ e __deepcopy__, como descrito em https://docs.python.org/pt-br/3/library/copy.html [documentação do módulo copy]

O compartilhamento de objetos através de apelidos também explica como a passagens de parâmetros funciona em Python, e o problema do uso de tipos mutáveis como parâmetros default. Vamos falar sobre essas questões a seguir.

6.5. Parâmetros de função como referências

O único modo de passagem de parâmetros em Python é a chamada por compartilhamento (call by sharing). É o mesmo modo usado na maioria das linguagens orientadas a objetos, incluinde Javascript, Ruby e Java (em Java isso se aplica aos tipos de referência; tipos primitivos usam a chamada por valor). Chamada por compartilhamento significa que cada parâmetro formal da função recebe uma cópia de cada referência nos argumentos. Em outras palavras, os parâmetros dentro da função se tornam apelidos dos argumentos.

O resultado desse esquema é que a função pode modificar qualquer objeto mutável passado a ela como parâmetro, mas não pode mudar a identidade daqueles objetos (isto é, ela não pode substituir integralmente um objeto por outro). Exemplo 97 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 97. Uma função pode mudar qualquer objeto mutável que receba
>>> def f(a, b):
...     a += b
...     return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y  (1)
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b  (2)
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)  (3)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))
  1. O número x não se altera.

  2. A lista a é alterada.

  3. A tupla t não se altera.

Outra questão relacionada a parâmetros de função é o uso de valores mutáveis como defaults, discutida a seguir.

6.5.1. Porque evitar tipos mutáveis como default em parâmetros

Parâmetros opcionais com valores default são um ótimo recurso para definição de funções em Python, permitindo que nossas APIs evoluam mantendo a compatibilidade com versões anteriores. Entretanto, você deve evitar usar objetos mutáveis como valores default em parâmetros.

Para ilustrar esse ponto, em Exemplo 98, modificamos o método __init__ da classe Bus de Exemplo 94 para criar HauntedBus. Tentamos ser espertos: em vez do valor default passengers=None, temos passengers=[], para evitar o if do __init__ anterior. Essa "esperteza" causa problemas.

Exemplo 98. Uma classe simples ilustrando o perigo de um default mutável
class HauntedBus:
    """A bus model haunted by ghost passengers"""

    def __init__(self, passengers=[]):  # (1)
        self.passengers = passengers  # (2)

    def pick(self, name):
        self.passengers.append(name)  # (3)

    def drop(self, name):
        self.passengers.remove(name)
  1. Quando o argumento passengers não é passado, esse parâmetro é vinculado ao objeto lista default, que inicialmente está vazio.

  2. Essa atribuição torna self.passengers um apelido de passengers, que por sua vez é um apelido para a lista default, quando um argumento passengers não é passado para a função.

  3. Quando os métodos .remove() e .append() são usados com self.passengers, estamos, na verdade, mudando a lista default, que é um atributo do objeto-função.

Exemplo 99 mostra o comportamento misterioso de HauntedBus.

Exemplo 99. Ônibus assombrados por passageiros fantasmas
>>> bus1 = HauntedBus(['Alice', 'Bill'])  (1)
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers  (2)
['Bill', 'Charlie']
>>> bus2 = HauntedBus()  (3)
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus()  (4)
>>> bus3.passengers  (5)
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers  (6)
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers  (7)
True
>>> bus1.passengers  (8)
['Bill', 'Charlie']
  1. bus1 começa com uma lista de dois passageiros.

  2. Até aqui, tudo bem: nenhuma surpresa em bus1.

  3. bus2 começa vazio, então a lista vazia default é vinculada a self.passengers.

  4. bus3 também começa vazio, e novamente a lista default é atribuída.

  5. A lista default não está mais vazia!

  6. Agora Dave, pego pelo bus3, aparece no bus2.

  7. O problema: bus2.passengers e bus3.passengers se referem à mesma lista.

  8. Mas bus1.passengers é uma lista diferente.

O problema é que instâncias de HauntedBus que não recebem uma lista de passageiros inicial acabam todas compartilhando a mesma lista de passageiros entre si.

Este tipo de bug pode ser muito sutil. Como Exemplo 99 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 99, você pode inspecionar o objeto HauntedBus.__init__ e ver os estudantes fantasma assombrando o atributo __defaults__:

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

>>> 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 94, __init__ checa se o argumento passengers é None. Se for, self.passengers é vinculado a uma nova lista vazia. Se passengers não for None, a implementação correta vincula uma cópia daquele argumento a self.passengers. A próxima seção explica porque copiar o argumento é uma boa prática.

6.5.2. Programação defensiva com argumentos mutáveis

Ao escrever uma função que recebe um argumento mutável, você deve considerar com cuidado se o cliente que chama sua função espera que o argumento passado seja modificado.

Por exemplo, se sua função recebe um dict e precisa modificá-lo durante seu processamento, esse efeito colateral deve ou não ser visível fora da função? A resposta, na verdade, depende do contexto. É tudo uma questão de alinhar as expectativas do autor da função com as do cliente da função.

O último exemplo com ônibus neste capítulo mostra como o TwilightBus viola as expectativas ao compartilhar sua lista de passageiros com seus clientes. Antes de estudar a implementação, veja como a classe TwilightBus funciona pela perspectiva de um cliente daquela classe, em Exemplo 100.

Exemplo 100. Passageiros desaparecem quando são deixados por um TwilightBus
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']  (1)
>>> bus = TwilightBus(basketball_team)  (2)
>>> bus.drop('Tina')  (3)
>>> bus.drop('Pat')
>>> basketball_team  (4)
['Sue', 'Maya', 'Diana']
  1. basketball_team contém o nome de cinco estudantes.

  2. Um TwilightBus é carregado com o time.

  3. O bus deixa uma estudante, depois outra.

  4. As passageiras desembarcadas desapareceram do time de basquete!

TwilightBus viola o "Princípio da Menor Surpresa Possível", uma boa prática do design de interfaces.[72] É certamente espantoso que quando o ônibus deixa uma estudante, seu nome seja removido da escalação do time de basquete.

Exemplo 101 é a implementação de TwilightBus e uma explicação do problema.

Exemplo 101. Uma classe simples mostrando os perigos de mudar argumentos recebidos
class TwilightBus:
    """A bus model that makes passengers vanish"""

    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []  # (1)
        else:
            self.passengers = passengers  #(2)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)  # (3)
  1. Aqui nós cuidadosamente criamos uma lista vazia quando passengers é None.

  2. Entretanto, esta atribuição transforma self.passengers em um apelido para passengers, que por sua vez é um apelido para o argumento efetivamente passado para __init__ (i.e. basketball_team em Exemplo 100).

  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 94:

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

👉 Dica

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

Os objetos nunca são destruídos explicitamente; no entanto, quando eles se tornam inacessíveis, eles 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.

>>> a = [1, 2]  (1)
>>> b = a       (2)
>>> del a       (3)
>>> b           (4)
[1, 2]
>>> b = [3]     (5)
  1. Cria o objeto [1, 2] e vincula a a ele.

  2. Vincula b ao mesmo objeto [1, 2].

  3. Apaga a referência a.

  4. [1, 2] não é afetado, pois b ainda aponta para ele.

  5. Reassociar b a um objeto diferente remove a última referência restante a [1, 2]. Agora o coletor de lixo pode descartar aquele objeto.

⚠️ Aviso

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 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__ 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" (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 102 usa weakref.finalize para registrar uma função callback a ser chamada quando o objeto é destruído.

Exemplo 102. Assistindo o fim de um objeto quando não resta nenhuma referência apontando para ele
>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1         (1)
>>> def bye():      (2)
...     print('...like tears in the rain.')
...
>>> ender = weakref.finalize(s1, bye)  (3)
>>> ender.alive  (4)
True
>>> del s1
>>> ender.alive  (5)
True
>>> s2 = 'spam'  (6)
...like tears in the rain.
>>> ender.alive
False
  1. s1 e s2 são apelidos do mesmo conjunto, {1, 2, 3}.

  2. Essa função não deve ser um método associado ao objeto prestes a ser destruído, nem manter uma referência para o objeto.

  3. Registra o callback bye no objeto referenciado por s1.

  4. O atributo .alive é True antes do objeto finalize ser chamado.

  5. Como vimos, del não apaga o objeto, apenas a referência s1 a ele.

  6. Reassociar a última referência, s2, torna {1, 2, 3} inacessível. Ele é destruído, o callback bye é invocado, e ender.alive se torna False.

O ponto principal de Exemplo 102 é 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 102. 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.

6.7. Peças que Python prega com imutáveis

✒️ Nota

Esta seção opcional discute alguns detalhes que, na verdade, não são muito importantes para 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.[73]

Exemplo 103 demonstra esse fato.

Exemplo 103. Uma tupla construída a partir de outra é, na verdade, exatamente a mesma tupla.
>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1  (1)
True
>>> t3 = t1[:]
>>> t3 is t1  (2)
True
  1. t1 e t2 estão vinculadas ao mesmo objeto

  2. Assim como t3.

O mesmo comportamento pode ser observado com instâncias de str, bytes e frozenset. Observe que frozenset não é uma sequência, então fs[:] não funciona se fs é um frozenset. Mas fs.copy() tem o mesmo efeito: ele trapaceia e retorna uma referência ao mesmo objeto, e não uma cópia, como mostra Exemplo 104.[74]

Exemplo 104. Strings literais podem criar objetos compartilhados.
>>> t1 = (1, 2, 3)
>>> t3 = (1, 2, 3)  # (1)
>>> t3 is t1  # (2)
False
>>> s1 = 'ABC'
>>> s2 = 'ABC'  # (3)
>>> s2 is s1 # (4)
True
  1. Criando uma nova tupla do zero.

  2. t1 e t3 são iguais, mas não são o mesmo objeto.

  3. Criando uma segunda str do zero.

  4. Surpresa: a e b se referem à mesma str!

O compartilhamento de strings literais é uma técnica de otimização chamada internalização (interning). O CPython usa uma técnica similar com inteiros pequenos, para evitar a duplicação desnecessária de números que aparecem com muita frequência em programas, como 0, 1, -1, etc. Observe que o CPython não internaliza todas as strings e inteiros, e o critério pelo qual ele faz isso é um detalhe de implementação não documentado.

⚠️ Aviso

Nunca dependa da internalização de str ou int! Sempre use == em vez de is para verificar a 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.[75]

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

Se duas variáveis se referem a objetos imutáveis de igual valor (a == b is True), na prática, dificilmente importa se elas se referem a cópias de mesmo valor ou são apelidos do mesmo objeto, porque o valor de objeto imutável não muda, com uma exceção. A exceção são as coleções imutáveis, como as tuplas: se uma coleção imutável contém referências para itens mutáveis, então seu valor pode de fato mudar quando o valor de um item mutável for modificado. Na prática, esse cenário não é tão comum. O que nunca muda numa coleção imutável são as identidades dos objetos mantidos ali. A classe frozenset não sofre desse problema, porque ela só pode manter elementos hashable, e o valor de um objeto hashable não pode mudar nunca, por definição.

O fato de variáveis manterem referências tem muitas consequências práticas para a programação em Python:

  • Uma atribuição simples não cria cópias.

  • Uma atribuição composta com += ou *= cria novos objetos se a variável à esquerda da atribuição estiver vinculada a um objeto imutável, mas pode modificar um objeto mutável diretamente.

  • Atribuir um novo valor a uma variável existente não muda o objeto previamente vinculado à variável. Isso se chama reassociar (rebinding); a variável está agora associada a um objeto diferente. Se aquela variável era a última referência ao objeto anterior, aquele objeto será eliminado pela coleta de lixo.

  • Parâmetros de função são passados como apelidos, o que significa que a função pode alterar qualquer objeto mutável recebido como argumento. Não há como evitar isso, exceto criando cópias locais ou usando objetos imutáveis (i.e., passando uma tupla em vez de uma lista)

  • Usar objetos mutáveis como valores default de parâmetros de função é perigoso, pois se os parâmetros forem modificados pela função, o default muda, afetando todas as chamadas posteriores que usem o default.

Em CPython, os objetos são descartados assim que o número de referências a eles chega a zero. Eles também podem ser descartados se formarem grupos com referências cíclicas sem nenhuma referência externa ao grupo.

Em algumas situações, pode ser útil manter uma referência para um objeto que não irá — por si só — manter o objeto vivo. Um exemplo é uma classe que queira manter o registro de todas as suas instâncias atuais. Isso pode ser feito com referências fracas, um mecanismo de baixo nível encontrado nas úteis coleções WeakValueDictionary, WeakKeyDictionary, WeakSet, e na função finalize do módulo weakref.

Para saber mais, leia "Weak References" em fluentpython.com.

6.9. Para saber mais

O capítulo "Modelo de Dados" de A Referência da Linguagem Python inicia com uma explicação bastante clara sobre identidades e valores de objetos.

Wesley Chun, autor da série Core Python, apresentou Understanding Python’s Memory Model, Mutability, and Methods (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" (EN) e "weakref—Garbage-Collectable References to Objects" (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¶, 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" 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" (EN) no Python Developer’s Guide, 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 (EN).

A Wikipedia tem um artigo sobre string interning (EN), que menciona o uso desta técnica em várias linguagens, incluindo Python.

A Wikipedia também tem um artigo sobre "Haddocks' Eyes", 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 de 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 ainda uma linguagem pura. Instâncias de classes definidas pelo usuário são mutáveis por default em Python — como na maioria das linguagens orientadas a objetos. Ao criar seus próprios objetos, você tem que tomar o cuidado adicional de torná-los imutáveis, se este for um requisito. Cada atributo do objeto precisa ser também imutável, senão você termina criando algo como uma tupla: imutável quanto ao ID do objeto, mas seu valor pode mudar se a tupla contiver um objeto mutável.

Objetos mutáveis também são a razão pela qual programar com threads é tão difícil: threads modificando objetos sem uma sincronização apropriada podem corromper dados. Sincronização excessiva, por outro lado, causa deadlocks. A linguagem e a plataforma Erlang — que inclui Elixir — foi projetada para maximizar o tempo de execução em aplicações distribuídas de alta concorrência, tais como aplicações de controle de telecomunicações. Naturalmente, eles escolheram tornar os dados imutáveis por default.

Destruição de objetos e coleta de lixo

Não há qualquer mecanismo em Python para destruir um objeto diretamente, e essa omissão é, na verdade, uma grande qualidade: se você pudesse destruir um objeto a qualquer momento, o que aconteceria com as referências que apontam para ele?

A coleta de lixo em CPython é feita principalmente por contagem de referências, que é fácil de implementar, mas vulnerável a vazamentos de memória (memory leaks) quando existem referências cíclicas. Assim, com a versão 2.0 (de outubro de 2000), um coletor de lixo geracional foi implementado, e ele consegue dispor de objetos inatingíveis que foram mantidos vivos por ciclos de referências.

Mas a contagem de referências ainda está lá como mecanismo básico, e ela causa a destruição imediata de objetos com zero referências. Isso significa que, em CPython — pelo menos por hora — é seguro escrever:

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:

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" (EN), onde eu aprendi esses detalhes sobre a segurança de open().write() em CPython.

Passagem de parâmetros: chamada por compartilhamento

Uma maneira popular de explicar como a passagem de parâmetros funciona em Python é a frase: "Parâmetros são passados por valor, mas os valores são referências." Isso não está errado, mas causa confusão porque os modos mais comuns de passagem de parâmetros nas linguagens antigas são chamada por valor (a função recebe uma cópia dos argumentos) e chamada por referência (a função recebe um ponteiro para o argumento). Em Python, a função recebe uma cópia dos argumentos, mas os argumentos são sempre referências. Então o valor dos objetos referenciados podem ser alterados pela função, se eles forem mutáveis, mas sua identidade não. Além disso, como a função recebe uma cópia da referência em um argumento, reassociar essa referência no corpo da função não tem qualquer efeito fora da função. Adotei o termo chamada por compartilhamento depois de ler sobre esse assunto em Programming Language Pragmatics, 3rd ed., de Michael L. Scott (Morgan Kaufmann), section "8.3.1: Parameter Modes."

Parte II: Funções como objetos

7. Funções como objetos de primeira classe

Nunca achei que 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 Python como uma linguagem funcional.[77][78]

— Guido van Rossum
BDFL de Python

No Python, funções são objetos de primeira classe. Estudiosos de linguagens de programação definem um "objeto de primeira classe" como uma entidade programática que pode ser:

  • Criada durante a execução de um programa

  • Atribuída a uma variável ou a um elemento em uma estrutura de dados

  • Passada como argumento para uma função

  • Devolvida como o resultado de uma função

Inteiros, strings e dicionários são outros exemplos de objetos de primeira classe no Python—nada de incomum aqui. Tratar funções como objetos de primeira classe é um recurso essencial das linguagens funcionais, tais como Clojure, Elixir e Haskell. Entretanto, funções de primeira classe são tão úteis que foram adotadas por linguagens muito populares, como o Javascript, o Go e o Java (desde o JDK 8), nenhuma das quais alega ser uma "linguagem funcional".

Esse capítulo e quase toda a Parte III do livro exploram as aplicações práticas de se tratar funções como objetos.

👉 Dica

O termo "funções de primeira classe" é largamente usado como uma forma abreviada de "funções como objetos de primeira classe". Ele não é ideal, por sugerir a existência de uma "elite" entre funções. No Python, todas as funções são de primeira classe.

7.1. Novidades nesse capítulo

A seção "Os nove sabores de objetos invocáveis" (Seção 7.5) se chamava "Sete sabores de objetos invocáveis" na primeira edição deste livro. Os novos invocáveis são corrotinas nativas e geradores assíncronos, introduzidos no Python 3.5 e 3.6, respectivamente. Ambos serão estudados no Capítulo 21, mas são mencionados aqui ao lado dos outros invocáveis.

A Seção 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 15.5. Quando escrevi a primeira edição, a PEP 484—Type Hints (Dicas de Tipo) (EN) ainda estava sendo considerada, e as anotações eram usadas de várias formas diferentes. Desde 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.

✒️ Nota

A primeira edição desse livro continha seções sobre a introspecção de objetos função, que desciam a 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), no fluentpython.com.

Agora vamos ver porque as funções de Python são objetos completos.

7.2. Tratando uma função como um objeto

A sessão de console no Exemplo 105 mostra que funções de Python são objetos. Ali criamos uma função, a chamamos, lemos seu atributo __doc__ e verificamos que o próprio objeto função é uma instância da classe function.

Exemplo 105. Cria e testa uma função, e então lê seu __doc__ e verifica seu tipo
>>> 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 de Python, o comando help(factorial) mostrará uma tela como a da Figura 22.

Tela de ajuda da função factorial
Figura 22. Tela de ajuda para factorial; o texto é criado a partir do atributo __doc__ da função.

O Exemplo 106 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. 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 106. Usa factorial usando de um nome diferentes, e passa factorial como um argumento
>>> 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 é o uso de funções de ordem superior, nosso próximo tópico.

7.3. Funções de ordem superior

Uma função que recebe uma função como argumento ou devolve uma função como resultado é uma função de ordem superior. Uma dessas funções é map, usada no Exemplo 106. Outra é a função embutida sorted: o argumento opcional key permite fornecer uma função, que será então aplicada na ordenação de cada item, como vimos na Seção 2.9. Por exemplo, para ordenar uma lista de palavras por tamanho, passe a função len como key, como no Exemplo 107.

Exemplo 107. Ordenando uma lista de palavras por tamanho
>>> 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 108, observe que as palavras na lista não são modificadas de forma alguma; apenas suas versões escritas na ordem inversa são utilizadas como critério de ordenação. Por isso as berries aparecem juntas.

Exemplo 108. Ordenando uma lista de palavras pela ordem inversa de escrita
>>> def reverse(word):
...     return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>

No paradigma funcional de programação, algumas das funções de ordem superior mais conhecidas são map, filter, reduce, e apply. A função apply foi descontinuada no Python 2.3 e removida no Python 3, por não ser mais necessária. Se você precisar chamar uma função com um conjuntos dinâmico de argumentos, pode escrever fn(*args, **kwargs) no lugar de apply(fn, args, kwargs).

As funções de ordem superior map, filter, e reduce ainda estão por aí, mas temos alternativas melhores para a maioria de seus casos de uso, como mostra a próxima seção.

7.3.1. Substitutos modernos para map, filter, e reduce

Linguagens funcionais normalmente oferecem as funções de ordem superior map, filter, and reduce (algumas vezes com nomes diferentes). As funções map e filter ainda estão embutidas no Python mas, desde a introdução das compreensões de lista e das expressões geradoras, não são mais tão importantes. Uma listcomp ou uma genexp fazem o mesmo que map e filter combinadas, e são mais legíveis. Considere o Exemplo 109.

Exemplo 109. Listas de fatoriais produzidas com map e filter, comparadas com alternativas escritas com compreensões de lista
>>> list(map(factorial, range(6)))  (1)
[1, 1, 2, 6, 24, 120]
>>> [factorial(n) for n in range(6)]  (2)
[1, 1, 2, 6, 24, 120]
>>> list(map(factorial, filter(lambda n: n % 2, range(6))))  (3)
[1, 6, 120]
>>> [factorial(n) for n in range(6) if n % 2]  (4)
[1, 6, 120]
>>>
  1. Cria uma lista de fatoriais de 0! a 5!.

  2. Mesma operação, com uma compreensão de lista.

  3. Lista de fatoriais de números ímpares até 5!, usando map e filter.

  4. A compreensão de lista realiza a mesma tarefa, substituindo map e filter, e tornando lambda desnecessário.

No Python 3, map e filter devolvem geradores—uma forma de iterador—então sua substituta direta é agora uma expressão geradora (no Python 2, essas funções devolviam listas, então sua alternativa mais próxima era a compreensão de lista).

A função reduce foi rebaixada de função embutida, no Python 2, para o módulo functools no Python 3. Seu caso de uso mais comum, a soma, é melhor servido pela função embutida sum, disponível desde que Python 2.3 (lançado em 2003). E isso é uma enorme vitória em termos de legibilidade e desempenho (veja Exemplo 110 abaixo).

Exemplo 110. Soma de inteiros até 99, realizada com reduce e sum
>>> from functools import reduce  (1)
>>> from operator import add  (2)
>>> reduce(add, range(100))  (3)
4950
>>> sum(range(100))  (4)
4950
>>>
  1. A partir de Python 3.0, reduce deixou de ser uma função embutida.

  2. Importa add para evitar a criação de uma função apenas para somar dois números.

  3. Soma os inteiros até 99.

  4. Mesma operação, com sum—não é preciso importar nem chamar reduce e add.

✒️ Nota

A ideia comum de sum e reduce é aplicar alguma operação sucessivamente a itens em uma série, acumulando os resultados anteriores, reduzindo assim uma série de valores a um único valor.

Outras funções de redução embutidas são all e any:

all(iterable)

Devolve True se não há nenhum elemento falso no iterável; all([]) devolve True.

any(iterable)

Devolve True se qualquer elemento do iterable for verdadeiro; any([]) devolve False.

Dou um explicação mais completa sobre reduce na Seção 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 17.10, quando estivermos tratando dos iteráveis. Para usar uma função de ordem superior, às vezes é conveniente criar um pequena função, que será usada apenas uma vez. As funções anônimas existem para isso. Vamos falar delas a seguir.

7.4. Funções anônimas

A palavra reservada lambda cria uma função anônima dentro de uma expressão Python.

Entretanto, a sintaxe simples de 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 111 é o exemplo do dicionário de rimas do Exemplo 108 reescrito com lambda, sem definir uma função reverse.

Exemplo 111. Ordenando uma lista de palavras escritas na ordem inversa usando lambda
>>> 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 conselho de Fredrik Lundh sobre refatoração.

A receita de Fredrik Lundh para refatoração de lambdas

Se você encontrar um trecho de código difícil de entender por causa de uma lambda, Fredrik Lundh sugere o seguinte procedimento de refatoração:

  1. Escreva um comentário explicando o que diabos aquela lambda faz.

  2. Estude o comentário por algum tempo, e pense em um nome que traduza sua essência.

  3. Converta a lambda para uma declaração def, usando aquele nome.

  4. Remova o comentário.

Esse passos são uma citação do "Programação Funcional—COMO FAZER), uma leitura obrigatória.

A sintaxe lambda é apenas açúcar sintático: uma expressão lambda cria um objeto função, exatamente como a declaração def. Esse é apenas um dos vários tipos de objetos invocáveis no Python. Na próxima seção revisamos todos eles.

7.5. Os nove sabores de objetos invocáveis

O operador de invocação () pode ser aplicado a outros objetos além de funções. Para determinar se um objeto é invocável, use a função embutida callable(). No Python 3.9, a documentação do modelo de dados lista nove tipos invocáveis:

Funções definidas pelo usuário

Criadas com comandos def ou expressões lambda.

Funções embutidas

Uma funções implementadas em C (no CPython), como len ou time.strftime.

Métodos embutidos

Métodos implementados em C, como dict.get.

Métodos

Funções definidas no corpo de uma classe.

Classes

Quando invocada, uma classe executa seu método __new__ para criar uma instância, e a seguir __init__, para inicializá-la. Então a instância é devolvida ao usuário. Como não existe um operador new no Python, invocar uma classe é como invocar uma função.[79]

Instâncias de classe

Se uma classe define um método __call__, suas instâncias podem então ser invocadas como funções—esse é o assunto da próxima seção.

Funções geradoras

Funções ou métodos que usam a palavra reservada yield em seu corpo. Quando chamadas, devolvem um objeto gerador.

Funções de corrotinas nativas

Funções ou métodos definidos com async def. Quando chamados, devolvem um objeto corrotina. Introduzidas no Python 3.5.

Funções geradoras assíncronas

Funções ou métodos definidos com async def, contendo yield em seu corpo. Quando chamados, devolvem um gerador assíncrono para ser usado com async for. Introduzidas no Python 3.6.

Funções geradoras, funções de corrotinas nativas e geradoras assíncronas são diferentes de outros invocáveis: os valores devolvidos tais funções nunca são dados da aplicação, mas objetos que exigem processamento adicional, seja para produzir dados da aplicação, seja para realizar algum trabalho útil. Funções geradoras devolvem iteradores. Ambos são tratados no Capítulo 17. Funções de corrotinas nativas e funções geradoras assíncronas devolvem objetos que só funcionam com a ajuda de um framework de programação assíncrona, tal como asyncio. Elas são o assunto do Capítulo 21.

👉 Dica

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():

>>> abs, str, 'Ni!'
(<built-in function abs>, <class 'str'>, 'Ni!')
>>> [callable(obj) for obj in (abs, str, 'Ni!')]
[True, True, False]

Vamos agora criar instâncias de classes que funcionam como objetos invocáveis.

7.6. Tipos invocáveis definidos pelo usuário

Não só as funções Python são objetos reais, também é possível fazer com que objetos Python arbitrários se comportem como funções. Para isso basta implementar o método de instância __call__.

O Exemplo 112 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.[80]

Exemplo 112. bingocall.py: Uma BingoCage faz apenas uma coisa: escolhe itens de uma lista embaralhada
import random

class BingoCage:

    def __init__(self, items):
        self._items = list(items)  # (1)
        random.shuffle(self._items)  # (2)

    def pick(self):  # (3)
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')  # (4)

    def __call__(self):  # (5)
        return self.pick()
  1. __init__ aceita qualquer iterável; criar uma cópia local evita efeitos colaterais inesperados sobre qualquer list passada como argumento.

  2. shuffle sempre vai funcionar, pois self._items é uma list.

  3. O método principal.

  4. Se self._items está vazia, gera uma exceção com uma mensagem apropriada.

  5. Atalho para bingo.pick(): bingo().

Aqui está uma demonstração simples do Exemplo 112. 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:

>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True

Uma classe que implemente __call__ é uma forma fácil de criar objetos similares a funções, com algum estado interno que precisa ser mantido de uma invocação para outra, como os itens restantes na BingoCage. Outro bom caso de uso para __call__ é a implementação de decoradores. Decoradores devem ser invocáveis, e muitas vezes é conveniente "lembrar" algo entre chamadas ao decorador (por exemplo, para memoization—a manutenção dos resultados de algum processamento complexo e/ou demorado para uso posterior) ou para separar uma implementação complexa por diferentes métodos.

A abordagem funcional para a criação de funções com estado interno é através do uso de clausuras (closures). Clausuras e decoradores são o assunto do Capítulo 9.

Vamos agora explorar a poderosa sintaxe oferecida pelo Python para declarar parâmetros de funções, e para passar argumentos para elas.

7.7. De parâmetros posicionais a parâmetros somente nomeados

Um dos melhores recursos das funções Python é seu mecanismo extremamente flexível de tratamento de parâmetros. Intimamente relacionados a isso são os usos de * e ** para desempacotar iteráveis e mapeamentos em argumentos separados quando chamamos uma função. Para ver esses recursos em ação, observe o código do Exemplo 113 e os testes mostrando seu uso no Exemplo 114.

Exemplo 113. tag gera elementos HTML; um argumento somente nomeado class_ é usado para passar atributos "class"; o _ é necessário porque class é uma palavra reservada no Python
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 114.

Exemplo 114. Algumas das muitas formas de invocar a função tag do Exemplo 113
>>> tag('br')  # (1)
'<br />'
>>> tag('p', 'hello')  # (2)
'<p>hello</p>'
>>> print(tag('p', 'hello', 'world'))
<p>hello</p>
<p>world</p>
>>> tag('p', 'hello', id=33)  # (3)
'<p id="33">hello</p>'
>>> print(tag('p', 'hello', 'world', class_='sidebar'))  # (4)
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
>>> tag(content='testing', name="img")  # (5)
'<img content="testing" />'
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
...           'src': 'sunset.jpg', 'class': 'framed'}
>>> tag(**my_tag)  # (6)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
  1. Um argumento posicional único produz uma tag vazia com aquele nome.

  2. Quaisquer argumentos após o primeiro serão capturados por *content na forma de uma tuple.

  3. Argumentos nomeados que não são mencionados explicitamente na assinatura de tag são capturados por **attrs como um dict.

  4. O parâmetro class_ só pode ser passado como um argumento nomeado.

  5. O primeiro argumento posicional também pode ser passado como argumento nomeado.

  6. Prefixar o dict my_tag com ** passa todos os seus itens como argumentos separados, que são então vinculados aos parâmetros nomeados, com o restante sendo capturado por **attrs. Nesse caso podemos ter um nome 'class' no dict de argumentos, porque ele é uma string, e não colide com a palavra reservada class.

Argumentos somente nomeados são um recurso de Python 3. No Exemplo 113, 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:

>>> def f(a, *, b):
...     return a, b
...
>>> f(1, b=2)
(1, 2)
>>> f(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given

Observe que argumentos somente nomeados não precisam ter um valor default: eles podem ser obrigatórios, como o b no exemplo acima.

7.7.1. Parâmetros somente posicionais

Desde Python 3.8, assinaturas de funções definidas pelo usuário podem especificar parâmetros somente posicionais. Esse recurso sempre existiu para funções embutidas, tal como divmod(a, b), que só pode ser chamada com parâmetros posicionais, e não na forma divmod(a=10, b=4).

Para definir uma função que requer parâmetros somente posicionais, use / na lista de parâmetros.

Esse exemplo, de "O que há de novo no Python 3.8", mostra como emular a função embutida divmod:

def divmod(a, b, /):
    return (a // b, a % b)

Todos os argumentos à esquerda da / são somente posicionais. Após a /, você pode especificar outros argumentos, que funcionam como da forma usual.

⚠️ Aviso

Uma / na lista de parâmetros é um erro de sintaxe no Python 3.7 ou anteriores.

Por exemplo, considere a função tag do Exemplo 113. Se quisermos que o parâmetro name seja somente posicional, podemos acrescentar uma / após aquele parâmetro na assinatura da função, assim:

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" e na PEP 570.

Após esse mergulho nos recursos flexíveis de declaração de argumentos no Python, o resto desse capítulo trata dos pacotes da biblioteca padrão mais úteis para programar em um estilo funcional.

7.8. Pacotes para programação funcional

Apesar de Guido deixar claro que não projetou Python para ser uma linguagem de programação funcional, o estilo de programação funcional pode ser amplamente utilizado, graças a funções de primeira classe, pattern matching e o suporte de pacotes como operator e functools, dos quais falaremos nas próximas duas seções..

7.8.1. O módulo operator

Na programação funcional, é muitas vezes conveniente usar um operador aritmético como uma função. Por exemplo, suponha que você queira multiplicar uma sequência de números para calcular fatoriais, mas sem usar recursão. Para calcular a soma, podemos usar sum, mas não há uma função equivalente para multiplicação. Você poderia usar reduce—como vimos na Seção 7.3.1—mas isso exige um função para multiplicar dois itens da sequência. O Exemplo 115 mostra como resolver esse problema usando lambda.

Exemplo 115. Fatorial implementado com `reduce`e uma função anônima
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 115 como o Exemplo 116.

Exemplo 116. Fatorial implementado com reduce e operator.mul
from functools import reduce
from operator import mul

def factorial(n):
    return reduce(mul, range(1, n+1))

Outro grupo de "lambdas de um só truque" que operator substitui são funções para extrair itens de sequências ou para ler atributos de objetos: itemgetter e attrgetter são fábricas que criam funções personalizadas para fazer exatamente isso.

O Exemplo 117 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 117. Demonstração de itemgetter para ordenar uma lista de tuplas (mesmos dados do Exemplo 10)
>>> 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:

>>> 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 118. 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 118. Demonstração de attrgetter para processar uma lista previamente definida de namedtuple chamada metro_data (a mesma lista que aparece no Exemplo 117)
>>> from collections import namedtuple
>>> LatLon = namedtuple('LatLon', 'lat lon')  # (1)
>>> Metropolis = namedtuple('Metropolis', 'name cc pop coord')  # (2)
>>> metro_areas = [Metropolis(name, cc, pop, LatLon(lat, lon))  # (3)
...     for name, cc, pop, (lat, lon) in metro_data]
>>> metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLon(lat=35.689722,
lon=139.691667))
>>> metro_areas[0].coord.lat  # (4)
35.689722
>>> from operator import attrgetter
>>> name_lat = attrgetter('name', 'coord.lat')  # (5)
>>>
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')):  # (6)
...     print(name_lat(city))  # (7)
...
('São Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
  1. Usa namedtuple para definir LatLon.

  2. Também define Metropolis.

  3. Cria a lista metro_areas com instâncias de Metropolis; observe o desempacotamento da tupla aninhada para extrair (lat, lon) e usá-los para criar o LatLon do atributo coord de Metropolis.

  4. Obtém a latitude de dentro de metro_areas[0] .

  5. Define um attrgetter para obter name e o atributo aninhado coord.lat.

  6. Usa attrgetter novamente para ordenar uma lista de cidades pela latitude.

  7. Usa o attrgetter definido em 5 para exibir apenas o nome e a latitude da cidade.

Abaixo está uma lista parcial das funções definidas em operator (nomes iniciando com _ foram omitidos por serem, em sua maioria, detalhes de implementação):

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

Exemplo 119. Demonstração de methodcaller: o segundo teste mostra a vinculação de argumentos adicionais
>>> 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 119 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:

>>> str.upper(s)
'THE TIME HAS COME'

O segundo teste do Exemplo 119 mostra que methodcaller pode também executar uma aplicação parcial para fixar alguns argumentos, como faz a função functools.partial. Esse é nosso próximo tópico.

7.8.2. Fixando argumentos com functools.partial

O módulo functools oferece várias funções de ordem superior. Já vimos reduce na Seção 7.3.1. Uma outra é partial: dado um invocável, ela produz um novo invocável com alguns dos argumentos do invocável original vinculados a valores pré-determinados. Isso é útil para adaptar uma função que recebe um ou mais argumentos a uma API que requer uma função de callback com menos argumentos. O Exemplo 120 é uma demonstração trivial.

Exemplo 120. Empregando partial para usar uma função com dois argumentos onde é necessário um invocável com apenas um argumento
>>> 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 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 121.

Exemplo 121. Criando uma função conveniente para normalizar Unicode com partial
>>> 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 122 mostra o uso de partial com a função tag (do Exemplo 113), para fixar um argumento posicional e um argumento nomeado.

Exemplo 122. Demonstração de partial aplicada à função tag, do Exemplo 113
>>> from tagger import tag
>>> tag
<function tag at 0x10206d1e0>  (1)
>>> from functools import partial
>>> picture = partial(tag, 'img', class_='pic-frame')  (2)
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />'  (3)
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', class_='pic-frame')  (4)
>>> picture.func  (5)
<function tag at 0x10206d1e0>
>>> picture.args
('img',)
>>> picture.keywords
{'class_': 'pic-frame'}
  1. Importa tag do Exemplo 113 e mostra seu ID.

  2. Cria a função picture a partir de tag, fixando o primeiro argumento posicional em 'img' e o argumento nomeado class_ em 'pic-frame'.

  3. picture funciona como esperado.

  4. partial() devolve um objeto functools.partial.[81]

  5. Um objeto functools.partial tem atributos que fornecem acesso à função original e aos argumentos fixados.

A função functools.partialmethod faz o mesmo que partial, mas foi projetada para trabalhar com métodos.

O módulo functools também inclui funções de ordem superior para serem usadas como decoradores de função, tais como cache e singledispatch, entre outras. Essas funções são tratadas no Capítulo 9, que também explica como implementar decoradores personalizados.

7.9. Resumo do capítulo

O objetivo deste capítulo foi explorar a natureza das funções como objetos de primeira classe no Python. As principais consequências disso são a possibilidade de atribuir funções a variáveis, passá-las para outras funções, armazená-las em estruturas de dados e acessar os atributos de funções, permitindo que frameworks e ferramentas usem essas informações.

Funções de ordem superior, parte importante da programação funcional, são comuns no Python. As funções embutidas sorted, min e max, além de functools.partial, são exemplos de funções de ordem superior muito usadas na linguagem. O uso de map, filter e reduce já não é tão frequente como costumava ser, graças às compreensões de lista (e estruturas similares, como as expressões geradoras) e à adição de funções embutidas de redução como sum, all e any.

Desde Python 3.6, existem nove sabores de invocáveis, de funções simples criadas com lambda a instâncias de classes que implementam __call__. Geradoras e corrotinas também são invocáveis, mas seu comportamento é muito diferente daquele de outros invocáveis. Todos os invocáveis podem ser detectados pela função embutida callable(). Invocáveis oferecem uma rica sintaxe para declaração de parâmetros formais, incluindo parâmetros nomeados, parâmetros somente posicionais e anotações.

Por fim, vimos algumas funções do módulo operator e functools.partial, que facilitam a programação funcional, minimizando a necessidade de uso da sintaxe funcionalmente inepta de lambda.

7.10. Leitura complementar

Nos próximos capítulos, continuaremos nossa jornada pela programação com objetos função. O Capítulo 8 é dedicado às dicas de tipo nos parâmetros de função e nos valores devolvidos por elas. O Capítulo 9 mergulha nos decoradores de função—um tipo especial de função de ordem superior—e no mecanismo de clausura (closure) que os faz funcionar. O Capítulo 10 mostra como as funções de primeira classe podem simplificar alguns padrões clássicos de projetos (design patterns) orientados a objetos.

Em A Referência da Linguagem Python, a seção "3.2. A hierarquia de tipos padrão" mostra os noves tipos invocáveis, juntamente com todos os outros tipos embutidos.

O capítulo 7 do Python Cookbook (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) (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", 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?) (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 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 (EN) e o vídeo (EN) da apresentação em Berlim—onde conheci Miroslav Šedivý e Jürgen Gmach, dois dos revisores técnicos desse livro.

Ponto de vista

Python é uma linguagem funcional?

Em algum momento do ano 2000, eu estava participando de uma oficina 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 de 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) (EN), assim:

Os "paradigmas" de linguagens de programação são um legado moribundo e tedioso de uma era passada. Os atuais projetistas de linguagens não tem qualquer respeito por eles, então por que nossos cursos aderem servilmente a tais "paradigmas"?

Nesse artigo, Python é mencionado nominalmente na seguinte passagem:

E como descrever linguagens como Python, Ruby, ou Perl? Seus criadores não tem paciência com as sutilezas 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 7.10.

Mesmo se esse não fosse o objetivo de Guido, dotar 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) (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 (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. 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 de Python.[82] O Lisp tem muitas virtudes, mas legibilidade não é uma delas.

Ironicamente, roubar a sintaxe de compreensão de lista de outra linguagem funcional—Haskell—reduziu significativamente a necessidade de usar map e filter, e também lambda.

Além da sintaxe limitada das funções anônimas, o maior obstáculo para uma adoção mais ampla de idiomas de programação funcional no Python é a ausência da eliminação de chamadas de cauda, uma otimização que permite o processamento, de forma eficiente em termos de memória, de uma função que faz uma chamada recursiva na "cauda" de seu corpo. Em outro post de blog, "Tail Recursion Elimination" (Eliminação de Recursão de Cauda) (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. Python não é gostoso de usar, aprender e ensinar por acidente. Guido o fez assim.

Então cá estamos: Python não é, por projeto, uma linguagem funcional—seja lá o quê isso signifique. 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 de 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[83] 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 (EN), "Rationale and Goals"; negritos mantidos do original.

Dicas de tipo foram a maior mudança na história de Python desde a unificação de tipos e classes 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 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 de Python, é mais simples e mais expressiva quando estamos escrevendo programas para explorar dados e ideias, como é o caso em ciência de dados, computação criativa e para aprender.

Este capítulo se concentra nas dicas de tipo de Python nas assinaturas de função. Capítulo 15 explora as dicas de tipo no contexto de classes e outros recursos do módulo typing.

Os tópicos mais importantes aqui são:

  • Uma introdução prática à tipagem gradual com Mypy

  • As perspectivas complementares da duck typing (tipagem pato) e da tipagem nominal

  • A revisão para principais categorias de tipos que podem surgir em anotações — isso representa cerca de 60% do capítulo

  • Os parâmetros variádicos das dicas de tipo (*args, **kwargs)

  • As limitações e desvantagens das dicas de tipo e da tipagem estática.

8.1. Novidades nesse capítulo

Este capítulo é completamente novo. As dicas de tipo apareceram no Python 3.5, após eu ter terminado de escrever a primeira edição de Python Fluente.

Dadas as limitações de um sistema de tipagem estática, a melhor ideia da PEP 484 foi propor um sistema de tipagem gradual. Vamos começar definindo o que isso significa.

8.2. Sobre tipagem gradual

A PEP 484 introduziu no Python um sistema de tipagem gradual. Outras linguagens com sistemas de tipagem gradual são o Typescript da Microsoft, Dart (a linguagem do SDK Flutter, criado pelo Google), e o Hack (um dialeto de PHP criado para uso na máquina virtual HHVM do Facebook). O próprio verificador de tipo MyPy começou como uma linguagem: um dialeto de Python de tipagem gradual com seu próprio interpretador. Guido van Rossum convenceu o criador do MyPy, Jukka Lehtosalo, a transformá-lo em uma ferramenta para checar código Python anotado.

Eis uma função com anotações de tipos:

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

Um sistema de tipagem gradual:

É opcional

Por default, o verificador de tipo não deve emitir avisos para código que não tenha dicas de tipo. Em vez disso, o verificador supõe o tipo Any quando não consegue determinar o tipo de um objeto. O tipo Any é considerado compatível com todos os outros tipos.

Não captura erros de tipagem durante a execução do código

Dicas de tipo são usadas por verificadores de tipo, analisadores de código-fonte (linters) e IDEs para emitir avisos. Eles não evitam que valores inconsistentes sejam passados para funções ou atribuídos a variáveis durante a execução. Por exemplo, nada impede que alguém chame tokenie(42), apesar da anotação de tipo do argumento s: str). A chamada ocorrerá, e teremos um erro de execução no corpo da função.

Não melhora o desempenho

Anotações de tipo fornecem dados que poderiam, em tese, permitir otimizações do bytecode gerado. Mas, até julho de 2021, tais otimizações não ocorrem em nenhum ambiente Python que eu conheça.[84]

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 (EN) sobre o JSON e #731, Support recursive types (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.

👉 Dica

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 da melhor forma possível o potencial e a flexibilidade de Python. Código sem dicas de tipo deveria ser aceito sem objeções quando anotações tornassem o uso de uma API menos amigável ou quando complicassem em demasia seu desenvolvimento.

8.3. Tipagem gradual na prática

Vamos ver como a tipagem gradual funciona na prática, começando com uma função simples e acrescentando gradativamente a ela dicas de tipo, guiados pelo Mypy.

✒️ Nota

Há muitos verificadores de tipo para Python compatíveis com a PEP 484, incluindo o pytype do Google, o Pyright da Microsoft, o Pyre do Facebook — além de verificadores incluídos em IDEs como o PyCharm. Eu escolhi usar o 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:

>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no birds'

Exemplo 123 mostra o código-fonte de show_count, sem anotações.

Exemplo 123. show_count de messages.py sem dicas de tipo.
def show_count(count, word):
    if count == 1:
        return f'1 {word}'
    count_str = str(count) if count else 'no'
    return f'{count_str} {word}s'

8.3.1. Usando o Mypy

Para começar a verificação de tipo, rodamos o comando mypy passando o módulo messages.py como parâmetro:

…/no_hints/ $ pip install mypy
[muitas mensagens omitidas...]
…/no_hints/ $ mypy messages.py
Success: no issues found in 1 source file

Na configuração default, o Mypy não encontra nenhum problema com o Exemplo 123.

⚠️ Aviso

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" (EN) do Mypy adverte que ele "é oficialmente software beta. Mudanças ocasionais irão quebrar a compatibilidade com versões mais antigas." O Mypy está gerando pelo menos um relatório diferente daquele que recebi quando escrevi o capítulo, em abril de 2020. E quando você estiver lendo essas linhas, talvez os resultados também sejam diferentes daqueles mostrados aqui.

Se a assinatura de uma função não tem anotações, Mypy a ignora por default — a menos que seja configurado de outra forma.

O Exemplo 124 também inclui testes de unidade do pytest. Este é código de messages_test.py.

Exemplo 124. messages_test.py sem dicas de tipo.
from pytest import mark

from messages import show_count

@mark.parametrize('qty, expected', [
    (1, '1 part'),
    (2, '2 parts'),
])
def test_show_count(qty, expected):
    got = show_count(qty, 'part')
    assert got == expected

def test_show_count_zero():
    got = show_count(0, 'part')
    assert got == 'no parts'

Agora vamos acrescentar dicas de tipo, guiados pelo Mypy.

8.3.2. Tornando o Mypy mais rigoroso

A opção de linha de comando --disallow-untyped-defs faz o Mypy apontar todas as definições de função que não tenham dicas de tipo para todos os argumentos e para o valor de retorno.

Usando --disallow-untyped-defs com o arquivo de teste produz três erros e uma observação:

…/no_hints/ $ mypy --disallow-untyped-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
messages_test.py:10: error: Function is missing a type annotation
messages_test.py:15: error: Function is missing a return type annotation
messages_test.py:15: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files (checked 1 source file)

Nas primeiras etapas da tipagem gradual, prefiro usar outra opção:

--disallow-incomplete-defs.

Inicialmente o Mypy não me dá nenhuma nova informação:

…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
Success: no issues found in 1 source file

Agora vou acrescentar apenas o tipo do retorno a show_count em messages.py:

def show_count(count, word) -> str:

Isso é suficiente para fazer o Mypy olhar para o código. Usando a mesma linha de comando anterior para verificar messages_test.py fará o Mypy examinar novamente o messages.py:

…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
for one or more arguments
Found 1 error in 1 file (checked 1 source file)

Agora posso gradualmente acrescentar dicas de tipo, função por função, sem receber avisos sobre as funções onde ainda não adicionei anotações Essa é uma assinatura completamente anotada que satisfaz o Mypy:

def show_count(count: int, word: str) -> str:
👉 Dica

Em vez de digitar opções de linha de comando como --disallow-incomplete-defs, você pode salvar sua configuração favorita da forma descrita na página Mypy configuration file (EN) na documentação do Mypy. Você pode incluir configurações globais e configurações específicas para cada módulo. Aqui está um mypy.ini simples, para servir de base:

[mypy]
python_version = 3.9
warn_unused_configs = True
disallow_incomplete_defs = True

8.3.3. Um valor default para um argumento

A função show_count no Exemplo 123 só funciona com substantivos regulares. Se o plural não pode ser composto acrescentando um 's', devemos deixar o usuário fornecer a forma plural, assim:

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

def test_irregular() -> None:
    got = show_count(2, 'child', 'children')
    assert got == '2 children'

O Mypy detecta o erro:

…/hints_2/ $ mypy messages_test.py
messages_test.py:22: error: Too many arguments for "show_count"
Found 1 error in 1 file (checked 1 source file)

Então edito show_count, acrescentando o argumento opcional plural no Exemplo 125.

Exemplo 125. showcount de hints_2/messages.py com um argumento opcional
def show_count(count: int, singular: str, plural: str = '') -> str:
    if count == 1:
        return f'1 {singular}'
    count_str = str(count) if count else 'no'
    if not plural:
        plural = singular + 's'
    return f'{count_str} {plural}'

E agora o Mypy reporta "Success."

⚠️ Aviso

Aqui está um erro de digitação que Python não reconhece. Você consegue encontrá-lo?

def hex2rgb(color=str) -> tuple[int, int, int]:

O relatório de erros do Mypy não é muito útil:

colors.py:24: error: Function is missing a type
    annotation for one or more arguments

A dica de tipo para o argumento color deveria ser color: str. Eu escrevi color=str, que não é uma anotação: ele determina que o valor default de color é str.

Pela minha experiência, esse é um erro comum e fácil de passar desapercebido, especialmente em dicas de tipo complexas.

Os seguintes detalhes são considerados um bom estilo para dicas de tipo:

  • Sem espaço entre o nome do parâmetro e o :; um espaço após o :

  • Espaços dos dois lados do = que precede um valor default de parâmetro

Por outro lado, a PEP 8 diz que não deve haver espaço em torno de = se não há nenhuma dica de tipo para aquele parâmetro específico.

Estilo de Código: use flake8 e blue

Em vez de decorar essas regrinhas bobas, use ferramentas como flake8 e blue. O flake8 informa sobre o estilo do código e várias outras questões, enquanto o blue reescreve o código-fonte com base na (maioria) das regras prescritas pela ferramenta de formatação de código black.

Se o objetivo é impor um estilo de programação "padrão", blue é melhor que black, porque segue o estilo próprio de Python, de usar aspas simples por default e aspas duplas como alternativa.

>>> "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 depende do repr() usar aspas simples por default.

Um dos autores do blue é Barry Warsaw, co-autor da PEP 8, core developer de Python desde 1994 e membro de Python’s Steering Council desde 2019. Daí estamos em ótima companhia quando escolhemos usar aspas simples.

Se você precisar mesmo usar o black, use a opção black -S. Isso deixará suas aspas intocadas.

8.3.4. Usando None como default

No Exemplo 125, o parâmetro plural está anotado como str, e o valor default é ''. Assim não há conflito de tipo.

Eu gosto dessa solução, mas em outros contextos None é um default melhor. Se o parâmetro opcional requer um tipo mutável, então None é o único default sensato, como vimos na Seção 6.5.1.

Com None como default para o parâmetro plural, a assinatura ficaria assim:

from typing import Optional

def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

Vamos destrinchar essa linha:

  • Optional[str] significa que plural pode ser uma str ou None.

  • É obrigatório fornecer explicitamente o valor default = None.

Se você não atribuir um valor default a plural, o runtime de 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.

⚠️ Aviso

Optional não é um bom nome, pois aquela anotação não torna o argumento opcional. O que o torna opcional é a atribuição de um valor default ao parâmetro. Optional[str] significa apenas: o tipo desse parâmetro pode ser str ou NoneType. Nas linguagens Haskell e Elm, um tipo parecido se chama Maybe.

Agora que tivemos um primeiro contato concreto com a tipagem gradual, vamos examinar o que o conceito de tipo significa na prática.

8.4. Tipos são definidos pelas operações possíveis

Há muitas definições do conceito de tipo na literatura. Aqui vamos assumir que tipo é um conjunto de valores e um conjunto de funções que podem ser aplicadas àqueles valores.

— PEP 483—A Teoria das Dicas de Tipo

Na prática, é mais útil considerar o conjunto de operações possíveis como a caraterística definidora de um tipo.[85]

Por exemplo, pensando nas operações possíveis, quais são os tipos válidos para x na função a seguir?

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:

from collections import abc

def double(x: abc.Sequence):
    return x * 2

Um verificador de tipo irá rejeitar esse código. Se você informar ao Mypy que x é do tipo abc.Sequence, ele vai marcar x * 2 como erro, pois a Sequence ABC 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 de Python aceita qualquer objeto como argumento x nas duas versões da função double. O cálculo de x * 2 pode funcionar, ou pode causar um TypeError, se a operação não for suportada por x. Por outro lado, Mypy vai marcar x * 2 como um erro quando analisar o código-fonte anotado de double, pois é uma operação não suportada pelo tipo declarado x: abc.Sequence.

Em um sistema de tipagem gradual, acontece uma interação entre duas perspectivas diferentes de tipo:

Duck typing ("tipagem pato")

A perspectiva adotada pelo Smalltalk — a primeira linguagem orientada a objetos — bem como em Python, JavaScript, e Ruby. Objetos tem tipo, mas variáveis (incluindo parâmetros) não. Na prática, não importa qual o tipo declarado de um objeto, importam apenas as operações que ele efetivamente suporta. Se eu posso invocar birdie.quack() então, nesse contexto, birdie é um pato. Por definição, duck typing só é aplicada durante a execução, quando se tenta aplicar operações sobre os objetos. Isso é mais flexível que a tipagem nominal, ao preço de permitir mais erros durante a execução.[86]

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 126 é 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.[87]

Exemplo 126. birds.py
class Bird:
    pass

class Duck(Bird):  # (1)
    def quack(self):
        print('Quack!')

def alert(birdie):  # (2)
    birdie.quack()

def alert_duck(birdie: Duck) -> None:  # (3)
    birdie.quack()

def alert_bird(birdie: Bird) -> None:  # (4)
    birdie.quack()
  1. Duck é uma subclasse de Bird.

  2. alert não tem dicas de tipo, então o verificador a ignora.

  3. alert_duck aceita um argumento do tipo Duck.

  4. alert_bird aceita um argumento do tipo Bird.

Verificando birds.py com Mypy, encontramos um problema:

…/birds/ $ mypy birds.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)

Só de analisar o código fonte, Mypy percebe que alert_bird é problemático: a dica de tipo declara o parâmetro birdie como do tipo Bird, mas o corpo da função chama birdie.quack() — e a classe Bird não tem esse método.

Agora vamos tentar usar o módulo birds em daffy.py no Exemplo 127.

Exemplo 127. daffy.py
from birds import *

daffy = Duck()
alert(daffy)       # (1)
alert_duck(daffy)  # (2)
alert_bird(daffy)  # (3)
  1. Chamada válida, pois alert não tem dicas de tipo.

  2. Chamada válida, pois alert_duck recebe um argumento do tipo Duck e daffy é um Duck.

  3. Chamada válida, pois alert_bird recebe um argumento do tipo Bird, e daffy também é um Bird — a superclasse de Duck.

Mypy reporta o mesmo erro em daffy.py, sobre a chamada a quack na função alert_bird definida em birds.py:

…/birds/ $ mypy daffy.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)

Mas o Python não vê qualquer problema com daffy.py em si: as três chamadas de função estão OK.

Agora, rodando daffy.py, o resultado é o seguinte:

…/birds/ $ python3 daffy.py
Quack!
Quack!
Quack!

Funciona perfeitamente! Viva o duck typing!

Durante a execução do programa, 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 128.

Exemplo 128. woody.py
from birds import *

woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)

O Mypy encontra dois erros ao verificar woody.py:

…/birds/ $ mypy woody.py
birds.py:16: error: "Bird" has no attribute "quack"
woody.py:5: error: Argument 1 to "alert_duck" has incompatible type "Bird";
expected "Duck"
Found 2 errors in 2 files (checked 1 source file)

O primeiro erro é em birds.py: a chamada a birdie.quack() em alert_bird, que já vimos antes. O segundo erro é em woody.py: woody é uma instância de Bird, então a chamada alert_duck(woody) é inválida, pois aquela função exige um Duck. Todo Duck é um Bird, mas nem todo Bird é um Duck.

Durante a execução, nenhuma das duas chamadas em woody.py funcionariam. A sucessão de falhas é melhor ilustrada em uma sessão no console, através das mensagens de erro, no Exemplo 129.

Exemplo 129. Erros durante a execução e como o Mypy poderia ter ajudado
>>> 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 126 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 127.

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.

✒️ Nota

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 com milhões de linhas de código em Python - como a Dropbox, o Google e o Facebook - investiram em equipes e ferramentas para promover a adoção global de dicas de tipo internamente, e hoje tem partes significativas e crescentes de sua base de código checadas para tipo em suas linhas (pipeline) de integração contínua.

Nessa seção exploramos as relações de tipos e operações no duck typing e na tipagem nominal, começando com a função simples double() — que deixamos sem dicas de tipo. Agora vamos dar uma olhada nos tipos mais importantes ao anotar funções.

Vamos ver um bom modo de adicionar dicas de tipo a double() quando examinarmos Seção 8.5.10. Mas antes disso, há tipos mais importantes para conhecer.

8.5. Tipos próprios para anotações

Quase todos os tipos em Python podem ser usados em dicas de tipo, mas há restrições e recomendações. Além disso, o módulo typing introduziu constructos especiais com uma semântica às vezes surpreendente.

Essa seção trata de todos os principais tipos que você pode usar em anotações:

  • typing.Any

  • Tipos e classes simples

  • typing.Optional e typing.Union

  • Coleções genéricas, incluindo tuplas e mapeamentos

  • Classes base abstratas

  • Iteradores genéricos

  • Genéricos parametrizados e TypeVar

  • typing.Protocols — crucial para duck typing estático

  • typing.Callable

  • typing.NoReturn — um bom modo de encerrar essa lista.

Vamos falar de um de cada vez, começando por um tipo que é estranho, aparentemente inútil, mas de uma importância fundamental.

8.5.1. O tipo Any

A pedra fundamental de qualquer sistema gradual de tipagem é o tipo Any, também conhecido como o tipo dinâmico. Quando um verificador de tipo vê um função sem tipo como esta:

def double(x):
    return x * 2

ele supõe isto:

def double(x: Any) -> Any:
    return x * 2

Isso significa que o argumento x e o valor de retorno podem ser de qualquer tipo, inclusive de tipos diferentes. Assume-se que Any pode suportar qualquer operação possível.

Compare Any com object. Considere essa assinatura:

def double(x: object) -> object:

Essa função também aceita argumentos de todos os tipos, porque todos os tipos são subtipo-de object.

Entretanto, um verificador de tipo vai rejeitar essa função:

def double(x: object) -> object:
    return x * 2

O problema é que object não suporta a operação __mul__. Veja o que diz o Mypy:

…/birds/ $ mypy double_object.py
double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)

Tipos mais gerais tem interfaces mais restritas, isto é, eles suportam menos operações. A classe object implementa menos operações que abc.Sequence, que implementa menos operações que abc.MutableSequence, que por sua vez implementa menos operações que list.

Mas Any é um tipo mágico que reside tanto no topo quanto na base da hierarquia de tipos. Ele é simultaneamente o tipo mais geral - então um argumento n: Any aceita valores de qualquer tipo - e o tipo mais especializado, suportando assim todas as operações possíveis. Pelo menos é assim que o verificador de tipo entende Any.

Claro, nenhum tipo consegue suportar qualquer operação possível, então usar Any impede o verificador de tipo de cumprir sua missão primária: detectar operações potencialmente ilegais antes que seu programa falhe e levante uma exceção durante sua execução.

8.5.1.1. Subtipo-de versus consistente-com

Sistemas tradicionais de tipagem nominal orientados a objetos se baseiam na relação subtipo-de. Dada uma classe T1 e uma subclasse T2, então T2 é subtipo-de T1.

Observe este código:

class T1:
    ...

class T2(T1):
    ...

def f1(p: T1) -> None:
    ...

o2 = T2()

f1(o2)  # OK

A chamada f1(o2) é uma aplicação do Princípio de Substituição de Liskov (Liskov Substitution Principle—LSP).

Barbara Liskov[88] 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:

def f2(p: T2) -> None:
    ...

o1 = T1()

f2(o1)  # type error

Do ponto de vista das operações suportadas, faz todo sentido: como uma subclasse, T2 herda e precisa suportar todas as operações suportadas por T1. Então uma instância de T2 pode ser usada em qualquer lugar onde se espera uma instância de T1. Mas o contrário não é necessariamente verdadeiro: T2 pode implementar métodos adicionais, então uma instância de T1 não pode ser usada onde se espera uma instância de T2. Este foco nas operações suportadas se reflete no nome _behavioral subtyping (subtipagem comportamental) (EN), também usado para se referir ao LSP.

Em um sistema de tipagem gradual há outra relação, consistente-com (consistent-with), que se aplica sempre que subtipo-de puder ser aplicado, com disposições especiais para o tipo Any.

As regras para consistente-com são:

  1. Dados T1 e um subtipo T2, então T2 é consistente-com T1 (substituição de Liskov).

  2. Todo tipo é consistente-com Any: você pode passar objetos de qualquer tipo em um argumento declarado como de tipo `Any.

  3. Any é consistente-com todos os tipos: você sempre pode passar um objeto de tipo Any onde um argumento de outro tipo for esperado.

Considerando as definições anteriores dos objetos o1 e o2, aqui estão alguns exemplos de código válido, ilustrando as regras #2 e #3:

def f3(p: Any) -> None:
    ...

o0 = object()
o1 = T1()
o2 = T2()

f3(o0)  #
f3(o1)  #  tudo certo: regra #2
f3(o2)  #

def f4():  # tipo implícito de retorno: `Any`
    ...

o4 = f4()  # tipo inferido: `Any`

f1(o4)  #
f2(o4)  #  tudo certo: regra #3
f3(o4)  #

Todo sistema de tipagem gradual precisa de um tipo coringa como Any

👉 Dica

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 tipo em todo lugar porque conseguem inferir o tipo de muitas expressões. Por exemplo, se eu escrever x = len(s) * 10, o verificador não precisa de uma declaração local explícita para saber que x é um int, desde que consiga encontrar dicas de tipo para len em algum lugar.

Agora podemos explorar o restante dos tipos usados em anotações.

8.5.2. Tipos simples e classes

Tipos simples como int, float, str, e bytes podem ser usados diretamente em dicas de tipo. Classes concretas da biblioteca padrão, de pacotes externos ou definidas pelo usuário — FrenchDeck, Vector2d, e Duck - também podem ser usadas em dicas de tipo.

Classes base abstratas também são úteis aqui. Voltaremos a elas quando formos estudar os tipos coleção, e em Seção 8.5.7.

Para classes, consistente-com é definido como subtipo_de: uma subclasse é consistente-com todas as suas superclasses.

Entretanto, "a praticidade se sobrepõe à pureza", então há uma exceção importante, discutida em seguida.

👉 Dica
int é Consistente-Com complex

Não há nenhuma relação nominal de subtipo entre os tipo nativos int, float e complex: eles são subclasses diretas de object. Mas a PEP 484 declara que int é consistente-com float, e float é consistente-com complex. Na prática, faz sentido: int implementa todas as operações que float implementa, e int implementa operações adicionais também - operações binárias como &, |, <<, etc. O resultado final é o seguinte: int é consistente-com complex. Para i = 3, i.real é 3 e i.imag é 0.

8.5.3. Os tipos Optional e Union

Nós vimos o tipo especial Optional em Seção 8.3.4. Ele resolve o problema de ter None como default, como no exemplo daquela seção:

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.

👉 Dica
Uma sintaxe melhor para Optional e Union em Python 3.10

Desde Python 3.10 é possível escrever str | bytes em vez de Union[str, bytes]. É menos digitação, e não há necessidade de importar Optional ou Union de typing. Compare a sintaxe antiga com a nova para a dica de tipo do parâmetro plural em show_count:

plural: Optional[str] = None    # before
plural: str | None = None       # after

O operador | também funciona com isinstance e issubclass para declarar o segundo argumento: isinstance(x, int | str). Para saber mais, veja PEP 604—Complementary syntax for Union[] (EN).

A assinatura da função nativa ord é um exemplo simples de Union - ela aceita str or bytes, e retorna um int:[89]

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:

from typing import Union

def parse_token(token: str) -> Union[str, float]:
    try:
        return float(token)
    except ValueError:
        return token

Se possível, evite criar funções que retornem o tipo Union, pois esse tipo exige um esforço extra do usuário: pois para saber o que fazer com o valor recebido da função será necessário verificar o tipo daquele valor durante a execução. Mas a parse_token no código acima é um caso de uso razoável no contexto de interpretador de expressões simples.

👉 Dica

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

Union[A, B, Union[C, D, E]]

é o mesmo que:

Union[A, B, C, D, E]

Union é mais útil com tipos que não sejam consistentes entre si. Por exemplo: Union[int, float] é redundante, pois int é consistente-com float. Se você usar apenas float para anotar o parâmetro, ele vai também aceitar valores int.

8.5.4. Coleções genéricas

A maioria das coleções em Python são heterogêneas.

Por exemplo, você pode inserir qualquer combinação de tipos diferentes em uma list. Entretanto, na prática isso não é muito útil: se você colocar objetos em uma coleção, você certamente vai querer executar alguma operação com eles mais tarde, e normalmente isso significa que eles precisam compartilhar pelo menos um método comum.[90]

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

Exemplo 130. tokenize com dicas de tipo para Python ≥ 3.9
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 (EN) lista as coleções da biblioteca padrão que aceitam dicas de tipo genéricas. A lista a seguir mostra apenas as coleções que usam a forma mais simples de dica de tipo genérica, container[item]:

list        collections.deque        abc.Sequence   abc.MutableSequence
set         abc.Container            abc.Set        abc.MutableSet
frozenset   abc.Collection

Os tipos tuple e mapping aceitam dicas de tipo mais complexas, como veremos em suas respectivas seções.

No Python 3.10, não há uma boa maneira de anotar array.array, levando em consideração o argumento typecode do construtor, que determina se o array contém inteiros ou floats. Um problema ainda mais complicado é verificar a faixa dos inteiros, para prevenir OverflowError durante a execução, ao se adicionar novos elementos. Por exemplo, um array com typecode=B só pode receber valores int de 0 a 255. Até Python 3.11, o sistema de tipagem estática de Python não consegue lidar com esse desafio.

Suporte a tipos de coleção descontinuados

(Você pode pular esse box se usa apenas Python 3.9 ou posterior.)

Em Python 3.7 e 3.8, você precisa importar um __future__ para fazer a notação [] funcionar com as coleções nativas, tal como list, como ilustrado no Exemplo 131.

Exemplo 131. tokenize com dicas de tipo para Python ≥ 3.7
from __future__ import annotations

def tokenize(text: str) -> list[str]:
    return text.upper().split()

O __future__ não funciona com Python 3.6 ou anterior. O Exemplo 132 mostra como anotar tokenize de uma forma que funciona com Python ≥ 3.5.

Exemplo 132. tokenize com dicas de tipo para Python ≥ 3.5
from typing import List

def tokenize(text: str) -> List[str]:
    return text.upper().split()

Para fornecer um suporte inicial a dicas de tipo genéricas, os autores da PEP 484 criaram dúzias de tipos genéricos no módulo typing. A Tabela 15 mostra alguns deles. Para a lista completa, consulte a documentação do módulo typing .

Tabela 15. Alguns tipos de coleção e seus equivalentes nas dicas de tipo
Collection Type hint equivalent

list

typing.List

set

typing.Set

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 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 de 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.[91] 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 Python 3.9. No ritmo atual, esse deverá ser Python 3.14, também conhecido como Python Pi.

Agora vamos ver como anotar tuplas genéricas.

8.5.5. Tipos tuple

Há três maneiras de anotar os tipos tuple.

  • Tuplas como registros (records)

  • Tuplas como registro com campos nomeados

  • Tuplas como sequências imutáveis.

8.5.5.1. Tuplas como registros

Se você está usando uma tuple como um registro, use o tipo tuple nativo e declare os tipos dos campos dentro dos [].

Por exemplo, a dica de tipo seria tuple[str, float, str] para aceitar uma tupla com nome da cidade, população e país: ('Shanghai', 24.28, 'China').

Observe uma função que recebe um par de coordenadas geográficas e retorna uma Geohash, usada assim:

>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'

O Exemplo 133 mostra a definição da função geohash, usando o pacote geolib do PyPI.

Exemplo 133. coordinates.py com a função geohash
from geolib import geohash as gh  # type: ignore  # (1)

PRECISION = 9

def geohash(lat_lon: tuple[float, float]) -> str:  # (2)
    return gh.encode(*lat_lon, PRECISION)
  1. Esse comentário evita que o Mypy avise que o pacote geolib não tem nenhuma dica de tipo.

  2. O parâmetro lat_lon, anotado como uma tuple com dois campos float.

👉 Dica

Com Python < 3.9, importe e use typing.Tuple nas dicas de tipo. Este tipo está descontinuado mas permanecerá na biblioteca padrão pelo menos até 2024.

8.5.5.2. Tuplas como registros com campos nomeados

Para a anotar uma tupla com muitos campos, ou tipos específicos de tupla que seu código usa com frequência, recomendo fortemente usar typing.NamedTuple, como visto no Capítulo 5. O Exemplo 134 mostra uma variante de Exemplo 133 com NamedTuple.

Exemplo 134. coordinates_named.py com NamedTuple, Coordinates e a função geohash
from typing import NamedTuple

from geolib import geohash as gh  # type: ignore

PRECISION = 9

class Coordinate(NamedTuple):
    lat: float
    lon: float

def geohash(lat_lon: Coordinate) -> str:
    return gh.encode(*lat_lon, PRECISION)

Como explicado na Seção 5.2, typing.NamedTuple é uma factory de subclasses de tuple, então Coordinate é consistente-com tuple[float, float], mas o inverso não é verdadeiro - afinal, Coordinate tem métodos extras adicionados por NamedTuple, como ._asdict(), e também poderia ter métodos definidos pelo usuário.

Na prática, isso significa que é seguro (do ponto de vista do tipo de argumento) passar uma instância de Coordinate para a função display, definida assim:

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}'
8.5.5.3. Tuplas como sequências imutáveis

Para anotar tuplas de tamanho desconhecido, usadas como listas imutáveis, você precisa especificar um único tipo, seguido de uma vírgula e …​ (isto é o símbolo de reticências de Python, formado por três pontos, não o caractere Unicode U+2026HORIZONTAL ELLIPSIS).

Por exemplo, tuple[int, …​] é uma tupla com itens int.

As reticências indicam que qualquer número de elementos >= 1 é aceitável. Não há como especificar campos de tipos diferentes para tuplas de tamanho arbitrário.

As anotações stuff: tuple[Any, …​] e stuff: tuple são equivalentes: stuff é uma tupla de tamanho desconhecido contendo objetos de qualquer tipo.

Aqui temos um função columnize, que transforma uma sequência em uma tabela de colunas e células, na forma de uma lista de tuplas de tamanho desconhecido. É útil para mostrar os itens em colunas, assim:

>>> animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus'.split()
>>> table = columnize(animals)
>>> table
[('drake', 'koala', 'yak'), ('fawn', 'lynx', 'zapus'), ('heron', 'tahr'),
 ('ibex', 'xerus')]
>>> for row in table:
...     print(''.join(f'{word:10}' for word in row))
...
drake     koala     yak
fawn      lynx      zapus
heron     tahr
ibex      xerus

O Exemplo 135 mostra a implementação de columnize. Observe o tipo do retorno:

list[tuple[str, ...]]
Exemplo 135. columnize.py retorna uma lista de tuplas de strings
from collections.abc import Sequence

def columnize(
    sequence: Sequence[str], num_columns: int = 0
) -> list[tuple[str, ...]]:
    if num_columns == 0:
        num_columns = round(len(sequence) ** 0.5)
    num_rows, reminder = divmod(len(sequence), num_columns)
    num_rows += bool(reminder)
    return [tuple(sequence[i::num_rows]) for i in range(num_rows)]

8.5.6. Mapeamentos genéricos

Tipos de mapeamento genéricos são anotados como MappingType[KeyType, ValueType]. O tipo nativo dict e os tipos de mapeamento em collections e collections.abc aceitam essa notação em Python ≥ 3.9. Para versões mais antigas, você deve usar typing.Dict e outros tipos de mapeamento no módulo typing, como discutimos em Suporte a tipos de coleção descontinuados.

O Exemplo 136 mostra um uso na prática de uma função que retorna um índice invertido para permitir a busca de caracteres Unicode pelo nome — uma variação do Exemplo 61 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':

>>> 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 136 mostra o código fonte de charindex.py com a função name_index. Além de uma dica de tipo dict[], este exemplo tem três outros aspectos que estão aparecendo pela primeira vez no livro.

Exemplo 136. charindex.py
import sys
import re
import unicodedata
from collections.abc import Iterator

RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1

def tokenize(text: str) -> Iterator[str]:  # (1)
    """return iterable of uppercased words"""
    for match in RE_WORD.finditer(text):
        yield match.group().upper()

def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
    index: dict[str, set[str]] = {}  # (2)
    for char in (chr(i) for i in range(start, end)):
        if name := unicodedata.name(char, ''):  # (3)
            for word in tokenize(name):
                index.setdefault(word, set()).add(char)
    return index
  1. tokenize é uma função geradora. Capítulo 17 é sobre geradores.

  2. A variável local index está anotada. Sem a dica, o Mypy diz: Need type annotation for 'index' (hint: "index: dict[<type>, <type>] = …​").

  3. Eu usei o operador morsa (walrus operator) := na condição do if. Ele atribui o resultado da chamada a unicodedata.name() a name, e a expressão inteira é calculada a partir daquele resultado. Quando o resultado é '', isso é falso, e o index não é atualizado.[92]

✒️ Nota

Ao usar dict como um registro, é comum que todas as chaves sejam do tipo str, com valores de tipos diferentes dependendo das chaves. Isso é tratado na Seção 15.3.

8.5.7. Classes bases abstratas

Seja conservador no que envia, mas liberal no que aceita.

— lei de Postel
ou o Princípio da Robustez

A Tabela 15 apresenta várias classes abstratas de collections.abc. Idealmente, uma função deveria aceitar argumentos desses tipos abstratos—​ou seus equivalentes de typing antes de Python 3.9—​e não tipos concretos. Isso dá mais flexibilidade a quem chama a função.

Considere essa assinatura de função:

from collections.abc import Mapping

def name2hex(name: str, color_map: Mapping[str, int]) -> str:

Usar abc.Mapping permite ao usuário da função fornecer uma instância de dict, defaultdict, ChainMap, uma subclasse de UserDict subclass, ou qualquer outra classe que seja um subtipo-de Mapping.

Por outro lado, veja essa assinatura:

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

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

def tokenize(text: str) -> list[str]:
    return text.upper().split()

No verbete de typing.List (EN - Tradução abaixo não oficial), a documentação de Python diz:

Versão genérica de list. Útil para anotar tipos de retorno. Para anotar argumentos é preferível usar um tipo de coleção abstrata , tal como Sequence ou Iterable.

Comentários similares aparecem nos verbetes de typing.Dict e typing.Set.

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 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" da PEP 585—Type Hinting Generics In Standard Collections (EN).

Para encerrar nossa discussão de ABCs em dicas de tipo, precisamos falar sobre os ABCs numbers.

8.5.7.1. A queda da torre numérica

O pacote numbers define a assim chamada torre numérica (numeric tower) descrita na PEP 3141—A Type Hierarchy for Numbers (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" da PEP 484 rejeita os ABCs numbers e manda tratar os tipo nativos complex, float, e int como casos especiais, como explicado em int é Consistente-Com complex. Vamos voltar a essa questão na Seção 13.6.8, em Capítulo 13, que é dedicada a comparar protocolos e ABCs

Na prática, se você quiser anotar argumentos numéricos para checagem de tipo estática, existem algumas opções:

  1. Usar um dos tipo concretos, int, float, ou complex — como recomendado pela PEP 488.

  2. Declarar um tipo union como Union[float, Decimal, Fraction].

  3. Se você quiser evitar a codificação explícita de tipos concretos, usar protocolos numéricos como SupportsFloat, tratados na Seção 13.6.2.

A Seção 8.5.10 abaixo é um pré-requisito para entender protocolos numéricos.

Antes disso, vamos examinar um dos ABCs mais úteis para dicas de tipo: Iterable.

8.5.8. Iterable

A documentação de typing.List que eu citei acima recomenda Sequence e Iterable para dicas de tipo de parâmetros de função.

Esse é um exemplo de argumento Iterable, na função math.fsum da biblioteca padrão:

def fsum(__seq: Iterable[float]) -> float:
👉 Dica
Arquivos Stub e o Projeto Typeshed

Até Python 3.10, a biblioteca padrão não tem anotações, mas o Mypy, o PyCharm, etc, conseguem encontrar as dicas de tipo necessárias no projeto Typeshed, na forma de arquivos stub: arquivos de código-fonte especiais, com uma extensão .pyi, que contém assinaturas anotadas de métodos e funções, sem a implementação - muito parecidos com headers em C.

A assinatura para math.fsum está em /stdlib/2and3/math.pyi. Os sublinhados iniciais em __seq são uma convenção estabelecida na PEP 484 para parâmetros apenas posicionais, como explicado em Seção 8.6.

O Exemplo 137 é outro exemplo do uso de um parâmetro Iterable, que produz itens que são tuple[str, str]. A função é usada assim:

>>> l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
>>> text = 'mad skilled noob powned leet'
>>> from replacer import zip_replace
>>> zip_replace(text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'

O Exemplo 137 mostra a implementação.

Exemplo 137. replacer.py
from collections.abc import Iterable

FromTo = tuple[str, str]  # (1)

def zip_replace(text: str, changes: Iterable[FromTo]) -> str:  # (2)
    for from_, to in changes:
        text = text.replace(from_, to)
    return text
  1. FromTo é um apelido de tipo: eu atribui tuple[str, str] a FromTo, para tornar a assinatura de zip_replace mais legível.

  2. changes tem que ser um Iterable[FromTo]; é o mesmo que escrever Iterable[tuple[str, str]], mas é mais curto e mais fácil de ler.

👉 Dica
O TypeAlias Explícito em Python 3.10

PEP 613—Explicit Type Aliases introduziu um tipo especial, o TypeAlias, para tornar as atribuições que criam apelidos de tipos mais visíveis e mais fáceis para os verificadores de tipo. A partir de Python 3.10, esta é a forma preferencial de criar um apelidos de tipo.

from typing import TypeAlias

FromTo: TypeAlias = tuple[str, str]
8.5.8.1. abc.Iterable versus abc.Sequence

Tanto math.fsum quanto replacer.zip_replace tem que percorrer todos os argumentos do Iterable para produzir um resultado. Dado um iterável sem fim tal como o gerador itertools.cycle como entrada, essas funções consumiriam toda a memória e derrubariam o processo Python. Apesar desse perigo potencial, é muito comum no Python moderno se oferecer funções que aceitam um Iterable como argumento, mesmo se elas tem que processar a estrutura inteira para obter um resultado. Isso dá a quem chama a função a opção de fornecer um gerador como dado de entrada, em vez de uma sequência pré-construída, com uma grande economia potencial de memória se o número de itens de entrada for grande.

Por outro lado, a função columnize no Exemplo 135 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 136, 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 138 define sample, uma função que recebe dois argumentos: uma Sequence de elementos de tipo T e um int. Ela retorna uma list de elementos do mesmo tipo T, escolhidos aleatoriamente do primeiro argumento.

O Exemplo 138 mostra a implementação.

Exemplo 138. sample.py
from collections.abc import Sequence
from random import shuffle
from typing import TypeVar

T = TypeVar('T')

def sample(population: Sequence[T], size: int) -> list[T]:
    if size < 1:
        raise ValueError('size must be >= 1')
    result = list(population)
    shuffle(result)
    return result[:size]

Aqui estão dois exemplos do motivo de eu usar um tipo variável em sample:

  • Se chamada com uma tupla de tipo tuple[int, …​] — que é consistente-com Sequence[int] - então o tipo parametrizado é int, então o tipo de retorno é list[int].

  • Se chamada com uma str — que é consistente-com Sequence[str] — então o tipo parametrizado é str, e o tipo do retorno é list[str].

✒️ Nota
Por que TypeVar é necessário?

Os autores da PEP 484 queriam introduzir dicas de tipo ao acrescentar o módulo typing, sem mudar nada mais na linguagem. Com uma metaprogramação inteligente, eles poderiam fazer o operador [] funcionar para classes como Sequence[T]. Mas o nome da variável T dentro dos colchetes precisa ser definido em algum lugar - ou o interpretador Python necessitaria de mudanças mais profundas, para suportar a notação de tipos genéricos como um caso especial de []. Por isso o construtor typing.TypeVar é necessário: para introduzir o nome da variável no namespace (espaço de nomes) corrente. Linguagens como Java, C# e TypeScript não exigem que o nome da variável seja declarado previamente, então eles não tem nenhum equivalente da classe TypeVar de Python.

Outro exemplo é a função statistics.mode da biblioteca padrão, que retorna o ponto de dado mais comum de uma série.

Aqui é uma exemplo de uso da documentação:

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

Exemplo 139. mode_float.py: mode que opera com float e seus subtipos [94]
from collections import Counter
from collections.abc import Iterable

def mode(data: Iterable[float]) -> float:
    pairs = Counter(data).most_common(1)
    if len(pairs) == 0:
        raise ValueError('no mode for empty data')
    return pairs[0][0]

Muitos dos usos de mode envolvem valores int ou float, mas 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.

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar('T')

def mode(data: Iterable[T]) -> T:

Quando aparece pela primeira vez na assinatura, o tipo parametrizado T pode ser qualquer tipo. Da segunda vez que aparece, ele vai significar o mesmo tipo que da primeira vez.

Assim, qualquer iterável é consistente-com Iterable[T], incluindo iterável de tipos unhashable que collections.Counter não consegue tratar. Precisamos restringir os tipos possíveis de se atribuir a T. Vamos ver maneiras diferentes de fazer isso nas duas seções seguintes.

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

from collections.abc import Iterable
from decimal import Decimal
from fractions import Fraction
from typing import TypeVar

NumberT = TypeVar('NumberT', float, Decimal, Fraction)

def mode(data: Iterable[NumberT]) -> NumberT:

Está melhor que antes, e era a assinatura de mode em statistics.pyi, o arquivo stub em typeshed em 25 de maio de 2020.

Entretanto, a documentação em statistics.mode inclui esse exemplo:

>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'

Na pressa, poderíamos apenas adicionar str à definição de NumberT:

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.

8.5.9.2. TypeVar delimitada

Examinando o corpo de mode no Exemplo 139, vemos que a classe Counter é usada para classificação. Counter é baseada em dict, então o tipo do elemento do iterável data precisa ser hashable.

A princípio, essa assinatura pode parecer que funciona:

from collections.abc import Iterable, Hashable

def mode(data: Iterable[Hashable]) -> Hashable:

Agora o problema é que o tipo do item retornado é Hashable: um ABC que implementa apenas o método __hash__. Então o verificador de tipo não vai permitir que façamos nada com o valor retornado, exceto chamar seu método hash(). Não é muito útil.

A solução está em outro parâmetro opcional de TypeVar: o parâmetro representado pela palavra-chave bound. Ele estabelece um limite superior para os tipos aceitos. No Exemplo 140, temos bound=Hashable. Isso significa que o tipo do parâmetro pode ser Hashable ou qualquer subtipo-de Hashable.[95]

Exemplo 140. mode_hashable.py: igual a Exemplo 139, mas com uma assinatura mais flexível
from collections import Counter
from collections.abc import Iterable, Hashable
from typing import TypeVar

HashableT = TypeVar('HashableT', bound=Hashable)

def mode(data: Iterable[HashableT]) -> HashableT:
    pairs = Counter(data).most_common(1)
    if len(pairs) == 0:
        raise ValueError('no mode for empty data')
    return pairs[0][0]

Em resumo:

  • Um tipo variável restrito será concretizado em um dos tipos nomeados na declaração do TypeVar.

  • Um tipo variável delimitado será concretizado pata o tipo inferido da expressão - desde que o tipo inferido seja consistente-com o limite declarado pelo argumento bound= do TypeVar.

✒️ Nota

É um pouco lamentável que a palavra-chave do argumento para declarar um TypeVar delimitado tenha sido chamado bound=, pois o verbo "to bind" (ligar ou vincular) é normalmente usado para indicar o estabelecimento do valor de uma variável, que na semântica de referência de Python é melhor descrita como vincular (bind) um nome a um valor. Teria sido menos confuso se a palavra-chave do argumento tivesse sido chamada boundary=.

O construtor de typing.TypeVar tem outros parâmetros opcionais - covariant e contravariant — que veremos em Capítulo 15, Seção 15.7.

Agora vamos concluir essa introdução a TypeVar com AnyStr.

8.5.9.3. O tipo variável pré-definido AnyStr

O módulo typing inclui um TypeVar pré-definido chamado AnyStr. Ele está definido assim:

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 de Python 3.8, capaz de permitir um uso de dicas de tipo mais pythônico.

8.5.10. Protocolos estáticos

✒️ Nota

Em programação orientada a objetos, o conceito de um "protocolo" como uma interface informal é tão antigo quanto Smalltalk, e foi uma parte essencial de Python desde o início. Entretanto, no contexto de dicas de tipo, um protocolo é uma subclasse de typing.Protocol, definindo uma interface que um verificador de tipo pode analisar. Os dois tipos de protocolo são tratados em Capítulo 13. Aqui apresento apenas uma rápida introdução no contexto de anotações de função.

O tipo Protocol, como descrito em PEP 544—Protocols: Structural subtyping (static duck typing) (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:

>>> top([4, 1, 5, 2, 6, 7, 3], 3)
[7, 6, 5]
>>> l = 'mango pear apple kiwi banana'.split()
>>> top(l, 3)
['pear', 'mango', 'kiwi']
>>>
>>> l2 = [(len(s), s) for s in l]
>>> l2
[(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')]
>>> top(l2, 3)
[(6, 'banana'), (5, 'mango'), (5, 'apple')]

Um genérico parametrizado top ficaria parecido com o mostrado no Exemplo 141.

Exemplo 141. a função top function com um parâmetro de tipo T indefinido
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:

>>> 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:[96]

>>> 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 141 deveria ser limitado a tipos que implementam __lt__. No Exemplo 140, precisávamos de um parâmetro de tipo que implementava __hash__, para poder usar typing.Hashable como limite superior do parâmetro de tipo. Mas agora não há um tipo adequado em typing ou abc para usarmos, então precisamos criar um.

O Exemplo 142 mostra o novo tipo SupportsLessThan, um Protocol.

Exemplo 142. comparable.py: a definição de um tipo Protocol, SupportsLessThan
from typing import Protocol, Any

class SupportsLessThan(Protocol):  # (1)
    def __lt__(self, other: Any) -> bool: ...  # (2)
  1. Um protocolo é uma subclasse de typing.Protocol.

  2. O corpo do protocolo tem uma ou mais definições de método, com …​ em seus corpos.

Um tipo T é consistente-com um protocolo P se T implementa todos os métodos definido em P, com assinaturas de tipo correspondentes.

Dado SupportsLessThan, nós agora podemos definir essa versão funcional de top no Exemplo 143.

Exemplo 143. top.py: definição da função top usando uma TypeVar com bound=SupportsLessThan
from collections.abc import Iterable
from typing import TypeVar

from comparable import SupportsLessThan

LT = TypeVar('LT', bound=SupportsLessThan)

def top(series: Iterable[LT], length: int) -> list[LT]:
    ordered = sorted(series, reverse=True)
    return ordered[:length]

Vamos testar top. O Exemplo 144 mostra parte de uma bateria de testes para uso com o pytest. Ele tenta chamar top primeiro com um gerador de expressões que produz tuple[int, str], e depois com uma lista de object. Com a lista de object, esperamos receber uma exceção de TypeError.

Exemplo 144. top_test.py: visão parcial da bateria de testes para top
from collections.abc import Iterator
from typing import TYPE_CHECKING  # (1)

import pytest

from top import top

# muitas linhas omitidas

def test_top_tuples() -> None:
    fruit = 'mango pear apple kiwi banana'.split()
    series: Iterator[tuple[int, str]] = (  # (2)
        (len(s), s) for s in fruit)
    length = 3
    expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
    result = top(series, length)
    if TYPE_CHECKING:  # (3)
        reveal_type(series)  # (4)
        reveal_type(expected)
        reveal_type(result)
    assert result == expected

# intentional type error
def test_top_objects_error() -> None:
    series = [object() for _ in range(4)]
    if TYPE_CHECKING:
        reveal_type(series)
    with pytest.raises(TypeError) as excinfo:
        top(series, 3)  # (5)
    assert "'<' not supported" in str(excinfo.value)
  1. A constante typing.TYPE_CHECKING é sempre False durante a execução do programa, mas os verificadores de tipo fingem que ela é True quando estão fazendo a verificação.

  2. Declaração de tipo explícita para a variável series, para tornar mais fácil a leitura da saída do Mypy.[97]

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

⚠️ Aviso

Desde o Mypy 0.910 (julho de 2021), em alguns casos a saída de reveal_type não mostra 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 145. Saída do mypy top_test.py (linha quebradas para facilitar a leitura)
…/comparable/ $ mypy top_test.py
top_test.py:32: note:
    Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]" (1)
top_test.py:33: note:
    Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test.py:34: note:
    Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]" (2)
top_test.py:41: note:
    Revealed type is "builtins.list[builtins.object*]" (3)
top_test.py:43: error:
    Value of type variable "LT" of "top" cannot be "object"  (4)
Found 1 error in 1 file (checked 1 source file)
  1. Em test_top_tuples, reveal_type(series) mostra que ele é um Iterator[tuple[int, str]]— que eu declarei explicitamente.

  2. reveal_type(result) confirma que o tipo produzido pela chamada a top é o que eu queria: dado o tipo de series, o result é list[tuple[int, str]].

  3. Em test_top_objects_error, reveal_type(series) mostra que ele é uma list[object*]. Mypy põe um * após qualquer tipo que tenha sido inferido: eu não anotei o tipo de series nesse teste.

  4. Mypy marca o erro que esse teste produz intencionalmente: o tipo dos elementos do Iterable series não pode ser object (ele tem que ser do tipo SupportsLessThan).

A principal vantagem de um tipo protocolo sobre os ABCs é que o tipo não precisa de nenhuma declaração especial para ser consistente-com um tipo protocolo. Isso permite que um protocolo seja criado aproveitando tipos pré-existentes, ou tipos implementados em bases de código que não estão sob nosso controle. Eu não tenho que derivar ou registrar str, tuple, float, set, etc. com SupportsLessThan para usá-los onde um parâmetro SupportsLessThan é esperado. Eles só precisam implementar __lt__. E o verificador de tipo ainda será capaz de realizar seu trabalho, porque SupportsLessThan está explicitamente declarado como um Protocol— diferente dos protocolos implícitos comuns no duck typing, que são invisíveis para o verificador de tipos.

A classe especial Protocol foi introduzida na PEP 544—Protocols: Structural subtyping (static duck typing). O Exemplo 143 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.[98]

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.

✒️ Nota

O typing.Protocol torna possível anotar a função double na Seção 8.4 sem perder 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:

Callable[[ParamType1, ParamType2], ReturnType]

A lista de parâmetros - [ParamType1, ParamType2] — pode ter zero ou mais tipos.

Aqui está um exemplo no contexto de uma função repl, parte do interpretador iterativo simples que veremos na Seção 18.3:[99]

def repl(input_fn: Callable[[Any], str] = input]) -> None:

Durante a utilização normal, a função repl usa a input nativa de Python para ler expressões inseridas pelo usuário. Entretanto, para testagem automatizada ou para integração com outras fontes de input, repl aceita um parâmetro input_fn opcional: um Callable com o mesmo parâmetro e tipo de retorno de input.

A input nativa tem a seguinte assinatura no typeshed:

def input(__prompt: Any = ...) -> str: ...

A assinatura de input é consistente-com esta dica de tipo Callable

Callable[[Any], str]

Não existe sintaxe para a nomear tipo de argumentos opcionais ou de palavra-chave. A documentação 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:

Callable[..., ReturnType]

A interação de parâmetros de tipo genéricos com uma hierarquia de tipos introduz um novo conceito: variância.

8.5.11.1. Variância em tipos callable

Imagine um sistema de controle de temperatura com uma função update simples, como mostrada no Exemplo 146. A função update chama a função probe para obter a temperatura atual, e chama display para mostrar a temperatura para o usuário. probe e display são ambas passadas como argumentos para update, por motivos didáticos. O objetivo do exemplo é contrastar duas anotações de Callable: uma com um tipo de retorno e outro com um tipo de parâmetro.

Exemplo 146. Ilustrando a variância.
from collections.abc import Callable

def update(  # (1)
        probe: Callable[[], float],  # (2)
        display: Callable[[float], None]  # (3)
    ) -> None:
    temperature = probe()
    # imagine lots of control code here
    display(temperature)

def probe_ok() -> int:  # (4)
    return 42

def display_wrong(temperature: int) -> None:  # (5)
    print(hex(temperature))

update(probe_ok, display_wrong)  # type error  # (6)

def display_ok(temperature: complex) -> None:  # (7)
    print(temperature)

update(probe_ok, display_ok)  # OK  # (8)
  1. update recebe duas funções callable como argumentos.

  2. probe precisa ser uma callable que não recebe nenhuma argumento e retorna um float

  3. display recebe um argumento float e retorna None.

  4. probe_ok é consistente-com Callable[[], float] porque retornar um int não quebra código que espera um float.

  5. display_wrong não é consistente-com Callable[[float], None] porque não há garantia que uma função esperando um int consiga lidar com um float; por exemplo, a função hex de 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.

👉 Dica

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

  • Um objeto list[int] não é aceitável porque ele não pode conter valores float que meu código pode precisar colocar em scores.

  • Um objeto list[complex] não é aceitável porque meu código pode precisar ordenar scores para encontrar a mediana, mas complex não fornece o método __lt__, então list[complex] não é ordenável.

Agora chegamos ou último tipo especial que examinaremos nesse capítulo.

8.5.12. NoReturn

Esse é um tipo especial usado apenas para anotar o tipo de retorno de funções que nunca retornam. Normalmente, elas existem para gerar exceções. Há dúzias dessas funções na biblioteca padrão.

Por exemplo, sys.exit() levanta SystemExit para encerrar o processo Python.

Sua assinatura no typeshed é:

def exit(__status: object = ...) -> NoReturn: ...

O parâmetro __status__ é apenas posicional, e tem um valor default. Arquivos stub não contém valores default, em vez disso eles usam …​. O tipo de __status é object, o que significa que pode também ser None, assim seria redundante escrever Optional[object].

Na Capítulo 24, o Exemplo 457 usa NoReturn em __flag_unknown_attrs, um método projetado para produzir uma mensagem de erro completa e amigável, e então levanta um AttributeError.

A última seção desse capítulo épico é sobre parâmetros posicionais e variádicos

8.6. Anotando parâmetros apenas posicionais e variádicos

Lembra da função tag do Exemplo 113? Da última vez que vimos sua assinatura foi em Seção 7.7.1:

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 faria:

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 é 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:

from typing import Optional

def tag(__name: str, *content: str, class_: Optional[str] = None,
        **attrs: str) -> str:

O Mypy entende e aplica as duas formas de declarar parâmetros puramente posicionais.

Para encerrar esse capítulo, vamos considerar brevemente os limites das dicas de tipo e do sistema de tipagem estática que elas suportam.

8.7. Tipos imperfeitos e testes poderosos

Os mantenedores de grandes bases de código corporativas relatam que muitos bugs são encontrados por verificadores de tipo estáticos, e o custo de resolvê-los é menor que se os mesmos bugs fossem descobertos apenas após o código estar rodando em produção. Entretanto, é essencial observar que a testagem automatizada era uma prática padrão largamente adotada muito antes da tipagem estática ser introduzida nas empresas que eu conheço.

Mesmo em contextos onde ela é mais benéfica, a tipagem estática não pode ser elevada a árbitro final da correção. Não é difícil encontrar:

Falsos Positivos

Ferramentas indicam erros de tipagem em código correto.

Falsos Negativos

Ferramentas não indicam erros em código incorreto.

Além disso, se formos forçados a checar o tipo de tudo, perdemos um pouco do poder expressivo de 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 de 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.

✒️ Nota

O título e a conclusão dessa seção foram inspirados pelo artigo "Strong Typing vs. Strong Testing" (EN) de Bruce Eckel, também publicado na antologia The Best Software Writing I (EN), editada por Joel Spolsky (Apress). Bruce é um fã de Python, e autor de livros sobre C++, Java, Scala, e Kotlin. Naquele texto, ele conta como foi um defensor da tipagem estática até aprender Python, e conclui: "Se um programa em Python tem testes de unidade adequados, ele poderá ser tão robusto quanto um programa em C++, Java, ou C# com testes de unidade adequados (mas será mais rápido escrever os testes em Python).

Isso encerra nossa cobertura das dicas de tipo em Python por agora. Elas serão também o ponto central do Capítulo 15, que trata de classes genéricas, variância, assinaturas sobrecarregadas, coerção de tipos (type casting), entre outros tópicos. Até lá, as dicas de tipo aparecerão em várias funções ao longo do livro.

8.8. Resumo do capítulo

Começamos com uma pequena introdução ao conceito de tipagem gradual, depois adotamos uma abordagem prática. É difícil ver como a tipagem gradual funciona sem uma ferramenta que efetivamente leia as dicas de tipo, então desenvolvemos uma função anotada guiados pelos relatórios de erro do Mypy.

Voltando à ideia de tipagem gradual, vimos como ela é um híbrido do duck typing tradicional de Python e da tipagem nominal mais familiar aos usuários de Java, C++ e de outras 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 de 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 de Python, coberto pelo duck typing, e a tipagem nominal que permite a verificadores de tipo estáticos encontrarem bugs.

Ao discutir alguns desses tipos, usamos o Mypy para localizar erros de checagem de tipo e tipos inferidos, com a ajuda da função mágica reveal_type() do Mypy.

A seção final mostrou como anotar parâmetros exclusivamente posicionais e variádicos.

Dicas de tipo são um tópico complexo e em constante evolução. Felizmente elas são um recurso opcional. Vamos manter 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[100] 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.[101]

— Guido van Rossum

8.9. Para saber mais

Bernát Gábor escreveu em seu excelente post, "The state of type hints in Python" (EN):

Dicas de Tipo deveriam ser usadas sempre que valha à pena escrever testes de unidade .

Eu sou um grande fã de testes, mas também escrevo muito código exploratório. Quando estou explorando, testes e dicas de tipo não ajudam. São um entrave.

Esse post do Gábor é uma das melhores introduções a dicas de tipo em Python que eu já encontrei, junto com o texto de Geir Arne Hjelle, "Python Type Checking (Guide)" (EN). "Hypermodern Python Chapter 4: Typing" (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 é 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) (EN) e uma página muito útil sobre problemas comuns e suas soluções (EN).

A documentação do módulo typing é uma boa referência rápida, mas não entra em muitos detalhes.

A PEP 483—The Theory of Type Hints (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 de 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" é uma ótima coleção de links para ferramentas e referências.

Ponto de vista

Apenas Pedale

Esqueça as desconfortáveis bicicletas ultraleves, as malhas brilhantes, os sapatos desajeitados que se prendem a pedais minúsculos, o esforço de quilômetros intermináveis. Em vez disso, faça como você fazia quando era criança - suba na sua bicicleta e descubra o puro prazer de pedalar.

— Grant Petersen
Just Ride: A Radically Practical Guide to Riding Your Bike (Apenas Pedale: Um Guia Radicalmente Prático sobre o Uso de sua Bicicleta) (Workman Publishing)

Se programar não é sua profissão principal, mas uma ferramenta útil no seu trabalho ou algo que você faz para aprender, experimentar e se divertir, você provavelmente não precisa de dicas de tipo mais que a maioria dos ciclistas precisa de sapatos com solas rígidas e presilhas metálicas.

Apenas programe.

O Efeito Cognitivo da Tipagem

Eu me preocupo com o efeito que as dicas de tipo terão sobre o estilo de programação em Python.

Concordo que usuários da maioria das APIs se beneficiam de dicas de tipo. Mas 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(). Ela é poderosa, entretanto fácil de entender. Mas vou mostrar na Seção 15.2.1 que são necessárias 14 linhas de dicas de tipo para anotar corretamente essa função - sem contar um typing.Protocol e algumas definições de TypeVar para sustentar aquelas dicas de tipo.

Me inquieta que a coação estrita de dicas de tipo em bibliotecas desencorajem programadores de sequer considerarem programar funções assim no futuro.

De acordo com o verbete em inglês na Wikipedia, "relatividade linguística" — ou a hipótese Sapir–Whorf — é um "princípio alegando que a estrutura de uma linguagem afeta a visão de mundo ou a cognição de seus falantes"

A Wikipedia continua:

  • A versão forte diz que a linguagem determina o pensamento, e que categorias linguísticas limitam e determinam as categorias cognitivas.

  • A versão fraca diz que as categorias linguísticas e o uso apenas influenciam o pensamento e as decisões.

Linguistas em geral concordam que a versão forte é falsa, mas há evidência empírica apoiando a versão fraca.

Não conheço estudos específicos com linguagens de programação, mas na minha experiência, elas tiveram grande impacto sobre a forma como eu abordo problemas. A primeira linguagem de programação que usei profissionalmente foi o Applesoft BASIC, na era dos computadores de 8 bits. Recursão não era diretamente suportada pelo BASIC - você tinha que produzir sua própria pilha de chamada (call stack) para obter recursão. Então eu nunca considerei usar algoritmos ou estruturas de dados recursivos. Eu sabia, em algum nível conceitual, que tais coisas existiam, mas elas não eram parte de meu arsenal de técnicas de resolução de problemas.

Décadas mais tarde, quando aprendi Elixir, gostei de resolver problemas com recursão e usei essa técnica além da conta - até descobrir que muitas das minhas soluções seriam mais simples se que usasse funções existentes nos módulos Enum e Stream do Elixir. Aprendi que código de aplicações em Elixir idiomático raramente contém chamadas recursivas explícitas - em vez disso, usam enums e streams que implementam recursão por trás das cortinas.

A relatividade linguística pode explicar a ideia recorrente (e também não provada) que aprender linguagens de programação diferentes torna alguém um programador melhor, especialmente quando as linguagens em questão suportam diferentes paradigmas de programação. Praticar com Elixir me tornou mais propenso a aplicar patterns funcionais quando escrevo programas em Python ou Go.

Agora voltando à Terra.

O pacote requests provavelmente teria uma API muito diferente se Kenneth Reitz estivesse decidido (ou tivesse recebido ordens de seu chefe) a anotar todas as suas funções. Seu objetivo era escrever uma API que fosse fácil de usar, flexível e poderosa. Ele conseguiu, dada a fantástica popularidade de requests - em maio de 2020, ela estava em #4 nas PyPI Stats, 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 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 nomeado files em requests.request():

Optional[
  Union[
    Mapping[
      basestring,
      Union[
        Tuple[basestring, Optional[Union[basestring, file]]],
        Tuple[basestring, Optional[Union[basestring, file]],
              Optional[basestring]],
        Tuple[basestring, Optional[Union[basestring, file]],
              Optional[basestring], Optional[Headers]]
      ]
    ],
    Iterable[
      Tuple[
        basestring,
        Union[
          Tuple[basestring, Optional[Union[basestring, file]]],
          Tuple[basestring, Optional[Union[basestring, file]],
                Optional[basestring]],
          Tuple[basestring, Optional[Union[basestring, file]],
                Optional[basestring], Optional[Headers]]
      ]
    ]
  ]
]

E isso assume essa definição:

Headers = Union[
  Mapping[basestring, basestring],
  Iterable[Tuple[basestring, basestring]],
]

Você acha que requests seria como é se os mantenedores insistissem em ter uma cobertura de dicas de tipo de 100%? SQLAlchemy é outro pacote importante que não trabalha muito bem com dicas de tipo.

O que torna essas bibliotecas fabulosas é incorporarem a natureza dinâmica de Python.

Apesar das dicas de tipo trazerem benefícios, há também um preço a ser pago.

Primeiro, há o significativo investimento em entender como o sistema de tipos funciona. Esse é um custo unitário.

Mas há também um custo recorrente, eterno.

Nós perdemos um pouco do poder expressivo de 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 de 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..[102]

Obrigado, Guido, pela tipagem opcional. Vamos usá-la como foi pensada, e não tentar anotar tudo em conformidade estrita com um estilo de programação que se parece com Java 1.5.

Duck Typing FTW

Duck typing encaixa bem no meu cérebro, e duck typing estático é um bom compromisso, permitindo checagem estática de tipo sem perder muito da flexibilidade que alguns sistemas de tipagem nominal só permitem ao custo de muita complexidade - isso quando permitem.

Antes da PEP 544, toda essa ideia de dicas de tipo me parecia completamente não-pythônica, Fiquei muito feliz quando vi typing.Protocol surgir em Python. Ele traz equilíbrio para a Força.

Genéricos ou Específicos?

De uma perspectiva de Python, o uso do termo "genérico" na tipagem é um retrocesso. Os sentidos comuns do termo "genérico" são "aplicável integralmente a um grupo ou uma classe" ou "sem uma marca distintiva."

Considere list versus list[str]. o primeiro é genérico: aceita qualquer objeto. O segundo é específico: só aceita str.

Por outro lado, o termo faz sentido em Java. Antes de 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.[103] O nome decorator provavelmente se origina de seu uso no âmbito dos compiladores—​uma árvore sintática é percorrida e anotada.

— PEP 318—Decorators for Functions and Methods ("Decoradores para Funções e Métodos"—EN)

Decoradores de função nos permitem "marcar" funções no código-fonte, para aprimorar de alguma forma seu comportamento. É um mecanismo muito poderoso. Por exemplo, o decorador @functools.cache armazena um mapeamento de argumentos para resultados, e depois usa esse mapeamento para evitar computar novamente o resultado quando a função é chamada com argumentos já vistos. Isso pode acelerar muito uma aplicação.

Mas para dominar esse recurso é preciso antes entender clausuras (closures)—o nome dado à estrutura onde uma função captura variáveis presentes no escopo onde a função é definida, necessárias para a execução da função futuramente.[104]

A palavra reservada mais obscura de 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 codar em um estilo funcional quando isso fizer sentido.

O objetivo último deste capítulo é explicar exatamente como funcionam os decoradores de função, desde simples decoradores de registro até os complicados decoradores parametrizados. Mas antes de chegar a esse objetivo, precisamos tratar de:

  • Como Python analisa a sintaxe de decoradores

  • Como Python decide se uma variável é local

  • Porque clausuras existem e como elas funcionam

  • Qual problema é resolvido por nonlocal

Após criar essa base, poderemos então enfrentar os outros tópicos relativos aos decoradores:

  • A implementação de um decorador bem comportado

  • Os poderosos decoradores na biblioteca padrão: @cache, @lru_cache, e @singledispatch

  • A implementação de um decorador parametrizado

9.1. Novidades nesse capítulo

O decorador de caching functools.cache—introduzido no Python 3.9—é mais simples que o tradicional functools.lru_cache, então falo primeiro daquele. Este último é tratado na Seção 9.9.2, incluindo a forma simplificada introduzida no Python 3.8.

A Seção 9.9.3 foi expandida e agora inclui dicas de tipo, a forma recomendada de usar functools.singledispatch desde Python 3.7.

A Seção 9.10 agora inclui um exemplo baseado em classes, o Exemplo 173.

Transferi o #ch_design_patterns para o final da Parte II: Funções como objetos, para melhorar a fluidez do livro. E a Seção 10.3 também aparece agora naquele capítulo, juntamente com outras variantes do padrão de projeto Estratégia usando invocáveis.

Começamos com uma introdução muito suave aos decoradores, e dali seguiremos para o restante dos tópicos listados no início do capítulo.

9.2. Introdução aos decoradores

Um decorador é um invocável que recebe outra função como um argumento (a função decorada).

Um decorador pode executar algum processamento com a função decorada, e ou a devolve ou a substitui por outra função ou por um objeto invocável.[105]

Em outras palavras, supondo a existência de um decorador chamado decorate, esse código:

@decorate
def target():
    print('running target()')

tem o mesmo efeito de:

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

Exemplo 147. Um decorador normalmente substitui uma função por outra, diferente
>>> def deco(func):
...     def inner():
...         print('running inner()')
...     return inner  (1)
...
>>> @deco
... def target():  (2)
...     print('running target()')
...
>>> target()  (3)
running inner()
>>> target  (4)
<function deco.<locals>.inner at 0x10063b598>
  1. deco devolve seu objeto função inner.

  2. target é decorada por deco.

  3. Invocar a target decorada causa, na verdade, a execução de inner.

  4. A inspeção revela que target é agora uma referência a inner.

Estritamente falando, decoradores são apenas açúcar sintático. Como vimos, é sempre possível chamar um decorador como um invocável normal, passando outra função como parâmetro. Algumas vezes isso inclusive é conveniente, especialmente quando estamos fazendo metaprogramação—mudando o comportamento de um programa durante a execução.

Três fatos essenciais nos dão um bom resumo dos decoradores:

  • Um decorador é uma função ou outro invocável.

  • Um decorador pode substituir a função decorada por outra, diferente.

  • Decoradores são executados imediatamente quando um módulo é carregado.

Vamos agora nos concentrar nesse terceiro ponto.

9.3. Quando Python executa decoradores

Uma característica fundamental dos decoradores é serem executados logo após a função decorada ser definida. Isso normalmente acontece no tempo de importação (isto é, quando um módulo é carregado pelo Python). Observe registration.py no Exemplo 148.

Exemplo 148. O módulo registration.py
registry = []  # (1)

def register(func):  # (2)
    print(f'running register({func})')  # (3)
    registry.append(func)  # (4)
    return func  # (5)

@register  # (6)
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3():  # (7)
    print('running f3()')

def main():  # (8)
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__ == '__main__':
    main()  # (9)
  1. registry vai manter referências para funções decoradas por @register.

  2. register recebe uma função como argumento.

  3. Exibe a função que está sendo decorada, para fins de demonstração.

  4. Insere func em registry.

  5. Devolve func: precisamos devolver uma função; aqui devolvemos a mesma função recebida como argumento.

  6. f1 e f2 são decoradas por @register.

  7. f3 não é decorada.

  8. main mostra registry, depois chama f1(), f2(), e f3().

  9. main() só é invocada se registration.py for executado como um script.

O resultado da execução de registration.py se parece com isso:

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

Observe que register roda (duas vezes) antes de qualquer outra função no módulo. Quando register é chamada, ela recebe o objeto função a ser decorado como argumento—por exemplo, <function f1 at 0x100631bf8>.

Após o carregamento do módulo, a lista registry contém referências para as duas funções decoradas: f1 e f2. Essa funções, bem como f3, são executadas apenas quando chamadas explicitamente por main.

Se registration.py for importado (e não executado como um script), a saída é essa:

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

Nesse momento, se você inspecionar registry, verá isso:

>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

O ponto central do Exemplo 148 é enfatizar que decoradores de função são executados assim que o módulo é importado, mas as funções decoradas só rodam quando são invocadas explicitamente. Isso ressalta a diferença entre o que pythonistas chamam de tempo de importação e tempo de execução.

9.4. Decoradores de registro

Considerando a forma como decoradores são normalmente usados em código do mundo real, o Exemplo 148 é incomum por duas razões:

  • A função do decorador é definida no mesmo módulo das funções decoradas. Em geral, um decorador real é definido em um módulo e aplicado a funções de outros módulos.

  • O decorador register devolve a mesma função recebida como argumento. Na prática, a maior parte dos decoradores define e devolve uma função interna.

Apesar do decorador register no Exemplo 148 devolver a função decorada inalterada, a técnica não é inútil. Decoradores parecidos são usados por muitos 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 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. Código que usa funções internas quase sempre depende de clausuras para operar corretamente. Para entender as clausuras, precisamos dar um passo atrás e revisar como o escopo de variáveis funciona no Python.

9.5. Regras de escopo de variáveis

No Exemplo 149, definimos e testamos uma função que lê duas variáveis: uma variável local a—definida como parâmetro de função—e a variável b, que não é definida em lugar algum na função.

Exemplo 149. Função lendo uma variável local e uma variável global
>>> 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 149, se atribuirmos um valor a um b global e então chamarmos f1, funciona:

>>> b = 6
>>> f1(3)
3
6

Agora vamos ver um exemplo que pode ser surpreendente.

Dê uma olhada na função f2, no Exemplo 150. As primeiras duas linhas são as mesmas da f1 do Exemplo 149, e então ela faz uma atribuição a b. Mas para com um erro no segundo print, antes da atribuição ser executada.

Exemplo 150. A variável b é local, porque um valor é atribuído a ela no corpo da função
>>> 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 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: 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 de 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:

>>> b = 6
>>> def f3(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
...
>>> f3(3)
3
6
>>> b
9

Nos exemplos anteriores, vimos dois escopos em ação:

O escopo global de módulo

Composto por nomes atribuídos a valores fora de qualquer bloco de classe ou função.

O escopo local da função f3

Composto por nomes atribuídos a valores como parâmetros, ou diretamente no corpo da função.

Há um outro escopo de onde variáveis podem vir, chamado nonlocal, e ele é fundamental para clausuras; vamos tratar disso em breve.

Após ver mais de perto como o escopo de variáveis funciona no Python, podemos enfrentar as clausuras na próxima seção, Seção 9.6. Se você tiver curiosidade sobre as diferenças no bytecode das funções no Exemplo 149 e no Exemplo 150, veja o quadro a seguir.

Comparando bytecodes

O módulo dis module oferece uma forma fácil de descompilar o bytecode de funções de Python. Leia no Exemplo 151 e no Exemplo 152 os bytecodes de f1 e f2, do Exemplo 149 e do Exemplo 150, respectivamente.

Exemplo 151. Bytecode da função f1 do Exemplo 149
>>> from dis import dis
>>> dis(f1)
  2           0 LOAD_GLOBAL              0 (print)  (1)
              3 LOAD_FAST                0 (a)  (2)
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 POP_TOP

  3          10 LOAD_GLOBAL              0 (print)
             13 LOAD_GLOBAL              1 (b)  (3)
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 POP_TOP
             20 LOAD_CONST               0 (None)
             23 RETURN_VALUE
  1. Carrega o nome global print.

  2. Carrega o nome local a.

  3. Carrega o nome global b.

Compare o bytecode de f1, visto no Exemplo 151 acima, com o bytecode de f2 no Exemplo 152.

Exemplo 152. Bytecode da função f2 do Exemplo 150
>>> 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 de Python está além da finalidade desse livro, mas eles estão documentados junto com o módulo, em "dis—Disassembler de bytecode de Python".

9.6. Clausuras

Na blogosfera, as clausuras são algumas vezes confundidas com funções anônimas. Muita gente confunde os dois conceitos por causa da história paralela dos dois recursos: definir funções dentro de outras funções não é tão comum ou conveniente, até existirem funções anônimas. E clausuras só importam a partir do momento em que você tem funções aninhadas. Daí que muitos aprendem as duas ideias ao mesmo tempo.

Na verdade, uma clausura é uma função—vamos chamá-la de f—com um escopo estendido, incorporando variáveis referenciadas no corpo de f que não são nem variáveis globais nem variáveis locais de f. Tais variáveis devem vir do escopo local de uma função externa que englobe f.

Não interessa aqui se a função é anônima ou não; o que importa é que ela pode acessar variáveis não-globais definidas fora de seu corpo.

É um conceito difícil de entender, melhor ilustrado por um exemplo.

Imagine uma função avg, para calcular a média de uma série de valores que cresce continuamente; por exemplo, o preço de fechamento de uma commodity através de toda a sua história. A cada dia, um novo preço é acrescentado, e a média é computada levando em conta todos os preços até ali.

Começando do zero, avg poderia ser usada assim:

>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

Da onde vem avg, e onde ela mantém o histórico com os valores anteriores?

Para começar, o Exemplo 153 mostra uma implementação baseada em uma classe.

Exemplo 153. average_oo.py: uma classe para calcular uma média contínua
class Averager():

    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)

A classe Averager cria instâncias invocáveis:

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

O Exemplo 154, a seguir, é uma implementação funcional, usando a função de ordem superior make_averager.

Exemplo 154. average.py: uma função de ordem superior para a clacular uma média contínua
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 155.

Exemplo 155. Testando o Exemplo 154
>>> 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 153, avg é uma instância de Averager, no Exemplo 154 é 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 23.

Diagrama de uma clausura
Figura 23. 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 Python mantém os nomes de variáveis locais e livres no atributo __code__, que representa o corpo compilado da função. O Exemplo 156 demonstra isso.

Exemplo 156. Inspecionando a função criada por make_averager no Exemplo 154
>>> 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 157 mostra esses atributos.

Exemplo 157. Continuando do Exemplo 155
>>> 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 154, 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 158 é uma implementação errada, apenas para ilustrar um ponto. Você consegue ver onde o código quebra?

Exemplo 158. Um função de ordem superior incorreta para calcular um média contínua sem manter todo o histórico
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count

    return averager

Se você testar o Exemplo 158, eis o resultado:

>>> 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 154, 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 159.

Exemplo 159. Calcula uma média contínua sem manter todo o histórico (corrigida com o uso de nonlocal)
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count

    return averager

Após estudar o nonlocal, podemos resumir como a consulta de variáveis funciona no Python.

9.7.1. A lógica da consulta de variáveis

Quando uma função é definida, o compilador de bytecode de Python determina como encontrar uma variável x que aparece na função, baseado nas seguintes regras:[106]

  • Se há uma declaração global x, x vem de e é atribuída à variável global x do módulo.[107]

  • Se há uma declaração nonlocal x, x vem de e atribuída à variável local x na função circundante mais próxima de onde x for definida.

  • Se x é um parâmetro ou tem um valor atribuído a si no corpo da função, então x é uma variável local.

  • Se x é referenciada mas não atribuída, e não é um parâmetro:

    • x será procurada nos escopos locais do corpos das funções circundantes (os escopos nonlocal).

    • Se x não for encontrada nos escopos circundantes, será lida do escopo global do módulo.

    • Se x não for encontrada no escopo global, será lida de __builtins__.__dict__.

Tendo visto as clausuras de Python, podemos agora de fato implementar decoradores com funções aninhadas.

9.8. Implementando um decorador simples

O Exemplo 160 é um decorador que cronometra cada invocação da função decorada e exibe o tempo decorrido, os argumentos passados, e o resultado da chamada.

Exemplo 160. clockdeco0.py: decorador simples que mostra o tempo de execução de funções
import time


def clock(func):
    def clocked(*args):  # (1)
        t0 = time.perf_counter()
        result = func(*args)  # (2)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked  # (3)
  1. Define a função interna clocked para aceitar qualquer número de argumentos posicionais.

  2. Essa linha só funciona porque a clausura para clocked engloba a variável livre func.

  3. Devolve a função interna para substituir a função decorada.

O Exemplo 161 demonstra o uso do decorador clock.

Exemplo 161. Usando o decorador clock
import time
from clockdeco0 import clock

@clock
def snooze(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

if __name__ == '__main__':
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))

O resultado da execução do Exemplo 161 é o seguinte:

$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720

9.8.1. Como isso funciona

Lembre-se que esse código:

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

na verdade faz isso:

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 160). 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:

>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>

Então factorial agora mantém uma referência para a função clocked. Daqui por diante, cada vez que factorial(n) for chamada, clocked(n) será executada. Essencialmente, clocked faz o seguinte:

  1. Registra o tempo inicial t0.

  2. Chama a função factorial original, salvando o resultado.

  3. Computa o tempo decorrido.

  4. Formata e exibe os dados coletados.

  5. Devolve o resultado salvo no passo 2.

Esse é o comportamento típico de um decorador: ele substitui a função decorada com uma nova função que aceita os mesmos argumentos e (normalmente) devolve o que quer que a função decorada deveria devolver, enquanto realiza também algum processamento adicional.

👉 Dica

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 encaixam nessa descrição. Mas, no nível da implementação, os decoradores de 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 160 tem alguns defeitos: ele não suporta argumentos nomeados, e encobre o __name__ e o __doc__ da função decorada. O Exemplo 162 usa o decorador functools.wraps para copiar os atributos relevantes de func para clocked. E nessa nova versão os argumentos nomeados também são tratados corretamente.

Exemplo 162. clockdeco.py: um decorador clock melhora
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

Python tem três funções embutidas projetadas para decorar métodos: property, classmethod e staticmethod. Vamos discutir property na Seção 22.4 e os outros na Seção 11.5.

No Exemplo 162 vimos outro decorador importante: functools.wraps, um auxiliar na criação de decoradores bem comportados. Três dos decoradores mais interessantes da biblioteca padrão são cache, lru_cache e singledispatch—todos do módulo functools. Falaremos deles a seguir.

9.9.1. Memoização com functools.cache

O decorador functools.cache implementa memoização:[108] uma técnica de otimização que funciona salvando os resultados de invocações anteriores de uma função dispendiosa, evitando repetir o processamento para argumentos previamente utilizados.

👉 Dica

O functools.cache foi introduzido no Python 3.9. Se você precisar rodar esses exemplo no Python 3.8, substitua @cache por @lru_cache. Em versões anteriores é preciso invocar o decorador, escrevendo @lru_cache(), como explicado na 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 163.

Exemplo 163. O modo recursivo e extremamente dispendioso de calcular o enésimo número na série de Fibonacci
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:

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

Exemplo 164. Implementação mais rápida, usando caching
import functools

from clockdeco import clock


@functools.cache  # (1)
@clock  # (2)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
    print(fibonacci(6))
  1. Essa linha funciona com Python 3.9 ou posterior. Veja a Seção 9.9.2 para uma alternativa que suporta versões anteriores de Python.

  2. Esse é um exemplo de decoradores empilhados: @cache é aplicado à função devolvida por @clock.

👉 Dica
Decoradore empilhados

Para entender os decoradores empilhados, 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:

@alpha
@beta
def my_fn():
    ...

é o mesmo que isso:

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 164, a função fibonacci é chamada apenas uma vez para cada valor de n:

$ 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 164 fez as 31 chamadas necessárias em 0,00017s (tempo total), enquanto o Exemplo 163 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.

⚠️ Aviso

O functools.cache pode consumir toda a memória disponível, se houver um número muito grande de itens no cache. Eu o considero mais adequado para scripts rápidos de linha de comando. Para processos de longa duração, recomendo usar functools.lru_cache com um parâmetro maxsize adequado, como explicado na próxima seção.

9.9.2. Usando o lru_cache

O decorador functools.cache é, na realidade, um mero invólucro em torno da antiga função functools.lru_cache, que é mais flexível e também compatível com Python 3.8 e outras versões anteriores.

A maior vantagem de @lru_cache é a possibilidade de limitar seu uso de memória através do parâmetro maxsize, que tem um default bastante conservador de 128—significando que o cache pode manter no máximo 128 registros simultâneos.

LRU é a sigla de Least Recently Used (literalmente "Usado Menos Recentemente"). Significa que registros que há algum tempo não são lidos, são descartados para dar lugar a novos itens.

Desde Python 3.8, lru_cache pode ser aplicado de duas formas. Abaixo vemos o modo mais simples em uso:

@lru_cache
def costly_function(a, b):
    ...

A outra forma—disponível desde Python 3.2—é invocá-lo como uma função, com ():

@lru_cache()
def costly_function(a, b):
    ...

Nos dois casos, os parâmetros default seriam utilizados. São eles:

maxsize=128

Estabelece o número máximo de registros a serem armazenados. Após o cache estar cheio, o registro menos recentemente usado é descartado, para dar lugar a cada novo item. Para um desempenho ótimo, maxsize deve ser uma potência de 2. Se você passar maxsize=None, a lógica LRU é desabilitada e o cache funciona mais rápido, mas os itens nunca são descartados, podendo levar a um consumo excessivo de memória. É assim que o @functools.cache funciona.

typed=False

Determina se os resultados de diferentes tipos de argumentos devem ser armazenados separadamente, Por exemplo, na configuração default, argumentos inteiros e de ponto flutuante considerados iguais são armazenados apenas uma vez. Assim, haverá apenas uma entrada para as chamadas f(1) e f(1.0). Se typed=True, aqueles argumentos produziriam registros diferentes, possivelmente armazenando resultados distintos.

Eis um exemplo de invocação de @lru_cache com parâmetros diferentes dos defaults:

@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
    ...

Vamos agora examinar outro decorador poderoso: functools.singledispatch.

9.9.3. Funções genéricas com despacho único

Imagine que estamos criando uma ferramenta para depurar aplicações web. Queremos gerar código HTML para tipos diferentes de objetos Python.

Poderíamos começar com uma função como essa:

import html

def htmlize(obj):
    content = html.escape(repr(obj))
    return f'<pre>{content}</pre>'

Isso funcionará para qualquer tipo de Python, mas agora queremos estender a função para gerar HTML específico para determinados tipos. Alguns exemplos seriam:

str

Substituir os caracteres de mudança de linha na string por '<br/>\n' e usar tags <p> tags em vez de <pre>.

int

Mostrar o número em formato decimal e hexadecimal (com um caso especial para bool).

list

Gerar uma lista em HTML, formatando cada item de acordo com seu tipo.

float e Decimal

Mostrar o valor como de costume, mas também na forma de fração (por que não?).

O comportamento que desejamos aparece no Exemplo 165.

Exemplo 165. htmlize() gera HTML adaptado para diferentes tipos de objetos
>>> htmlize({1, 2, 3})  # (1)
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre>&lt;built-in function abs&gt;</pre>'
>>> htmlize('Heimlich & Co.\n- a game')  # (2)
'<p>Heimlich &amp; Co.<br/>\n- a game</p>'
>>> htmlize(42)  # (3)
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}]))  # (4)
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
>>> htmlize(True)  # (5)
'<pre>True</pre>'
>>> htmlize(fractions.Fraction(2, 3))  # (6)
'<pre>2/3</pre>'
>>> htmlize(2/3)   # (7)
'<pre>0.6666666666666666 (2/3)</pre>'
>>> htmlize(decimal.Decimal('0.02380952'))
'<pre>0.02380952 (1/42)</pre>'
  1. A função original é registrada para object, então ela serve para capturar e tratar todos os tipos de argumentos que não foram capturados pelas outras implementações.

  2. Objetos str também passam por escape de HTML, mas são cercados por <p></p>, com quebras de linha <br/> inseridas antes de cada '\n'.

  3. Um int é exibido nos formatos decimal e hexadecimal, dentro de um bloco <pre></pre>.

  4. Cada item na lista é formatado de acordo com seu tipo, e a sequência inteira é apresentada como uma lista HTML.

  5. Apesar de ser um subtipo de int, bool recebe um tratamento especial.

  6. Mostra Fraction como uma fração.

  7. Mostra float e Decimal com a fração equivalemte aproximada.

9.9.3.1. Despacho único de funções

Como não temos no Python a sobrecarga de métodos ao estilo de Java, não podemos simplesmente criar variações de htmlize com assinaturas diferentes para cada tipo de dado que queremos tratar de forma distinta. Uma solução possível em Python seria transformar htmlize em uma função de despacho, com uma cadeia de if/elif/… ou match/case/… chamando funções especializadas como htmlize_str, htmlize_int, etc. Isso não é extensível pelos usuários de nosso módulo, e é desajeitado: com o tempo, a despachante htmlize de tornaria grande demais, e o acoplamento entre ela e as funções especializadas seria excessivamente sólido.

O decorador functools.singledispatch permite que diferentes módulos contribuam para a solução geral, e que você forneça facilmente funções especializadas, mesmo para tipos pertencentes a pacotes externos que não possam ser editados. Se você decorar um função simples com @singledispatch, ela se torna o ponto de entrada para uma função genérica: Um grupo de funções que executam a mesma operação de formas diferentes, dependendo do tipo do primeiro argumento. É isso que signifca o termo despacho único. Se mais argumentos fossem usados para selecionar a função específica, teríamos um despacho múltiplo. O Exemplo 166 mostra como funciona.

⚠️ Aviso

functools.singledispatch existe desde Python 3.4, mas só passou a suportar dicas de tipo no Python 3.7. As últimas duas funções no Exemplo 166 ilustram a sintaxe que funciona em todas as versões de Python desde a 3.4.

Exemplo 166. @singledispatch cria uma @htmlize.register personalizada, para empacotar várias funções em uma função genérica
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers

@singledispatch  # (1)
def htmlize(obj: object) -> str:
    content = html.escape(repr(obj))
    return f'<pre>{content}</pre>'

@htmlize.register  # (2)
def _(text: str) -> str:  # (3)
    content = html.escape(text).replace('\n', '<br/>\n')
    return f'<p>{content}</p>'

@htmlize.register  # (4)
def _(seq: abc.Sequence) -> str:
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

@htmlize.register  # (5)
def _(n: numbers.Integral) -> str:
    return f'<pre>{n} (0x{n:x})</pre>'

@htmlize.register  # (6)
def _(n: bool) -> str:
    return f'<pre>{n}</pre>'

@htmlize.register(fractions.Fraction)  # (7)
def _(x) -> str:
    frac = fractions.Fraction(x)
    return f'<pre>{frac.numerator}/{frac.denominator}</pre>'

@htmlize.register(decimal.Decimal)  # (8)
@htmlize.register(float)
def _(x) -> str:
    frac = fractions.Fraction(x).limit_denominator()
    return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'
  1. @singledispatch marca a função base, que trata o tipo object.

  2. Cada função especializada é decorada com @«base».register.

  3. O tipo do primeiro argumento passado durante a execução determina quando essa definição de função em particular será utilizada. O nome das funções especializadas é irrelevante; _ é uma boa escolha para deixar isso claro.[109]

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

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

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 de Python pode fornecer alternativas para o tipo int com número fixo de bits como subclasses de numbers.Integral.[112]

👉 Dica

Usar ABCs ou typing.Protocol com @singledispatch permite que seu código suporte classes 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 (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 (EN).

✒️ Nota

O @singledispatch não foi criado para trazer para Python a sobrecarga de métodos no estilo de 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 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 166.

Vimos alguns decoradores recebendo argumentos, por exemplo @lru_cache() e o htmlize.register(float) criado por @singledispatch no Exemplo 166. A próxima seção mostra como criar decoradores que aceitam parâmetros.

9.10. Decoradores parametrizados

Ao analisar um decorador no código-fonte, Python passa a função decorada como primeiro argumento para a função do decorador. Mas como fazemos um decorador aceitar outros argumentos? A resposta é: criar uma fábrica de decoradores que recebe aqueles argumentos e devolve um decorador, que é então aplicado à função a ser decorada. Confuso? Com certeza. Vamos começar com um exemplo baseado no decorador mais simples que vimos: register no Exemplo 167.

Exemplo 167. O módulo registration.py resumido, do Exemplo 148, repetido aqui por conveniência
registry = []

def register(func):
    print(f'running register({func})')
    registry.append(func)
    return func

@register
def f1():
    print('running f1()')

print('running main()')
print('registry ->', registry)
f1()

9.10.1. Um decorador de registro parametrizado

Para tornar mais fácil a habilitação ou desabilitação do registro executado por register, faremos esse último aceitar um parâmetro opcional active que, se False, não registra a função decorada. Conceitualmente, a nova função register não é um decorador mas uma fábrica de decoradores. Quando chamada, ela devolve o decorador que será realmente aplicado à função alvo.

Exemplo 168. Para aceitar parâmetros, o novo decorador register precisa ser invocado como uma função
registry = set()  # (1)

def register(active=True):  # (2)
    def decorate(func):  # (3)
        print('running register'
              f'(active={active})->decorate({func})')
        if active:   # (4)
            registry.add(func)
        else:
            registry.discard(func)  # (5)

        return func  # (6)
    return decorate  # (7)

@register(active=False)  # (8)
def f1():
    print('running f1()')

@register()  # (9)
def f2():
    print('running f2()')

def f3():
    print('running f3()')
  1. registry é agora um set, tornando mais rápido acrescentar ou remover funções.

  2. register recebe um argumento nomeado opcional.

  3. A função interna decorate é o verdadeiro decorador; observe como ela aceita uma função como argumento.

  4. Registra func apenas se o argumento active (obtido da clausura) for True.

  5. Se not active e func in registry, remove a função.

  6. Como decorate é um decorador, ele deve devolver uma função.

  7. register é nossa fábrica de decoradores, então devolve decorate.

  8. A fábrica @register precisa ser invocada como uma função, com os parâmetros desejados.

  9. Mesmo se nenhum parâmetro for passado, ainda assim register deve ser chamado como uma função—@register()—isto é, para devolver o verdadeiro decorador, decorate.

O ponto central aqui é que register() devolve decorate, que então é aplicado à função decorada.

O código do Exemplo 168 está em um módulo registration_param.py. Se o importarmos, veremos o seguinte:

>>> 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 inseri-la (ou removê-la). Veja o Exemplo 169 para uma demonstração da adição e remoção de funções do registry.

Exemplo 169. Usando o módulo registration_param listado no Exemplo 168
>>> from registration_param import *
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry  # (1)
{<function f2 at 0x10073c268>}
>>> register()(f3)  # (2)
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry  # (3)
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2)  # (4)
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry  # (5)
{<function f3 at 0x10073c158>}
  1. Quando o módulo é importado, f2 é inserida no registry.

  2. A expressão register() devolve decorate, que então é aplicado a f3.

  3. A linha anterior adicionou f3 ao registry.

  4. Essa chamada remove f2 do registry.

  5. Confirma que apenas f3 permanece no registry.

O funcionamento de decoradores parametrizados é bastante complexo, e esse que acabamos de discutir é mais simples que a maioria. Decoradores parametrizados em geral substituem a função decorada, e sua construção exige um nível adicional de aninhamento. Vamos agora explorar a arquitetura de uma dessas pirâmides de funções.

9.10.2. Um decorador parametrizado de cronometragem

Nessa seção vamos revisitar o decorador clock, acrescentando um recurso: os usuários podem passar uma string de formatação, para controlar a saída do relatório sobre função cronometrada. Veja o Exemplo 170.

✒️ Nota

Para simplificar, o Exemplo 170 está baseado na implementação inicial de clock no Exemplo 160, e não na versão aperfeiçoada do Exemplo 162 que usa @functools.wraps, acrescentando assim mais uma camada de função.

Exemplo 170. Módulo clockdeco_param.py: o decorador clock parametrizado
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):  # (1)
    def decorate(func):      # (2)
        def clocked(*_args): # (3)
            t0 = time.perf_counter()
            _result = func(*_args)  # (4)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)  # (5)
            result = repr(_result)  # (6)
            print(fmt.format(**locals()))  # (7)
            return _result  # (8)
        return clocked  # (9)
    return decorate  # (10)

if __name__ == '__main__':

    @clock()  # (11)
    def snooze(seconds):
        time.sleep(seconds)

    for i in range(3):
        snooze(.123)
  1. clock é a nossa fábrica de decoradores parametrizados.

  2. decorate é o verdadeiro decorador.

  3. clocked envolve a função decorada.

  4. _result é o resultado efetivo da função decorada.

  5. _args mantém os verdadeiros argumentos de clocked, enquanto args é a str usada para exibição.

  6. result é a str que representa _result, para exibição.

  7. Usar **locals() aqui permite que qualquer variável local de clocked seja referenciada em fmt.[113]

  8. clocked vai substituir a função decorada, então ela deve devolver o mesmo que aquela função devolve.

  9. decorate devolve clocked.

  10. clock devolve decorate.

  11. Nesse auto-teste, clock() é chamado sem argumentos, então o decorador aplicado usará o formato default, str.

Se você rodar o Exemplo 170 no console, o resultado é o seguinte:

$ python3 clockdeco_param.py
[0.12412500s] snooze(0.123) -> None
[0.12411904s] snooze(0.123) -> None
[0.12410498s] snooze(0.123) -> None

Para exercitar a nova funcionalidade, vamos dar uma olhada em dois outros módulos que usam o clockdeco_param, o Exemplo 171 e o Exemplo 172, e nas saídas que eles geram.

Exemplo 171. clockdeco_param_demo1.py
import time
from clockdeco_param import clock

@clock('{name}: {elapsed}s')
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)

Saída do Exemplo 171:

$ python3 clockdeco_param_demo1.py
snooze: 0.12414693832397461s
snooze: 0.1241159439086914s
snooze: 0.12412118911743164s
Exemplo 172. clockdeco_param_demo2.py
import time
from clockdeco_param import clock

@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)

Saída do Exemplo 172:

$ python3 clockdeco_param_demo2.py
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
✒️ Nota

Lennart Regebro—um dos revisores técnicos da primeira edição—argumenta seria melhor programar decoradores como classes implementando __call__, e não como funções (caso dos exemplos nesse capítulo). Concordo que aquela abordagem é melhor para decoradores não-triviais. Mas para explicar a ideia básica desse recurso da linguagem, funções são mais fáceis de entender. Para técnicas robustas de criação de decoradores, veja as referências na Seção 9.12, especialmente o blog de Graham Dumpleton e o módulo wrapt, .

A próxima seção traz um exemplo no estilo recomendado por Regebro e Dumpleton.

9.10.3. Um decorador de cronometragem em forma de classe

Como um último exemplo, o Exemplo 173 mostra a implementação de um decorador parametrizado clock, programado como uma classe com __call__. Compare o Exemplo 170 com o Exemplo 173. Qual você prefere?

Exemplo 173. Módulo clockdeco_cls.py: decorador parametrizado clock, implementado como uma classe
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

class clock:  # (1)

    def __init__(self, fmt=DEFAULT_FMT):  # (2)
        self.fmt = fmt

    def __call__(self, func):  # (3)
        def clocked(*_args):
            t0 = time.perf_counter()
            _result = func(*_args)  # (4)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(self.fmt.format(**locals()))
            return _result
        return clocked
  1. Ao invés de uma função externa clock, a classe clock é nossa fábrica de decoradores parametrizados. A nomeei com um c minúsculo, para deixar claro que essa implementação é uma substituta direta para aquela no Exemplo 170.

  2. O argumento passado em clock(my_format) é atribuído ao parâmetro fmt aqui. O construtor da classe devolve uma instância de clock, com my_format armazenado em self.fmt.

  3. __call__ torna a instância de clock invocável. Quando chamada, a instância substitui a função decorada com clocked.

  4. clocked envolve a função decorada.

Isso encerra nossa exploração dos decoradores de função. Veremos os decoradores de classe no Capítulo 24.

9.11. Resumo do capítulo

Percorremos um terreno acidentado nesse capítulo. Tentei tornar a jornada tão suave quanto possível, mas entramos definitivamente nos domínios da meta-programação.

Partimos de um decorador simples @register, sem uma função interna, e terminamos com um @clock() parametrizado envolvendo dois níveis de funções aninhadas.

Decoradores de registro, apesar de serem essencialmente simples, tem aplicações reais nos 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 164. Para decoradores mais sofisticados, uma implementação baseada em classes pode ser mais fácil de ler e manter.

Como exemplos de decoradores parametrizados na biblioteca padrão, visitamos os poderosos @cache e @singledispatch, do módulo functools.

9.12. Leitura complementar

O item #26 do livro Effective Python, 2nd ed. (EN) (Addison-Wesley), de Brett Slatkin, trata das melhores práticas para decoradores de função, e recomenda sempre usar functools.wraps—que vimos no Exemplo 162.[114]

Graham Dumpleton tem, em seu blog, uma série de posts abrangentes (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). Seus conhecimentos profundos sobre o assunto também estão muito bem demonstrados no módulo wrapt, 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 Capítulo 23 na Parte III: Classes e protocolos é sobre descritores.

"Metaprogramming" (Metaprogramação) (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) (EN).

Criada quando os decoradores ainda eram um recurso novo no Python, a página wiki Python Decorator Library (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) (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) (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 Python.

Em um nível mais teórico, a PEP 227—​Statically Nested Scopes (Escopos Estaticamente Aninhados) (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 (EN) traz a justificativa e uma descrição detalhada do mecanismo de funções genéricas de despacho único. Um post de Guido van Rossum de março de 2005 "Five-Minute Multimethods in Python" (Multi-métodos em Python em Cinco Minutos) (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 é apenas um exemplo didático. Para conhecer uma implementação de funções genéricas de despacho múltiplo moderna e pronta para uso em produção, veja a Reg de Martijn Faassen–autor de Morepath, um framework web guiado por modelos e orientado a 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 Python tivesse escopo dinâmico e não tivesse clausuras, poderíamos improvisar avg—similar ao Exemplo 154—assim:

>>> ### 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) 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) é 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). 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":

É um testemunho eloquente dos perigos do escopo dinâmico, que mesmo o primeiro exemplo 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 (EN).

Por muitos anos, o lambda de Python não ofereceu clausuras, contribuindo para a má fama deste recurso entre os fãs da programação funcional na blogosfera. Isso foi resolvido no Python 2.2 (de dezembro de 2001), mas a blogosfera tem uma memória muito boa. Desde então, lambda é embaraçoso apenas por sua sintaxe limitada.

O decoradores de Python e o padrão de projeto Decorador

Os decoradores de função de 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 de 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:

O decorador se adapta à interface do componente decorado, assim sua presença é transparente 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.[115]

— Ralph Johnson
co-autor do clássico "Padrões de Projetos"

Em engenharia de software, um padrão de projeto é uma receita genérica para solucionar um problema de design frequente. Não é preciso conhecer padrões de projeto para acompanhar esse capítulo, vou explicar os padrões usados nos exemplos.

O uso de padrões de projeto em programação foi popularizado pelo livro seminal Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos (Addison-Wesley), de Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides—também conhecidos como "the Gang of Four" (A Gangue dos Quatro). O livro é um catálogo de 23 padrões, cada um deles composto por arranjos de classes e exemplificados com código em C++, mas assumidos como úteis também em outras linguagens orientadas a objetos.

Apesar dos padrões de projeto serem independentes da linguagem, isso não significa que todo padrão se aplica a todas as linguagens. Por exemplo, o Capítulo 17 vai mostrar que não faz sentido emular a receita do padrão Iterator (Iterador) (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:

A escolha da linguagem de programação é importante, pois ela influencia nosso ponto de vista. Nossos padrões supõe uma linguagem com recursos equivalentes aos de 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 incluído 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.[116]

Em sua apresentação de 1996, "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (EN), Peter Norvig afirma que 16 dos 23 padrões no Padrões de Projeto original se tornam "invisíveis ou mais simples" em uma linguagem dinâmica (slide 9). Ele está falando das linguagens Lisp e Dylan, mas muitos dos recursos dinâmicos relevantes também estão presentes no Python. Em especial, no contexto de linguagens com funções de primeira classe, Norvig sugere repensar os padrões clássicos conhecidos como Estratégia (Strategy), Comando (Command), Método Template (Template Method) e Visitante (Visitor).

O objetivo desse capítulo é mostrar como—em alguns casos—as funções podem realizar o mesmo trabalho das classes, com um código mais legível e mais conciso. Vamos refatorar uma implementaçao de Estratégia usando funções como objetos, removendo muito código redundante. Vamos também discutir uma abordagem similar para simplificar o padrão Comando.

10.1. Novidades nesse capítulo

Movi este capítulo para o final da Parte II, para poder então aplicar o decorador de registro na Seção 10.3, e também usar dicas de tipo nos exemplos. A maior parte das dicas de tipo usadas nesse capítulo não são complicadas, e ajudam na legibilidade.

10.2. Estudo de caso: refatorando Estratégia

Estratégia é um bom exemplo de um padrão de projeto que pode ser mais simples em Python, usando funções como objetos de primeira classe. Na próxima seção vamos descrever e implementar Estratégia usando a estrutura "clássica" descrita em Padrões de Projetos. Se você estiver familiarizado com o padrão clássico, pode pular direto para Seção 10.2.2, onde refatoramos o código usando funções, reduzindo significativamente o número de linhas.

10.2.1. Estratégia clássica

O diagrama de classes UML na Figura 24 retrata um arranjo de classes exemplificando o padrão Estratégia.

Cálculos de desconto de um pedido como estratégias
Figura 24. Diagrama de classes UML para o processamento de descontos em um pedido, implementado com o padrão de projeto Estratégia.

O padrão Estratégia é resumido assim em Padrões de Projetos:

Define uma família de algoritmos, encapsula cada um deles, e os torna intercambiáveis. Estratégia permite que o algoritmo varie de forma independente dos clientes que o usam.

Um exemplo claro de Estratégia, aplicado ao domínio do ecommerce, é o cálculo de descontos em pedidos de acordo com os atributos do cliente ou pela inspeção dos itens do pedido.

Considere uma loja online com as seguintes regras para descontos:

  • Clientes com 1.000 ou mais pontos de fidelidade recebem um desconto global de 5% por pedido.

  • Um desconto de 10% é aplicado a cada item com 20 ou mais unidades no mesmo pedido.

  • Pedidos com pelo menos 10 itens diferentes recebem um desconto global de 7%.

Para simplificar, vamos assumir que apenas um desconto pode ser aplicado a cada pedido.

O diagrama de classes UML para o padrão Estratégia aparece na Figura 24. Seus participantes são:

Contexto (Context)

Oferece um serviço delegando parte do processamento para componentes intercambiáveis, que implementam algoritmos alternativos. No exemplo de ecommerce, o contexto é uma classe Order, configurada para aplicar um desconto promocional de acordo com um de vários algoritmos.

Estratégia (Strategy)

A interface comum dos componentes que implementam diferentes algoritmos. No nosso exemplo, esse papel cabe a uma classe abstrata chamada Promotion.

Estratégia concreta (Concrete strategy)

Cada uma das subclasses concretas de Estratégia. FidelityPromo, BulkPromo, e LargeOrderPromo são as três estratégias concretas implementadas.

O código no Exemplo 174 segue o modelo da Figura 24. Como descrito em Padrões de Projetos, a estratégia concreta é escolhida pelo cliente da classe de contexto. No nosso exemplo, antes de instanciar um pedido, o sistema deveria, de alguma forma, selecionar o estratégia de desconto promocional e passá-la para o construtor de Order. A seleção da estratégia está fora do escopo do padrão.

Exemplo 174. Implementação da classe Order com estratégias de desconto intercambiáveis
from abc import ABC, abstractmethod
from collections.abc import Sequence
from decimal import Decimal
from typing import NamedTuple, Optional


class Customer(NamedTuple):
    name: str
    fidelity: int


class LineItem(NamedTuple):
    product: str
    quantity: int
    price: Decimal

    def total(self) -> Decimal:
        return self.price * self.quantity


class Order(NamedTuple):  # the Context
    customer: Customer
    cart: Sequence[LineItem]
    promotion: Optional['Promotion'] = None

    def total(self) -> Decimal:
        totals = (item.total() for item in self.cart)
        return sum(totals, start=Decimal(0))

    def due(self) -> Decimal:
        if self.promotion is None:
            discount = Decimal(0)
        else:
            discount = self.promotion.discount(self)
        return self.total() - discount

    def __repr__(self):
        return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'


class Promotion(ABC):  # the Strategy: an abstract base class
    @abstractmethod
    def discount(self, order: Order) -> Decimal:
        """Return discount as a positive dollar amount"""


class FidelityPromo(Promotion):  # first Concrete Strategy
    """5% discount for customers with 1000 or more fidelity points"""

    def discount(self, order: Order) -> Decimal:
        rate = Decimal('0.05')
        if order.customer.fidelity >= 1000:
            return order.total() * rate
        return Decimal(0)


class BulkItemPromo(Promotion):  # second Concrete Strategy
    """10% discount for each LineItem with 20 or more units"""

    def discount(self, order: Order) -> Decimal:
        discount = Decimal(0)
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * Decimal('0.1')
        return discount


class LargeOrderPromo(Promotion):  # third Concrete Strategy
    """7% discount for orders with 10 or more distinct items"""

    def discount(self, order: Order) -> Decimal:
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * Decimal('0.07')
        return Decimal(0)

Observe que no Exemplo 174, programei Promotion como uma classe base abstrata (ABC), para usar o decorador @abstractmethod e deixar o padrão mais explícito.

O Exemplo 175 apresenta os doctests usados para demonstrar e verificar a operação de um módulo implementando as regras descritas anteriormente.

Exemplo 175. Amostra de uso da classe Order com a aplicação de diferentes promoções
    >>> joe = Customer('John Doe', 0)  # (1)
    >>> ann = Customer('Ann Smith', 1100)
    >>> cart = (LineItem('banana', 4, Decimal('.5')),  # (2)
    ...         LineItem('apple', 10, Decimal('1.5')),
    ...         LineItem('watermelon', 5, Decimal(5)))
    >>> Order(joe, cart, FidelityPromo())  # (3)
    <Order total: 42.00 due: 42.00>
    >>> Order(ann, cart, FidelityPromo())  # (4)
    <Order total: 42.00 due: 39.90>
    >>> banana_cart = (LineItem('banana', 30, Decimal('.5')),  # (5)
    ...                LineItem('apple', 10, Decimal('1.5')))
    >>> Order(joe, banana_cart, BulkItemPromo())  # (6)
    <Order total: 30.00 due: 28.50>
    >>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) # (7)
    ...                  for sku in range(10))
    >>> Order(joe, long_cart, LargeOrderPromo())  # (8)
    <Order total: 10.00 due: 9.30>
    >>> Order(joe, cart, LargeOrderPromo())
    <Order total: 42.00 due: 42.00>
  1. Dois clientes: joe tem 0 pontos de fidelidade, ann tem 1.100.

  2. Um carrinho de compras com três itens.

  3. A promoção FidelityPromo não dá qualquer desconto para joe.

  4. ann recebe um desconto de 5% porque tem pelo menos 1.000 pontos.

  5. O banana_cart contém 30 unidade do produto "banana" e 10 maçãs.

  6. Graças à BulkItemPromo, joe recebe um desconto de $1,50 no preço das bananas.

  7. O long_cart tem 10 itens diferentes, cada um custando $1,00.

  8. joe recebe um desconto de 7% no pedido total, por causa da LargerOrderPromo.

O Exemplo 174 funciona perfeitamente bem, mas a mesma funcionalidade pode ser implementada com menos linhas de código em Python, se usarmos funções como objetos. Veremos como fazer isso na próxima seção.

10.2.2. Estratégia baseada em funções

Cada estratégia concreta no Exemplo 174 é uma classe com um único método, discount. Além disso, as instâncias de estratégia não tem nenhum estado (nenhum atributo de instância). Você poderia dizer que elas se parecem muito com funções simples, e estaria certa. O Exemplo 176 é uma refatoração do Exemplo 174, substituindo as estratégias concretas por funções simples e removendo a classe abstrata Promo. São necessários apenas alguns pequenos ajustes na classe Order.[117]

Exemplo 176. A classe Order com as estratégias de descontos implementadas como funções
from collections.abc import Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, Callable, NamedTuple


class Customer(NamedTuple):
    name: str
    fidelity: int


class LineItem(NamedTuple):
    product: str
    quantity: int
    price: Decimal

    def total(self):
        return self.price * self.quantity

@dataclass(frozen=True)
class Order:  # the Context
    customer: Customer
    cart: Sequence[LineItem]
    promotion: Optional[Callable[['Order'], Decimal]] = None  # (1)

    def total(self) -> Decimal:
        totals = (item.total() for item in self.cart)
        return sum(totals, start=Decimal(0))

    def due(self) -> Decimal:
        if self.promotion is None:
            discount = Decimal(0)
        else:
            discount = self.promotion(self)  # (2)
        return self.total() - discount

    def __repr__(self):
        return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'


# (3)


def fidelity_promo(order: Order) -> Decimal:  # (4)
    """5% discount for customers with 1000 or more fidelity points"""
    if order.customer.fidelity >= 1000:
        return order.total() * Decimal('0.05')
    return Decimal(0)


def bulk_item_promo(order: Order) -> Decimal:
    """10% discount for each LineItem with 20 or more units"""
    discount = Decimal(0)
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * Decimal('0.1')
    return discount


def large_order_promo(order: Order) -> Decimal:
    """7% discount for orders with 10 or more distinct items"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * Decimal('0.07')
    return Decimal(0)
  1. Essa dica de tipo diz: promotion pode ser None, ou pode ser um invocável que recebe uma Order como argumento e devolve um Decimal.

  2. Para calcular o desconto, chama o invocável self.promotion, passando self como um argumento. Veja a razão disso logo abaixo.

  3. Nenhuma classe abstrata.

  4. Cada estratégia é uma função.

👉 Dica
Por que self.promotion(self)?

Na classe Order, promotion não é um método. É um atributo de instância que por acaso é invocável. Então a primeira parte da expressão, self.promotion, busca aquele invocável. Mas, ao invocá-lo, precisamos fornecer uma instância de Order, que neste caso é self. Por isso self aparece duas vezes na expressão.

A Seção 23.4 vai explicar o mecanismo que vincula automaticamente métodos a instâncias. Mas isso não se aplica a promotion, pois ela não é um método.

O código no Exemplo 176 é mais curto que o do Exemplo 174. Usar a nova Order é também um pouco mais simples, como mostram os doctests no Exemplo 177.

Exemplo 177. Amostra do uso da classe Order com as promoções como funções
    >>> joe = Customer('John Doe', 0)  # (1)
    >>> ann = Customer('Ann Smith', 1100)
    >>> cart = [LineItem('banana', 4, Decimal('.5')),
    ...         LineItem('apple', 10, Decimal('1.5')),
    ...         LineItem('watermelon', 5, Decimal(5))]
    >>> Order(joe, cart, fidelity_promo)  # (2)
    <Order total: 42.00 due: 42.00>
    >>> Order(ann, cart, fidelity_promo)
    <Order total: 42.00 due: 39.90>
    >>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
    ...                LineItem('apple', 10, Decimal('1.5'))]
    >>> Order(joe, banana_cart, bulk_item_promo)  # (3)
    <Order total: 30.00 due: 28.50>
    >>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
    ...               for item_code in range(10)]
    >>> Order(joe, long_cart, large_order_promo)
    <Order total: 10.00 due: 9.30>
    >>> Order(joe, cart, large_order_promo)
    <Order total: 42.00 due: 42.00>
  1. Mesmos dispositivos de teste do Exemplo 174.

  2. Para aplicar uma estratégia de desconto a uma Order, basta passar a função de promoção como argumento.

  3. Uma função de promoção diferente é usada aqui e no teste seguinte.

Observe os textos explicativos do Exemplo 177—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)".[118] 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."[119] O compartilhamento é recomendado para reduzir o custo da criação de um novo objeto concreto de estratégia, quando a mesma estratégia é aplicada repetidamente a cada novo contexto—no nosso exemplo, a cada nova instância de Order. Então, para contornar uma desvantagem do padrão Estratégia—seu custo durante a execução—os autores recomendam a aplicação de mais outro padrão. Enquanto isso, o número de linhas e custo de manutenção de seu código vão se acumulando.

Um caso de uso mais espinhoso, com estratégias concretas complexas mantendo estados internos, pode exigir a combinação de todas as partes dos padrões de projeto Estratégia e Peso Mosca. Muitas vezes, porém, estratégias concretas não tem estado interno; elas lidam apenas com dados vindos do contexto. Neste caso, não tenha dúvida, use as boas e velhas funções ao invés de escrever classes de um só metodo implementando uma interface de um só método declarada em outra classe diferente. Uma função pesa menos que uma instância de uma classe definida pelo usuário, e não há necessidade do Peso Mosca, pois cada função da estratégia é criada apenas uma vez por processo Python, quando o módulo é carregado. Uma função simples também é um "objeto compartilhado que pode ser usado em múltiplos contextos simultaneamente".

Uma vez implementado o padrão Estratégia com funções, outras possibilidades nos ocorrem. Suponha que você queira criar uma "meta-estratégia", que seleciona o melhor desconto disponível para uma dada Order. Nas próximas seções vamos estudar as refatorações adicionais para implementar esse requisito, usando abordagens que se valem de funções e módulos vistos como objetos.

10.2.3. Escolhendo a melhor estratégia: uma abordagem simples

Dados os mesmos clientes e carrinhos de compras dos testes no Exemplo 177, vamos agora acrescentar três testes adicionais ao Exemplo 178.

Exemplo 178. A funcão best_promo aplica todos os descontos e devolve o maior
    >>> Order(joe, long_cart, best_promo)  # (1)
    <Order total: 10.00 due: 9.30>
    >>> Order(joe, banana_cart, best_promo)  # (2)
    <Order total: 30.00 due: 28.50>
    >>> Order(ann, cart, best_promo)  # (3)
    <Order total: 42.00 due: 39.90>
  1. best_promo selecionou a larger_order_promo para o cliente joe.

  2. Aqui joe recebeu o desconto de bulk_item_promo, por comprar muitas bananas.

  3. Encerrando a compra com um carrinho simples, best_promo deu à cliente fiel ann o desconto da fidelity_promo.

A implementação de best_promo é muito simples. Veja o Exemplo 179.

Exemplo 179. best_promo encontra o desconto máximo iterando sobre uma lista de funções
promos = [fidelity_promo, bulk_item_promo, large_order_promo]  # (1)


def best_promo(order: Order) -> Decimal:  # (2)
    """Compute the best discount available"""
    return max(promo(order) for promo in promos)  # (3)
  1. promos: lista de estratégias implementadas como funções.

  2. best_promo recebe uma instância de Order como argumento, como as outras funções *_promo.

  3. Usando uma expressão geradora, aplicamos cada uma das funções de promos a order, e devolvemos o maior desconto encontrado.

O Exemplo 179 é 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 179 funcionar e ser fácil de ler, há alguma duplicação que poderia levar a um bug sutil: para adicionar uma nova estratégia, precisamos escrever a função e lembrar de incluí-la na lista promos. De outra forma a nova promoção só funcionará quando passada explicitamente como argumento para Order, e não será considerada por best_promotion.

Vamos examinar algumas soluções para essa questão.

10.2.4. Encontrando estratégias em um módulo

Módulos também são objetos de primeira classe no Python, e a biblioteca padrão oferece várias funções para lidar com eles. A função embutida globals é descrita assim na documentação de Python:

globals()

Devolve um dicionário representando a tabela de símbolos globais atual. Isso é sempre o dicionário do módulo atual (dentro de uma função ou método, esse é o módulo onde a função ou método foram definidos, não o módulo de onde são chamados).

O Exemplo 180 é uma forma um tanto hacker de usar globals para ajudar best_promo a encontrar automaticamente outras funções *_promo disponíveis.

Exemplo 180. A lista promos é construída a partir da introspecção do espaço de nomes global do módulo
from decimal import Decimal
from strategy import Order
from strategy import (
    fidelity_promo, bulk_item_promo, large_order_promo  # (1)
)

promos = [promo for name, promo in globals().items()  # (2)
                if name.endswith('_promo') and        # (3)
                   name != 'best_promo'               # (4)
]


def best_promo(order: Order) -> Decimal:              # (5)
    """Compute the best discount available"""
    return max(promo(order) for promo in promos)
  1. Importa as funções de promoções, para que fiquem disponíveis no espaço de nomes global.[120]

  2. Itera sobre cada item no dict devolvido por globals().

  3. Seleciona apenas aqueles valores onde o nome termina com o sufixo _promo e…​

  4. …​filtra e remove a própria best_promo, para evitar uma recursão infinita quando best_promo for invocada.

  5. Nenhuma mudança em best_promo.

Outra forma de coletar as promoções disponíveis seria criar um módulo e colocar nele todas as funções de estratégia, exceto best_promo.

No Exemplo 181, 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 181 depende da importação do módulo promotions bem como de inspect, que fornece funções de introspecção de alto nível.

Exemplo 181. A lista promos é construída a partir da introspecção de um novo módulo, promotions
from decimal import Decimal
import inspect

from strategy import Order
import promotions


promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]


def best_promo(order: Order) -> Decimal:
    """Compute the best discount available"""
    return max(promo(order) for promo in promos)

A função inspect.getmembers devolve os atributos de um objeto—neste caso, o módulo promotions—opcionalmente filtrados por um predicado (uma função booleana). Usamos inspect.isfunction para obter apenas as funções do módulo.

O Exemplo 181 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 181 não é oferecer uma solução completa, mas enfatizar um uso possível da introspecção de módulo.

Uma alternativa mais explícita para coletar dinamicamente as funções de desconto promocional seria usar um decorador simples. É nosso próximo tópico.

10.3. Padrão Estratégia aperfeiçoado com um decorador

Lembre-se que nossa principal objeção ao Exemplo 179 foi a repetição dos nomes das funções em suas definições e na lista promos, usada pela função best_promo para determinar o maior desconto aplicável. A repetição é problemática porque alguém pode acrescentar uma nova função de estratégia promocional e esquecer de adicioná-la manualmente à lista promos—caso em que best_promo vai silenciosamente ignorar a nova estratégia, introduzindo no sistema um bug sutil. O Exemplo 182 resolve esse problema com a técnica vista na Seção 9.4.

Exemplo 182. A lista promos é preenchida pelo decorador promotion
Promotion = Callable[[Order], Decimal]

promos: list[Promotion] = []  # (1)


def promotion(promo: Promotion) -> Promotion:  # (2)
    promos.append(promo)
    return promo


def best_promo(order: Order) -> Decimal:
    """Compute the best discount available"""
    return max(promo(order) for promo in promos)  # (3)


@promotion  # (4)
def fidelity(order: Order) -> Decimal:
    """5% discount for customers with 1000 or more fidelity points"""
    if order.customer.fidelity >= 1000:
        return order.total() * Decimal('0.05')
    return Decimal(0)


@promotion
def bulk_item(order: Order) -> Decimal:
    """10% discount for each LineItem with 20 or more units"""
    discount = Decimal(0)
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * Decimal('0.1')
    return discount


@promotion
def large_order(order: Order) -> Decimal:
    """7% discount for orders with 10 or more distinct items"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * Decimal('0.07')
    return Decimal(0)
  1. A lista promos é global no módulo, e começa vazia.

  2. promotion é um decorador de registro: ele devolve a função promo inalterada, após inserí-la na lista promos.

  3. Nenhuma mudança é necessária em best_promo, pois ela se baseia na lista promos.

  4. Qualquer função decorada com @promotion será adicionada a promos.

Essa solução tem várias vantagens sobre aquelas apresentadas anteriormente:

  • As funções de estratégia de promoção não precisam usar nomes especiais—não há necessidade do sufixo _promo.

  • O decorador @promotion realça o propósito da função decorada, e também torna mais fácil desabilitar temporariamente uma promoção: basta transformar a linha do decorador em comentário.

  • Estratégias de desconto promocional podem ser definidas em outros módulos, em qualquer lugar do sistema, desde que o decorador @promotion seja aplicado a elas.

Na próxima seção vamos discutir Comando (Command)—outro padrão de projeto que é algumas vezes implementado via classes de um só metodo, quando funções simples seriam suficientes.

10.4. O padrão Comando

Comando é outro padrão de projeto que pode ser simplificado com o uso de funções passadas como argumentos. A Figura 25 mostra o arranjo das classes nesse padrão.

Aplicação do padrão Comando a um editor de texto
Figura 25. 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 receptor (receiver) diferente: o objeto que implementa a ação. Para PasteCommand, o receptor é Document. Para OpenCommand, o receptor á 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 receptor). No exemplo em Padrões de Projetos, cada remetente é um item de menu em uma aplicação gráfica, e os receptors 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 receptor para executar a operação desejada. Assim, o remetente não precisa conhecer a interface do receptor, e receptors 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 25 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 183.

Exemplo 183. Cada instância de MacroCommand tem uma lista interna de comandos
class MacroCommand:
    """A command that executes a list of commands"""

    def __init__(self, commands):
        self.commands = list(commands)  # (1)

    def __call__(self):
        for command in self.commands:  # (2)
            command()
  1. Criar uma nova lista com os itens do argumento commands garante que ela seja iterável e mantém uma cópia local de referências a comandos em cada instância de MacroCommand.

  2. Quando uma instância de MacroCommand é invocada, cada comando em self.commands é chamado em sequência.

Usos mais avançados do padrão Comando—para implementar "desfazer", por exemplo—podem exigir mais que uma simples função de callback. Mesmo assim, Python oferece algumas alternativas que merecem ser consideradas:

  • Uma instância invocável como MacroCommand no Exemplo 183 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 de Python implementa uma interface de método único, e esse método se chama __call__.

10.5. Resumo do Capítulo

Como apontou Peter Norvig alguns anos após o surgimento do clássico Padrões de Projetos, "16 dos 23 padrões tem implementações qualitativamente mais simples em Lisp ou Dylan que em C++, pelo menos para alguns usos de cada padrão" (slide 9 da apresentação de Norvig, "Design Patterns in Dynamic Languages" presentation (Padrões de Projetos em Linguagens Dinâmicas)). Python compartilha alguns dos recursos dinâmicos das linguagens Lisp e Dylan, especialmente funções de primeira classe, nosso foco nesse capítulo.

Na mesma palestra citada no início deste capítulo, refletindo sobre o 20º aniversário de Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos, Ralph Johnson afirmou que um dos defeitos do livro é: "Excesso de ênfase nos padrões como linhas de chegada, em vez de como etapas em um processo de design".[121] 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 primeira 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. (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 de 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 (EN) e um conjunto de slides em seu site pessoal (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 de 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 em 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 Python e Ruby estão mais próximos entre si que de Java ou do C++.

Em "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (slides), Peter Norvig mostra como funções de primeira classe (e outros recursos dinâmicos) tornam vários dos padrões de projeto originais mais simples ou mesmo desnecessários.

A "Introdução" do Padrões de Projetos original, de Gamma et al. já vale o preço do livro—mais até que o catálogo de 23 padrões, que inclui desde receitas muito importantes até algumas raramente úteis. Alguns princípios de projetos de software muito citados, como "Programe para uma interface, não para uma implementação" e "Prefira a composição de objetos à herança de classe", vem ambos daquela introdução.

A aplicação de padrões a projetos se originou com o arquiteto Christopher Alexander et al., e foi apresentada no livro A Pattern Language ("Uma Linguagem de Padrões") (Oxford University Press). A ideia de Alexander é criar um vocabulário padronizado, permitindo que equipes compartilhem decisões comuns em projetos de edificações. M. J. Dominus wrote “‘Design Patterns’ Aren’t” (Padrões de Projetos Não São), 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

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)). No Capítulo 9, vimos que Python também tem funções genéricas (na 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 de Python sobre padrões de projetos é muito pequena, se comparada à existente para Java, C++ ou Ruby. Na Seção 10.6, mencionei Learning Python Design Patterns ("Aprendendo Padrões de Projeto de 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 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:

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

Parte III: Classes e protocolos

11. Um objeto pythônico

Para uma biblioteca ou framework, ser pythônica significa tornar tão fácil e tão natural quanto possível que uma programadora Python descubra como realizar uma tarefa.[123]

— Martijn Faassen
criador de frameworks Python e JavaScript

Graças ao Modelo de Dados de 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 um framework, os programadores que usarão suas classes talvez esperem que elas se comportem como as classes fornecidas pelo Python. Satisfazer tal expectativa é um dos jeitos de ser "pythônico".

Esse capítulo começa onde o Capítulo 1 terminou, mostrando como implementar vários métodos especiais comumente vistos em objetos Python de diferentes tipos.

Veremos como:

  • Suportar as funções embutidas que convertem objetos para outros tipos (por exemplo, repr(), bytes(), complex(), etc.)

  • Implementar um construtor alternativo como um método da classe

  • Estender a mini-linguagem de formatação usada pelas f-strings, pela função embutida format() e pelo método str.format()

  • Fornecer acesso a atributos apenas para leitura

  • Tornar um objetos hashable, para uso em sets e como chaves de dict

  • Economizar memória com __slots__

Vamos fazer tudo isso enquanto desenvolvemos Vector2d, um tipo simples de vetor euclidiano bi-dimensional. No Capítulo 12, o mesmo código servirá de base para uma classe de vetor N-dimensional.

A evolução do exemplo será interrompida para discutirmos dois tópicos conceituais:

  • Como e quando usar os decoradores @classmethod e @staticmethod

  • Atributos privados e protegidos no Python: uso, convenções e limitações

11.1. Novidades nesse capítulo

Acrescentei uma nova epígrafe e também algumas palavras ao segundo parágrafo do capítulo, para falar do conceito de "pythônico"—que na primeira edição era discutido apenas no final do livro.

A Seção 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 Python 3.0, e as ideias centrais apareceram no Python 2.2.

Vamos começar pelos métodos de representação de objetos.

11.2. Representações de objetos

Todas as linguagens orientadas a objetos tem pelo menos uma forma padrão de se obter uma representação de qualquer objeto como uma string. Python tem duas formas:

repr()

Devolve uma string representando o objeto como o desenvolvedor quer vê-lo. É o que aparece quando o console de 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.

⚠️ Aviso

Se você está vindo de Python 2, lembre-se que no Python 3 __repr__, __str__ e __format__ devem sempre devolver strings Unicode (tipo str). Apenas __bytes__ deveria devolver uma sequência de bytes (tipo bytes).

11.3. A volta da classe Vector

Para demonstrar os vários métodos usados para gerar representações de objetos, vamos criar uma classe Vector2d, similar à que vimos no Capítulo 1. O Exemplo 184 ilustra o comportamento básico que esperamos de uma instância de Vector2d.

Exemplo 184. Instâncias de Vector2d têm várias representações
    >>> v1 = Vector2d(3, 4)
    >>> print(v1.x, v1.y)  # (1)
    3.0 4.0
    >>> x, y = v1  # (2)
    >>> x, y
    (3.0, 4.0)
    >>> v1  # (3)
    Vector2d(3.0, 4.0)
    >>> v1_clone = eval(repr(v1))  # (4)
    >>> v1 == v1_clone  # (5)
    True
    >>> print(v1)  # (6)
    (3.0, 4.0)
    >>> octets = bytes(v1)  # (7)
    >>> octets
    b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
    >>> abs(v1)  # (8)
    5.0
    >>> bool(v1), bool(Vector2d(0, 0))  # (9)
    (True, False)
  1. Os componentes de um Vector2d podem ser acessados diretamente como atributos (não é preciso invocar métodos getter).

  2. Um Vector2d pode ser desempacotado para uma tupla de variáveis.

  3. O repr de um Vector2d emula o código-fonte usado para construir a instância.

  4. Usar eval aqui mostra que o repr de um Vector2d é uma representação fiel da chamada a seu construtor.[124]

  5. Vector2d suporta a comparação com ==; isso é útil para testes.

  6. print chama str, que no caso de Vector2d exibe um par ordenado.

  7. bytes usa o método __bytes__ para produzir uma representação binária.

  8. abs usa o método __abs__ para devolver a magnitude do Vector2d.

  9. bool usa o método __bool__ para devolver False se o Vector2d tiver magnitude zero, caso contrário esse método devolve True.

O Vector2d do Exemplo 184 é implementado em vector2d_v0.py (no Exemplo 185). O código está baseado no Exemplo 2, exceto pelos métodos para os operadores + e *, que veremos mais tarde no Capítulo 16. Vamos acrescentar o método para ==, já que ele é útil para testes. Nesse ponto, Vector2d usa vários métodos especiais para oferecer operações que um pythonista espera encontrar em um objeto bem projetado.

Exemplo 185. vector2d_v0.py: todos os métodos até aqui são métodos especiais
from array import array
import math


class Vector2d:
    typecode = 'd'  # (1)

    def __init__(self, x, y):
        self.x = float(x)    # (2)
        self.y = float(y)

    def __iter__(self):
        return (i for i in (self.x, self.y))  # (3)

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)  # (4)

    def __str__(self):
        return str(tuple(self))  # (5)

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # (6)
                bytes(array(self.typecode, self)))  # (7)

    def __eq__(self, other):
        return tuple(self) == tuple(other)  # (8)

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

    def __bool__(self):
        return bool(abs(self))  # (10)
  1. typecode é um atributo de classe, usado na conversão de instâncias de Vector2d de/para bytes.

  2. Converter x e y para float em __init__ captura erros mais rápido, algo útil quando Vector2d é chamado com argumentos inadequados.

  3. __iter__ torna um Vector2d iterável; é isso que faz o desempacotamento funcionar (por exemplo, x, y = my_vector). Vamos implementá-lo aqui usando uma expressão geradora para produzir os componentes, um após outro.[125]

  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.

⚠️ Aviso

O método __eq__ no Exemplo 185 funciona para operandos Vector2d, mas também devolve True ao comparar instâncias de Vector2d a outros iteráveis contendo os mesmos valores numéricos (por exemplo, Vector(3, 4) == [3, 4]). Isso pode ser considerado uma característica ou um bug. Essa discussão terá que esperar até o Capítulo 16, onde falamos de sobrecarga de operadores.

Temos um conjunto bastante completo de métodos básicos, mas ainda precisamos de uma maneira de reconstruir um Vector2d a partir da representação binária produzida por bytes().

11.4. Um construtor alternativo

Já que podemos exportar um Vector2d na forma de bytes, naturalmente precisamos de um método para importar um Vector2d de uma sequência binária. Procurando na biblioteca padrão por algo similar, descobrimos que array.array tem um método de classe chamado .frombytes, adequado a nossos propósitos—​já o vimos na Seção 2.10.1. Adotamos o mesmo nome e usamos sua funcionalidade em um método de classe para Vector2d em vector2d_v1.py (no Exemplo 186).

Exemplo 186. 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 185)
    @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.[126]

  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 de Python. Vamos então falar um pouco disso.

11.5. classmethod versus staticmethod

O decorador classmethod não é mencionado no tutorial de Python, nem tampouco o staticmethod. Qualquer um que tenha aprendido OO com Java pode se perguntar porque Python tem esses dois decoradores, e não apenas um deles.

Vamos começar com classmethod. O Exemplo 186 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 186. 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 187 compara a operação de classmethod e staticmethod.

Exemplo 187. Comparando o comportamento de classmethod e staticmethod
>>> class Demo:
...     @classmethod
...     def klassmeth(*args):
...         return args  # (1)
...     @staticmethod
...     def statmeth(*args):
...         return args  # (2)
...
>>> Demo.klassmeth()  # (3)
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth()   # (4)
()
>>> Demo.statmeth('spam')
('spam',)
  1. klassmeth apenas devolve todos os argumentos posicionais.

  2. statmeth faz o mesmo.

  3. Não importa como ele seja invocado, Demo.klassmeth recebe sempre a classe Demo como primeiro argumento.

  4. Demo.statmeth se comporta exatamente como uma boa e velha função.

✒️ Nota

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

Agora que vimos para que serve o classmethod (e que o staticmethod não é muito útil), vamos voltar para a questão da representação de objetos e entender como gerar uma saída formatada.

11.6. Exibição formatada

As f-strings, a função embutida format() e o método str.format() delegam a formatação efetiva para cada tipo, chamando seu método .__format__(format_spec). O format_spec especifica a formatação desejada, e é:

  • O segundo argumento em format(my_obj, format_spec), ou

  • O que quer que apareça após os dois pontos (:) em um campo de substituição delimitado por {} dentro de uma f-string ou o fmt em fmt.str.format()

Por exemplo:

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

👉 Dica

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. Após pegar o jeito desta última, leia "Literais de string formatados" e "Sintaxe das string de formato", para aprender sobre a notação de campo de substituição ({:}), usada em f-strings e no método str.format() (incluindo os marcadores de conversão !s, !r, e !a). F-strings não tornam str.format() obsoleto: na maioria dos casos f-strings resolvem o problema, mas algumas vezes é melhor especificar a string de formatação em outro lugar (e não onde ela será renderizada).

Alguns tipos embutidos tem seus próprios códigos de apresentação na Mini-Linguagem de Especificação de Formato. Por exemplo—entre muitos outros códigos—o tipo int suporta b e x, para saídas em base 2 e base 16, respectivamente, enquanto float implementa f, para uma exibição de ponto fixo, e %, para exibir porcentagens:

>>> 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():

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

>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'

Entretanto, se você passar um especificador de formato, object.__format__ gera um TypeError:

>>> format(v1, '.3f')
Traceback (most recent call last):
  ...
TypeError: non-empty format string passed to object.__format__

Vamos corrigir isso implementando nossa própria mini-linguagem de formatação. O primeiro passo será presumir que o especificador de formato fornecido pelo usuário tem por objetivo formatar cada componente float do vetor. Esse é o resultado esperado:

>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'

O Exemplo 188 implementa __format__ para produzir as formatações vistas acima.

Exemplo 188. O método Vector2d.__format__, versão #1
    # inside the Vector2d class

    def __format__(self, fmt_spec=''):
        components = (format(c, fmt_spec) for c in self)  # (1)
        return '({}, {})'.format(*components)  # (2)
  1. Usa a função embutida format para aplicar o fmt_spec a cada componente do vetor, criando um iterável de strings formatadas.

  2. Insere as strings formatadas na fórmula '(x, y)'.

Agora vamos acrescentar um código de formatação personalizado à nossa mini-linguagem: se o especificador de formato terminar com 'p', vamos exibir o vetor em coordenadas polares: <r, θ>, onde r é a magnitute e θ (theta) é o ângulo em radianos. O restante do especificador de formato (o que quer que venha antes do 'p') será usado como antes.

👉 Dica

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 vemos que inteiros usam os códigos 'bcdoxXn', floats usam 'eEfFgGn%' e strings usam 's'. Então escolhi 'p' para coordenadas polares. Como cada classe interpreta esses códigos de forma independente, reutilizar uma letra em um formato personalizado para um novo tipo não é um erro, mas pode ser confuso para os usuários.

Para gerar coordenadas polares, já temos o método __abs__ para a magnitude. Vamos então escrever um método angle simples, usando a função math.atan2(), para obter o ângulo. Eis o código:

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

Exemplo 189. O método Vector2d.__format__, versão #2, agora com coordenadas polares
    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):  # (1)
            fmt_spec = fmt_spec[:-1]  # (2)
            coords = (abs(self), self.angle())  # (3)
            outer_fmt = '<{}, {}>'  # (4)
        else:
            coords = self  # (5)
            outer_fmt = '({}, {})'  # (6)
        components = (format(c, fmt_spec) for c in coords)  # (7)
        return outer_fmt.format(*components)  # (8)
  1. O formato termina com 'p': usa coordenadas polares.

  2. Remove o sufixo 'p' de fmt_spec.

  3. Cria uma tuple de coordenadas polares: (magnitude, angle).

  4. Configura o formato externo com chaves de ângulo.

  5. Caso contrário, usa os componentes x, y de self para coordenadas retângulares.

  6. Configura o formato externo com parênteses.

  7. Gera um iterável cujos componentes são strings formatadas.

  8. Insere as strings formatadas no formato externo.

Com o Exemplo 189, obtemos resultados como esses:

>>> format(Vector2d(1, 1), 'p')
'<1.4142135623730951, 0.7853981633974483>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'

Como mostrou essa seção, não é difícil estender a Mini-Linguagem de Especificação de Formato para suportar tipos definidos pelo usuário.

Vamos agora passar a um assunto que vai além das aparências: tornar nosso Vector2d hashable, para podermos criar conjuntos de vetores ou usá-los como chaves em um dict.

11.7. Um Vector2d hashable

Da forma como ele está definido até agora, as instâncias de nosso Vector2d não são hashable, então não podemos colocá-las em um set:

>>> v1 = Vector2d(3, 4)
>>> hash(v1)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'
>>> set([v1])
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'

Para tornar um Vector2d hashable, precisamos implementar __hash__ (__eq__ também é necessário, mas já temos esse método). Além disso, precisamos tornar imutáveis as instâncias do vetor, como vimos na Seção 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:

>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 7
Traceback (most recent call last):
  ...
AttributeError: can't set attribute

Faremos isso transformando os componentes x e y em propriedades apenas para leitura no Exemplo 190.

Exemplo 190. vector2d_v3.py: apenas as mudanças necessárias para tornar Vector2d imutável são exibidas aqui; a listagem completa está no Exemplo 194
class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)  # (1)
        self.__y = float(y)

    @property  # (2)
    def x(self):  # (3)
        return self.__x  # (4)

    @property  # (5)
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))  # (6)

    # remaining methods: same as previous Vector2d
  1. Usa exatamente dois sublinhados como prefixo (com zero ou um sublinhado como sufixo), para tornar um atributo privado.[128]

  2. O decorador @property marca o método getter de uma propriedade.

  3. O método getter é nomeado de acordo com o nome da propriedade pública que ele expõe: x.

  4. Apenas devolve self.__x.

  5. Repete a mesma fórmula para a propriedade y.

  6. Todos os métodos que apenas leem os componentes x e y podem permanecer como estavam, lendo as propriedades públicas através de self.x e self.y em vez de usar os atributos privados. Então essa listagem omite o restante do código da classe.

✒️ Nota

Vector.x e Vector.y são exemplos de propriedades apenas para leitura. Propriedades para leitura/escrita serão tratadas no Capítulo 22, onde mergulhamos mais fundo no decorador @property.

Agora que nossos vetores estão razoavelmente protegidos contra mutação acidental, podemos implementar o método __hash__. Ele deve devolver um int e, idealmente, levar em consideração os hashs dos atributos do objeto usados também no método __eq__, pois objetos que são considerados iguais ao serem comparados devem ter o mesmo hash. A documentação do método especial __hash__ sugere computar o hash de uma tupla com os componentes, e é isso que fazemos no Exemplo 191.

Exemplo 191. vector2d_v3.py: implementação de hash
    # inside class Vector2d:

    def __hash__(self):
        return hash((self.x, self.y))

Com o acréscimo do método __hash__, temos agora vetores hashable:

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

Não é estritamente necessário implementar propriedades ou proteger de alguma forma os atributos de instância para criar um tipo hashable. Só é necessário implementar corretamente __hash__ e __eq__. Mas, supostamente, o valor de um objeto hashable nunca deveria mudar, então isso me dá uma boa desculpa para falar sobre propriedades apenas para leitura.

Se você estiver criando um tipo com um valor numérico escalar que faz sentido, você pode também implementar os métodos __int__ e __float__, invocados pelos construtores int() e float(), que são usados, em alguns contextos, para coerção de tipo. Há também o método __complex__, para suportar o construtor embutido complex(). Talvez Vector2d pudesse oferecer o __complex__, mas deixo isso como um exercício para vocês.

11.8. Suportando o pattern matching posicional

Até aqui, instâncias de Vector2d são compatíveis com o pattern matching com instâncias de classe—vistos na Seção 5.8.2.

No Exemplo 192, todos aqueles padrões nomeados funcionam como esperado.

Exemplo 192. Padrões nomeados para sujeitos Vector2d—requer Python 3.10
def keyword_pattern_demo(v: Vector2d) -> None:
    match v:
        case Vector2d(x=0, y=0):
            print(f'{v!r} is null')
        case Vector2d(x=0):
            print(f'{v!r} is vertical')
        case Vector2d(y=0):
            print(f'{v!r} is horizontal')
        case Vector2d(x=x, y=y) if x==y:
            print(f'{v!r} is diagonal')
        case _:
            print(f'{v!r} is awesome')

Entretanto, se tentamos usar um padrão posicional, como esse:

        case Vector2d(_, 0):
            print(f'{v!r} is horizontal')

o resultado é esse:

TypeError: Vector2d() accepts 0 positional sub-patterns (1 given)

Para fazer Vector2d funcionar 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.

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

Exemplo 193. Padrões posicionais para sujeitos Vector2d—requer Python 3.10
def positional_pattern_demo(v: Vector2d) -> None:
    match v:
        case Vector2d(0, 0):
            print(f'{v!r} is null')
        case Vector2d(0):
            print(f'{v!r} is vertical')
        case Vector2d(_, 0):
            print(f'{v!r} is horizontal')
        case Vector2d(x, y) if x==y:
            print(f'{v!r} is diagonal')
        case _:
            print(f'{v!r} is awesome')

O atributo de classe __match_args__ não precisa incluir todos os atributos públicos de instância. Em especial, se o __init__ da classe tem argumentos obrigatórios e opcionais, que são depois vinculados a atributos de instância, pode ser razoável nomear apenas os argumentos obrigatórios em __match_args__, omitindo os opcionais.

Vamos dar um passo atrás e revisar tudo o que programamos até aqui no Vector2d.

11.9. Listagem completa Vector2d, versão 3

Já estamos trabalhando no Vector2d há algum tempo, mostrando apenas trechos isolados. O Exemplo 194 é uma listagem completa e consolidada de vector2d_v3.py, incluindo os doctests que usei durante o desenvolvimento.

Exemplo 194. vector2d_v3.py: o pacote completo
"""
A two-dimensional vector class

    >>> v1 = Vector2d(3, 4)
    >>> print(v1.x, v1.y)
    3.0 4.0
    >>> x, y = v1
    >>> x, y
    (3.0, 4.0)
    >>> v1
    Vector2d(3.0, 4.0)
    >>> v1_clone = eval(repr(v1))
    >>> v1 == v1_clone
    True
    >>> print(v1)
    (3.0, 4.0)
    >>> octets = bytes(v1)
    >>> octets
    b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
    >>> abs(v1)
    5.0
    >>> bool(v1), bool(Vector2d(0, 0))
    (True, False)


Test of ``.frombytes()`` class method:

    >>> v1_clone = Vector2d.frombytes(bytes(v1))
    >>> v1_clone
    Vector2d(3.0, 4.0)
    >>> v1 == v1_clone
    True


Tests of ``format()`` with Cartesian coordinates:

    >>> format(v1)
    '(3.0, 4.0)'
    >>> format(v1, '.2f')
    '(3.00, 4.00)'
    >>> format(v1, '.3e')
    '(3.000e+00, 4.000e+00)'


Tests of the ``angle`` method::

    >>> Vector2d(0, 0).angle()
    0.0
    >>> Vector2d(1, 0).angle()
    0.0
    >>> epsilon = 10**-8
    >>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
    True
    >>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
    True


Tests of ``format()`` with polar coordinates:

    >>> format(Vector2d(1, 1), 'p')  # doctest:+ELLIPSIS
    '<1.414213..., 0.785398...>'
    >>> format(Vector2d(1, 1), '.3ep')
    '<1.414e+00, 7.854e-01>'
    >>> format(Vector2d(1, 1), '0.5fp')
    '<1.41421, 0.78540>'


Tests of `x` and `y` read-only properties:

    >>> v1.x, v1.y
    (3.0, 4.0)
    >>> v1.x = 123
    Traceback (most recent call last):
      ...
    AttributeError: can't set attribute 'x'


Tests of hashing:

    >>> v1 = Vector2d(3, 4)
    >>> v2 = Vector2d(3.1, 4.2)
    >>> len({v1, v2})
    2

"""

from array import array
import math

class Vector2d:
    __match_args__ = ('x', 'y')

    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __hash__(self):
        return hash((self.x, self.y))

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

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

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)

Recordando, nessa seção e nas anteriores vimos alguns dos métodos especiais essenciais que você pode querer implementar para obter um objeto completo.

✒️ Nota

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.

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 194, Vector2d é um exemplo didático com uma lista extensiva de métodos especiais relacionados à representação de objetos, não um modelo para qualquer classe definida pelo usuário.

Na próxima seção, deixamos o Vector2d de lado por um tempo para discutir o design e as desvantagens do mecanismo de atributos privados no Python—o prefixo de duplo sublinhado em self.__x.

11.10. Atributos privados e "protegidos" no Python

Em Python, não há como criar variáveis privadas como as criadas com o modificador private em Java. O que temos no Python é um mecanismo simples para prevenir que um atributo "privado" em uma subclasse seja acidentalmente sobrescrito.

Considere o seguinte cenário: alguém escreveu uma classe chamada Dog, que usa um atributo de instância mood internamente, sem expô-lo. Você precisa criar a uma subclasse Beagle de Dog. Se você criar seu próprio atributo de instância mood, sem saber da colisão de nomes, vai afetar o atributo mood usado pelos métodos herdados de Dog. Isso seria bem complicado de depurar.

Para prevenir esse tipo de problema, se você nomear o atributo de instância no formato __mood (dois sublinhados iniciais e zero ou no máximo um sublinhado no final), Python armazena o nome no __dict__ da instância, prefixado com um sublinhado seguido do nome da classe. Na classe Dog, por exemplo, __mood se torna _Dog__mood e em Beagle ele será _Beagle__mood.

Esse recurso da linguagem é conhecido pela encantadora alcunha de desfiguração de nome ("name mangling").

O Exemplo 195 mostra o resultado na classe Vector2d do Exemplo 190.

Exemplo 195. Nomes de atributos privados são "desfigurados", prefixando-os com o _ e o nome da classe
>>> 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 26 ilustra outro dispositivo 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 195—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.[129]

interruptores com coberturas de proteção
Figura 26. 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.[130] É 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 de Python.[131] 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.[132]

Vamos agora voltar à nossa classe Vector2d. Na próxima seção trataremos de um atributo (e não um método) especial que afeta o armazenamento interno de um objeto, com um imenso impacto potencial sobre seu uso de memória, mas pouco efeito sobre sua interface pública: __slots__.

11.11. Economizando memória com __slots__

Por default, Python armazena os atributos de cada instância em um dict chamado __dict__. Como vimos em Seção 3.9, um dict ocupa um espaço significativo de memória, mesmo com as otimizações mencionadas naquela seção. Mas se você definir um atributo de classe chamado __slots__, que mantém uma sequência de nomes de atributos, Python usará um modelo alternativo de armazenamento para os atributos de instância: os atributos nomeados em __slots__ serão armazenados em um array de referências oculto, que usa menos memória que um dict. Vamos ver como isso funciona através de alguns exemplos simples, começando pelo Exemplo 196.

Exemplo 196. A classe Pixel usa __slots__
>>> class Pixel:
...     __slots__ = ('x', 'y')  # (1)
...
>>> p = Pixel()  # (2)
>>> p.__dict__  # (3)
Traceback (most recent call last):
  ...
AttributeError: 'Pixel' object has no attribute '__dict__'
>>> p.x = 10  # (4)
>>> p.y = 20
>>> p.color = 'red'  # (5)
Traceback (most recent call last):
  ...
AttributeError: 'Pixel' object has no attribute 'color'
  1. __slots__ deve estar presente quando a classe é criada; acrescentá-lo ou modificá-lo posteriormente não tem qualquer efeito. Os nomes de atributos podem estar em uma tuple ou em uma list. Prefiro usar uma tuple, para deixar claro que não faz sentido modificá-la.

  2. Cria uma instância de Pixel, pois os efeitos de __slots__ são vistos nas instâncias.

  3. Primeiro efeito: instâncias de Pixel não têm um __dict__.

  4. Define normalmente os atributos p.x e p.y.

  5. Segundo efeito: tentar definir um atributo não listado em __slots__ gera um AttributeError.

Até aqui, tudo bem. Agora vamos criar uma subclasse de Pixel, no Exemplo 197, para ver o lado contraintuitivo de __slots__.

Exemplo 197. OpenPixel é uma subclasse de Pixel
>>> class OpenPixel(Pixel):  # (1)
...     pass
...
>>> op = OpenPixel()
>>> op.__dict__  # (2)
{}
>>> op.x = 8  # (3)
>>> op.__dict__  # (4)
{}
>>> op.x  # (5)
8
>>> op.color = 'green'  # (6)
>>> op.__dict__  # (7)
{'color': 'green'}
  1. OpenPixel não declara qualquer atributo próprio.

  2. Surpresa: instâncias de OpenPixel têm um __dict__.

  3. Se você definir o atributo x (nomeado no __slots__ da classe base Pixel)…​

  4. …​ele não será armazenado no __dict__ da instância…​

  5. …​mas sim no array oculto de referências na instância.

  6. Se você definir um atributo não nomeado no __slots__…​

  7. …​ele será armazenado no __dict__ da instância.

O Exemplo 197 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 198.

Exemplo 198. The ColorPixel, another subclass of Pixel
>>> 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 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 6.6). Esse atributo existe por default em instâncias de classes definidas pelo usuário. Entretanto, se a classe define __slots__, e é necessário que as instâncias possam ser alvo de referências fracas, então é preciso incluir __weakref__ entre os atributos nomeados em __slots__.

Vejamos agora o efeito da adição de __slots__ a Vector2d.

11.11.1. Uma medida simples da economia gerada por __slots__

Exemplo 199 mostra a implementação de __slots__ em Vector2d.

Exemplo 199. vector2d_v3_slots.py: o atributo __slots__ é a única adição a Vector2d
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 200, usei vector2d_v3.Vector2d (do Exemplo 190); na segunda execução usei a versão com __slots__ do Exemplo 199.

Exemplo 200. mem_test.py cria 10 milhões de instâncias de Vector2d, usando a classe definida no módulo nomeado
$ 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 200, 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.

👉 Dica

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 2.10.3), que são eficientes no de uso de memória, e também tem funções para processamento numérico extremamente otimizadas, muitas das quais operam sobre o array inteiro ao mesmo tempo. Projetei a classe Vector2d apenas como um contexto para a discussão de métodos especiais, pois sempre que possível tento evitar exemplos vagos com Foo e Bar.

11.11.2. Resumindo os problemas com __slots__

O atributo de classe __slots__ pode proporcionar uma economia significativa de memória se usado corretamente, mas existem algumas ressalvas:

  • É preciso lembrar de redeclarar __slots__ em cada subclasse, para evitar que suas instâncias tenham um __dict__.

  • Instâncias só poderão ter os atributos listados em __slots__, a menos que __dict__ seja incluído em __slots__ (mas isso pode anular a economia de memória).

  • Classe que usam __slots__ não podem usar o decorador @cached_property, a menos que nomeiem __dict__ explicitamente em __slots__.

  • Instâncias não podem ser alvo de referências fracas, a menos que __weakref__ seja incluído em __slots__.

O último tópico do capítulo trata da sobreposição de um atributo de classe em instâncias e subclasses.

11.12. Sobrepondo atributos de classe

Um recurso característico de Python é a forma como atributos de classe podem ser usados como valores default para atributos de instância. Vector2d contém o atributo de classe typecode. Ele é usado duas vezes no método __bytes__, mas é lido intencionalmente como self.typecode. As instâncias de Vector2d são criadas sem um atributo typecode próprio, então self.typecode vai, por default, se referir ao atributo de classe Vector2d.typecode.

Mas se incluirmos um atributo de instância que não existe, estamos criando um novo atributo de instância—por exemplo, um atributo de instância typecode—e o atributo de classe com o mesmo nome permanece intocado. Entretanto, daí em diante, sempre que algum código referente àquela instância contiver self.typecode, o typecode da instância será usado, na prática escondendo o atributo de classe de mesmo nome. Isso abre a possibilidade de personalizar uma instância individual com um typecode diferente.

O Vector2d.typecode default é 'd': isso significa que cada componente do vetor será representado como um número de ponto flutuante de dupla precisão e 8 bytes de tamanho quando for exportado para bytes. Se definirmos o typecode de uma instância Vector2d como 'f' antes da exportação, cada componente será exportado como um número de ponto flutuante de precisão simples e 4 bytes de tamanho.. O Exemplo 201 demonstra isso.

✒️ Nota

Estamos falando do acréscimo de um atributo de instância, assim o Exemplo 201 usa a implementação de Vector2d sem __slots__, como aparece no Exemplo 194.

Exemplo 201. Personalizando uma instância pela definição do atributo typecode, que antes era herdado da classe
>>> from vector2d_v3 import Vector2d
>>> v1 = Vector2d(1.1, 2.2)
>>> dumpd = bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'
>>> len(dumpd)  # (1)
17
>>> v1.typecode = 'f'  # (2)
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf)  # (3)
9
>>> Vector2d.typecode  # (4)
'd'
  1. A representação default em bytes tem 17 bytes de comprimento.

  2. Define typecode como 'f' na instância v1.

  3. Agora bytes tem 9 bytes de comprimento.

  4. Vector2d.typecode não foi modificado; apenas a instância v1 usa o typecode 'f'.

Isso deixa claro porque a exportação para bytes de um Vector2d tem um prefixo typecode: queríamos suportar diferentes formatos de exportação.

Para modificar um atributo de classe, é preciso redefini-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:

>>> 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 202 mostra como se faz.

Exemplo 202. O ShortVector2d é uma subclasse de Vector2d, que apenas sobrepõe o typecode default
>>> from vector2d_v3 import Vector2d
>>> class ShortVector2d(Vector2d):  # (1)
...     typecode = 'f'
...
>>> sv = ShortVector2d(1/11, 1/27)  # (2)
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035)  # (3)
>>> len(bytes(sv))  # (4)
9
  1. Cria ShortVector2d como uma subclasse de Vector2d apenas para sobrepor o atributo de classe typecode.

  2. Cria sv, uma instância de ShortVector2d, para demonstração.

  3. Verifica o repr de sv.

  4. Verifica que a quantidade de bytes exportados é 9, e não 17 como antes.

Esse exemplo também explica porque não escrevi explicitamente o class_name em Vector2d.​__repr__, optando por obtê-lo de type(self).__name__, assim:

    # 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 de Python: oferecendo diferentes representações do objeto, fornecendo um código de formatação personalizado, expondo atributos somente para leitura e suportando hash() para se integrar a conjuntos e mapeamentos.

11.13. Resumo do capítulo

O objetivo desse capítulo foi demonstrar o uso dos métodos especiais e as convenções na criação de uma classe pythônica bem comportada.

Será vector2d_v3.py (do Exemplo 194) mais pythônica que vector2d_v0.py (do Exemplo 185)? A classe Vector2d em vector2d_v3.py com certeza utiliza mais recursos de 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 de Python), de Tim Peter, diz:

Simples é melhor que complexo.

Um objeto deve ser tão simples quanto seus requerimentos exigem—e não um desfile de recursos da linguagem. Se o código for parte de uma aplicação, ele deveria se concentrar naquilo que for necessário para suportar os usuários finais, e nada mais. Se o código for parte de uma biblioteca para uso por outros programadores, então é razoável implementar métodos especiais que suportam comportamentos esperados por pythonistas. Por exemplo, __eq__ pode não ser necessário para suportar um requisito do negócio, mas torna a classe mais fácil de testar.

Minha meta, ao expandir o código do Vector2d, foi criar um contexto para a discussão dos métodos especiais e das convenções de programação em Python. Os exemplos neste capítulo demonstraram vários dos métodos especiais vistos antes na Tabela 1 (do Capítulo 1):

  • Métodos de representação de strings e bytes: __repr__, __str__, __format__ e __bytes__

  • Métodos para reduzir um objeto a um número: __abs__, __bool__ e __hash__

  • O operador __eq__, para suportar testes e hashing (juntamente com __hash__)

Quando suportamos a conversão para bytes, também implementamos um construtor alternativo, Vector2d.frombytes(), que nos deu um contexto para falar dos decoradores @classmethod (muito conveniente) e @staticmethod (não tão útil: funções a nível do módulo são mais simples). O método frombytes foi inspirado pelo método de mesmo nome na classe array.array.

Vimos que a Mini-Linguagem de Especificação de Formato é extensível, ao implementarmos um método __format__ que analisa uma format_spec fornecida à função embutida format(obj, format_spec) ou dentro de campos de substituição '{:«format_spec»}' em f-strings ou ainda strings usadas com o método str.format().

Para preparar a transformação de instâncias de Vector2d em hashable, fizemos um esforço para torná-las imutáveis, ao menos prevenindo modificações acidentais, programando os atributos x e y como privados, e expondo-os como propriedades apenas para leitura. Nós então implementamos __hash__ usando a técnica recomendada, aplicar o operador xor aos hashes dos atributos da instância.

Discutimos a seguir a economia de memória e as ressalvas de se declarar um atributo __slots__ em Vector2d. Como o uso de __slots__ tem efeitos colaterais, ele só faz real sentido quando é preciso processar um número muito grande de instâncias—pense em milhões de instâncias, não apenas milhares. Em muitos destes casos, usar a pandas pode ser a melhor opção.

O último tópico tratado foi a sobreposição de um atributo de classe acessado através das instâncias (por exemplo, self.typecode). Fizemos isso primeiro criando um atributo de instância, depois criando uma subclasse e sobrescrevendo o atributo no nível da classe.

Por todo o capítulo, apontei como escolhas de design nos exemplos foram baseadas no estudo das APIs dos objetos padrão de 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 de Python.

— Antigo provérbio chinês

11.14. Leitura complementar

Este capítulo tratou de vários dos métodos especiais do modelo de dados, então naturalmente as referências primárias são as mesmas do Capítulo 1, onde tivemos uma ideia geral do mesmo tópico. Por conveniência, vou repetir aquelas quatro recomendações anteriores aqui, e acrescentar algumas outras:

O capítulo "Modelo de Dados" em A Referência da Linguagem Python

A maioria dos métodos usados nesse capítulo estão documentados em "3.3.1. Personalização básica".

Python in a Nutshell, 3ª ed., de Alex Martelli, Anna Ravenscroft, e Steve Holden

Trata com profundidade dos métodos especiais .

Python Cookbook, 3ª ed., de David Beazley e Brian K. Jones

Práticas modernas de Python demonstradas através de receitas. Especialmente o Capítulo 8, "Classes and Objects" (Classes e Objetos), que contém várias receitas relacionadas às discussões deste capítulo.

Python Essential Reference, 4ª ed., de David Beazley

Trata do modelo de dados em detalhes, apesar de falar apenas de Python 2.6 e do 3.0 (na quarta edição). Todos os conceitos fundamentais são os mesmos, e a maior parte das APIs do Modelo de Dados não mudou nada desde Python 2.2, quando os tipos embutidos e as classes definidas pelo usuário foram unificados.

Em 2015—o ano que terminei a primeira edição de Python Fluente—Hynek Schlawack começou a desenvolver o pacote attrs. Da documentação de attrs:

attrs é um pacote Python que vai 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)

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 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) (EN).

Uma percepção precoce da necessidade de strings de representação diferentes para objetos apareceu em 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) (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 11.2.

Ponto de Vista

Propriedades ajudam a reduzir custos iniciais

Nas primeiras versões de Vector2d, os atributos x e y eram públicos, como são, por default, todos os atributos de instância e classe no Python. Naturalmente, os usuários de vetores precisam acessar seus componentes. Apesar de nossos vetores serem iteráveis e poderem ser desempacotados em um par de variáveis, também é desejável poder escrever my_vector.x e my_vector.y para obter cada componente.

Quando sentimos a necessidade de evitar modificações acidentais dos atributos x e y, implementamos propriedades, mas nada mudou no restante do código ou na interface pública de Vector2d, como se verifica através dos doctests. Continuamos podendo acessar my_vector.x and my_vector.y.

Isso mostra que podemos sempre iniciar o desenvolvimento de nossas classes da maneira mais simples possível, com atributos públicos, pois quando (ou se) nós mais tarde precisarmos impor mais controle, com getters e setters, estes métodos podem ser implementados usando propriedades, sem mudar nada no código que já interage com nossos objetos através dos nomes que eram, inicialmente, simples atributos públicos (x e y, por exemplo).

Essa abordagem é o oposto daquilo que é encorajado pela linguagem Java: um programador Java não pode começar com atributos públicos simples e apenas mais tarde, se necessário, implementar propriedades, porque elas não existem naquela linguagem. Portanto, escrever getters e setters é a regra em Java—mesmo quando esses métodos não fazem nada de útil—porque a API não pode evoluir de atributos públicos simples para getters e setters sem quebrar todo o código que já use aqueles atributos.

Além disso, como Martelli, Ravenscroft e Holden observam no Python in a Nutshell, 3rd ed., digitar chamadas a getters e setters por toda parte é patético. Você é obrigado a escrever coisas como:

>>> my_object.set_foo(my_object.get_foo() + 1)

Apenas para fazer isso:

>>> my_object.foo += 1

Ward Cunningham, inventor do wiki e um pioneiro da Programação Extrema (Extreme Programming), recomenda perguntar: "Qual a coisa mais simples que tem alguma chance de funcionar?" A ideia é se concentrar no objetivo.[133] Implementar setters e getters desde o início é uma distração em relação ao objetivo Em Python, podemos simplesmente usar atributos públicos, sabendo que podemos transformá-los mais tarde em propriedades, se essa necessidade surgir .

Proteção versus segurança em atributos privados

O Perl não tem nenhum amor por privacidade forçada. Ele preferiria que você não entrasse em sua sala de estar [apenas] por não ter sido convidado, e não porque ele tem uma espingarda.

— Larry Wall
criador do Perl

Python e o Perl estão em polos 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 de 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 (EN) de 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 203).

Exemplo 203. Confidential.java: uma classe Java com um campo privado chamado secret
public class Confidential {

    private String secret = "";

    public Confidential(String text) {
        this.secret = text.toUpperCase();
    }
}

No Exemplo 203, 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 de Java) para obter o valor de um campo privado. O código aparece no Exemplo 204.

Exemplo 204. expose.py: código em Jython para ler o conteúdo de um campo privado em outra classe
#!/usr/bin/env jython
# NOTE: Jython is still Python 2.7 in late2020

import Confidential

message = Confidential('top secret text')
secret_field = Confidential.getDeclaredField('secret')
secret_field.setAccessible(True)  # break the lock!
print 'message.secret =', secret_field.get(message)

Executando o Exemplo 204, o resultado é esse:

$ 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 de 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 no repositório de código do Python Fluente).

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 (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 de Python. Seus elementos serão números de ponto flutuante, e ao final do capítulo a classe suportará o seguinte:

  • O protocolo de sequência básico: __len__ e __getitem__

  • Representação segura de instâncias com muitos itens

  • Suporte adequado a fatiamento, produzindo novas instâncias de Vector

  • Hashing agregado, levando em consideração o valor de cada elemento contido na sequência

  • Um extensão personalizada da linguagem de formatação

Também vamos implementar, com __getattr__, o acesso dinâmico a atributos, como forma de substituir as propriedades apenas para leitura que usamos no Vector2d—apesar disso não ser típico de tipos sequência.

Nossa apresentação voltada para o código será interrompida por uma discussão conceitual sobre a ideia de protocolos como uma interface informal. Vamos discutir a relação entre protocolos e duck typing, e as implicações práticas disso na criação de seus próprios tipos

12.1. Novidades nesse capítulo

Não ocorreu qualquer grande modificação neste capítulo. Há uma breve discussão nova sobre o typing.Protocol em um quadro de dicas, no final da Seção 12.4.

Na Seção 12.5.2, a implementação do __getitem__ no Exemplo 210 está mais concisa e robusta que o exemplo na primeira edição, graças ao duck typing e ao operator.index. Essa mudança foi replicada para as implementações seguintes de Vector aqui e no Capítulo 16.

Vamos começar.

12.2. Vector: Um tipo sequência definido pelo usuário

Nossa estratégia na implementação de Vector será usar composição, não herança. Vamos armazenar os componentes em um array de números de ponto flutuante, e implementar os métodos necessários para que nossa classe Vector se comporte como uma sequência plana imutável.

Mas antes de implementar os métodos de sequência, vamos desenvolver uma implementação básica de Vector compatível com nossa classe Vector2d, vista anteriormente—​exceto onde tal compatibilidade não fizer sentido.

Aplicações de vetores além de três dimensões

Quem precisa de vetores com 1.000 dimensões? Vetores N-dimensionais (com valores grandes de N) são bastante utilizados em recuperação de informação, onde documentos e consultas textuais são representados como vetores, com uma dimensão para cada palavra. Isso se chama Modelo de Espaço Vetorial (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 de 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 do PyPi, de Radim Řehůřek, implementa a modelagem de espaço vetorial para processamento de linguagem natural e recuperação de informação, usando a NumPy e a SciPy.

12.3. Vector versão #1: compatível com Vector2d

A primeira versão de Vector deve ser tão compatível quanto possível com nossa classe Vector2d desenvolvida anteriormente.

Entretanto, pela própria natureza das classes, o construtor de Vector não é compatível com o construtor de Vector2d. Poderíamos fazer Vector(3, 4) e Vector(3, 4, 5) funcionarem, recebendo argumentos arbitrários com *args em __init__. Mas a melhor prática para um construtor de sequências é receber os dados através de um argumento iterável, como fazem todos os tipos embutidos de sequências. O Exemplo 205 mostra algumas maneiras de instanciar objetos do nosso novo Vector.

Exemplo 205. Testes de Vector.__init__ e Vector.__repr__
>>> 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]).

⚠️ Aviso

Quando um Vector tem mais de seis componentes, a string produzida por repr() é abreviada com …​, como visto na última linha do Exemplo 205. Isso é fundamental para qualquer tipo de coleção 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 206. O módulo reprlib se chamava repr no Python 2.7.

O Exemplo 206 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 206. vector_v1.py: derived from vector2d_v1.py
from array import array
import reprlib
import math


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # (1)

    def __iter__(self):
        return iter(self._components)  # (2)

    def __repr__(self):
        components = reprlib.repr(self._components)  # (3)
        components = components[components.find('['):-1]  # (4)
        return f'Vector({components})'

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(self._components))  # (5)

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __abs__(self):
        return math.hypot(*self)  # (6)

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

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # (7)
  1. O atributo de instância "protegido" self._components vai manter um array com os componentes do Vector.

  2. Para permitir iteração, devolvemos um itereador sobre self._components.[134]

  3. Usa reprlib.repr() para obter um representação de tamanho limitado de self._components (por exemplo, array('d', [0.0, 1.0, 2.0, 3.0, 4.0, …​])).

  4. Remove o prefixo array('d', e o ) final, antes de inserir a string em uma chamada ao construtor de Vector.

  5. Cria um objeto bytes diretamente de self._components.

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

👉 Dica

Por seu papel na depuração, chamar repr() em um objeto não deveria nunca gerar uma exceção. 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 objeto receptor (self).

Observe que os métodos __str__, __eq__, e __bool__ são idênticos a suas versões em Vector2d, e apenas um caractere mudou em frombytes (um * foi removido na última linha). Isso é um dos benefícios de termos tornado o Vector2d original iterável.

Aliás, poderíamos ter criado Vector como uma subclasse de Vector2d, mas escolhi não fazer isso por duas razões. Em primeiro lugar, os construtores incompatíveis de fato tornam a relação de super/subclasse desaconselhável. Eu até poderia contornar isso como um tratamento engenhoso dos parâmetros em __init__, mas a segunda razão é mais importante: queria que Vector fosse um exemplo independente de uma classe que implementa o protocolo de sequência. É o que faremos a seguir, após uma discussão sobre o termo protocolo.

12.4. Protocolos e o duck typing

Já no Capítulo 1, vimos que não é necessário herdar de qualquer classe em especial para criar um tipo sequência completamente funcional em Python; basta implementar os métodos que satisfazem o protocolo de sequência. Mas de que tipo de protocolo estamos falando?

No contexto da programação orientada a objetos, um protocolo é uma interface informal, definida apenas na documentação (e não no código). Por exemplo, o protocolo de sequência no Python implica apenas no métodos __len__ e __getitem__. Qualquer classe Spam, que implemente esses métodos com a assinatura e a semântica padrões, pode ser usada em qualquer lugar onde uma sequência for esperada. É irrelevante se Spam é uma subclasse dessa ou daquela outra classe; tudo o que importa é que ela fornece os métodos necessários. Vimos isso no Exemplo 1, reproduzido aqui no Exemplo 207.

Exemplo 207. Código do Exemplo 1, reproduzido aqui por conveniência
import collections

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

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

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

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

    def __getitem__(self, position):
        return self._cards[position]

A classe FrenchDeck, no Exemplo 207, pode tirar proveito de muitas facilidades de 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

Com a PEP 544—Protocols: Structural subtyping (static duck typing) (Protocolos:sub-tipagem estrutural (duck typing estático)) (EN), o Python 3.8 suporta classes protocolo: subclasses de typing.Protocol, que estudamos na Seção 8.5.10. Esse novo uso da palavra 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 13.3 no Capítulo 13 traz maiores detalhes.

Vamos agora implementar o protocolo sequência em Vector, primeiro sem suporte adequado ao fatiamento, que acrescentaremos mais tarde.

12.5. Vector versão #2: Uma sequência fatiável

Como vimos no exemplo da classe FrenchDeck, suportar o protocolo de sequência é muito fácil se você puder delegar para um atributo sequência em seu objeto, como nosso array self._components. Esses __len__ e __getitem__ de uma linha são um bom começo:

class Vector:
    # many lines omitted
    # ...

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

    def __getitem__(self, index):
        return self._components[index]

Após tais acréscimos, agora todas as seguintes operações funcionam:

>>> 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 Python transforma a sintaxe my_seq[1:3] em argumentos para my_seq.__getitem__(...).

12.5.1. Como funciona o fatiamento

Uma demonstração vale mais que mil palavras, então dê uma olhada no Exemplo 208.

Exemplo 208. Examinando o comportamento de __getitem__ e fatias
>>> class MySeq:
...     def __getitem__(self, index):
...         return index  # (1)
...
>>> s = MySeq()
>>> s[1]  # (2)
1
>>> s[1:4]  # (3)
slice(1, 4, None)
>>> s[1:4:2]  # (4)
slice(1, 4, 2)
>>> s[1:4:2, 9]  # (5)
(slice(1, 4, 2), 9)
>>> s[1:4:2, 7:9]  # (6)
(slice(1, 4, 2), slice(7, 9, None))
  1. Para essa demonstração, o método __getitem__ simplesmente devolve o que for passado a ele.

  2. Um único índice, nada de novo.

  3. A notação 1:4 se torna slice(1, 4, None).

  4. slice(1, 4, 2) significa comece em 1, pare em 4, ande de 2 em 2.

  5. Surpresa: a presença de vírgulas dentro do [] significa que __getitem__ recebe uma tupla.

  6. A tupla pode inclusive conter vários objetos slice.

Vamos agora olhar mais de perto a própria classe slice, no Exemplo 209.

Exemplo 209. Inspecionando os atributos da classe slice
>>> slice  # (1)
<class 'slice'>
>>> dir(slice) # (2)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
 '__format__', '__ge__', '__getattribute__', '__gt__',
 '__hash__', '__init__', '__le__', '__lt__', '__ne__',
 '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
 'indices', 'start', 'step', 'stop']
  1. slice é um tipo embutido (que já vimos antes na Seção 2.7.2).

  2. Inspecionando uma slice descobrimos os atributos de dados start, stop, e step, e um método indices.

No Exemplo 209, a chamada dir(slice) revela um atributo indices, um método pouco conhecido mas muito interessante. Eis o que diz help(slice.indices):

S.indices(len) → (start, stop, stride)

Supondo uma sequência de tamanho len, calcula os índices start (início) e stop (fim), e a extensão do stride (passo) da fatia estendida descrita por S. Índices fora dos limites são recortados, exatamente como acontece em uma fatia normal.

Em outras palavras, indices expõe a lógica complexa implementada nas sequências embutidas, para lidar graciosamente com índices inexistentes ou negativos e com fatias maiores que a sequência original. Esse método produz tuplas "normalizadas" com os inteiros não-negativos start, stop, e stride ajustados para uma sequência de um dado tamanho.

Aqui estão dois exemplos, considerando uma sequência de len == 5, por exemplo 'ABCDE':

>>> slice(None, 10, 2).indices(5)  # (1)
(0, 5, 2)
>>> slice(-3, None, None).indices(5)  # (2)
(2, 5, 1)
  1. 'ABCDE'[:10:2] é o mesmo que 'ABCDE'[0:5:2].

  2. 'ABCDE'[-3:] é o mesmo que 'ABCDE'[2:5:1].

No código de nosso Vector não vamos precisar do método slice.indices(), pois quando recebermos uma fatia como argumento vamos delegar seu tratamento para o array interno _components. Mas quando você não puder contar com os serviços de uma sequência subjacente, esse método ajuda evita a necessidade de implementar uma lógica sutil.

Agora que sabemos como tratar fatias, vamos ver a implementação aperfeiçoada de Vector.__getitem__.

12.5.2. Um __getitem__ que trata fatias

O Exemplo 210 lista os dois métodos necessários para fazer Vector se comportar como uma sequência: __len__ e __getitem__ (com o último implementado para tratar corretamente o fatiamento).

Exemplo 210. Parte de vector_v2.py: métodos __len__ e __getitem__ adicionados à classe Vector, de vector_v1.py (no Exemplo 206)
    def __len__(self):
        return len(self._components)

    def __getitem__(self, key):
        if isinstance(key, slice):  # (1)
            cls = type(self)  # (2)
            return cls(self._components[key])  # (3)
        index = operator.index(key)  # (4)
        return self._components[index]  # (5)
  1. Se o argumento key é uma slice…​

  2. …​obtém a classe da instância (isto é, Vector) e…​

  3. …​invoca a classe para criar outra instância de Vector a partir de uma fatia do array _components.

  4. Se podemos obter um index de key…​

  5. …​devolve o item específico de _components.

A função operator.index() chama o método especial __index__. A função e o método especial foram definidos na PEP 357—Allowing Any Object to be Used for Slicing (Permitir que Qualquer Objeto seja Usado para Fatiamento) (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.

✒️ Nota

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 isinstance com key, para verificar se esse argumento era um inteiro. O uso de operator.index evita esse teste, e gera um Type​Error com uma mensagem muito informativa, se não for possível obter o index a partir de key. Observe a última mensagem de erro no Exemplo 211, abaixo.

Após a adição do código do Exemplo 210 à classe Vector class, temos o comportamento apropriado para fatiamento, como demonstra o Exemplo 211 .

Exemplo 211. Testes do Vector.__getitem__ aperfeiçoado, do Exemplo 210
    >>> v7 = Vector(range(7))
    >>> v7[-1]  # (1)
    6.0
    >>> v7[1:4]  # (2)
    Vector([1.0, 2.0, 3.0])
    >>> v7[-1:]  # (3)
    Vector([6.0])
    >>> v7[1,2]  # (4)
    Traceback (most recent call last):
      ...
    TypeError: 'tuple' object cannot be interpreted as an integer
  1. Um índice inteiro recupera apenas o valor de um componente, um float.

  2. Uma fatia como índice cria um novo Vector.

  3. Um fatia de len == 1 também cria um Vector.

  4. Vector não suporta indexação multidimensional, então tuplas de índices ou de fatias geram um erro.

12.6. Vector versão #3: acesso dinâmico a atributos

Ao evoluir Vector2d para Vector, perdemos a habilidade de acessar os componentes do vetor por nome (por exemplo, v.x, v.y). Agora estamos trabalhando com vetores que podem ter um número grande de componentes. Ainda assim, pode ser conveniente acessar os primeiros componentes usando letras como atalhos, algo como x, y, z em vez de v[0], v[1], and v[2].

Aqui está a sintaxe alternativa que queremos oferecer para a leitura dos quatro primeiros componentes de um vetor:

>>> 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 190). 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, 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.[135] 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 212 lista nosso método __getattr__. Ele basicamente verifica se o atributo desejado é uma das letras xyzt. Em caso positivo, devolve o componente correspondente do vetor.

Exemplo 212. Parte de vector_v3.py: método __getattr__ acrescentado à classe Vector
    __match_args__ = ('x', 'y', 'z', 't')  # (1)

    def __getattr__(self, name):
        cls = type(self)  # (2)
        try:
            pos = cls.__match_args__.index(name)  # (3)
        except ValueError:  # (4)
            pos = -1
        if 0 <= pos < len(self._components):  # (5)
            return self._components[pos]
        msg = f'{cls.__name__!r} object has no attribute {name!r}'  # (6)
        raise AttributeError(msg)
  1. Define __match_args__ para permitir pattern matching posicional sobre os atributos dinâmicos suportados por __getattr__.[136]

  2. Obtém a classe de Vector, para uso posterior.

  3. Tenta obter a posição de name em __match_args__.

  4. .index(name) gera um ValueError quando name não é encontrado; define pos como -1. (Eu preferiria usar algo como str.find aqui, mas tuple não implementa esse método.)

  5. Se pos está dentro da faixa de componentes disponíveis, devolve aquele componente.

  6. Se chegamos até aqui, gera um AttributeError com uma mensagem de erro padrão.

Não é difícil implementar __getattr__, mas neste caso não é o suficiente. Observe a interação bizarra no Exemplo 213.

Exemplo 213. Comportamento inapropriado: realizar uma atribuição a v.x não gera um erro, mas introduz uma inconsistência
>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x  # (1)
0.0
>>> v.x = 10  # (2)
>>> v.x  # (3)
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])  # (4)
  1. Acessa o elemento v[0] como v.x.

  2. Atribui um novo valor a v.x. Isso deveria gera uma exceção.

  3. Ler v.x obtém o novo valor, 10.

  4. Entretanto, os componentes do vetor não mudam.

Você consegue explicar o que está acontecendo? Em especial, por que v.x devolve 10 na segunda consulta (<3>), se aquele valor não está presente no array de componentes do vetor? Se você não souber responder de imediato, estude a explicação de __getattr__ que aparece logo antes do Exemplo 212. 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 213 ocorre devido à forma como __getattr__ funciona: 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 214.

Exemplo 214. Parte de vector_v3.py: o método __setattr__ na classe Vector
    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:  # (1)
            if name in cls.__match_args__:  # (2)
                error = 'readonly attribute {attr_name!r}'
            elif name.islower():  # (3)
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''  # (4)
            if error:  # (5)
                msg = error.format(cls_name=cls.__name__, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)  # (6)
  1. Tratamento especial para nomes de atributos com uma única letra.

  2. Se name está em __match_args__, configura mensagens de erro específicas.

  3. Se name é uma letra minúscula, configura a mensagem de erro sobre todos os nomes de uma única letra.

  4. Caso contrário, configura uma mensagem de erro vazia.

  5. Se existir uma mensagem de erro não-vazia, gera um AttributeError.

  6. Caso default: chama __setattr__ na superclasse para obter o comportamento padrão.

👉 Dica

A função super() fornece uma maneira de acessar dinamicamente métodos de superclasses, uma necessidade em uma linguagem dinâmica que suporta herança múltipla, como 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 214. Falaremos mais sobre super na 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.

⚠️ Aviso

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. Entretanto, por todas as ressalvas discutidas na Seção 11.11.2, usar __slots__ apenas para prevenir a criação de atributos de instância não é recomendado. __slots__ deve ser usado apenas para economizar memória, e apenas quando isso for um problema real.

Mesmo não suportando escrita nos componentes de Vector, aqui está uma lição importante deste exemplo: muitas vezes, quando você implementa __getattr__, é necessário também escrever o __setattr__, para evitar comportamentos inconsistentes em seus objetos.

Para permitir a modificação de componentes, poderíamos implementar __setitem__, para permitir v[0] = 1.1, e/ou __setattr__, para fazer v.x = 1.1 funcionar. Mas Vector permanecerá imutável, pois queremos torná-lo hashable, na próxima seção.

12.7. Vector versão #4: o hash e um == mais rápido

Vamos novamente implementar um método __hash__. Juntamente com o __eq__ existente, isso tornará as instâncias de Vector hashable.

O __hash__ do Vector2d (no Exemplo 191) 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,[137] mas computar o hash de todos os componentes do vetor é um bom caso de uso para ela. A Figura 27 ilustra a ideia geral da função reduce.

Diagrama de reduce
Figura 27. 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 lstfn(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):

>>> 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 215 demonstra a ideia da computação de um xor agregado, fazendo isso de três formas diferente: com um loop for e com dois modos diferentes de usar reduce.

Exemplo 215. Três maneiras de calcular o xor acumulado de inteiros de 0 a 5
>>> n = 0
>>> for i in range(1, 6):  # (1)
...     n ^= i
...
>>> n
1
>>> import functools
>>> functools.reduce(lambda a, b: a^b, range(6))  # (2)
1
>>> import operator
>>> functools.reduce(operator.xor, range(6))  # (3)
1
  1. xor agregado com um loop for e uma variável de acumulação.

  2. functools.reduce usando uma função anônima.

  3. functools.reduce substituindo a lambda personalizada por operator.xor.

Das alternativas apresentadas no Exemplo 215, a última é minha favorita, e o loop for vem a seguir. Qual sua preferida?

Como visto na Seção 7.8.1, operator oferece a funcionalidade de todos os operadores infixos de 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 216 apresenta as modificações relevantes.

Exemplo 216. Parte de vector_v4.py: duas importações e o método __hash__ adicionados à classe Vector de vector_v3.py
from array import array
import reprlib
import math
import functools  # (1)
import operator  # (2)


class Vector:
    typecode = 'd'

    # many lines omitted in book listing...

    def __eq__(self, other):  # (3)
        return tuple(self) == tuple(other)

    def __hash__(self):
        hashes = (hash(x) for x in self._components)  # (4)
        return functools.reduce(operator.xor, hashes, 0)  # (5)

    # more lines omitted...
  1. Importa functools para usar reduce.

  2. Importa operator para usar xor.

  3. Não há mudanças em __eq__; listei-o aqui porque é uma boa prática manter __eq__ e __hash__ próximos no código-fonte, pois eles precisam trabalhar juntos.

  4. Cria uma expressão geradora para computar sob demanda o hash de cada componente.

  5. Alimenta reduce com hashes e a função xor, para computar o código hash agregado; o terceiro argumento, 0, é o inicializador (veja o próximo aviso).

⚠️ 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 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 216 é um exemplo perfeito de uma computação de map-reduce (mapeia e reduz). Veja a (Figura 28).