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 do Python 0.9.6 <piscadela marota>.[1]

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

"Python é uma linguagem fácil de aprender e poderosa." Essas são as primeiras palavras do tutorial oficial do 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 do 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 o 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 o 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 do Python de A a Z. A ênfase está em recursos da linguagem característicos do 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 do 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 do 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 do 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 precendentes 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 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 do 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 tratatos 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 do 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 ("Correspondência 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 outras 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 do 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 do 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 do 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 o 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 oi 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 do 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 registri 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 o 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 o 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 o 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

Se um link aparece entre colchetes [assim], ele não funciona porque é uma referência para uma seção não identificada. Precisamos corrigir.

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 do Python

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

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

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

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

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

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

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

  • Coleções

  • Acesso a atributos

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

  • Sobrecarga (overloading) de operadores

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

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

  • Programação assíncrona usando await

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

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

✒️ 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 que "Qualquer uso de nomes no formato __*__ que não siga explicitamente o uso documentado, em qualquer contexto, está sujeito a quebra sem aviso prévio."

1.1. Novidades nesse capítulo

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

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

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

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

👉 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 do 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. O Python já tem uma função que devolve um item aleatório de uma sequência: random.choice. Podemos usá-la em uma instância de FrenchDeck:

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

Acabamos de ver duas vantagens de usar os métodos especiais no contexto do Modelo de Dados do Python.

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

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

Mas fica melhor.

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

>>> 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 do Python usadas neste livro com o doctest, para garantir a precisão. Quando a saída era grande demais, a parte omitida está marcada por reticências (…​), como na última linha do trecho de código anterior.

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

A iteração muitas vezes é implícita. Se uma coleção não fornecer um método __contains__, o operador in realiza uma busca sequencial. No nosso caso, in funciona com nossa classe FrenchDeck porque ela é iterável. Veja a seguir:

>>> 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 uso do modelo de dados e de composição. Ao implementar os métodos especiais __len__ e __getitem__, nosso FrenchDeck se comporta como uma sequência Python padrão, podendo assim se beneficiar de recursos centrais da linguagem (por exemplo, iteração e fatiamento), e da biblioteca padrão, como mostramos nos exemplos usando random.choice, reversed, e sorted. Graças à composição, as implementações de __len__ e __getitem__ podem delegar todo o trabalho para um objeto list, especificamente self._cards.

✒️ 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. Em Capítulo 13 vamos corrigir isso acrescentando um método __setitem__ de uma linha. Você consegue imaginar como ele seria implementado?

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

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

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

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

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

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

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

  • Emular tipos numéricos

  • Representar objetos na forma de strings

  • Determinar o valor booleano de um objeto

  • Implementar de coleções

1.3.1. Emulando tipos numéricos

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

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

👉 Dica

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

vetores 2D
Figura 1. Exemplo de adição de vetores bi-dimensionais; Vector(2, 4) + Vector(2, 1) resulta em Vector(4, 5).

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

>>> 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. A simple two-dimensional vector class
"""
vector2d.py: a simplistic class demonstrating some special methods

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

This example is greatly expanded later in the book.

Addition::

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

Absolute value::

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

Scalar multiplication::

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

"""


import math

class Vector:

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

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

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

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

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

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

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

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

⚠️ 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 Capítulo 16.

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

1.3.2. Representação de strings

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

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

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

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

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

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

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

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

👉 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 do Python ter um tipo bool, ele aceita qualquer objeto em um contexto booleano, tal como as expressões controlando uma instrução if ou while, ou como operandos de and, or e not. Para determinar se um valor x é verdadeiro ou falso, o Python invoca bool(x), que devolve True ou False.

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

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

Observe que o método especial __bool__ permite que seus objetos sigam as regras de teste do valor verdade definidas no capítulo "Tipos Embutidos" da documentação da Biblioteca Padrão do 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 é a sigla para a mesma expressão em inglês, Abstract Base Classes). As ABCs e o módulo collections.abc são tratados no capítulo Capítulo 13. O objetivo dessa pequena seção é dar uma visão panorâmica das interfaces das coleções mais importantes do Python, mostrando como elas são criadas a partir de métodos especiais.

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 no 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, o Python não exige que classes concretas herdem de qualquer dessas ABCs. Qualquer classe que implemente __len__ satisfaz a interface Sized.

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

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

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

  • Set, a interface dos tipos embutidos set e frozenset

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

✒️ Nota

Desde o Python 3.7, o tipo dict é oficialmente "ordenado", mas isso só que 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 do 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, bit a bit, ou de comparação. Para ter uma visão geral do que está disponível, veja tabelas a seguir.

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

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

Representação de string/bytes

__repr__ __str__ __format__ __bytes__ __fspath__

Conversão para número

__bool__ __complex__ __int__ __float__ __hash__ __index__

Emulação de coleções

__len__ __getitem__ __setitem__ __delitem__ __contains__

Iteração

__iter__ __aiter__ __next__ __anext__ __reversed__

Execução de chamável ou corrotina

__call__ __await__

Gerenciamento de contexto

__enter__ __exit__ __aexit__ __aenter__

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

__new__ __init__ __del__

Gerenciamento de atributos

__getattr__ __getattribute__ __setattr__ __delattr__ __dir__

Descritores de atributos

__get__ __set__ __delete__ __set_name__

Classes base abstratas

__instancecheck__ __subclasscheck__

Metaprogramação de classes

__prepare__ __init_subclass__ __class_getitem__ __mro_entries__

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

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

Unário numérico

- + abs()

__neg__ __pos__ __abs__

Comparação rica

< <= == != > >=

__lt__ __le__ __eq__ __ne__ __gt__ __ge__

Aritmético

+ - * / // % @ divmod() round() ** 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

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

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

1.5. Porque len não é um método?

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

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

✒️ 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 do Python, que antecipou muitas das funcionalidades desta última—tinha o operador #, que era o equivalente de len (se escrevia #s). Quando usado como operador infixo, x#s contava as ocorrências de x em s, que em Python obtemos com s.count(x), para qualquer sequência s.

1.6. Resumo do capítulo

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

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

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

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

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

1.7. Para saber mais

O capítulo "Modelo de Dados" 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 do Python 3: Python Essential Reference (EN), 4th ed. (Addison-Wesley), e Python Cookbook, 3rd ed. (EN) (O’Reilly), com a co-autoria de Brian K. Jones.

O 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 do Python é um exemplo.

Ponto de Vista

Modelo de dados ou modelo de objetos?

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

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

Métodos de "trouxas"

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

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

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

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

Metaobjetos

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

Um protocolo de metaobjetos rico permite estender a linguagem para suportar novos paradigmas de programação. Gregor Kiczales, o primeiro autor do AMOP, mais tarde se tornou um pioneiro da programação orientada a aspecto, e o autor inicial do AspectJ, uma extensão de Java implementando aquele paradigma. A programação orientada a aspecto é muito mais fácil de implementar em uma linguagem dinâmica como Python, e algumas frameworks fazem exatamente isso. O exemplo mais importante é a zope.interface (EN), parte da 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 o Python, Guido foi um dos desenvolvedores da linguagem ABC—um projeto de pesquisa de 10 anos para criar um ambiente de programação para iniciantes. A ABC introduziu várias ideias que hoje consideramos "pithônicas": operações genéricas com diferentes tipos de sequências, tipos tupla e mapeamento embutidos, estrutura [do código] por indentação, tipagem forte sem declaração de variáveis, entre outras. O Python não é assim tão amigável por acidente.

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

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

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

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

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

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

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

  • Lendo de fatias e escrevendo em fatias

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

2.1. Novidades neste capítulo

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

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

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

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

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

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

✒️ 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 1.

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

Dessa forma, sequências planas são mais compactas, mas estão limitadas a manter valores primitivos como bytes e números inteiros e de ponto flutuante.

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

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

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

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

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

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

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

👉 Dica

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

2.3.1. Compreensões de lista e legibilidade

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

Exemplo 1. Cria uma lista de pontos de código Unicode a partir de uma string
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
...     codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
Exemplo 2. Cria uma lista de pontos de código Unicode a partir de uma string, usando uma listcomp
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]

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

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

Naturalmente, é possível abusar das compreensões de lista para escrever código verdadeiramente incompreensível. Já vi código Python usando listcomps apenas para repetir um bloco de código por seus efeitos colaterais. Se você não vai fazer alguma coisa com a lista criada, não deveria usar essa sintaxe. Além disso, tente manter o código curto. Se uma compreensão ocupa mais de duas linhas, provavelmente seria melhor quebrá-la ou reescrevê-la como um bom e velho loop for. Avalie qual o melhor caminho: em Python, como em português, não existem regras absolutas para se escrever bem.

👉 Dica
Dica de sintaxe

No código Python, quebras de linha são ignoradas dentro de pares de [], {}, ou (). Então você pode usar múltiplas linhas para criar listas, listcomps, tuplas, dicionários, etc., sem necessidade de usar o marcador de continuação de linha \, que não funciona se após o \ você acidentalmente digitar um espaço. Outro detalhe, quando aqueles pares de delimitadores são usados para definir um literal com uma série de itens separados por vírgulas, uma vírgula solta no final será ignorada. Daí, por exemplo, quando se codifica uma lista a partir de um literal com múltiplas linhas, é de bom tom deixar uma vírgula após o último item. Isso torna um pouco mais fácil ao próximo programador acrescentar mais um item àquela lista, e reduz o ruído quando se lê os diffs.

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

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

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

  2. last permanece.

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

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

2.3.2. Listcomps versus map e filter

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

Considere o Exemplo 3.

Exemplo 3. A mesma lista, criada por uma listcomp e por uma composição de map/filter
>>> 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 do Python Fluente é um teste de velocidade simples, comparando listcomp com filter/map.

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

2.3.3. Produtos cartesianos

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

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

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

Exemplo 4. Produto cartesiano usando uma compreensão de lista
>>> 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 poderia começar de uma listcomp, mas uma genexp (expressão geradora) economiza memória, pois ela produz itens um de cada vez usando o protocolo iterador, em vez de criar uma lista inteira apenas para alimentar outro construtor.

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

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

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

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

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

Exemplo 6. Produto cartesiano em uma expressão geradora
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in (f'{c} {s}' for c in colors for s in sizes):  (1)
...     print(tshirt)
...
black S
black M
black L
white S
white M
white L
  1. A expressão geradora produz um item por vez; uma lista com todas as seis variações de camisetas nunca aparece neste exemplo.

✒️ 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 do Python: a tupla.

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

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

2.4.1. Tuplas como registros

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

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

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

Exemplo 7. Tuplas usadas como registros
>>> 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 Seção 2.6. E no console do Python, o resultado do comando anterior é atribuído a _—a menos que o resultado seja None.

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

Mas muitas vezes não é preciso se dar ao trabalho de criar uma classe apenas para nomear os campos, especialmente se você aproveitar o desempacotamento e evitar o uso de índices para acessar os campos. No Exemplo 7, atribuímos ('Tokyo', 2003, 32_450, 0.66, 8014) a city, year, pop, chg, area em um único comando. E daí o operador % atribuiu cada item da tupla passport para a posição correspondente da string de formato no argumento print. Esses foram dois exemplos de desempacotamento de tuplas.

✒️ 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 Seção 2.5 fala muito mais sobre desempacotamento, não apenas de tuplas, mas também de sequências e iteráveis em geral.

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

2.4.2. Tuplas como listas imutáveis

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

Clareza

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

Desempenho

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

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

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

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

>>> 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 do Python, Raymond Hettinger, em uma resposta à questão "Are tuples more efficient than lists in Python?" (As tuplas são mais eficientes que as listas no Python?) no StackOverflow. Em resumo, Hettinger escreveu:

  • Para avaliar uma tupla literal, o compilador Python gera bytecode para uma constante tupla em uma operação; mas para um literal lista, o bytecode gerado insere cada elemento como uma constante separada no stack de dados, e então cria a lista.

  • Dada a tupla t, tuple(t) simplesmente devolve uma referência para a mesma t. Não há necessidade de cópia. Por outro lado, dada uma lista l, o construtor list(l) precisa criar uma nova cópia de l.

  • Devido a seu tamanho fixo, uma instância de tuple tem alocado para si o espaço exato de memória que precisa. Em contrapartida, instâncias de list tem alocadas para si memória adicional, para amortizar o custo de acréscimos futuros.

  • As referências para os itens em uma tupla são armazenadas em um array na struct da tupla, enquanto uma lista mantém um ponteiro para um array de referências armazenada em outro lugar. Essa indireção é necessária porque, quando a lista cresce além do espaço alocado naquele momento, o Python precisa realocar o array de referências para criar espaço. A indireção adicional torna o cache da CPU menos eficiente.

2.4.3. Comparando os métodos de tuplas e listas

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

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

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

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

s.append(e)

Acrescenta um elemento após o último

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.count(e)

Conta as ocorrências de um elemento

s.__delitem__(p)

Remove o item na posição p

s.extend(it)

Acrescenta itens do iterável it

s.__getitem__(p)

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

s.__getnewargs__()

Suporte a serialização otimizada com pickle

s.index(e)

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

s.insert(p, e)

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

s.__iter__()

Obtém o iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

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

s.__rmul__(n)

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

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

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

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

>>> 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 do 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 Seção 3.2.2.

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

2.5.3. Desempacotamento aninhado

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

Exemplo 8. Desempacotando tuplas aninhadas para acessar a longitude
metro_areas = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),  # (1)
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

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

if __name__ == '__main__':
    main()
  1. Cada tupla contém um registro com quatro campos, o último deles um par de coordenadas.

  2. Ao atribuir o último campo a uma tupla aninhada, desempacotamos as coordenadas.

  3. O teste lon ⇐ 0: seleciona apenas cidades no hemisfério ocidental.

A saída do Exemplo 8 é:

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

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

2.6. Pattern matching com sequências

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

✒️ Nota

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

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

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

Exemplo 9. Método de uma classe Robot imaginária
    def handle_command(self, message):
        match message:  # (1)
            case ['BEEPER', frequency, times]:  # (2)
                self.beep(times, frequency)
            case ['NECK', angle]:  # (3)
                self.rotate_neck(angle)
            case ['LED', ident, intensity]:  # (4)
                self.leds[ident].set_brightness(ident, intensity)
            case ['LED', ident, red, green, blue]:  # (5)
                self.leds[ident].set_color(ident, red, green, blue)
            case _:  # (6)
                raise InvalidCommand(message)
  1. A expressão após a palavra-chave match é o sujeito (subject). O sujeito contém os dados que o Python vai comparar aos padrões em cada instrução case.

  2. Esse padrão casa com qualquer sujeito que seja uma sequência de três itens. O primeiro item deve ser a string BEEPER. O segundo e o terceiro itens podem ser qualquer coisa, e serão vinculados às variáveis frequency e times, nessa ordem.

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

  4. Isso vai casar com uma sujeito de três itens começando com LED. Se o número de itens não for correspondente, o Python segue para o próximo case.

  5. Outro padrão de sequência começando com 'LED', agora com cinco itens—incluindo a constante 'LED'.

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

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

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

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

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

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

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

  1. O sujeito é uma sequência, e

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

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

Por exemplo, o padrão [name, _, _, (lat, lon)] no Exemplo 10 casa com uma sequência de quatro itens, e o último item tem que ser uma sequência de dois itens.

Padrões de sequência podem ser escritos como tuplas e listas, mas a sintaxe usada não faz diferença: em um padrão de sequência, colchetes e parênteses tem o mesmo significado. Escrevi o padrão como uma lista com uma tupla aninhada de dois itens para evitar a repetição de colchetes ou parênteses no Exemplo 10.

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

⚠️ 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 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 10:

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

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

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

As duas funções principais do lis.py são parse e evaluate.[11] 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 Seção 18.3 para aprender mais sobre o funcionamento do lis.py.

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

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

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

Usando match/case em Python ≥ 3.10, podemos refatorar evaluate, como mostrado no Exemplo 12.

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

Padrões alternativos para lambda

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

(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() do Python.

No Exemplo 12, 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.

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 12. A ordem desses cases de define é irrelevante nesse exemplo, pois nenhum sujeito pode casar com esses dois padrões: o segundo elemento deve ser um Symbol na forma original de define, mas deve ser uma sequência começando com um Symbol no atalho de define para definição de função.

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

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

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

(quote exp)

['quote', exp]

(if test conseq alt)

['if', test, conseq, alt]

(lambda (parms…) body1 body2…)

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

(define name exp)

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

(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 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 Seção 2.12).

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

2.7.2. Objetos fatia

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

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

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

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

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

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

Exemplo 13. Itens de um arquivo tabular de fatura
>>> invoice = """
... 0.....6.................................40........52...55........
... 1909  Pimoroni PiBrella                     $17.50    3    $52.50
... 1489  6mm Tactile Switch x20                 $4.95    2     $9.90
... 1510  Panavise Jr. - PV-201                 $28.00    1    $28.00
... 1601  PiTFT Mini Kit 320x240                $34.95    1    $34.95
... """
>>> SKU = slice(0, 6)
>>> DESCRIPTION = slice(6, 40)
>>> UNIT_PRICE = slice(40, 52)
>>> QUANTITY =  slice(52, 55)
>>> ITEM_TOTAL = slice(55, None)
>>> line_items = invoice.split('\n')[2:]
>>> for item in line_items:
...     print(item[UNIT_PRICE], item[DESCRIPTION])
...
    $17.50   Pimoroni PiBrella
     $4.95   6mm Tactile Switch x20
    $28.00   Panavise Jr. - PV-201
    $34.95   PiTFT Mini Kit 320x240

Voltaremos aos objetos slice quando formos discutir a criação de suas próprias coleções, na seção Seção 12.5. Enquanto isso, do ponto de vista do usuário, o fatiamento tem recursos adicionais, tais como fatias multidimensionais e a notação de reticências (...). Siga comigo.

2.7.3. Fatiamento multidimensional e reticências

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

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

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

As reticências—escritas como três pontos finais (...) e não como (Unicode U+2026)—são reconhecidas como um símbolo pelo parser do Python. Esse símbolo é um apelido para o objeto Ellipsis, a única instância da classe ellipsis.[13] 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 do Python. Se você souber de algum, me avise. Esses recursos sintáticos existem para suportar tipos definidos pelo usuário ou extensões como o NumPy.

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

2.7.4. Atribuindo a fatias

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

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

Exemplo 14. 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 15.

Exemplo 15. 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 15 é 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 14 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, o 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.[14]

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

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

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

Exemplo 16. 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 17 é a saída real em um console rodando Python 3.10.[16]

Exemplo 17. 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 do Python. A Figura 5 é uma composição de duas capturas de tela, mostrando os estados inicial e final da tupla t do Exemplo 17.

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

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

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

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

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

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

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

  • Evite colocar objetos mutáveis em tuplas.

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

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

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

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

O método list.sort ordena uma lista no mesmo lugar—isto é, sem criar uma cópia. Ele devolve None para nos lembrar que muda a própria instância e não cria uma nova lista. Essa é uma convenção importante da API do Python: funções e métodos que mudam um objeto no mesmo lugar deve devolver None, para deixar claro a quem chamou que o receptor[17] 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 do Python é estável (isto é, ele preserva a ordem relativa de itens que resultam iguais na comparação):[18]

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

  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, o 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 Seção 4.8 trata de maneiras corretas de ordenar texto da forma esperada por seres humanos.

Uma vez ordenadas, podemos realizar em nossas sequências de forma muito eficiente. Um algoritmo de busca binária já é fornecido no módulo bisect da biblioteca padrão do Python. Aquele módulo também inclui a função bisect.insort, que você pode usar para assegurar que suas sequências ordenadas permaneçam ordenadas. Há uma introdução ilustrada ao módulo bisect no post "Managing Ordered Sequences with Bisect" (Gerenciando Sequências Ordenadas com Bisect) (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[20] 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 do Python quase tão enxuto quanto um array do C. Como mostrado na Figura 1, um array de valores float não mantém instâncias completas de float, mas apenas pacotes de bytes representando seus valores em código de máquina—de forma similar a um array de double na linguagem C. Ao criar um array, você fornece um código de tipo (typecode), uma letra que determina o tipo C subjacente usado para armazenar cada item no array. Por exemplo, b é o código de tipo para o que o C chama de signed char, um inteiro variando de -128 a 127. Se você criar uma array('b'), então cada item será armazenado em um único byte e será interpretado como um inteiro. Para grandes sequências de números, isso economiza muita memória. E o Python não permite que você insira qualquer número que não corresponda ao tipo do array.

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

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

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

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

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

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

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

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

  8. Verifica a igualdade do conteúdo dos arrays

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

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

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

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

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

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

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[21]

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

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 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 20 mostra como criar views alternativas da mesmo array de 6 bytes, para operar com ele como uma matriz de 2x3 ou de 3x2.

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

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

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

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

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

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

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

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

Exemplo 21. Mudando o valor de um item em um array de inteiros de 16 bits trocando apenas o valor de um de seus bytes
>>> 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 do Python, para que você a aproveite ao máximo. Mas a NumPy é tão maravilhosa que exige um desvio.

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

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

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

Exemplo 22. Operações básicas com linhas e colunas em uma numpy.ndarray
>>> import numpy as np (1)
>>> a = np.arange(12)  (2)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape  (3)
(12,)
>>> a.shape = 3, 4  (4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> a[2]  (5)
array([ 8,  9, 10, 11])
>>> a[2, 1]  (6)
9
>>> a[:, 1]  (7)
array([1, 5, 9])
>>> a.transpose()  (8)
array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])
  1. Importa a NumPy, que precisa ser instalada previamente (ela não faz parte da biblioteca padrão do Python). Por convenção, numpy é importada como np.

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

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

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

  5. Obtém a linha no índice 2

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

  7. Obtém a coluna no índice 1

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

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

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

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

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

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

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

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

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

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

Mas isso foi apenas um aperitivo.

A NumPy e a SciPy são bibliotecas formidáveis, e estão na base de outras ferramentas fantásticas, como a Pandas (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) do 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 que livros inteiros sejam escritos sobre eles. Este não um desses livros. Mas nenhuma revisão das sequências do Python estaria completa sem pelo menos uma breve passagem pelos arrays da NumPy.

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

2.10.4. Deques e outras filas

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

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

Exemplo 23. Usando um deque
>>> 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[22]

s.pop()

Remove e devolve último item[23]

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 do Python implementam filas:

queue

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

multiprocessing

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

asyncio

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

heapq

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

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

2.11. Resumo do capítulo

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

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

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

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

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

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

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

Concatenação repetida, como em seq * n, é conveniente e, tomando cuidado, pode ser usada para inicializar listas de listas contendo itens imutáveis. Atribuição aumentada com += e *= se comporta de forma diferente com sequências mutáveis e imutáveis. No último caso, esses operadores necessariamente criam novas sequências. Mas se a sequência alvo é mutável, ela em geral é modificada no lugar—mas nem sempre, depende de como a sequência é implementada.

O método sort e a função embutida sorted são fáceis de usar e flexíveis, graças ao argumento opcional key: uma função para calcular o critério de ordenação. E aliás, key também pode ser usado com as funções embutidas min e max.

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

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

2.12. Leitura complementar

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

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

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

A PEP 3132—​Extended Iterable Unpacking (Desempacotamento Iterável Estendido) (EN) é a fonte canônica para ler sobre o novo uso da sintaxe *extra no lado esquerdo de atribuições paralelas. Se você quiser dar uma olhada no processo de evolução do Python, "Missing *-unpacking generalizations" (As generalizações esquecidas de * no desempacotamento) (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 Seção 2.6, o texto introdutório "Correspondência 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 pattern matching é bom para você. SE você precisar de mais argumentos para se convencer ou convencer outros que o pattern matching é bom para o Python, leia as 22 páginas de PEP 635—Structural Pattern Matching: Motivation and Rationale (_Casamento de Padrões Estrutural: Motivação e Justificativa) (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 do Python.

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

Ponto de Vista

A natureza das tuplas

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

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

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

Sequências planas versus sequências contêineres

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

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 do Python 2 cmp(a, b). Usar key é mais simples e mais eficiente. É mais simples porque basta definir uma função de um único argumento que recupera ou calcula o critério a ser usado para ordenar seus objetos; isso é mais fácil que escrever uma função de dois argumentos para devolver –1, 0, 1. Também é mais eficiente, porque a função key é invocada apenas uma vez por item, enquanto a comparação de dois argumentos é chamada a cada vez que o algoritmo de ordenação precisa comparar dois itens. Claro, o Python também precisa comparar as chaves ao ordenar, mas aquela comparação é feita em código C otimizado, não em uma função Python escrita por você.

Por sinal, usando key podemos ordenar uma lista bagunçada de números e strings "parecidas com números". Só precisamos decidir se queremos tratar todos os itens como inteiros ou como strings:

>>> 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 no Java padrão quanto no Android, um fato que ficou muito conhecido quando a Oracle usou parte do código relacionado ao Timsort como evidência da violação da propriedade intelectual da Sun pelo Google. Por exemplo, veja essa ordem do Juiz William Alsup (EN) de 2012. Em 2021, a Suprema Corte dos Estados Unidos decidiu que o uso do código do Java pelo Google é "fair use"[24]

O Timsort foi inventado por Tim Peters, um dos desenvolvedores principais do Python, e tão produtivo que se acredita que ele seja uma inteligência artificial, o Timbot. Você pode ler mais sobre essa teoria da conspiração em "Python Humor" (EN). Tim também escreveu "The Zen of Python": import this.

3. Dicionários e conjuntos

O Python é feito basicamente de dicionários cobertos por muitas camadas de açú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 do Python. Atributos de classes e de instâncias, espaços de nomes de módulos e argumentos nomeados de funções são alguns dos elementos fundamentais do Python representados na memória por dicionários. O __builtins__.__dict__ armazena todos os tipos, funções e objetos embutidos.

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

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

Aqui está um breve esquema do capítulo:

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

  • Métodos comuns dos tipos de mapeamentos

  • Tratamento especial para chaves ausentes

  • Variantes de dict na biblioteca padrão

  • Os tipos set e frozenset

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

3.1. Novidades nesse capítulo

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

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

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

  • A seção Seção 3.6.1 agora se concentra nas pequenas mas ainda relevantes diferenças entre dict e OrderedDict—levando em conta que, desde o Python 3.6, dict passou a manter a ordem de inserção das chaves.

  • Novas seções sobre os objetos view devolvidos por dict.keys, dict.items, e dict.values: a Seção 3.8 e a Seção 3.12.

A implementação interna de dict e set ainda está alicerçada em tabelas de hash, mas o código de dict teve duas otimizações importantes, que economizam memória e preservam o ordem de inserção das chaves. As seções Seção 3.9 e Seção 3.11 resumem o que você precisa saber sobre isso para usar bem as estruturas efetadas.

✒️ 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 o Python 3.6) .

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

3.2. A sintaxe moderna dos dicts

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

3.2.1. Compreensões de dict

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

Exemplo 1. Exemplos de compreensões de dict
>>> 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 o Python 3.5.

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

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

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

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

Exemplo 2. creator.py: get_creators() extrai o nome dos criadores em registros de mídia
def get_creators(record: dict) -> list:
    match record:
        case {'type': 'book', 'api': 2, 'authors': [*names]}:  # (1)
            return names
        case {'type': 'book', 'api': 1, 'author': name}:  # (2)
            return [name]
        case {'type': 'book'}:  # (3)
            raise ValueError(f"Invalid 'book' record: {record!r}")
        case {'type': 'movie', 'director': name}:  # (4)
            return [name]
        case _:  # (5)
            raise ValueError(f'Invalid record: {record!r}')
  1. Casa com qualquer mapeamento na forma 'type': 'book', 'api' :2, e uma chave 'authors' mapeada para uma sequência. Devolve os itens da sequência, como uma nova list.

  2. Casa com qualquer mapeamento na forma 'type': 'book', 'api' :1, e uma chave 'author' mapeada para qualquer objeto. Devolve aquele objeto dentro de uma list.

  3. Qualquer outro mapeamento na forma 'type': 'book' é inválido e gera um ValueError.

  4. Casa qualquer mapeamento na forma 'type': 'movie' e uma chave 'director' mapeada para um único objeto. Devolve o objeto dentro de uma list.

  5. Qualquer outro sujeito é inválido e gera um ValueError.

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

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

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

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

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

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

A maior utilidade dessas ABCs é documentar e formalizar as interfaces padrão para os mapeamentos, e servir e critério para testes com isinstance em código que precise suportar mapeamentos de forma geral:

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