Para Marta, com todo o meu amor.
Prefácio
Eis um plano: se uma pessa 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]
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 destr
ebytes
--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 dev
,async for
easync 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 bookquestions@oreilly.com.
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 permissions@oreilly.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 bookquestions@oreilly.com, 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.
No Facebook: http://facebook.com/oreilly.
No Twitter: https://twitter.com/oreillymedia.
No YouTube: http://www.youtube.com/oreillymedia.
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 inacreditavelmente 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:
-
inglês,
-
português brasileiro,
-
chinês simplificado (China),
-
chinês tradicional (Taiwan),
-
japonês,
-
coreano,
-
russo,
-
francês,
-
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
ouasync 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 |
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 |
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__
.
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
Nesse casos, usei a diretiva |
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 |
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 |
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__
.
"""
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 |
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étodostr.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 "What is the difference between |
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
Isso é mais difícil de ler, mas evita a jornada através de |
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.
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 suportarfor
, desempacotamento, e outras formas de iteração -
Sized
para suportar a função embutidalen
-
Container
para suportar o operadorin
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 comolist
estr
-
Mapping
, implementado pordict
,collections.defaultdict
, etc. -
Set
, a interface dos tipos embutidosset
efrozenset
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 |
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).
Categoria | Nomes dos métodos |
---|---|
Representação de string/bytes |
|
Conversão para número |
|
Emulação de coleções |
|
Iteração |
|
Execução de chamável ou corrotina |
|
Gerenciamento de contexto |
|
Criação e destruição de instâncias |
|
Gerenciamento de atributos |
|
Descritores de atributos |
|
Classes base abstratas |
|
Metaprogramação de classes |
|
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.
Categoria do operador | Símbolos | Nomes de métodos |
---|---|---|
Unário numérico |
|
|
Comparação rica |
|
|
Aritmético |
|
|
Aritmética reversa |
operadores aritméticos com operandos invertidos) |
|
Atribuição aritmética aumentada |
|
|
Bit a bit |
|
|
Bit a bit reversa |
(operadores bit a bit com os operandos invertidos) |
|
Atribuição bit a bit aumentada |
|
|
✒️ 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 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 |
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.
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
etuple
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
, ecollections.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
, earray.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.
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
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 |
Outra forma de agrupar as sequências é por mutabilidade:
- Sequências mutáveis
-
Por exemplo,
list
,bytearray
,array.array
ecollections.deque
. - Sequências imutáveis
-
Por exemplo,
tuple
,str
, ebytes
.
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
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
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
... codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
>>> 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 |
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.
>>> 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.
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.
>>> 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')]
-
Isso gera uma lista de tuplas ordenadas por cor, depois por tamanho.
-
Observe que a lista resultante é ordenada como se os loops
for
estivessem aninhados na mesma ordem que eles aparecem na listcomp. -
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.
>>> 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])
-
Se a expressão geradora é o único argumento em uma chamada de função, não há necessidade de duplicar os parênteses circundantes.
-
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 dearray
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
.
>>> 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
-
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.
>>> 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
-
Latitude e longitude do Aeroporto Internacional de Los Angeles.
-
Dados sobre Tóquio: nome, ano, população (em milhares), crescimento populacional (%) e área (km²).
-
Uma lista de tuplas no formato (código_de_país, número_do_passaporte).
-
Iterando sobre a lista,
passport
é vinculado a cada tupla. -
O operador de formatação
%
entende as tuplas e trata cada item como um campo separado. -
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 |
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 umalist
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.
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 mesmat
. Não há necessidade de cópia. Por outro lado, dada uma listal
, o construtorlist(l)
precisa criar uma nova cópia del
. -
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 delist
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.
list |
tuple |
||
---|---|---|---|
|
● |
● |
s + s2—concatenação |
|
● |
s += s2—concatenação no mesmo lugar |
|
|
● |
Acrescenta um elemento após o último |
|
|
● |
Apaga todos os itens |
|
|
● |
● |
|
|
● |
Cópia rasa da lista |
|
|
● |
● |
Conta as ocorrências de um elemento |
|
● |
Remove o item na posição |
|
|
● |
Acrescenta itens do iterável |
|
|
● |
● |
s[p]—obtém o item na posição |
|
● |
Suporte a serialização otimizada com |
|
|
● |
● |
Encontra a posição da primeira ocorrência de |
|
● |
Insere elemento |
|
|
● |
● |
Obtém o iterador |
|
● |
● |
len(s)—número de itens |
|
● |
● |
s * n—concatenação repetida |
|
● |
s *= n—concatenação repetida no mesmo lugar |
|
|
● |
● |
n * s—concatenação repetida inversa[6] |
|
● |
Remove e devolve o último item ou o item na posição opcional |
|
|
● |
Remove a primeira ocorrência do elemento |
|
|
● |
Reverte, no lugar, a ordem dos itens |
|
|
● |
Obtém iterador para examinar itens, do último para o primeiro |
|
|
● |
s[p] = e—coloca |
|
|
● |
Ordena os itens no lugar, com os argumentos nomeados opcionais |
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.
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()
-
Cada tupla contém um registro com quatro campos, o último deles um par de coordenadas.
-
Ao atribuir o último campo a uma tupla aninhada, desempacotamos as coordenadas.
-
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:
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)
-
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çãocase
. -
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áveisfrequency
etimes
, nessa ordem. -
Isso casa com qualquer sujeito com dois itens, se o primeiro for
'NECK'
. -
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óximocase
. -
Outro padrão de sequência começando com
'LED'
, agora com cinco itens—incluindo a constante'LED'
. -
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
.
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}')
-
O sujeito desse
match
érecord
—isto é, cada uma das tuplas emmetro_areas
. -
Uma instrução
case
tem duas partes: um padrão e uma guarda opcional, com a palavra-chaveif
.
Em geral, um padrão de sequência casa com o sujeito se estas três condições forem verdadeiras:
-
O sujeito é uma sequência, e
-
O sujeito e o padrão tem o mesmo número de itens, e
-
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
|
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 |
---|---|
|
|
|
|
|
|
|
|
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 |
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 |
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.
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.
match/case
—requer Python ≥ 3.10def 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))
-
Casa se o sujeito for uma sequência de dois itens começando com
'quote'
. -
Casa se o sujeito for uma sequência de quatro itens começando com
'if'
. -
Casa se o sujeito for uma sequência com três ou mais itens começando com
'lambda'
. A guarda assegura quebody
não esteja vazio. -
Casa de o sujeito for uma sequência de três itens começando com
'define'
, seguido de uma instância deSymbol
. -
é uma boa prática ter um
case
para capturar todo o resto. Neste exemplo, seexp
não casar com nenhum dos padrões, a expressão está mal-formada, então gera umSyntaxError
.
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.
Sintaxe do Scheme | Padrão de sequência |
---|---|
|
|
|
|
|
|
|
|
|
|
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 |
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)
quantomy_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 escrevamy_list[:x]
emy_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.
>>> 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]
-
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 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.
>>> board = [['_'] * 3 for i in range(3)] (1)
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' (2)
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
-
Cria uma lista de três listas, cada uma com três itens. Inspeciona a estrutura criada.
-
Coloca um "X" na linha 1, coluna 2, e verifica o resultado.
Um atalho tentador mas errado seria fazer algo como o Exemplo 15.
>>> weird_board = [['_'] * 3] * 3 (1)
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' (2)
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
-
A lista externa é feita de três referências para a mesma lista interna. Enquanto ela não é modificada, tudo parece correr bem.
-
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)
-
A mesma
row
é anexada três vezes aoboard
.
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', '_', '_']]
-
Cada iteração cria uma nova
row
e a acrescenta aoboard
. -
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)
-
O ID da lista inicial.
-
Após a multiplicação, a lista é o mesmo objeto, com novos itens anexados.
-
O ID da tupla inicial.
-
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]
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
O que acontece a seguir? Escolha a melhor alternativa:
-
t
se torna(1, 2, [30, 40, 50, 60])
. -
É gerado um
TypeError
com a mensagem'tuple' object does not support item assignment
(o objeto tupla não suporta atribuição de itens). -
Nenhuma das alternativas acima..
-
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]
>>> 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.
Se olharmos o bytecode gerado pelo Python para a expressão s[a] += b
(Exemplo 18), fica claro como isso acontece.
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
-
Coloca o valor de
s[a]
noTOS
(Top Of Stack, topo da pilha de execução_). -
Executa
TOS += b
. Isso é bem sucedido seTOS
se refere a um objeto mutável (no Exemplo 17 é uma lista). -
Atribui
s[a] = TOS
. Isso falha ses
é imutável (a tuplat
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 |
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, ekey=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 |
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)
-
Isso produz uma lista de strings ordenadas alfabeticamente.[19]
-
Inspecionando a lista original, vemos que ela não mudou.
-
Isso é a ordenação "alfabética" anterior, invertida.
-
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.
-
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."
-
Até aqui, a ordenação da lista
fruits
original não mudou. -
Isso ordena a lista no mesmo lugar, devolvendo
None
(que o console omite). -
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, |
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.
>>> 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
-
Importa o tipo
array
. -
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. -
Inspeciona o último número no array.
-
Salva o array em um arquivo binário.
-
Cria um array vazio de números de ponto flutuante de dupla precisão
-
Lê 10 milhões de números do arquivo binário.
-
Inspeciona o último número no array.
-
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
.
list | array | ||
---|---|---|---|
|
● |
● |
|
|
● |
● |
|
|
● |
● |
Acrescenta um elemento após o último |
|
● |
Permuta os bytes de todos os itens do array para conversão de endianness (ordem de interpretação bytes) |
|
|
● |
Apaga todos os itens |
|
|
● |
● |
|
|
● |
Cópia rasa da lista |
|
|
● |
Suporte a |
|
|
● |
● |
Conta as ocorrências de um elemento |
|
● |
Suporte otimizado a |
|
|
● |
● |
Remove item na posição |
|
● |
● |
Acrescenta itens a partir do iterável |
|
● |
Acrescenta itens de uma sequência de bytes, interpretada como valores em código de máquina empacotados |
|
|
● |
Acrescenta |
|
|
● |
Acrescenta itens de lista; se um deles causar um |
|
|
● |
● |
|
|
● |
● |
Encontra a posição da primeira ocorrência de |
|
● |
● |
Insere elemento |
|
● |
Tamanho em bytes de cada item do array |
|
|
● |
● |
Obtém iterador |
|
● |
● |
|
|
● |
● |
|
|
● |
● |
|
|
● |
● |
n * s—concatenação repetida invertida[21] |
|
● |
● |
Remove e devolve o item na posição |
|
● |
● |
Remove a primeira ocorrência do elemento |
|
● |
● |
Reverte a ordem dos itens no mesmo lugar |
|
● |
Obtém iterador para percorrer itens do último até o primeiro |
|
|
● |
● |
s[p] = e—coloca |
|
● |
Ordena itens no mesmo lugar, com os argumentos de palavra-chave opcionais |
|
|
● |
Devolve itens como pacotes de valores em código de máquina em um objeto |
|
|
● |
Grava itens como pacotes de valores em código de máquina no arquivo binário |
|
|
● |
Devolve os itens como objetos numéricos em uma |
|
|
● |
String de um caractere identificando o tipo em C dos itens |
👉 Dica
|
Até o Python 3.10, o tipo
Para manter a ordem de um array ordenado ao acrescentar novos itens, use a função |
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.
>>> 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])
-
Cria um array de 6 bytes (código de tipo
'B'
). -
Cria uma
memoryview
a partir daquele array, e a exporta como uma lista. -
Cria uma nova
memoryview
a partir da anterior, mas com2
linhas e3
colunas. -
Ainda outra
memoryview
, agora com3
linhas e2
colunas. -
Sobrescreve o byte em
m2
, na linha1
, coluna1
com22
. -
Sobrescreve o byte em
m3
, na linha1
, coluna1
com33
. -
Mostra o array original, provando que a memória era compartilhada entre
octets
,m1
,m2
, em3
.
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.
>>> 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)
-
Cria uma
memoryview
a partir de um array de 5 inteiros com sinal de 16 bits (código de tipo'h'
). -
memv
vê os mesmos 5 itens no array. -
Cria
memv_oct
, transformando os elementos dememv
em bytes (código de tipo'B'
). -
Exporta os elementos de
memv_oct
como uma lista de 10 bytes, para inspeção. -
Atribui o valor
4
ao byte com offset5
. -
Observe a mudança em
numbers
: um4
no byte mais significativo de um inteiro de 2 bytes sem sinal é1024
.
✒️ Nota
|
Você pode ver um exemplo de inspeção de uma |
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.
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]])
-
Importa a NumPy, que precisa ser instalada previamente (ela não faz parte da biblioteca padrão do Python). Por convenção,
numpy
é importada comonp
. -
Cria e inspeciona uma
numpy.ndarray
com inteiros de0
a11
. -
Inspeciona as dimensões do array: essa é um array com uma dimensão e 12 elementos.
-
Muda o formato do array, acrescentando uma dimensão e depois inspecionando o resultado.
-
Obtém a linha no índice
2
-
Obtém elemento na posição
2, 1
. -
Obtém a coluna no índice
1
-
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])
-
Carrega 10 milhões de números de ponto flutuante de um arquivo de texto.
-
Usa a notação de fatiamento de sequência para inspecionar os três últimos números.
-
Multiplica cada elemento no array
floats
por.5
e inspeciona novamente os três últimos elementos. -
Importa o cronômetro de medida de tempo em alta resolução (disponível desde o Python 3.3).
-
Divide cada elemento por
3
; o tempo decorrido para dividir os 10 milhões de números de ponto flutuante é menos de 40 milissegundos. -
Salva o array em um arquivo binário .npy.
-
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.
-
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
.
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)
-
O argumento opcional
maxlen
determina o número máximo de itens permitidos nessa instância dedeque
; isso estabelece o valor de um atributo de instânciamaxlen
, somente de leitura. -
Rotacionar com
n > 0
retira itens da direita e os recoloca pela esquerda; quandon < 0
, os itens são retirados pela esquerda e anexados pela direita. -
Acrescentar itens a um
deque
cheio (len(d) == d.maxlen
) elimina itens da ponta oposta. Observe, na linha seguinte, que o0
foi descartado. -
Acrescentar três itens à direita derruba
-1
,1
, e2
da extremidade esquerda. -
Observe que
extendleft(iter)
acrescenta cada item sucessivo do argumentoiter
do lado esquerdo dodeque
, 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.
list | deque | ||
---|---|---|---|
|
● |
s + s2—concatenação |
|
|
● |
● |
s += s2—concatenação no mesmo lugar |
|
● |
● |
Acrescenta um elemento à direita (após o último) |
|
● |
Acrescenta um elemento à esquerda (antes do primeiro) |
|
|
● |
● |
Apaga todos os itens |
|
● |
|
|
|
● |
Cópia rasa da lista |
|
|
● |
Suporte a |
|
|
● |
● |
Conta ocorrências de um elemento |
|
● |
● |
Remove item na posição |
|
● |
● |
Acrescenta item do iterável |
|
● |
Acrescenta item do iterável |
|
|
● |
● |
s[p]—obtém item ou fatia na posição |
|
● |
Encontra a primeira ocorrência de |
|
|
● |
Insere elemento |
|
|
● |
● |
Obtém iterador |
|
● |
● |
len(s)—número de itens |
|
● |
s * n—concatenação repetida |
|
|
● |
s *= n—concatenação repetida no mesmo lugar |
|
|
● |
n * s—concatenação repetida invertida[22] |
|
|
● |
● |
Remove e devolve último item[23] |
|
● |
Remove e devolve primeiro item |
|
|
● |
● |
Remove primeira ocorrência do elemento |
|
● |
● |
Inverte a ordem do itens no mesmo lugar |
|
● |
● |
Obtém iterador para percorrer itens, do último para o primeiro |
|
● |
Move |
|
|
● |
● |
s[p] = e—coloca |
|
● |
Ordena os itens no mesmo lugar, com os argumentos de palavra-chave opcionais |
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
, ePriorityQueue
. Essas classes podem ser usadas para comunicação segura entre threads. Todas, excetoSimpleQueue
, podem ser delimitadas passando um argumentomaxsize
maior que 0 ao construtor. Entretanto, elas não descartam um item para abrir espaço, como fazdeque
. 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, eQueue
, delimitada, muito similares àquelas no pacotequeue
, mas projetadas para comunicação entre processos. Uma fila especializada,multiprocessing.JoinableQueue
, é disponibilizada para gerenciamento de tarefas. asyncio
-
Fornece
Queue
,LifoQueue
,PriorityQueue
, eJoinableQueue
com APIs inspiradas pelas classes nos módulosqueue
emultiprocessing
, 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 comoheappush
eheappop
, 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).
O post de Eli Bendersky em seu blog, "Less copies in Python with the buffer protocol and memoryviews" (Menos cópias em Python, com o protocolo de buffer e mamoryviews) inclui um pequeno tutorial sobre memoryview
.
Há muitos livros tratando da NumPy no mercado, e muitos não mencionam "NumPy" no título. Dois exemplos são o Python Data Science Handbook, 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.
3. Dicionários e conjuntos
O Python é feito basicamente de dicionários cobertos por muitas camadas de açúcar sintático
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
efrozenset
-
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 pelosdicts
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
eOrderedDict
—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
, edict.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:
|
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.
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'}
-
Um iterável de pares chave-valor como
dial_codes
pode ser passado diretamente para o construtor dedict
, mas… -
…aqui permutamos os pares:
country
é a chave, ecode
é o valor. -
Ordenando
country_dial
por nome, revertendo novamente os pares, colocando os valores em maiúsculas e filtrando os itens comcode < 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
.
get_creators()
extrai o nome dos criadores em registros de mídiadef 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}')
-
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 novalist
. -
Casa com qualquer mapeamento na forma
'type': 'book', 'api' :1
, e uma chave'author'
mapeada para qualquer objeto. Devolve aquele objeto dentro de umalist
. -
Qualquer outro mapeamento na forma
'type': 'book'
é inválido e gera umValueError
. -
Casa qualquer mapeamento na forma
'type': 'movie'
e uma chave'director'
mapeada para um único objeto. Devolve o objeto dentro de umalist
. -
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 umcase
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 |
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 |