Para Marta, com todo o meu amor.
Prefácio
Eis um plano: se uma pessoa usar um recurso que você não entende, mate-a. É mais fácil que aprender algo novo, e em pouco tempo os únicos programadores sobreviventes usarão apenas um subconjunto minúsculo e fácil de entender de Python 0.9.6 <piscadela marota>.[1]
lendário colaborador do CPython e autor do Zen de Python
"Python é uma linguagem fácil de aprender e poderosa." Essas são as primeiras palavras do tutorial oficial de Python 3.10. Isso é verdade, mas há uma pegadinha: como a linguagem é fácil de entender e de começar a usar, muitos programadores praticantes de Python se contentam apenas com uma fração de seus poderosos recursos.
Uma programadora experiente pode começar a escrever código Python útil em questão de horas. Conforme as primeiras horas produtivas se tornam semanas e meses, muitos desenvolvedores continuam escrevendo código Python com um forte sotaque das linguagens que aprenderam antes. Mesmo se Python for sua primeira linguagem, muitas vezes ela é apresentada nas universidades e em livros introdutórios evitando deliberadamente os recursos específicos da linguagem.
Como professor, ensinando Python para programadores experientes em outras linguagens, vejo outro problema: só sentimos falta daquilo que conhecemos. Vindo de outra linguagem, qualquer um é capaz de imaginar que Python suporta expressões regulares, e procurar esse tema na documentação. Mas se você nunca viu desempacotamento de tuplas ou descritores de atributos, talvez nunca procure por eles, e pode acabar não usando esses recursos, só por que são novos para você.
Este livro não é uma referência exaustiva de Python de A a Z. A ênfase está em recursos da linguagem característicos de Python ou incomuns em outras linguagens populares. Vamos nos concentrar principalmente nos aspectos centrais da linguagem e pacotes essenciais da biblioteca padrão. Apenas alguns exemplos mostram o uso de pacotes externos como FastAPI, httpx, e Curio.
Para quem é esse livro
Escrevi este livro para programadores que já usam Python e desejem se tornar fluentes em Python 3 moderno. Testei os exemplos em Python 3.10—e a maioria também em Python 3.9 e 3.8. Os exemplos que exigem especificamente Python 3.10 estão indicados.
Caso não tenha certeza se conhece Python o suficiente para acompanhar o livro, revise o tutorial oficial de Python. Tópicos tratados no tutorial não serão explicados aqui, exceto por alguns recursos mais novos.
Para quem esse livro não é
Se está começando a estudar Python, poderá achar difícil acompanhar este livro. Mais ainda, se você o ler muito cedo em sua jornada pela linguagem, pode ficar com a impressão que todo script Python precisa se valer de métodos especiais e truques de metaprogramação. Abstração prematura é tão ruim quanto otimização prematura.
Para quem está aprendendo a programar, recomendo o livro Pense em Python de Allen Downey, disponível na Web.
Se já sabe programar e está aprendendo Python, o tutorial oficial de Python foi traduzido pela comunidade Python brasileira.
Como ler este livro
Recomendo que todos leiam o Capítulo 1. Após a leitura do capítulo "O modelo de dados de Python", o público principal deste livro não terá problema em pular diretamente para qualquer outra parte, mas muitas vezes assumo que você leu os capítulos precedentes de cada parte específica. Pense nas partes Parte I: Estruturas de dados até a Parte V: Metaprogramação como cinco livros dentro do livro.
Tentei enfatizar o uso de classes e módulos que já existem antes de discutir como criar seus próprios.
Por exemplo, na Parte I: Estruturas de dados,
o Capítulo 2 trata dos tipos de sequências que estão prontas para serem usadas,
incluindo algumas que não recebem muita atenção, como collections.deque
.
Criar sequências definidas pelo usuário só é discutido na Parte III: Classes e protocolos, onde também vemos como usar as classes base abstratas (ABCs) de collections.abc
.
Criar suas próprias ABCs é discutido ainda mais tarde, na Parte III: Classes e protocolos, pois acredito na importância de estar confortável usando uma ABC antes de escrever uma.
Essa abordagem tem algumas vantagens. Primeiro, saber o que está disponivel para uso imediato pode evitar que você reinvente a roda. Usamos as classes de coleções existentes com mais frequência que implementamos nossas próprias coleções, e podemos prestar mais atenção ao uso avançado de ferramentas prontas adiando a discussão sobre a criação de novas ferramentas. Também é mais provável herdamos de ABCs existentes que criar uma nova ABC do zero. E, finalmente, acredito ser mais fácil entender as abstrações após vê-las em ação.
A desvantagem dessa estratégia são as referências a pontos futuros espalhadas pelo livro. Acredito que isso é mais fácil de tolerar agora que você sabe porque escolhi esse caminho.
Cinco livros em um
Aqui estão os principais tópicos de cada parte do livro:
- Parte I: Estruturas de dados
-
O Capítulo 1 introduz o Modelo de Dados de Python e explica porque os métodos especiais (por exemplo,
__repr__
) são a chave do comportamento consistente de objetos de todos os tipos. Os métodos especiais são tratados em maiores detalhes ao longo do livro. Os capítulos restantes dessa parte cobrem o uso de tipos coleção: sequências, mapeamentos e conjuntos, bem como a separação destr
ebytes
--causa de muitas celebrações entre usuários de Python 3, e de muita dor para usuários de Python 2 obrigados a migrar suas bases de código. Também são abordadas as fábricas de classe de alto nível na biblioteca padrão: fábricas de tuplas nomeadas e o decorador@dataclass
. Pattern matching ("casamento de padrões")—novidade no Python 3.10—é tratada em seções do Capítulo 2, do Capítulo 3 e do Capítulo 5, que discutem padrões para sequências, padrões para mapeamentos e padrões para instâncias de classes. O último capítulo na Parte I: Estruturas de dados versa sobre o ciclo de vida dos objetos: referências, mutabilidade e coleta de lixo (garbage collection). - Parte II: Funções como objetos
-
Aqui falamos sobre funções como objetos de primeira classe na linguagem: o significado disso, como isso afeta alguns padrões de projetos populares e como aproveitar as clausuras para implementar decoradores de função. Também são vistos aqui o conceito geral de invocáveis no Python, atributos de função, introspecção, anotação de parâmetros e a nova declaração
nonlocal
no Python 3. O Capítulo 8 introduz um novo tópico importante, dicas de tipo em assinaturas de função. - Parte III: Classes e protocolos
-
Agora o foco se volta para a criação "manual" de classes—em contraste com o uso de fábricas de classe vistas no Capítulo 5. Como qualquer linguagem orientada a objetos, Python tem seu conjunto particular de recursos que podem ou não estar presentes na linguagem na qual você ou eu aprendemos programação baseada em classes. Os capítulos explicam como criar suas próprias coleções, classes base abstratas (ABCs) e protocolos, bem como as formas de lidar com herança múltipla e como implementar a sobrecarga de operadores, quando fizer sentido. O Capítulo 15 continua a conversa sobre dicas de tipo.
- Parte IV: Controle de fluxo
-
Nesta parte são tratados os mecanismos da linguagem e as bibliotecas que vão além do controle de fluxo tradicional, com condicionais, laços e sub-rotinas. Começamos com os geradores, visitamos a seguir os gerenciadores de contexto e as corrotinas, incluindo a desafiadora mas poderosa sintaxe do
yield from
. O Capítulo 18 inclui um exemplo significativo, usando pattern matching em um interpretador de linguagem simples mas funcional. O Capítulo 19 é novo, apresentando uma visão geral das alternativas para processamento concorrente e paralelo no Python, suas limitações, e como a arquitetura de software permite ao Python operar na escala da Web. Reescrevi o capítulo sobre programação assíncrona, para enfatizar os recursos centrais da linguagem—por exemplo,await
,async def
,async for
easync with
, e mostrar como eles são usados com asyncio e outros frameworks. - Parte V: Metaprogramação
-
Essa parte começa com uma revisão de técnicas para criação de classes com atributos criados dinamicamente para lidar com dados semi-estruturados, tal como conjuntos de dados JSON. A seguir tratamos do mecanismo familiar das propriedades, antes de mergulhar no funcionamento do acesso a atributos de objetos no Python em um nível mais baixo, usando descritores. A relação entre funções, métodos e descritores é explicada. Por toda a Parte V: Metaprogramação, a implementação passo a passo de uma biblioteca de validação de campos revela questões sutis, levando às ferramentas avançadas do capítulo final: decoradores de classes e metaclasses.
Abordagem "mão na massa"
Frequentemente usaremos o console interativo de Python para explorar a linguagem e as bibliotecas. Acho isso importante para enfatizar o poder dessa ferramenta de aprendizagem, especialmente para quem teve mais experiência com linguagens estáticas compiladas, que não oferecem um REPL.[2]
Um dos pacotes padrão de testagem de Python, o doctest
, funciona simulando sessões de console e verificando se as expressões resultam nas resposta exibidas. Usei doctest
para verificar a maior parte do código desse livro, incluindo as listagens do console.
Não é necessário usar ou sequer saber da existência do doctest
para acompanhar o texto:
a principal característica dos doctests é que eles imitam transcrições de sessões
interativas no console de Python, assim qualquer pessoa pode reproduzir as demonstrações facilmente.
Algumas vezes vou explicar o que queremos realizar mostrando um doctest antes do código que implementa a solução. Estabelecer precisamente o quê deve ser feito, antes de pensar sobre como fazer, ajuda a focalizar nosso esforço de codificação. Escrever os testes previamente é a base de desenvolvimento dirigido por testes (TDD, test-driven development), e também acho essa técnica útil para ensinar.
Também escrevi testes de unidade para alguns dos exemplos maiores usando pytest—que acho mais fácil de usar e mais poderoso que o módulo unittest da bibliotexa padrão.
Você vai descobrir que pode verificar a maior parte do código do livro digitando python3 -m doctest example_script.py
ou pytest
no console de seu sistema operacional.
A configuração do pytest.ini, na raiz do repositório do código de exemplo, assegura que doctests são coletados e executados pelo comando pytest
.
Ponto de vista: minha perspectiva pessoal
Venho usando, ensinando e debatendo Python desde 1998, e gosto de estudar e comparar linguagens de programação, seus projetos e a teoria por trás delas. Ao final de alguns capítulos acrescentei uma seção "Ponto de vista", apresentando minha perspectiva sobre Python e outras linguagens. Você pode pular essas partes, se não tiver interesse em tais discussões. Seu conteúdo é inteiramente opcional.
Conteúdo na na Web
Criei dois sites para este livro:
- https://pythonfluente.com
-
O texto integral em português traduzido por Paulo Candido de Oliveira Filho. É que você está lendo agora.
- https://fluentpython.com
-
Contém textos em inglês para ambas edições do livro, além de um glossário. É um material que eu cortei para não ultrapassar o limite de 1.000 páginas.
O repositório de exemplos de código está no GitHub.
Convenções usadas no livro
As seguintes convenções tipográficas são usadas neste livro:
- Itálico
-
Indica novos termos, URLs, endereços de email, nomes e extensões de arquivos [3].
Espaçamento constante
-
Usado para listagens de programas, bem como dentro de parágrafos para indicar elementos programáticos tais como nomes de variáveis ou funções, bancos de dados, tipos de dados, variáveis do ambiente, instruções e palavras-chave.
Observe que quando uma quebra de linha cai dentro de um termo de
espaçamento constante
, o hífen não é utilizado—pois ele poderia ser erroneamente entendido como parte do termo. Espaçamento constante em negrito
-
Mostra comandos ou outro texto que devem ser digitados literalmente pelo usuário.
Espaçamento constante em itálico
-
Mostra texto que deve ser substituído por valores fornecidos pelo usuário ou por valores determinados pelo contexto.
👉 Dica
|
Esse elemento é uma dica ou sugestão. |
✒️ Nota
|
Este elemento é uma nota ou observação. |
⚠️ Aviso
|
Este elemento é um aviso ou alerta. |
Usando os exemplos de código
Todos os scripts e a maior parte dos trechos de código que aparecem no livro estão disponíveis no repositório de código de Python Fluente, no GitHub.
Se você tiver uma questão técnica ou algum problema para usar o código, por favor mande um email para 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 registrei seus nomes—mil desculpas.
Durante minha pesquisa, aprendi sobre tipagem, concorrência, pattern matching e metaprogramação interagindo com Michael Albert, Pablo Aguilar, Kaleb Barrett, David Beazley, J. S. O. Bueno, Bruce Eckel, Martin Fowler, Ivan Levkivskyi, Alex Martelli, Peter Norvig, Sebastian Rittau, Guido van Rossum, Carol Willing e Jelle Zijlstra.
Os editores da O’Reilly Jeff Bleiel, Jill Leonard e Amelia Blevins fizeram sugestões que melhoraram o fluxo do texto em muitas partes. Jeff Bleiel e o editor de produção Danny Elfanbaum me apoiaram durante essa longa maratona.
As ideias e sugestões de cada um deles tornaram o livro melhor e mais preciso. Inevitavelmente, vão restar erros de minha própria criação no produto final. Me desculpo antecipadamente.
Por fim gostaria de estender meus sinceros agradecimento a meus colegas na Thoughtworks Brasil—e especialmente a meu mentor, Alexey Bôas—que apoiou este projeto de muitas formas até o fim.
Claro, todos os que me ajudaram a entender Python e a escrever a primeira edição merecem agora agradecimentos em dobro. Não haveria segunda edição sem o sucesso da primeira.
Agradecimentos da primeira edição
O tabuleiro e as peças de xadrez Bauhaus, criadas por Josef Hartwig, são um exemplo de um excelente design: belo, simples e claro. Guido van Rossum, filho de um arquiteto e irmão de projetista de fonte magistral, criou um obra prima de design de linguagens. Adoro ensinar Python porque ele é belo, simples e claro.
Alex Martelli e Anna Ravenscroft foram os primeiros a verem o esquema desse livro, e me encorajaram a submetê-lo à O’Reilly para publicação. Seus livros me ensinaram Python idiomático e são modelos de clareza, precisão e profundidade em escrita técnica. Os 6,200+ posts de Alex no Stack Overflow (EN) são uma fonte de boas ideias sobre a linguagem e seu uso apropriado.
Martelli e Ravenscroft foram também revisores técnicos deste livro, juntamente com Lennart Regebro e Leonardo Rochael. Todos nesta proeminente equipe de revisão técnica têm pelo menos 15 anos de experiência com Python, com muitas contribuições a projetos Python de alto impacto, em contato constante com outros desenvolvedores da comunidade. Em conjunto, eles me enviaram centenas de correções, sugestões, questões e opiniões, acrescentando imenso valor ao livro. Victor Stinner gentilmente revisou o Capítulo 21, trazendo seu conhecimento especializado, como um dos mantenedores do asyncio
, para a equipe de revisão técnica. Foi um grande privilégio e um prazer colaborar com eles por estes muitos meses.
A editora Meghan Blanchette foi uma fantástica mentora, e me ajudou a melhorar a organização e o fluxo do texto do livro, me mostrando que partes estavam monótonas e evitando que eu atrasasse o projeto ainda mais. Brian MacDonald editou os capítulo na Parte II: Funções como objetos quando Meghan estava ausente. Adorei trabalhar com eles e com todos na O’Reilly, incluindo a equipe de suporte e desenvolvimento do Atlas (Atlas é a plataforma de publicação de livros da O’Reilly, que eu tive a felicidade de usar para escrever esse livro).
Mario Domenech Goulart deu sugestões numerosas e detalhadas, desde a primeira versão do livro. Também recebi muitas sugestões e comentários de Dave Pawson, Elias Dorneles, Leonardo Alexandre Ferreira Leite, Bruce Eckel, J. S. Bueno, Rafael Gonçalves, Alex Chiaranda, Guto Maia, Lucas Vido e Lucas Brunialti.
Ao longo dos anos, muitas pessoas me encorajaram a me tornar um autor, mas os mais persuasivos foram Rubens Prates, Aurelio Jargas, Rudá Moura e Rubens Altimari. Mauricio Bussab me abriu muitas portas, incluindo minha primeira experiência real na escrita de um livro. Renzo Nuccitelli apoiou este projeto de escrita o tempo todo, mesmo quando significou iniciar mais lentamente nossa parceria no python.pro.br.
A maravilhosa comunidade brasileira de Python é inteligente, generosa e divertida. O The Python Brasil group tem milhares de membros, e nossas conferências nacionais reúnem centenas de pessoas. Mas os mais influemtes em minha jornada como pythonista foram Leonardo Rochael, Adriano Petrich, Daniel Vainsencher, Rodrigo RBP Pimentel, Bruno Gola, Leonardo Santagada, Jean Ferri, Rodrigo Senra, J. S. Bueno, David Kwast, Luiz Irber, Osvaldo Santana, Fernando Masanori, Henrique Bastos, Gustavo Niemayer, Pedro Werneck, Gustavo Barbieri, Lalo Martins, Danilo Bellini, e Pedro Kroger.
Dorneles Tremea foi um grande amigo, (e incrivelmente generoso com seu tempo e seu conhecimento), um hacker fantástico e o mais inspirador líder da Associação Python Brasil. Ele nos deixou cedo demais.
Meus estudantes, ao longo desses anos, me ensinaram muito através de suas perguntas, ideias, feedbacks e soluções criativas para problemas. Érico Andrei e a Simples Consultoria tornaram possível que eu me concentrasse em ser um professor de Python pela primeira vez.
Martijn Faassen foi meu mentor de Grok e compartilhou ideias valiosas sobre Python e os neandertais. Seu trabalho e o de Paul Everitt, Chris McDonough, Tres Seaver, Jim Fulton, Shane Hathaway, Lennart Regebro, Alan Runyan, Alexander Limi, Martijn Pieters, Godefroid Chapelle e outros, dos planetas Zope, Plone e Pyramid, foram decisivos para minha carreira. Graças ao Zope e a surfar na primeira onda da web, pude começar a ganhar a vida com Python em 1998. José Octavio Castro Neves foi meu sócio na primeira software house baseada em Python do Brasil.
Tenho gurus demais na comunidade Python como um todo para listar todos aqui, mas além daqueles já mencionados, eu tenho uma dívida com Steve Holden, Raymond Hettinger, A.M. Kuchling, David Beazley, Fredrik Lundh, Doug Hellmann, Nick Coghlan, Mark Pilgrim, Martijn Pieters, Bruce Eckel, Michele Simionato, Wesley Chun, Brandon Craig Rhodes, Philip Guo, Daniel Greenfeld, Audrey Roy e Brett Slatkin, por me ensinarem novas e melhores formas de ensinar Python.
A maior parte dessas páginas foi escrita no meu home office e em dois laboratórios: o CoffeeLab e o Garoa Hacker Clube. O CoffeeLab é o quartel general dos geeks cafeinados na Vila Madalena, em São Paulo, Brasil. O Garoa Hacker Clube é um espaço hacker aberto a todos: um laboratório comunitário onde qualquer um é livre para tentar novas ideias.
A comunidade Garoa me forneceu inspiração, infraestrutura e distração. Acho que Aleph gostaria desse liro.
Minha mãe, Maria Lucia, e meu pai, Jairo, sempre me apoiaram de todas as formas. Gostaria que ele estivesse aqui para ver esse livro; e fico feliz de poder compartilhá-lo com ela.
Minha esposa, Marta Mello, suportou 15 meses de um marido que estava sempre trabalhando, mas continuou me apoiando e me guiando através dos momentos mais críticos do projeto, quando temi que poderia abandonar a maratona.
Agradeço a todos vocês, por tudo.
Sobre esta tradução
Python Fluente, Segunda Edição é uma tradução direta de Fluent Python, Second Edition (O’Reilly, 2022). Não é uma obra derivada de Python Fluente (Novatec, 2015).
A presente tradução foi autorizada pela O’Reilly Media para distribuição nos termos da licença CC BY-NC-ND. Os arquivos-fonte em formato Asciidoc estão no repositório público https://github.com/pythonfluente/pythonfluente2e.
Enquanto publicávamos a tradução ao longo de 2023, muitas correções foram enviadas por leitores como issues (defeitos) ou pull requests (correções) no repositório. Agradeceço a todas as pessoas que colaboraram!
✒️ Nota
|
Correções e sugestões de melhorias são bem vindas! Para contribuir, veja os issues no repositório https://github.com/pythonfluente/pythonfluente2e. Contamos com sua colaboração. 🙏 |
Histórico das traduções
Escrevi a primeira e a segunda edições deste livro originalmente em inglês, para serem mais facilmente distribuídas no mercado internacional.
Cedi os direitos exclusivos para a O’Reilly Media, nos termos usuais de contratos com editoras famosas: elas ficam com a maior parte do lucro, o direito de publicar, e o direito de vender licenças para tradução em outros idiomas.
Até 2022, a primeira edição foi publicada nesses idiomas:
-
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 de Python
O senso estético de Guido para o design de linguagens é incrível. Conheci muitos projetistas capazes de criar linguagens teoricamente lindas, que ninguém jamais usaria. Mas Guido é uma daquelas raras pessoas capaz de criar uma linguagem só um pouco menos teoricamente linda que, por isso mesmo, é uma delícia para programar. [4]
criador do Jython, co-criador do AspectJ, e arquiteto do .Net DLR—Dynamic Language Runtime
Uma das melhores qualidades de Python é sua consistência. Após trabalhar com Python por algum tempo é possível intuir, de modo informado e correto, o funcionamento de recursos que você acabou de conhecer.
Entretanto, se você aprendeu outra linguagem orientada a objetos antes de Python,
pode achar estranho usar len(collection)
em vez de collection.len()
.
Essa aparente esquisitice é a ponta de um iceberg que, quando bem compreendido,
é a chave para tudo aquilo que chamamos de pythônico.
O iceberg se chama o Modelo de Dados de Python,
e é a API que usamos para fazer nossos objetos lidarem bem com os aspectos mais idiomáticos da linguagem.
É possível pensar no modelo de dados como uma descrição de Python na forma de um framework. Ele formaliza as interfaces dos elementos constituintes da própria linguagem, como sequências, funções, iteradores, corrotinas, classes, gerenciadores de contexto e assim por diante.
Quando usamos um framework,
passamos um bom tempo programando métodos que são chamados pelo framework,
e não pelas nossas classes.
O mesmo acontece quando nos valemos do Modelo de Dados de Python para criar novas classes.
O interpretador de Python invoca
métodos especiais para realizar operações básicas sobre os objetos,
muitas vezes acionadas por uma sintaxe especial.
Os
nomes dos métodos especiais são sempre precedidos e seguidos de dois sublinhados.
Por exemplo, a sintaxe obj[key]
está amparada no método especial __getitem__
.
Para resolver my_collection[key]
, o interpretador chama my_collection.__getitem__(key)
.
Implementamos métodos especiais quando queremos que nossos objetos suportem e interajam com elementos fundamentais da linguagem, tais como:
-
Coleções
-
Acesso a atributos
-
Iteração (incluindo iteração assíncrona com
async for
) -
Sobrecarga (overloading) de operadores
-
Invocação de funções e métodos
-
Representação e formatação de strings
-
Programação assíncrona usando
await
-
Criação e destruição de objetos
-
Contextos gerenciados usando as instruções
with
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 de Python, que é muito estável. As mudanças mais significativas foram:
-
Métodos especiais que suportam programação assíncrona e outras novas funcionalidades foram acrescentados às tabelas em Seção 1.4.
-
A Figura 2, mostrando o uso de métodos especiais em Seção 1.3.4, incluindo a classe base abstrata
collections.abc.Collection
, introduzida n Python 3.6.
Além disso, aqui e por toda essa segunda edição,
adotei a sintaxe f-string, introduzida n Python 3.6,
que é mais legível e muitas vezes mais conveniente que as notações de formatação de strings mais antigas:
o
método str.format()
e o operador %
.
👉 Dica
|
Existe ainda uma razão para usar |
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 de Python,
uma instância de FrenchDeck
responde à
função len()
,
devolvendo o número de cartas naquele baralho:
>>> deck = FrenchDeck()
>>> len(deck)
52
Ler cartas específicas do baralho é fácil, graças ao método __getitem__
.
Por exemplo, a primeira e a última carta:
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
Deveríamos criar um método para obter uma carta aleatória? Não é necessário.
Python já tem uma função que devolve um item aleatório de uma sequência: random.choice
.
Podemos usá-la em uma instância de FrenchDeck
:
>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>> choice(deck)
Card(rank='2', suit='clubs')
Acabamos de ver duas vantagens de usar os métodos especiais no contexto do Modelo de Dados de Python.
-
Os usuários de suas classes não precisam memorizar nomes arbitrários de métodos para operações comuns ("Como obter o número de itens? É
.size()
,.length()
ou outra coisa?") -
É mais fácil de aproveitar a rica biblioteca padrão de Python e evitar reinventar a roda, como no caso da função
random.choice
.
Mas não é só isso.
Como nosso __getitem__
usa o
operador []
de self._cards
,
nosso baralho suporta fatiamento automaticamente.
Podemos olhar as três primeiras cartas no topo de um baralho,
e depois pegar só os ases, iniciando com o índice 12 e pulando 13 cartas por vez:
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
E como já temos o método especial __getitem__
, nosso baralho é um objeto iterável,
ou seja, pode ser percorrido em um laço for
:
>>> for card in deck: # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...
Também podemos iterar sobre o baralho na ordem inversa:
>>> for card in reversed(deck): # doctest: +ELLIPSIS
... print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...
✒️ Nota
|
Reticências nos doctests
Sempre que possível,
extraí as listagens do console de Python usadas neste livro com o
Nesse casos, usei a diretiva |
A iteração muitas vezes é implícita.
Python invoca o método __contains__
da coleção para tratar o operador in
: student in team
.
Mas se a coleção não fornece um método __contains__
,
o operador in
realiza uma busca sequencial.
No nosso caso, in
funciona com nossa classe FrenchDeck
porque ela é iterável.
Veja a seguir:
>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False
E o ordenamento?
Um sistema comum de ordenar cartas é por seu valor numérico (ases sendo os mais altos) e depois por naipe,
na ordem espadas (o mais alto), copas, ouros e paus (o mais baixo).
Aqui está uma função que ordena as cartas com essa regra,
devolvendo 0
para o 2 de paus e 51
para o Ás de espadas.
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]
Podemos agora listar nosso baralho em ordem crescente de usando spades_high
como critério de ordenação:
>>> for card in sorted(deck, key=spades_high): # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (46 cards omitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')
Apesar da FrenchDeck
herdar implicitamente da classe object
,
a maior parte de sua funcionalidade não é herdada, vem do modelo de dados e de composição.
Ao implementar os métodos especiais __len__
e __getitem__
,
nosso FrenchDeck
se comporta como uma sequência Python padrão,
podendo assim se beneficiar de recursos centrais da linguagem (por exemplo, iteração e fatiamento),
e da biblioteca padrão, como mostramos nos exemplos usando random.choice
,
reversed
, e sorted
.
Graças à composição,
as implementações de __len__
e __getitem__
podem delegar todo o trabalho para um objeto list
,
especificamente self._cards
.
✒️ Nota
|
E como embaralhar as cartas?
Como foi implementado até aqui, um |
1.3. Como os métodos especiais são utilizados
A
primeira coisa para se saber sobre os métodos especiais é que eles foram feitos para
serem chamados pelo interpretador Python, e não por você.
Você não escreve my_object.__len__()
.
Escreve len(my_object)
e, se my_object
é uma instância de uma classe definida por você,
entã Python chama o método __len__
que você implementou.
Mas o interpretador pega um atalho quando está lidando com um tipo embutido como list
, str
, bytearray
,
ou extensões compiladas como os arrays do NumPy.
As coleções de tamanho variável de Python escritas em C incluem uma
struct[5]
chamada PyVarObject
, com um campo ob_size
que mantém o número de itens na coleção.
Então, se my_object
é uma instância de algum daqueles tipos embutidos,
len(my_object)
lê o valor do campo ob_size
,
e isso é muito mais rápido que chamar um método.
Na maior parte das vezes, a chamada a um método especial é implícita.
Por exemplo, o comando for i in x:
na verdade gera uma invocação de iter(x)
,
que por sua vez pode chamar x.__iter__()
se esse método estiver disponível,
ou usar x.__getitem__()
, como no exemplo do FrenchDeck
.
Em condições normais, seu código não deveria conter muitas chamadas diretas a métodos especiais.
A menos que você esteja fazendo muita metaprogramação,
implementar métodos especiais deve ser muito mais frequente que invocá-los explicitamente.
O
único método especial que é chamado frequentemente pelo seu código é __init__
,
para invocar o método de inicialização da superclasse na implementação do seu próprio __init__
.
Geralmente, se você precisa invocar um método especial,
é melhor chamar a função embutida relacionada (por exemplo, len
, iter
, str
, etc.).
Essas funções chamam o método especial correspondente,
mas também fornecem outros serviços e—para tipos embutidos—são mais rápidas que chamadas a métodos.
Veja, por exemplo, Seção 17.3.1 no Capítulo 17.
Na próxima seção veremos alguns dos usos mais importantes dos métodos especiais:
-
Emular tipos numéricos
-
Representar objetos na forma de strings
-
Determinar o valor booleano de um objeto
-
Implementar coleções
1.3.1. Emulando tipos numéricos
Vários
métodos especiais permitem que objetos criados pelo usuário respondam a operadores como +
.
Vamos tratar disso com mais detalhes no Capítulo 16.
Aqui nosso objetivo é continuar ilustrando o uso dos métodos especiais, através de outro exemplo simples.
Vamos implementar uma classe para representar vetores bi-dimensionais—isto é, vetores euclidianos como aqueles usados em matemática e física (veja a Figura 1).
👉 Dica
|
O tipo embutido |
Vector(2, 4) + Vector(2, 1)
devolve Vector(4, 5)
.Vamos começar a projetar a API para essa classe escrevendo em uma sessão de console simulada, que depois podemos usar como um doctest. O trecho a seguir testa a adição de vetores ilustrada na Figura 1:
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)
Observe como o operador +
produz um novo objeto Vector(4, 5)
.
A função embutida abs
devolve o valor absoluto de números inteiros e de ponto flutuante, e a magnitude de números complex
.
Então, por consistência, nossa API também usa abs
para calcular a magnitude de um vetor:
>>> v = Vector(3, 4)
>>> abs(v)
5.0
Podemos
também implementar o operador *
, para realizar multiplicação escalar
(isto é, multiplicar um vetor por um número para obter um novo vetor de mesma direção e magnitude multiplicada):
>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0
O Exemplo 2 é uma classe Vector
que implementa as operações descritas acima,
usando os métodos especiais
__repr__
, __abs__
, __add__
, e __mul__
.
"""
vector2d.py: a simplistic class demonstrating some special methods
It is simplistic for didactic reasons. It lacks proper error handling,
especially in the ``__add__`` and ``__mul__`` methods.
This example is greatly expanded later in the book.
Addition::
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)
Absolute value::
>>> v = Vector(3, 4)
>>> abs(v)
5.0
Scalar multiplication::
>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0
"""
import math
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f'Vector({self.x!r}, {self.y!r})'
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
Implementamos cinco métodos especiais, além do costumeiro __init__
.
Veja que nenhum deles é chamado diretamente dentro da classe ou durante seu uso normal,
ilustrado pelos doctests.
Como mencionado antes, o interpretador Python é o único usuário frequente da maioria dos métodos especiais.
O Exemplo 2 implementa dois operadores: +
e *
,
para demonstrar o uso básico de __add__
e __mul__
.
No dois casos, os métodos criam e devolvem uma nova instância de Vector
,
e não modificam nenhum dos operandos: self
e other
são apenas lidos.
Esse é o comportamento esperado de operadores infixos: criar novos objetos e não tocar em seus operandos.
Vou falar muito mais sobre esse tópico no Capítulo 16.
⚠️ Aviso
|
Da forma como está implementado, o Exemplo 2 permite multiplicar um |
Nas seções seguintes vamos discutir os outros métodos especiais em
Vector
.
1.3.2. Representação de strings
O
método especial __repr__
é chamado pelo repr
embutido para obter a representação do objeto como uma string,
para exibir ou imprimir.
Sem um __repr__
personalizado, o console de Python mostraria uma instância de Vector
como
<Vector object at 0x10e100070>
.
O console iterativo e o depurador chamam repr
para exibir o resultado das expressões.
O repr
também é usado:
-
Pelo marcador posicional
%r
na formatação clássica com o operador%
. Ex.:'%r' % my_obj
-
Pelo sinalizador de conversão
!r
na nova sintaxe de strings de formato usada nas f-strings e no mé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 o construtor espera que os argumentos sejam números, não str
.
A string devolvida por __repr__
não deve ser ambígua e, se possível,
deve corresponder ao código-fonte necessário para recriar o objeto representado.
É por isso que nossa representação de Vector
se parece com uma chamada ao construtor da classe,
por exemplo Vector(3, 4)
.
Por outro lado,
__str__
é chamado pelo método embutido str()
e usado implicitamente pela função print
.
Ele deve devolver uma string apropriada para ser exibida aos usuários finais.
Algumas vezes a própria string devolvida por __repr__
é adequada para exibir ao usuário,
e você não precisa programar __str__
,
porque a implementação de __str__
herdada da classe object
já invoca __repr__
.
O Exemplo 66 é um dos muitos exemplos neste livro com um __str__
personalizado.
👉 Dica
|
Programadores com experiência anterior em linguagens que contém o método "What is the difference between |
1.3.3. O valor booleano de um tipo personalizado
Apesar
de Python ter um tipo bool
, ele aceita qualquer objeto em um contexto booleano,
tal como as expressões controlando instruções if
ou while
, ou como operandos de and
, or
e not
.
Para determinar se um valor x
é verdadeiro ou falso, Python invoca bool(x)
,
que devolve somente True
ou False
.
Por default, instâncias de classes definidas pelo usuário são consideradas verdadeiras,
a menos que __bool__
ou __len__
estejam implementadas.
Basicamente, bool(x)
chama x.__bool__()
e usa o resultado.
Se __bool__
não está implementado, Python tenta invocar x.__len__()
,
e se esse último devolver zero, bool
devolve False
.
Caso contrário, bool
devolve True
.
Nossa implementação de __bool__
é conceitualmente simples:
ela devolve False
se a magnitude do vetor for zero, caso contrário devolve True
.
Convertemos a magnitude para um valor booleano usando bool(abs(self))
,
porque espera-se que __bool__
devolva um booleano.
Fora dos métodos __bool__
, raramente é necessário chamar bool()
explicitamente,
porque qualquer objeto pode ser usado em um contexto booleano.
Observe que o método especial __bool__
permite que seus objetos sigam as
regras de teste do valor verdade definidas no
capítulo "Tipos Embutidos"
da documentação da Biblioteca Padrão de Python.
✒️ Nota
|
Essa é uma implementação mais rápida de
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 é sigla para Abstract Base Class).
As ABCs e o módulo collections.abc
são tratados no Capítulo 13.
O objetivo dessa pequena seção é dar uma visão panorâmica das interfaces das coleções mais importantes de Python,
mostrando como elas são criadas a partir de métodos especiais.
list
e dict
. O restante dos métodos tem implementações concretas, então as subclasses podem herdá-los.Cada uma das ABCs no topo da hierarquia tem um único método especial.
A ABC Collection
(introduzida n Python 3.6) unifica as três interfaces essenciais,
que toda coleção deveria implementar:
-
Iterable
, para suportarfor
, desempacotamento, e outras formas de iteração -
Sized
para suportar a função embutidalen
-
Container
para suportar o operadorin
Na verdade, Python não exige que classes concretas herdem de qualquer dessas ABCs.
Qualquer classe que implemente __len__
satisfaz a interface Sized
.
Três especializações muito importantes de Collection
são:
-
Sequence
, formalizando a interface de tipos embutidos 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 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 de Python.
1.4. Visão geral dos métodos especiais
O capítulo "Modelo de Dados" de A Referência da Linguagem Python lista mais de 80 nomes de métodos especiais. Mais da metade deles implementa operadores aritméticos, de comparação, ou bit-a-bit. Para ter uma visão geral do que está disponível, veja tabelas a seguir.
A Tabela 1 mostra nomes de métodos especiais, excluindo aqueles usados para implementar
operadores infixos ou funções matemáticas fundamentais como abs
.
A maioria desses métodos será tratado ao longo do livro, incluindo as adições mais recentes:
métodos especiais assíncronos como __anext__
(acrescentado n Python 3.5), e o método de personalização de classes, __init_subclass__
(d Python 3.6).
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 n Python 3.5 para suportar o uso de @
como um operador infixo de multiplicação de matrizes, como veremos no 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
|
Python invoca um método especial de operador reverso no segundo argumento quando o método especial correspondente não pode ser usado no primeiro operando. Atribuições aumentadas são atalho combinando um operador infixo com uma atribuição de variável, por exemplo `a += b`. O Capítulo 16 explica em detalhes os operadores reversos e a atribuição aumentada. |
1.5. Por que len não é um método?
Em 2013, fiz
essa pergunta a Raymond Hettinger, um dos desenvolvedores principais do Python,
e o núcleo de sua resposta era uma citação do "The Zen of Python" (O Zen do Python) (EN): "a praticidade vence a pureza."
Na Seção 1.3, descrevi como len(x)
roda muito rápido quando x
é uma instância de um tipo embutido.
Nenhum método é chamado para os objetos embutidos do CPython: o tamanho é simplesmente lido de um campo em uma struct C.
Obter o número de itens em uma coleção é uma operação comum, e precisa funcionar de forma eficiente para tipos tão básicos e diferentes como
str
, list
, memoryview
, e assim por diante.
Em outras palavras, len
não é chamado como um método porque recebe um tratamento especial como parte do Modelo de Dados de Python, da mesma forma que abs
.
Mas graças ao método especial __len__
, também é possível fazer len
funcionar com nossos objetos personalizados.
Isso é um compromisso justo entre a necessidade de objetos embutidos eficientes e a consistência da linguagem.
Também de "O Zen de Python": "Casos especiais não são especiais o bastante para quebrar as regras."
✒️ Nota
|
Pensar em |
1.6. Resumo do capítulo
Ao implementar métodos especiais, seus objetos podem se comportar como tipos embutidos, permitindo o estilo de programação expressivo que a comunidade considera pythônico.
Uma exigência básica para um objeto em Python é fornecer strings representando a si mesmo que possam ser usadas, uma para depuração e registro (log), outra para apresentar aos usuários finais. É para isso que os métodos especiais __repr__
e __str__
existem no modelo de dados.
Emular sequências, como mostrado com o exemplo do FrenchDeck
, é um dos usos mais comuns dos métodos especiais.
Por exemplo, bibliotecas de banco de dados frequentemente devolvem resultados de consultas na forma de coleções similares a sequências.
Tirar o máximo proveito dos tipos de sequências existentes é o assunto do Capítulo 2.
Como implementar suas próprias sequências será visto na Capítulo 12,
onde criaremos uma extensão multidimensional da classe Vector
.
Graças à sobrecarga de operadores, Python oferece uma rica seleção de tipos numéricos, desde os tipos embutidos até decimal.Decimal
e fractions.Fraction
,
todos eles suportando operadores aritméticos infixos.
As bibliotecas de ciência de dados NumPy suportam operadores infixos com matrizes e tensores. A implementação de operadores—incluindo operadores reversos e atribuição aumentada—será vista no Capítulo 16, usando melhorias do exemplo Vector
.
Também veremos o uso e a implementação da maioria dos outros métodos especiais do Modelo de Dados de Python ao longo deste livro.
1.7. Para saber mais
O capítulo "Modelo de Dados" em A Referência da Linguagem Python é a fonte canônica para o assunto desse capítulo e de uma boa parte deste livro.
Python in a Nutshell, 3rd ed. (EN), de Alex Martelli, Anna Ravenscroft, e Steve Holden (O’Reilly) tem uma excelente cobertura do modelo de dados. Sua descrição da mecânica de acesso a atributos é a mais competente que já vi, perdendo apenas para o próprio código-fonte em C do CPython. Martelli também é um contribuidor prolífico do Stack Overflow, com mais de 6200 respostas publicadas. Veja seu perfil de usuário no Stack Overflow.
David Beazley tem dois livros tratando do modelo de dados em detalhes, no contexto de Python 3: Python Essential Reference (EN), 4th ed. (Addison-Wesley), e Python Cookbook, 3rd ed (EN) (O’Reilly), colaborando com Brian K. Jones.
The Art of the Metaobject Protocol (EN) (MIT Press) de Gregor Kiczales, Jim des Rivieres, e Daniel G. Bobrow explica o conceito de um protocolo de metaobjetos, do qual o Modelo de Dados de Python é um exemplo.
2. Uma coleção de sequências
Como vocês podem ter notado, várias das operações mencionadas funcionam da mesma forma com textos, listas e tabelas. Coletivamente, textos, listas e tabelas são chamados de 'trens' (trains). [...] O comando `FOR` também funciona, de forma geral, em trens.
Leo Geurts, Lambert Meertens, e Steven Pembertonm, ABC Programmer's Handbook, p. 8. (Bosko Books)
Antes de criar Python, Guido foi um dos desenvolvedores da linguagem ABC—um projeto de pesquisa de 10 anos para criar um ambiente de programação para iniciantes. A ABC introduziu várias ideias que hoje consideramos "pythônicas": operações genéricas com diferentes tipos de sequências, tipos tupla e mapeamento embutidos, estrutura de blocos por indentação, tipagem forte sem declaração de variáveis, entre outras. Python não é assim tão amigável por acidente.
Python herdou da ABC o tratamento uniforme de sequências. Strings, listas, sequências de bytes, arrays, elementos XML e resultados vindos de bancos de dados compartilham um rico conjunto de operações comuns, incluindo iteração, fatiamento, ordenação e concatenação.
Entender a variedade de sequências disponíveis no Python evita que reinventemos a roda, e sua interface comum nos inspira a criar APIs que suportem e aproveitem bem os tipos de sequências existentes e futuras.
A maior parte da discussão deste capítulo se aplica às sequências em geral,
desde a conhecida list
até os tipos str
e bytes
, adicionados no Python 3.
Tópicos específicos sobre listas, tuplas, arrays e filas também foram incluídos,
mas os detalhes sobre strings Unicode e sequências de bytes são tratados no Capítulo 4.
Além disso, a ideia aqui é falar sobre os tipos de sequências prontas para usar.
A criação de novos tipos de sequência é o tema do Capítulo 12.
Os principais tópicos cobertos neste capítulo são:
-
Compreensão de listas e os fundamentos das expressões geradoras.
-
O uso de tuplas como registros versus o uso de tuplas como listas imutáveis
-
Desempacotamento de sequências e padrões de sequências.
-
Lendo de fatias e escrevendo em fatias
-
Tipos especializados de sequências, tais como arrays e filas
2.1. Novidades neste capítulo
A atualização mais importante desse capítulo é
a Seção 2.6,
primeira abordagem das instruções match/case
introduzidas no Python 3.10.
As outras mudanças não são atualizações e sim aperfeiçoamentos da primeira edição:
-
Um novo diagrama e uma nova descrição do funcionamento interno das sequências, contrastando contêineres e sequências planas.
-
Uma comparação entre
list
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 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 3.
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 4 ajuda a visualizar como as sequências mutáveis herdam todos os métodos
das sequências imutáveis e implementam vários métodos adicionais.
Os tipos embutidos de sequências na verdade não são subclasses das classes base abstratas (ABCs)
Sequence
e MutableSequence
, mas sim subclasses virtuais registradas com aquelas
ABCs—como veremos no Capítulo 13.
Por serem subclasses virtuais, tuple
e list
passam nesses testes:
>>> from collections import abc
>>> issubclass(tuple, abc.Sequence)
True
>>> issubclass(list, abc.MutableSequence)
True
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 3. Entretanto, após aprender sobre as listcomps, acho o Exemplo 4 mais legível, porque deixa sua intenção explícita.
Um loop for
pode ser usado para muitas coisas diferentes:
percorrer uma sequência para contar ou encontrar itens, computar valores agregados (somas, médias),
ou inúmeras outras tarefas.
O código no Exemplo 3 está criando uma lista.
Uma listcomp, por outro lado, é mais clara. Seu objetivo é sempre criar uma nova lista.
Naturalmente, é possível abusar das compreensões de lista para escrever código verdadeiramente incompreensível.
Já vi código Python usando listcomps apenas para repetir um bloco de código por seus efeitos colaterais.
Se você não vai fazer alguma coisa com a lista criada, não deveria usar essa sintaxe.
Além disso, tente manter o código curto. Se uma compreensão ocupa mais de duas linhas,
provavelmente seria melhor quebrá-la ou reescrevê-la como um bom e velho loop for
.
Avalie qual o melhor caminho: em Python, como em português, não existem regras absolutas para se escrever bem.
👉 Dica
|
Dica de sintaxe
Em
código Python, quebras de linha são ignoradas dentro de pares de |
Compreensões de lista criam listas a partir de sequências ou de qualquer outro tipo iterável,
filtrando e transformando os itens.
As funções embutidas filter
e map
podem fazer o mesmo, mas perde-se alguma legibilidade, como veremos a seguir.
2.3.2. Listcomps versus map e filter
Listcomps
fazem tudo que as funções map
e filter
fazem,
sem os malabarismos exigidos pela funcionalidade limitada do lambda
de Python.
Considere o Exemplo 5.
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]
Eu acreditava que map
e filter
eram mais rápidas que as listcomps equivalentes, mas Alex Martelli
assinalou que não é o caso—pelo menos não nos exemplos acima.
O script listcomp_speed.py no
repositório de código de Python Fluente
é um teste de desempenho simples, comparando listcomp com filter/map
.
Vou falar mais sobre map
e filter
no Capítulo 7.
Vamos agora ver o uso de listcomps para computar produtos cartesianos:
uma lista contendo tuplas criadas a partir de todos os itens de duas ou mais listas.
2.3.3. Produtos cartesianos
Listcomps podem criar listas a partir do produto cartesiano de dois ou mais iteráveis. Os itens resultantes de um produto cartesiano são tuplas criadas com os itens de cada iterável na entrada, e a lista resultante tem o tamanho igual ao produto da multiplicação dos tamanhos dos iteráveis usados. Veja a Figura 5.
Por exemplo, imagine que você precisa produzir uma lista de camisetas disponíveis em duas cores e três tamanhos. O Exemplo 6 mostra como produzir tal lista usando uma listcomp. O resultado tem seis itens.
>>> 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 pode usar uma listcomp, mas uma genexp (expressão geradora) economiza memória, pois ela produz itens um de cada vez usando o protocolo iterador, em vez de criar uma lista inteira apenas para alimentar outro construtor.
As genexps usam a mesma sintaxe das listcomps, mas são delimitadas por parênteses em vez de colchetes.
O Exemplo 7 demonstra o uso básico de genexps para criar uma tupla e um array.
>>> 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 2.10.1.
O Exemplo 8 usa uma genexp com um produto cartesiano para
gerar uma relação de camisetas de duas cores em três tamanhos.
Diferente do Exemplo 6,
aquela lista de camisetas com seis itens nunca é criada na memória:
a expressão geradora alimenta o loop for
produzindo um item por vez.
Se as duas listas usadas no produto cartesiano tivessem mil itens cada uma,
usar uma função geradora evitaria o custo de construir uma lista
com um milhão de itens apenas para passar ao loop for
.
>>> 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 é criada neste exemplo.
✒️ Nota
|
O Capítulo 17 explica em detalhes o funcionamento de geradoras. A ideia aqui é apenas mostrar o uso de expressões geradores para inicializar sequências diferentes de listas, ou produzir uma saída que não precise ser mantida na memória. |
Vamos agora estudar outra sequência fundamental de Python: a tupla.
2.4. Tuplas não são apenas listas imutáveis
Alguns textos introdutórios de Python apresentam as tuplas como "listas imutáveis", mas isso é subestimá-las. Tuplas tem duas funções: elas podem ser usada como listas imutáveis e também como registros sem nomes de campos. Esse uso algumas vezes é negligenciado, então vamos começar por ele.
2.4.1. Tuplas como registros
Tuplas podem conter registros: cada item na tupla contém os dados de um campo, e a posição do item indica seu significado.
Se você pensar em uma tupla apenas como uma lista imutável, a quantidade e a ordem dos elementos pode ser importante ou não, dependendo do contexto. Mas quando usamos uma tupla como uma coleção de campos, o número de itens em geral é fixo e sua ordem é sempre importante.
O Exemplo 9 mostras tuplas usadas como registros. Observe que, em todas as expressões, ordenar a tupla destruiria a informação, pois o significado de cada campo é dado por sua posição na tupla.
>>> 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 9, atribuímos
('Tokyo', 2003, 32_450, 0.66, 8014)
a city, year, pop, chg, area
em um único comando.
E daí o operador %
atribuiu cada item da tupla passport
para a posição correspondente
da string de formato no argumento print
.
Esses foram dois exemplos de desempacotamento de tuplas.
✒️ Nota
|
O termo "desempacotamento de tuplas" (tuple unpacking) é muito usado entre os pythonistas, mas desempacotamento de iteráveis é mais preciso e está ganhando popularidade, como no título da PEP 3132 — Extended Iterable Unpacking (Desempacotamento Estendido de Iteráveis). A Seção 2.5 fala muito mais sobre desempacotamento, não apenas de tuplas, mas também de sequências e iteráveis em geral. |
Agora vamos considerar o uso da classe tuple
como uma variante imutável da classe
list
.
2.4.2. Tuplas como listas imutáveis
O interpretador Python e a biblioteca padrão fazem uso extensivo das tuplas como listas imutáveis, e você deve seguir o exemplo. Isso traz dois benefícios importantes:
- Clareza
-
Quando você vê uma
tuple
no código, sabe que seu tamanho nunca mudará. - Desempenho
-
Uma
tuple
usa menos memória que 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 uma tupla não podem ser apagadas ou substituídas.
Mas se uma daquelas referências apontar para um objeto mutável, e aquele objeto mudar, então o valor da tuple
muda.
O próximo trecho de código ilustra esse fato criando duas tuplas—a
e b
— que inicialmente são iguais.
A Figura 6 representa a disposição inicial da tupla b
na memória.
Quando o último item em b
muda, b
e a
se tornam diferentes:
>>> a = (10, 'alpha', [1, 2])
>>> b = (10, 'alpha', [1, 2])
>>> a == b
True
>>> b[-1].append(99)
>>> a == b
False
>>> b
(10, 'alpha', [1, 2, 99])
Tuplas com itens mutáveis podem ser uma fonte de bugs.
Se uma tupla contém qualquer item mutável,
ela não pode ser usada como chave em um dict
ou como elemento em um set
.
O motivo será explicado em Seção 3.4.1.
Se você quiser determinar explicitamente se uma tupla (ou qualquer outro objeto) tem um valor fixo,
pode usar a função embutida hash
para criar uma função fixed
, assim:
>>> def fixed(o):
... try:
... hash(o)
... except TypeError:
... return False
... return True
...
>>> tf = (10, 'alpha', (1, 2))
>>> tm = (10, 'alpha', [1, 2])
>>> fixed(tf)
True
>>> fixed(tm)
False
Vamos aprofundar essa questão em Seção 6.3.2.
Apesar dessa ressalva, as tuplas são frequentemente usadas como listas imutáveis. Elas oferecem algumas vantagens de desempenho, explicadas por uma dos desenvolvedores principais de Python, Raymond Hettinger, em uma resposta à questão "Are tuples more efficient than lists in Python?" (As tuplas são mais eficientes que as listas no Python?) no StackOverflow. Em resumo, Hettinger escreveu:
-
Para avaliar uma tupla literal, o compilador Python gera bytecode para uma constante tupla em uma operação; mas para um literal lista, o bytecode gerado insere cada elemento como uma constante separada na pilha, e então cria a lista.
-
Dada a tupla
t
,tuple(t)
simplesmente devolve uma referência para a 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
reservam memória adicional, para amortizar o custo de acréscimos futuros. -
As referências para os itens em uma tupla são armazenadas em um array na struct da tupla, enquanto uma lista mantém um ponteiro para um array de referências armazenada em outro lugar. Essa indireção é necessária porque, quando a lista cresce além do espaço alocado naquele momento, Python precisa realocar o array de referências para criar espaço. A indireção adicional torna o cache da CPU menos eficiente.
2.4.3. Comparando os métodos de tuplas e listas
Quando
usamos uma tupla como uma variante imutável de list
, é bom saber o quão similares são suas APIs.
Como se pode ver na Tabela 3,
tuple
suporta todos os métodos de list
que não envolvem adicionar ou remover itens,
com uma exceção—tuple
não possui o método __reversed__
.
Entretanto, isso é só uma otimização; reversed(my_tuple)
funciona sem esse método.
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[7] |
|
● |
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 acessar itens de sequências,
um processo desnecessário e vulnerável a erros.
Além disso, o desempacotamento funciona tendo qualquer objeto iterável como fonte de dados—incluindo iteradores,
que não suportam a notação de índice ([]
).
O único requisito é que o iterável produza exatamente um item por variável do lado esquerdo da atribuição,
a menos que você use um asterisco (*
) para capturar os itens em excesso, como explicado na Seção 2.5.1.
A forma mais visível de desempacotamento é a atribuição paralela; isto é, atribuir itens de um iterável a uma tupla de variáveis, como vemos nesse exemplo:
>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # unpacking
>>> latitude
33.9425
>>> longitude
-118.408056
Uma aplicação elegante de desempacotamento é permutar os valores de variáveis sem usar uma variável temporária:
>>> b, a = a, b
Outro exemplo de desempacotamento é prefixar um argumento com *
ao chamar uma função:
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4)
O código acima mostra outro uso do desempacotamento:
permitir que funções devolvam múltiplos valores de forma conveniente para quem as chama.
Em ainda outro exemplo, a função os.path.split()
cria uma tupla (path, last_part)
a partir de um caminho do sistema de arquivos:
>>> import os
>>> _, filename = os.path.split('/home/luciano/.ssh/id_rsa.pub')
>>> filename
'id_rsa.pub'
Outra forma de usar apenas alguns itens quando desempacotando é com a sintaxe *
, que veremos a seguir.
2.5.1. Usando * para recolher itens em excesso
Definir
parâmetros de função com *args
para capturar argumentos arbitrários em excesso é um recurso clássico de Python.
No Python 3, essa ideia foi estendida para se aplicar também à atribuição paralela:
>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])
No contexto da atribuição paralela, o prefixo *
pode ser aplicado a exatamente uma variável,
mas pode aparecer em qualquer posição:
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)
2.5.2. Desempacotando com * em chamadas de função e sequências literais
A PEP 448—Additional Unpacking Generalizations (Generalizações adicionais de desempacotamento) (EN) introduziu uma sintaxe mais flexível para desempacotamento iterável, melhor resumida em "O que há de novo no Python 3.5" (EN).
Em chamadas de função, podemos usar *
múltiplas vezes:
>>> def fun(a, b, c, d, *rest):
... return a, b, c, d, rest
...
>>> fun(*[1, 2], 3, *range(4, 7))
(1, 2, 3, 4, (5, 6))
O *
pode também ser usado na definição de literais list
, tuple
, ou set
, como
visto nesses exemplos de
"O que há de novo no Python 3.5" (EN):
>>> *range(4), 4
(0, 1, 2, 3, 4)
>>> [*range(4), 4]
[0, 1, 2, 3, 4]
>>> {*range(4), 4, *(5, 6, 7)}
{0, 1, 2, 3, 4, 5, 6, 7}
A PEP 448 introduziu uma nova sintaxe similar para **
, que veremos na Seção 3.2.2.
Por fim, outro importante aspecto do desempacotamento de tuplas: ele funciona com estruturas aninhadas.
2.5.3. Desempacotamento aninhado
O alvo de um desempacotamento pode usar aninhamento,
por exemplo (a, b, (c, d))
.
Python fará a coisa certa se o valor tiver a mesma estrutura aninhada.
O Exemplo 10 mostra o desempacotamento aninhado em ação.
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 10 é:
| latitude | longitude
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
São Paulo | -23.5478 | -46.6358
O alvo da atribuição de um desempacotamento pode também ser uma lista, mas bons casos de uso aqui são raros.
Aqui está o único que conheço: se você tem uma consulta de banco de dados que devolve um único registro
(por exemplo, se o código SQL tem a instrução LIMIT 1
),
daí é possível desempacotar e ao mesmo tempo se assegurar que há apenas um resultado com o seguinte código:
>>> [record] = query_returning_single_row()
Se o registro contiver apenas um campo, é possível obtê-lo diretamente, assim:
>>> [[field]] = query_returning_single_row_with_single_field()
Ambos os exemplos acima podem ser escritos com tuplas, mas não esqueça da peculiaridade sintática,
tuplas com um único item devem ser escritas com uma vírgula final.
Então o primeiro alvo seria (record,)
e o segundo ((field,),)
.
Nos dois casos, esquecer aquela vírgula causa um bug
silencioso.[9]
Agora vamos estudar pattern matching, que suporta maneiras ainda mais poderosas para desempacotar sequências.
2.6. Pattern matching com sequências
O
novo recurso mais visível de Python 3.10 é o pattern matching
(casamento de padrões) com a instrução match/case
, proposta na
PEP 634—Structural Pattern Matching: Specification (Casamento Estrutural de Padrões: Especificação) (EN).
✒️ Nota
|
Carol Willing, uma das desenvolvedoras principais de Python, escreveu uma excelente introdução ao pattern matching na seção "Casamento de padrão estrutural"[10] em "O que há de novo no Python 3.10". Você pode querer ler aquela revisão rápida. Neste livro, dividi o tratamento do casamento de padrões em diferentes capítulos, dependendo dos tipos de padrão: Na Seção 3.3 e na Seção 5.8. E há um exemplo mais longo na Seção 18.3. |
Vamos ao primeiro exemplo do tratamento de sequências com match/case
.
Imagine que você está construindo um robô que aceita comandos, enviados como sequências de palavras e números, como BEEPER 440 3
.
Após separar o comando em partes e analisar os números, você teria uma mensagem como ['BEEPER', 440, 3]
.
Então, você poderia usar um método assim para interpretar mensagens naquele formato:
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 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, 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.[11]
Uma melhoria fundamental do match
sobre o switch
é a desestruturação—uma forma mais avançada de desempacotamento.
Desestruturação é uma palavra nova no vocabulário de Python,
mas é usada com frequência na documentação de linguagens
que suportam o pattern matching—como Scala e Elixir.
Como um primeiro exemplo de desestruturação, o Exemplo 12 mostra parte do Exemplo 10 reescrito com match/case
.
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 12
casa com uma sequência de quatro itens, e o último item tem que ser uma sequência de dois itens.
Padrões de sequência podem ser escritos como tuplas e listas, mas a sintaxe usada não faz diferença: em um padrão de sequência, colchetes e parênteses tem o mesmo significado. Escrevi o padrão como uma lista com uma tupla aninhada de dois itens para evitar a repetição de colchetes ou parênteses no Exemplo 12.
Um padrão de sequência pode casar com instâncias da maioria das subclasses reais ou virtuais de collections.abc.Sequence
, com a exceção de str
, bytes
, e bytearray
.
⚠️ Aviso
|
Instâncias de
|
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 12:
match record:
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
O bloco aninhado com o comando print
só será executado se o padrão casar e a expressão guarda for verdadeira.
👉 Dica
|
A desestruturação com padrões é tão expressiva que, algumas vezes, um |
O Exemplo 12 não melhora o Exemplo 10. É apenas um exemplo para contrastar duas formas de fazer a mesma coisa. O próximo exemplo mostra como o pattern matching contribui para a criação de código claro, conciso e eficaz.
2.6.1. Casando padrões de sequência em um interpretador
Peter Norvig, da Universidade de Stanford, escreveu o
lis.py:
um interpretador de um subconjunto do dialeto Scheme da linguagem de programação Lisp, em 132 belas linhas de código Python legível.
Peguei o código fonte de Norvig (publicado sob a licença MIT) e o atualizei para Python 3.10, para exemplificar o pattern matching.
Nessa seção, vamos comparar uma parte fundamental do código de Norvig—que usa if/elif
e desempacotamento—com uma nova versão usando match/case
.
As duas funções principais do lis.py são parse
e evaluate
.[12]
O parser (analisador sintático) recebe as expressões entre parênteses do Scheme e devolve listas Python. Aqui estão dois exemplos:
>>> parse('(gcd 18 45)')
['gcd', 18, 45]
>>> parse('''
... (define double
... (lambda (n)
... (* n 2)))
... ''')
['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]]
O avaliador recebe listas como essas e as executa.
O primeiro exemplo está chamando uma função gcd
com 18
e 45
como argumentos.
Quando executada, ela computa o maior divisor comum (gcd são as iniciais do termo em inglês, _greatest common divisor) dos argumentos (que é 9).
O segundo exemplo está definindo uma função chamada double
com um parâmetro n
.
O corpo da função é a expressão (* n 2)
.
O resultado da chamada a uma função em Scheme é o valor da última expressão no corpo da função chamada.
Nosso foco aqui é a desestruturação de sequências, então não vou explicar as ações do avaliador. Veja a Seção 18.3 para aprender mais sobre o funcionamento do lis.py.
O Exemplo 13 mostra o avaliador de Norvig com algumas pequenas modificações, e abreviado para mostrar apenas os padrões de sequência.
match/case
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
if isinstance(exp, Symbol): # variable reference
return env[exp]
# ... lines omitted
elif exp[0] == 'quote': # (quote exp)
(_, x) = exp
return x
elif exp[0] == 'if': # (if test conseq alt)
(_, test, consequence, alternative) = exp
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
elif exp[0] == 'lambda': # (lambda (parm…) body…)
(_, parms, *body) = exp
return Procedure(parms, body, env)
elif exp[0] == 'define':
(_, name, value_exp) = exp
env[name] = evaluate(value_exp, env)
# ... more lines omitted
Observe como cada instrução elif
verifica o primeiro item da lista, e então desempacota a lista, ignorando o primeiro item.
O uso extensivo do desempacotamento sugere que Norvig é um fã do pattern matching,
mas ele originalmente escreveu aquele código em Python 2 (apesar de agora ele funcionar com qualquer Python 3)
Usando match/case
em Python ≥ 3.10, podemos refatorar evaluate
, como mostrado no Exemplo 14.
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 se 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 14 é mais curto e mais seguro que o Exemplo 13.
2.6.1.1. Padrões alternativos para lambda
Essa é a sintaxe de lambda
no Scheme,
usando a convenção sintática onde
o sufixo …
significa que o elemento pode aparecer zero ou mais vezes:
(lambda (parms…) body1 body2…)
Um padrão simples para o case
de 'lambda'
seria esse:
case ['lambda', parms, *body] if body:
Entretanto, isso casa com qualquer valor na posição parms
,
incluindo o primeiro x
nesse sujeito inválido:
['lambda', 'x', ['*', 'x', 2]]
A lista aninhada após a palavra-chave lambda
do Scheme
contém os nomes do parâmetros formais da função,
e deve ser uma lista mesmo que contenha apenas um elemento.
Ela pode também ser uma lista vazia,
se função não receber parâmetros—como a random.random()
de Python.
No Exemplo 14, tornei o padrão de 'lambda'
mais seguro usando um padrão de sequência aninhado:
case ['lambda', [*parms], *body] if body:
return Procedure(parms, body, env)
Em um padrão de sequência, o *
pode aparecer apenas uma vez por sequência.
Aqui temos duas sequências: a externa e a interna.
Acrescentando os caracteres [*]
em torno de parms
fez o padrão mais parecido com a sintaxe do Scheme da qual ele trata,
e nos deu uma verificação estrutural adicional.
2.6.1.2. Sintaxe abreviada para definição de função
O Scheme tem uma sintaxe alternativa de define
, para criar uma função nomeada sem usar um lambda
aninhado. Tal sintaxe funciona assim:
(define (name parm…) body1 body2…)
A palavra-chave define
é seguida por uma lista com o name
da nova função e zero ou mais nomes de parâmetros. Após a lista vem o corpo da função, com uma ou mais expressões.
Acrescentar essas duas linhas ao match
cuida da implementação:
case ['define', [Symbol() as name, *parms], *body] if body:
env[name] = Procedure(parms, body, env)
Eu colocaria esse case
após o case
da outra forma de define
no Exemplo 14.
A ordem desses cases de define
é irrelevante nesse exemplo, pois nenhum sujeito pode casar com esses dois padrões:
o segundo elemento deve ser um Symbol
na forma original de define
,
mas deve ser uma sequência começando com um Symbol
no atalho de define
para definição de função.
Agora pense em quanto trabalho teríamos para adicionar o suporte a essa segunda sintaxe de define
sem a ajuda do pattern matching no Exemplo 13.
A instrução match
faz muito mais que o switch
das linguagens similares ao C.
O pattern matching é um exemplo de programação declarativa: o código descreve "o que" você quer casar, em vez de "como" casar. A forma do código segue a forma dos dados, como ilustra a Tabela 4.
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 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 2.12).
Agora vamos olhar mais de perto a forma como Python interpreta a notação de fatiamento.
2.7.2. Objetos fatia
Isso não é segredo, mas vale a pena repetir, só para ter certeza:
s[a:b:c]
pode ser usado para especificar um passo ou salto c
, fazendo com que a fatia resultante pule itens. O passo pode ser também negativo, devolvendo os itens em ordem inversa.
Três exemplos esclarecem a questão:
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
Vimos outro exemplo no Capítulo 1, quando usamos deck[12::13]
para obter todos os ases de uma baralho não embaralhado:
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
A notação a:b:c
só é válida entre []
quando usada como operador de indexação ou de subscrição (subscript),
e produz um objeto fatia (slice object): slice(a, b, c)
.
Como veremos na Seção 12.5.1, para avaliar a expressão seq[start:stop:step]
,
o Python chama seq.__getitem__(slice(start, stop, step))
.
Mesmo se você não for implementar seus próprios tipos de sequência, saber dos objetos fatia é útil, porque eles permitem que você atribua nomes às fatias, da mesma forma que planilhas permitem dar nomes a faixas de células.
Suponha que você precise analisar um arquivo de dados como a fatura mostrada na Exemplo 15.
Em vez de encher seu código de fatias explícitas fixas, você pode nomeá-las.
Veja como isso torna legível o loop for
no final do exemplo.
>>> invoice = """
... 0.....6.................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... """
>>> SKU = slice(0, 6)
>>> DESCRIPTION = slice(6, 40)
>>> UNIT_PRICE = slice(40, 52)
>>> QUANTITY = slice(52, 55)
>>> ITEM_TOTAL = slice(55, None)
>>> line_items = invoice.split('\n')[2:]
>>> for item in line_items:
... print(item[UNIT_PRICE], item[DESCRIPTION])
...
$17.50 Pimoroni PiBrella
$4.95 6mm Tactile Switch x20
$28.00 Panavise Jr. - PV-201
$34.95 PiTFT Mini Kit 320x240
Voltaremos aos objetos slice quando formos discutir a criação de suas próprias coleções, na Seção 12.5.
Enquanto isso, do ponto de vista do usuário, o fatiamento tem recursos adicionais, tais como fatias multidimensionais e a notação de reticências (...
).
Siga comigo.
2.7.3. Fatiamento multidimensional e reticências
O operador []
pode também receber múltiplos índices ou fatias separadas por vírgulas.
Os métodos especiais __getitem__
e __setitem__
, que tratam o operador []
, apenas recebem os índices em a[i, j]
como uma tupla.
Em outras palavras, para avaliar a[i, j]
, Python chama a.__getitem__((i, j))
.
Isso é usado, por exemplo, no pacote externo NumPy, onde itens de uma numpy.ndarray
bi-dimensional
podem ser recuperados usando a sintaxe a[i, j]
, e uma fatia bi-dimensional é obtida com uma expressão como a[m:n, k:l]
. O Exemplo 24, abaixo nesse mesmo capítulo, mostra o uso dessa notação.
Exceto por memoryview
, os tipos embutidos de sequência de Python são uni-dimensionais, então aceitam apenas um índice ou fatia, e não uma tupla de índices ou fatias.[13]
As reticências—escritas como três pontos finais (...
) e não como …
(Unicode U+2026)—são reconhecidas como um símbolo pelo parser de Python. Esse símbolo é um apelido para o objeto Ellipsis
, a única instância da classe ellipsis
.[14]
Dessa forma, ele pode ser passado como argumento para funções e como parte da especificação de uma fatia, como em f(a, ..., z)
ou a[i:...]
.
O NumPy usa ...
como atalho ao fatiar arrays com muitas dimensões; por exemplo, se x
é um array com quatro dimensões, x[i, ...]
é um atalho para x[i, :, :, :,]
.
Veja "NumPy quickstart" (EN)
para saber mais sobre isso.
No momento em que escrevo isso, desconheço usos de Ellipsis
ou de índices multidimensionais na biblioteca padrão de Python.
Se você souber de algum, me avise.
Esses recursos sintáticos existem para suportar tipos definidos pelo usuário ou extensões como o NumPy.
Fatias não são úteis apenas para extrair informações de sequências; elas podem também ser usadas para modificar sequências mutáveis no lugar—isto é, sem precisar reconstruí-las do zero.
2.7.4. Atribuindo a fatias
Sequências mutáveis podem ser transplantadas, extirpadas e, de forma geral, modificadas no lugar com o uso da notação de fatias no lado esquerdo de um comando de atribuição ou como alvo de um comando del
.
Os próximos exemplos dão uma ideia do poder dessa notação:
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100 (1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]
-
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 16.
>>> 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 17.
>>> 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 17 é 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 16 equivale ao seguinte código:
>>> board = []
>>> for i in range(3):
... row = ['_'] * 3 # (1)
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board # (2)
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]
-
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, 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.[15]
Vimos casos de uso comuns para +=
.
A próxima seção mostra um caso lateral intrigante, que realça o real significado de "imutável" no contexto das tuplas.
2.8.3. Um quebra-cabeça com a atribuição +=
Tente responder sem usar o console: qual o resultado da avaliação das duas expressões no Exemplo 18?[16]
>>> 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 19 é a saída real em um console rodando Python 3.10.[17]
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
O Online Python Tutor (EN) é uma ferramenta online fantástica para visualizar em detalhes o funcionamento de Python. A Figura 7 é uma composição de duas capturas de tela, mostrando os estados inicial e final da tupla t
do Exemplo 19.
Se olharmos o bytecode gerado pelo Python para a expressão s[a] += b
(Exemplo 20), 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 19 é uma lista). -
Atribui
s[a] = TOS
. Isso falha ses
é imutável (a tuplat
no Exemplo 19).
Esse exemplo é um caso raro—em meus 20 anos usando Python, nunca vi esse comportamento estranho estragar o dia de alguém.
Há três lições para tirar daqui:
-
Evite colocar objetos mutáveis em tuplas.
-
A atribuição aumentada não é uma operação atômica—acabamos de vê-la gerar uma exceção após executar parte de seu trabalho.
-
Inspecionar o bytecode de Python não é muito difícil, e pode ajudar a ver o que está acontecendo por debaixo dos panos.
Após testemunharmos as sutilezas do uso de +
e *
para concatenação, podemos mudar de assunto e tratar de outra operação essencial com sequências: ordenação.
2.9. list.sort versus a função embutida sorted
O método list.sort
ordena uma lista no mesmo lugar—isto é, sem criar uma cópia. Ele devolve None
para nos lembrar que muda a própria instância e não cria uma nova lista.
Essa é uma convenção importante da API de Python:
funções e métodos que mudam um objeto no mesmo lugar deve devolver None
,
para deixar claro a quem chamou que o receptor[18]
foi modificado, e que nenhum objeto novo foi criado.
Um comportamento similar pode ser observado, por exemplo,
na função random.shuffle(s)
, que devolve None
após
embaralhar os itens de uma sequência mutável in-place (no lugar),
isto é, mudando a posição dos itens dentro da própria sequência.
✒️ Nota
|
A convenção de devolver |
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 de Python é estável (isto é, ele preserva a ordem relativa de itens que resultam iguais na comparação):[19]
>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry'] (1)
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] (2)
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple'] (3)
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry'] (4)
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple'] (5)
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] (6)
>>> fruits.sort() (7)
>>> fruits
['apple', 'banana', 'grape', 'raspberry'] (8)
-
Isso produz uma lista de strings ordenadas alfabeticamente.[20]
-
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, Python ordena as strings lexicograficamente por código de caractere. Isso quer dizer que as letras maiúsculas ASCII virão antes das minúsculas, e que os caracteres não-ASCII dificilmente serão ordenados de forma razoável. A Seção 4.8 trata de maneiras corretas de ordenar texto da forma esperada por seres humanos. |
Uma vez ordenadas, podemos realizar buscas em nossas sequências de forma muito eficiente.
Um algoritmo de busca binária já é fornecido no módulo bisect
da biblioteca padrão de Python.
Aquele módulo também inclui a função bisect.insort
,
que você pode usar para assegurar que suas sequências ordenadas permaneçam ordenadas.
Há uma introdução ilustrada ao módulo bisect
no post "Managing Ordered Sequences with Bisect" (Gerenciando Sequências Ordenadas com Bisect) (EN)
em fluentpython.com, o website que complementa este livro.
Muito do que vimos até aqui neste capítulo se aplica a sequências em geral, não apenas a listas ou tuplas.
Programadores Python às vezes usam excessivamente o tipo list
, por ele ser tão conveniente—eu mesmo já fiz isso. Por exemplo, se você está processando grandes listas de números, deveria considerar usar arrays em vez de listas. O restante do capítulo é dedicado a alternativas a listas e tuplas.
2.10. Quando uma lista não é a resposta
O tipo list
é flexível e fácil de usar mas, dependendo dos requerimentos específicos, há opções melhores.
Por exemplo, um array
economiza muita memória se você precisa manipular milhões de valores de ponto flutuante.
Por outro lado, se você está constantemente acrescentando e removendo itens das pontas opostas de uma lista, é bom saber que um deque
(uma fila com duas pontas) é uma estrutura de dados FIFO[21] mais eficiente.
👉 Dica
|
Se seu código frequentemente verifica se um item está presente em uma coleção (por exemplo, |
O restante desse capítulo discute tipos mutáveis de sequências que, em muitos casos, podem substituir as listas. Começamos pelos arrays.
2.10.1. Arrays
Se uma lista contém apenas números, uma array.array
é um substituto mais eficiente.
Arrays suportam todas as operações das sequências mutáveis (incluindo .pop
, .insert
, e .extend
), bem como métodos adicionais para carregamento e armazenamento rápidos, tais como
.frombytes
e .tofile
.
Um array de Python quase tão enxuto quanto um array do C.
Como mostrado na Figura 3, um array
de valores float
não mantém instâncias completas de float
, mas apenas pacotes de bytes representando seus valores em código de máquina—de forma similar a um array de double
na linguagem C.
Ao criar um array
, você fornece um código de tipo (typecode), uma letra que determina o tipo C subjacente usado para armazenar cada item no array.
Por exemplo, b é o código de tipo para o que o C chama de signed char
, um inteiro variando de -128 a 127.
Se você criar uma array('b')
, então cada item será armazenado em um único byte e será interpretado como um inteiro. Para grandes sequências de números, isso economiza muita memória.
E Python não permite que você insira qualquer número que não corresponda ao tipo do array.
O Exemplo 21 mostra a criação, o armazenamento e o carregamento de um array de 10 milhões de números de ponto flutuante aleatórios.
>>> 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, Python tem os tipos bytes
e bytearray
, discutidos na Capítulo 4.
Vamos encerrar essa seção sobre arrays com a Tabela 5,
comparando as características de list
e array.array
.
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[22] |
|
● |
● |
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é 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 2.10.3).
Travis Oliphant, autor principal da NumPy, responde assim à questão "When should a memoryview be used?" Quando se deve usar uma memoryview?:
Uma memoryview é essencialmente uma estrutura de array Numpy generalizada dentro do próprio Python (sem a matemática). Ela permite compartilhar memória entre estruturas de dados (coisas como imagens PIL, bancos de dados SQLite, arrays da NumPy, etc.) sem copiar antes. Isso é muito importante para conjuntos grandes de dados.
Usando uma notação similar ao módulo array
, o método memoryview.cast
permite mudar a forma como múltiplos bytes são lidos ou escritos como unidades, sem a necessidade de mover os bits. memoryview.cast
devolve ainda outro objeto memoryview
, sempre compartilhando a mesma memória.
O Exemplo 22 mostra como criar views alternativas da mesmo array de 6 bytes, para operar com ele como uma matriz de 2x3 ou de 3x2.
>>> 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 23 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 de Python, para que você a aproveite ao máximo. Mas a NumPy é tão maravilhosa que exige um desvio.
Por suas operações avançadas de arrays e matrizes, o Numpy é a razão pela qual Python se tornou uma das principais linguagens para aplicações de computação científica. A Numpy implementa tipos multidimensionais e homogêneos de arrays e matrizes, que podem conter não apenas números, mas também registros definidos pelo usuário. E fornece operações eficientes ao nível desses elementos.
A SciPy é uma biblioteca criada usando a NumPy, e oferece inúmeros algoritmos de computação científica, incluindo álgebra linear, cálculo numérico e estatística. A SciPy é rápida e confiável porque usa a popular base de código C e Fortran do Repositório Netlib. Em outras palavras, a SciPy dá a cientistas o melhor de dois mundos: um prompt iterativo e as APIs de alto nível de Python, junto com funções estáveis e de eficiência comprovada para processamento de números, otimizadas em C e Fortran
O Exemplo 24, uma amostra muito rápida da Numpy, demonstra algumas operações básicas com arrays bi-dimensionais.
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 de 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 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) de Python. O projeto Dask suporta a paralelização do processamento da NumPy, da Pandas e da scikit-learn para grupos (clusters) de máquinas. Esses pacotes merecem livros inteiros. Este não é um desses livros, mas nenhuma revisão das sequências de Python estaria completa sem pelo menos uma breve passagem pelos arrays da NumPy.
Tendo olhado as sequências planas—arrays padrão e arrays da NumPy—vamos agora nos voltar para um grupo completamente diferentes de substitutos para a boa e velha list
: filas (queues).
2.10.4. Deques e outras filas
Os métodos .append
e .pop
tornam uma list
usável como uma pilha (stack) ou uma fila (queue) (usando .append
e .pop(0)
, se obtém um comportamento FIFO).
Mas inserir e remover da cabeça de uma lista (a posição com índice 0) é caro, pois a lista toda precisa ser deslocada na memória.
A classe collections.deque
é uma fila de duas pontas e segura para usar com threads, projetada para inserção e remoção rápida nas duas pontas. É também a estrutura preferencial se você precisa manter uma lista de "últimos itens vistos" ou coisa semelhante, pois um deque
pode ser delimitado—isto é, criado com um tamanho máximo fixo. Se um deque
delimitado está cheio, quando se adiciona um novo item, o item na ponta oposta é descartado.
O Exemplo 25 mostra algumas das operações típicas com um deque
.
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[23] |
|
|
● |
● |
Remove e devolve último item[24] |
|
● |
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 de 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 de Python são geralmente categorizadas como mutáveis ou imutáveis, mas também é útil considerar um outro eixo: sequências planas e sequências contêiner. As primeiras são mais compactas, mais rápidas e mais fáceis de usar, mas estão limitadas a armazenar dados atômicos como números, caracteres e bytes. As sequências contêiner são mais flexíveis, mas podem surpreender quando contêm objetos mutáveis. Então, quando armazenando estruturas de dados aninhadas, é preciso ter cuidado para usar tais sequências da forma correta.
Infelizmente Python não tem um tipo de sequência contêiner imutável infalível: mesmo as tuplas "imutáveis" podem ter seus valores modificados quando contêm itens mutáveis como listas ou objetos definidos pelo usuário.
Compreensões de lista e expressões geradoras são notações poderosas para criar e inicializar sequências. Se você ainda não se sente confortável com essas técnicas, gaste o tempo necessário para aprender seu uso básico. Não é difícil, e você logo vai estar gostando delas.
As tuplas no Python tem dois papéis: como registros de campos sem nome e como listas imutáveis.
Ao usar uma tupla como uma lista imutável,
lembre-se que só é garantido que o valor de uma tupla será fixo se todos os seus itens também forem imutáveis.
Chamar hash(t)
com a tupla como argumento é uma forma rápida de se assegurar que seu valor é fixo.
Se t
contiver itens mutáveis, um TypeError
é gerado.
Quando uma tupla é usada como registro,
o desempacotamento de tuplas é a forma mais segura e legível de extrair seus campos.
Além das tuplas, *
funciona com listas e iteráveis em vários contextos,
e alguns de seus casos de uso apareceram no Python 3.5 com a
PEP 448—Additional Unpacking Generalizations (Generalizações de Desempacotamento Adicionais)
(EN).
Python 3.10 introduziu o casamento de padrões com match/case
,
suportando um tipo de desempacotamento mais poderoso, conhecido como desestruturação.
Fatiamento de sequências é um dos recursos de sintaxe preferidos de Python,
e é ainda mais poderoso do que muita gente pensa.
Fatiamento multidimensional e a notação de reticências (...
), como usados no NumPy,
podem também ser suportados por sequências definidas pelo usuário.
Atribuir a fatias é uma forma muito expressiva de editar sequências mutáveis.
Concatenação repetida, como em seq * n
, é conveniente e, tomando cuidado,
pode ser usada para inicializar listas de listas contendo itens imutáveis.
Atribuição aumentada com +=
e *=
se comporta de forma diferente com sequências mutáveis e imutáveis.
No último caso, esses operadores necessariamente criam novas sequências.
Mas se a sequência alvo é mutável, ela em geral é modificada no lugar—mas nem sempre,
depende de como a sequência é implementada.
O método sort
e a função embutida sorted
são fáceis de usar e flexíveis,
graças ao argumento opcional key
: uma função para calcular o critério de ordenação.
E aliás, key
também pode ser usado com as funções embutidas min
e max
.
Além de listas e tuplas, a biblioteca padrão de Python oferece array.array
.
Apesar da NumPy e da SciPy não serem parte da biblioteca padrão,
se você faz qualquer tipo de processamento numérico em grandes conjuntos de dados,
estudar mesmo uma pequena parte dessas bibliotecas pode levar você muito longe.
Terminamos com uma visita à versátil collections.deque
, também segura para usar com threads.
Comparamos sua API com a de list
na Tabela 6
e mencionamos as outras implementações de filas na biblioteca padrão.
2.12. Leitura complementar
O capítulo 1, "Data Structures" (Estruturas de Dados) do Python Cookbook, 3rd ed. (EN) (O’Reilly), de David Beazley e Brian K. Jones, traz muitas receitas usando sequências, incluindo a "Recipe 1.11. Naming a Slice" (Receita 1.11. Nomeando uma Fatia), onde aprendi o truque de atribuir fatias a variáveis para melhorar a legibilidade, como ilustrado no nosso Exemplo 15.
A segunda edição do Python Cookbook foi escrita para Python 2.4, mas a maior parte de seu código funciona com Python 3, e muitas das receitas dos capítulos 5 e 6 lidam com sequências. O livro foi editado por Alex Martelli, Anna Ravenscroft, e David Ascher, e inclui contribuições de dúzias de pythonistas. A terceira edição foi reescrita do zero, e se concentra mais na semântica da linguagem—especialmente no que mudou no Python 3—enquanto o volume mais antigo enfatiza a pragmática (isto é, como aplicar a linguagem a problemas da vida real). Apesar de algumas das soluções da segunda edição não serem mais a melhor abordagem, honestamente acho que vale a pena ter à mão as duas edições do Python Cookbook.
O "HowTo - Ordenação"
oficial de Python tem vários exemplos de técnicas avançadas de uso de sorted
e list.sort
.
A PEP 3132—Extended Iterable Unpacking (Desempacotamento Iterável Estendido)
(EN) é a fonte canônica para ler sobre o novo uso da sintaxe *extra
no lado esquerdo de atribuições paralelas.
Se você quiser dar uma olhada no processo de evolução de Python,
"Missing *-unpacking generalizations" (As generalizações esquecidas de * no desempacotamento)
(EN) é um tópico do bug tracker propondo melhorias na notação de desempacotamento iterável.
PEP 448—Additional Unpacking Generalizations (Generalizações de Desempacotamento Adicionais)
(EN) foi o resultado de discussões ocorridas naquele tópico.
Como mencionei na Seção 2.6, o texto introdutório "Casamento de padrão estrutural", de Carol Willing, no "O que há de novo no Python 3.10", é uma ótima introdução a esse novo grande recurso, em mais ou menos 1.400 palavras (isso é menos de 5 páginas quando o Firefox converte o HTML em PDF). A PEP 636—Structural Pattern Matching: Tutorial (Casamento de Padrões Estrutural: Tutorial) (EN) também é boa, mas mais longa. A mesma PEP 636 inclui o "Appendix A—Quick Intro" (Apêndice A-Introdução Rápida) (EN). Ele é menor que a introdução de Willing, porque omite as considerações gerais sobre os motivos pelos quais o casamento de padrões é útil. Se você precisar de mais argumentos para se convencer ou convencer outros que o casamento de padrões foi bom para o Python, leia as 22 páginas de PEP 635—Structural Pattern Matching: Motivation and Rationale (_Casamento de Padrões Estrutural: Motivação e Justificativa) (EN).
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 de Python.
A melhor defesa da convenção de Python de excluir o último item range
e fatias foi escrita pelo grande Edsger W. Dijkstra, em uma nota curta intitulada
"Why Numbering Should Start at Zero" (Porque a Numeração Deve Começar em Zero).
O assunto da nota é notação matemática, mas ela é relevante para Python porque
Dijkstra explica, com humor e rigor, porque uma sequência como 2, 3, …, 12
deveria sempre ser expressa como 2 ≤ i < 13.
Todas as outras convenções razoáveis são refutadas, bem como a ideia de deixar cada usuário escolher uma convenção.
O título se refere à indexação baseada em zero, mas a nota na verdade é sobre porque é desejável que 'ABCDE'[1:3]
signifique 'BC'
e não 'BCD'
, e porque faz todo sentido escrever
range(2, 13)
para produzir 2, 3, 4, …, 12.
E, por sinal, a nota foi escrita à mão, mas é linda e totalmente legível.
A letra de Dijkstra é tão cristalina que alguém criou uma fonte a partir de suas anotações.
3. Dicionários e conjuntos
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 de Python.
Atributos de classes e de instâncias, espaços de nomes de módulos e argumentos nomeados de funções são alguns dos elementos fundamentais de Python representados na memória por dicionários.
O __builtins__.__dict__
armazena todos os tipos, funções e objetos embutidos.
Por seu papel crucial, os dicts de Python são extremamente otimizados—e continuam recebendo melhorias. As Tabelas de hash são o motor por trás do alto desempenho dos dicts de Python.
Outros tipos embutidos baseados em tabelas de hash são set
e frozenset
. Eles oferecem uma API mais completa e operadores mais robustos que os conjuntos que você pode ter encontrado em outras linguagens populares. Em especial, os conjuntos de Python implementam todas as operações fundamentais da teoria dos conjuntos, como união, intersecção, testes de subconjuntos, etc. Com eles, podemos expressar algoritmos de forma mais declarativa, evitando o excesso de loops e condicionais aninhados.
Aqui está um breve esquema do capítulo:
-
A sintaxe moderna para criar e manipular
dicts
e mapeamentos, incluindo desempacotamento aumentado e pattern matching (casamento de padrões) -
Métodos comuns dos tipos de mapeamentos
-
Tratamento especial para chaves ausentes
-
Variantes de
dict
na biblioteca padrão -
Os tipos
set
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 3.2 fala da sintaxe aperfeiçoada de desempacotamento e de diferentes maneiras de mesclar mapeamentos—incluindo os operadores
|
e|=
, suportados pelosdicts
desde Python 3.9. -
A Seção 3.3 ilustra o manuseio de mapeamentos com
match/case
, recurso que surgiu no Python 3.10. -
A Seção 3.6.1 agora se concentra nas pequenas mas ainda relevantes diferenças entre
dict
eOrderedDict
—levando em conta que, desde Python 3.6,dict
passou a manter a ordem de inserção das chaves. -
Novas seções sobre os objetos view devolvidos por
dict.keys
,dict.items
, 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 Python 2.7, a sintaxe das listcomps e genexps foi adaptada para compreensões de dict
(e também compreensões de set
, que veremos em breve). Uma dictcomp (compreensão de dict) cria uma instância de dict
, recebendo pares key:value
de qualquer iterável. O Exemplo 26 mostra o uso de compreensões de dict
para criar dois dicionários a partir de uma mesma lista de tuplas.
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 Python 3.5.
Primeiro, podemos aplicar **
a mais de um argumento em uma chamada de função. Isso funciona quando todas as chaves são strings e únicas, para todos os argumentos (porque argumentos nomeados duplicados são proibidos):
>>> def dump(**kwargs):
... return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}
Em segundo lugar, **
pode ser usado dentro de um literal dict
—também múltiplas vezes:
>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}
Nesse caso, chaves duplicadas são permitidas. Cada ocorrência sobrescreve ocorrências anteriores—observe o valor mapeado para x
no exemplo.
Essa sintaxe também pode ser usada para mesclar mapas, mas isso pode ser feito de outras formas. Siga comigo.
3.2.3. Fundindo mapeamentos com |
Desde a versão 3.9, Python suporta o uso de |
e |=
para mesclar mapeamentos.
Isso faz todo sentido, já que estes são também os operadores de união de conjuntos.
O operador |
cria um novo mapeamento:
>>> d1 = {'a': 1, 'b': 3}
>>> d2 = {'a': 2, 'b': 4, 'c': 6}
>>> d1 | d2
{'a': 2, 'b': 4, 'c': 6}
O tipo do novo mapeamento normalmente será o mesmo do operando da esquerda—no exemplo, d1
—mas ele pode ser do tipo do segundo operando se tipos definidos pelo usuário estiverem envolvidos na operação, dependendo das regras de sobrecarga de operadores, que exploraremos no Capítulo 16.
Para atualizar mapeamentos existentes no mesmo lugar, use |=
.
Retomando o exemplo anterior, ali d1
não foi modificado. Mas aqui sim:
>>> d1
{'a': 1, 'b': 3}
>>> d1 |= d2
>>> d1
{'a': 2, 'b': 4, 'c': 6}
👉 Dica
|
Se você precisa manter código rodando no Python 3.8 ou anterior, a seção "Motivation" (Motivação) (EN) da PEP 584—Add Union Operators To dict (Acrescentar Operadores de União a dict) (EN) inclui um bom resumo das outras formas de mesclar mapeamentos. |
Agora vamos ver como o pattern matching se aplica aos mapeamentos.
3.3. Pattern matching com mapeamentos
A instrução match/case
suporta sujeitos que sejam objetos mapeamento.
Padrões para mapeamentos se parecem com literais dict
, mas podem casar com instâncias de qualquer subclasse real ou virtual de collections.abc.Mapping
.[26]
No Capítulo 2 nos concentramos apenas nos padrões de sequência, mas tipos diferentes de padrões podem ser combinados e aninhados. Graças à desestruturação, o pattern matching é uma ferramenta poderosa para processar registros estruturados como sequências e mapeamentos aninhados, que frequentemente precisamos ler de APIs JSON ou bancos de dados com schemas semi-estruturados, como o MongoDB, o EdgeDB, ou o PostgreSQL. O Exemplo 27 demonstra isso.
As dicas de tipo simples em get_creators
tornam claro que ela recebe um dict
e devolve uma list
.
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 27 mostra algumas práticas úteis para lidar com dados semi-estruturados, tais como registros JSON:
-
Incluir um campo descrevendo o tipo de registro (por exemplo,
'type': 'movie'
) -
Incluir um campo identificando a versão do schema (por exemplo,
'api': 2'
), para permitir evoluções futuras das APIs públicas. -
Ter cláusulas
case
para processar registros inválidos de um tipo específico (por exemplo,'book'
), bem como 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 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 8.
A maior utilidade dessas ABCs é documentar e formalizar as interfaces padrão para os mapeamentos, e servir e critério para testes com isinstance
em código que precise suportar mapeamentos de forma geral:
>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
>>> isinstance(my_dict, abc.MutableMapping)
True
👉 Dica
|
Usar |
MutableMapping
e suas superclasses de collections.abc
(as setas de herança apontam das subclasses para as superclasses; nomes em itálico indicam classes e métodos abstratosPara implementar uma mapeamento personalizado, é mais fácil estender collections.UserDict
, ou envolver um dict
por composição, ao invés de criar uma subclasse dessas ABCs.
A classe collections.UserDict
e todas as classes concretas de mapeamentos da biblioteca padrão encapsulam o dict
básico em suas implementações, que por sua vez é criado sobre uma tabela de hash. Assim, todas elas compartilham a mesma limitação, as chaves precisam ser hashable (os valores não precisam ser hashable, só as chaves).
Se você precisa de uma recapitulação, a próxima seção explica isso.
3.4.1. O que é hashable?
Aqui está parte da definição de hashable, adaptado do Glossário de Python:
Um objeto é hashable se tem um código de hash que nunca muda durante seu ciclo de vida (precisa ter um método hash()) e pode ser comparado com outros objetos (precisa ter um método eq()). Objetos hashable que são comparados como iguais devem ter o mesmo código de hash.[27]
Tipos numéricos e os tipos planos imutáveis str
e bytes
são todos hashable.
Tipos contêineres são hashable se forem imutáveis e se todos os objetos por eles contidos forem também hashable.
Um frozenset
é sempre hashable, pois todos os elementos que ele contém devem ser, por definição, hashable.
Uma tuple
é hashable apenas se todos os seus itens também forem. Observe as tuplas tt
, tl
, and tf
:
>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110
O código de hash de um objeto pode ser diferente dependendo da versão de Python, da arquitetura da máquina, e pelo sal acrescentado ao cálculo do hash por razões de segurança.[28] O código de hash de um objeto corretamente implementado tem a garantia de ser constante apenas dentro de um processo Python.
Tipos definidos pelo usuário são hashble por default, pois seu código de hash é seu id()
, e o método __eq__()
herdado da classe objetct
apenas compara os IDs dos objetos. Se um objeto implementar seu próprio __eq__()
, que leve em consideração seu estado interno, ele será hashable apenas se seu __hash__()
sempre devolver o mesmo código de hash. Na prática, isso exige que __eq__()
e __hash__()
levem em conta apenas atributos de instância que nunca mudem durante a vida do objeto.
Vamos agora revisar a API dos tipos de mapeamento mais comumente usado no Python: dict
, defaultdict
, e OrderedDict
.
3.4.2. Revisão dos métodos mais comuns dos mapeamentos
A API básica para mapeamentos é muito variada. A Tabela 7 mostra os métodos implementados por dict
e por duas variantes populares: defaultdict
e OrderedDict
, ambas classes definidas no módulo collections
.
dict | defaultdict | OrderedDict | ||
---|---|---|---|---|
|
● |
● |
● |
Remove todos os itens. |
|
● |
● |
● |
|
|
● |
● |
● |
Cópia rasa. |
|
● |
Suporte a |
||
|
● |
Chamável invocado por |
||
|
● |
● |
● |
|
|
● |
● |
● |
Novo mapeamento com chaves no iterável |
|
● |
● |
● |
Obtém item com chave |
|
● |
● |
● |
d[k]—obtém item com chave |
|
● |
● |
● |
Obtém uma view dos itens—pares |
|
● |
● |
● |
Obtém iterador das chaves. |
|
● |
● |
● |
Obtém view das chaves. |
|
● |
● |
● |
|
|
● |
Chamado quando |
||
|
● |
Move |
||
|
● |
● |
● |
Suporte a |
|
● |
● |
● |
Suporte a |
|
● |
● |
● |
Remove e devolve valor em |
|
● |
● |
● |
Remove e devolve, na forma |
|
● |
● |
● |
Suporte a |
|
● |
● |
● |
Suporte a |
|
● |
● |
● |
Se |
|
● |
● |
● |
|
|
● |
● |
● |
Atualiza |
|
● |
● |
● |
Obtém uma view dos valores. |
A forma como d.update(m)
lida com seu primeiro argumento, m
, é um excelente exemplo de duck typing (tipagem pato):
ele primeiro verifica se m
possui um método keys
e, em caso afirmativo, assume que m
é um mapeamento. Caso contrário, update()
reverte para uma iteração sobre m
, presumindo que seus item são pares (chave, valor)
.
O construtor da maioria dos mapeamentos de Python usa internamente a lógica de update()
,
o que quer dizer que eles podem ser inicializados por outros mapeamentos ou a partir de qualquer objeto iterável que produza pares (chave, valor)
.
Um método sutil dos mapeamentos é setdefault()
.
Ele evita buscas redundantes de chaves quando precisamos atualizar o valor em um item no mesmo lugar.
A próxima seção mostra como ele pode ser usado.
3.4.3. Inserindo ou atualizando valores mutáveis
Alinhada à filosofia de falhar rápido de Python, a consulta a um dict
com d[k]
gera um erro quando k
não é uma chave existente. Pythonistas sabem que d.get(k, default)
é uma alternativa a d[k]
sempre que receber um valor default é mais conveniente que tratar um KeyError
. Entretanto, se você está buscando um valor mutável e quer atualizá-lo, há um jeito melhor.
Considere um script para indexar texto, produzindo um mapeamento no qual cada chave é uma palavra, e o valor é uma lista das posições onde aquela palavra ocorre, como mostrado no Exemplo 28.
line_number
, column_number
) (número da linha, _número da coluna)$ python3 index0.py zen.txt
a [(19, 48), (20, 53)]
Although [(11, 1), (16, 1), (18, 1)]
ambiguity [(14, 16)]
and [(15, 23)]
are [(21, 12)]
aren [(10, 15)]
at [(16, 38)]
bad [(19, 50)]
be [(15, 14), (16, 27), (20, 50)]
beats [(11, 23)]
Beautiful [(3, 1)]
better [(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8), (18, 25)]
...
O Exemplo 29 é um script aquém do ideal, para mostrar um caso onde dict.get
não é a melhor maneira de lidar com uma chave ausente.
Ele foi adaptado de um exemplo de Alex Martelli.[32]
dict.get
para obter e atualizar uma lista de ocorrências de palavras de um índice (uma solução melhor é apresentada no Exemplo 30)"""Build an index mapping word -> list of occurrences"""
import re
import sys
WORD_RE = re.compile(r'\w+')
index = {}
with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start() + 1
location = (line_no, column_no)
# this is ugly; coded like this to make a point
occurrences = index.get(word, []) # (1)
occurrences.append(location) # (2)
index[word] = occurrences # (3)
# display in alphabetical order
for word in sorted(index, key=str.upper): # (4)
print(word, index[word])
-
Obtém a lista de ocorrências de
word
, ou[]
se a palavra não for encontrada. -
Acrescenta uma nova localização a
occurrences
. -
Coloca a
occurrences
modificada no dictindex
; isso exige uma segunda busca emindex
. -
Não estou chamando
str.upper
no argumentokey=
desorted
, apenas passando uma referência àquele método, para que a funçãosorted
possa usá-lo para normalizar as palavras antes de ordená-las.[33]
As três linhas tratando de occurrences
no Exemplo 29 podem ser substituídas por uma única linha usando dict.setdefault
. O Exemplo 30 fica mais próximo do código apresentado por Alex Martelli.
dict.setdefault
para obter e atualizar uma lista de ocorrências de uma palavra em uma única linha de código; compare com o Exemplo 29"""Build an index mapping word -> list of occurrences"""
import re
import sys
WORD_RE = re.compile(r'\w+')
index = {}
with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start() + 1
location = (line_no, column_no)
index.setdefault(word, []).append(location) # (1)
# display in alphabetical order
for word in sorted(index, key=str.upper):
print(word, index[word])
-
Obtém a lista de ocorrências de
word
, ou a define como[]
, se não for encontrada;setdefault
devolve o valor, então ele pode ser atualizado sem uma segunda busca.
Em outras palavras, o resultado final desta linha…
my_dict.setdefault(key, []).append(new_value)
…é o mesmo que executar…
if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)
…exceto que este último trecho de código executa pelo menos duas buscas por key
—três se a chave não for encontrada—enquanto setdefault
faz tudo isso com uma única busca.
Uma questão relacionada, o tratamento de chaves ausentes em qualquer busca (e não apenas para inserção de valores), é o assunto da próxima seção.
3.5. Tratamento automático de chaves ausentes
Algumas vezes é conveniente que os mapeamentos devolvam algum valor padronizado quando se busca por uma chave ausente. Há duas abordagem principais para esse fim: uma é usar um defaultdict
em vez de um dict
simples. A outra é criar uma subclasse de dict
ou de qualquer outro tipo de mapeamento e acrescentar um método __missing__
. Vamos ver as duas soluções a seguir.
3.5.1. defaultdict: outra perspectiva sobre as chaves ausentes
Uma instância de collections.defaultdict
cria itens com um valor default sob demanda, sempre que uma chave ausente é buscada usando a sintaxe d[k]
.
O Exemplo 31 usa defaultdict
para fornecer outra solução elegante para o índice de palavras do Exemplo 30.
Funciona assim: ao instanciar um defaultdict
, você fornece um chamável que produz um valor default sempre que __getitem__
recebe uma chave inexistente como argumento.
Por exemplo, dado um defaultdict
criado por dd = defaultdict(list)
, se 'new-key'
não estiver em dd
, a expressão dd['new-key']
segue os seguintes passos:
-
Chama list() para criar uma nova lista.
-
Insere a lista em
dd
usando'new-key'
como chave. -
Devolve uma referência para aquela lista.
O chamável que produz os valores default é mantido em um atributo de instância chamado default_factory
.
defaultdict
em vez do método setdefault
"""Build an index mapping word -> list of occurrences"""
import collections
import re
import sys
WORD_RE = re.compile(r'\w+')
index = collections.defaultdict(list) # (1)
with open(sys.argv[1], encoding='utf-8') as fp:
for line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line):
word = match.group()
column_no = match.start() + 1
location = (line_no, column_no)
index[word].append(location) # (2)
# display in alphabetical order
for word in sorted(index, key=str.upper):
print(word, index[word])
-
Cria um
defaultdict
com o construtor delist
comodefault_factory
. -
Se
word
não está inicialmente noindex
, odefault_factory
é chamado para produzir o valor ausente, que neste caso é umalist
vazia, que então é atribuída aindex[word]
e devolvida, de forma que a operação.append(location)
é sempre bem sucedida.
Se nenhum default_factory
é fornecido, o KeyError
usual é gerado para chaves ausente.
⚠️ Aviso
|
O |
O mecanismo que faz defaultdict
funcionar, chamando default_factory
, é o método especial __missing__
, um recurso discutido a seguir.
3.5.2. O método __missing__
Por trás da forma como os mapeamentos lidam com chaves ausentes está o método muito apropriadamente chamado __missing__
.[34]. Esse método não é definido na classe base dict
, mas dict
está ciente de sua possibilidade: se você criar uma subclasse de dict
e incluir um método __missing__
, o dict.__getitem__
padrão vai chamar seu método sempre que uma chave não for encontrada, em vez de gerar um KeyError
.
Suponha que você queira um mapeamento onde as chaves são convertidas para str
quando são procuradas. Um caso de uso concreto seria uma biblioteca para dispositivos IoT (Internet of Things, Internet das Coisas)[35], onde uma placa programável com portas genéricas programáveis (por exemplo, uma Raspberry Pi ou uma Arduino) é representada por uma classe "Placa" com um atributo minha_placa.portas
, que é uma mapeamento dos identificadores das portas físicas para objetos de software portas. O identificador da porta física pode ser um número ou uma string como "A0"
ou "P9_12"
. Por consistência, é desejável que todas as chaves em placa.portas
seja strings, mas também é conveniente buscar uma porta por número, como em meu-arduino.porta[13]
, para evitar que iniciantes tropecem quando quiserem fazer piscar o LED na porta 13 de seus Arduinos. O Exemplo 32 mostra como tal mapeamento funcionaria.
StrKeyDict0
a converte para str
quando ela não é encontradaTests for item retrieval using `d[key]` notation::
>>> d = StrKeyDict0([('2', 'two'), ('4', 'four')])
>>> d['2']
'two'
>>> d[4]
'four'
>>> d[1]
Traceback (most recent call last):
...
KeyError: '1'
Tests for item retrieval using `d.get(key)` notation::
>>> d.get('2')
'two'
>>> d.get(4)
'four'
>>> d.get(1, 'N/A')
'N/A'
Tests for the `in` operator::
>>> 2 in d
True
>>> 1 in d
False
O Exemplo 33 implementa a classe StrKeyDict0
, que passa nos doctests acima.
👉 Dica
|
Uma forma melhor de criar uma mapeamento definido pelo usuário é criar uma subclasse de |
StrKeyDict0
converte chaves não-string para string no momento da consulta (vejas os testes no Exemplo 32)class StrKeyDict0(dict): # (1)
def __missing__(self, key):
if isinstance(key, str): # (2)
raise KeyError(key)
return self[str(key)] # (3)
def get(self, key, default=None):
try:
return self[key] # (4)
except KeyError:
return default # (5)
def __contains__(self, key):
return key in self.keys() or str(key) in self.keys() # (6)
-
StrKeyDict0
herda dedict
. -
Verifica se
key
já é umastr
. Se é, e está ausente, gera umKeyError
. -
Cria uma
str
dekey
e a procura. -
O método
get
delega para__getitem__
usando a notaçãoself[key]
; isso dá oportunidade para nosso__missing__
agir. -
Se um
KeyError
foi gerado,__missing__
já falhou, então devolvemos odefault
. -
Procura pela chave não-modificada (a instância pode conter chaves não-
str
), depois por umastr
criada a partir da chave.
Considere por um momento o motivo do teste isinstance(key, str)
ser necessário na implementação de __missing__
.
Sem aquele teste, nosso método __missing__
funcionaria bem com qualquer chave k
—str
ou não—sempre que str(k)
produzisse uma chave existente. Mas se str(k)
não for uma chave existente, teríamos uma recursão infinita.
Na última linha de __missing__
, self[str(key)]
chamaria __getitem__
, passando aquela chave str
, e __getitem__
, por sua vez, chamaria
__missing__
novamente.
O método __contains__
também é necessário para que o comportamento nesse exemplo seja consistente, pois a operação k in d
o chama, mas o método
herdado de dict
não invoca __missing__
com chaves ausentes. Há um detalhe sutil em nossa implementação de __contains__
: não verificamos a existência da chave da forma pythônica normal—k in d
—porque str(key) in self
chamaria
__contains__
recursivamente. Evitamos isso procurando a chave explicitamente em self.keys()
.
Uma busca como k in my_dict.keys()
é eficiente em Python 3 mesmo para mapeamentos muito grandes, porque dict.keys()
devolve uma view, que é similar a um set, como veremos na Seção 3.12.
Entretanto, lembre-se que k in my_dict
faz o mesmo trabalho, e é mais rápido porque evita a busca nos atributos para encontrar o método .keys
.
Eu tinha uma razão específica para usar self.keys()
no método __contains__
do
Exemplo 33.
A verificação da chave não-modificada key in self.keys()
é necessária por correção, pois StrKeyDict0
não obriga todas as chaves no dicionário a serem do tipo str
.
Nosso único objetivo com esse exemplo simples foi fazer a busca "mais amigável", e não forçar tipos.
⚠️ Aviso
|
Classes definidas pelo usuário derivadas de mapeamentos da biblioteca padrão podem ou não usar
|
3.5.3. O uso inconsistente de __missing__ na biblioteca padrão
Considere os seguintes cenários, e como eles afetam a busca de chaves ausentes:
- subclasse de
dict
-
Uma subclasse de
dict
que implemente apenas__missing__
e nenhum outro método. Nesse caso,__missing__
pode ser chamado apenas emd[k]
, que usará o__getitem__
herdado dedict
. - subclasse de
collections.UserDict
-
Da mesma forma, uma subclasse de
UserDict
que implemente apenas__missing__
e nenhum outro método. O métodoget
herdado deUserDict
chama__getitem__
. Isso significa que__missing__
pode ser chamado para tratar de consultas comd[k]
e comd.get(k)
. - subclasse de
abc.Mapping
com o__getitem__
mais simples possível -
Uma subclasse mínima de
abc.Mapping
, implementando__missing__
e os métodos abstratos obrigatórios, incluindo uma implementação de__getitem__
que não chama__missing__
. O método__missing__
nunca é acionado nessa classe. - subclasse de
abc.Mapping
com__getitem__
chamando__missing__
-
Uma subclasse mínima de
abc.Mapping
, implementando__missing__
e os métodos abstratos obrigatórios, incluindo uma implementação de__getitem__
que chama__missing__
. O método__missing__
é acionado nessa classe para consultas por chaves ausentes feitas comd[k]
,d.get(k)
, ek in d
.
Veja missing.py no repositório de exemplos de código para demonstrações dos cenários descritos acima.
Os quatro cenários que acabo de descrever supõem implementações mínimas.
Se a sua subclasse implementa __getitem__
, get
, e __contains__
,
então você pode ou não fazer tais métodos usarem __missing__
, dependendo de suas necessidades.
O ponto aqui é mostrar que é preciso ter cuidado ao criar subclasses dos mapeamentos da biblioteca padrão para usar __missing__
, porque as classes base suportam comportamentos default diferentes.
Não se esqueça que o comportamento de setdefault
e update
também é afetado pela consulta de chaves.
E por fim, dependendo da lógica de seu __missing__
,
pode ser necessário implementar uma lógica especial em __setitem__
,
para evitar inconsistências ou comportamentos surpreeendentes.
Veremos um exemplo disso na Seção 3.6.5.
Até aqui tratamos dos tipos de mapeamentos dict
e defaultdict
, mas a biblioteca padrão traz outras implementações de mapeamentos, que discutiremos a seguir.
3.6. Variações de dict
Nessa seção falaremos brevemente sobre os tipos de mapeamentos incluídos na biblioteca padrão diferentes de defaultdict
, já visto na Seção 3.5.1.
3.6.1. collections.OrderedDict
Agora que o dict
embutido também mantém as chaves ordenadas (desde Python 3.6), o motivo mais comum para usar OrderedDict
é escrever código compatível com versões anteriores de Python.
Dito isso, a documentação lista algumas diferenças entre dict
e OrderedDict
que ainda persistem e que cito aqui—apenas reordenando os itens conforme sua relevância no uso diário:
-
A operação de igualdade para
OrderedDict
verifica a igualdade da ordenação. -
O método
popitem()
deOrderedDict
tem uma assinatura diferente, que aceita um argumento opcional especificando qual item será devolvido. -
OrderedDict
tem um métodomove_to_end()
, que reposiciona de um elemento para uma ponta do dicionário de forma eficiente. -
O
dict
comum foi projetado para ser muito bom nas operações de mapeamento. Monitorar a ordem de inserção era uma preocupação secundária. -
OrderedDict
foi projetado para ser bom em operações de reordenamento. Eficiência espacial, velocidade de iteração e o desempenho de operações de atualização eram preocupações secundárias. -
Em termos do algoritmo, um
OrderedDict
lida melhor que um dict com operações frequentes de reordenamento. Isso o torna adequado para monitorar acessos recentes (em um cache LRU[36], por exemplo).
3.6.2. collections.ChainMap
Uma instância de ChainMap
mantém uma lista de mapeamentos que podem ser consultados como se fossem um mapeamento único.
A busca é realizada em cada mapa incluído, na ordem em que eles aparecem na chamada ao construtor,
e é bem sucedida assim que a chave é encontrada em um daqueles mapeamentos.
Por exemplo:
>>> d1 = dict(a=1, b=3)
>>> d2 = dict(a=2, b=4, c=6)
>>> from collections import ChainMap
>>> chain = ChainMap(d1, d2)
>>> chain['a']
1
>>> chain['c']
6
A instância de ChainMap
não cria cópias dos mapeamentos, mantém referências para eles.
Atualizações ou inserções a um ChainMap
afetam apenas o primeiro mapeamento passado.
Continuando do exemplo anterior:
>>> chain['c'] = -1
>>> d1
{'a': 1, 'b': 3, 'c': -1}
>>> d2
{'a': 2, 'b': 4, 'c': 6}
Um ChainMap
é útil na implementação de linguagens com escopos aninhados,
onde cada mapeamento representa um contexto de escopo,
desde o escopo aninhado mais interno até o mais externo. A seção
"Objetos ChainMap", na documentação de collections
, apresenta vários exemplos do uso de Chainmap
,
incluindo esse trecho inspirado nas regras básicas de consulta de variáveis no Python:
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))
O Exemplo 350 mostra uma subclasse de ChainMap
usada para implementar um interpretador parcial da linguagem de programação Scheme.
3.6.3. collections.Counter
Um mapeamento que mantém uma contagem inteira para cada chave.
Atualizar uma chave existente adiciona à sua contagem.
Isso pode ser usado para contar instâncias de objetos hashable ou como um multiset ("conjunto múltiplo"), discutido adiante nessa seção.
Counter
implementa os operadores +
e -
para combinar contagens, e outros métodos úteis tal como o most_common([n])
, que devolve uma lista ordenada de tuplas com os n itens mais comuns e suas contagens; veja a documentação.
Aqui temos um Counter
usado para contar as letras em palavras:
>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.update('aaaaazzz')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(3)
[('a', 10), ('z', 3), ('b', 2)]
Observe que as chaves 'b'
e 'r'
estão empatadas em terceiro lugar, mas
ct.most_common(3)
mostra apenas três contagens.
Para usar collections.Counter
como um conjunto múltiplo, trate cada chave como um elemento de um conjunto, e a contagem será o número de ocorrências daquele elemento no conjunto.
3.6.4. shelve.Shelf
O módulo shelve
na biblioteca padrão fornece armazenamento persistente a um mapeamento de chaves em formato string para objetos Python serializados no formato binário pickle
.
O nome curioso, shelve
, faz sentido quando você percebe que potes de pickle
são armazenadas em prateleiras.[37]
A função de módulo shelve.open
devolve uma instância de shelve.Shelf
—um banco de dados DBM simples de chave-valor, baseado no módulo dbm
, com as seguintes características:
-
shelve.Shelf
é uma subclasse deabc.MutableMapping
, então fornece os métodos essenciais esperados de um tipo mapeamento. -
Além disso,
shelve.Shelf
fornece alguns outros métodos de gerenciamento de E/S, comosync
eclose
. -
Uma instância de
Shelf
é um gerenciador de contexto, então é possível usar um blocowith
para garantir que ela seja fechada após o uso. -
Chaves e valores são salvos sempre que um novo valor é atribuído a uma chave.
-
As chaves devem ser strings.
-
Os valores devem ser objetos que o módulo
pickle
possa serializar.
A documentação para os módulos shelve, dbm (EN), e pickle traz mais detalhes e também algumas ressalvas.
⚠️ Aviso
|
O |
As classes OrderedDict
, ChainMap
, Counter
, e Shelf
podem ser usadas diretamente, mas também podem ser personalizadas por subclasses.
UserDict
, por outro lado, foi planejada apenas como uma classe base a ser estendida.
3.6.5. Criando subclasses de UserDict em vez de dict
É
melhor criar um novo tipo de mapeamento estendendo collections.UserDict
em vez de dict
.
Percebemos isso quando tentamos estender nosso StrKeyDict0
do Exemplo 33 para assegurar que qualquer chave adicionada ao mapeamento seja armazenada como str
.
A principal razão pela qual é melhor criar uma subclasse de UserDict
em vez de dict
é que o tipo embutido tem alguns atalhos de implementação, que acabam nos obrigando a sobrepor métodos que poderíamos apenas herdar de UserDict
sem maiores problemas.[38]
Observe que UserDict
não herda de dict
, mas usa uma composição:
a classe tem uma instância interna de dict
, chamada data
, que mantém os itens propriamente ditos. Isso evita recursão indesejada quando escrevemos métodos especiais, como __setitem__
, e simplifica a programação de __contains__
, quando comparado com o Exemplo 33.
Graças a UserDict
, o StrKeyDict
(Exemplo 34) é mais conciso que o StrKeyDict0
(Exemplo 33), mais ainda faz melhor: ele armazena todas as chaves como str
, evitando surpresas desagradáveis se a instância for criada ou atualizada com dados contendo chaves de outros tipos (que não string).
StrKeyDict
sempre converte chaves que não sejam strings para str
na inserção, atualização e buscaimport collections
class StrKeyDict(collections.UserDict): # (1)
def __missing__(self, key): # (2)
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]
def __contains__(self, key):
return str(key) in self.data # (3)
def __setitem__(self, key, item):
self.data[str(key)] = item # (4)
-
StrKeyDict
estendeUserDict
. -
__missing__
é exatamente igual ao do Exemplo 33. -
__contains__
é mais simples: podemos assumir que todas as chaves armazenadas sãostr
, e podemos operar sobreself.data
em vez de invocarself.keys()
, como fizemos emStrKeyDict0
. -
__setitem__
converte qualquerkey
para umastr
. Esse método é mais fácil de sobrepor quando podemos delegar para o atributoself.data
.
Como UserDict
estende abc.MutableMapping
, o restante dos métodos que fazem de StrKeyDict
uma mapeamento completo são herdados de UserDict
, MutableMapping
, ou Mapping
. Estes últimos contém vários métodos concretos úteis, apesar de serem classes base abstratas (ABCs). Os seguinte métodos são dignos de nota:
MutableMapping.update
-
Esse método poderoso pode ser chamado diretamente, mas também é usado por
__init__
para criar a instância a partir de outros mapeamentos, de iteráveis de pares(chave, valor)
, e de argumentos nomeados. Como usaself[chave] = valor
para adicionar itens, ele termina por invocar nossa implementação de__setitem__
. Mapping.get
-
No
StrKeyDict0
(Exemplo 33), precisamos codificar nosso próprioget
para devolver os mesmos resultados de__getitem__
, mas no Exemplo 34 herdamosMapping.get
, que é implementado exatamente comoStrKeyDict0.get
(consulte o código-fonte de Python).
👉 Dica
|
Antoine Pitrou escreveu a PEP 455—Adding a key-transforming dictionary to collections (Acrescentando um dicionário com transformação de chaves a collections) (EN) e um patch para aperfeiçoar o módulo |
Sabemos que existem tipos de sequências imutáveis, mas e mapeamentos imutáveis? Bem, não há um tipo real desses na biblioteca padrão, mas um substituto está disponível. É o que vem a seguir.
3.7. Mapeamentos imutáveis
Os tipos de mapeamentos disponíveis na biblioteca padrão são todos mutáveis, mas pode ser necessário impedir que os usuários mudem um mapeamento por acidente. Um caso de uso concreto pode ser encontrado, novamente, em uma biblioteca de programação de hardware como a Pingo, mencionada na Seção 3.5.2:
o mapeamento board.pins
representa as portas de GPIO (General Purpose Input/Output, Entrada/Saída Genérica) em um dispositivo. Dessa forma, seria útil evitar atualizações descuidadas de board.pins
, pois o hardware não pode ser modificado via software: qualquer mudança no mapeamento o tornaria inconsistente com a realidade física do dispositivo.
O módulo types
oferece uma classe invólucro (wrapper) chamada MappingProxyType
que, dado um mapeamento, devolve uma instância de mappingproxy
, que é um proxy somente para leitura (mas dinâmico) do mapeamento original. Isso significa que atualizações ao mapeamento original são refletidas no mappingproxy
, mas nenhuma mudança pode ser feita através desse último. Veja uma breve demonstração no Exemplo 35.
MappingProxyType
cria uma instância somente de leitura de mappingproxy
a partir de um dict
>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1] (1)
'A'
>>> d_proxy[2] = 'x' (2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d[2] = 'B'
>>> d_proxy (3)
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>
-
Os items em
d
podem ser vistos através ded_proxy
. -
Não é possível fazer modificações através de
d_proxy
. -
d_proxy
é dinâmica: qualquer mudança emd
é refletida ali.
Isso pode ser usado assim na prática, no cenário da programação de hardware:
o construtor em uma subclasse concreta Board
preencheria um mapeamento privado com os objetos porta, e o exporia aos clientes da API via um atributo público .portas
, implementado como um mappingproxy
.
Dessa forma os clientes não poderiam acrescentar, remover ou modificar as portas por acidente.
A seguir veremos views—que permitem operações de alto desempenho em um dict
, sem cópias desnecessárias dos dados.
3.8. Views de dicionários
Os métodos de instância de dict
.keys()
, .values()
, e .items()
devolvem instâncias de classes chamadas dict_keys
, dict_values
, e dict_items
, respectivamente.
Essas views de dicionário são projeções somente para leitura de estruturas de dados internas usadas na implemetação de dict
.
Elas evitam o uso de memória adicional dos métodos equivalentes no Python 2, que devolviam listas, duplicando dados já presentes no dict
alvo. E também substituem os métodos antigos que devolviam iteradores.
O Exemplo 36 mostra algumas operações básicas suportadas por todas as views de dicionários.
.values()
devolve uma view dos valores em um dict
>>> d = dict(a=10, b=20, c=30)
>>> values = d.values()
>>> values
dict_values([10, 20, 30]) (1)
>>> len(values) (2)
3
>>> list(values) (3)
[10, 20, 30]
>>> reversed(values) (4)
<dict_reversevalueiterator object at 0x10e9e7310>
>>> values[0] (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'dict_values' object is not subscriptable
-
O
repr
de um objeto view mostra seu conteúdo. -
Podemos consultar a
len
de uma view. -
Views são iteráveis, então é fácil criar listas a partir delas.
-
Views implementam
__reversed__
, devolvendo um iterador personalizado. -
Não é possível usar
[]
para obter itens individuais de uma view.
Um objeto view é um proxy dinâmico.
Se o dict
fonte é atualizado, as mudanças podem ser vistas imediatamente através de uma view existente.
Continuando do Exemplo 36:
>>> d['z'] = 99
>>> d
{'a': 10, 'b': 20, 'c': 30, 'z': 99}
>>> values
dict_values([10, 20, 30, 99])
As classes dict_keys
, dict_values
, e dict_items
são internas:
elas não estão disponíveis via __builtins__
ou qualquer módulo da biblioteca padrão,
e mesmo que você obtenha uma referência para uma delas, não pode usar essa referência para criar uma view do zero no seu código Python:
>>> values_class = type({}.values())
>>> v = values_class()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create 'dict_values' instances
A classe dict_values
é a view de dicionário mais simples—ela implementa apenas os métodos especiais __len__
, __iter__
, e __reversed__
.
Além desses métodos, dict_keys
e dict_items
implementam vários métodos dos sets, quase tantos quanto a classe frozenset
.
Após vermos os conjuntos (sets), teremos mais a dizer sobre dict_keys
e dict_items
, na Seção 3.12.
Agora vamos ver algumas regras e dicas baseadas na forma como dict
é implementado debaixo dos panos.
3.9. Consequências práticas da forma como dict funciona
A implementação da tabela de hash do dict
de Python é muito eficiente, mas é importante entender os efeitos práticos desse design:
-
Chaves devem ser objetos hashable. Eles devem implementar métodos
__hash__
e__eq__
apropriados, como descrito na Seção 3.4.1. -
O acesso aos itens através da chave é muito rápido. Mesmo que um
dict
tenha milhões de chaves, Python pode localizar uma chave diretamente, computando o código hash da chave e derivando um deslocamento do índice na tabela de hash, com um possível ônus de um pequeno número de tentativas até encontrar a entrada correspondente. -
A ordenação das chaves é preservada, como efeito colateral de um layout de memória mais compacto para
dict
no CPython 3.6, que se tornou um recurso oficial da linguagem no 3.7. -
Apesar de seu novo layout compacto, os dicts apresentam, inevitavelmente, um uso adicional significativo de memória. A estrutura de dados interna mais compacta para um contêiner seria um array de ponteiros para os itens.[39] Comparado a isso, uma tabela de hash precisa armazenar mais dados para cada entrada e, para manter a eficiência, Python precisa manter pelo menos um terço das linhas da tabela de hash vazias.
-
Para economizar memória, evite criar atributos de instância fora do método
__init__
.
Essa última dica, sobre atributos de instância, é consequência do comportamento default de Python, de armazenar atributos de instância em um atributo __dict__
especial, que é um dict
vinculado a cada instância.[40]
Desde a implementação da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves) (EN), no Python 3.3,
instâncias de uma classe podem compartilhar uma tabela de hash comum, armazenada com a classe.
Essa tabela de hash comum é compartilhada pelo __dict__
de cada nova instância que, quando __init__
retorna, tenha os mesmos nomes de atributos que a primeira instância daquela classe a ser criada. O __dict__
de cada instância então pode manter apenas seus próprios valores de atributos como uma simples array de ponteiros.
Acrescentar um atributo de instância após o __init__
obriga Python a criar uma nova tabela de hash só para o __dict__
daquela instância (que era o comportamento default antes de Python 3.3).
De acordo com a PEP 412, essa otimização reduz o uso da memória entre 10% e 20% em programas orientados as objetos.
Os detalhes das otimizações do layout compacto e do compartilhamento de chaves são bastante complexos. Para saber mais, por favor leio o texto "Internals of sets and dicts" (EN) em fluentpython.com.
Agora vamos estudar conjuntos(sets).
3.10. Teoria dos conjuntos
Conjuntos
não são novidade no Python, mais ainda são um tanto subutilizados.
O tipo set
e seu irmão imutável, frozenset
,
surgiram inicialmente como módulos na biblioteca padrão de Python 2.3,
e foram promovidos a tipos embutidos no Python 2.6.
✒️ Nota
|
Nesse livro, uso a palavra "conjunto" para me referir tanto a |
Um conjunto é uma coleção de objetos únicos. Uma grande utilidade dos conjuntos é descartar itens duplicados:
>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
>>> set(l)
{'eggs', 'spam', 'bacon'}
>>> list(set(l))
['eggs', 'spam', 'bacon']
👉 Dica
|
Para remover elementos duplicados preservando a ordem da primeira ocorrência de cada item, você pode fazer isso com um
|
Elementos de um conjunto devem ser hashable.
O tipo set
não é hashable, então não é possível criar um set
com instâncias aninhadas de set
.
Mas frozenset
é hashable, então você pode ter instâncias de frozenset
dentro de um set
.
Além de impor a unicidade de cada elemento,
os tipos conjunto implementam muitas operações entre conjuntos como operadores infixos.
Assim, dados dois conjuntos a
e b
, a | b
devolve sua união,
a & b
calcula a intersecção, a - b
a diferença, e a ^ b
a diferença simétrica.
Quando bem utilizadas,
as operações de conjuntos podem reduzir tanto a contagem de linhas quanto o tempo de execução de programas Python,
ao mesmo tempo em que tornam o código mais legível e mais fácil de entender—pela remoção de loops e lógica condicional.
Por exemplo, imagine que você tem um grande conjunto de endereços de email (o "palheiro"—haystack
)
e um conjunto menor de endereços (as "agulhas"—needles`
),
e precisa contar quantas agulhas existem no palheiro.
Graças à interseção de set
(o operador &
),
é possível codar isso em uma expressão simples (veja o Exemplo 37).
found = len(needles & haystack)
Sem o operador de intersecção, seria necessário escrever o Exemplo 38 para realizar a mesma tarefa executa pelo Exemplo 37.
found = 0
for n in needles:
if n in haystack:
found += 1
O Exemplo 37 é um pouco mais rápido que o Exemplo 38.
Por outro lado, o Exemplo 38 funciona para quaisquer objetos iteráveis needles
e haystack
,
enquanto o Exemplo 37 exige que ambos sejam conjuntos.
Mas se você não tem conjuntos à mão, pode sempre criá-los na hora, como mostra o Exemplo 39.
found = len(set(needles) & set(haystack))
# another way:
found = len(set(needles).intersection(haystack))
Claro, há o custo extra envolvido na criação dos conjuntos no Exemplo 39,
mas se ou as needles
ou o haystack
já forem um set
,
a alternativa no Exemplo 39 pode ser mais barata que o Exemplo 38.
Qualquer dos exemplos acima é capaz de buscar 1000 elementos em um haystack
de 10 milhões
de itens em cerca de 0,3 milisegundos—isso é cerca de 0,3 microsegundos por elemento.
Além do teste de existência extremamente rápido (graças à tabela de hash subjacente),
os tipos embutidos set
e frozenset
oferecem uma rica API para criar novos conjuntos ou,
no caso de set
, para modificar conjuntos existentes.
Vamos discutir essas operações em breve, após uma observação sobre sintaxe.
3.10.1. Sets literais
A sintaxe de literais set
—{1}
, {1, 2}
, etc.—parece
muito com a notação matemática, mas tem uma importante exceção:
não há notação literal para o set
vazio, então precisamos nos lembrar de escrever set()
.
⚠️ Aviso
|
Peculiaridade sintática
Para criar um |
No Python 3, a representação padrão dos sets como strings sempre usa a notação {…}
,
exceto para o conjunto vazio:
>>> s = {1}
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s.pop()
1
>>> s
set()
A sintaxe do set
literal, como {1, 2, 3}
, é mais rápida e mais legível que uma chamada ao construtor (por exemplo, set([1, 2, 3])
).
Essa última forma é mais lenta porque, para avaliá-la, Python precisa buscar o nome set
para obter seu construtor,
daí criar uma lista e, finalmente, passá-la para o construtor.
Por outro lado, para processar um literal como {1, 2, 3}
,
o Python roda um bytecode especializado, BUILD_SET
.[41]
Não há sintaxe especial para representar literais frozenset
—eles só podem ser criados chamando seu construtor.
Sua representação padrão como string no Python 3 se parece com uma chamada ao construtor de frozenset
com um argumento set`
.
Observe a saída na sessão de console a seguir:
>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
E por falar em sintaxe, a ideia das listcomps foi adaptada para criar conjuntos também.
3.10.2. Compreensões de conjuntos
Compreensões de conjuntos (setcomps) apareceram há bastante tempo, no Python 2.7, junto com as dictcomps que vimos na Seção 3.2.1. O Exemplo 40 mostra procedimento.
>>> from unicodedata import name (1)
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} (2)
{'§', '=', '¢', '#', '¤', '<', '¥', 'µ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}
-
Importa a função
name
deunicodedata
para obter os nomes dos caracteres. -
Cria um conjunto de caracteres com códigos entre 32 e 255 que contenham a palavra
'SIGN'
em seus nomes.
A ordem da saída muda a cada processo Python, devido ao hash "salgado", mencionado na Seção 3.4.1.
Questões de sintaxe à parte, vamos considerar agora o comportamento dos conjuntos.
3.11. Consequências práticas da forma de funcionamento dos conjuntos
Os tipos set
e frozenset
são ambos implementados com um tabela de hash. Isso tem os seguintes efeitos:
-
Elementos de conjuntos tem que ser objetos hashable. Eles precisam implementar métodos
__hash__
e__eq__
adequados, como descrido na Seção 3.4.1. -
O teste de existência de um elemento é muito eficiente. Um conjunto pode ter milhões de elementos, mas um elemento pode ser localizado diretamente, computando o código hash da chave e derivando um deslocamento do índice, com o possível ônus de um pequeno número de tentativas até encontrar a entrada correspondente ou exaurir a busca.
-
Conjuntos usam mais memória se comparados aos simples ponteiros de um array para seus elementos—que é uma estrutura mais compacta, mas também muito mais lenta para buscas se seu tamanho cresce além de uns poucos elementos.
-
A ordem dos elementos depende da ordem de inserção, mas não de forma útil ou confiável. Se dois elementos são diferentes mas tem o mesmo código hash, sua posição depende de qual elemento foi inserido primeiro.
-
Acrescentar elementos a um conjunto muda a ordem dos elementos existentes. Isso ocorre porque o algoritmo se torna menos eficiente se a tabela de hash estiver com mais de dois terços de ocupação, então Python pode ter que mover e redimensionar a tabela conforme ela cresce. Quando isso acontece, os elementos são reinseridos e sua ordem relativa pode mudar.
Veja o post "Internals of sets and dicts" (EN) no fluentpython.com para maiores detalhes.
Agora vamos revisar a vasta seleção de operações oferecidas pelos conjuntos.
3.11.1. Operações de conjuntos
A Figura 9 dá
uma visão geral dos métodos disponíveis em conjuntos mutáveis e imutáveis. Muitos deles são métodos especiais que sobrecarregam operadores, tais como &
and >=
. A Tabela 8 mostra os operadores matemáticos de conjuntos que tem operadores ou métodos correspondentes no Python. Note que alguns operadores e métodos realizam mudanças no mesmo lugar sobre o conjunto alvo (por exemplo, &=
, difference_update
, etc.). Tais operações não fazem sentido no mundo ideal dos conjuntos matemáticos, e também não são implementadas em frozenset
.
👉 Dica
|
Os operadores infixos na Tabela 8 exigem que os dois operandos sejam conjuntos, mas todos os outros métodos recebem um ou mais argumentos iteráveis.
Por exemplo, para produzir a união de quatro coleções, |
MutableSet
e suas superclasses em collections.abc
(nomes em itálico são classes e métodos abstratos; métodos de operadores reversos foram omitidos por concisão).Math symbol | Python operator | Method | Description |
---|---|---|---|
S ∩ Z |
|
|
Intersecção de |
|
|
Operador |
|
|
Intersecção de |
||
|
|
|
|
|
|
||
S ∪ Z |
|
|
União de |
|
|
|
|
|
União de |
||
|
|
|
|
|
|
||
S \ Z |
|
|
Complemento relativo ou diferença entre |
|
|
Operador |
|
|
Diferença entre |
||
|
|
|
|
|
|
||
S ∆ Z |
|
|
Diferença simétrica (o complemento da intersecção |
|
|
Operador |
|
|
Complemento de |
||
|
|
|
|
|
|
A Tabela 9 lista predicados de conjuntos: operadores e métodos que devolvem True
ou False
.
Math symbol | Python operator | Method | Description |
---|---|---|---|
S ∩ Z = ∅ |
|
|
|
e ∈ S |
|
|
Elemento |
S ⊆ Z |
|
|
|
|
|
||
S ⊂ Z |
|
|
|
S ⊇ Z |
|
|
|
|
|
||
S ⊃ Z |
|
|
|
Além de operadores e métodos derivados da teoria matemática dos conjuntos, os tipos conjunto implementam outros métodos para tornar seu uso prático, resumidos na Tabela 10.
set | frozenset | ||
---|---|---|---|
|
● |
Adiciona elemento |
|
|
● |
Remove todos os elementos de |
|
|
● |
● |
Cópia rasa de |
|
● |
Remove elemento |
|
|
● |
● |
Obtém iterador de |
|
● |
● |
|
|
● |
Remove e devolve um elemento de |
|
|
● |
Remove elemento |
Isso encerra nossa visão geral dos recursos dos conjuntos. Como prometido na Seção 3.8, vamos agora ver como dois dos tipos de views de dicionários se comportam de forma muito similar a um frozenset
.
3.12. Operações de conjuntos em views de dict
A Tabela 11 mostra como os objetos view devolvidos pelos métodos .keys()
e .items()
de dict são notavelmente similares a um frozenset
.
frozenset | dict_keys | dict_items | Description | |
---|---|---|---|---|
|
● |
● |
● |
|
|
● |
● |
● |
operador |
|
● |
● |
● |
|
|
● |
Cópia rasa de |
||
|
● |
Diferença entre |
||
|
● |
Intersecção de |
||
|
● |
● |
● |
|
|
● |
|
||
|
● |
|
||
|
● |
● |
● |
obtém iterador para |
|
● |
● |
● |
|
|
● |
● |
● |
|
|
● |
● |
● |
Operador |
|
● |
● |
Obtém iterador para |
|
|
● |
● |
● |
Operador |
|
● |
● |
● |
|
|
● |
Complemento de |
||
|
● |
União de |
||
|
● |
● |
● |
|
|
● |
● |
● |
Operador |
Especificamente, dict_keys
e dict_items
implementam os métodos especiais para suportar as poderosas operações de conjuntos &
(intersecção), |
(união), -
(diferença), and ^
(diferença simétrica).
Por exemplo, usando &
é fácil obter as chaves que aparecem em dois dicionários:
>>> d1 = dict(a=1, b=2, c=3, d=4)
>>> d2 = dict(b=20, d=40, e=50)
>>> d1.keys() & d2.keys()
{'b', 'd'}
Observe que o valor devolvido por &
é um set
.
Melhor ainda: os operadores de conjuntos em views de dicionários são compatíveis com instâncias de set
.
Veja isso:
>>> s = {'a', 'e', 'i'}
>>> d1.keys() & s
{'a'}
>>> d1.keys() | s
{'a', 'c', 'b', 'd', 'i', 'e'}
⚠️ Aviso
|
Uma view obtida de Por outro lado, uma view devolvida por |
Usar operações de conjunto com views pode evitar a necessidade de muitos loops e ifs quando seu código precisa inspecionar o conteúdo de dicionários. Deixe a eficiente implementação de Python em C trabalhar para você!
Com isso, encerramos esse capítulo.
3.13. Resumo do capítulo
Dicionários são a pedra fundamental de Python.
Ao longo dos anos, a sintaxe literal familiar, {k1: v1, k2: v2}
, foi aperfeiçoada para suportar desempacotamento com **
e pattern matching, bem como com compreensões de dict
.
Além do dict
básico, a biblioteca padrão oferece mapeamentos práticos prontos para serem usados, como o defaultdict
, o ChainMap
, e o Counter
, todos definidos no módulo collections
. Com a nova implementação de dict
, o OrderedDict
não é mais tão útil quanto antes, mas deve permanecer na biblioteca padrão para manter a compatibilidade retroativa—e por suas características específicas ausentes em dict
, tal como a capacidade de levar em consideração o ordenamento das chaves em uma comparação ==
. Também no módulo collections
está o UserDict
, uma classe base fácil de usar na criação de mapeamentos personalizados.
Dois métodos poderosos disponíveis na maioria dos mapeamentos são setdefault
e update
. O método setdefault
pode atualizar itens que mantenham valores mutáveis—por exemplo, em um dict
de valores list
—evitando uma segunda busca pela mesma chave. O método update
permite inserir ou sobrescrever itens em massa a partir de qualquer outro mapeamento, desde iteráveis que forneçam pares (chave, valor)
até argumentos nomeados. Os construtores de mapeamentos também usam update
internamente, permitindo que instâncias sejam inicializadas a partir de outros mapeamentos, de iteráveis e de argumentos nomeados.
Desde Python 3.9 também podemos usar o operador |=
para atualizar uma mapeamento e
o operador |
para criar um novo mapeamento a partir a união de dois mapeamentos.
Um gancho elegante na API de mapeamento é o método __missing__
, que permite personalizar o que acontece quando uma chave não é encontrada ao se usar a sintaxe d[k]
syntax,
que invoca __getitem__
.
O módulo collections.abc
oferece as classes base abstratas Mapping
e MutableMapping
como interfaces padrão, muito úteis para checagem de tipo durante a execução.
O MappingProxyType
, do módulo types
, cria uma fachada imutável para um mapeamento que você precise proteger de modificações acidentais.
Existem também ABCs para Set
e MutableSet
.
Views de dicionários foram uma grande novidade no Python 3, eliminando o uso desnecessário de memória dos métodos .keys()
, .values()
, e .items()
de Python 2, que criavam listas duplicando os dados na instância alvo de dict
. Além disso, as classes dict_keys
e dict_items
suportam os operadores e métodos mais úteis de frozenset
.
3.14. Leitura complementar
Na documentação da Biblioteca Padrão de Python,
a seção "collections—Tipos de dados de contêineres"
inclui exemplos e receitas práticas para vários tipos de mapeamentos. O código-fonte de Python para o módulo, Lib/collections/__init__.py, é uma excelente referência para qualquer um que deseje criar novos tipos de mapeamentos ou entender a lógica dos tipos existentes. O capítulo 1 do Python Cookbook, 3rd ed. (O’Reilly), de David Beazley e Brian K. Jones traz 20 receitas práticas e perpicazes usando estruturas de dados—a maioria mostrando formas inteligentes de usar dict
.
Greg Gandenberger defende a continuidade do uso de collections.OrderedDict
,
com os argumentos de que "explícito é melhor que implícito," compatibilidade retroativa,
e o fato de algumas ferramentas e bibliotecas presumirem que a ordenação das chaves de um dict
é irrelevante—nesse post:
"Python Dictionaries Are Now Ordered. Keep Using OrderedDict" (Os dicionários de Python agora são ordenados. Continue a usar OrderedDict) (EN).
A PEP 3106—Revamping dict.keys(), .values() and .items() (Renovando dict.keys(), .values() e .items()) (EN) foi onde Guido van Rossum apresentou o recurso de views de dicionário para Python 3. No resumo, ele afirma que a ideia veio da Java Collections Framework.
O PyPy foi o primeiro interpretador Python a implementar a proposta de Raymond Hettinger de dicts compactos, e eles escreverem em seu blog sobre isso, em "Faster, more memory efficient and more ordered dictionaries on PyPy" (Dicionários mais rápidos, mais eficientes em termos de memória e mais ordenados no PyPy) (EN), reconhecendo que um layout similar foi adotado no PHP 7, como descrito em PHP’s new hashtable implementation (A nova implementação de tabelas de hash de PHP) (EN). É sempre muito bom quando criadores citam trabalhos anteriores de outros.
Na PyCon 2017, Brandon Rhodes apresentou "The Dictionary Even Mightier" (O dicionário, ainda mais poderoso) (EN), uma continuação de sua apresentação animada clássica "The Mighty Dictionary" (O poderoso dicionário) (EN)—incluindo colisões de hash animadas!
Outro vídeo atual mas mais aprofundado sobre o funcionamento interno do dict
de Python é "Modern Dictionaries" (Dicionários modernos) (EN) de Raymond Hettinger, onde ele nos diz que após inicialmente fracassar em convencer os desenvolvedores principais de Python sobre os dicts compactos, ele persuadiu a equipe do PyPy, eles os adotaram, a ideia ganhou força, e finalmente foi adicionada ao CPython 3.6 por INADA Naoki.
Para saber todos os detalhes, dê uma olhada nos extensos comentários no código-fonte do CPython para Objects/dictobject.c (EN) e no documento de design em Objects/dictnotes.txt (EN).
A justificativa para a adição de conjuntos ao Python está documentada na
PEP 218—Adding a Built-In Set Object Type (Adicionando um objeto embutido de tipo conjunto).
Quando a PEP 218 foi aprovada, nenhuma sintaxe literal especial foi adotada para conjuntos.
Os literais set
foram criados para Python 3 e implementados retroativamente no Python 2.7,
assim como as compreensões de dict
e set
.
Na PyCon 2019, apresentei
"Set Practice: learning from Python’s set types" (A Prática dos Conjuntos: aprendendo com os tipos conjunto de Python) (EN),
descrevendo casos de uso de conjuntos em programas reais, falando sobre o design de sua API, e sobre a implementação da uintset
, uma classe de conjunto para elementos inteiros, usando um vetor de bits ao invés de uma tabela de hash, inspirada por um exemplo do capítulo 6 do excelente The Go Programming Language (A Linguagem de Programação Go) (EN), de Alan Donovan e Brian Kernighan (Addison-Wesley).
A revista Spectrum, do IEEE, tem um artigo sobre Hans Peter Luhn, um prolífico inventor que patenteou um conjunto de cartões interligados que permitiam selecionar receitas de coquetéis a partir dos ingredientes disponíveis, entre inúmeras outras invenções, incluindo… tabelas de hash! Veja "Hans Peter Luhn and the Birth of the Hashing Algorithm" (Hans Peter Luhn e o Nascimento do Algoritmo de Hash).
4. Texto em Unicode versus Bytes
Humanos usam texto. Computadores falam em bytes.[44]
Python 3 introduziu uma forte distinção entre strings de texto humano e sequências de bytes puros. A conversão automática de sequências de bytes para texto Unicode ficou para trás no Python 2. Este capítulo trata de strings Unicode, sequências de bytes, e das codificações usadas para converter umas nas outras.
Dependendo do que você faz com Python, pode achar que entender o Unicode não é importante.
Isso é improvável, mas mesmo que seja o caso, não há como escapar da separação entre str
e bytes
,
que agora exige conversões explícitas.
Como um bônus, você descobrirá que os tipos especializados de sequências binárias bytes
e bytearray
oferecem recursos que a classe str
"pau para toda obra" de Python 2 não oferecia.
Nesse capítulo, veremos os seguintes tópicos:
-
Caracteres, pontos de código e representações binárias
-
Recursos exclusivos das sequências binárias:
bytes
,bytearray
, ememoryview
-
Codificando para o Unicode completo e para conjuntos de caracteres legados
-
Evitando e tratando erros de codificação
-
Melhores práticas para lidar com arquivos de texto
-
A armadilha da codificação default e questões de E/S padrão
-
Comparações seguras de texto Unicode com normalização
-
Funções utilitárias para normalização, case folding (equiparação maiúsculas/minúsculas) e remoção de sinais diacríticos por força bruta
-
Ordenação correta de texto Unicode com
locale
e a biblioteca pyuca -
Metadados de caracteres do banco de dados Unicode
-
APIs duais, que processam
str
ebytes
4.1. Novidades nesse capítulo
O suporte ao Unicode no Python 3 sempre foi muito completo e estável, então o acréscimo mais notável é a Seção 4.9.1, descrevendo um utilitário de linha de comando para busca no banco de dados Unicode—uma forma de encontrar gatinhos sorridentes ou hieróglifos do Egito antigo.
Vale a pena mencionar que o suporte a Unicode no Windows ficou melhor e mais simples desde Python 3.6, como veremos na Seção 4.6.1.
Vamos começar então com os conceitos não-tão-novos mas fundamentais de caracteres, pontos de código e bytes.
✒️ Nota
|
Para essa segunda edição, expandi a seção sobre o módulo Lá você também vai encontrar o "Building Multi-character Emojis" (Criando emojis multi-caractere) (EN), descrevendo como combinar caracteres Unicode para criar bandeiras de países, bandeiras de arco-íris, pessoas com tonalidades de pele diferentes e ícones de diferentes tipos de famílias. |
4.2. Questões de caracteres
O conceito de "string" é bem simples: uma string é uma sequência de caracteres. O problema está na definição de "caractere".
Em 2023, a melhor definição de "caractere" que temos é um caractere Unicode. Consequentemente, os itens que compõe um str
de Python 3 são caracteres Unicode, como os itens de um objeto unicode
no Python 2. Em contraste, os itens de uma str
no Python 2 são bytes, assim como os itens num objeto bytes
de Python 3.
O padrão Unicode separa explicitamente a identidade dos caracteres de representações binárias específicas:
-
A identidade de um caractere é chamada de ponto de código (code point). É um número de 0 a 1.114.111 (na base 10), representado no padrão Unicode na forma de 4 a 6 dígitos hexadecimais precedidos pelo prefixo "U+", de U+0000 a U+10FFFF. Por exemplo, o ponto de código da letra A é U+0041, o símbolo do Euro é U+20AC, e o símbolo musical da clave de sol corresponde ao ponto de código U+1D11E. Cerca de 13% dos pontos de código válidos tem caracteres atribuídos a si no Unicode 13.0.0, a versão do padrão usada no Python 3.10.
-
Os bytes específicos que representam um caractere dependem da codificação (encoding) usada. Uma codificação, nesse contexto, é um algoritmo que converte pontos de código para sequências de bytes, e vice-versa. O ponto de código para a letra A (U+0041) é codificado como um único byte,
\x41
, na codificação UTF-8, ou como os bytes\x41\x00
na codificação UTF-16LE. Em um outro exemplo, o UTF-8 exige três bytes para codificar o símbolo do Euro (U+20AC):\xe2\x82\xac
. Mas no UTF-16LE o mesmo ponto de código é U+20AC representado com dois bytes:\xac\x20
.
Converter pontos de código para bytes é codificar; converter bytes para pontos de código é decodificar. Veja o Exemplo 41.
>>> s = 'café'
>>> len(s) # (1)
4
>>> b = s.encode('utf8') # (2)
>>> b
b'caf\xc3\xa9' # (3)
>>> len(b) # (4)
5
>>> b.decode('utf8') # (5)
'café'
-
A
str
'café'
tem quatro caracteres Unicode. -
Codifica
str
parabytes
usando a codificação UTF-8. -
bytes
literais são prefixados com umb
. -
bytes
b
tem cinco bytes (o ponto de código para "é" é codificado com dois bytes em UTF-8). -
Decodifica
bytes
parastr
usando a codificação UTF-8.
👉 Dica
|
Um jeito fácil de sempre lembrar a distinção entre |
Apesar do str
de Python 3 ser quase o tipo unicode
de Python 2 com um novo nome,
o bytes
de Python 3 não é meramente o velho str
renomeado,
e há também o tipo estreitamente relacionado bytearray
.
Então vale a pena examinar os tipos de sequências binárias antes de avançar para questões de codificação/decodificação.
4.3. Os fundamentos do byte
Os novos tipos de sequências binárias são diferentes do str
de Python 2 em vários aspectos.
A primeira coisa importante é que existem dois tipos embutidos básicos de sequências binárias:
o tipo imutável bytes
, introduzido no Python 3, e o tipo mutável bytearray
,
introduzido há tempos, no Python 2.6[45]. A documentação de Python algumas vezes usa o termo genérico "byte string" (string de bytes, na documentação em português) para se referir a bytes
e bytearray
.
Cada item em bytes
ou bytearray
é um inteiro entre 0 e 255,
e não uma string de um caractere, como no str
de Python 2.
Entretanto, uma fatia de uma sequência binária sempre produz uma sequência binária do mesmo tipo—incluindo fatias de tamanho 1. Veja o Exemplo 42.
bytes e como `bytearray
>>> cafe = bytes('café', encoding='utf_8') (1)
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] (2)
99
>>> cafe[:1] (3)
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr (4)
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] (5)
bytearray(b'\xa9')
-
bytes
pode ser criado a partir de umastr
, dada uma codificação. -
Cada item é um inteiro em
range(256)
. -
Fatias de
bytes
também sãobytes
—mesmo fatias de um único byte. -
Não há uma sintaxe literal para
bytearray
: elas aparecem comobytearray()
com um literalbytes
como argumento. -
Uma fatia de
bytearray
também é umabytearray
.
⚠️ Aviso
|
O fato de |
Apesar de sequências binárias serem na verdade sequências de inteiros, sua notação literal reflete o fato delas frequentemente embutirem texto ASCII. Assim, quatro formas diferentes de apresentação são utilizadas, dependendo do valor de cada byte:
-
Para bytes com código decimais de 32 a 126—do espaço ao
~
(til)—é usado o próprio caractere ASCII. -
Para os bytes correspondendo ao tab, à quebra de linha, ao carriage return (CR) e à
\
, são usadas as sequências de escape\t
,\n
,\r
, e\\
. -
Se os dois delimitadores de string,
'
e"
, aparecem na sequência de bytes, a sequência inteira é delimitada com'
, e qualquer'
dentro da sequência é precedida do caractere de escape, assim\'
.[46] -
Para qualquer outro valor do byte, é usada uma sequência de escape hexadecimal (por exemplo,
\x00
é o byte nulo).
É por isso que no Exemplo 42 vemos b’caf\xc3\xa9'
:
os primeiros três bytes, b’caf'
, estão na faixa de impressão do ASCII, ao contrário dos dois últimos.
Tanto bytes
quanto bytearray
suportam todos os métodos de str
, exceto aqueles relacionados a formatação (format
, format_map
)
e aqueles que dependem de dados Unicode, incluindo casefold
, isdecimal
, isidentifier
, isnumeric
, isprintable
, e encode
.
Isso significa que você pode usar os métodos conhecidos de string, como endswith
, replace
, strip
, translate
, upper
e dezenas de outros, com sequências binárias—mas com argumentos bytes
em vez de str
.
Além disso, as funções de expressões regulares no módulo re
também funcionam com sequências binárias, se a regex for compilada a partir de uma sequência binária ao invés de uma str
.
Desde Python 3.5, o operador %
voltou a funcionar com sequências binárias.[47]
As sequências binárias tem um método de classe que str
não possui, chamado fromhex
, que cria uma sequência binária a partir da análise de pares de dígitos hexadecimais, separados opcionalmente por espaços:
>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'
As outras formas de criar instâncias de bytes
ou bytearray
são chamadas a seus construtores com:
-
Uma
str
e um argumento nomeadoencoding
-
Um iterável que forneça itens com valores entre 0 e 255
-
Um objeto que implemente o protocolo de buffer (por exemplo
bytes
,bytearray
,memoryview
,array.array
), que copia os bytes do objeto fonte para a recém-criada sequência binária
⚠️ Aviso
|
Até Python 3.5, era possível chamar |
Criar uma sequência binária a partir de um objeto tipo buffer é uma operação de baixo nível que pode envolver conversão de tipos. Veja uma demonstração no Exemplo 43.
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2]) (1)
>>> octets = bytes(numbers) (2)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' (3)
-
O typecode
'h'
cria umarray
de short integers (inteiros de 16 bits). -
octets
mantém uma cópia dos bytes que compõemnumbers
. -
Esses são os 10 bytes que representam os 5 inteiros pequenos.
Criar um objeto bytes
ou bytearray
a partir de qualquer fonte tipo buffer vai sempre copiar os bytes. Já objetos memoryview
permitem compartilhar memória entre estruturas de dados binários, como vimos na Seção 2.10.2.
Após essa exploração básica dos tipos de sequências de bytes de Python, vamos ver como eles são convertidos de e para strings.
4.4. Codificadores/Decodificadores básicos
A distribuição de Python inclui mais de 100 codecs (encoders/decoders, _codificadores/decodificadores) para conversão de texto para bytes e vice-versa.
Cada codec tem um nome, como 'utf_8'
, e muitas vezes apelidos, tais como 'utf8'
, 'utf-8'
, e 'U8'
,
que você pode usar como o argumento de codificação em funções como
open()
, str.encode()
, bytes.decode()
, e assim por diante.
O Exemplo 44 mostra o mesmo texto codificado como três sequências de bytes diferentes.
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
... print(codec, 'El Niño'.encode(codec), sep='\t')
...
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
A Figura 10 mostra um conjunto de codecs gerando bytes a partir de caracteres como a letra "A" e o símbolo musical da clave de sol. Observe que as últimas três codificações tem bytes múltiplos e tamanho variável.
Aqueles asteriscos todos na Figura 10 deixam claro que algumas codificações, como o ASCII e mesmo o multi-byte GB2312, não conseguem representar todos os caracteres Unicode. As codificações UTF, por outro lado, foram projetadas para lidar com todos os pontos de código do Unicode.
As codificações apresentadas na Figura 10 foram escolhidas para montar uma amostra representativa:
latin1
a.k.a.iso8859_1
-
Importante por ser a base de outras codificações,tal como a
cp1252
e o próprio Unicode (observe que os valores binários dolatin1
aparecem nos bytes docp1252
e até nos pontos de código). cp1252
-
Um superconjunto útil de
latin1
, criado pela Microsoft, acrescentando símbolos convenientes como as aspas curvas e o € (euro); alguns aplicativos de Windows chamam essa codificação de "ANSI", mas ela nunca foi um padrão ANSI real. cp437
-
O conjunto de caracteres original do IBM PC, com caracteres de desenho de caixas. Incompatível com o
latin1
, que surgiu depois. gb2312
-
Padrão antigo para codificar ideogramas chineses simplificados usados na República da China; uma das várias codificações muito populares para línguas asiáticas.
utf-8
-
De longe a codificação de 8 bits mais comum na web. Em julho de 2021, o "W3Techs: Usage statistics of character encodings for websites" afirma que 97% dos sites usam UTF-8, um grande avanço sobre os 81,4% de setembro de 2014, quando escrevi este capítulo na primeira edição.
utf-16le
-
Uma forma do esquema de codificação UTF de 16 bits; todas as codificações UTF-16 suportam pontos de código acima de U+FFFF, através de sequências de escape chamadas "pares substitutos".
⚠️ Aviso
|
A UTF-16 sucedeu a codificação de 16 bits original do Unicode 1.0—a UCS-2—há muito tempo, em 1996. A UCS-2 ainda é usada em muitos sistemas, apesar de ter sido descontinuada ainda no século passado, por suportar apenas ponto de código até U+FFFF. Em 2021, mas de 57% dos pontos de código alocados estava acima de U+FFFF, incluindo os importantíssimos emojis. |
Após completar essa revisão das codificações mais comuns, vamos agora tratar das questões relativas a operações de codificação e decodificação.
4.5. Entendendo os problemas de codificação/decodificação
Apesar de existir uma exceção genérica, UnicodeError
, o erro relatado pelo Python em geral é mais específico:
ou é um UnicodeEncodeError
(ao converter uma str
para sequências binárias) ou é um UnicodeDecodeError
(ao ler uma sequência binária para uma str
).
Carregar módulos de Python também pode geram um SyntaxError
, quando a codificação da fonte for inesperada.
Vamos ver como tratar todos esses erros nas próximas seções.
👉 Dica
|
A primeira coisa a observar quando aparece um erro de Unicode é o tipo exato da exceção.
É um |
4.5.1. Tratando o UnicodeEncodeError
A maioria dos codecs não-UTF entendem apenas um pequeno subconjunto dos caracteres Unicode.
Ao converter texto para bytes, um UnicodeEncodeError
será gerado se um caractere não estiver definido na codificação alvo, a menos que seja fornecido um tratamento especial, passando um argumento errors
para o método ou função de codificação.
O comportamento para tratamento de erro é apresentado no Exemplo 45.
>>> city = 'São Paulo'
>>> city.encode('utf_8') (1)
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1') (2)
b'S\xe3o Paulo'
>>> city.encode('cp437') (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore') (4)
b'So Paulo'
>>> city.encode('cp437', errors='replace') (5)
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace') (6)
b'São Paulo'
-
As codificações UTF lidam com qualquer
str
-
iso8859_1
também funciona com a string'São Paulo'
. -
cp437
não consegue codificar o'ã'
("a" com til). O método default de tratamento de erro, —'strict'
—gera umUnicodeEncodeError
. -
O método de tratamento
errors='ignore'
pula os caracteres que não podem ser codificados; isso normalmente é uma péssima ideia, levando a perda silenciosa de informação. -
Ao codificar,
errors='replace'
substitui os caracteres não-codificáveis por um'?'
; aqui também há perda de informação, mas os usuários recebem um alerta de que algo está faltando. -
'xmlcharrefreplace'
substitui os caracteres não-codificáveis por uma entidade XML. Se você não pode usar UTF e não pode perder informação, essa é a única opção.
✒️ Nota
|
O tratamento de erros de |
O ASCII é um subconjunto comum a todas as codificações que conheço, então a codificação deveria sempre funcionar se o texto for composto exclusivamente por caracteres ASCII.
Python 3.7 trouxe um novo método booleano, str.isascii()
, para verificar se seu texto Unicode é 100% ASCII.
Se for, você deve ser capaz de codificá-lo para bytes em qualquer codificação sem gerar um UnicodeEncodeError
.
4.5.2. Tratando o UnicodeDecodeError
Nem todo byte contém um caractere ASCII válido, e nem toda sequência de bytes é um texto codificado em UTF-8 ou UTF-16 válidos; assim, se você presumir uma dessas codificações ao converter um sequência binária para texto, pode receber um UnicodeDecodeError
, se bytes inesperados forem encontrados.
Por outro lado, várias codificações de 8 bits antigas, como a 'cp1252'
, a 'iso8859_1'
e a 'koi8_r'
são capazes de decodificar qualquer série de bytes, incluindo ruído aleatório, sem reportar qualquer erro. Portanto, se seu programa presumir a codificação de 8 bits errada, ele vai decodificar lixo silenciosamente.
👉 Dica
|
Caracteres truncados ou distorcidos são conhecidos como "gremlins" ou "mojibake" (文字化け—"texto modificado" em japonês). |
O Exemplo 46 ilustra a forma como o uso do codec errado pode produzir gremlins ou um UnicodeDecodeError
.
str
para bytes: sucesso e tratamento de erro>>> octets = b'Montr\xe9al' (1)
>>> octets.decode('cp1252') (2)
'Montréal'
>>> octets.decode('iso8859_7') (3)
'Montrιal'
>>> octets.decode('koi8_r') (4)
'MontrИal'
>>> octets.decode('utf_8') (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace') (6)
'Montr�al'
-
A palavra "Montréal" codificada em
latin1
;'\xe9'
é o byte para "é". -
Decodificar com Windows 1252 funciona, pois esse codec é um superconjunto de
latin1
. -
ISO-8859-7 foi projetado para a língua grega, então o byte
'\xe9'
é interpretado de forma incorreta, e nenhum erro é gerado. -
KOI8-R é foi projetado para o russo. Agora
'\xe9'
significa a letra "И" do alfabeto cirílico. -
O codec
'utf_8'
detecta queoctets
não é UTF-8 válido, e gera umUnicodeDecodeError
. -
Usando
'replace'
para tratamento de erro, o\xe9
é substituído por "�" (ponto de código #U+FFFD), o caractere oficial do Unicode chamadoREPLACEMENT CHARACTER
, criado exatamente para representar caracteres desconhecidos.
4.5.3. O SyntaxError ao carregar módulos com codificação inesperada
UTF-8 é a codificação default para fontes no Python 3, da mesma forma que ASCII era o default no Python 2. Se você carregar um módulo .py contendo dados que não estejam em UTF-8, sem declaração codificação, receberá uma mensagem como essa:
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line 1, but no encoding declared; see https://python.org/dev/peps/pep-0263/ for details
Como o UTF-8 está amplamente instalado em sistemas GNU/Linux e macOS,
um cenário onde isso tem mais chance de ocorrer é na abertura de um arquivo .py criado no Windows, com cp1252
.
Observe que esse erro ocorre mesmo no Python para Windows, pois a codificação default para fontes de Python 3 é UTF-8 em todas as plataformas.
Para resolver esse problema, acrescente o comentário mágico coding
no início do arquivo, como no Exemplo 47.
# coding: cp1252
print('Olá, Mundo!')
👉 Dica
|
Agora que o código fonte de Python 3 não está mais limitado ao ASCII, e por default usa a excelente codificação UTF-8, a melhor "solução" para código fonte em codificações antigas como |
Suponha que você tem um arquivo de texto, seja ele código-fonte ou poesia, mas não sabe qual codificação foi usada. Como detectar a codificação correta? Respostas na próxima seção.
4.5.4. Como descobrir a codificação de uma sequência de bytes
Como descobrir a codificação de uma sequência de bytes? Resposta curta: não é possível. Você precisa ser informado.
Alguns protocolos de comunicação e formatos de arquivo, como o HTTP e o XML, contêm cabeçalhos que nos dizem explicitamente como o conteúdo está codificado. Você pode ter certeza que algumas sequências de bytes não estão em ASCII, pois elas contêm bytes com valores acima de 127, e o modo como o UTF-8 e o UTF-16 são construídos também limita as sequências de bytes possíveis.
Entretanto, considerando que as linguagens humanas também tem suas regras e restrições, uma vez que você supõe que uma série de bytes é um texto humano simples,
pode ser possível intuir sua codificação usando heurística e estatística.
Por exemplo, se bytes com valor b'\x00'
bytes forem comuns, é provável que seja uma codificação de 16 ou 32 bits, e não um esquema de 8 bits, pois caracteres nulos em texto simples são erros.
Quando a sequência de bytes `b'\x20\x00'` aparece com frequência, é mais provável que esse seja o caractere de espaço (U+0020) na codificação UTF-16LE, e não o obscuro caractere U+2000 (EN QUAD
)—seja lá o que for isso.
É assim que o pacote "Chardet—The Universal Character Encoding Detector (Chardet—O Detector Universal de Codificações de Caracteres)" trabalha para descobrir cada uma das mais de 30 codificações suportadas.
Chardet é uma biblioteca Python que pode ser usada em seus programas, mas que também inclui um utilitário de comando de linha, chardetect
.
Aqui está a forma como ele analisa o código fonte desse capítulo:
$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99
Apesar de sequências binárias de texto codificado normalmente não trazerem dicas sobre sua codificação, os formatos UTF podem preceder o conteúdo textual por um marcador de ordem dos bytes. Isso é explicado a seguir.
4.5.5. BOM: um gremlin útil
No Exemplo 44, você pode ter notado um par de bytes extra no início de uma sequência codificada em UTF-16. Aqui estão eles novamente:
>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
Os bytes são b'\xff\xfe'
. Isso é um BOM—sigla para byte-order mark (marcador de ordem de bytes)—indicando a ordenação de bytes "little-endian" da CPU Intel onde a codificação foi realizada.
Em uma máquina little-endian, para cada ponto de código, o byte menos significativo aparece primeiro:
a letra 'E'
, ponto de código U+0045 (decimal 69), é codificado nas posições 2 e 3 dos bytes como 69
e 0
:
>>> list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
Em uma CPU big-endian, a codificação seria invertida; 'E'
seria codificado como 0
e 69
.
Para evitar confusão, a codificação UTF-16 precede o texto a ser codificado com o caractere especial invisível ZERO WIDTH NO-BREAK SPACE
(U+FEFF).
Em um sistema little-endian, isso é codificado como b'\xff\xfe'
(decimais 255, 254).
Como, por design, não existe um caractere U+FFFE em Unicode, a sequência de bytes b'\xff\xfe'
tem que ser o ZERO WIDTH NO-BREAK SPACE
em uma codificação little-endian,
e então o codec sabe qual ordenação de bytes usar.
Há uma variante do UTF-16—o UTF-16LE—que é explicitamente little-endian, e outra que é explicitamente big-endian, o UTF-16BE. Se você usá-los, um BOM não será gerado:
>>> u16le = 'El Niño'.encode('utf_16le')
>>> list(u16le)
[69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
>>> u16be = 'El Niño'.encode('utf_16be')
>>> list(u16be)
[0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]
Se o BOM estiver presente, supõe-se que ele será filtrado pelo codec UTF-16,
então recebemos apenas o conteúdo textual efetivo do arquivo, sem o ZERO WIDTH NO-BREAK SPACE
inicial.
O padrão Unicode diz que se um arquivo é UTF-16 e não tem um BOM, deve-se presumir que ele é UTF-16BE (big-endian). Entretanto, a arquitetura x86 da Intel é little-endian, daí que há uma grande quantidade de UTF-16 little-endian e sem BOM no mundo.
Toda essa questão de ordenação dos bytes (endianness) só afeta codificações que usam palavras com mais de um byte, como UTF-16 e UTF-32.
Uma grande vantagem do UTF-8 é produzir a mesma sequência independente da ordenação dos bytes, então um BOM não é necessário.
No entanto, algumas aplicações Windows (em especial o Notepad) mesmo assim acrescentam o BOM a arquivos UTF-8—e o Excel depende do BOM para detectar um arquivo UTF-8, caso contrário ele presume que o conteúdo está codificado com uma página de código do Windows.
Essa codificação UTF-8 com BOM é chamada UTF-8-SIG no registro de codecs de Python.
O caractere U+FEFF codificado em UTF-8-SIG é a sequência de três bytes b'\xef\xbb\xbf'
.
Então, se um arquivo começa com aqueles três bytes, é provavelmente um arquivo UTF-8 com um BOM.
👉 Dica
|
A dica de Caleb sobre o UTF-8-SIG
Caleb Hattingh—um dos revisores técnicos—sugere sempre usar o codec UTF-8-SIG para ler arquivos UTF-8. Isso é inofensivo, pois o UTF-8-SIG lê corretamente arquivos com ou sem um BOM, e não devolve o BOM propriamente dito.
Para escrever arquivos, recomendo usar UTF-8, para interoperabilidade integral.
Por exemplo, scripts Python podem ser tornados executáveis em sistemas Unix, se começarem com o comentário: |
Vamos agora ver como tratar arquivos de texto no Python 3.
4.6. Processando arquivos de texto
A melhor prática para lidar com E/S de texto é o "Sanduíche de Unicode" (Unicode sandwich)
(Figura 11).[48]
Isso significa que os bytes
devem ser decodificados para str
o mais cedo possível na entrada
(por exemplo, ao abrir um arquivo para leitura).
O "recheio" do sanduíche é a lógica do negócio de seu programa,
onde o tratamento do texto é realizado exclusivamente sobre objetos str
.
Você nunca deveria codificar ou decodificar no meio de outro processamento.
Na saída, as str
são codificadas para bytes
o mais tarde possível.
A maioria dos frameworks web funciona assim, e raramente tocamos em bytes
ao usá-los.
No Django, por exemplo, suas views devem produzir str
em Unicode; o próprio Django se encarrega de codificar a resposta para bytes
, usando UTF-8 como default.
Python 3 torna mais fácil seguir o conselho do sanduíche de Unicode, pois o embutido open()
executa a decodificação necessária na leitura e a codificação ao escrever arquivos em modo texto. Dessa forma, tudo que você recebe de my_file.read()
e passa para my_file.write(text)
são objetos str
.
Assim, usar arquivos de texto é aparentemente simples. Mas se você confiar nas codificações default, pode acabar levando uma mordida.
Observe a sessão de console no Exemplo 48. Você consegue ver o erro?
>>> open('cafe.txt', 'w', encoding='utf_8').write('café')
4
>>> open('cafe.txt').read()
'café'
O erro: especifiquei a codificação UTF-8 ao escrever o arquivo, mas não fiz isso na leitura, então Python assumiu a codificação de arquivo default do Windows—página de código 1252—e os bytes finais foram decodificados como os caracteres 'é'
ao invés de 'é'
.
Executei o Exemplo 48 no Python 3.8.1, 64 bits, no Windows 10 (build 18363). Os mesmos comandos rodando em um GNU/Linux ou um macOS recentes funcionam perfeitamente, pois a codificação default desses sistemas é UTF-8, dando a falsa impressão que tudo está bem. Se o argumento de codificação fosse omitido ao abrir o arquivo para escrita, a codificação default do locale seria usada, e poderíamos ler o arquivo corretamente usando a mesma codificação. Mas aí o script geraria arquivos com conteúdo binário diferente dependendo da plataforma, ou mesmo das configurações do locale na mesma plataforma, criando problemas de compatibilidade.
👉 Dica
|
Código que precisa rodar em múltiplas máquinas ou múltiplas ocasiões não deveria jamais depender de defaults de codificação.
Sempre passe um argumento |
Um detalhe curioso no Exemplo 48 é que a função write
na primeira instrução informa que foram escritos quatro caracteres, mas na linha seguinte são lidos cinco caracteres.
O Exemplo 49 é uma versão estendida do Exemplo 48, e explica esse e outros detalhes.
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp # (1)
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café') # (2)
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size # (3)
5
>>> fp2 = open('cafe.txt')
>>> fp2 # (4)
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding # (5)
'cp1252'
>>> fp2.read() # (6)
'café'
>>> fp3 = open('cafe.txt', encoding='utf_8') # (7)
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read() # (8)
'café'
>>> fp4 = open('cafe.txt', 'rb') # (9)
>>> fp4 # (10)
<_io.BufferedReader name='cafe.txt'>
>>> fp4.read() # (11)
b'caf\xc3\xa9'
-
Por default,
open
usa o modo texto e devolve um objetoTextIOWrapper
com uma codificação específica. -
O método
write
de umTextIOWrapper
devolve o número de caracteres Unicode escritos. -
os.stat
diz que o arquivo tem 5 bytes; o UTF-8 codifica'é'
com 2 bytes, 0xc3 e 0xa9. -
Abrir um arquivo de texto sem uma codificação explícita devolve um
TextIOWrapper
com a codificação configurada para um default do locale. -
Um objeto
TextIOWrapper
tem um atributo de codificação que pode ser inspecionado: neste caso,cp1252
. -
Na codificação
cp1252
do Windows, o byte 0xc3 é um "Ã" (A maiúsculo com til), e 0xa9 é o símbolo de copyright. -
Abrindo o mesmo arquivo com a codificação correta.
-
O resultado esperado: os mesmo quatro caracteres Unicode para
'café'
. -
A flag
'rb'
abre um arquivo para leitura em modo binário. -
O objeto devolvido é um
BufferedReader
, e não umTextIOWrapper
. -
Ler do arquivo obtém bytes, como esperado.
👉 Dica
|
Não abra arquivos de texto no modo binário, a menos que seja necessário analisar o conteúdo do arquivo para determinar sua codificação—e mesmo assim, você deveria estar usando o Chardet em vez de reinventar a roda (veja a Seção 4.5.4). Programas comuns só deveriam usar o modo binário para abrir arquivos binários, como arquivos de imagens raster ou bitmaps. |
O problema no Exemplo 49 vem de se confiar numa configuração default ao se abrir um arquivo de texto. Há várias fontes de tais defaults, como mostra a próxima seção.
4.6.1. Cuidado com os defaults de codificação
Várias configurações afetam os defaults de codificação para E/S no Python. Veja o script default_encodings.py script no Exemplo 50.
import locale
import sys
expressions = """
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""
my_file = open('dummy', 'w')
for expression in expressions.split():
value = eval(expression)
print(f'{expression:>30} -> {value!r}')
A saída do Exemplo 50 no GNU/Linux (Ubuntu 14.04 a 19.10) e no macOS (10.9 a 10.14) é idêntica, mostrando que UTF-8
é usado em toda parte nesses sistemas:
$ python3 default_encodings.py
locale.getpreferredencoding() -> 'UTF-8'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'UTF-8'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
No Windows, porém, a saída é o Exemplo 51.
> chcp (1)
Active code page: 437
> python default_encodings.py (2)
locale.getpreferredencoding() -> 'cp1252' (3)
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp1252' (4)
sys.stdout.isatty() -> True (5)
sys.stdout.encoding -> 'utf-8' (6)
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
-
chcp
mostra a página de código ativa para o console:437
. -
Executando default_encodings.py, com a saída direcionada para o console.
-
locale.getpreferredencoding()
é a configuração mais importante. -
Arquivos de texto usam`locale.getpreferredencoding()` por default.
-
A saída está direcionada para o console, então
sys.stdout.isatty()
éTrue
. -
Agora,
sys.stdout.encoding
não é a mesma que a página de código informada porchcp
!
O suporte a Unicode no próprio Windows e no Python para Windows melhorou desde que escrevi a primeira edição deste livro.
O Exemplo 51 costumava informar quatro codificações diferentes no Python 3.4 rodando no Windows 7.
As codificações para stdout
, stdin
, e stderr
costumavam ser iguais à da página de código ativa informada pelo comando chcp
, mas agora são todas utf-8
, graças à PEP 528—Change Windows console encoding to UTF-8 (Mudar a codificação do console no Windows para UTF-8) (EN), implementada no Python 3.6, e ao suporte a Unicode no PowerShell do cmd.exe (desde o Windows 1809, de outubro de 2018).[49]
É esquisito que o chcp
e o sys.stdout.encoding
reportem coisas diferentes quando o stdout
está escrevendo no console, mas é ótimo podermos agora escrever strings Unicode sem erros de codificação no Windows—a menos que o usuário redirecione a saída para um arquivo, como veremos adiante.
Isso não significa que todos os seus emojis favoritos vão aparecer: isso também depende da fonte usada pelo console.
Outra mudança foi a PEP 529—Change Windows filesystem encoding to UTF-8 (Mudar a codificação do sistema de arquivos do Windows para UTF-8), também implementada no Python 3.6, que modificou a codificação do sistema de arquivos (usada para representar nomes de diretórios e de arquivos), da codificação proprietária MBCS da Microsoft para UTF-8.
Entretanto, se a saída do Exemplo 50 for redirecionada para um arquivo, assim…
Z:\>python default_encodings.py > encodings.log
…aí o valor de sys.stdout.isatty()
se torna False
, e sys.stdout.encoding
é determinado por locale.getpreferredencoding()
, 'cp1252'
naquela máquina—mas sys.stdin.encoding
e sys.stderr.encoding
seguem como utf-8
.
👉 Dica
|
No Exemplo 52, usei a expressão de escape |
Isso significa que um script como o Exemplo 52 funciona quando está escrevendo no console, mas pode falhar quando a saída é redirecionada para um arquivo.
import sys
from unicodedata import name
print(sys.version)
print()
print('sys.stdout.isatty():', sys.stdout.isatty())
print('sys.stdout.encoding:', sys.stdout.encoding)
print()
test_chars = [
'\N{HORIZONTAL ELLIPSIS}', # exists in cp1252, not in cp437
'\N{INFINITY}', # exists in cp437, not in cp1252
'\N{CIRCLED NUMBER FORTY TWO}', # not in cp437 or in cp1252
]
for char in test_chars:
print(f'Trying to output {name(char)}:')
print(char)
O Exemplo 52 mostra o resultado de uma chamada a sys.stdout.isatty()
, o valor de sys.stdout.encoding
, e esses três caracteres:
-
'…'
HORIZONTAL ELLIPSIS
—existe no CP 1252 mas não no CP 437. -
'∞'
INFINITY
—existe no CP 437 mas não no CP 1252. -
'㊷'
CIRCLED NUMBER FORTY TWO
—não existe nem no CP 1252 nem no CP 437.
Quando executo o stdout_check.py no PowerShell ou no cmd.exe, funciona como visto na Figura 12.
Apesar de chcp
informar o código ativo como 437, sys.stdout.encoding
é UTF-8, então tanto HORIZONTAL ELLIPSIS
quanto INFINITY
são escritos corretamente.
O CIRCLED NUMBER FORTY TWO
é substituído por um retângulo, mas nenhum erro é gerado.
Presume-se que ele seja reconhecido como um caractere válido, mas a fonte do console não tem o glifo para mostrá-lo.
Entretanto, quando redireciono a saída de stdout_check.py para um arquivo, o resultado é o da Figura 13.
O primeiro problema demonstrado pela Figura 13 é o UnicodeEncodeError
mencionando o caractere '\u221e'
,
porque sys.stdout.encoding
é 'cp1252'
—uma página de código que não tem o caractere INFINITY
.
Lendo out.txt com o comando type
—ou um editor de Windows como o VS Code ou o Sublime Text—mostra que, ao invés do HORIZONTAL ELLIPSIS, consegui um 'à'
(LATIN SMALL LETTER A WITH GRAVE
).
Acontece que o valor binário 0x85 no CP 1252 significa '…'
, mas no CP 437 o mesmo valor binário representa o 'à'
.
Então, pelo visto, a página de código ativa tem alguma importância, não de uma forma razoável ou útil, mas como uma explicação parcial para uma experiência ruim com o Unicode.
✒️ Nota
|
Para realizar esses experimentos, usei um laptop configurado para o mercado norte-americano, rodando Windows 10 OEM. Versões de Windows localizadas para outros países podem ter configurações de codificação diferentes. No Brasil, por exemplo, o console do Windows usa a página de código 850 por default—e não a 437. |
Para encerrar esse enlouquecedor tópico de codificações default, vamos dar uma última olhada nas diferentes codificações no Exemplo 51:
-
Se você omitir o argumento
encoding
ao abrir um arquivo, o default é dado porlocale.getpreferredencoding()
('cp1252'
no Exemplo 51). -
Antes de Python 3.6, a codificação de
sys.stdout|stdin|stderr
costumava ser determinada pela variável do ambientePYTHONIOENCODING
—agora essa variável é ignorada, a menos quePYTHONLEGACYWINDOWSSTDIO
seja definida como uma string não-vazia. Caso contrário, a codificação da E/S padrão será UTF-8 para E/S interativa, ou definida porlocale.getpreferredencoding()
, se a entrada e a saída forem redirecionadas para ou de um arquivo. -
sys.getdefaultencoding()
é usado internamente pelo Python em conversões implícitas de dados binários de ou parastr
. Não há suporte para mudar essa configuração. -
sys.getfilesystemencoding()
é usado para codificar/decodificar nomes de arquivo (mas não o conteúdo dos arquivos). Ele é usado quandoopen()
recebe um argumentostr
para um nome de arquivo; se o nome do arquivo é passado como um argumentobytes
, ele é entregue sem modificação para a API do sistema operacional.
✒️ Nota
|
Já faz muito anos que, no GNU/Linux e no macOS, todas essas codificações são definidas como UTF-8 por default,
então a E/S entende e exibe todos os caracteres Unicode.
No Windows, não apenas codificações diferentes são usadas no mesmo sistema,
elas também são, normalmente, páginas de código como |
Resumindo, a configuração de codificação mais importante devolvida por locale.getpreferredencoding()
é a default para abrir arquivos de texto e para sys.stdout/stdin/stderr
, quando eles são redirecionados para arquivos.
Entretanto, a documentação diz (em parte):
locale.getpreferredencoding(do_setlocale=True)
Retorna a codificação da localidade usada para dados de texto, de acordo com as preferências do usuário. As preferências do usuário são expressas de maneira diferente em sistemas diferentes e podem não estar disponíveis programaticamente em alguns sistemas, portanto, essa função retorna apenas uma estimativa. […]
Assim, o melhor conselho sobre defaults de codificação é: não confie neles.
Você evitará muitas dores de cabeça se seguir o conselho do sanduíche de Unicode, e sempre tratar codificações de forma explícita em seus programas.
Infelizmente, o Unicode é trabalhoso mesmo se você converter seus bytes
para str
corretamente.
As duas próximas seções tratam de assuntos que são simples no reino do ASCII,
mas ficam muito complexos no planeta Unicode: normalização de texto (isto é, transformar o texto em uma representação uniforme para comparações) e ordenação.
4.7. Normalizando o Unicode para comparações confiáveis
Comparações de strings são dificultadas pelo fato do Unicode ter combinações de caracteres: sinais diacríticos e outras marcações que são anexadas aos caractere anterior, ambos aparecendo juntos como um só caractere quando impressos.
Por exemplo, a palavra "café" pode ser composta de duas formas, usando quatro ou cinco pontos de código, mas o resultado parece exatamente o mesmo:
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False
Colocar COMBINING ACUTE ACCENT
(U+0301) após o "e" resulta em "é".
No padrão Unicode, sequências como 'é'
e 'e\u0301'
são chamadas de "equivalentes canônicas",
e se espera que as aplicações as tratem como iguais. Mas Python vê duas sequências de pontos de código diferentes, e não as considera iguais.
A solução é a unicodedata.normalize()
.
O primeiro argumento para essa função é uma dessas quatro strings: 'NFC'
, 'NFD'
, 'NFKC'
, e 'NFKD'
.
Vamos começar pelas duas primeiras.
A Forma Normal C (NFC) combina os ponto de código para produzir a string equivalente mais curta, enquanto a NFD decompõe, expandindo os caracteres compostos em caracteres base e separando caracteres combinados. Ambas as normalizações fazem as comparações funcionarem da forma esperada, como mostra o próximo exemplo:
>>> from unicodedata import normalize
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) == normalize('NFC', s2)
True
>>> normalize('NFD', s1) == normalize('NFD', s2)
True
Drivers de teclado normalmente geram caracteres compostos, então o texto digitado pelos usuários estará na NFC por default. Entretanto, por segurança, pode ser melhor normalizar as strings com normalize('NFC', user_text)
antes de salvá-las.
A NFC também é a forma de normalização recomendada pelo W3C em
"Character Model for the World Wide Web: String Matching and Searching" (Um Modelo de Caracteres para a World Wide Web: Casamento de Strings e Busca) (EN).
Alguns caracteres singulares são normalizados pela NFC em um outro caractere singular. O símbolo para o ohm (Ω), a unidade de medida de resistência elétrica, é normalizado para a letra grega ômega maiúscula. Eles são visualmente idênticos, mas diferentes quando comparados, então a normalizaçào é essencial para evitar surpresas:
>>> from unicodedata import normalize, name
>>> ohm = '\u2126'
>>> name(ohm)
'OHM SIGN'
>>> ohm_c = normalize('NFC', ohm)
>>> name(ohm_c)
'GREEK CAPITAL LETTER OMEGA'
>>> ohm == ohm_c
False
>>> normalize('NFC', ohm) == normalize('NFC', ohm_c)
True
As outras duas formas de normalização são a NFKC e a NFKD, a letra K significando "compatibilidade".
Essas são formas mais fortes de normalizaçào, afetando os assim chamados "caracteres de compatibilidade".
Apesar de um dos objetivos do Unicode ser a existência de um único ponto de código "canônico" para cada caractere, alguns caracteres aparecem mais de uma vez, para manter compatibilidade com padrões pré-existentes.
Por exemplo, o MICRO SIGN
, µ
(U+00B5
), foi adicionado para permitir a conversão bi-direcional com o latin1
, que o inclui, apesar do mesmo caractere ser parte do alfabeto grego com o ponto de código U+03BC
(GREEK SMALL LETTER MU
).
Assim, o símbolo de micro é considerado um "caractere de compatibilidade".
Nas formas NFKC e NFKD, cada caractere de compatibilidade é substituído por uma "decomposição de compatibilidade" de um ou mais caracteres, que é considerada a representação "preferencial", mesmo se ocorrer alguma perda de formatação—idealmente, a formatação deveria ser responsabilidade de alguma marcação externa, não parte do Unicode. Para exemplificar, a decomposição de compatibilidade da fração um meio, '½'
(U+00BD
), é a sequência de três caracteres '1/2'
, e a decomposição de compatibilidade do símbolo de micro, 'µ'
(U+00B5
), é o mu minúsculo, 'μ'
(U+03BC
).[50]
É assim que a NFKC funciona na prática:
>>> from unicodedata import normalize, name
>>> half = '\N{VULGAR FRACTION ONE HALF}'
>>> print(half)
½
>>> normalize('NFKC', half)
'1⁄2'
>>> for char in normalize('NFKC', half):
... print(char, name(char), sep='\t')
...
1 DIGIT ONE
⁄ FRACTION SLASH
2 DIGIT TWO
>>> four_squared = '4²'
>>> normalize('NFKC', four_squared)
'42'
>>> micro = 'µ'
>>> micro_kc = normalize('NFKC', micro)
>>> micro, micro_kc
('µ', 'μ')
>>> ord(micro), ord(micro_kc)
(181, 956)
>>> name(micro), name(micro_kc)
('MICRO SIGN', 'GREEK SMALL LETTER MU')
Ainda que '1⁄2'
seja um substituto razoável para '½'
,
e o símbolo de micro ser realmente a letra grega mu minúscula, converter '4²'
para '42'
muda o sentido.
Uma aplicação poderia armazenar '4²'
como '4<sup>2</sup>'
,
mas a função normalize
não sabe nada sobre formatação.
Assim, NFKC ou NFKD podem perder ou distorcer informações,
mas podem produzir representações intermediárias convenientes para buscas ou indexação.
Infelizmente, com o Unicode tudo é sempre mais complicado do que parece à primeira vista.
Para o VULGAR FRACTION ONE HALF
, a normalização NFKC produz 1 e 2 unidos pelo FRACTION SLASH
,
em vez do SOLIDUS
, também conhecido como "barra" ("slash" em inglês)—o familiar caractere com código decimal 47 em ASCII.
Portanto, buscar pela sequência ASCII de três caracteres '1/2'
não encontraria a sequência Unicode normalizada.
⚠️ Aviso
|
As normalizações NFKC e NFKD causam perda de dados e devem ser aplicadas apenas em casos especiais, como busca e indexação, e não para armazenamento permanente do texto. |
Ao preparar texto para busca ou indexação, há outra operação útil: case folding [51], nosso próximo assunto.
4.7.1. Case Folding
Case folding é essencialmente a conversão de todo o texto para minúsculas, com algumas transformações adicionais. A operação é suportada pelo método str.casefold()
.
Para qualquer string s
contendo apenas caracteres latin1
, s.casefold()
produz o mesmo resultado de s.lower()
, com apenas duas exceções—o símbolo de micro, 'µ'
, é trocado pela letra grega mu minúscula (que é exatamente igual na maioria das fontes) e a letra alemã Eszett (ß), também chamada "s agudo" (scharfes S) se torna "ss":
>>> micro = 'µ'
>>> name(micro)
'MICRO SIGN'
>>> micro_cf = micro.casefold()
>>> name(micro_cf)
'GREEK SMALL LETTER MU'
>>> micro, micro_cf
('µ', 'μ')
>>> eszett = 'ß'
>>> name(eszett)
'LATIN SMALL LETTER SHARP S'
>>> eszett_cf = eszett.casefold()
>>> eszett, eszett_cf
('ß', 'ss')
Há quase 300 pontos de código para os quais str.casefold()
e str.lower()
devolvem resultados diferentes.
Como acontece com qualquer coisa relacionada ao Unicode, case folding é um tópico complexo, com muitos casos linguísticos especiais, mas o grupo central de desenvolvedores de Python fez um grande esforço para apresentar uma solução que, espera-se, funcione para a maioria dos usuários.
Nas próximas seções vamos colocar nosso conhecimento sobre normalização para trabalhar, desenvolvendo algumas funções utilitárias.
4.7.2. Funções utilitárias para casamento de texto normalizado
Como vimos, é seguro usar a NFC e a NFD, e ambas permitem comparações razoáveis entre strings Unicode. A NFC é a melhor forma normalizada para a maioria das aplicações, e str.casefold()
é a opção certa para comparações indiferentes a maiúsculas/minúsculas.
Se você precisa lidar com texto em muitas línguas diferentes, seria muito útil acrescentar às suas ferramentas de trabalho um par de funções como nfc_equal
e fold_equal
, do Exemplo 53.
"""
Utility functions for normalized Unicode string comparison.
Using Normal Form C, case sensitive:
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1 == s2
False
>>> nfc_equal(s1, s2)
True
>>> nfc_equal('A', 'a')
False
Using Normal Form C with case folding:
>>> s3 = 'Straße'
>>> s4 = 'strasse'
>>> s3 == s4
False
>>> nfc_equal(s3, s4)
False
>>> fold_equal(s3, s4)
True
>>> fold_equal(s1, s2)
True
>>> fold_equal('A', 'a')
True
"""
from unicodedata import normalize
def nfc_equal(str1, str2):
return normalize('NFC', str1) == normalize('NFC', str2)
def fold_equal(str1, str2):
return (normalize('NFC', str1).casefold() ==
normalize('NFC', str2).casefold())
Além da normalização e do case folding do Unicode—ambos partes desse padrão—algumas vezes faz sentido aplicar transformações mais profundas, como por exemplo mudar 'café'
para 'cafe'
. Vamos ver quando e como na próxima seção.
4.7.3. "Normalização" extrema: removendo sinais diacríticos
O tempero secreto da busca do Google inclui muitos truques, mas um deles aparentemente é ignorar sinais diacríticos (acentos e cedilhas, por exemplo), pelo menos em alguns contextos. Remover sinais diacríticos não é uma forma regular de normalização, pois muitas vezes muda o sentido das palavras e pode produzir falsos positivos em uma busca. Mas ajuda a lidar com alguns fatos da vida: as pessoas às vezes são preguiçosas ou desconhecem o uso correto dos sinais diacríticos, e regras de ortografia mudam com o tempo, levando acentos a desaparecerem e reaparecerem nas línguas vivas.
Além do caso da busca, eliminar os acentos torna as URLs mais legíveis, pelo menos nas línguas latinas. Veja a URL do artigo da Wikipedia sobre a cidade de São Paulo:
https://en.wikipedia.org/wiki/S%C3%A3o_Paulo
O trecho %C3%A3
é a renderização em UTF-8 de uma única letra, o "ã" ("a" com til). A forma a seguir é muito mais fácil de reconhecer, mesmo com a ortografia incorreta:
https://en.wikipedia.org/wiki/Sao_Paulo
Para remover todos os sinais diacríticos de uma str
, você pode usar uma função como a do Exemplo 54.
import unicodedata
import string
def shave_marks(txt):
"""Remove all diacritic marks"""
norm_txt = unicodedata.normalize('NFD', txt) # (1)
shaved = ''.join(c for c in norm_txt
if not unicodedata.combining(c)) # (2)
return unicodedata.normalize('NFC', shaved) # (3)
-
Decompõe todos os caracteres em caracteres base e marcações combinadas.
-
Filtra e retira todas as marcações combinadas.
-
Recompõe todos os caracteres.
Exemplo 55 mostra alguns usos para shave_marks
.
shave_marks
do Exemplo 54>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> shave_marks(order)
'“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”' (1)
>>> Greek = 'Ζέφυρος, Zéfiro'
>>> shave_marks(Greek)
'Ζεφυρος, Zefiro' (2)
-
Apenas as letras "è", "ç", e "í" foram substituídas.
-
Tanto "έ" quando "é" foram substituídas.
A função shave_marks
do Exemplo 54 funciona bem, mas talvez vá longe demais. Frequentemente, a razão para remover os sinais diacríticos é transformar texto de uma língua latina para ASCII puro, mas shave_marks
também troca caracteres não-latinos—como letras gregas—que nunca se tornarão ASCII apenas pela remoção de seus acentos. Então faz sentido analisar cada caractere base e remover as marcações anexas apenas se o caractere base for uma letra do alfabeto latino. É isso que o Exemplo 56 faz.
def shave_marks_latin(txt):
"""Remove all diacritic marks from Latin base characters"""
norm_txt = unicodedata.normalize('NFD', txt) # (1)
latin_base = False
preserve = []
for c in norm_txt:
if unicodedata.combining(c) and latin_base: # (2)
continue # ignore diacritic on Latin base char
preserve.append(c) # (3)
# if it isn't a combining char, it's a new base char
if not unicodedata.combining(c): # (4)
latin_base = c in string.ascii_letters
shaved = ''.join(preserve)
return unicodedata.normalize('NFC', shaved) # (5)
-
Decompõe todos os caracteres em caracteres base e marcações combinadas.
-
Pula as marcações combinadas quando o caractere base é latino.
-
Caso contrário, mantém o caractere original.
-
Detecta um novo caractere base e determina se ele é latino.
-
Recompõe todos os caracteres.
Um passo ainda mais radical substituiria os símbolos comuns em textos de línguas ocidentais (por exemplo, aspas curvas, travessões, os círculos de bullet points, etc) em seus equivalentes ASCII
. É isso que a função asciize
faz no Exemplo 57.
single_map = str.maketrans("""‚ƒ„ˆ‹‘’“”•–—˜›""", # (1)
"""'f"^<''""---~>""")
multi_map = str.maketrans({ # (2)
'€': 'EUR',
'…': '...',
'Æ': 'AE',
'æ': 'ae',
'Œ': 'OE',
'œ': 'oe',
'™': '(TM)',
'‰': '<per mille>',
'†': '**',
'‡': '***',
})
multi_map.update(single_map) # (3)
def dewinize(txt):
"""Replace Win1252 symbols with ASCII chars or sequences"""
return txt.translate(multi_map) # (4)
def asciize(txt):
no_marks = shave_marks_latin(dewinize(txt)) # (5)
no_marks = no_marks.replace('ß', 'ss') # (6)
return unicodedata.normalize('NFKC', no_marks) # (7)
-
Cria uma tabela de mapeamento para substituição de caractere para caractere.
-
Cria uma tabela de mapeamento para substituição de string para caractere.
-
Funde as tabelas de mapeamento.
-
dewinize
não afeta texto emASCII
oulatin1
, apenas os acréscimos da Microsoft aolatin1
nocp1252
. -
Aplica
dewinize
e remove as marcações de sinais diacríticos. -
Substitui o Eszett por "ss" (não estamos usando case folding aqui, pois queremos preservar maiúsculas e minúsculas).
-
Aplica a normalização NFKC para compor os caracteres com seus pontos de código de compatibilidade.
O Exemplo 58 mostra a asciize
em ação.
asciize
, do Exemplo 57>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> dewinize(order)
'"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."' (1)
>>> asciize(order)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."' (2)
-
dewinize
substitui as aspas curvas, os bullets, e o ™ (símbolo de marca registrada). -
asciize
aplicadewinize
, remove os sinais diacríticos e substitui o'ß'
.
⚠️ Aviso
|
Cada língua tem suas próprias regras para remoção de sinais diacríticos. Por exemplo, os alemães trocam o |
Resumindo, as funções em simplify.py vão bem além da normalização padrão, e realizam uma cirurgia profunda no texto, com boas chances de mudar seu sentido. Só você pode decidir se deve ir tão longe, conhecendo a língua alvo, os seus usuários e a forma como o texto transformado será utilizado.
Isso conclui nossa discussão sobre normalização de texto Unicode.
Vamos agora ordenar nossos pensamentos sobre ordenação no Unicode.
4.8. Ordenando texto Unicode
Python ordena sequências de qualquer tipo comparando um por um os itens em cada sequência. Para strings, isso significa comparar pontos de código. Infelizmente, isso produz resultados inaceitáveis para qualquer um que use caracteres não-ASCII.
Considere ordenar uma lista de frutas cultivadas no Brazil:
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
As regras de ordenação variam entre diferentes locales, mas em português e em muitas línguas que usam o alfabeto latino, acentos e cedilhas raramente fazem diferença na ordenação.[52] Então "cajá" é lido como "caja," e deve vir antes de "caju."
A lista fruits
ordenada deveria ser:
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
O modo padrão de ordenar texto não-ASCII em Python é usar a função locale.strxfrm
que, de acordo com a
documentação do módulo locale
, "Transforma uma string em uma que pode ser usada em comparações com reconhecimento de localidade."
Para poder usar locale.strxfrm
, você deve primeiro definir um locale adequado para sua aplicação, e rezar para que o SO o suporte. A sequência de comando no Exemplo 59 pode funcionar para você.
locale.strxfrm
como chave de ornenamentoimport locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale)
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)
Executando o Exemplo 59 no GNU/Linux (Ubuntu 19.10) com o locale pt_BR.UTF-8
instalado, consigo o resultado correto:
'pt_BR.UTF-8'
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
Portanto, você precisa chamar setlocale(LC_COLLATE, «your_locale»)
antes de usar locale.strxfrm
como a chave de ordenação.
Porém, aqui vão algumas ressalvas:
-
Como as configurações de locale são globais, não é recomendado chamar
setlocale
em uma biblioteca. Sua aplicação ou framework deveria definir o locale no início do processo, e não mudá-lo mais depois disso. -
O locale desejado deve estar instalado no SO, caso contrário
setlocale
gera uma exceção delocale.Error: unsupported locale setting
. -
Você tem que saber como escrever corretamente o nome do locale.
-
O locale precisa ser corretamente implementado pelos desenvolvedores do SO. Tive sucesso com o Ubuntu 19.10, mas não no macOS 10.14. No macOS, a chamada
setlocale(LC_COLLATE, 'pt_BR.UTF-8')
devolve a string'pt_BR.UTF-8'
sem qualquer reclamação. Massorted(fruits, key=locale.strxfrm)
produz o mesmo resultado incorreto desorted(fruits)
. Também tentei os localesfr_FR
,es_ES
, ede_DE
no macOS, maslocale.strxfrm
nunca fez seu trabalho direito.[53]
Portanto, a solução da biblioteca padrão para ordenação internacionalizada funciona, mas parece ter suporte adequado apenas no GNU/Linux (talvez também no Windows, se você for um especialista). Mesmo assim, ela depende das configurações do locale, criando dores de cabeça na implantação.
Felizmente, há uma solução mais simples: a biblioteca pyuca, disponível no PyPI.
4.8.1. Ordenando com o Algoritmo de Ordenação do Unicode
James Tauber, contribuidor muito ativo do Django, deve ter sentido essa nossa mesma dor, e criou a pyuca, uma implementação integralmente em Python do Algoritmo de Ordenação do Unicode (UCA, sigla em inglês para Unicode Collation Algorithm). O Exemplo 60 mostra como ela é fácil de usar.
pyuca.Collator.sort_key
>>> import pyuca
>>> coll = pyuca.Collator()
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=coll.sort_key)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
Isso é simples e funciona no GNU/Linux, no macOS, e no Windows, pelo menos com a minha pequena amostra.
A pyuca
não leva o locale em consideração. Se você precisar personalizar a ordenação, pode fornecer um caminho para uma tabela própria de ordenação para o construtor Collator()
. Sem qualquer configuração adicional, a biblioteca usa o allkeys.txt, incluído no projeto. Esse arquivo é apenas uma cópia da Default Unicode Collation Element Table (Tabela Default de Ordenação de Elementos Unicode) do Unicode.org .
👉 Dica
|
PyICU: A recomendação do Miro para ordenação com Unicode
(O revisor técnico Miroslav Šedivý é um poliglota e um especialista em Unicode. Eis o que ele escreveu sobre a pyuca.) A pyuca tem um algoritmo de ordenação que não respeita o padrão de ordenação de linguagens individuais. Por exemplo, [a letra] Ä em alemão fica entre o A e o B, enquanto em sueco ela vem depois do Z. Dê uma olhada na PyICU, que funciona como locale sem modificar o locale do processo. Ela também é necessária se você quiser mudar a capitalização de iİ/ıI em turco. A PyICU inclui uma extensão que precisa ser compilada, então pode ser mais difícil de instalar em alguns sistemas que a pyuca, que é toda feita em Python. |
E por sinal, aquela tabela de ordenação é um dos muitos arquivos de dados que formam o banco de dados do Unicode, nosso próximo assunto.
4.9. O banco de dados do Unicode
O padrão Unicode fornece todo um banco de dados—na forma de vários arquivos de texto estruturados—que inclui não apenas a tabela mapeando pontos de código para nomes de caracteres, mas também metadados sobre os caracteres individuais e como eles se relacionam. Por exemplo, o banco de dados do Unicode registra se um caractere pode ser impresso, se é uma letra, um dígito decimal ou algum outro símbolo numérico.
É assim que os métodos de str
isalpha
, isprintable
, isdecimal
e isnumeric
funcionam.
str.casefold
também usa informação de uma tabela do Unicode.
✒️ Nota
|
A função |
4.9.1. Encontrando caracteres por nome
O módulo unicodedata
tem funções para obter os metadados de caracteres, incluindo
unicodedata.name()
, que devolve o nome oficial do caractere no padrão.
A Figura 14 demonstra essa função.[54]
unicodedata.name()
no console de Python.Você pode usar a função name()
para criar aplicações que permitem aos usuários buscarem caracteres por nome.
A Figura 15 demonstra o script de comando de linha cf.py, que recebe como argumentos uma ou mais palavras, e lista os caracteres que tem aquelas palavras em seus nomes Unicode oficiais.
O código fonte completo de cf.py aparece no Exemplo 61.
⚠️ Aviso
|
O suporte a emojis varia muito entre sistemas operacionais e aplicativos. Nos últimos anos, o terminal do macOS tem oferecido o melhor suporte para emojis, seguido por terminais gráficos GNU/Linux modernos. O cmd.exe e o PowerShell do Windows agora suportam saída Unicode, mas enquanto escrevo essa seção, em janeiro de 2020, eles ainda não mostram emojis—pelo menos não sem configurações adicionais. O revisor técnico Leonardo Rochael me falou sobre um novo terminal para Windows da Microsoft, de código aberto, que pode ter um suporte melhor a Unicode que os consoles antigos da Microsoft. Não tive tempo de testar. |
No Exemplo 61, observe que o comando if
, na função find
, usa o método .issubset()
para testar rapidamente se todas as palavras no conjunto query
aparecem na lista de palavras criada a partir do nome do caractere.
Graças à rica API de conjuntos de Python, não precisamos de um loop for
aninhado e de outro if
para implementar essa verificação
#!/usr/bin/env python3
import sys
import unicodedata
START, END = ord(' '), sys.maxunicode + 1 # (1)
def find(*query_words, start=START, end=END): # (2)
query = {w.upper() for w in query_words} # (3)
for code in range(start, end):
char = chr(code) # (4)
name = unicodedata.name(char, None) # (5)
if name and query.issubset(name.split()): # (6)
print(f'U+{code:04X}\t{char}\t{name}') # (7)
def main(words):
if words:
find(*words)
else:
print('Please provide words to find.')
if __name__ == '__main__':
main(sys.argv[1:])
-
Configura os defaults para a faixa de pontos de código da busca.
-
find
aceitaquery_words
e somente argumentos nomeados (opcionais) para limitar a faixa da busca, facilitando os testes. -
Converte
query_words
em um conjunto de strings capitalizadas. -
Obtém o caractere Unicode para
code
. -
Obtém o nome do caractere, ou
None
se o ponto de código não estiver atribuído a um caractere. -
Se há um nome, separa esse nome em uma lista de palavras, então verifica se o conjunto
query
é um subconjunto daquela lista. -
Mostra uma linha com o ponto de código no formato
U+9999
, o caractere e seu nome.
O módulo unicodedata
tem outras funções interessantes. A seguir veremos algumas delas, relacionadas a obter informação de caracteres com sentido numérico.
4.9.2. O sentido numérico de caracteres
O módulo unicodedata
inclui funções para determinar se um caractere Unicode representa um número e, se for esse o caso, seu valor numérico em termos humanos—em contraste com o número de seu ponto de código.
O Exemplo 62 demonstra o uso de unicodedata.name()
e unicodedata.numeric()
,
junto com os métodos .isdecimal()
e .isnumeric()
de str
.
import unicodedata
import re
re_digit = re.compile(r'\d')
sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'
for char in sample:
print(f'U+{ord(char):04x}', # (1)
char.center(6), # (2)
're_dig' if re_digit.match(char) else '-', # (3)
'isdig' if char.isdigit() else '-', # (4)
'isnum' if char.isnumeric() else '-', # (5)
f'{unicodedata.numeric(char):5.2f}', # (6)
unicodedata.name(char), # (7)
sep='\t')
-
Ponto de código no formato
U+0000
. -
O caractere, centralizado em uma
str
de tamanho 6. -
Mostra
re_dig
se o caractere casa com a regexr'\d'
. -
Mostra
isdig
sechar.isdigit()
éTrue
. -
Mostra
isnum
sechar.isnumeric()
éTrue
. -
Valor numérico formatado com tamanho 5 e duas casa decimais.
-
O nome Unicode do caractere.
Executar o Exemplo 62 gera a Figura 16, se a fonte do seu terminal incluir todos aqueles símbolos.
re_dig
significa que o caractere casa com a expressão regular r'\d'
.A sexta coluna da Figura 16 é o resultado da chamada a unicodedata.numeric(char)
com o caractere. Ela mostra que o Unicode sabe o valor numérico de símbolos que representam números. Assim, se você quiser criar uma aplicação de planilha que suporta dígitos tamil ou numerais romanos, vá fundo!
A Figura 16 mostra que a expressão regular r'\d'
casa com o dígito "1" e com o dígito devanágari 3, mas não com alguns outros caracteres considerados dígitos pela função isdigit
.
O módulo re
não é tão conhecedor de Unicode quanto deveria ser.
O novo módulo regex
, disponível no PyPI, foi projetado para um dia substituir o re
, e fornece um suporte melhor ao Unicode.[55]
Voltaremos ao módulo re
na próxima seção.
Ao longo desse capítulo, usamos várias funções de unicodedata
, mas há muitas outras que não mencionamos. Veja a documentação da biblioteca padrão para o módulo unicodedata
.
A seguir vamos dar uma rápida passada pelas APIs de modo dual, com funções que aceitam argumentos str
ou bytes
e dão a eles tratamento especial dependendo do tipo.
4.10. APIs de modo dual para str e bytes
A biblioteca padrão de Python tem funções que aceitam argumentos str
ou bytes
e se comportam de forma diferente dependendo do tipo recebido. Alguns exemplos podem ser encontrados nos módulos re
e os
.
4.10.1. str versus bytes em expressões regulares
Se você criar uma expressão regular com bytes
, padrões tal como \d
e \w
vão casar apenas com caracteres ASCII; por outro lado, se esses padrões forem passados como str
, eles vão casar com dígitos Unicode ou letras além do ASCII. O Exemplo 63 e a Figura 17 comparam como letras, dígitos ASCII, superescritos e dígitos tamil casam em padrões str
e bytes
.
str
e como bytes
import re
re_numbers_str = re.compile(r'\d+') # (1)
re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+') # (2)
re_words_bytes = re.compile(rb'\w+')
text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef" # (3)
" as 1729 = 1³ + 12³ = 9³ + 10³.") # (4)
text_bytes = text_str.encode('utf_8') # (5)
print(f'Text\n {text_str!r}')
print('Numbers')
print(' str :', re_numbers_str.findall(text_str)) # (6)
print(' bytes:', re_numbers_bytes.findall(text_bytes)) # (7)
print('Words')
print(' str :', re_words_str.findall(text_str)) # (8)
print(' bytes:', re_words_bytes.findall(text_bytes)) # (9)
-
As duas primeiras expressões regulares são do tipo
str
. -
As duas últimas são do tipo
bytes
. -
Texto Unicode para ser usado na busca, contendo os dígitos tamil para
1729
(a linha lógica continua até o símbolo de fechamento de parênteses). -
Essa string é unida à anterior no momento da compilação (veja "2.4.2. String literal concatenation" (Concatenação de strings literais) em A Referência da Linguagem Python).
-
Uma string
bytes
é necessária para a busca com as expressões regularesbytes
. -
O padrão
str
r'\d+'
casa com os dígitos ASCII e tamil. -
O padrão
bytes
rb'\d+'
casa apenas com os bytes ASCII para dígitos. -
O padrão
str
r'\w+'
casa com letras, superescritos e dígitos tamil e ASCII. -
O padrão
bytes
rb'\w+'
casa apenas com bytes ASCII para letras e dígitos.
O Exemplo 63 é um exemplo trivial para destacar um ponto: você pode usar expressões regulares com str
ou bytes
, mas nesse último caso os bytes fora da faixa do ASCII são tratados como caracteres que não representam dígitos nem palavras.
Para expressões regulares str
, há uma marcação re.ASCII
, que faz \w
, \W
, \b
, \B
, \d
, \D
, \s
, e \S
executarem um casamento apenas com ASCII. Veja a documentaçào do módulo re
para maiores detalhes.
Outro módulo importante é o os
.
4.10.2. str versus bytes nas funções de os
O kernel do GNU/Linux não conhece Unicode então, no mundo real, você pode encontrar nomes de arquivo compostos de sequências de bytes que não são válidas em nenhum esquema razoável de codificação, e não podem ser decodificados para str
. Servidores de arquivo com clientes usando uma variedade de diferentes SOs são particularmente inclinados a apresentar esse cenário.
Para mitigar esse problema, todas as funções do módulo os
que aceitam nomes de arquivo ou caminhos podem receber seus argumentos como str
ou bytes
. Se uma dessas funções é chamada com um argumento str
, o argumento será automaticamente convertido usando o codec informado por sys.getfilesystemencoding()
, e a resposta do SO será decodificada com o mesmo codec. Isso é quase sempre o que se deseja, mantendo a melhor prática do sanduíche de Unicode.
Mas se você precisa lidar com (e provavelmente corrigir) nomes de arquivo que não podem ser processados daquela forma, você pode passar argumentos bytes
para as funções de os
, e receber bytes
de volta. Esse recurso permite que você processe qualquer nome de arquivo ou caminho, independende de quantos gremlins encontrar. Veja o Exemplo 64.
listdir
com argumentos str
e bytes
, e os resultados>>> os.listdir('.') # (1)
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.') # (2)
[b'abc.txt', b'digits-of-\xcf\x80.txt']
-
O segundo nome de arquivo é "digits-of-π.txt" (com a letra grega pi).
-
Dado um argumento
byte
,listdir
devolve nomes de arquivos como bytes:b'\xcf\x80'
é a codificação UTF-8 para a letra grega pi.
Para ajudar no processamento manual de sequências str
ou bytes
que são nomes de arquivos ou caminhos,
o módulo os
fornece funções especiais de codificação e decodificação, os.fsencode(name_or_path)
e os.fsdecode(name_or_path)
. Ambas as funções aceitam argumentos dos tipos str
, bytes
ou, desde Python 3.6, um objeto que implemente a interface os.PathLike
.
O Unicode é um buraco de coelho bem fundo. É hora de encerrar nossa exploração de str
e bytes
.
4.11. Resumo do capítulo
Começamos o capítulo descartando a noção de que 1 caractere == 1 byte
. A medida que o mundo adota o Unicode, precisamos manter o conceito de strings de texto separado das sequências binárias que as representam em arquivos, e Python 3 aplica essa separação.
Após uma breve passada pelos tipos de dados sequências binárias—bytes
, bytearray
, e memoryview
—, mergulhamos na codificação e na decodificação, com uma amostragem dos codecs importantes, seguida por abordagens para prevenir ou lidar com os abomináveis UnicodeEncodeError
, UnicodeDecodeError
e os SyntaxError
causados pela codificação errada em arquivos de código-fonte de Python.
A seguir consideramos a teoria e a prática de detecção de codificação na ausência de metadados: em teoria, não pode ser feita, mas na prática o pacote Chardet consegue realizar esse feito para uma grande quantidade de codificações populares. Marcadores de ordem de bytes foram apresentados como a única dica de codificação encontrada em arquivos UTF-16 e UTF-32—algumas vezes também em arquivos UTF-8.
Na seção seguinte, demonstramos como abrir arquivos de texto, uma tarefa fácil exceto por uma armadilha: o argumento nomeado encoding=
não é obrigatório quando se abre um arquivo de texto, mas deveria ser. Se você não especificar a codificação, terminará com um programa que consegue produzir "texto puro" que é incompatível entre diferentes plataformas, devido a codificações default conflitantes. Expusemos então as diferentes configurações de codificação usadas pelo Python, e como detectá-las.
Uma triste realidade para usuários de Windows é o fato dessas configurações muitas vezes terem valores diferentes dentro da mesma máquina, e desses valores serem mutuamente incompatíveis; usuários do GNU/Linux e do macOS, por outro lado, vivem em um lugar mais feliz, onde o UTF-8 é o default por (quase) toda parte.
O Unicode fornece múltiplas formas de representar alguns caracteres, então a normalização é um pré-requisito para a comparação de textos. Além de explicar a normalização e o case folding, apresentamos algumas funções úteis que podem ser adaptadas para as suas necessidades, incluindo transformações drásticas como a remoção de todos os acentos. Vimos como ordenar corretamente texto Unicode, usando o módulo padrão locale
—com algumas restrições—e uma alternativa que não depende de complexas configurações de locale: a biblioteca externa pyuca.
Usamos o banco de dados do Unicode para programar um utilitário de comando de linha que busca caracteres por nome—em 28 linhas de código, graças ao poder de Python.
Demos uma olhada em outros metadados do Unicode, e vimos rapidamente as APIs de modo dual, onde algumas funções podem ser chamadas com argumentos str
ou bytes
, produzindo resultados diferentes.
4.12. Leitura complementar
A palestra de Ned Batchelder na PyCon US 2012, "Pragmatic Unicode, or, How Do I Stop the Pain?" (Unicode Pragmático, ou, Como Eu Fiz a Dor Sumir?) (EN), foi marcante. Ned é tão profissional que forneceu uma transcrição completa da palestra, além dos slides e do vídeo.
"Character encoding and Unicode in Python: How to (╯°□°)╯︵ ┻━┻ with dignity" (Codificação de caracteres e o Unicode no Python: como (╯°□°)╯︵ ┻━┻ com dignidade) (slides, vídeo) (EN) foi uma excelente palestra de Esther Nam e Travis Fischer na PyCon 2014, e foi onde encontrei a concisa epígrafe desse capítulo: "Humanos usam texto. Computadores falam em bytes."
Lennart Regebro—um dos revisores técnicos da primeira edição desse livro—compartilha seu "Useful Mental Model of Unicode (UMMU)" (Modelo Mental Útil do Unicode) em um post curto, "Unconfusing Unicode: What Is Unicode?" (Desconfundindo o Unicode: O Que É O Unicode?) (EN). O Unicode é um padrão complexo, então o UMMU de Lennart é realmente um ponto de partida útil.
O "Unicode HOWTO" oficial na documentação de Python aborda o assunto por vários ângulos diferentes, de uma boa introdução histórica a detalhes de sintaxe, codecs, expressões regulares, nomes de arquivo, e boas práticas para E/S sensível ao Unicode (isto é, o sanduíche de Unicode), com vários links adicionais de referências em cada seção.
O Chapter 4, "Strings" (Capítulo 4, "Strings"), do maravilhosos livro Dive into Python 3 (EN), de Mark Pilgrim (Apress), também fornece uma ótima introdução ao suporte a Unicode no Python 3. No mesmo livro, o Capítulo 15 descreve como a biblioteca Chardet foi portada de Python 2 para Python 3, um valioso estudo de caso, dado que a mudança do antigo tipo str
para o novo bytes
é a causa da maioria das dores da migração, e esta é uma preocupação central em uma biblioteca projetada para detectar codificações.
Se você conhece Python 2 mas é novo no Python 3, o artigo "What’s New in Python 3.0" (O quê há de novo no Python 3.0) (EN), de Guido van Rossum, tem 15 pontos resumindo as mudanças, com vários links. Guido inicia com uma afirmação brutal: "Tudo o que você achava que sabia sobre dados binários e Unicode mudou". O post de Armin Ronacher em seu blog, "The Updated Guide to Unicode on Python" O Guia Atualizado do Unicode no Python, é bastante profundo e realça algumas das armadilhas do Unicode no Python (Armin não é um grande fã de Python 3).
O capítulo 2 ("Strings and Text" Strings e Texto) do Python Cookbook, 3rd ed. (EN) (O’Reilly), de David Beazley e Brian K. Jones, tem várias receitas tratando de normalização de Unicode, sanitização de texto, e execução de operações orientadas para texto em sequências de bytes. O capítulo 5 trata de arquivos e E/S, e inclui a "Recipe 5.17. Writing Bytes to a Text File" (Receita 5.17. Escrevendo Bytes em um Arquivo de Texto), mostrando que sob qualquer arquivo de texto há sempre uma sequência binária que pode ser acessada diretamente quando necessário. Mais tarde no mesmo livro, o módulo struct
é usado em "Recipe 6.11. Reading and Writing Binary Arrays of Structures" (Receita 6.11. Lendo e Escrevendo Arrays Binárias de Estruturas).
O blog "Python Notes" de Nick Coghlan tem dois posts muito relevantes para esse capítulo: "Python 3 and ASCII Compatible Binary Protocols" (Python 3 e os Protocolos Binários Compatíveis com ASCII) (EN) e "Processing Text Files in Python 3" (Processando Arquivos de Texto em Python 3) (EN). Fortemente recomendado.
Uma lista de codificações suportadas pelo Python está disponível em "Standard Encodings" (EN), na documentação do módulo codecs
. Se você precisar obter aquela lista de dentro de um programa, pode ver como isso é feito no script /Tools/unicode/listcodecs.py, que acompanha o código-fonte do CPython.
Os livros Unicode Explained (Unicode Explicado) (EN), de Jukka K. Korpela (O’Reilly) e Unicode Demystified (Unicode Desmistificado), de Richard Gillam (Addison-Wesley) não são específicos sobre Python, nas foram muito úteis para meu estudo dos conceitos do Unicode. Programming with Unicode (Programando com Unicode), de Victor Stinner, é um livro gratuito e publicado pelo próprio autor (Creative Commons BY-SA) tratando de Unicode em geral, bem como de ferramentas e APIs no contexto dos principais sistemas operacionais e algumas linguagens de programação, incluindo Python.
As páginas do W3C "Case Folding: An Introduction" (Case Folding: Uma Introdução) (EN) e "Character Model for the World Wide Web: String Matching" (O Modelo de Caracteres para a World Wide Web: Casamento de Strings) (EN) tratam de conceitos de normalização, a primeira uma suave introdução e a segunda uma nota de um grupo de trabalho escrita no seco jargão dos padrões—o mesmo tom do "Unicode Standard Annex #15—Unicode Normalization Forms" (Anexo 15 do Padrão Unicode—Formas de Normalização do Unicode) (EN). A seção "Frequently Asked Questions, Normalization" (Perguntas Frequentes, Normalização) (EN) do Unicode.org é mais fácil de ler, bem como o "NFC FAQ" (EN) de Mark Davis—autor de vários algoritmos do Unicode e presidente do Unicode Consortium quando essa seção foi escrita.
Em 2016, o Museu de Arte Moderna (MoMA) de New York adicionou à sua coleção
o emoji original (EN),
os 176 emojis desenhados por Shigetaka Kurita em 1999 para a NTT DOCOMO—a provedora de telefonia móvel japonesa.
Indo mais longe no passado, a Emojipedia (EN) publicou o artigo "Correcting the Record on the First Emoji Set" (Corrigindo o Registro [Histórico] sobre o Primeiro Conjunto de Emojis) (EN), atribuindo ao SoftBank do Japão o mais antigo conjunto conhecido de emojis, implantado em telefones celulares em 1997.
O conjunto do SoftBank é a fonte de 90 emojis que hoje fazem parte do Unicode, incluindo o U+1F4A9 (PILE OF POO
).
O emojitracker.com, de Matthew Rothenberg, é um painel ativo mostrando a contagem do uso de emojis no Twitter, atualizado em tempo real.
Quando escrevo isso, FACE WITH TEARS OF JOY
(U+1F602) é o emoji mais popular no Twitter, com mais de
3.313.667.315 ocorrências registradas.
5. Fábricas de classes de dados
Classes de dados são como crianças. São boas como um ponto de partida mas, para participarem como um objeto adulto, precisam assumir alguma responsabilidade.
Martin Fowler and Kent Beck em Refactoring, primeira edição, Capítulo 3, seção "Bad Smells in Code, Data Class" (Mau cheiro no código, classe de dados), página 87 (Addison-Wesley).
Python oferece algumas formas de criar uma classe simples, apenas uma coleção de campos, com pouca ou nenhuma funcionalidade adicional.
Esse padrão é conhecido como "classe de dados"—e dataclasses
é um dos pacotes que suporta tal modelo.
Este capítulo trata de três diferentes fábricas de classes que podem ser utilizadas como atalhos para escrever classes de dados:
collections.namedtuple
-
A forma mais simples—disponível desde Python 2.6.
typing.NamedTuple
-
Uma alternativa que requer dicas de tipo nos campos—desde Python 3.5, com a sintaxe
class
adicionada no 3.6. @dataclasses.dataclass
-
Um decorador de classe que permite mais personalização que as alternativas anteriores, acrescentando várias opções e, potencialmente, mais complexidade—desde Python 3.7.
Após falar sobre essas fábricas de classes, vamos discutir o motivo de classe de dados ser também o nome um code smell: um padrão de programação que pode ser um sintoma de um design orientado a objetos ruim.
✒️ Nota
|
A classe Entretanto, |
5.1. Novidades nesse capítulo
Este capítulo é novo, aparece nessa segunda edição do Python Fluente. A Seção 5.3 era parte do capítulo 2 da primeira edição, mas o restante do capítulo é inteiramente inédito.
Vamos começar por uma visão geral, por alto, das três fábricas de classes.
5.2. Visão geral das fábricas de classes de dados
Considere uma classe simples, representando um par de coordenadas geográficas, como aquela no Exemplo 65.
class Coordinate:
def __init__(self, lat, lon):
self.lat = lat
self.lon = lon
A tarefa da classe Coordinate
é manter os atributos latitude e longitude.
Escrever o __init__
padrão fica cansativo muito rápido,
especialmente se sua classe tiver mais que alguns poucos atributos:
cada um deles é mencionado três vezes!
E aquele código repetitivo não nos dá sequer os recursos básicos que esperamos de um objeto Python:
>>> from coordinates import Coordinate
>>> moscow = Coordinate(55.76, 37.62)
>>> moscow
<coordinates.Coordinate object at 0x107142f10> (1)
>>> location = Coordinate(55.76, 37.62)
>>> location == moscow (2)
False
>>> (location.lat, location.lon) == (moscow.lat, moscow.lon) (3)
True
-
O
__repr__
herdado deobject
não é muito útil. -
O
==
não faz sentido; o método__eq__
herdado deobject
compara os IDs dos objetos. -
Comparar duas coordenadas exige a comparação explícita de cada atributo.
As fábricas de classes de dados tratadas nesse capítulo fornecem automaticamente os métodos __init__
, __repr__
, e __eq__
necessários, além alguns outros recursos úteis.
✒️ Nota
|
Nenhuma das fábricas de classes discutidas aqui depende de herança para funcionar. Tanto |
Aqui está uma classe Coordinate
criada com uma namedtuple
—uma função fábrica que cria uma subclasse de tuple
com o nome e os campos especificados:
>>> from collections import namedtuple
>>> Coordinate = namedtuple('Coordinate', 'lat lon')
>>> issubclass(Coordinate, tuple)
True
>>> moscow = Coordinate(55.756, 37.617)
>>> moscow
Coordinate(lat=55.756, lon=37.617) (1)
>>> moscow == Coordinate(lat=55.756, lon=37.617) (2)
True
-
Um
__repr__
útil. -
Um
__eq__
que faz sentido.
A typing.NamedTuple
, mais recente, oferece a mesma funcionalidade e acrescenta anotações de tipo a cada campo:
>>> import typing
>>> Coordinate = typing.NamedTuple('Coordinate',
... [('lat', float), ('lon', float)])
>>> issubclass(Coordinate, tuple)
True
>>> typing.get_type_hints(Coordinate)
{'lat': <class 'float'>, 'lon': <class 'float'>}
👉 Dica
|
Uma tupla nomeada e com dicas de tipo pode também ser construída passando os campos como argumentos nomeados, assim:
Além de ser mais legível, essa forma permite fornecer o mapeamento de campos e tipos como |
Desde Python 3.6, typing.NamedTuple
pode também ser usada em uma instrução class
,
com as anotações de tipo escritas como descrito na PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (EN).
É muito mais legível, e torna fácil sobrepor métodos ou acrescentar métodos novos.
O Exemplo 66 é a mesma classe Coordinate
, com um par de atributos float
e um __str__
personalziado, para mostrar a coordenada no formato 55.8°N, 37.6°E.
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lon: float
def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
⚠️ Aviso
|
Apesar de
|
No método __init__
gerado por typing.NamedTuple
, os campos aparecem como parâmetros e na mesma ordem em que aparecem na declaração class
.
Assim como typing.NamedTuple
, o decorador dataclass
suporta a sintaxe da PEP 526 (EN) para declarar atributos de instância.
O decorador lê as anotações das variáveis e gera métodos automaticamente para sua classe.
Como comparação, veja a classe Coordinate
equivante escrita com a ajuda do decorador dataclass
, como mostra o Exemplo 67.
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
Observe que o corpo das classes no Exemplo 66 e no Exemplo 67 são idênticos—a diferença está na própria declaração class
.
O decorador @dataclass
não depende de herança ou de uma metaclasse, então não deve interferir no uso desses mecanismos pelo usuário.[57]
A classe Coordinate
no Exemplo 67 é uma subclasse de object
.
5.2.1. Principais recursos
As diferentes fábricas de classes de dados tem muito em comum, como resume a Tabela 12.
namedtuple | NamedTuple | dataclass | |
---|---|---|---|
instâncias mutáveis |
NÃO |
NÃO |
SIM |
sintaxe de declaração de classe |
NÃO |
SIM |
SIM |
criar um dict |
x._asdict() |
x._asdict() |
dataclasses.asdict(x) |
obter nomes dos campos |
x._fields |
x._fields |
[f.name for f in dataclasses.fields(x)] |
obter defaults |
x._field_defaults |
x._field_defaults |
[f.default for f in dataclasses.fields(x)] |
obter tipos dos campos |
N/A |
x.__annotations__ |
x.__annotations__ |
nova instância com modificações |
x._replace(…) |
x._replace(…) |
dataclasses.replace(x, …) |
nova classe durante a execução |
namedtuple(…) |
NamedTuple(…) |
dataclasses.make_dataclass(…) |
⚠️ Aviso
|
As classes criadas por |
Vamos agora detalhar aqueles recursos principais.
5.2.1.1. Instâncias mutáveis
A diferença fundamental entre essas três fábricas de classes é que collections.namedtuple
e typing.NamedTuple
criam subclasses de tuple
, e portanto as instâncias são imutáveis. Por default, @dataclass
produz classes mutáveis. Mas o decorador aceita o argumento nomeado frozen
—que aparece no Exemplo 67. Quando frozen=True
, a classe vai gerar uma exceção se você tentar atribuir um valor a um campo após a instância ter sido inicializada.
5.2.1.2. Sintaxe de declaração de classe
Apenas typing.NamedTuple
e dataclass
suportam a sintaxe de declaração de class
regular, tornando mais fácil acrescentar métodos e docstrings à classe que está sendo criada.
5.2.1.3. Construir um dict
As duas variantes de tuplas nomeadas fornecem um método de instância (._asdict
), para construir um objeto dict
a partir dos campos de uma instância de classe de dados.
O módulo dataclasses
fornece uma função para fazer o mesmo: dataclasses.asdict
.
5.2.1.4. Obter nomes dos campos e valores default
Todas as três fábricas de classes permitem que você obtenha os nomes dos campos e os valores default (que podem ser configurados para cada campo).
Nas classes de tuplas nomeadas, aqueles metadados estão nos atributos de classe ._fields
e ._fields_defaults
.
Você pode obter os mesmos metadados em uma classe decorada com dataclass
usando a função fields
do módulo dataclasses
. Ele devolve uma tupla de objetos Field
com vários atributos, incluindo name
e default
.
5.2.1.5. Obter os tipos dos campos
Classes definidas com a ajuda de typing.NamedTuple
e @dataclass
contêm um mapeamento dos nomes dos campos para seus tipos, o atributo de classe __annotations__
.
Como já mencionado, use a função typing.get_type_hints
em vez de ler diretamente de
__annotations__
.
5.2.1.6. Nova instância com modificações
Dada uma instância de tupla nomeada x
, a chamada x._replace(**kwargs)
devolve uma nova instância com os valores de alguns atributos modificados, de acordo com os argumentos nomeados incluídos na chamada. A função de módulo dataclasses.replace(x, **kwargs)
faz o mesmo para uma instância de uma classe decorada com dataclass
.
5.2.1.7. Nova classe durante a execução
Apesar da sintaxe de declaração de classe ser mais legível, ela é fixa no código. Um framework pode ter a necessidade de criar classes de dados durante a execução. Para tanto, podemos usar a sintaxe default de chamada de função de collections.namedtuple
, que também é suportada por typing.NamedTuple
. O módulo dataclasses
oferece a função make_dataclass
, com o mesmo propósito.
Após essa visão geral dos principais recursos das fábricas de classes de dados, vamos examinar cada uma delas mais de perto, começando pela mais simples.
5.3. Tuplas nomeadas clássicas
A função collections.namedtuple
é uma fábrica que cria subclasses de tuple
, acrescidas de nomes de campos, um nome de classe, e um __repr__
informativo.
Classes criadas com namedtuple
podem ser usadas onde quer que uma tupla seja necessária. Na verdade, muitas funções da biblioteca padrão, que antes devolviam tuplas agora devolvem, por conveniência, tuplas nomeadas, sem afetar de forma alguma o código do usuário.
👉 Dica
|
Cada instância de uma classe criada por |
O Exemplo 68 mostra como poderíamos definir uma tupla nomeada para manter informações sobre uma cidade.
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates') (1)
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) (2)
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population (3)
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'
-
São necessários dois parâmetros para criar uma tupla nomeada: um nome de classe e uma lista de nomes de campos, que podem ser passados como um iterável de strings ou como uma única string com os nomes delimitados por espaços.
-
Na inicialização de uma instância, os valores dos campos devem ser passados como argumentos posicionais separados (uma
tuple
, por outro lado, é inicializada com um único iterável) -
É possível acessar os campos por nome ou por posição.
Como uma subclasse de tuple
, City
herda métodos úteis, tal como __eq__
e os métodos especiais para operadores de comparação—incluindo __lt__
, que permite ordenar listas de instâncias de City
.
Uma tupla nomeada oferece alguns atributos e métodos além daqueles herdados de tuple
.
O Exemplo 69 demonstra os mais úteis dentre eles: o atributo de classe _fields
, o método de classe _make(iterable)
, e o método de instância _asdict()
.
>>> City._fields (1)
('name', 'country', 'population', 'location')
>>> Coordinate = namedtuple('Coordinate', 'lat lon')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, Coordinate(28.613889, 77.208889))
>>> delhi = City._make(delhi_data) (2)
>>> delhi._asdict() (3)
{'name': 'Delhi NCR', 'country': 'IN', 'population': 21.935,
'location': Coordinate(lat=28.613889, lon=77.208889)}
>>> import json
>>> json.dumps(delhi._asdict()) (4)
'{"name": "Delhi NCR", "country": "IN", "population": 21.935,
"location": [28.613889, 77.208889]}'
-
._fields
é uma tupla com os nomes dos campos da classe. -
._make()
cria umaCity
a partir de um iterável;City(*delhi_data)
faria o mesmo. -
._asdict()
devolve umdict
criado a partir da instância de tupla nomeada. -
._asdict()
é útil para serializar os dados no formato JSON, por exemplo.
⚠️ Aviso
|
Até Python 3.7, o método |
Desde Python 3.7, a namedtuple
aceita o argumento nomeado defaults
, fornecendo um iterável de N valores default para cada um dos N campos mais à direita na definição da classe.
O Exemplo 70 mostra como definir uma tupla nomeada Coordinate
com um valor default para o campo reference
.
>>> Coordinate = namedtuple('Coordinate', 'lat lon reference', defaults=['WGS84'])
>>> Coordinate(0, 0)
Coordinate(lat=0, lon=0, reference='WGS84')
>>> Coordinate._field_defaults
{'reference': 'WGS84'}
Na Seção 5.2.1.2, mencionei que é mais fácil programar métodos com a sintaxe de classe suportada por typing.NamedTuple
and @dataclass
.
Você também pode acrescentar métodos a uma namedtuple
, mas é um remendo.
Pule a próxima caixinha se você não estiver interessada em gambiarras.
Agora vamos ver a variante typing.NamedTuple
.
5.4. Tuplas nomeadas com tipo
A classe Coordinate
com um campo default, do Exemplo 70, pode ser escrita usando typing.NamedTuple
, como se vê no Exemplo 72.
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float # (1)
lon: float
reference: str = 'WGS84' # (2)
-
Todo campo de instância precisa ter uma anotação de tipo.
-
O campo de instância
reference
é anotado com um tipo e um valor default.
As classes criadas por typing.NamedTuple
não tem qualquer método além daqueles que collections.namedtuple
também gera—e aquele herdados de tuple
.
A única diferença é a presença do atributo de classe __annotations__
—que Python ignora completamente durante a execução do programa.
Dado que o principal recurso de typing.NamedTuple
são as anotações de tipo, vamos dar uma rápida olhada nisso antes de continuar nossa exploração das fábricas de classes de dados.
5.5. Introdução às dicas de tipo
Dicas de tipo—também chamadas anotações de tipo—são formas de declarar o tipo esperado dos argumentos, dos valores devolvidos, das variáveis e dos atributos de funções.
A primeira coisa que você precisa saber sobre dicas de tipo é que elas não são impostas de forma alguma pelo compilador de bytecode ou pelo interpretador de Python.
✒️ Nota
|
Essa é uma introdução muito breve sobre dicas de tipo, suficiente apenas para que a sintaxe e o propósito das anotações usadas nas declarações de |
5.5.1. Nenhum efeito durante a execução
Pense nas dicas de tipo de Python como "documentação que pode ser verificada por IDEs e verificadores de tipo".
Isso porque as dicas de tipo não tem qualquer impacto sobre o comportamento de programas em Python durante a execução. Veja o Exemplo 73.
>>> import typing
>>> class Coordinate(typing.NamedTuple):
... lat: float
... lon: float
...
>>> trash = Coordinate('Ni!', None)
>>> print(trash)
Coordinate(lat='Ni!', lon=None) # (1)
-
Eu avisei: não há verificação de tipo durante a execução!
Se você incluir o código do Exemplo 73 em um módulo de Python,
ela vai rodar e exibir uma Coordinate
sem sentido, e sem gerar qualquer erro ou aviso:
$ python3 nocheck_demo.py
Coordinate(lat='Ni!', lon=None)
O objetivo primário das dicas de tipo é ajudar os verificadores de tipo externos, como o Mypy ou o verificador de tipo embutido do PyCharm IDE. Essas são ferramentas de análise estática: elas verificam código-fonte Python "parado", não código em execução.
Para observar o efeito das dicas de tipo, é necessário executar umas dessas ferramentas sobre seu código—como um linter (analisador de código). Por exemplo, eis o quê o Mypy tem a dizer sobre o exemplo anterior:
$ mypy nocheck_demo.py
nocheck_demo.py:8: error: Argument 1 to "Coordinate" has
incompatible type "str"; expected "float"
nocheck_demo.py:8: error: Argument 2 to "Coordinate" has
incompatible type "None"; expected "float"
Como se vê, dada a definição de Coordinate
, o Mypy sabe que os dois argumentos para criar um instância devem ser do tipo float
,
mas atribuição a trash
usa uma str
e None
.[59]
Vamos falar agora sobre a sintaxe e o significado das dicas de tipo.
5.5.2. Sintaxe de anotação de variáveis
Tanto typing.NamedTuple
quanto @dataclass
usam a sintaxe de anotações de variáveis definida na PEP 526 (EN).
Vamos ver aqui uma pequena introdução àquela sintaxe, no contexto da definição de atributos em declarações class
.
A sintaxe básica da anotação de variáveis é :
var_name: some_type
A seção "Acceptable type hints" (_Dicas de tipo aceitáveis), na PEP 484, explica o que são tipo aceitáveis. Porém, no contexto da definição de uma classe de dados, os tipos mais úteis geralmente serão os seguintes:
-
Uma classe concreta, por exemplo
str
ouFrenchDeck
. -
Um tipo de coleção parametrizada, como
list[int]
,tuple[str, float]
, etc. -
typing.Optional
, por exemploOptional[str]
—para declarar um campo que pode ser umastr
ouNone
.
Você também pode inicializar uma variável com um valor. Em uma declaração de typing.NamedTuple
ou @dataclass
, aquele valor se tornará o default daquele atributo quando o argumento correspondente for omitido na chamada de inicialização:
var_name: some_type = a_value
5.5.3. O significado das anotações de variáveis
Vimos, no tópico Seção 5.5.1, que dicas de tipo não tem qualquer efeito durante a execução de um programa.
Mas no momento da importação—quando um módulo é carregado—o Python as lê para construir o dicionário __annotations__
, que typing.NamedTuple
e @dataclass
então usam para aprimorar a classe.
Vamos começar essa exploração no Exemplo 74, com uma classe simples,
para mais tarde ver que recursos adicionais são acrescentados por typing.NamedTuple
e @dataclass
.
class DemoPlainClass:
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)
-
a
se torna um registro em__annotations__
, mas é então descartada: nenhum atributo chamadoa
é criado na classe. -
b
é salvo como uma anotação, e também se torna um atributo de classe com o valor1.1
. -
c
é só um bom e velho atributo de classe básico, sem uma anotação.
Podemos checar isso no console, primeiro lendo o __annotations__
da DemoPlainClass
,
e daí tentando obter os atributos chamados a
, b
, e c
:
>>> from demo_plain import DemoPlainClass
>>> DemoPlainClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoPlainClass.a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'DemoPlainClass' has no attribute 'a'
>>> DemoPlainClass.b
1.1
>>> DemoPlainClass.c
'spam'
Observe que o atributo especial __annotations__
é criado pelo interpretador
para registrar dicas de tipo que aparecem no código-fonte—mesmo em uma classe básica.
O a
sobrevive apenas como uma anotação, não se torna um atributo da classe, porque nenhum valor é atribuído a
ele.[60]
O b
e o c
são armazenados como atributos de classe porque são vinculados a valores.
Nenhum desses três atributos estará em uma nova instância de DemoPlainClass
.
Se você criar um objeto o = DemoPlainClass()
, o.a
vai gerar um AttributeError
,
enquanto o.b
e o.c
vão obter os atributos de classe com os valores
1.1
e 'spam'
—que é apenas o comportamento normal de um objeto Python.
5.5.3.1. Inspecionando uma typing.NamedTuple
Agora vamos examinar uma classe criada com typing.NamedTuple
(Exemplo 75),
usando os mesmos atributos e anotações da DemoPlainClass
do Exemplo 74.
typing.NamedTuple
import typing
class DemoNTClass(typing.NamedTuple):
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)
-
a
se torna uma anotação e também um atributo de instância. -
b
é outra anotação, mas também se torna um atributo de instância com o valor default1.1
. -
c
é só um bom e velho atributo de classe comum; não será mencionado em nenhuma anotação.
Inspecionando a DemoNTClass
, temos o seguinte:
>>> from demo_nt import DemoNTClass
>>> DemoNTClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoNTClass.a
<_collections._tuplegetter object at 0x101f0f940>
>>> DemoNTClass.b
<_collections._tuplegetter object at 0x101f0f8b0>
>>> DemoNTClass.c
'spam'
Aqui vemos as mesmas anotações para a
e b
que vimos no Exemplo 74.
Mas typing.NamedTuple
cria os atributos de classe a
e b
.
O atributo c é apenas um atributo de classe simples, com o valor 'spam'
.
Os atributos de classe a
e b
são descritores (descriptors)—um recurso avançado tratado no Capítulo 23.
Por ora, pense neles como similares a um getter de propriedades do objeto[61]:
métodos que não exigem o operador explícito de chamada ()
para obter um atributo de instância.
Na prática, isso significa que a
e b
vão funcionar como atributos de instância somente para leitura—o que faz sentido, se lembrarmos que instâncias de DemoNTClass
são apenas tuplas chiques, e tuplas são imutáveis.
A DemoNTClass
também recebe uma docstring personalizada:
>>> DemoNTClass.__doc__
'DemoNTClass(a, b)'
Vamos examinar uma instância de DemoNTClass
:
>>> nt = DemoNTClass(8)
>>> nt.a
8
>>> nt.b
1.1
>>> nt.c
'spam'
Para criar nt
, precisamos passar pelo menos o argumento a
para DemoNTClass
. O construtor também aceita um argumento b
, mas como este último tem um valor default (de 1.1
), ele é opcional.
Como esperado, o objeto nt
possui os atributos a
e b
; ele não tem um atributo c
, mas Python obtém c
da classe, como de hábito.
Se você tentar atribuir valores para nt.a
, nt.b
, nt.c
, ou mesmo para nt.z
, vai gerar uma exceção AttributeError
, com mensagens de erro sutilmente distintas. Tente fazer isso, e reflita sobre as mensagens.
5.5.3.2. Inspecionando uma classe decorada com dataclass
Vamos agora examinar o Exemplo 76.
@dataclass
from dataclasses import dataclass
@dataclass
class DemoDataClass:
a: int # (1)
b: float = 1.1 # (2)
c = 'spam' # (3)
-
a
se torna uma anotação, e também um atributo de instância controlado por um descritor. -
b
é outra anotação, e também se torna um atributo de instância com um descritor e um valor default de1.1
. -
c
é apenas um atributo de classe comum; nenhuma anotação se refere a ele.
Podemos então verificar o __annotations__
, o __doc__
, e os atributos a
, b
, c
no DemoDataClass
:
>>> from demo_dc import DemoDataClass
>>> DemoDataClass.__annotations__
{'a': <class 'int'>, 'b': <class 'float'>}
>>> DemoDataClass.__doc__
'DemoDataClass(a: int, b: float = 1.1)'
>>> DemoDataClass.a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'DemoDataClass' has no attribute 'a'
>>> DemoDataClass.b
1.1
>>> DemoDataClass.c
'spam'
O __annotations__
e o __doc__
não guardam surpresas.
Entretanto, não há um atributo chamado a
em DemoDataClass
—diferente do que ocorre na DemoNTClass
do Exemplo 75,
que inclui um descritor para obter a
das instâncias da classe, como atributos somente para leitura (aquele misterioso <_collections.tuplegetter>
).
Isso ocorre porque o atributo a
só existirá nas instâncias de DemoDataClass
.
Será um atributo público, que poderemos obter e definir, a menos que a classe seja frozen.
Mas b
e c
existem como atributos de classe, com b
contendo o valor default para o atributo de instância b
, enquanto c
é apenas um atributo de classe que não será vinculado a instâncias.
Vejamos como se parece uma instância de DemoDataClass
:
>>> dc = DemoDataClass(9)
>>> dc.a
9
>>> dc.b
1.1
>>> dc.c
'spam'
Novamente, a
e b
são atributos de instância, e c
é um atributo de classe obtido através da instância.
Como mencionado, instâncias de DemoDataClass
são mutáveis—e nenhuma verificação de tipo é realizada durante a execução:
>>> dc.a = 10
>>> dc.b = 'oops'
Podemos fazer atribuições ainda mais ridículas:
>>> dc.c = 'whatever'
>>> dc.z = 'secret stash'
Agora a instância dc
tem um atributo c
—mas isso não muda o atributo de classe c
.
E podemos adicionar um novo atributo z
.
Isso é o comportamento normal de Python: instâncias regulares podem ter seus próprios atributos, que não aparecem na classe.[62]
5.6. Mais detalhes sobre @dataclass
Até agora, só vimos exemplos simples do uso de @dataclass
. Esse decorador aceita vários argumentos nomeados. Esta é sua assinatura:
@dataclass(*, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False)
O *
na primeira posição significa que os parâmetros restantes são todos parâmetros nomeados. A Tabela 13 os descreve.
Option | Meaning | Default | Notes |
---|---|---|---|
|
Gera o |
|
Ignorado se o
|
|
Gera o |
|
Ignorado se o
|
|
Gera o |
|
Ignorado se o
|
|
Gera |
|
Se |
|
Gera o |
|
Semântica complexa e várias restrições—veja a: documentação de dataclass. |
|
Cria instâncias "imutáveis" |
|
As instâncias estarão razoavelmente protegidas contra mudanças acidentais, mas não serão realmente imutáveis.[63] |
Os defaults são, de fato, as configurações mais úteis para os casos de uso mais comuns. As opções mais prováveis de serem modificadas de seus defaults são:
frozen=True
-
Protege as instâncias da classe de modificações acidentais.
order=True
-
Permite ordenar as instâncias da classe de dados.
Dada a natureza dinâmica de objetos Python, não é muito difícil para um programador curioso contornar a proteção oferecida por frozen=True
. Mas os truques necessários são fáceis de perceber em uma revisão do código.
Se tanto o argumento eq
quanto o frozen
forem True
, @dataclass
produz um método
__hash__
adequado, e daí as instâncias serão hashable.
O __hash__
gerado usará dados de todos os campos que não forem individualmente excluídos usando uma opção de campo, que veremos na Seção 5.6.1.
Se frozen=False
(o default), @dataclass
definirá __hash__
como None
, sinalizando que as instâncias não são hashable, e portanto sobrepondo o __hash__
de qualquer superclasse.
A PEP 557—Data Classes (Classe de Dados) (EN) diz o seguinte sobre unsafe_hash
:
Apesar de não ser recomendado, você pode forçar Classes de Dados a criarem um método
__hash__
comunsafe_hash=True
. Pode ser esse o caso, se sua classe for logicamente imutável e mesmo assim possa ser modificada. Este é um caso de uso especializado e deve ser considerado com cuidado.
Deixo o unsafe_hash
por aqui.
Se você achar que precisa usar essa opção, leia a
documentação de dataclasses.dataclass
.
Outras personalizações da classe de dados gerada podem ser feitas no nível dos campos.
5.6.1. Opções de campo
Já vimos a opção de campo mais básica: fornecer (ou não) um valor default junto com a dica de tipo.
Os campos de instância declarados se tornarão parâmetros no __init__
gerado.
Python não permite parâmetros sem um default após parâmetros com defaults.
Então, após declarar um campo com um valor default, cada um dos campos seguintes deve também ter um default.
Valores default mutáveis são a fonte mais comum de bugs entre desenvolvedores Python iniciantes.
Em definições de função, um valor default mutável é facilmente corrompido, quando uma invocação da função modifica o default, mudando o comportamento nas invocações posteriores—um tópico que vamos explorar na Seção 6.5.1 (no Capítulo 6).
Atributos de classe são frequentemente usados como valores default de atributos para instâncias, inclusive em classes de dados.
E o @dataclass
usa os valores default nas dicas de tipo para gerar parâmetros com defaults no
__init__
.
Para prevenir bugs, o @dataclass
rejeita a definição de classe que aparece no Exemplo 77.
ValueError
@dataclass
class ClubMember:
name: str
guests: list = []
Se você carregar o módulo com aquela classe ClubMember
, o resultado será esse:
$ python3 club_wrong.py
Traceback (most recent call last):
File "club_wrong.py", line 4, in <module>
class ClubMember:
...several lines omitted...
ValueError: mutable default <class 'list'> for field guests is not allowed:
use default_factory
A mensagem do ValueError
explica o problema e sugere uma solução: usar a default_factory
. O Exemplo 78 mostra como corrigir a ClubMember
.
ClubMember
funcionafrom dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)
No campo guests
do Exemplo 78, em vez de uma lista literal, o valor default é definido chamando a função dataclasses.field
com default_factory=list
.
O parâmetro default_factory
permite que você forneça uma função, classe ou qualquer outro invocável, que será chamado com zero argumentos, para gerar um valor default a cada vez que uma instância da classe de dados for criada. Dessa forma, cada instância de ClubMember
terá sua própria list
—ao invés de todas as instâncias compartilharem a mesma list
da classe, que raramente é o que queremos, e muitas vezes é um bug.
⚠️ Aviso
|
É bom que |
Se você estudar a documentação do módulo dataclasses
, verá um campo list
definido com uma sintaxe nova, como no Exemplo 79.
ClubMember
é mais precisafrom dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list[str] = field(default_factory=list) # (1)
-
list[str]
significa "uma lista de str."
A nova sintaxe list[str]
é um tipo genérico parametrizado: desde Python 3.9,
o tipo embutido list
aceita aquela notação com colchetes para especificar o tipo dos itens da lista.
⚠️ Aviso
|
Antes de Python 3.9, as coleções embutidas não suportavam a notação de tipagem genérica. Como uma solução temporária, há tipos correspondentes de coleções no módulo |
Vamos tratar dos tipos genéricos no Capítulo 8. Por ora, observe que o Exemplo 78 e o Exemplo 79 estão ambos corretos, e que o verificador de tipagem Mypy não reclama de nenhuma das duas definições de classe.
A diferença é que aquele guests: list
significa que guests
pode ser uma list
de objetos de qualquer natureza, enquanto guests: list[str]
diz que guests
deve ser uma list
na qual cada item é uma str
.
Isso permite que o verificador de tipos encontre (alguns) bugs em código que insira itens inválidos na lista, ou que leia itens dali.
A default_factory
é possivelmente a opção mais comum da função field
, mas há várias outras, listadas na Tabela 14.
Option | Meaning | Default |
---|---|---|
|
Valor default para o campo |
|
|
função com 0 parâmetros usada para produzir um valor default |
|
|
Incluir o campo nos parâmetros de |
|
|
Incluir o campo em |
|
|
Usar o campo nos métodos de comparação |
|
|
Incluir o campo no cálculo de |
None[65] |
|
Mapeamento com dados definidos pelo usuário; ignorado por |
|
A opção default
existe porque a chamada a field
toma o lugar do valor default na anotação do campo. Se você quisesse criar um campo athlete
com o valor default False
, e também omitir aquele campo do método __repr__
, escreveria o seguinte:
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)
athlete: bool = field(default=False, repr=False)
5.6.2. Processamento pós-inicialização
O método __init__
gerado por @dataclass
apenas recebe os argumentos passados e os atribui—ou seus valores default, se o argumento não estiver presente—aos atributos de instância, que são campos da instância. Mas pode ser necessário fazer mais que isso para inicializar a instância. Se for esse o caso, você pode fornecer um método __post_init__
.
Quando esse método existir, @dataclass
acrescentará código ao __init__
gerado para invocar
__post_init__
como o último passo da inicialização.
Casos de uso comuns para __post_init__
são validação e o cálculo de valores de campos baseado em outros campos. Vamos estudar um exemplo simples, que usa __post_init__
pelos dois motivos.
Primeiro, dê uma olhada no comportamento esperado de uma subclasse de ClubMember
, chamada HackerClubMember
, como descrito por doctests no Exemplo 80.
HackerClubMember
"""
``HackerClubMember`` objects accept an optional ``handle`` argument::
>>> anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')
>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')
If ``handle`` is omitted, it's set to the first part of the member's name::
>>> leo = HackerClubMember('Leo Rochael')
>>> leo
HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')
Members must have a unique handle. The following ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::
>>> leo2 = HackerClubMember('Leo DaVinci')
Traceback (most recent call last):
...
ValueError: handle 'Leo' already exists.
To fix, ``leo2`` must be created with an explicit ``handle``::
>>> leo2 = HackerClubMember('Leo DaVinci', handle='Neo')
>>> leo2
HackerClubMember(name='Leo DaVinci', guests=[], handle='Neo')
"""
Observe que precisamos fornecer handle
como um argumento nomeado, pois HackerClubMember
herda name
e guests
de ClubMember
, e acrescenta o campo handle
. A docstring gerada para HackerClubMember
mostra a ordem dos campos na chamada de inicialização:
>>> HackerClubMember.__doc__
"HackerClubMember(name: str, guests: list = <factory>, handle: str = '')"
Aqui <factory>
é um caminho mais curto para dizer que algum invocável vai produzir o valor default para guests
(no nosso caso, a fábrica é a classe list
).
O ponto é o seguinte: para fornecer um handle
mas não um guests
, precisamos passar handle
como um argumento nomeado.
A seção "Herança na documentação do módulo dataclasses
explica como a ordem dos campos é analisada quando existem vários níveis de herança.
✒️ Nota
|
No ch_inheritance vamos falar sobre o uso indevido da herança, especialmente quando as superclasses não são abstratas.
Criar uma hierarquia de classes de dados é, em geral, uma má ideia, mas nos serviu bem aqui para tornar o Exemplo 81 mais curto, e permitir que nos concentrássemos na declaração do campo |
O Exemplo 81 mostra a implementação.
HackerClubMember
from dataclasses import dataclass
from club import ClubMember
@dataclass
class HackerClubMember(ClubMember): # (1)
all_handles = set() # (2)
handle: str = '' # (3)
def __post_init__(self):
cls = self.__class__ # (4)
if self.handle == '': # (5)
self.handle = self.name.split()[0]
if self.handle in cls.all_handles: # (6)
msg = f'handle {self.handle!r} already exists.'
raise ValueError(msg)
cls.all_handles.add(self.handle) # (7)
-
HackerClubMember
estendeClubMember
. -
all_handles
é um atributo de classe. -
handle
é um campo de instância do tipostr
, com uma string vazia como valor default; isso o torna opcional. -
Obtém a classe da instância.
-
Se
self.handle
é a string vazia, a define como a primeira parte dename
. -
Se
self.handle
está emcls.all_handles
, gera umValueError
. -
Insere o novo
handle
emcls.all_handles
.
O Exemplo 81 funciona como esperado, mas não é satisfatório pra um verificador estático de tipos. A seguir veremos a razão disso, e como resolver o problema.
5.6.3. Atributos de classe tipados
Se verificarmos os tipos de Exemplo 81 com o Mypy, seremos repreendidos:
$ mypy hackerclub.py
hackerclub.py:37: error: Need type annotation for "all_handles"
(hint: "all_handles: Set[<type>] = ...")
Found 1 error in 1 file (checked 1 source file)
Infelizmente, a dica fornecida pelo Mypy (versão 0.910 quando essa seção foi revisada) não é muito útil no contexto do uso de @dataclass
.
Primeiro, ele sugere usar Set
, mas desde Python 3.9 podemos usar set
—sem a necessidade de importar Set
de typing
.
E mais importante, se acrescentarmos uma dica de tipo como set[…]
a all_handles
, @dataclass
vai encontrar essa anotação e transformar all_handles
em um campo de instância.
Vimos isso acontecer na Seção 5.5.3.2.
A forma de contornar esse problema definida na
PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (EN)
é horrível.
Para criar uma variável de classe com uma dica de tipo, precisamos usar um pseudo-tipo chamado typing.ClassVar
, que aproveita a notação de tipos genéricos ([]
) para definir o tipo da variável e também para declará-la como um atributo de classe.
Para fazer felizes tanto o verificador de tipos quando o @dataclass
, deveríamos declarar o all_handles
do Exemplo 81 assim:
all_handles: ClassVar[set[str]] = set()
Aquela dica de tipo está dizendo o seguinte:
all_handles
é um atributo de classe do tipo set-de-str, com umset
vazio como valor default.
Para escrever aquela anotação precisamos também importar ClassVar
do módulo typing
.
O decorador @dataclass
não se importa com os tipos nas anotações, exceto em dois casos, e este é um deles: se o tipo for ClassVar
, um campo de instância não será gerado para aquele atributo.
O outro caso onde o tipo de um campo é relevante para @dataclass
é quando declaramos variáveis apenas de inicialização, nosso próximo tópico.
5.6.4. Variáveis de inicialização que não são campos
Algumas vezes pode ser necessário passar para __init__
argumentos que não são campos de instância.
Tais argumentos são chamados "argumentos apenas de inicialização" (init-only variables) pela documentação de dataclasses
.
Para declarar um argumento desses, o módulo dataclasses
oferece o pseudo-tipo InitVar
, que usa a mesma sintaxe de typing.ClassVar
.
O exemplo dados na documentação é uma classe de dados com um campo inicializado a partir de um banco de dados, e o objeto banco de dados precisa ser passado para o __init__
.
O Exemplo 82 mostra o código que ilustra a seção "Variáveis de inicialização apenas".
dataclasses
@dataclass
class C:
i: int
j: int | None = None
database: InitVar[DatabaseType | None] = None
def __post_init__(self, database):
if self.j is None and database is not None:
self.j = database.lookup('j')
c = C(10, database=my_database)
Veja como o atributo database
é declarado. InitVar
vai evitar que @dataclass
trate database
como um campo regular.
Ele não será definido como um atributo de instância, e a função dataclasses.fields
não vai listá-lo.
Entretanto, database
será um dos argumentos aceitos pelo __init__
gerado, e também será passado para o __post_init__
. Ao escrever aquele método é preciso adicionar o argumento correspondente à sua assinatura, como mostra o Exemplo 82.
Esse longo tratamento de @dataclass
cobriu os recursos mais importantes desse decorador—alguns deles apareceram em seções anteriores, como na Seção 5.2.1, onde falamos em paralelo das três fábricas de classes de dados. A documentação de dataclasses
e a PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (EN) têm todos os detalhes.
Na próxima seção apresento um exemplo mais completo com o @dataclass
.
5.6.5. Exemplo de @dataclass: o registro de recursos do Dublin Core
Frequentemente as classes criadas com o @dataclass
vão ter mais campos que os exemplos muito curtos apresentados até aqui.
O Dublin Core (EN) oferece a fundação para um exemplo mais típico de @dataclass
.
O Dublin Core é um esquema de metadados que visa descrever objetos digitais, tais como, videos, sons, imagens, textos e sites na web. Aplicações de Dublin Core utilizam XML e o RDF (Resource Description Framework).[66]
O padrão define 15 campos opcionais; a classe Resource
, no Exemplo 83, usa 8 deles.
Resource
, uma classe baseada nos termos do Dublin Corefrom dataclasses import dataclass, field
from typing import Optional
from enum import Enum, auto
from datetime import date
class ResourceType(Enum): # (1)
BOOK = auto()
EBOOK = auto()
VIDEO = auto()
@dataclass
class Resource:
"""Media resource description."""
identifier: str # (2)
title: str = '<untitled>' # (3)
creators: list[str] = field(default_factory=list)
date: Optional[date] = None # (4)
type: ResourceType = ResourceType.BOOK # (5)
description: str = ''
language: str = ''
subjects: list[str] = field(default_factory=list)
-
Esse
Enum
vai fornecer valores de um tipo seguro para o campoResource.type
. -
identifier
é o único campo obrigatório. -
title
é o primeiro campo com um default. Isso obriga todos os campos abaixo dele a fornecerem defaults. -
O valor de
date
pode ser uma instância dedatetime.date
ouNone
. -
O default do campo
type
éResourceType.BOOK
.
O Exemplo 84 mostra um doctest, para demonstrar como um registro Resource
aparece no código.
Resource
, uma classe baseada nos termos do Dublin Core >>> description = 'Improving the design of existing code'
>>> book = Resource('978-0-13-475759-9', 'Refactoring, 2nd Edition',
... ['Martin Fowler', 'Kent Beck'], date(2018, 11, 19),
... ResourceType.BOOK, description, 'EN',
... ['computer programming', 'OOP'])
>>> book # doctest: +NORMALIZE_WHITESPACE
Resource(identifier='978-0-13-475759-9', title='Refactoring, 2nd Edition',
creators=['Martin Fowler', 'Kent Beck'], date=datetime.date(2018, 11, 19),
type=<ResourceType.BOOK: 1>, description='Improving the design of existing code',
language='EN', subjects=['computer programming', 'OOP'])
O __repr__
gerado pelo @dataclass
é razoável,
mas podemos torná-lo mais legível.
Esse é o formato que queremos para repr(book)
:
>>> book # doctest: +NORMALIZE_WHITESPACE
Resource(
identifier = '978-0-13-475759-9',
title = 'Refactoring, 2nd Edition',
creators = ['Martin Fowler', 'Kent Beck'],
date = datetime.date(2018, 11, 19),
type = <ResourceType.BOOK: 1>,
description = 'Improving the design of existing code',
language = 'EN',
subjects = ['computer programming', 'OOP'],
)
O Exemplo 85 é o código para o __repr__
, produzindo o formato que aparece no trecho anterior.
Esse exemplo usa dataclass.fields
para obter os nomes dos campos da classe de dados.
dataclass/resource_repr.py
: código para o método __repr__
, implementado na classe Resource
do Exemplo 83 def __repr__(self):
cls = self.__class__
cls_name = cls.__name__
indent = ' ' * 4
res = [f'{cls_name}('] # (1)
for f in fields(cls): # (2)
value = getattr(self, f.name) # (3)
res.append(f'{indent}{f.name} = {value!r},') # (4)
res.append(')') # (5)
return '\n'.join(res) # (6)
-
Dá início à lista
res
, para criar a string de saída com o nome da classe e o parênteses abrindo. -
Para cada campo
f
na classe… -
…obtém o atributo nomeado da instância.
-
Anexa uma linha indentada com o nome do campo e
repr(value)
—é isso que o!r
faz. -
Acrescenta um parênteses fechando.
-
Cria uma string de múltiplas linhas a partir de
res
, e devolve essa string.
Com esse exemplo, inspirado pelo espírito de Dublin, Ohio, concluímos nosso passeio pelas fábricas de classes de dados de Python.
Classes de dados são úteis, mas podem estar sendo usadas de forma excessiva em seu projeto. A próxima seção explica isso.
5.7. A classe de dados como cheiro no código
Independente de você implementar uma classe de dados escrevendo todo o código ou aproveitando as facilidades oferecidas por alguma das fábricas de classes descritas nesse capítulo, fique alerta: isso pode sinalizar um problema em seu design.
No Refactoring: Improving the Design of Existing Code (Refatorando: Melhorando o Design de Código Existente), 2nd ed. (Addison-Wesley), Martin Fowler e Kent Beck apresentam um catálogo de "cheiros no código"[67]—padrões no código que podem indicar a necessidade de refatoração. O verbete entitulado "Data Class" (Classe de Dados) começa assim:
Essas são classes que tem campos, métodos para obter e definir os campos, e nada mais. Tais classes são recipientes burros de dados, e muitas vezes são manipuladas de forma excessivamente detalhada por outras classes.
No site pessoal de Fowler, há um post muito esclarecedor chamado "Code Smell" (Cheiro no Código) (EN). Esse texto é muito relevante para nossa discussão, pois o autor usa a classe de dados como um exemplo de cheiro no código, e sugere alternativas para lidar com ele. Abaixo está a tradução integral daquele artigo.[68]
A principal ideia da programação orientada a objetos é manter o comportamento e os dados juntos, na mesma unidade de código: uma classe. Se uma classe é largamente utilizada mas não tem qualquer comportamento próprio significativo, é bem provável que o código que interage com as instâncias dessa classe esteja espalhado (ou mesmo duplicado) em métodos e funções ao longo de todo o sistema—uma receita para dores de cabeça na manutenção. Por isso, as refatorações de Fowler para lidar com uma classe de dados envolvem trazer responsabilidades de volta para a classe.
Levando o que foi dito acima em consideração, há alguns cenários comuns onde faz sentido ter um classe de dados com pouco ou nenhum comportamento.
5.7.1. A classe de dados como um esboço
Nesse cenário, a classe de dados é uma implementação simplista inicial de uma classe, para dar início a um novo projeto ou módulo. Com o tempo, a classe deve ganhar seus próprios métodos, deixando de depender de métodos de outras classes para operar sobre suas instâncias. O esboço é temporário; ao final do processo, sua classe pode se tornar totalmente independente da fábrica usada inicialmente para criá-la.
Python também é muito usado para resolução rápida de problemas e para experimentaçào, e nesses casos é aceitável deixar o esboço pronto para uso.
5.7.2. A classe de dados como representação intermediária
Uma classe de dados pode ser útil para criar registros que serão exportados para o JSON ou algum outro formato de intercomunicação, ou para manter dados que acabaram de ser importados, cruzando alguma fronteira do sistema. Todas as fábricas de classes de dados de Python oferecem um método ou uma função para converter uma instância em um dict
simples, e você sempre pode invocar o construtor com um dict
, usado para passar argumentos nomeados expandidos com **
. Um dict
desses é muito similar a um registro JSON.
Nesse cenário, as instâncias da classe de dados devem ser tratadas como objetos imutáveis—mesmo que os campos sejam mutáveis, não deveriam ser modificados nessa forma intermediária. Mudá-los significa perder o principal benefício de manter os dados e o comportamento próximos. Quando o processo de importação/exportação exigir mudança nos valores, você deve implementar seus próprios métodos de fábrica, em vez de usar os métodos "as dict" existentes ou os construtores padrão.
5.8. Pattern Matching com instâncias de classes
Padrões de classe são projetados para "casar" com instâncias de classes por tipo e—opcionalmente—por atributos. O sujeito de um padrão de classe pode ser uma instância de qualquer classe, não apenas instâncias de classes de dados.[69]
Há três variantes de padrões de classes: simples, nomeado e posicional. Vamos estudá-las nessa ordem.
5.8.1. Padrões de classe simples
Já vimos um exemplo de padrões de classe simples usados como sub-padrões na Seção 2.6:
case [str(name), _, _, (float(lat), float(lon))]:
Aquele padrão "casa" com uma sequência de quatro itens, onde o primeiro item deve ser uma instância de str
e o último item deve ser um tupla de dois elementos, com duas instâncias de float
.
A sintaxe dos padrões de classe se parece com a invocação de um construtor.
Abaixo temos um padrão de classe que "casa" com valores float
sem vincular uma variável (o corpo do case
pode ser referir a x
diretamente, se necessário):
match x:
case float():
do_something_with(x)
Mas isso aqui possivelmente será um bug no seu código:
match x:
case float: # DANGER!!!
do_something_with(x)
No exemplo anterior, case float:
"casa" com qualquer sujeito, pois Python entende float
como uma variável, que é então vinculada ao sujeito.
A sintaxe float(x)
do padrão simples é um caso especial que se aplica apenas a onze tipos embutidos "abençoados", listados no final da seção "Class Patterns" (Padrões de Classe) (EN) da
PEP 634—Structural Pattern Matching: Specification ((Pattern Matching Estrutural: Especificação):
bool bytearray bytes dict float frozenset int list set str tuple
Nessas classes, a variável que parece um argumento do construtor—por exemplo, o x
em float(x)
—é vinculada a toda a instância do sujeito ou à parte do sujeito que "casa" com um sub-padrão, como exemplificado por str(name)
no padrão de sequência que vimos antes:
case [str(name), _, _, (float(lat), float(lon))]:
Se a classe não de um daqueles onze tipos embutidos "abençoados", então essas variáveis parecidas com argumentos representam padrões a serem testados com atributos de uma instância daquela classe.
5.8.2. Padrões de classe nomeados
Para entender como usar padrões de classe nomeados,
observe a classe City
e suas cinco instâncias no Exemplo 86, abaixo.
City
e algumas instânciasimport typing
class City(typing.NamedTuple):
continent: str
name: str
country: str
cities = [
City('Asia', 'Tokyo', 'JP'),
City('Asia', 'Delhi', 'IN'),
City('North America', 'Mexico City', 'MX'),
City('North America', 'New York', 'US'),
City('South America', 'São Paulo', 'BR'),
]
Dadas essas definições, a seguinte função devolve uma lista de cidades asiáticas:
def match_asian_cities():
results = []
for city in cities:
match city:
case City(continent='Asia'):
results.append(city)
return results
O padrão City(continent='Asia')
encontra qualquer instância de City
onde o atributo continent
seja igual a 'Asia'
, independente do valor dos outros atributos.
Para coletar o valor do atributo country
, você poderia escrever:
def match_asian_countries():
results = []
for city in cities:
match city:
case City(continent='Asia', country=cc):
results.append(cc)
return results
O padrão City(continent='Asia', country=cc)
encontra as mesmas cidades asiáticas, como antes, mas agora a variável cc
está vinculada ao atributo country
da instância.
Isso inclusive funciona se a variável do padrão também se chamar country
:
match city:
case City(continent='Asia', country=country):
results.append(country)
Padrões de classe nomeados são bastante legíveis, e funcionam com qualquer classe que possua atributos de instância públicos. Mas eles são um tanto prolixos.
Padrões de classe posicionais são mais convenientes em alguns casos, mas exigem suporte explícito da classe do sujeito, como veremos a seguir.
5.8.3. Padrões de classe posicionais
Dadas as definições do Exemplo 86, a seguinte função devolveria uma lista de cidades asiáticas, usando um padrão de classe posicional:
def match_asian_cities_pos():
results = []
for city in cities:
match city:
case City('Asia'):
results.append(city)
return results
O padrão City('Asia')
encontra qualquer instância de City
na qual o valor do primeiro atributo seja Asia
, independente do valor dos outros atributos.
Se você quiser obter o valor do atributo country
, poderia escrever:
def match_asian_countries_pos():
results = []
for city in cities:
match city:
case City('Asia', _, country):
results.append(country)
return results
O padrão City('Asia', _, country)
encontra as mesmas cidades de antes, mas agora variável country
está vinculada ao terceiro atributo da instância.
Eu falei do "primeiro" ou do "terceiro" atributos, mas o quê isso realmente significa?
City
(ou qualquer classe) funciona com padrões posicionais graças a um atributo de classe especial chamado __match_args__
,
que as fábricas de classe vistas nesse capítulo criam automaticamente.
Esse é o valor de __match_args__
na classe City
:
>>> City.__match_args__
('continent', 'name', 'country')
Como se vê, __match_args__
declara os nomes dos atributos na ordem em que eles serão usados em padrões posicionais.
Na Seção 11.8 vamos escrever código para definir __match_args__
em uma classe que criaremos sem a ajuda de uma fábrica de classes.
👉 Dica
|
Você pode combinar argumentos nomeados e posicionais em um padrão.
Alguns, mas não todos, os atributos de instância disponíveis para o match podem estar listados no
|
Hora de um resumo de capítulo.
5.9. Resumo do Capítulo
O tópico principal desse capítulo foram as fábricas de classes de dados collections.namedtuple
, typing.NamedTuple
, e dataclasses.dataclass
.
Vimos como cada uma delas gera classes de dados a partir de descrições, fornecidas como argumentos a uma função fábrica ou, no caso das duas últimas, a partir de uma declaração class
com dicas de tipo.
Especificamente, ambas as variantes de tupla produzem subclasses de tuple
, acrescentando apenas a capacidade de acessar os campos por nome, e criando também um atributo de classe _fields
, que lista os nomes dos campos na forma de uma tupla de strings.
A seguir colocamos lado a lado os principais recursos de cada uma das três fábricas de classes, incluindo como extrair dados da instância como um dict
, como obter os nomes e valores default dos campos, e como criar uma nova instância a partir de uma instância existente.
Isso levou ao nosso primeiro contato com dicas de tipo, especialmente aquelas usadas para anotar atributos em uma declaração class
, usando a notação introduzida no Python 3.6 com a PEP 526—Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) (EN).
O aspecto provavelmente mais surpreeendente das dicas de tipo em geral é o fato delas não terem qualquer efeito durante a execução.
Python continua sendo uma linguagem dinâmica.
Ferramentas externas, como o Mypy, são necessárias para aproveitar a informação de tipagem na detecção de erros via análise estática do código-fonte.
Após um resumo básico da sintaxe da PEP 526, estudamos os efeitos das anotações em uma classe simples e em classes criadas por typing.NamedTuple
e por @dataclass
.
A seguir falamos sobre os recursos mais usados dentre os oferecidos por @dataclass
, e sobre a opção default_factory
da função dataclasses.field
.
Também demos uma olhada nas dicas de pseudo-tipo especiais typing.ClassVar
e
dataclasses.InitVar
, importantes no contexto das classes de dados.
Esse tópico central foi concluído com um exemplo baseado no schema Dublin Core, ilustrando como usar dataclasses.fields
para iterar sobre os atributos de uma instância de Resource
em um
__repr__
personalizado.
Então alertamos contra os possíveis usos abusivos das classes de dados, frustrando um princípio básico da programação orientada a objetos: os dados e as funções que acessam os dados devem estar juntos na mesma classe. Classes sem uma lógica podem ser um sinal de uma lógica fora de lugar.
Na última seção, vimos como o pattern matching funciona com instâncias de qualquer classe como sujeitos—e não apenas das classes criadas com as fábricas apresentadas nesse capítulo.
5.10. Leitura complementar
A documentação padrão de Python para as fábricas de classes de dados vistas aqui é muito boa, e inclui muitos pequenos exemplos.
Em especial para @dataclass
, a maior parte da PEP 557—Data Classes (Classes de Dados) (EN) foi copiada para a documentação do módulo dataclasses
.
Entretanto, algumas seções informativas da PEP 557 não foram copiadas,
incluindo "Why not just use namedtuple?" (Por que simplesmente não usar namedtuple?),
"Why not just use typing.NamedTuple?" (Por que simplesmente não usar typing.NamedTuple?), e a seção "Rationale" (Justificativa), que termina com a seguinte Q&A:
Quando não é apropriado usar Classes de Dados?
Quando for exigida compatibilidade da API com tuplas de dicts. Quando for exigida validação de tipo além daquela oferecida pelas PEPs 484 e 526 , ou quando for exigida validação ou conversão de valores.
PEP 557 "Justificativa"
Em RealPython.com, Geir Arne Hjelle escreveu um "Ultimate guide to data classes in Python 3.7" (O guia definitivo das classes de dados no Python 3.7) (EN) muito completo.
Na PyCon US 2018, Raymond Hettinger apresentou "Dataclasses: The code generator to end all code generators" (video) (Dataclasses: O gerador de código para acabar com todos os geradores de código) (EN).
Para mais recursos e funcionalidade avançada, incluindo validação, o
projeto attrs (EN),
liderado por Hynek Schlawack, surgiu anos antes de dataclasses
e oferece mais facilidades, com a promessa de "trazer de volta a alegria de criar classes, liberando você do tedioso trabalho de implementar protocolos de objeto
(também conhecidos como métodos dunder)".
A influência do attrs sobre o @dataclass
é reconhecida por Eric V. Smith na PEP 557.
Isso provavelmente inclui a mais importante decisão de Smith sobre a API: o uso de um decorador de classe em vez de uma classe base ou de uma metaclasse para realizar a tarefa.
Glyph—fundador do projeto Twisted—escreveu uma excelente introdução à attrs em "The One Python Library Everyone Needs" (A Biblioteca Python que Todo Mundo Precisa Ter) (EN). A documentação da attrs inclui uma discussão aobre alternativas.
O autor de livros, instrutor e cientista maluco da computação Dave Beazley escreveu o
cluegen, um outro gerador de classes de dados.
Se você já assistiu alguma palestra do David, sabe que ele é um mestre na metaprogramação Python a partir de princípios básicos.
Então achei inspirador descobrir, no arquivo README.md do cluegen,
o caso de uso concreto que o motivou a criar uma alternativa ao @dataclass
de Python,
e sua filosofia de apresentar uma abordagem para resolver o problema,
ao invés de fornecer uma ferramenta: a ferramenta pode inicialmente ser mais rápida de usar ,
mas a abordagem é mais flexível e pode ir tão longe quanto você deseje.
Sobre a classe de dados como um cheiro no código, a melhor fonte que encontrei foi livro de Martin Fowler, Refactoring ("Refatorando"), 2ª ed. A versão mais recente não traz a citação da epígrafe deste capitulo, "Classes de dados são como crianças…", mas apesar disso é a melhor edição do livro mais famoso de Fowler, em especial para pythonistas, pois os exemplos são em JavaScript moderno, que é mais próximo de Python que de Java—a linguagem usada na primeira edição.
O site Refactoring Guru (Guru da Refatoração) (EN) também tem uma descrição do data class code smell (classe de dados como cheiro no código) (EN).
6. Referências, mutabilidade, e memória
“Você está triste,” disse o Cavaleiro em um tom de voz ansioso: “deixe eu cantar para você uma canção reconfortante. […] O nome da canção se chama ‘OLHOS DE HADOQUE’.”
“Oh, esse é o nome da canção?,” disse Alice, tentando parecer interessada.
“Não, você não entendeu,” retorquiu o Cavaleiro, um pouco irritado. “É assim que o nome É CHAMADO. O nome na verdade é ‘O ENVELHECIDO HOMEM VELHO.‘”
de Lewis Caroll
Alice e o Cavaleiro dão o tom do que veremos nesse capítulo. O tema é a distinção entre objetos e seus nomes; um nome não é o objeto; o nome é algo diferente.
Começamos o capítulo apresentando uma metáfora para variáveis em Python: variáveis são rótulos, não caixas. Se variáveis de referência não são novidade para você, a analogia pode ainda ser útil para ilustrar questões de aliasing (“apelidamento”) para alguém.
Depois discutimos os conceitos de identidade, valor e apelidamento de objetos. Uma característica surpreendente das tuplas é revelada: elas são imutáveis, mas seus valores podem mudar. Isso leva a uma discussão sobre cópias rasas e profundas. Referências e parâmetros de funções são o tema seguinte: o problema com parâmetros mutáveis por default e formas seguras de lidar com argumentos mutáveis passados para nossas funções por clientes.
As últimas seções do capítulo tratam de coleta de lixo (“garbage collection”), o comando del
e de algumas peças que Python prega com objetos imutáveis.
É um capítulo bastante árido, mas os tópicos tratados podem causar muitos bugs sutis em programas reais em Python.
6.1. Novidades nesse capítulo
Os tópicos tratados aqui são muito estáveis e fundamentais. Não foi introduzida nenhuma mudança digna de nota nesta segunda edição.
Acrescentei um exemplo usando is
para testar a existência de um objeto sentinela, e um aviso sobre o mau uso do operador is
no final de Seção 6.3.1.
Este capítulo estava na Parte IV, mas decidi abordar esses temas mais cedo, pois eles funcionam melhor como o encerramento da Parte II, “Estruturas de Dados”, que como abertura de “Práticas de Orientação a Objetos"
✒️ Nota
|
A seção sobre “Referências Fracas” da primeira edição deste livro agora é um post em fluentpython.com. |
Vamos começar desaprendendo que uma variável é como uma caixa onde você guarda dados.
6.2. Variáveis não são caixas
Em 1997, fiz um curso de verão sobre Java no MIT. A professora, Lynn Stein [70] , apontou que a metáfora comum, de “variáveis como caixas”, na verdade, atrapalha o entendimento de variáveis de referência em linguagens orientadas a objetos. As variáveis em Python são como variáveis de referência em Java; uma metáfora melhor é pensar em uma variável como um rótulo (ou etiqueta) que associa um nome a um objeto. O exemplo e a figura a seguir ajudam a entender o motivo disso.
Exemplo 87 é uma interação simples que não pode ser explicada por “variáveis como caixas”. A Figura 18 ilustra o motivo de metáfora da caixa estar errada em Python, enquanto etiquetas apresentam uma imagem mais útil para entender como variáveis funcionam.
>>> a = [1, 2, 3] (1)
>>> b = a (2)
>>> a.append(4) (3)
>>> b (4)
[1, 2, 3, 4]
-
Cria uma lista [1, 2, 3] e a vincula à variável
a
. -
Vincula a variável
b
ao mesmo valor referenciado pora
. -
Modifica a lista referenciada por
a
, anexando um novo item. -
É possível ver o efeito através da variável
b
. Se você pensar emb
como uma caixa que guardava uma cópia de[1, 2, 3]
da caixaa
, este comportamento não faz sentido.
Assim, a instrução b = a
não copia o conteúdo de uma caixa a
para uma caixa b
.
Ela cola uma nova etiqueta b
no objeto que já tem a etiqueta a
.
A professora Stein também falava sobre atribuição de uma maneira bastante específica. Por exemplo, quando discutia sobre um objeto representando uma gangorra em uma simulação, ela dizia: “A variável g foi atribuída à gangorra”, mas nunca “A gangorra foi atribuída à variável g”. Com variáveis de referência, faz muito mais sentido dizer que a variável é atribuída a um objeto, não o contrário. Afinal, o objeto é criado antes da atribuição. Exemplo 88 prova que o lado direito de uma atribuição é processado primeiro.
Já que o verbo “atribuir” é usado de diferentes maneiras,
uma alternativa útil é “vincular”:
a declaração de atribuição em Python x = …
vincula o nome x
ao objeto criado ou referenciado no lado direito.
E o objeto precisa existir antes que um nome possa ser vinculado a ele,
como demonstra Exemplo 88.
>>> class Gizmo:
... def __init__(self):
... print(f'Gizmo id: {id(self)}')
...
>>> x = Gizmo()
Gizmo id: 4301489152 (1)
>>> y = Gizmo() * 10 (2)
Gizmo id: 4301489432 (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>>
>>> dir() (4)
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']
-
A saída
Gizmo id: …
é um efeito colateral da criação de uma instância deGizmo
. -
Multiplicar uma instância de
Gizmo
levanta uma exceção. -
Aqui está a prova que um segundo
Gizmo
foi de fato instanciado antes que a multiplicação fosse tentada. -
Mas a variável
y
nunca foi criada, porque a exceção aconteceu enquanto a parte direita da atribuição estava sendo executada.
👉 Dica
|
Para entender uma atribuição em Python, leia primeiro o lado direito: é ali que o objeto é criado ou recuperado. Depois disso, a variável do lado esquerdo é vinculada ao objeto, como uma etiqueta colada a ele. Esqueça as caixas. |
Como variáveis são apenas meras etiquetas, nada impede que um objeto tenha várias etiquetas vinculadas a si. Quando isso acontece, você tem apelidos (aliases), nosso próximo tópico.
6.3. Identidade, igualdade e apelidos
Lewis Carroll é o pseudônimo literário do Prof. Charles Lutwidge Dodgson. O Sr. Carroll não é apenas igual ao Prof. Dodgson, eles são exatamente a mesma pessoa. Exemplo 89 expressa essa ideia em Python.
charles
e lewis
se referem ao mesmo objeto>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
>>> lewis = charles (1)
>>> lewis is charles
True
>>> id(charles), id(lewis) (2)
(4300473992, 4300473992)
>>> lewis['balance'] = 950 (3)
>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
-
lewis
é um apelido paracharles
. -
O operador
is
e a funçãoid
confirmam essa afirmação. -
Adicionar um item a
lewis
é o mesmo que adicionar um item acharles
.
Entretanto, suponha que um impostor—vamos chamá-lo de Dr. Alexander Pedachenko—diga que é o verdadeiro Charles L. Dodgson, nascido em 1832. Suas credenciais podem ser as mesmas, mas o Dr. Pedachenko não é o Prof. Dodgson. Figura 19 ilustra esse cenário.
charles
e lewis
estão vinculados ao mesmo objeto; alex
está vinculado a um objeto diferente de valor igual.Exemplo 90 implementa e testa o objeto alex como apresentado em Figura 19.
alex
e charles
são iguais quando comparados, mas alex
não é charles
>>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} (1)
>>> alex == charles (2)
True
>>> alex is not charles (3)
True
-
alex
é uma referência a um objeto que é uma réplica do objeto vinculado acharles
. -
Os objetos são iguais quando comparados devido à implementação de
__eq__
na classedict
. -
Mas são objetos distintos. Essa é a forma pythônica de escrever a negação de uma comparação de identidade:
a is not b
.
Exemplo 89 é um exemplo de apelidamento (aliasing). Naquele código, lewis
e charles
são apelidos: duas variáveis vinculadas ao mesmo objeto. Por outro lado, alex
não é um apelido para charles
: essas variáveis estão vinculadas a objetos diferentes. Os objetos vinculados a alex
e charles
tem o mesmo valor — é isso que ==
compara — mas tem identidades diferentes.
Na The Python Language Reference (Referência da Linguagem Python), https://docs.python.org/pt-br/3/reference/datamodel.html#objects-values-and-types está escrito:
A identidade de um objeto nunca muda após ele ter sido criado; você pode pensar nela como o endereço do objeto na memória. O operador
is
compara a identidade de dois objetos; a funçãoid()
retorna um inteiro representando essa identidade.
O verdadeiro significado do ID de um objeto depende da implementação da linguagem. Em CPython, id()
retorna o endereço de memória do objeto, mas outro interpretador Python pode retornar algo diferente. O ponto fundamental é que o ID será sempre um valor numérico único, e ele jamais mudará durante a vida do objeto.
Na prática, nós raramente usamos a função id()
quando programamos. A verificação de identidade é feita, na maior parte das vezes, com o operador is
, que compara os IDs dos objetos, então nosso código não precisa chamar id()
explicitamente. A seguir vamos falar sobre is
versus ==
.
👉 Dica
|
Para o revisor técnico Leonardo Rochael, o uso mais frequente de |
6.3.1. Escolhendo Entre == e is
O operador ==
compara os valores de objetos (os dados que eles contêm), enquanto is
compara suas identidades.
Quando estamos programando, em geral, nos preocupamos mais com os valores que com as identidades dos objetos, então ==
aparece com mais frequência que is
em programas Python.
Entretanto, se você estiver comparando uma variável com um singleton (um objeto único) faz mais sentido usar is
.
O caso mais comum, de longe, é verificar se a variável está vinculada a None
.
Esta é a forma recomendada de fazer isso:
x is None
E a forma apropriada de escrever sua negação é:
x is not None
None
é o singleton mais comum que testamos com is
.
Objetos sentinela são outro exemplo de singletons que testamos com is
.
Veja um modo de criar e testar um objeto sentinela:
END_OF_DATA = object()
# ... many lines
def traverse(...):
# ... more lines
if node is END_OF_DATA:
return
# etc.
O operador is
é mais rápido que ==
, pois não pode ser sobrecarregado. Daí Python não precisa encontrar e invocar métodos especiais para calcular seu resultado, e o processamento é tão simples quanto comparar dois IDs inteiros. Por outro lado, a == b
é açúcar sintático para a.__eq__(b)
. O método __eq__
, herdado de object
, compara os IDs dos objetos, então produz o mesmo resultado de is
. Mas a maioria dos tipos embutidos sobrepõe __eq__
com implementações mais úteis, que levam em consideração os valores dos atributos dos objetos. A determinação da igualdade pode envolver muito processamento—por exemplo, quando se comparam coleções grandes ou estruturas aninhadas com muitos níveis.
⚠️ Aviso
|
Normalmente estamos mais interessados na igualdade que na identidade de objetos. Checar se o objeto é |
Para concluir essa discussão de identidade versus igualdade, vamos ver como o tipo notoriamente imutável tuple
não é assim tão invariável quanto você poderia supor.
6.3.2. A imutabilidade relativa das tuplas
As tuplas, como a maioria das coleções em Python — lists, dicts, sets, etc..— são contêiners: eles armazenam referências para objetos.[71]
Se os itens referenciados forem mutáveis, eles poderão mudar, mesmo que tupla em si não mude. Em outras palavras, a imutabilidade das tuplas, na verdade, se refere ao conteúdo físico da estrutura de dados tupla
(isto é, as referências que ela mantém), e não se estende aos objetos referenciados.
Exemplo 91 ilustra uma situação em que o valor de uma tupla muda como resultado de mudanças em um objeto mutável ali referenciado. O que não pode nunca mudar em uma tupla é a identidade dos itens que ela contém.
t1
e t2
inicialmente são iguais, mas a mudança em um item mutável dentro da tupla t1
as torna diferentes>>> t1 = (1, 2, [30, 40]) (1)
>>> t2 = (1, 2, [30, 40]) (2)
>>> t1 == t2 (3)
True
>>> id(t1[-1]) (4)
4302515784
>>> t1[-1].append(99) (5)
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1]) (6)
4302515784
>>> t1 == t2 (7)
False
-
t1
é imutável, mast1[-1]
é mutável. -
Cria a tupla
t2
, cujos itens são iguais àqueles det1
. -
Apesar de serem objetos distintos, quando comparados
t1
et2
são iguais, como esperado. -
Obtém o ID da lista na posição
t1[-1]
. -
Modifica diretamente a lista
t1[-1]
. -
O ID de
t1[-1]
não mudou, apenas seu valor. -
t1
et2
agora são diferentes
Essa imutabilidade relativa das tuplas está por trás do enigma Seção 2.8.3. Essa também é razão pela qual não é possível gerar o hash de algumas tuplas, como vimos em Seção 3.4.1.
A distinção entre igualdade e identidade tem outras implicações quando você precisa copiar um objeto. Uma cópia é um objeto igual com um ID diferente. Mas se um objeto contém outros objetos, é preciso que a cópia duplique os objetos internos ou eles podem ser compartilhados? Não há uma resposta única. A seguir discutimos esse ponto.
6.4. A princípio, cópias são rasas
A forma mais fácil de copiar uma lista (ou a maioria das coleções mutáveis nativas) é usando o construtor padrão do próprio tipo. Por exemplo:
>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1) (1)
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1 (2)
True
>>> l2 is l1 (3)
False
-
list(l1)
cria uma cópia del1
. -
As cópias são iguais…
-
…mas se referem a dois objetos diferentes.
Para listas e outras sequências mutáveis, o atalho l2 = l1[:]
também cria uma cópia.
Contudo, tanto o construtor quanto [:]
produzem uma cópia rasa (shallow copy). Isto é, o contêiner externo é duplicado, mas a cópia é preenchida com referências para os mesmos itens contidos no contêiner original. Isso economiza memória e não causa qualquer problema se todos os itens forem imutáveis. Mas se existirem itens mutáveis, isso pode gerar surpresas desagradáveis.
Em Exemplo 92 criamos uma lista contendo outra lista e uma tupla, e então fazemos algumas mudanças para ver como isso afeta os objetos referenciados.
👉 Dica
|
Se você tem um computador conectado à internet disponível, recomendo fortemente que você assista à animação interativa do Exemplo 92 em Online Python Tutor. No momento em que escrevo, o link direto para um exemplo pronto no pythontutor.com não estava funcionando de forma estável. Mas a ferramenta é ótima, então vale a pena gastar seu tempo copiando e colando o código. |
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # (1)
l1.append(100) # (2)
l1[1].remove(55) # (3)
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22] # (4)
l2[2] += (10, 11) # (5)
print('l1:', l1)
print('l2:', l2)
-
l2
é uma cópia rasa del1
. Este estado está representado em Figura 20. -
Concatenar
100
al1
não tem qualquer efeito sobrel2
. -
Aqui removemos
55
da lista internal1[1]
. Isso afetal2
, poisl2[1]
está associado à mesma lista eml1[1]
. -
Para um objeto mutável como a lista referida por
l2[1]
, o operador+=
altera a lista diretamente. Essa mudança é visível eml1[1]
, que é um apelido paral2[1]
. -
+=
em uma tupla cria uma nova tupla e reassocia a variávell2[2]
a ela. Isso é equivalente a fazerl2[2] = l2[2] + (10, 11)
. Agora as tuplas na última posição del1
el2
não são mais o mesmo objeto. Veja Figura 21.
l2 = list(l1)
em Exemplo 92. l1
e l2
se referem a listas diferentes, mas as listas compartilham referências para um mesmo objeto interno, a lista [66, 55, 44]
e para a tupla (7, 8, 9)
. (Diagrama gerado pelo Online Python Tutor)A saída de Exemplo 92 é Exemplo 93, e o estado final dos objetos está representado em Figura 21.
l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
l1
e l2
: elas ainda compartilham referências para o mesmo objeto lista, que agora contém [66, 44, 33, 22]
, mas a operação l2[2] += (10, 11)
criou uma nova tupla com conteúdo (7, 8, 9, 10, 11)
, sem relação com a tupla (7, 8, 9)
referenciada por l1[2]
. (Diagram generated by the Online Python Tutor.)Já deve estar claro que cópias rasas são fáceis de criar, mas podem ou não ser o que você quer. Nosso próximo tópico é a criação de cópias profundas.
6.4.1. Cópias profundas e cópias rasas
Trabalhar
com cópias rasas nem sempre é um problema, mas algumas vezes você vai precisar criar cópias profundas
(isto é, cópias que não compartilham referências de objetos internos).
O módulo copy
oferece as funções deepcopy
e copy
, que retornam cópias profundas e rasas de objetos arbitrários.
Para ilustrar o uso de copy()
e deepcopy()
, Exemplo 94 define uma classe simples,
Bus
, representando um ônibus escolar que é carregado com passageiros,
e então pega ou deixa passageiros ao longo de sua rota.
class Bus:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
Agora, no Exemplo 95 interativo, vamos criar um objeto bus (bus1) e
dois clones—uma cópia rasa (bus2) e uma cópia profunda (bus3)—para ver o que acontece quando bus1
deixa um passageiro.
copy
versus deepcopy
>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752) (1)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David'] (2)
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800) (3)
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David'] (4)
-
Usando
copy
edeepcopy
, criamos três instâncias distintas deBus
. -
Após
bus1
deixar'Bill'
, ele também desaparece debus2
. -
A inspeção do atributo dos
passengers
mostra quebus1
ebus2
compartilham o mesmo objeto lista, poisbus2
é uma cópia rasa debus1
. -
bus3
é uma cópia profunda debus1
, então seu atributopassengers
se refere a outra lista.
Observe que, em geral, criar cópias profundas não é uma questão simples. Objetos podem conter referências cíclicas que fariam um algoritmo ingênuo entrar em um loop infinito. A função 'deepcopy' lembra dos objetos já copiados, de forma a tratar referências cíclicas de modo elegante. Isso é demonstrado em Exemplo 96.
b
tem uma referência para a
e então é concatenado a a
; ainda assim, deepcopy
consegue copiar a
.>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]
Além disso, algumas vezes uma cópia profunda pode ser profunda demais. Por exemplo, objetos podem ter referências para recursos externos ou para singletons (objetos únicos) que não devem ser copiados. Você pode controlar o comportamento de copy
e de deepcopy
implementando os métodos especiais __copy__
e __deepcopy__
, como descrito em https://docs.python.org/pt-br/3/library/copy.html [documentação do módulo copy
]
O compartilhamento de objetos através de apelidos também explica como a passagens de parâmetros funciona em Python, e o problema do uso de tipos mutáveis como parâmetros default. Vamos falar sobre essas questões a seguir.
6.5. Parâmetros de função como referências
O único modo de passagem de parâmetros em Python é a chamada por compartilhamento (call by sharing). É o mesmo modo usado na maioria das linguagens orientadas a objetos, incluinde Javascript, Ruby e Java (em Java isso se aplica aos tipos de referência; tipos primitivos usam a chamada por valor). Chamada por compartilhamento significa que cada parâmetro formal da função recebe uma cópia de cada referência nos argumentos. Em outras palavras, os parâmetros dentro da função se tornam apelidos dos argumentos.
O resultado desse esquema é que a função pode modificar qualquer objeto mutável passado a ela como parâmetro, mas não pode mudar a identidade daqueles objetos (isto é, ela não pode substituir integralmente um objeto por outro).
Exemplo 97 mostra uma função simples usando +=
com um de seus parâmetros. Quando passamos números, listas e tuplas para a função, os argumentos originais são afetados de maneiras diferentes.
>>> def f(a, b):
... a += b
... return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y (1)
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b (2)
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u) (3)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))
-
O número
x
não se altera. -
A lista
a
é alterada. -
A tupla
t
não se altera.
Outra questão relacionada a parâmetros de função é o uso de valores mutáveis como defaults, discutida a seguir.
6.5.1. Porque evitar tipos mutáveis como default em parâmetros
Parâmetros opcionais com valores default são um ótimo recurso para definição de funções em Python, permitindo que nossas APIs evoluam mantendo a compatibilidade com versões anteriores. Entretanto, você deve evitar usar objetos mutáveis como valores default em parâmetros.
Para ilustrar esse ponto, em Exemplo 98,
modificamos o método __init__
da classe Bus
de Exemplo 94 para criar HauntedBus
.
Tentamos ser espertos: em vez do valor default passengers=None
,
temos passengers=[]
, para evitar o if
do __init__
anterior.
Essa "esperteza" causa problemas.
class HauntedBus:
"""A bus model haunted by ghost passengers"""
def __init__(self, passengers=[]): # (1)
self.passengers = passengers # (2)
def pick(self, name):
self.passengers.append(name) # (3)
def drop(self, name):
self.passengers.remove(name)
-
Quando o argumento
passengers
não é passado, esse parâmetro é vinculado ao objeto lista default, que inicialmente está vazio. -
Essa atribuição torna
self.passengers
um apelido depassengers
, que por sua vez é um apelido para a lista default, quando um argumentopassengers
não é passado para a função. -
Quando os métodos
.remove()
e.append()
são usados comself.passengers
, estamos, na verdade, mudando a lista default, que é um atributo do objeto-função.
Exemplo 99 mostra o comportamento misterioso de HauntedBus
.
>>> bus1 = HauntedBus(['Alice', 'Bill']) (1)
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers (2)
['Bill', 'Charlie']
>>> bus2 = HauntedBus() (3)
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus() (4)
>>> bus3.passengers (5)
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers (6)
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers (7)
True
>>> bus1.passengers (8)
['Bill', 'Charlie']
-
bus1
começa com uma lista de dois passageiros. -
Até aqui, tudo bem: nenhuma surpresa em
bus1
. -
bus2
começa vazio, então a lista vazia default é vinculada aself.passengers
. -
bus3
também começa vazio, e novamente a lista default é atribuída. -
A lista default não está mais vazia!
-
Agora
Dave
, pego pelobus3
, aparece nobus2
. -
O problema:
bus2.passengers
ebus3.passengers
se referem à mesma lista. -
Mas
bus1.passengers
é uma lista diferente.
O problema é que instâncias de HauntedBus
que não recebem uma lista de passageiros inicial acabam todas compartilhando a mesma lista de passageiros entre si.
Este tipo de bug pode ser muito sutil. Como Exemplo 99 demonstra, quando HauntedBus
recebe uma lista com passageiros como parâmetro, ele funciona como esperado. As coisas estranhas acontecem somente quando HauntedBus
começa vazio, pois aí self.passengers
se torna um apelido para o valor default do parâmetro passengers
. O problema é que cada valor default é processado quando a função é definida — i.e., normalmente quando o módulo é carregado — e os valores default se tornam atributos do objeto-função. Assim, se o valor default é um objeto mutável e você o altera, a alteração vai afetar todas as futuras chamadas da função.
Após executar as linhas do exemplo em Exemplo 99, você pode inspecionar o objeto
HauntedBus.__init__
e ver os estudantes fantasma assombrando o atributo __defaults__
:
>>> dir(HauntedBus.__init__) # doctest: +ELLIPSIS
['__annotations__', '__call__', ..., '__defaults__', ...]
>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)
Por fim, podemos verificar que bus2.passengers
é um apelido vinculado ao primeiro elemento do atributo HauntedBus.__init__.__defaults__
:
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True
O problema com defaults mutáveis explica porque None
é normalmente usado como valor default para parâmetros que podem receber valores mutáveis. Em Exemplo 94, __init__
checa se o argumento passengers
é None
. Se for, self.passengers
é vinculado a uma nova lista vazia. Se passengers
não for None
, a implementação correta vincula uma cópia daquele argumento a self.passengers
.
A próxima seção explica porque copiar o argumento é uma boa prática.
6.5.2. Programação defensiva com argumentos mutáveis
Ao escrever uma função que recebe um argumento mutável, você deve considerar com cuidado se o cliente que chama sua função espera que o argumento passado seja modificado.
Por exemplo, se sua função recebe um dict
e precisa modificá-lo durante seu processamento,
esse efeito colateral deve ou não ser visível fora da função?
A resposta, na verdade, depende do contexto.
É tudo uma questão de alinhar as expectativas do autor da função com as do cliente da função.
O último exemplo com ônibus neste capítulo mostra como o TwilightBus
viola as expectativas
ao compartilhar sua lista de passageiros com seus clientes.
Antes de estudar a implementação, veja como a classe TwilightBus
funciona pela
perspectiva de um cliente daquela classe, em Exemplo 100.
TwilightBus
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] (1)
>>> bus = TwilightBus(basketball_team) (2)
>>> bus.drop('Tina') (3)
>>> bus.drop('Pat')
>>> basketball_team (4)
['Sue', 'Maya', 'Diana']
-
basketball_team
contém o nome de cinco estudantes. -
Um
TwilightBus
é carregado com o time. -
O
bus
deixa uma estudante, depois outra. -
As passageiras desembarcadas desapareceram do time de basquete!
TwilightBus
viola o "Princípio da Menor Surpresa Possível", uma boa prática do design de interfaces.[72] É certamente espantoso que quando o ônibus deixa uma estudante, seu nome seja removido da escalação do time de basquete.
Exemplo 101 é a implementação de TwilightBus
e uma explicação do problema.
class TwilightBus:
"""A bus model that makes passengers vanish"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = [] # (1)
else:
self.passengers = passengers #(2)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name) # (3)
-
Aqui nós cuidadosamente criamos uma lista vazia quando
passengers
éNone
. -
Entretanto, esta atribuição transforma
self.passengers
em um apelido parapassengers
, que por sua vez é um apelido para o argumento efetivamente passado para__init__
(i.e.basketball_team
em Exemplo 100). -
Quando os métodos
.remove()
e.append()
são usados comself.passengers
, estamos, na verdade, modificando a lista original recebida como argumento pelo construtor.
O problema aqui é que o ônibus está apelidando a lista passada para o construtor. Ao invés disso, ele deveria manter sua própria lista de passageiros. A solução é simples: em __init__
, quando o parâmetro passengers
é fornecido, self.passengers
deveria ser inicializado com uma cópia daquela lista, como fizemos, de forma correta, em Exemplo 94:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) (1)
-
Cria uma cópia da lista
passengers
, ou converte o argumento paralist
se ele não for uma lista.
Agora nossa manipulação interna da lista de passageiros não afetará o argumento usado para inicializar o ônibus. E com uma vantagem adicional, essa solução é mais flexível: agora o argumento passado no parâmetro passengers
pode ser uma tupla ou qualquer outro tipo iterável, como set
ou mesmo resultados de uma consulta a um banco de dados, pois o construtor de list
aceita qualquer iterável. Ao criar nossa própria lista, estamos também assegurando que ela suporta os métodos necessários, .remove()
e .append()
, operações que usamos nos métodos .pick()
e .drop()
.
👉 Dica
|
A menos que um método tenha o objetivo explícito de alterar um objeto recebido como argumento, você deveria pensar bem antes de apelidar tal objeto e simplesmente vinculá-lo a uma variável interna de sua classe. Quando em dúvida, crie uma cópia. Os clientes de sua classe ficarão mais felizes. Claro, criar uma cópia não é grátis: há um custo de processamento e uso de memória. Entretanto, uma API que causa bugs sutis é, em geral, um problema bem maior que uma que seja um pouco mais lenta ou que use mais recursos. |
Agora vamos conversar sobre um dos comandos mais incompreendidos em Python: del
.
6.6. del e coleta de lixo
Os objetos nunca são destruídos explicitamente; no entanto, quando eles se tornam inacessíveis, eles podem ser coletados como lixo.
A primeira estranheza sobre del
é ele não ser uma função, mas um comando.
Escrevemos del x
e não del(x)
— apesar dessa última forma funcionar também, mas apenas porque as expressões x
e (x)
em geral terem o mesmo significado em Python.
O segundo aspecto surpreendente é que del
apaga referências, não objetos. A coleta de lixo pode eliminar um objeto da memória como resultado indireto de del
, se a variável apagada for a última referência ao objeto. Reassociar uma variável também pode reduzir a zero o número de referências a um objeto, causando sua destruição.
>>> a = [1, 2] (1)
>>> b = a (2)
>>> del a (3)
>>> b (4)
[1, 2]
>>> b = [3] (5)
-
Cria o objeto
[1, 2]
e vinculaa
a ele. -
Vincula
b
ao mesmo objeto[1, 2]
. -
Apaga a referência
a
. -
[1, 2]
não é afetado, poisb
ainda aponta para ele. -
Reassociar
b
a um objeto diferente remove a última referência restante a[1, 2]
. Agora o coletor de lixo pode descartar aquele objeto.
⚠️ Aviso
|
Existe um método especial |
Em CPython, o algoritmo primário de coleta de lixo é a contagem de referências. Essencialmente, cada objeto mantém uma contagem do número de referências apontando para si. Assim que a contagem chega a zero, o objeto é imediatamente destruído: CPython invoca o método __del__
no objeto (se definido) e daí libera a memória alocada para aquele objeto. Em CPython 2.0, um algoritmo de coleta de lixo geracional foi acrescentado, para detectar grupos de objetos envolvidos em referências cíclicas — grupos que pode ser inacessíveis mesmo que existam referências restantes, quando todas as referências mútuas estão contidas dentro daquele grupo. Outras implementações de Python tem coletores de lixo mais sofisticados, que não se baseiam na contagem de referências, o que significa que o método __del__
pode não ser chamado imediatamente quando não existem mais referências ao objeto. Veja "PyPy, Garbage Collection, and a Deadlock" (EN) by A. Jesse Jiryu Davis para uma discussão sobre os usos próprios e impróprios de __del__
.
Para demonstrar o fim da vida de um objeto, Exemplo 102 usa weakref.finalize
para registrar uma função callback a ser chamada quando o objeto é destruído.
>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1 (1)
>>> def bye(): (2)
... print('...like tears in the rain.')
...
>>> ender = weakref.finalize(s1, bye) (3)
>>> ender.alive (4)
True
>>> del s1
>>> ender.alive (5)
True
>>> s2 = 'spam' (6)
...like tears in the rain.
>>> ender.alive
False
-
s1
es2
são apelidos do mesmo conjunto,{1, 2, 3}
. -
Essa função não deve ser um método associado ao objeto prestes a ser destruído, nem manter uma referência para o objeto.
-
Registra o callback
bye
no objeto referenciado pors1
. -
O atributo
.alive
éTrue
antes do objetofinalize
ser chamado. -
Como vimos,
del
não apaga o objeto, apenas a referências1
a ele. -
Reassociar a última referência,
s2
, torna{1, 2, 3}
inacessível. Ele é destruído, o callbackbye
é invocado, eender.alive
se tornaFalse
.
O ponto principal de Exemplo 102 é mostrar explicitamente que del
não apaga objetos, mas que objetos podem ser apagados como uma consequência de se tornarem inacessíveis após o uso de del
.
Você pode estar se perguntando porque o objeto {1, 2, 3}
foi destruído em Exemplo 102. Afinal, a referência s1
foi passada para a função finalize
, que precisa tê-la mantido para conseguir monitorar o objeto e invocar o callback. Isso funciona porque finalize
mantém uma referência fraca (weak reference) para {1, 2, 3}. Referências fracas não aumentam a contagem de referências de um objeto. Assim, uma referência fraca não evita que o objeto alvo seja destruído pelo coletor de lixo. Referências fracas são úteis em cenários de caching, pois não queremos que os objetos "cacheados" sejam mantidos vivos apenas por terem uma referência no cache.
✒️ Nota
|
Referências fracas são um tópico muito especializado, então decidi retirá-lo dessa segunda edição. Em vez disso, publiquei a nota "Weak References" em fluentpython.com. |
6.7. Peças que Python prega com imutáveis
✒️ Nota
|
Esta seção opcional discute alguns detalhes que, na verdade, não são muito importantes para usuários de Python, e que podem não se aplicar a outras implementações da linguagem ou mesmo a futuras versões de CPython. Entretanto, já vi muita gente tropeçar nesses casos laterais e daí passar a usar o operador |
Eu fiquei surpreso em descobrir que, para uma tupla t
, a chamada t[:]
não cria uma cópia, mas devolve uma referência para o mesmo objeto. Da mesma forma, tuple(t)
também retorna uma referência para a mesma tupla.[73]
Exemplo 103 demonstra esse fato.
>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1 (1)
True
>>> t3 = t1[:]
>>> t3 is t1 (2)
True
-
t1
et2
estão vinculadas ao mesmo objeto -
Assim como
t3
.
O mesmo comportamento pode ser observado com instâncias de str
, bytes
e frozenset
. Observe que frozenset
não é uma sequência, então fs[:]
não funciona se fs
é um frozenset
. Mas fs.copy()
tem o mesmo efeito: ele trapaceia e retorna uma referência ao mesmo objeto, e não uma cópia, como mostra Exemplo 104.[74]
>>> t1 = (1, 2, 3)
>>> t3 = (1, 2, 3) # (1)
>>> t3 is t1 # (2)
False
>>> s1 = 'ABC'
>>> s2 = 'ABC' # (3)
>>> s2 is s1 # (4)
True
-
Criando uma nova tupla do zero.
-
t1
et3
são iguais, mas não são o mesmo objeto. -
Criando uma segunda
str
do zero. -
Surpresa:
a
eb
se referem à mesmastr
!
O compartilhamento de strings literais é uma técnica de otimização chamada internalização (interning). O CPython usa uma técnica similar com inteiros pequenos, para evitar a duplicação desnecessária de números que aparecem com muita frequência em programas, como 0, 1, -1, etc. Observe que o CPython não internaliza todas as strings e inteiros, e o critério pelo qual ele faz isso é um detalhe de implementação não documentado.
⚠️ Aviso
|
Nunca dependa da internalização de |
Os truques discutidos nessa seção, incluindo o comportamento de frozenset.copy()
, são mentiras inofensivas que economizam memória e tornam o interpretador mais rápido. Não se preocupe, elas não trarão nenhum problema, pois se aplicam apenas a tipos imutáveis. Provavelmente, o melhor uso para esse tipo de detalhe é ganhar apostas contra outros Pythonistas.[75]
6.8. Resumo do capítulo
Todo objeto em Python tem uma identidade, um tipo e um valor. Apenas o valor do objeto pode mudar ao longo do tempo.[76]
Se duas variáveis se referem a objetos imutáveis de igual valor (a == b
is True
), na prática, dificilmente importa se elas se referem a cópias de mesmo valor ou são apelidos do mesmo objeto, porque o valor de objeto imutável não muda, com uma exceção. A exceção são as coleções imutáveis, como as tuplas: se uma coleção imutável contém referências para itens mutáveis, então seu valor pode de fato mudar quando o valor de um item mutável for modificado. Na prática, esse cenário não é tão comum. O que nunca muda numa coleção imutável são as identidades dos objetos mantidos ali. A classe frozenset
não sofre desse problema, porque ela só pode manter elementos hashable, e o valor de um objeto hashable não pode mudar nunca, por definição.
O fato de variáveis manterem referências tem muitas consequências práticas para a programação em Python:
-
Uma atribuição simples não cria cópias.
-
Uma atribuição composta com
+=
ou*=
cria novos objetos se a variável à esquerda da atribuição estiver vinculada a um objeto imutável, mas pode modificar um objeto mutável diretamente. -
Atribuir um novo valor a uma variável existente não muda o objeto previamente vinculado à variável. Isso se chama reassociar (rebinding); a variável está agora associada a um objeto diferente. Se aquela variável era a última referência ao objeto anterior, aquele objeto será eliminado pela coleta de lixo.
-
Parâmetros de função são passados como apelidos, o que significa que a função pode alterar qualquer objeto mutável recebido como argumento. Não há como evitar isso, exceto criando cópias locais ou usando objetos imutáveis (i.e., passando uma tupla em vez de uma lista)
-
Usar objetos mutáveis como valores default de parâmetros de função é perigoso, pois se os parâmetros forem modificados pela função, o default muda, afetando todas as chamadas posteriores que usem o default.
Em CPython, os objetos são descartados assim que o número de referências a eles chega a zero. Eles também podem ser descartados se formarem grupos com referências cíclicas sem nenhuma referência externa ao grupo.
Em algumas situações, pode ser útil manter uma referência para um objeto que não irá — por si só — manter o objeto vivo. Um exemplo é uma classe que queira manter o registro de todas as suas instâncias atuais. Isso pode ser feito com referências fracas, um mecanismo de baixo nível encontrado nas úteis coleções WeakValueDictionary
, WeakKeyDictionary
, WeakSet
, e na função finalize
do módulo weakref
.
Para saber mais, leia "Weak References" em fluentpython.com.
6.9. Para saber mais
O capítulo "Modelo de Dados" de A Referência da Linguagem Python inicia com uma explicação bastante clara sobre identidades e valores de objetos.
Wesley Chun, autor da série Core Python, apresentou Understanding Python’s Memory Model, Mutability, and Methods (EN) na EuroPython 2011, discutindo não apenas o tema desse capítulo como também o uso de métodos especiais.
Doug Hellmann escreveu os posts "copy – Duplicate Objects" (EN) e "weakref—Garbage-Collectable References to Objects" (EN), cobrindo alguns dos tópicos que acabamos de tratar.
Você pode encontrar mais informações sobre o coletor de lixo geracional do CPython em the gc — Interface para o coletor de lixo¶, que começa com a frase "Este módulo fornece uma interface para o opcional garbage collector". O adjetivo "opcional" usado aqui pode ser surpreendente, mas o capítulo "Modelo de Dados" também afirma:
Uma implementação tem permissão para adiar a coleta de lixo ou omiti-la completamente — é uma questão de detalhe de implementação como a coleta de lixo é implementada, desde que nenhum objeto que ainda esteja acessível seja coletado.
Pablo Galindo escreveu um texto mais aprofundado sobre o Coletor de Lixo em Python, em "Design of CPython’s Garbage Collector" (EN) no Python Developer’s Guide, voltado para contribuidores novos e experientes da implementação CPython.
O coletor de lixo do CPython 3.4 aperfeiçoou o tratamento de objetos contendo um método __del__
,
como descrito em PEP 442—Safe object finalization (EN).
A Wikipedia tem um artigo sobre string interning (EN), que menciona o uso desta técnica em várias linguagens, incluindo Python.
A Wikipedia também tem um artigo sobre "Haddocks' Eyes", a canção de Lewis Carroll que mencionei no início deste capítulo. Os editores da Wikipedia escreveram que a letra é usada em trabalhos de lógica e filosofia "para elaborar o status simbólico do conceito de 'nome': um nome como um marcador de identificação pode ser atribuído a qualquer coisa, incluindo outro nome, introduzindo assim níveis diferentes de simbolização."
Parte II: Funções como objetos
7. Funções como objetos de primeira classe
Nunca achei que Python tenha sido fortemente influenciado por linguagens funcionais, independente do que outros digam ou pensem. Eu estava muito mais familiarizado com linguagens imperativas, como o C e o Algol e, apesar de ter tornado as funções objetos de primeira classe, não via Python como uma linguagem funcional.[77][78]
BDFL de Python
No Python, funções são objetos de primeira classe. Estudiosos de linguagens de programação definem um "objeto de primeira classe" como uma entidade programática que pode ser:
-
Criada durante a execução de um programa
-
Atribuída a uma variável ou a um elemento em uma estrutura de dados
-
Passada como argumento para uma função
-
Devolvida como o resultado de uma função
Inteiros, strings e dicionários são outros exemplos de objetos de primeira classe no Python—nada de incomum aqui. Tratar funções como objetos de primeira classe é um recurso essencial das linguagens funcionais, tais como Clojure, Elixir e Haskell. Entretanto, funções de primeira classe são tão úteis que foram adotadas por linguagens muito populares, como o Javascript, o Go e o Java (desde o JDK 8), nenhuma das quais alega ser uma "linguagem funcional".
Esse capítulo e quase toda a Parte III do livro exploram as aplicações práticas de se tratar funções como objetos.
👉 Dica
|
O termo "funções de primeira classe" é largamente usado como uma forma abreviada de "funções como objetos de primeira classe". Ele não é ideal, por sugerir a existência de uma "elite" entre funções. No Python, todas as funções são de primeira classe. |
7.1. Novidades nesse capítulo
A seção "Os nove sabores de objetos invocáveis" (Seção 7.5) se chamava "Sete sabores de objetos invocáveis" na primeira edição deste livro. Os novos invocáveis são corrotinas nativas e geradores assíncronos, introduzidos no Python 3.5 e 3.6, respectivamente. Ambos serão estudados no Capítulo 21, mas são mencionados aqui ao lado dos outros invocáveis.
A Seção 7.7.1 é nova, e fala de um recurso que surgiu no Python 3.8.
Transferi a discussão sobre acesso a anotações de funções durante a execução para a Seção 15.5. Quando escrevi a primeira edição, a PEP 484—Type Hints (Dicas de Tipo) (EN) ainda estava sendo considerada, e as anotações eram usadas de várias formas diferentes. Desde Python 3.5, anotações precisam estar em conformidade com a PEP 484. Assim, o melhor lugar para falar delas é durante a discussão das dicas de tipo.
✒️ Nota
|
A primeira edição desse livro continha seções sobre a introspecção de objetos função, que desciam a detalhes de baixo nível e distraiam o leitor do assunto principal do capítulo. Fundi aquelas seções em um post entitulado "Introspection of Function Parameters" (Introspecção de Parâmetros de Funções), no fluentpython.com. |
Agora vamos ver porque as funções de Python são objetos completos.
7.2. Tratando uma função como um objeto
A sessão de console no Exemplo 105 mostra que funções de Python são objetos. Ali criamos uma função, a chamamos, lemos seu atributo
__doc__
e verificamos que o próprio objeto função é uma instância da classe function
.
__doc__
e verifica seu tipo>>> def factorial(n): (1)
... """returns n!"""
... return 1 if n < 2 else n * factorial(n - 1)
...
>>> factorial(42)
1405006117752879898543142606244511569936384000000000
>>> factorial.__doc__ (2)
'returns n!'
>>> type(factorial) (3)
<class 'function'>
-
Isso é uma sessão do console, então estamos criando uma função "durante a execução".
-
__doc__
é um dos muitos atributos de objetos função. -
factorial
é um instância da classefunction
.
O atributo __doc__
é usado para gerar o texto de ajuda de um objeto. No console de Python, o comando help(factorial)
mostrará uma tela como a da Figura 22.
factorial
; o texto é criado a partir do atributo __doc__
da função.O Exemplo 106 mostra a natureza de "primeira classe" de um objeto função.
Podemos atribuir tal objeto a uma variável fact
e invocá-lo por esse nome.
Podemos também passar factorial
como argumento para a função map
.
Invocar map(function, iterable)
devolve um iterável no qual cada item é o resultado de uma chamada ao primeiro argumento (uma função) com elementos sucessivos do segundo argumento (um iterável), range(11)
no exemplo.
factorial
usando de um nome diferentes, e passa factorial
como um argumento>>> fact = factorial
>>> fact
<function factorial at 0x...>
>>> fact(5)
120
>>> map(factorial, range(11))
<map object at 0x...>
>>> list(map(factorial, range(11)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
Ter funções de primeira classe permite programar em um estilo funcional. Um dos marcos da programação funcional é o uso de funções de ordem superior, nosso próximo tópico.
7.3. Funções de ordem superior
Uma função que recebe uma função como argumento ou devolve uma função como resultado
é uma função de ordem superior.
Uma dessas funções é map
, usada no Exemplo 106. Outra é a função embutida sorted
:
o argumento opcional key
permite fornecer uma função, que será então aplicada na ordenação de cada item, como vimos na Seção 2.9.
Por exemplo, para ordenar uma lista de palavras por tamanho, passe a função len
como key
, como no Exemplo 107.
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
>>>
Qualquer função com um argumento pode ser usada como chave. Por exemplo, para criar um dicionário de rimas pode ser útil ordenar cada palavra escrita ao contrário. No Exemplo 108, observe que as palavras na lista não são modificadas de forma alguma; apenas suas versões escritas na ordem inversa são utilizadas como critério de ordenação. Por isso as berries aparecem juntas.
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
No paradigma funcional de programação, algumas das funções de ordem superior mais conhecidas são map
, filter
, reduce
, e apply
.
A função apply
foi descontinuada no Python 2.3 e removida no Python 3, por não ser mais necessária. Se você precisar chamar uma função com um conjuntos dinâmico de argumentos, pode escrever fn(*args, **kwargs)
no lugar de apply(fn, args, kwargs)
.
As funções de ordem superior map
, filter
, e reduce
ainda estão por aí, mas temos alternativas melhores para a maioria de seus casos de uso, como mostra a próxima seção.
7.3.1. Substitutos modernos para map, filter, e reduce
Linguagens funcionais normalmente oferecem as funções de ordem superior map
, filter
, and reduce
(algumas vezes com nomes diferentes).
As funções map
e filter
ainda estão embutidas no Python mas, desde a introdução das compreensões de lista e das expressões geradoras, não são mais tão importantes.
Uma listcomp ou uma genexp fazem o mesmo que map
e filter
combinadas, e são mais legíveis.
Considere o Exemplo 109.
map
e filter
, comparadas com alternativas escritas com compreensões de lista>>> list(map(factorial, range(6))) (1)
[1, 1, 2, 6, 24, 120]
>>> [factorial(n) for n in range(6)] (2)
[1, 1, 2, 6, 24, 120]
>>> list(map(factorial, filter(lambda n: n % 2, range(6)))) (3)
[1, 6, 120]
>>> [factorial(n) for n in range(6) if n % 2] (4)
[1, 6, 120]
>>>
-
Cria uma lista de fatoriais de 0! a 5!.
-
Mesma operação, com uma compreensão de lista.
-
Lista de fatoriais de números ímpares até 5!, usando
map
efilter
. -
A compreensão de lista realiza a mesma tarefa, substituindo
map
efilter
, e tornandolambda
desnecessário.
No Python 3, map
e filter
devolvem geradores—uma forma de iterador—então sua substituta direta é agora uma expressão geradora (no Python 2, essas funções devolviam listas, então sua alternativa mais próxima era a compreensão de lista).
A função reduce
foi rebaixada de função embutida, no Python 2, para o módulo functools
no Python 3. Seu caso de uso mais comum, a soma, é melhor servido pela função embutida sum
, disponível desde que Python 2.3 (lançado em 2003). E isso é uma enorme vitória em termos de legibilidade e desempenho (veja Exemplo 110 abaixo).
reduce
e sum
>>> from functools import reduce (1)
>>> from operator import add (2)
>>> reduce(add, range(100)) (3)
4950
>>> sum(range(100)) (4)
4950
>>>
-
A partir de Python 3.0,
reduce
deixou de ser uma função embutida. -
Importa
add
para evitar a criação de uma função apenas para somar dois números. -
Soma os inteiros até 99.
-
Mesma operação, com
sum
—não é preciso importar nem chamarreduce
eadd
.
✒️ Nota
|
A ideia comum de |
Outras funções de redução embutidas são all
e any
:
all(iterable)
-
Devolve
True
se não há nenhum elemento falso no iterável;all([])
devolveTrue
. any(iterable)
-
Devolve
True
se qualquer elemento doiterable
for verdadeiro;any([])
devolveFalse
.
Dou um explicação mais completa sobre reduce
na Seção 12.7,
onde um exemplo mais longo, atravessando várias seções, cria um contexto
significativo para o uso dessa função.
As funções de redução serão resumidas mais à frente no livro, na Seção 17.10,
quando estivermos tratando dos iteráveis.
Para usar uma função de ordem superior, às vezes é conveniente criar um pequena
função, que será usada apenas uma vez. As funções anônimas existem para isso.
Vamos falar delas a seguir.
7.4. Funções anônimas
A palavra reservada lambda
cria uma função anônima dentro de uma expressão Python.
Entretanto, a sintaxe simples de Python força os corpos de funções lambda
a serem expressões puras. Em outras palavras, o corpo não pode conter outras instruções Python como while
, try
, etc. A atribuição com =
também é uma instrução, então não pode ocorrer em um lambda
.
A nova sintaxe da expressão de atribuição, usando :=
, pode ser usada. Porém, se você precisar dela, seu lambda
provavelmente é muito complicado e difícil de ler, e deveria ser refatorado para um função regular usando def
.
O melhor uso das funções anônimas é no contexto de uma lista de argumentos para uma função de ordem superior.
Por exemplo, o Exemplo 111 é o exemplo do dicionário de rimas do Exemplo 108 reescrito com lambda
, sem definir uma função reverse
.
lambda
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
Fora do contexto limitado dos argumentos das funções de ordem superior, funções anônimas raramente são úteis no Python.
As restrições sintáticas tendem a tornar ilegíveis ou intratáveis as lambdas
não-triviais. Se uma lambda
é difícil de ler, aconselho fortemente seguir o conselho de Fredrik Lundh sobre refatoração.
A sintaxe lambda
é apenas açúcar sintático: uma expressão lambda
cria um objeto função, exatamente como a declaração def
.
Esse é apenas um dos vários tipos de objetos invocáveis no Python. Na próxima seção revisamos todos eles.
7.5. Os nove sabores de objetos invocáveis
O operador de invocação ()
pode ser aplicado a outros objetos além de funções.
Para determinar se um objeto é invocável, use a função embutida callable()
.
No Python 3.9, a documentação do modelo de dados lista nove tipos invocáveis:
- Funções definidas pelo usuário
-
Criadas com comandos
def
ou expressõeslambda
. - Funções embutidas
-
Uma funções implementadas em C (no CPython), como
len
outime.strftime
. - Métodos embutidos
-
Métodos implementados em C, como
dict.get
. - Métodos
-
Funções definidas no corpo de uma classe.
- Classes
-
Quando invocada, uma classe executa seu método
__new__
para criar uma instância, e a seguir__init__
, para inicializá-la. Então a instância é devolvida ao usuário. Como não existe um operadornew
no Python, invocar uma classe é como invocar uma função.[79] - Instâncias de classe
-
Se uma classe define um método
__call__
, suas instâncias podem então ser invocadas como funções—esse é o assunto da próxima seção. - Funções geradoras
-
Funções ou métodos que usam a palavra reservada
yield
em seu corpo. Quando chamadas, devolvem um objeto gerador. - Funções de corrotinas nativas
-
Funções ou métodos definidos com
async def
. Quando chamados, devolvem um objeto corrotina. Introduzidas no Python 3.5. - Funções geradoras assíncronas
-
Funções ou métodos definidos com
async def
, contendoyield
em seu corpo. Quando chamados, devolvem um gerador assíncrono para ser usado comasync for
. Introduzidas no Python 3.6.
Funções geradoras, funções de corrotinas nativas e geradoras assíncronas são diferentes de outros invocáveis: os valores devolvidos tais funções nunca são dados da aplicação, mas objetos que exigem processamento adicional, seja para produzir dados da aplicação, seja para realizar algum trabalho útil. Funções geradoras devolvem iteradores. Ambos são tratados no Capítulo 17. Funções de corrotinas nativas e funções geradoras assíncronas devolvem objetos que só funcionam com a ajuda de um framework de programação assíncrona, tal como asyncio. Elas são o assunto do Capítulo 21.
👉 Dica
|
Dada a variedade dos tipos de invocáveis existentes no Python, a forma mais segura de determinar se um objeto é invocável é usando a função embutida >>> abs, str, 'Ni!' (<built-in function abs>, <class 'str'>, 'Ni!') >>> [callable(obj) for obj in (abs, str, 'Ni!')] [True, True, False] |
Vamos agora criar instâncias de classes que funcionam como objetos invocáveis.
7.6. Tipos invocáveis definidos pelo usuário
Não só as funções Python são objetos reais, também é possível fazer com que objetos Python arbitrários se comportem como funções. Para isso basta implementar o método de instância __call__
.
O Exemplo 112 implementa uma classe BingoCage
. Uma instância é criada a partir de qualquer iterável, e mantém uma list
interna de itens, em ordem aleatória. Invocar a instância extrai um item.[80]
BingoCage
faz apenas uma coisa: escolhe itens de uma lista embaralhadaimport random
class BingoCage:
def __init__(self, items):
self._items = list(items) # (1)
random.shuffle(self._items) # (2)
def pick(self): # (3)
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage') # (4)
def __call__(self): # (5)
return self.pick()
-
__init__
aceita qualquer iterável; criar uma cópia local evita efeitos colaterais inesperados sobre qualquerlist
passada como argumento. -
shuffle
sempre vai funcionar, poisself._items
é umalist
. -
O método principal.
-
Se
self._items
está vazia, gera uma exceção com uma mensagem apropriada. -
Atalho para
bingo.pick()
:bingo()
.
Aqui está uma demonstração simples do Exemplo 112. Observe como uma instância de bingo
pode ser invocada como uma função, e como a função embutida callable()
a reconhece como um objeto invocável:
>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True
Uma classe que implemente __call__
é uma forma fácil de criar objetos similares a funções, com algum estado interno que precisa ser mantido de uma invocação para outra, como os itens restantes na BingoCage
.
Outro bom caso de uso para __call__
é a implementação de decoradores. Decoradores devem ser invocáveis, e muitas vezes é conveniente "lembrar" algo entre chamadas ao decorador (por exemplo, para memoization—a manutenção dos resultados de algum processamento complexo e/ou demorado para uso posterior) ou para separar uma implementação complexa por diferentes métodos.
A abordagem funcional para a criação de funções com estado interno é através do uso de clausuras (closures). Clausuras e decoradores são o assunto do Capítulo 9.
Vamos agora explorar a poderosa sintaxe oferecida pelo Python para declarar parâmetros de funções, e para passar argumentos para elas.
7.7. De parâmetros posicionais a parâmetros somente nomeados
Um dos melhores recursos das funções Python é seu mecanismo extremamente flexível de tratamento de parâmetros. Intimamente relacionados a isso são os usos de *
e **
para desempacotar iteráveis e mapeamentos em argumentos separados quando chamamos uma função.
Para ver esses recursos em ação, observe o código do Exemplo 113 e os testes mostrando seu uso no Exemplo 114.
tag
gera elementos HTML; um argumento somente nomeado class_
é usado para passar atributos "class"; o _
é necessário porque class
é uma palavra reservada no Pythondef tag(name, *content, class_=None, **attrs):
"""Generate one or more HTML tags"""
if class_ is not None:
attrs['class'] = class_
attr_pairs = (f' {attr}="{value}"' for attr, value
in sorted(attrs.items()))
attr_str = ''.join(attr_pairs)
if content:
elements = (f'<{name}{attr_str}>{c}</{name}>'
for c in content)
return '\n'.join(elements)
else:
return f'<{name}{attr_str} />'
A função tag
pode ser invocada de muitas formas, como demonstra o Exemplo 114.
tag
do Exemplo 113>>> tag('br') # (1)
'<br />'
>>> tag('p', 'hello') # (2)
'<p>hello</p>'
>>> print(tag('p', 'hello', 'world'))
<p>hello</p>
<p>world</p>
>>> tag('p', 'hello', id=33) # (3)
'<p id="33">hello</p>'
>>> print(tag('p', 'hello', 'world', class_='sidebar')) # (4)
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
>>> tag(content='testing', name="img") # (5)
'<img content="testing" />'
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'class': 'framed'}
>>> tag(**my_tag) # (6)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
-
Um argumento posicional único produz uma
tag
vazia com aquele nome. -
Quaisquer argumentos após o primeiro serão capturados por
*content
na forma de umatuple
. -
Argumentos nomeados que não são mencionados explicitamente na assinatura de
tag
são capturados por**attrs
como umdict
. -
O parâmetro
class_
só pode ser passado como um argumento nomeado. -
O primeiro argumento posicional também pode ser passado como argumento nomeado.
-
Prefixar o
dict
my_tag
com**
passa todos os seus itens como argumentos separados, que são então vinculados aos parâmetros nomeados, com o restante sendo capturado por**attrs
. Nesse caso podemos ter um nome'class'
nodict
de argumentos, porque ele é uma string, e não colide com a palavra reservadaclass
.
Argumentos somente nomeados são um recurso de Python 3. No Exemplo 113, o parâmetro class_
só pode ser passado como um argumento nomeado—ele nunca captura argumentos posicionais não-nomeados. Para especificar argumentos somente nomeados ao definir uma função, eles devem ser nomeados após o argumento prefixado por *
. Se você não quer incluir argumentos posicionais variáveis, mas ainda assim deseja incluir argumentos somente nomeados, coloque um *
sozinho na assinatura, assim:
>>> def f(a, *, b):
... return a, b
...
>>> f(1, b=2)
(1, 2)
>>> f(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given
Observe que argumentos somente nomeados não precisam ter um valor default: eles podem ser obrigatórios, como o b
no exemplo acima.
7.7.1. Parâmetros somente posicionais
Desde Python 3.8, assinaturas de funções definidas pelo usuário podem especificar parâmetros somente posicionais. Esse recurso sempre existiu para funções embutidas, tal como divmod(a, b)
,
que só pode ser chamada com parâmetros posicionais, e não na forma divmod(a=10, b=4)
.
Para definir uma função que requer parâmetros somente posicionais, use /
na lista de parâmetros.
Esse exemplo, de "O que há de novo no Python 3.8", mostra como emular a função embutida divmod
:
def divmod(a, b, /):
return (a // b, a % b)
Todos os argumentos à esquerda da /
são somente posicionais. Após a /
, você pode especificar outros argumentos, que funcionam como da forma usual.
⚠️ Aviso
|
Uma |
Por exemplo, considere a função tag
do Exemplo 113.
Se quisermos que o parâmetro name
seja somente posicional, podemos acrescentar uma /
após aquele parâmetro na assinatura da função, assim:
def tag(name, /, *content, class_=None, **attrs):
...
Você pode encontrar outros exemplos de parâmetros somente posicionais no já citado "O que há de novo no Python 3.8" e na PEP 570.
Após esse mergulho nos recursos flexíveis de declaração de argumentos no Python, o resto desse capítulo trata dos pacotes da biblioteca padrão mais úteis para programar em um estilo funcional.
7.8. Pacotes para programação funcional
Apesar de Guido deixar claro que não projetou Python para ser uma linguagem de programação funcional, o estilo de programação funcional pode ser amplamente utilizado, graças a funções de primeira classe, pattern matching e o suporte de pacotes como operator
e functools
, dos quais falaremos nas próximas duas seções..
7.8.1. O módulo operator
Na programação funcional, é muitas vezes conveniente usar um operador aritmético como uma função. Por exemplo, suponha que você queira multiplicar uma sequência de números para calcular fatoriais, mas sem usar recursão. Para calcular a soma, podemos usar sum
, mas não há uma função equivalente para multiplicação. Você poderia usar reduce—como vimos na Seção 7.3.1—mas isso exige um função para multiplicar dois itens da sequência. O Exemplo 115 mostra como resolver esse problema usando lambda
.
from functools import reduce
def factorial(n):
return reduce(lambda a, b: a*b, range(1, n+1))
O módulo operator
oferece funções equivalentes a dezenas de operadores, para você não precisar escrever funções triviais como lambda a, b: a*b
.
Com ele, podemos reescrever o Exemplo 115 como o Exemplo 116.
reduce
e operator.mul
from functools import reduce
from operator import mul
def factorial(n):
return reduce(mul, range(1, n+1))
Outro grupo de "lambdas de um só truque" que operator
substitui são funções para extrair itens de sequências ou para ler atributos de objetos:
itemgetter
e attrgetter
são fábricas que criam funções personalizadas para fazer exatamente isso.
O Exemplo 117 mostra um uso frequente de itemgetter
: ordenar uma lista de tuplas pelo valor de um campo.
No exemplo, as cidades são exibidas por ordem de código de país (campo 1).
Essencialmente, itemgetter(1)
cria uma função que, dada uma coleção, devolve o item no índice 1.
Isso é mais fácil de escrever e ler que lambda fields: fields[1]
, que faz a mesma coisa.
itemgetter
para ordenar uma lista de tuplas (mesmos dados do Exemplo 10)>>> metro_data = [
... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
... ('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
... ]
>>>
>>> from operator import itemgetter
>>> for city in sorted(metro_data, key=itemgetter(1)):
... print(city)
...
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
Se você passar múltiplos argumentos de indice para itemgetter
, a função criada por ela vai devolver tuplas com os valores extraídos, algo que pode ser útil para ordenar usando chaves múltiplas:
>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
... print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'São Paulo')
>>>
Como itemgetter
usa o operador []
, ela suporta não apenas sequências, mas também mapeamentos e qualquer classe que implemente
__getitem__
.
Uma irmã de itemgetter
é attrgetter
, que cria funções para extrair atributos por nome. Se você passar os nomes de vários atributos como argumentos para attrgetter
, ela vai devolver um tupla de valores. Além disso, se o nome de qualquer argumento contiver um .
(ponto), attrgetter
navegará por objetos aninhados para encontrar o atributo. Esses comportamento são apresentados no Exemplo 118. Não é exatamente uma sessão de console curta, pois precisamos criar uma estrutura aninhada para demonstrar o tratamento de atributos com .
por attrgetter
.
attrgetter
para processar uma lista previamente definida de namedtuple
chamada metro_data
(a mesma lista que aparece no Exemplo 117)>>> from collections import namedtuple
>>> LatLon = namedtuple('LatLon', 'lat lon') # (1)
>>> Metropolis = namedtuple('Metropolis', 'name cc pop coord') # (2)
>>> metro_areas = [Metropolis(name, cc, pop, LatLon(lat, lon)) # (3)
... for name, cc, pop, (lat, lon) in metro_data]
>>> metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLon(lat=35.689722,
lon=139.691667))
>>> metro_areas[0].coord.lat # (4)
35.689722
>>> from operator import attrgetter
>>> name_lat = attrgetter('name', 'coord.lat') # (5)
>>>
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')): # (6)
... print(name_lat(city)) # (7)
...
('São Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
-
Usa
namedtuple
para definirLatLon
. -
Também define
Metropolis
. -
Cria a lista
metro_areas
com instâncias deMetropolis
; observe o desempacotamento da tupla aninhada para extrair(lat, lon)
e usá-los para criar oLatLon
do atributocoord
deMetropolis
. -
Obtém a latitude de dentro de
metro_areas[0]
. -
Define um
attrgetter
para obtername
e o atributo aninhadocoord.lat
. -
Usa
attrgetter
novamente para ordenar uma lista de cidades pela latitude. -
Usa o attrgetter definido em
para exibir apenas o nome e a latitude da cidade.
Abaixo está uma lista parcial das funções definidas em operator
(nomes iniciando com _
foram omitidos por serem, em sua maioria, detalhes de implementação):
>>> [name for name in dir(operator) if not name.startswith('_')]
['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains',
'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt',
'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imatmul',
'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior',
'ipow', 'irshift', 'is_', 'is_not', 'isub', 'itemgetter',
'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul',
'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos',
'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor']
A maior parte dos 54 nomes listados é auto-evidente. O grupo de nomes formados por um i
inicial e o nome de outro operador—por exemplo iadd
, iand
, etc—correspondem aos operadores de atribuição aumentada—por exemplo, +=
, &=
, etc. Essas funções mudam seu primeiro argumento no mesmo lugar, se o argumento for mutável; se não, funcionam como seus pares sem o prefixo i
: simplemente devolvem o resultado da operação.
Das funções restantes de operator
, methodcaller
será a última que veremos. Ela é algo similar a attrgetter
e itemgetter
, no sentido de criarem uma função durante a execução. A função criada invoca por nome um método do objeto passado como argumento, como mostra o Exemplo 119.
methodcaller
: o segundo teste mostra a vinculação de argumentos adicionais>>> from operator import methodcaller
>>> s = 'The time has come'
>>> upcase = methodcaller('upper')
>>> upcase(s)
'THE TIME HAS COME'
>>> hyphenate = methodcaller('replace', ' ', '-')
>>> hyphenate(s)
'The-time-has-come'
O primeiro teste no Exemplo 119 está ali apenas para mostrar o funcionamento de methodcaller
; se você precisa usar str.upper
como uma função, basta chamá-lo na classe str
, passando uma string como argumento, assim:
>>> str.upper(s)
'THE TIME HAS COME'
O segundo teste do Exemplo 119 mostra que methodcaller
pode também executar uma aplicação parcial para fixar alguns argumentos, como faz a função functools.partial
. Esse é nosso próximo tópico.
7.8.2. Fixando argumentos com functools.partial
O módulo functools
oferece várias funções de ordem superior. Já vimos reduce
na Seção 7.3.1.
Uma outra é partial
: dado um invocável, ela produz um novo invocável com alguns dos argumentos do invocável original vinculados a valores pré-determinados.
Isso é útil para adaptar uma função que recebe um ou mais argumentos a uma API que requer uma função de callback com menos argumentos.
O Exemplo 120 é uma demonstração trivial.
partial
para usar uma função com dois argumentos onde é necessário um invocável com apenas um argumento>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) (1)
>>> triple(7) (2)
21
>>> list(map(triple, range(1, 10))) (3)
[3, 6, 9, 12, 15, 18, 21, 24, 27]
-
Cria uma nova função
triple
a partir demul
, vinculando o primeiro argumento posicional a3
. -
Testa a função.
-
Usa
triple
commap
;mul
não funcionaria commap
nesse exemplo.
Um exemplo mais útil envolve a função unicode.normalize
, que vimos na Seção 4.7. Se você trabalha com texto em muitas línguas diferentes, pode querer aplicar unicode.normalize('NFC', s)
a qualquer string s
, antes de compará-la ou armazená-la. Se você precisa disso com frequência, é conveninete ter uma função nfc
para executar essa tarefa, como no Exemplo 121.
partial
>>> import unicodedata, functools
>>> nfc = functools.partial(unicodedata.normalize, 'NFC')
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> s1 == s2
False
>>> nfc(s1) == nfc(s2)
True
partial
recebe um invocável como primeiro argumento, seguido de um número arbitrário de argumentos posicionais e nomeados para vincular.
O Exemplo 122 mostra o uso de partial
com a função tag
(do Exemplo 113), para fixar um argumento posicional e um argumento nomeado.
>>> from tagger import tag
>>> tag
<function tag at 0x10206d1e0> (1)
>>> from functools import partial
>>> picture = partial(tag, 'img', class_='pic-frame') (2)
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />' (3)
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', class_='pic-frame') (4)
>>> picture.func (5)
<function tag at 0x10206d1e0>
>>> picture.args
('img',)
>>> picture.keywords
{'class_': 'pic-frame'}
-
Importa
tag
do Exemplo 113 e mostra seu ID. -
Cria a função
picture
a partir detag
, fixando o primeiro argumento posicional em'img'
e o argumento nomeadoclass_
em'pic-frame'
. -
picture
funciona como esperado. -
partial()
devolve um objetofunctools.partial
.[81] -
Um objeto
functools.partial
tem atributos que fornecem acesso à função original e aos argumentos fixados.
A função functools.partialmethod
faz o mesmo que partial
, mas foi projetada para trabalhar com métodos.
O módulo functools
também inclui funções de ordem superior para serem usadas como decoradores de função, tais como cache
e singledispatch
, entre outras.
Essas funções são tratadas no Capítulo 9, que também explica como implementar decoradores personalizados.
7.9. Resumo do capítulo
O objetivo deste capítulo foi explorar a natureza das funções como objetos de primeira classe no Python. As principais consequências disso são a possibilidade de atribuir funções a variáveis, passá-las para outras funções, armazená-las em estruturas de dados e acessar os atributos de funções, permitindo que frameworks e ferramentas usem essas informações.
Funções de ordem superior, parte importante da programação funcional, são comuns no Python. As funções embutidas sorted
, min
e max
, além de functools.partial
, são exemplos de funções de ordem superior muito usadas na linguagem.
O uso de map
, filter
e reduce
já não é tão frequente como costumava ser, graças às compreensões de lista (e estruturas similares, como as expressões geradoras) e à adição de funções embutidas de redução como sum
, all
e any
.
Desde Python 3.6, existem nove sabores de invocáveis, de funções simples criadas com lambda
a instâncias de classes que implementam __call__
.
Geradoras e corrotinas também são invocáveis, mas seu comportamento é muito diferente daquele de outros invocáveis.
Todos os invocáveis podem ser detectados pela função embutida callable()
. Invocáveis oferecem uma rica sintaxe para declaração de parâmetros formais, incluindo parâmetros nomeados, parâmetros somente posicionais e anotações.
Por fim, vimos algumas funções do módulo operator
e functools.partial
, que facilitam a programação funcional, minimizando a necessidade de uso da sintaxe funcionalmente inepta de lambda
.
7.10. Leitura complementar
Nos próximos capítulos, continuaremos nossa jornada pela programação com objetos função. O Capítulo 8 é dedicado às dicas de tipo nos parâmetros de função e nos valores devolvidos por elas. O Capítulo 9 mergulha nos decoradores de função—um tipo especial de função de ordem superior—e no mecanismo de clausura (closure) que os faz funcionar. O Capítulo 10 mostra como as funções de primeira classe podem simplificar alguns padrões clássicos de projetos (design patterns) orientados a objetos.
Em A Referência da Linguagem Python, a seção "3.2. A hierarquia de tipos padrão" mostra os noves tipos invocáveis, juntamente com todos os outros tipos embutidos.
O capítulo 7 do Python Cookbook (EN), 3ª ed. (O’Reilly), de David Beazley e Brian K. Jones, é um excelente complemento a esse capítulo, bem como ao Capítulo 9, tratando basicamente dos mesmos conceitos, mas com uma abordagem diferente.
Veja a PEP 3102—Keyword-Only Arguments (Argumentos somente nomeados) (EN) se você estiver interessada na justificativa e nos casos desse recurso.
Uma ótima introdução à programação funcional em Python é o "Programação Funcional COMO FAZER", de A. M. Kuchling. O principal foco daquele texto, entretanto, é o uso de iteradores e geradoras, assunto do Capítulo 17.
A questão no StackOverflow, "Python: Why is functools.partial necessary?" (Python: Por que functools.partial é necessária?) (EN), tem uma resposta muito informativa (e engraçada) escrita por Alex Martelli, co-autor do clássico Python in a Nutshell (O’Reilly).
Refletindo sobre a pergunta "Seria Python uma linguagem funcional?", criei uma de minhas palestras favoritas, "Beyond Paradigms" ("Para Além dos Paradigmas"), que apresentei na PyCaribbean, na PyBay e na PyConDE. Veja os slides (EN) e o vídeo (EN) da apresentação em Berlim—onde conheci Miroslav Šedivý e Jürgen Gmach, dois dos revisores técnicos desse livro.
8. Dicas de tipo em funções
É preciso enfatizar que Python continuará sendo uma linguagem de tipagem dinâmica, e os autores não tem qualquer intenção de algum dia tornar dicas de tipo obrigatórias, mesmo que por mera convenção.
Guido van Rossum, Jukka Lehtosalo, e Łukasz Langa, PEP 484—Type Hints PEP 484—Type Hints (EN), "Rationale and Goals"; negritos mantidos do original.
Dicas de tipo foram a maior mudança na história de Python desde a unificação de tipos e classes no Python 2.2, lançado em 2001. Entretanto, as dicas de tipo não beneficiam igualmente a todos as pessoas que usam Python. Por isso deverão ser sempre opcionais.
A PEP 484—Type Hints introduziu a sintaxe e a semântica para declarações explícitas de tipo em argumentos de funções, valores de retorno e variáveis. O objetivo é ajudar ferramentas de desenvolvimento a encontrarem bugs nas bases de código em Python através de análise estática, isto é, sem precisar efetivamente executar o código através de testes.
Os maiores beneficiários são engenheiros de software profissionais que usam IDEs (Ambientes de Desenvolvimento Integrados) e CI (Integração Contínua). A análise de custo-benefício que torna as dicas de tipo atrativas para esse grupo não se aplica a todos os usuários de Python.
A base de usuários de Python vai muito além dessa classe de profissionais. Ela inclui cientistas, comerciantes, jornalistas, artistas, inventores, analistas e estudantes de inúmeras áreas — entre outros. Para a maioria deles, o custo de aprender dicas de tipo será certamente maior — a menos que já conheçam uma outra linguagem com tipos estáticos, subtipos e tipos genéricos. Os benefícios serão menores para muitos desses usuários, dada a forma como que eles interagem com Python, o tamanho menor de suas bases de código e de suas equipes — muitas vezes "equipes de um".
A tipagem dinâmica, default de Python, é mais simples e mais expressiva quando estamos escrevendo programas para explorar dados e ideias, como é o caso em ciência de dados, computação criativa e para aprender.
Este capítulo se concentra nas dicas de tipo de Python nas assinaturas de função.
Capítulo 15 explora as dicas de tipo no contexto de classes e outros recursos do módulo typing
.
Os tópicos mais importantes aqui são:
-
Uma introdução prática à tipagem gradual com Mypy
-
As perspectivas complementares da duck typing (tipagem pato) e da tipagem nominal
-
A revisão para principais categorias de tipos que podem surgir em anotações — isso representa cerca de 60% do capítulo
-
Os parâmetros variádicos das dicas de tipo (
*args
,**kwargs
) -
As limitações e desvantagens das dicas de tipo e da tipagem estática.
8.1. Novidades nesse capítulo
Este capítulo é completamente novo. As dicas de tipo apareceram no Python 3.5, após eu ter terminado de escrever a primeira edição de Python Fluente.
Dadas as limitações de um sistema de tipagem estática, a melhor ideia da PEP 484 foi propor um sistema de tipagem gradual. Vamos começar definindo o que isso significa.
8.2. Sobre tipagem gradual
A PEP 484 introduziu no Python um sistema de tipagem gradual. Outras linguagens com sistemas de tipagem gradual são o Typescript da Microsoft, Dart (a linguagem do SDK Flutter, criado pelo Google), e o Hack (um dialeto de PHP criado para uso na máquina virtual HHVM do Facebook). O próprio verificador de tipo MyPy começou como uma linguagem: um dialeto de Python de tipagem gradual com seu próprio interpretador. Guido van Rossum convenceu o criador do MyPy, Jukka Lehtosalo, a transformá-lo em uma ferramenta para checar código Python anotado.
Eis uma função com anotações de tipos:
def tokenize(s: str) -> list[str]:
"Convert a string into a list of tokens."
return s.replace('(', ' ( ').replace(')', ' ) ').split()
A assinatura informa que a função tokenize
recebe uma str
e devolve list[str]
: uma lista de strings.
A utilidade dessa função será explicada no Exemplo 349.
Um sistema de tipagem gradual:
- É opcional
-
Por default, o verificador de tipo não deve emitir avisos para código que não tenha dicas de tipo. Em vez disso, o verificador supõe o tipo
Any
quando não consegue determinar o tipo de um objeto. O tipoAny
é considerado compatível com todos os outros tipos. - Não captura erros de tipagem durante a execução do código
-
Dicas de tipo são usadas por verificadores de tipo, analisadores de código-fonte (linters) e IDEs para emitir avisos. Eles não evitam que valores inconsistentes sejam passados para funções ou atribuídos a variáveis durante a execução. Por exemplo, nada impede que alguém chame
tokenie(42)
, apesar da anotação de tipo do argumentos: str
). A chamada ocorrerá, e teremos um erro de execução no corpo da função. - Não melhora o desempenho
-
Anotações de tipo fornecem dados que poderiam, em tese, permitir otimizações do bytecode gerado. Mas, até julho de 2021, tais otimizações não ocorrem em nenhum ambiente Python que eu conheça.[84]
O melhor aspecto de usabilidade da tipagem gradual é que as anotações são sempre opcionais.
Nos sistemas de tipagem estáticos, a maioria das restrições de tipo são fáceis de expressar, muitas são desajeitadas, muitas são difíceis e algumas são impossíveis: Por exemplo, em julho de 2021, tipos recursivos não tinham suporte — veja as questões #182, Define a JSON type (EN) sobre o JSON e #731, Support recursive types (EN) do MyPy.
É perfeitamente possível que você escreva um ótimo programa Python, que consiga passar por uma boa cobertura de testes, mas ainda assim não consiga acrescentar dicas de tipo que satisfaçam um verificador de tipagem. Não tem problema; esqueça as dicas de tipo problemáticas e entregue o programa!
Dicas de tipo são opcionais em todos os níveis: você pode criar ou usar pacotes inteiros sem dicas de tipo, pode silenciar o verificador ao importar um daqueles pacotes sem dicas de tipo para um módulo onde você use dicas de tipo, e você também pode adicionar comentários especiais, para fazer o verificador de tipos ignorar linhas específicas do seu código.
👉 Dica
|
Tentar impor uma cobertura de 100% de dicas de tipo irá provavelmente estimular seu uso de forma impensada, apenas para satisfazer essa métrica. Isso também vai impedir equipes de aproveitarem da melhor forma possível o potencial e a flexibilidade de Python. Código sem dicas de tipo deveria ser aceito sem objeções quando anotações tornassem o uso de uma API menos amigável ou quando complicassem em demasia seu desenvolvimento. |
8.3. Tipagem gradual na prática
Vamos ver como a tipagem gradual funciona na prática, começando com uma função simples e acrescentando gradativamente a ela dicas de tipo, guiados pelo Mypy.
✒️ Nota
|
Há muitos verificadores de tipo para Python compatíveis com a PEP 484, incluindo o pytype do Google, o Pyright da Microsoft, o Pyre do Facebook — além de verificadores incluídos em IDEs como o PyCharm. Eu escolhi usar o Mypy nos exemplos por ele ser o mais conhecido. Entretanto, algum daqueles outros pode ser mais adequado para alguns projetos ou equipes. O Pytype, por exemplo, foi projetado para lidar com bases de código sem nenhuma dica de tipo e ainda assim gerar recomendações úteis. Ele é mais tolerante que o MyPy, e consegue também gerar anotações para o seu código. |
Vamos anotar uma função show_count
, que retorna uma string com um número e uma palavra no singular ou no plural, dependendo do número:
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no birds'
Exemplo 123 mostra o código-fonte de show_count
, sem anotações.
show_count
de messages.py sem dicas de tipo.def show_count(count, word):
if count == 1:
return f'1 {word}'
count_str = str(count) if count else 'no'
return f'{count_str} {word}s'
8.3.1. Usando o Mypy
Para começar a verificação de tipo, rodamos o comando mypy
passando o módulo messages.py como parâmetro:
…/no_hints/ $ pip install mypy
[muitas mensagens omitidas...]
…/no_hints/ $ mypy messages.py
Success: no issues found in 1 source file
Na configuração default, o Mypy não encontra nenhum problema com o Exemplo 123.
⚠️ Aviso
|
Durante a revisão deste capítulo estou usando Mypy 0.910, a versão mais recente no momento (em julho de 2021). A "Introduction" (EN) do Mypy adverte que ele "é oficialmente software beta. Mudanças ocasionais irão quebrar a compatibilidade com versões mais antigas." O Mypy está gerando pelo menos um relatório diferente daquele que recebi quando escrevi o capítulo, em abril de 2020. E quando você estiver lendo essas linhas, talvez os resultados também sejam diferentes daqueles mostrados aqui. |
Se a assinatura de uma função não tem anotações, Mypy a ignora por default — a menos que seja configurado de outra forma.
O Exemplo 124 também inclui testes de unidade do pytest
.
Este é código de messages_test.py.
from pytest import mark
from messages import show_count
@mark.parametrize('qty, expected', [
(1, '1 part'),
(2, '2 parts'),
])
def test_show_count(qty, expected):
got = show_count(qty, 'part')
assert got == expected
def test_show_count_zero():
got = show_count(0, 'part')
assert got == 'no parts'
Agora vamos acrescentar dicas de tipo, guiados pelo Mypy.
8.3.2. Tornando o Mypy mais rigoroso
A opção de linha de comando --disallow-untyped-defs
faz o Mypy apontar todas as definições de função que não tenham dicas de tipo para todos os argumentos e para o valor de retorno.
Usando --disallow-untyped-defs
com o arquivo de teste produz três erros e uma observação:
…/no_hints/ $ mypy --disallow-untyped-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
messages_test.py:10: error: Function is missing a type annotation
messages_test.py:15: error: Function is missing a return type annotation
messages_test.py:15: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files (checked 1 source file)
Nas primeiras etapas da tipagem gradual, prefiro usar outra opção:
--disallow-incomplete-defs
.
Inicialmente o Mypy não me dá nenhuma nova informação:
…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
Success: no issues found in 1 source file
Agora vou acrescentar apenas o tipo do retorno a show_count
em messages.py:
def show_count(count, word) -> str:
Isso é suficiente para fazer o Mypy olhar para o código. Usando a mesma linha de comando anterior para verificar messages_test.py fará o Mypy examinar novamente o messages.py:
…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
for one or more arguments
Found 1 error in 1 file (checked 1 source file)
Agora posso gradualmente acrescentar dicas de tipo, função por função, sem receber avisos sobre as funções onde ainda não adicionei anotações Essa é uma assinatura completamente anotada que satisfaz o Mypy:
def show_count(count: int, word: str) -> str:
👉 Dica
|
Em vez de digitar opções de linha de comando como [mypy] python_version = 3.9 warn_unused_configs = True disallow_incomplete_defs = True |
8.3.3. Um valor default para um argumento
A função show_count
no Exemplo 123 só funciona com substantivos regulares. Se o plural não pode ser composto acrescentando um 's'
, devemos deixar o usuário fornecer a forma plural, assim:
>>> show_count(3, 'mouse', 'mice')
'3 mice'
Vamos experimentar um pouco de "desenvolvimento orientado a tipos." Primeiro acrescento um teste usando aquele terceiro argumento. Não esqueça de adicionar a dica do tipo de retorno à função de teste, senão o Mypy não vai inspecioná-la.
def test_irregular() -> None:
got = show_count(2, 'child', 'children')
assert got == '2 children'
O Mypy detecta o erro:
…/hints_2/ $ mypy messages_test.py
messages_test.py:22: error: Too many arguments for "show_count"
Found 1 error in 1 file (checked 1 source file)
Então edito show_count
, acrescentando o argumento opcional plural
no Exemplo 125.
showcount
de hints_2/messages.py com um argumento opcionaldef show_count(count: int, singular: str, plural: str = '') -> str:
if count == 1:
return f'1 {singular}'
count_str = str(count) if count else 'no'
if not plural:
plural = singular + 's'
return f'{count_str} {plural}'
E agora o Mypy reporta "Success."
⚠️ Aviso
|
Aqui está um erro de digitação que Python não reconhece. Você consegue encontrá-lo?
O relatório de erros do Mypy não é muito útil:
A dica de tipo para o argumento Pela minha experiência, esse é um erro comum e fácil de passar desapercebido, especialmente em dicas de tipo complexas. |
Os seguintes detalhes são considerados um bom estilo para dicas de tipo:
-
Sem espaço entre o nome do parâmetro e o
:
; um espaço após o:
-
Espaços dos dois lados do
=
que precede um valor default de parâmetro
Por outro lado, a PEP 8 diz que não deve haver espaço em torno de =
se não há nenhuma dica de tipo para aquele parâmetro específico.
8.3.4. Usando None como default
No Exemplo 125, o parâmetro plural
está anotado como str
, e o valor default é ''
. Assim não há conflito de tipo.
Eu gosto dessa solução, mas em outros contextos None
é um default melhor. Se o parâmetro opcional requer um tipo mutável, então None
é o único default sensato, como vimos na Seção 6.5.1.
Com None
como default para o parâmetro plural
, a assinatura ficaria assim:
from typing import Optional
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:
Vamos destrinchar essa linha:
-
Optional[str]
significa queplural
pode ser umastr
ouNone
. -
É obrigatório fornecer explicitamente o valor default
= None
.
Se você não atribuir um valor default a plural
, o runtime de Python vai tratar o parâmetro como obrigatório. Lembre-se: durante a execução do programa, as dicas de tipo são ignoradas.
Veja que é preciso importar Optional
do módulo typing
. Quando importamos tipos, é uma boa prática usar a sintaxe from typing import X
, para reduzir o tamanho das assinaturas das funções.
⚠️ Aviso
|
|
Agora que tivemos um primeiro contato concreto com a tipagem gradual, vamos examinar o que o conceito de tipo significa na prática.
8.4. Tipos são definidos pelas operações possíveis
Há muitas definições do conceito de tipo na literatura. Aqui vamos assumir que tipo é um conjunto de valores e um conjunto de funções que podem ser aplicadas àqueles valores.
Na prática, é mais útil considerar o conjunto de operações possíveis como a caraterística definidora de um tipo.[85]
Por exemplo, pensando nas operações possíveis, quais são os tipos válidos para x
na função a seguir?
def double(x):
return x * 2
O tipo do parâmetro x
pode ser numérico (int
, complex
, Fraction
, numpy.uint32
, etc.), mas também pode ser uma sequência (str
, tuple
, list
, array
), uma numpy.array
N-dimensional, ou qualquer outro tipo que implemente ou herde um método __mul__
que aceite um inteiro como argumento.
Entretanto, considere a anotação double
abaixo. Ignore por enquanto a ausência do tipo do retorno, vamos nos concentrar no tipo do parâmetro:
from collections import abc
def double(x: abc.Sequence):
return x * 2
Um verificador de tipo irá rejeitar esse código.
Se você informar ao Mypy que x
é do tipo abc.Sequence
, ele vai marcar x * 2
como erro, pois a Sequence
ABC não implementa ou herda o método __mul__
. Durante a execução, o código vai funcionar com sequências concretas como str
, tuple
, list
, array
, etc., bem como com números, pois durante a execução as dicas de tipo são ignoradas. Mas o verificador de tipo se preocupa apenas com o que estiver explicitamente declarado, e abc.Sequence
não suporta __mul__
.
Por essa razão o título dessa seção é "Tipos São Definidos pelas Operações Possíveis." O runtime de Python aceita qualquer objeto como argumento x
nas duas versões da função double
. O cálculo de x * 2
pode funcionar, ou pode causar um TypeError
, se a operação não for suportada por x
. Por outro lado, Mypy vai marcar x * 2
como um erro quando analisar o código-fonte anotado de double
, pois é uma operação não suportada pelo tipo declarado x: abc.Sequence
.
Em um sistema de tipagem gradual, acontece uma interação entre duas perspectivas diferentes de tipo:
- Duck typing ("tipagem pato")
-
A perspectiva adotada pelo Smalltalk — a primeira linguagem orientada a objetos — bem como em Python, JavaScript, e Ruby. Objetos tem tipo, mas variáveis (incluindo parâmetros) não. Na prática, não importa qual o tipo declarado de um objeto, importam apenas as operações que ele efetivamente suporta. Se eu posso invocar
birdie.quack()
então, nesse contexto,birdie
é um pato. Por definição, duck typing só é aplicada durante a execução, quando se tenta aplicar operações sobre os objetos. Isso é mais flexível que a tipagem nominal, ao preço de permitir mais erros durante a execução.[86] - Tipagem nominal
-
É a perspectiva adotada em C++, Java, e C#, e suportada em Python anotado. Objetos e variáveis tem tipos. Mas objetos só existem durante a execução, e o verificador de tipo só se importa com o código-fonte, onde as variáveis (incluindo parâmetros de função) tem anotações com dicas de tipo. Se
Duck
é uma subclasse deBird
, você pode atribuir uma instância deDuck
a um parâmetro anotado comobirdie: Bird
. Mas no corpo da função, o verificador considera a chamadabirdie.quack()
ilegal, poisbirdie
é nominalmente umBird
, e aquela classe não fornece o método.quack()
. Não interessa que o argumento real, durante a execução, é umDuck
, porque a tipagem nominal é aplicada de forma estática. O verificador de tipo não executa qualquer pedaço do programa, ele apenas lê o código-fonte. Isso é mais rígido que duck typing, com a vantagem de capturar alguns bugs durante o desenvolvimento, ou mesmo em tempo real, enquanto o código está sendo digitado em um IDE.
O Exemplo 126 é um exemplo bobo que contrapõe duck typing e tipagem nominal, bem como verificação de tipo estática e comportamento durante a execução.[87]
class Bird:
pass
class Duck(Bird): # (1)
def quack(self):
print('Quack!')
def alert(birdie): # (2)
birdie.quack()
def alert_duck(birdie: Duck) -> None: # (3)
birdie.quack()
def alert_bird(birdie: Bird) -> None: # (4)
birdie.quack()
-
Duck
é uma subclasse deBird
. -
alert
não tem dicas de tipo, então o verificador a ignora. -
alert_duck
aceita um argumento do tipoDuck
. -
alert_bird
aceita um argumento do tipoBird
.
Verificando birds.py com Mypy, encontramos um problema:
…/birds/ $ mypy birds.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)
Só de analisar o código fonte, Mypy percebe que alert_bird
é problemático: a dica de tipo declara o parâmetro birdie
como do tipo Bird
, mas o corpo da função chama birdie.quack()
— e a classe Bird
não tem esse método.
Agora vamos tentar usar o módulo birds
em daffy.py no Exemplo 127.
from birds import *
daffy = Duck()
alert(daffy) # (1)
alert_duck(daffy) # (2)
alert_bird(daffy) # (3)
-
Chamada válida, pois
alert
não tem dicas de tipo. -
Chamada válida, pois
alert_duck
recebe um argumento do tipoDuck
edaffy
é umDuck
. -
Chamada válida, pois
alert_bird
recebe um argumento do tipoBird
, edaffy
também é umBird
— a superclasse deDuck
.
Mypy reporta o mesmo erro em daffy.py, sobre a chamada a quack
na função alert_bird
definida em birds.py:
…/birds/ $ mypy daffy.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)
Mas o Python não vê qualquer problema com daffy.py em si: as três chamadas de função estão OK.
Agora, rodando daffy.py, o resultado é o seguinte:
…/birds/ $ python3 daffy.py
Quack!
Quack!
Quack!
Funciona perfeitamente! Viva o duck typing!
Durante a execução do programa, Python não se importa com os tipos declarados. Ele usa apenas duck typing. O Mypy apontou um erro em alert_bird
, mas a chamada da função com daffy
funciona corretamente quando executada.
À primeira vista isso pode surpreender muitos pythonistas: um verificador de tipo estático muitas vezes encontra erros em código que sabemos que vai funcionar quanto executado.
Entretanto, se daqui a alguns meses você for encarregado de estender o exemplo bobo do pássaro, você agradecerá ao Mypy. Observe esse módulo woody.py module, que também usa birds
, no Exemplo 128.
from birds import *
woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)
O Mypy encontra dois erros ao verificar woody.py:
…/birds/ $ mypy woody.py
birds.py:16: error: "Bird" has no attribute "quack"
woody.py:5: error: Argument 1 to "alert_duck" has incompatible type "Bird";
expected "Duck"
Found 2 errors in 2 files (checked 1 source file)
O primeiro erro é em birds.py: a chamada a birdie.quack()
em alert_bird
, que já vimos antes.
O segundo erro é em woody.py: woody
é uma instância de Bird
, então a chamada alert_duck(woody)
é inválida, pois aquela função exige um Duck.
Todo Duck
é um Bird
, mas nem todo Bird
é um Duck
.
Durante a execução, nenhuma das duas chamadas em woody.py funcionariam. A sucessão de falhas é melhor ilustrada em uma sessão no console, através das mensagens de erro, no Exemplo 129.
>>> from birds import *
>>> woody = Bird()
>>> alert(woody) # (1)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_duck(woody) # (2)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_bird(woody) # (3)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
-
O Mypy não tinha como detectar esse erro, pois não há dicas de tipo em
alert
. -
O Mypy avisou do problema:
Argument 1 to "alert_duck" has incompatible type "Bird"; expected "Duck"
(Argumento 1 paraalert_duck
é do tipo incompatível "Bird"; argumento esperado era "Duck") -
O Mypy está avisando desde o Exemplo 126 que o corpo da função
alert_bird
está errado:"Bird" has no attribute "quack"
(Bird não tem um atributo "quack")
Este pequeno experimento mostra que o duck typing é mais fácil para o iniciante e mais flexível, mas permite que operações não suportadas causem erros durante a execução.
A tipagem nominal detecta os erros antes da execução, mas algumas vezes rejeita código que seria executado sem erros - como a chamada a alert_bird(daffy)
no Exemplo 127.
Mesmo que funcione algumas vezes, o nome da função alert_bird
está incorreto: seu código exige um objeto que suporte o método .quack()
, que não existe em Bird
.
Nesse exemplo bobo, as funções tem uma linha apenas.
Mas na vida real elas poderiam ser mais longas, e poderiam passar o argumento birdie
para outras funções, e a origem daquele argumento poderia estar a muitas chamadas de função de distância, tornando difícil localizar a causa do erro durante a execução.
O verificador de tipos impede que muitos erros como esse aconteçam durante a execução de um programa.
✒️ Nota
|
O valor das dicas de tipo é questionável em exemplos minúsculo que cabem em um livro. Os benefícios crescem conforme o tamanho da base de código afetada. É por essa razão que empresas com milhões de linhas de código em Python - como a Dropbox, o Google e o Facebook - investiram em equipes e ferramentas para promover a adoção global de dicas de tipo internamente, e hoje tem partes significativas e crescentes de sua base de código checadas para tipo em suas linhas (pipeline) de integração contínua. |
Nessa seção exploramos as relações de tipos e operações no duck typing e na tipagem nominal, começando com a função simples double()
— que deixamos sem dicas de tipo. Agora vamos dar uma olhada nos tipos mais importantes ao anotar funções.
Vamos ver um bom modo de adicionar dicas de tipo a double()
quando examinarmos Seção 8.5.10. Mas antes disso, há tipos mais importantes para conhecer.
8.5. Tipos próprios para anotações
Quase todos os tipos em Python podem ser usados em dicas de tipo, mas há restrições e recomendações. Além disso, o módulo typing
introduziu constructos especiais com uma semântica às vezes surpreendente.
Essa seção trata de todos os principais tipos que você pode usar em anotações:
-
typing.Any
-
Tipos e classes simples
-
typing.Optional
etyping.Union
-
Coleções genéricas, incluindo tuplas e mapeamentos
-
Classes base abstratas
-
Iteradores genéricos
-
Genéricos parametrizados e
TypeVar
-
typing.Protocols
— crucial para duck typing estático -
typing.Callable
-
typing.NoReturn
— um bom modo de encerrar essa lista.
Vamos falar de um de cada vez, começando por um tipo que é estranho, aparentemente inútil, mas de uma importância fundamental.
8.5.1. O tipo Any
A pedra fundamental de qualquer sistema gradual de tipagem é o tipo Any
, também conhecido como o tipo dinâmico. Quando um verificador de tipo vê um função sem tipo como esta:
def double(x):
return x * 2
ele supõe isto:
def double(x: Any) -> Any:
return x * 2
Isso significa que o argumento x
e o valor de retorno podem ser de qualquer tipo, inclusive de tipos diferentes. Assume-se que Any
pode suportar qualquer operação possível.
Compare Any
com object
. Considere essa assinatura:
def double(x: object) -> object:
Essa função também aceita argumentos de todos os tipos, porque todos os tipos são subtipo-de object
.
Entretanto, um verificador de tipo vai rejeitar essa função:
def double(x: object) -> object:
return x * 2
O problema é que object
não suporta a operação __mul__
. Veja o que diz o Mypy:
…/birds/ $ mypy double_object.py
double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)
Tipos mais gerais tem interfaces mais restritas, isto é, eles suportam menos operações. A classe object
implementa menos operações que abc.Sequence
,
que implementa menos operações que abc.MutableSequence
,
que por sua vez implementa menos operações que list
.
Mas Any
é um tipo mágico que reside tanto no topo quanto na base da hierarquia de tipos. Ele é simultaneamente o tipo mais geral - então um argumento n: Any
aceita valores de qualquer tipo - e o tipo mais especializado, suportando assim todas as operações possíveis.
Pelo menos é assim que o verificador de tipo entende Any
.
Claro, nenhum tipo consegue suportar qualquer operação possível, então usar Any
impede o verificador de tipo de cumprir sua missão primária: detectar operações potencialmente ilegais antes que seu programa falhe e levante uma exceção durante sua execução.
8.5.1.1. Subtipo-de versus consistente-com
Sistemas tradicionais de tipagem nominal orientados a objetos se baseiam na relação subtipo-de.
Dada uma classe T1
e uma subclasse T2
, então T2
é subtipo-de T1
.
Observe este código:
class T1:
...
class T2(T1):
...
def f1(p: T1) -> None:
...
o2 = T2()
f1(o2) # OK
A chamada f1(o2)
é uma aplicação do Princípio de Substituição de Liskov (Liskov Substitution Principle—LSP).
Barbara Liskov[88] na verdade definiu é subtipo-de em termos das operações suportadas. Se um objeto do tipo T2
substitui um objeto do tipo T1
e o programa continua se comportando de forma correta, então T2
é subtipo-de T1
.
Seguindo com o código visto acima, essa parte mostra uma violação do LSP:
def f2(p: T2) -> None:
...
o1 = T1()
f2(o1) # type error
Do ponto de vista das operações suportadas, faz todo sentido: como uma subclasse, T2
herda e precisa suportar todas as operações suportadas por T1
. Então uma instância de T2
pode ser usada em qualquer lugar onde se espera uma instância de T1
. Mas o contrário não é necessariamente verdadeiro: T2
pode implementar métodos adicionais, então uma instância de T1
não pode ser usada onde se espera uma instância de T2
. Este foco nas operações suportadas se reflete no nome _behavioral subtyping (subtipagem comportamental) (EN), também usado para se referir ao LSP.
Em um sistema de tipagem gradual há outra relação, consistente-com (consistent-with), que se aplica sempre que subtipo-de puder ser aplicado, com disposições especiais para o tipo Any
.
As regras para consistente-com são:
-
Dados
T1
e um subtipoT2
, entãoT2
é consistente-comT1
(substituição de Liskov). -
Todo tipo é consistente-com
Any
: você pode passar objetos de qualquer tipo em um argumento declarado como de tipo `Any. -
Any
é consistente-com todos os tipos: você sempre pode passar um objeto de tipoAny
onde um argumento de outro tipo for esperado.
Considerando as definições anteriores dos objetos o1
e o2
, aqui estão alguns exemplos de código válido, ilustrando as regras #2 e #3:
def f3(p: Any) -> None:
...
o0 = object()
o1 = T1()
o2 = T2()
f3(o0) #
f3(o1) # tudo certo: regra #2
f3(o2) #
def f4(): # tipo implícito de retorno: `Any`
...
o4 = f4() # tipo inferido: `Any`
f1(o4) #
f2(o4) # tudo certo: regra #3
f3(o4) #
Todo sistema de tipagem gradual precisa de um tipo coringa como Any
👉 Dica
|
O verbo "inferir" é um sinônimo bonito para "adivinhar", quando usado no contexto da análise de tipos. Verificadores de tipo modernos, em Python e outras linguagens, não precisam de anotações de tipo em todo lugar porque conseguem inferir o tipo de muitas expressões. Por exemplo, se eu escrever |
Agora podemos explorar o restante dos tipos usados em anotações.
8.5.2. Tipos simples e classes
Tipos simples como int
, float
, str
, e bytes
podem ser usados diretamente em dicas de tipo.
Classes concretas da biblioteca padrão, de pacotes externos ou definidas pelo usuário — FrenchDeck
, Vector2d
, e Duck
- também podem ser usadas em dicas de tipo.
Classes base abstratas também são úteis aqui. Voltaremos a elas quando formos estudar os tipos coleção, e em Seção 8.5.7.
Para classes, consistente-com é definido como subtipo_de: uma subclasse é consistente-com todas as suas superclasses.
Entretanto, "a praticidade se sobrepõe à pureza", então há uma exceção importante, discutida em seguida.
👉 Dica
|
int é Consistente-Com complex
Não há nenhuma relação nominal de subtipo entre os tipo nativos |
8.5.3. Os tipos Optional e Union
Nós vimos o tipo especial Optional
em Seção 8.3.4. Ele resolve o problema de ter None
como default, como no exemplo daquela seção:
from typing import Optional
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:
A sintaxe Optional[str]
é na verdade um atalho para Union[str, None]
,
que significa que o tipo de plural
pode ser str
ou None
.
👉 Dica
|
Uma sintaxe melhor para Optional e Union em Python 3.10
Desde Python 3.10 é possível escrever
O operador |
A assinatura da função nativa ord
é um exemplo simples de Union
- ela aceita str
or bytes
,
e retorna um int
:[89]
def ord(c: Union[str, bytes]) -> int: ...
Aqui está um exemplo de uma função que aceita uma str
, mas pode retornar uma str
ou um float
:
from typing import Union
def parse_token(token: str) -> Union[str, float]:
try:
return float(token)
except ValueError:
return token
Se possível, evite criar funções que retornem o tipo Union
, pois esse tipo exige um esforço extra do usuário: pois para saber o que fazer com o valor recebido da função será necessário verificar o tipo daquele valor durante a execução.
Mas a parse_token
no código acima é um caso de uso razoável no contexto de interpretador de expressões simples.
👉 Dica
|
Na Seção 4.10, vimos funções que aceitam tanto |
Union[]
exige pelo menos dois tipos.
Tipos Union
aninhados tem o mesmo efeito que uma Union
"achatada" .
Então esta dica de tipo:
Union[A, B, Union[C, D, E]]
é o mesmo que:
Union[A, B, C, D, E]
Union
é mais útil com tipos que não sejam consistentes entre si. Por exemplo: Union[int, float]
é redundante, pois int
é consistente-com float
. Se você usar apenas float
para anotar o parâmetro, ele vai também aceitar valores int
.
8.5.4. Coleções genéricas
A maioria das coleções em Python são heterogêneas.
Por exemplo, você pode inserir qualquer combinação de tipos diferentes em uma list
.
Entretanto, na prática isso não é muito útil: se você colocar objetos em uma coleção, você certamente vai querer executar alguma operação com eles mais tarde, e normalmente isso significa que eles precisam compartilhar pelo menos um método comum.[90]
Tipos genéricos podem ser declarados com parâmetros de tipo, para especificar o tipo de item com o qual eles conseguem trabalhar.
Por exemplo, uma list
pode ser parametrizada para restringir o tipo de elemento ali contido, como se pode ver no Exemplo 130.
tokenize
com dicas de tipo para Python ≥ 3.9def tokenize(text: str) -> list[str]:
return text.upper().split()
Em Python ≥ 3.9, isso significa que tokenize
retorna uma list
onde todos os elementos são do tipo str
.
As anotações stuff: list
e stuff: list[Any]
significam a mesma coisa: stuff
é uma lista de objetos de qualquer tipo.
👉 Dica
|
Se você estiver usando Python 3.8 ou anterior, o conceito é o mesmo, mas você precisa de mais código para funcionar - como explicado em Suporte a tipos de coleção descontinuados. |
A PEP 585—Type Hinting Generics In Standard Collections (EN) lista as coleções da biblioteca padrão que aceitam dicas de tipo genéricas. A lista a seguir mostra apenas as coleções que usam a forma mais simples de dica de tipo genérica, container[item]
:
list collections.deque abc.Sequence abc.MutableSequence
set abc.Container abc.Set abc.MutableSet
frozenset abc.Collection
Os tipos tuple
e mapping aceitam dicas de tipo mais complexas, como veremos em suas respectivas seções.
No Python 3.10, não há uma boa maneira de anotar array.array
, levando em consideração o argumento typecode
do construtor, que determina se o array contém inteiros ou floats. Um problema ainda mais complicado é verificar a faixa dos inteiros, para prevenir OverflowError
durante a execução, ao se adicionar novos elementos. Por exemplo, um array
com typecode=B
só pode receber valores int
de 0 a 255. Até Python 3.11, o sistema de tipagem estática de Python não consegue lidar com esse desafio.
Agora vamos ver como anotar tuplas genéricas.
8.5.5. Tipos tuple
Há três maneiras de anotar os tipos tuple
.
-
Tuplas como registros (records)
-
Tuplas como registro com campos nomeados
-
Tuplas como sequências imutáveis.
8.5.5.1. Tuplas como registros
Se você está usando uma tuple
como um registro, use o tipo tuple
nativo e declare os tipos dos campos dentro dos []
.
Por exemplo, a dica de tipo seria tuple[str, float, str]
para aceitar uma tupla com nome da cidade, população e país:
('Shanghai', 24.28, 'China')
.
Observe uma função que recebe um par de coordenadas geográficas e retorna uma Geohash, usada assim:
>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'
O Exemplo 133 mostra a definição da função geohash
, usando o pacote geolib
do PyPI.
geohash
from geolib import geohash as gh # type: ignore # (1)
PRECISION = 9
def geohash(lat_lon: tuple[float, float]) -> str: # (2)
return gh.encode(*lat_lon, PRECISION)
-
Esse comentário evita que o Mypy avise que o pacote
geolib
não tem nenhuma dica de tipo. -
O parâmetro
lat_lon
, anotado como umatuple
com dois camposfloat
.
👉 Dica
|
Com Python < 3.9, importe e use |
8.5.5.2. Tuplas como registros com campos nomeados
Para a anotar uma tupla com muitos campos, ou tipos específicos de tupla que seu código usa com frequência, recomendo fortemente usar typing.NamedTuple
, como visto no Capítulo 5.
O Exemplo 134 mostra uma variante de Exemplo 133 com NamedTuple
.
NamedTuple
, Coordinates
e a função geohash
from typing import NamedTuple
from geolib import geohash as gh # type: ignore
PRECISION = 9
class Coordinate(NamedTuple):
lat: float
lon: float
def geohash(lat_lon: Coordinate) -> str:
return gh.encode(*lat_lon, PRECISION)
Como explicado na Seção 5.2, typing.NamedTuple
é uma factory de subclasses de tuple
, então Coordinate
é consistente-com tuple[float, float]
, mas o inverso não é verdadeiro - afinal, Coordinate
tem métodos extras adicionados por NamedTuple
, como ._asdict()
, e também poderia ter métodos definidos pelo usuário.
Na prática, isso significa que é seguro (do ponto de vista do tipo de argumento) passar uma instância de Coordinate
para a função display
, definida assim:
def display(lat_lon: tuple[float, float]) -> str:
lat, lon = lat_lon
ns = 'N' if lat >= 0 else 'S'
ew = 'E' if lon >= 0 else 'W'
return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'
8.5.5.3. Tuplas como sequências imutáveis
Para anotar tuplas de tamanho desconhecido, usadas como listas imutáveis, você precisa especificar um único tipo, seguido de uma vírgula e …
(isto é o símbolo de reticências de Python, formado por três pontos, não o caractere Unicode U+2026
—HORIZONTAL ELLIPSIS
).
Por exemplo, tuple[int, …]
é uma tupla com itens int
.
As reticências indicam que qualquer número de elementos >= 1 é aceitável. Não há como especificar campos de tipos diferentes para tuplas de tamanho arbitrário.
As anotações stuff: tuple[Any, …]
e stuff: tuple
são equivalentes:
stuff
é uma tupla de tamanho desconhecido contendo objetos de qualquer tipo.
Aqui temos um função columnize
, que transforma uma sequência em uma tabela de colunas e células, na forma de uma lista de tuplas de tamanho desconhecido.
É útil para mostrar os itens em colunas, assim:
>>> animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus'.split()
>>> table = columnize(animals)
>>> table
[('drake', 'koala', 'yak'), ('fawn', 'lynx', 'zapus'), ('heron', 'tahr'),
('ibex', 'xerus')]
>>> for row in table:
... print(''.join(f'{word:10}' for word in row))
...
drake koala yak
fawn lynx zapus
heron tahr
ibex xerus
O Exemplo 135 mostra a implementação de columnize
.
Observe o tipo do retorno:
list[tuple[str, ...]]
from collections.abc import Sequence
def columnize(
sequence: Sequence[str], num_columns: int = 0
) -> list[tuple[str, ...]]:
if num_columns == 0:
num_columns = round(len(sequence) ** 0.5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
8.5.6. Mapeamentos genéricos
Tipos de mapeamento genéricos são anotados como MappingType[KeyType, ValueType]
.
O tipo nativo dict
e os tipos de mapeamento em collections
e collections.abc
aceitam essa notação em Python ≥ 3.9. Para versões mais antigas, você deve usar typing.Dict
e outros tipos de mapeamento no módulo typing
, como discutimos em Suporte a tipos de coleção descontinuados.
O Exemplo 136 mostra um uso na prática de uma função que retorna um índice invertido para permitir a busca de caracteres Unicode pelo nome — uma variação do Exemplo 61 mais adequada para código server-side (também chamado back-end), como veremos no Capítulo 21.
Dado o início e o final dos códigos de caractere Unicode, name_index
retorna um dict[str, set[str]]
, que é um índice invertido mapeando cada palavra para um conjunto de caracteres que tem aquela palavra em seus nomes. Por exemplo, após indexar os caracteres ASCII de 32 a 64, aqui estão os conjuntos de caracteres mapeados para as palavras 'SIGN'
e 'DIGIT'
, e a forma de encontrar o caractere chamado 'DIGIT EIGHT'
:
>>> index = name_index(32, 65)
>>> index['SIGN']
{'$', '>', '=', '+', '<', '%', '#'}
>>> index['DIGIT']
{'8', '5', '6', '2', '3', '0', '1', '4', '7', '9'}
>>> index['DIGIT'] & index['EIGHT']
{'8'}
O Exemplo 136 mostra o código fonte de charindex.py com a função name_index
.
Além de uma dica de tipo dict[]
, este exemplo tem três outros aspectos que estão aparecendo pela primeira vez no livro.
import sys
import re
import unicodedata
from collections.abc import Iterator
RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1
def tokenize(text: str) -> Iterator[str]: # (1)
"""return iterable of uppercased words"""
for match in RE_WORD.finditer(text):
yield match.group().upper()
def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
index: dict[str, set[str]] = {} # (2)
for char in (chr(i) for i in range(start, end)):
if name := unicodedata.name(char, ''): # (3)
for word in tokenize(name):
index.setdefault(word, set()).add(char)
return index
-
tokenize
é uma função geradora. Capítulo 17 é sobre geradores. -
A variável local
index
está anotada. Sem a dica, o Mypy diz:Need type annotation for 'index' (hint: "index: dict[<type>, <type>] = …")
. -
Eu usei o operador morsa (walrus operator)
:=
na condição doif
. Ele atribui o resultado da chamada aunicodedata.name()
aname
, e a expressão inteira é calculada a partir daquele resultado. Quando o resultado é''
, isso é falso, e oindex
não é atualizado.[92]
✒️ Nota
|
Ao usar |
8.5.7. Classes bases abstratas
Seja conservador no que envia, mas liberal no que aceita.
ou o Princípio da Robustez
A Tabela 15 apresenta várias classes abstratas de collections.abc
.
Idealmente, uma função deveria aceitar argumentos desses tipos abstratos—ou seus equivalentes de typing
antes de Python 3.9—e não tipos concretos. Isso dá mais flexibilidade a quem chama a função.
Considere essa assinatura de função:
from collections.abc import Mapping
def name2hex(name: str, color_map: Mapping[str, int]) -> str:
Usar abc.Mapping
permite ao usuário da função fornecer uma instância de dict
, defaultdict
, ChainMap
, uma subclasse de UserDict
subclass, ou qualquer outra classe que seja um subtipo-de Mapping
.
Por outro lado, veja essa assinatura:
def name2hex(name: str, color_map: dict[str, int]) -> str:
Agora color_map
tem que ser um dict
ou um de seus subtipos, tal como defaultdict
ou OrderedDict
.
Especificamente, uma subclasse de collections.UserDict
não passaria pela verificação de tipo para color_map
, a despeito de ser a maneira recomendada de criar mapeamentos definidos pelo usuário, como vimos na Seção 3.6.5.
O Mypy rejeitaria um UserDict
ou uma instância de classe derivada dele, porque UserDict
não é uma subclasse de dict
; eles são irmãos. Ambos são subclasses de abc.MutableMapping
.[93]
Assim, em geral é melhor usar abc.Mapping
ou abc.MutableMapping
em dicas de tipos de parâmetros, em vez de dict
(ou typing.Dict
em código antigo).
Se a função name2hex
não precisar modificar o color_map
recebido, a dica de tipo mais precisa para color_map
é abc.Mapping
.
Desse jeito, quem chama não precisa fornecer um objeto que implemente métodos como setdefault
, pop
, e update
, que fazem parte da interface de MutableMapping
, mas não de Mapping
.
Isso reflete a segunda parte da lei de Postel:
"[seja] liberal no que aceita."
A lei de Postel também nos diz para sermos conservadores no que enviamos. O valor de retorno de uma função é sempre um objeto concreto, então a dica de tipo do valor de saída deve ser um tipo concreto, como no exemplo em Seção 8.5.4 — que usa list[str]
:
def tokenize(text: str) -> list[str]:
return text.upper().split()
No verbete de typing.List
(EN - Tradução abaixo não oficial), a documentação de Python diz:
Versão genérica de
list
. Útil para anotar tipos de retorno. Para anotar argumentos é preferível usar um tipo de coleção abstrata , tal comoSequence
ouIterable
.
Comentários similares aparecem nos verbetes de typing.Dict
e typing.Set
.
Lembre-se que a maioria dos ABCs de collections.abc
e outras classes concretas de collections
, bem como as coleções nativas, suportam notação de dica de tipo genérica como collections.deque[str]
desde Python 3.9. As coleções correspondentes em typing
só precisavam suportar código escrito em Python 3.8 ou anterior. A lista completa de classes que se tornaram genéricas aparece em na seção "Implementation" da
PEP 585—Type Hinting Generics In Standard Collections (EN).
Para encerrar nossa discussão de ABCs em dicas de tipo, precisamos falar sobre os ABCs numbers
.
8.5.7.1. A queda da torre numérica
O pacote numbers
define a assim chamada torre numérica (numeric tower) descrita na PEP 3141—A Type Hierarchy for Numbers (EN).
A torre é uma hierarquia linear de ABCs, com Number
no topo:
-
Number
-
Complex
-
Real
-
Rational
-
Integral
Esses ABCs funcionam perfeitamente para checagem de tipo durante a execução, mas eles não são suportados para checagem de tipo estática. A seção "Numeric Tower" da PEP 484 rejeita os ABCs numbers
e manda tratar os tipo nativos complex
, float
, e int
como casos especiais, como explicado em int é Consistente-Com complex. Vamos voltar a essa questão na Seção 13.6.8, em Capítulo 13, que é dedicada a comparar protocolos e ABCs
Na prática, se você quiser anotar argumentos numéricos para checagem de tipo estática, existem algumas opções:
-
Usar um dos tipo concretos,
int
,float
, oucomplex
— como recomendado pela PEP 488. -
Declarar um tipo union como
Union[float, Decimal, Fraction]
. -
Se você quiser evitar a codificação explícita de tipos concretos, usar protocolos numéricos como
SupportsFloat
, tratados na Seção 13.6.2.
A Seção 8.5.10 abaixo é um pré-requisito para entender protocolos numéricos.
Antes disso, vamos examinar um dos ABCs mais úteis para dicas de tipo: Iterable
.
8.5.8. Iterable
A documentação de typing.List
que eu citei acima recomenda Sequence
e Iterable
para dicas de tipo de parâmetros de função.
Esse é um exemplo de argumento Iterable
, na função math.fsum
da biblioteca padrão:
def fsum(__seq: Iterable[float]) -> float:
👉 Dica
|
Arquivos Stub e o Projeto Typeshed
Até Python 3.10, a biblioteca padrão não tem anotações, mas o Mypy, o PyCharm, etc, conseguem encontrar as dicas de tipo necessárias no projeto Typeshed, na forma de arquivos stub: arquivos de código-fonte especiais, com uma extensão .pyi, que contém assinaturas anotadas de métodos e funções, sem a implementação - muito parecidos com headers em C. A assinatura para |
O Exemplo 137 é outro exemplo do uso de um parâmetro Iterable
, que produz itens que são tuple[str, str]
. A função é usada assim:
>>> l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
>>> text = 'mad skilled noob powned leet'
>>> from replacer import zip_replace
>>> zip_replace(text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'
O Exemplo 137 mostra a implementação.
from collections.abc import Iterable
FromTo = tuple[str, str] # (1)
def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # (2)
for from_, to in changes:
text = text.replace(from_, to)
return text
-
FromTo
é um apelido de tipo: eu atribuituple[str, str]
aFromTo
, para tornar a assinatura dezip_replace
mais legível. -
changes
tem que ser umIterable[FromTo]
; é o mesmo que escreverIterable[tuple[str, str]]
, mas é mais curto e mais fácil de ler.
👉 Dica
|
O TypeAlias Explícito em Python 3.10
PEP 613—Explicit Type Aliases introduziu um tipo especial, o
|
8.5.8.1. abc.Iterable versus abc.Sequence
Tanto math.fsum
quanto replacer.zip_replace
tem que percorrer todos os argumentos do Iterable
para produzir um resultado. Dado um iterável sem fim tal como o gerador itertools.cycle
como entrada, essas funções consumiriam toda a memória e derrubariam o processo Python. Apesar desse perigo potencial, é muito comum no Python moderno se oferecer funções que aceitam um Iterable
como argumento, mesmo se elas tem que processar a estrutura inteira para obter um resultado. Isso dá a quem chama a função a opção de fornecer um gerador como dado de entrada, em vez de uma sequência pré-construída, com uma grande economia potencial de memória se o número de itens de entrada for grande.
Por outro lado, a função columnize
no Exemplo 135 requer uma Sequence
, não um Iterable
, pois ela precisa obter a len()
do argumento para calcular previamente o número de linhas.
Assim como Sequence
, o melhor uso de Iterable
é como tipo de argumento. Ele é muito vago como um tipo de saída. Uma função deve ser mais precisa sobre o tipo concreto que retorna.
O tipo Iterator
, usado como tipo do retorno no Exemplo 136, está intimamente relacionado a Iterable
. Voltaremos a ele em Capítulo 17, que trata de geradores e iteradores clássicos.
8.5.9. Genéricos parametrizados e TypeVar
Um genérico parametrizado é um tipo genérico, escrito na forma list[T]
, onde T
é um tipo variável que será vinculado a um tipo específico a cada uso. Isso permite que um tipo de parâmetro seja refletido no tipo resultante.
O Exemplo 138 define sample
, uma função que recebe dois argumentos:
uma Sequence
de elementos de tipo T
e um int
.
Ela retorna uma list
de elementos do mesmo tipo T
, escolhidos aleatoriamente do primeiro argumento.
O Exemplo 138 mostra a implementação.
from collections.abc import Sequence
from random import shuffle
from typing import TypeVar
T = TypeVar('T')
def sample(population: Sequence[T], size: int) -> list[T]:
if size < 1:
raise ValueError('size must be >= 1')
result = list(population)
shuffle(result)
return result[:size]
Aqui estão dois exemplos do motivo de eu usar um tipo variável em sample
:
-
Se chamada com uma tupla de tipo
tuple[int, …]
— que é consistente-comSequence[int]
- então o tipo parametrizado éint
, então o tipo de retorno élist[int]
. -
Se chamada com uma
str
— que é consistente-comSequence[str]
— então o tipo parametrizado éstr
, e o tipo do retorno élist[str]
.
✒️ Nota
|
Por que TypeVar é necessário?
Os autores da PEP 484 queriam introduzir dicas de tipo ao acrescentar o módulo |
Outro exemplo é a função statistics.mode
da biblioteca padrão, que retorna o ponto de dado mais comum de uma série.
Aqui é uma exemplo de uso da documentação:
>>> mode([1, 1, 2, 3, 3, 3, 3, 4])
3
Sem o uso de TypeVar
, mode
poderia ter uma assinatura como a apresentada no Exemplo 139.
from collections import Counter
from collections.abc import Iterable
def mode(data: Iterable[float]) -> float:
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
Muitos dos usos de mode
envolvem valores int
ou float
, mas Python tem outros tipos numéricos, e é desejável que o tipo de retorno siga o tipo dos elementos do Iterable
recebido.
Podemos melhorar aquela assinatura usando TypeVar
. Vamos começar com uma assinatura parametrizada simples, mas errada.
from collections.abc import Iterable
from typing import TypeVar
T = TypeVar('T')
def mode(data: Iterable[T]) -> T:
Quando aparece pela primeira vez na assinatura, o tipo parametrizado T
pode ser qualquer tipo. Da segunda vez que aparece, ele vai significar o mesmo tipo que da primeira vez.
Assim, qualquer iterável é consistente-com Iterable[T]
, incluindo iterável de tipos unhashable que collections.Counter
não consegue tratar.
Precisamos restringir os tipos possíveis de se atribuir a T
.
Vamos ver maneiras diferentes de fazer isso nas duas seções seguintes.
8.5.9.1. TypeVar restrito
O TypeVar
aceita argumentos posicionais adicionais para restringir o tipo parametrizado.
Podemos melhorar a assinatura de mode
para aceitar um número específico de tipos, assim:
from collections.abc import Iterable
from decimal import Decimal
from fractions import Fraction
from typing import TypeVar
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
def mode(data: Iterable[NumberT]) -> NumberT:
Está melhor que antes, e era a assinatura de mode
em
statistics.pyi, o arquivo stub em typeshed
em 25 de maio de 2020.
Entretanto, a documentação em statistics.mode
inclui esse exemplo:
>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'
Na pressa, poderíamos apenas adicionar str
à definição de NumberT
:
NumberT = TypeVar('NumberT', float, Decimal, Fraction, str)
Com certeza funciona, mas NumberT
estaria muito mal batizado se aceitasse str
.
Mais importante, não podemos ficar listando tipos para sempre, cada vez que percebermos que mode
pode lidar com outro deles.
Podemos fazer com melhor com um outro recurso de TypeVar
, como veremos a seguir.
8.5.9.2. TypeVar delimitada
Examinando o corpo de mode
no Exemplo 139, vemos que a classe Counter
é usada para classificação. Counter
é baseada em dict
, então o tipo do elemento do iterável data
precisa ser hashable.
A princípio, essa assinatura pode parecer que funciona:
from collections.abc import Iterable, Hashable
def mode(data: Iterable[Hashable]) -> Hashable:
Agora o problema é que o tipo do item retornado é Hashable
:
um ABC que implementa apenas o método __hash__
.
Então o verificador de tipo não vai permitir que façamos nada com o valor retornado, exceto chamar seu método hash()
. Não é muito útil.
A solução está em outro parâmetro opcional de TypeVar
: o parâmetro representado pela palavra-chave bound
. Ele estabelece um limite superior para os tipos aceitos.
No Exemplo 140, temos bound=Hashable
. Isso significa que o tipo do parâmetro pode ser Hashable
ou qualquer subtipo-de Hashable
.[95]
from collections import Counter
from collections.abc import Iterable, Hashable
from typing import TypeVar
HashableT = TypeVar('HashableT', bound=Hashable)
def mode(data: Iterable[HashableT]) -> HashableT:
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
Em resumo:
-
Um tipo variável restrito será concretizado em um dos tipos nomeados na declaração do TypeVar.
-
Um tipo variável delimitado será concretizado pata o tipo inferido da expressão - desde que o tipo inferido seja consistente-com o limite declarado pelo argumento
bound=
do TypeVar.
✒️ Nota
|
É um pouco lamentável que a palavra-chave do argumento para declarar um |
O construtor de typing.TypeVar
tem outros parâmetros opcionais - covariant
e contravariant
— que veremos em Capítulo 15, Seção 15.7.
Agora vamos concluir essa introdução a TypeVar
com AnyStr
.
8.5.9.3. O tipo variável pré-definido AnyStr
O módulo typing
inclui um TypeVar pré-definido chamado AnyStr
.
Ele está definido assim:
AnyStr = TypeVar('AnyStr', bytes, str)
AnyStr
é usado em muitas funções que aceitam tanto bytes
quanto str
, e retornam valores do tipo recebido.
Agora vamos ver typing.Protocol
, um novo recurso de Python 3.8, capaz de permitir um uso de dicas de tipo mais pythônico.
8.5.10. Protocolos estáticos
✒️ Nota
|
Em programação orientada a objetos, o conceito de um "protocolo" como uma interface informal é tão antigo quanto Smalltalk, e foi uma parte essencial de Python desde o início. Entretanto, no contexto de dicas de tipo, um protocolo é uma subclasse de |
O tipo Protocol
, como descrito em
PEP 544—Protocols: Structural subtyping (static duck typing) (EN), é similar às interfaces em Go: um tipo protocolo é definido especificando um ou mais métodos, e o verificador de tipo analisa se aqueles métodos estão implementados onde um tipo daquele protocolo é usado.
Em Python, uma definição de protocolo é escrita como uma subclasse de typing.Protocol
.
Entretanto, classes que implementam um protocolo não precisam herdar, registrar ou declarar qualquer relação com a classe que define o protocolo. É função do verificador de tipo encontrar os tipos de protocolos disponíveis e exigir sua utilização.
Abaixo temos um problema que pode ser resolvido com a ajuda de Protocol
e TypeVar
.
Suponha que você quisesse criar uma função top(it, n)
, que retorna os n
maiores elementos do iterável it
:
>>> top([4, 1, 5, 2, 6, 7, 3], 3)
[7, 6, 5]
>>> l = 'mango pear apple kiwi banana'.split()
>>> top(l, 3)
['pear', 'mango', 'kiwi']
>>>
>>> l2 = [(len(s), s) for s in l]
>>> l2
[(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')]
>>> top(l2, 3)
[(6, 'banana'), (5, 'mango'), (5, 'apple')]
Um genérico parametrizado top
ficaria parecido com o mostrado no Exemplo 141.
top
function com um parâmetro de tipo T
indefinidodef top(series: Iterable[T], length: int) -> list[T]:
ordered = sorted(series, reverse=True)
return ordered[:length]
O problema é, como restringir T
?
Ele não pode ser Any
ou object
, pois series
precisa funcionar com sorted
. A sorted
nativa na verdade aceita Iterable[Any]
, mas só porque o parâmetro opcional key
recebe uma função que calcula uma chave de ordenação arbitrária para cada elemento. O que acontece se você passar para sorted
uma lista de objetos simples, mas não fornecer um argumento key
?
Vamos tentar:
>>> l = [object() for _ in range(4)]
>>> l
[<object object at 0x10fc2fca0>, <object object at 0x10fc2fbb0>,
<object object at 0x10fc2fbc0>, <object object at 0x10fc2fbd0>]
>>> sorted(l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'object' and 'object'
A mensagem de erro mostra que sorted
usa o operador <
nos elementos do iterável.
É só isso? Vamos tentar outro experimento rápido:[96]
>>> class Spam:
... def __init__(self, n): self.n = n
... def __lt__(self, other): return self.n < other.n
... def __repr__(self): return f'Spam({self.n})'
...
>>> l = [Spam(n) for n in range(5, 0, -1)]
>>> l
[Spam(5), Spam(4), Spam(3), Spam(2), Spam(1)]
>>> sorted(l)
[Spam(1), Spam(2), Spam(3), Spam(4), Spam(5)]
Isso confirma a suspeita: eu consigo passar um lista de Spam
para sort
, porque Spam
implementa __lt__
— o método especial subjacente ao operador <
.
Então o parâmetro de tipo T
no Exemplo 141 deveria ser limitado a tipos que implementam __lt__
.
No Exemplo 140, precisávamos de um parâmetro de tipo que implementava __hash__
, para poder usar typing.Hashable
como limite superior do parâmetro de tipo.
Mas agora não há um tipo adequado em typing
ou abc
para usarmos, então precisamos criar um.
O Exemplo 142 mostra o novo tipo SupportsLessThan
, um Protocol
.
Protocol
, SupportsLessThan
from typing import Protocol, Any
class SupportsLessThan(Protocol): # (1)
def __lt__(self, other: Any) -> bool: ... # (2)
-
Um protocolo é uma subclasse de
typing.Protocol
. -
O corpo do protocolo tem uma ou mais definições de método, com
…
em seus corpos.
Um tipo T
é consistente-com um protocolo P
se T
implementa todos os métodos definido em P
, com assinaturas de tipo correspondentes.
Dado SupportsLessThan
, nós agora podemos definir essa versão funcional de top
no Exemplo 143.
top
usando uma TypeVar
com bound=SupportsLessThan
from collections.abc import Iterable
from typing import TypeVar
from comparable import SupportsLessThan
LT = TypeVar('LT', bound=SupportsLessThan)
def top(series: Iterable[LT], length: int) -> list[LT]:
ordered = sorted(series, reverse=True)
return ordered[:length]
Vamos testar top
. O Exemplo 144 mostra parte de uma bateria de testes para uso com o pytest
.
Ele tenta chamar top
primeiro com um gerador de expressões que produz tuple[int, str]
, e depois com uma lista de object
.
Com a lista de object
, esperamos receber uma exceção de TypeError
.
top
from collections.abc import Iterator
from typing import TYPE_CHECKING # (1)
import pytest
from top import top
# muitas linhas omitidas
def test_top_tuples() -> None:
fruit = 'mango pear apple kiwi banana'.split()
series: Iterator[tuple[int, str]] = ( # (2)
(len(s), s) for s in fruit)
length = 3
expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
result = top(series, length)
if TYPE_CHECKING: # (3)
reveal_type(series) # (4)
reveal_type(expected)
reveal_type(result)
assert result == expected
# intentional type error
def test_top_objects_error() -> None:
series = [object() for _ in range(4)]
if TYPE_CHECKING:
reveal_type(series)
with pytest.raises(TypeError) as excinfo:
top(series, 3) # (5)
assert "'<' not supported" in str(excinfo.value)
-
A constante
typing.TYPE_CHECKING
é sempreFalse
durante a execução do programa, mas os verificadores de tipo fingem que ela éTrue
quando estão fazendo a verificação. -
Declaração de tipo explícita para a variável
series
, para tornar mais fácil a leitura da saída do Mypy.[97] -
Esse
if
evita que as três linhas seguintes sejam executadas durante o teste. -
reveal_type()
não pode ser chamada durante a execução, porque não é uma função regular, mas sim um mecanismo de depuração do Mypy - por isso não háimport
para ela. Mypy vai produzir uma mensagem de depuração para cada chamada à pseudo-funçãoreveal_type()
, mostrando o tipo inferido do argumento. -
Essa linha será marcada pelo Mypy como um erro.
Os testes anteriores são bem sucedidos - mas eles funcionariam de qualquer forma, com ou sem dicas de tipo em top.py.
Mais precisamente, se eu verificar aquele arquivo de teste com o Mypy, verei que o TypeVar
está funcionando como o esperado.
Veja a saída do comando mypy
no Exemplo 145.
⚠️ Aviso
|
Desde o Mypy 0.910 (julho de 2021), em alguns casos a saída de |
…/comparable/ $ mypy top_test.py
top_test.py:32: note:
Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]" (1)
top_test.py:33: note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test.py:34: note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]" (2)
top_test.py:41: note:
Revealed type is "builtins.list[builtins.object*]" (3)
top_test.py:43: error:
Value of type variable "LT" of "top" cannot be "object" (4)
Found 1 error in 1 file (checked 1 source file)
-
Em
test_top_tuples
,reveal_type(series)
mostra que ele é umIterator[tuple[int, str]]
— que eu declarei explicitamente. -
reveal_type(result)
confirma que o tipo produzido pela chamada atop
é o que eu queria: dado o tipo deseries
, oresult
élist[tuple[int, str]]
. -
Em
test_top_objects_error
,reveal_type(series)
mostra que ele é umalist[object*]
. Mypy põe um*
após qualquer tipo que tenha sido inferido: eu não anotei o tipo deseries
nesse teste. -
Mypy marca o erro que esse teste produz intencionalmente: o tipo dos elementos do
Iterable
series
não pode serobject
(ele tem que ser do tipoSupportsLessThan
).
A principal vantagem de um tipo protocolo sobre os ABCs é que o tipo não precisa de nenhuma declaração especial para ser consistente-com um tipo protocolo. Isso permite que um protocolo seja criado aproveitando tipos pré-existentes, ou tipos implementados em bases de código que não estão sob nosso controle.
Eu não tenho que derivar ou registrar str
, tuple
, float
, set
, etc. com SupportsLessThan
para usá-los onde um parâmetro SupportsLessThan
é esperado.
Eles só precisam implementar __lt__
.
E o verificador de tipo ainda será capaz de realizar seu trabalho, porque SupportsLessThan
está explicitamente declarado como um Protocol
— diferente dos protocolos implícitos comuns no duck typing, que são invisíveis para o verificador de tipos.
A classe especial Protocol
foi introduzida na
PEP 544—Protocols: Structural subtyping (static duck typing).
O Exemplo 143 demonstra porque esse recurso é conhecido como duck typing estático (static duck typing): a solução para anotar o parâmetro series
de top
era dizer "O tipo nominal de series
não importa, desde que ele implemente o método __lt__
."
Em Python, o duck typing sempre permitiu dizer isso de forma implícita, deixando os verificadores de tipo estáticos sem ação.
Um verificador de tipo não consegue ler o código fonte em C do CPython, ou executar experimentos no console para descobrir que sorted
só requer que seus elementos suportem <
.
Agora podemos tornar o duck typing explícito para os verificadores estáticos de tipo. Por isso faz sentido dizer que typing.Protocol
nos oferece duck typing estático.[98]
Há mais para falar sobre typing.Protocol
. Vamos voltar a ele na Parte IV, onde Capítulo 13 compara as abordagens da tipagem estrutural, do duck typing e dos ABCs - outro modo de formalizar protocolos.
Além disso, a Seção 15.2 (no Capítulo 15) explica como declarar assinaturas de funções de sobrecarga (overload) com @typing.overload
, e inclui um exemplo bastante extenso usando typing.Protocol
e uma TypeVar
delimitada.
✒️ Nota
|
O |
8.5.11. Callable
Para anotar parâmetros de callback ou objetos callable retornados por funções de ordem superior, o módulo collections.abc
oferece o tipo Callable
, disponível no módulo typing
para quem ainda não estiver usando Python 3.9.
Um tipo Callable
é parametrizado assim:
Callable[[ParamType1, ParamType2], ReturnType]
A lista de parâmetros - [ParamType1, ParamType2]
— pode ter zero ou mais tipos.
Aqui está um exemplo no contexto de uma função repl
, parte do interpretador iterativo simples que veremos na Seção 18.3:[99]
def repl(input_fn: Callable[[Any], str] = input]) -> None:
Durante a utilização normal, a função repl
usa a input
nativa de Python para ler expressões inseridas pelo usuário.
Entretanto, para testagem automatizada ou para integração com outras fontes de input, repl
aceita um parâmetro input_fn
opcional:
um Callable
com o mesmo parâmetro e tipo de retorno de input
.
A input
nativa tem a seguinte assinatura no typeshed:
def input(__prompt: Any = ...) -> str: ...
A assinatura de input
é consistente-com esta dica de tipo Callable
Callable[[Any], str]
Não existe sintaxe para a nomear tipo de argumentos opcionais ou de palavra-chave. A
documentação de typing.Callable
diz "tais funções são raramente usadas como tipo de callback." Se você precisar de um dica de tipo para acompanhar uma função com assinatura flexível, substitua o lista de parâmetros inteira por …
- assim:
Callable[..., ReturnType]
A interação de parâmetros de tipo genéricos com uma hierarquia de tipos introduz um novo conceito: variância.
8.5.11.1. Variância em tipos callable
Imagine um sistema de controle de temperatura com uma função update
simples, como mostrada no Exemplo 146.
A função update
chama a função probe
para obter a temperatura atual, e chama display
para mostrar a temperatura para o usuário.
probe
e display
são ambas passadas como argumentos para update
, por motivos didáticos. O objetivo do exemplo é contrastar duas anotações de Callable
: uma com um tipo de retorno e outro com um tipo de parâmetro.
from collections.abc import Callable
def update( # (1)
probe: Callable[[], float], # (2)
display: Callable[[float], None] # (3)
) -> None:
temperature = probe()
# imagine lots of control code here
display(temperature)
def probe_ok() -> int: # (4)
return 42
def display_wrong(temperature: int) -> None: # (5)
print(hex(temperature))
update(probe_ok, display_wrong) # type error # (6)
def display_ok(temperature: complex) -> None: # (7)
print(temperature)
update(probe_ok, display_ok) # OK # (8)
-
update
recebe duas funções callable como argumentos. -
probe
precisa ser uma callable que não recebe nenhuma argumento e retorna umfloat
-
display
recebe um argumentofloat
e retornaNone
. -
probe_ok
é consistente-comCallable[[], float]
porque retornar umint
não quebra código que espera umfloat
. -
display_wrong
não é consistente-comCallable[[float], None]
porque não há garantia que uma função esperando umint
consiga lidar com umfloat
; por exemplo, a funçãohex
de Python aceita umint
mas rejeita umfloat
. -
O Mypy marca essa linha porque
display_wrong
é incompatível com a dica de tipo no parâmetrodisplay
emupdate
. -
display_ok
é consistente_comCallable[[float], None]
porque uma função que aceita umcomplex
também consegue lidar com um argumentofloat
. -
Mypy está satisfeito com essa linha.
Resumindo,
não há problema em fornecer uma função de callback que retorne um int
quando o código espera uma função callback que retorne um float
, porque um valor int
sempre pode ser usado onde um float
é esperado.
Formalmente, dizemos que Callable[[], int]
é subtipo-de Callable[[], float]
— assim como int
é subtipo-de float
.
Isso significa que Callable
é covariante no que diz respeito aos tipos de retorno, porque a relação subtipo-de dos tipos int
e float
aponta na mesma direção que os tipo Callable
que os usam como tipos de retorno.
Por outro lado, é um erro de tipo fornecer uma função callback que recebe um argumento int
quando é necessário um callback que possa processar um float
.
Formalmente, Callable[[int], None]
não é subtipo-de Callable[[float], None]
.
Apesar de int
ser subtipo-de float
, no Callable
parametrizado a relação é invertida:
Callable[[float], None]
é subtipo-de Callable[[int], None]
.
Assim dizemos que aquele Callable
é contravariante a respeito dos tipos de parâmetros declarados.
A Seção 15.7 no Capítulo 15 explica variância em mais detalhes e com exemplos de tipos invariantes, covariantes e contravariantes.
👉 Dica
|
Por hora, saiba que a maioria dos tipos genéricos parametrizados são invariantes, portanto mais simples.
Por exemplo, se eu declaro
|
Agora chegamos ou último tipo especial que examinaremos nesse capítulo.
8.5.12. NoReturn
Esse é um tipo especial usado apenas para anotar o tipo de retorno de funções que nunca retornam. Normalmente, elas existem para gerar exceções. Há dúzias dessas funções na biblioteca padrão.
Por exemplo, sys.exit()
levanta SystemExit
para encerrar o processo Python.
Sua assinatura no typeshed
é:
def exit(__status: object = ...) -> NoReturn: ...
O parâmetro __status__
é apenas posicional, e tem um valor default.
Arquivos stub não contém valores default, em vez disso eles usam …
.
O tipo de __status
é object
, o que significa que pode também ser None
,
assim seria redundante escrever Optional[object]
.
Na Capítulo 24,
o Exemplo 457 usa NoReturn
em __flag_unknown_attrs
, um método projetado para produzir uma mensagem de erro completa e amigável, e então levanta um AttributeError
.
A última seção desse capítulo épico é sobre parâmetros posicionais e variádicos
8.6. Anotando parâmetros apenas posicionais e variádicos
Lembra da função tag
do Exemplo 113?
Da última vez que vimos sua assinatura foi em Seção 7.7.1:
def tag(name, /, *content, class_=None, **attrs):
Aqui está tag
, completamente anotada e ocupando várias linhas - uma convenção comum para assinaturas longas,
com quebras de linha como o formatador blue faria:
from typing import Optional
def tag(
name: str,
/,
*content: str,
class_: Optional[str] = None,
**attrs: str,
) -> str:
Observe a dica de tipo *content: str
, para parâmetros posicionais arbitrários;
Isso significa que todos aqueles argumentos tem que ser do tipo str
.
O tipo da variável local content
no corpo da função será tuple[str, …]
.
A dica de tipo para argumentos de palavra-chave arbitrários é attrs: str
neste exemplo, portanto o tipo de attrs
dentro da função será dict[str, str]
.
Para uma dica de tipo como attrs: float
,
o tipo de attrs
na função seria dict[str, float]
.``
Se for necessário que o parâmetro attrs
aceite valores de tipos diferentes, é preciso usar uma Union[]
ou Any
: **attrs: Any
.
A notação /
para parâmetros puramente posicionais só está disponível com Python ≥ 3.8.
Em Python 3.7 ou anterior, isso é um erro de sintaxe.
A convenção da PEP 484 é prefixar o nome cada parâmetro puramente posicional com dois sublinhados.
Veja a assinatura de tag
novamente, agora em duas linhas, usando a convenção da PEP 484:
from typing import Optional
def tag(__name: str, *content: str, class_: Optional[str] = None,
**attrs: str) -> str:
O Mypy entende e aplica as duas formas de declarar parâmetros puramente posicionais.
Para encerrar esse capítulo, vamos considerar brevemente os limites das dicas de tipo e do sistema de tipagem estática que elas suportam.
8.7. Tipos imperfeitos e testes poderosos
Os mantenedores de grandes bases de código corporativas relatam que muitos bugs são encontrados por verificadores de tipo estáticos, e o custo de resolvê-los é menor que se os mesmos bugs fossem descobertos apenas após o código estar rodando em produção. Entretanto, é essencial observar que a testagem automatizada era uma prática padrão largamente adotada muito antes da tipagem estática ser introduzida nas empresas que eu conheço.
Mesmo em contextos onde ela é mais benéfica, a tipagem estática não pode ser elevada a árbitro final da correção. Não é difícil encontrar:
- Falsos Positivos
-
Ferramentas indicam erros de tipagem em código correto.
- Falsos Negativos
-
Ferramentas não indicam erros em código incorreto.
Além disso, se formos forçados a checar o tipo de tudo, perdemos um pouco do poder expressivo de Python:
-
Alguns recursos convenientes não podem ser checados de forma estática: por exemplo, o desempacotamento de argumentos como em
config(**settings)
. -
Recursos avançados como propriedades, descritores, metaclasses e metaprogramação em geral, têm suporte muito deficiente ou estão além da compreensão dos verificadores de tipo
-
Verificadores de tipo ficam obsoletos e/ou incompatíveis após o lançamento de novas versões de Python, rejeitando ou mesmo quebrando ao analisar código com novos recursos da linguagem - algumas vezes por mais de um ano.
Restrições comuns de dados não podem ser expressas no sistema de tipo - mesmo restrições simples. Por exemplo, dicas de tipo são incapazes de assegurar que "quantidade deve ser um inteiro > 0" ou que "label deve ser uma string com 6 a 12 letras em ASCII." Em geral, dicas de tipo não são úteis para localizar erros na lógica do negócio subjacente ao código.
Dadas essas ressalvas, dicas de tipo não podem ser o pilar central da qualidade do software, e torná-las obrigatórias sem qualquer exceção só amplificaria os aspectos negativos.
Considere o verificador de tipo estático como uma das ferramentas na estrutura moderna de integração de código, ao lado de testadores, analisadores de código (linters), etc. O objetivo de uma estrutura de produção de integração de código é reduzir as falhas no software, e testes automatizados podem encontrar muitos bugs que estão fora do alcance de dicas de tipo. Qualquer código que possa ser escrito em Python pode ser testado em Python - com ou sem dicas de tipo.
✒️ Nota
|
O título e a conclusão dessa seção foram inspirados pelo artigo "Strong Typing vs. Strong Testing" (EN) de Bruce Eckel, também publicado na antologia The Best Software Writing I (EN), editada por Joel Spolsky (Apress). Bruce é um fã de Python, e autor de livros sobre C++, Java, Scala, e Kotlin. Naquele texto, ele conta como foi um defensor da tipagem estática até aprender Python, e conclui: "Se um programa em Python tem testes de unidade adequados, ele poderá ser tão robusto quanto um programa em C++, Java, ou C# com testes de unidade adequados (mas será mais rápido escrever os testes em Python). |
Isso encerra nossa cobertura das dicas de tipo em Python por agora. Elas serão também o ponto central do Capítulo 15, que trata de classes genéricas, variância, assinaturas sobrecarregadas, coerção de tipos (type casting), entre outros tópicos. Até lá, as dicas de tipo aparecerão em várias funções ao longo do livro.
8.8. Resumo do capítulo
Começamos com uma pequena introdução ao conceito de tipagem gradual, depois adotamos uma abordagem prática. É difícil ver como a tipagem gradual funciona sem uma ferramenta que efetivamente leia as dicas de tipo, então desenvolvemos uma função anotada guiados pelos relatórios de erro do Mypy.
Voltando à ideia de tipagem gradual, vimos como ela é um híbrido do duck typing tradicional de Python e da tipagem nominal mais familiar aos usuários de Java, C++ e de outras linguagens de tipagem estática.
A maior parte do capítulo foi dedicada a apresentar os principais grupos de tipos usados em anotações.
Muitos dos tipos discutidos estão relacionados a tipos conhecidos de objetos de Python, tais como coleções, tuplas e callables - estendidos para suportar notação genérica do tipo Sequence[float]
.
Muitos daqueles tipos são substitutos temporários, implementados no módulo typing
antes que os tipos padrão fossem modificados para suportar genéricos, no Python 3.9.
Alguns desses tipos são entidade especiais.
Any
, Optional
, Union
, e NoReturn
não tem qualquer relação com objetos reais na memória, existem apenas no domínio abstrato do sistema de tipos.
Estudamos genéricos parametrizados e variáveis de tipo, que trazem mais flexibilidade para as dicas de tipo sem sacrificar a segurança da tipagem.
Genéricos parametrizáveis se tornam ainda mais expressivos com o uso de Protocol
.
Como só surgiu no Python 3.8, Protocol
ainda não é muito usado - mas é de uma enorme importância.
Protocol
permite duck typing estático:
É a ponte fundamental entre o núcleo de Python, coberto pelo duck typing, e a tipagem nominal que permite a verificadores de tipo estáticos encontrarem bugs.
Ao discutir alguns desses tipos, usamos o Mypy para localizar erros de checagem de tipo e tipos inferidos, com a ajuda da função mágica reveal_type()
do Mypy.
A seção final mostrou como anotar parâmetros exclusivamente posicionais e variádicos.
Dicas de tipo são um tópico complexo e em constante evolução. Felizmente elas são um recurso opcional. Vamos manter Python acessível para a maior base de usuários possível, e parar de defender que todo código Python precisa ter dicas de tipo - como já presenciei em sermões públicos de evangelistas da tipagem.
Nosso BDFL[100] emérito liderou a movimento de inclusão de dicas de tipo em Python, então é muito justo que esse capítulo comece e termine com palavras dele.
Não gostaria de uma versão de Python na qual eu fosse moralmente obrigado a adicionar dicas de tipo o tempo todo. Eu realmente acho que dicas de tipo tem seu lugar, mas há muitas ocasiões em que elas não valem a pena, e é maravilhoso que possamos escolher usá-las.[101]
8.9. Para saber mais
Bernát Gábor escreveu em seu excelente post, "The state of type hints in Python" (EN):
Dicas de Tipo deveriam ser usadas sempre que valha à pena escrever testes de unidade .
Eu sou um grande fã de testes, mas também escrevo muito código exploratório. Quando estou explorando, testes e dicas de tipo não ajudam. São um entrave.
Esse post do Gábor é uma das melhores introduções a dicas de tipo em Python que eu já encontrei, junto com o texto de Geir Arne Hjelle, "Python Type Checking (Guide)" (EN). "Hypermodern Python Chapter 4: Typing" (EN), de Claudio Jolowicz, é uma introdução mas curta que também fala de validação de checagem de tipo durante a execução.
Para uma abordagem mais aprofundada, a documentação do Mypy é a melhor fonte. Ela é útil independente do verificador de tipo que você esteja usando, pois tem páginas de tutorial e de referência sobre tipagem em Python em geral - não apenas sobre o próprio Mypy.
Lá você também encontrará uma conveniente página de referência (ou _cheat sheet) (EN) e uma página muito útil sobre problemas comuns e suas soluções (EN).
A documentação do módulo typing
é uma boa referência rápida, mas não entra em muitos detalhes.
A PEP 483—The Theory of Type Hints (EN) inclui uma explicação aprofundada sobre variância, usando Callable
para ilustrar a contravariância.
As referências definitivas são as PEP relacionadas a tipagem.
Já existem mais de 20 delas.
A audiência alvo das PEPs são os core developers (desenvolvedores principais da linguagem em si) e o Steering Council de Python, então elas pressupõe uma grande quantidade de conhecimento prévio, e certamente não são uma leitura leve.
Como já mencionado, o Capítulo 15 cobre outros tópicos sobre tipagem, e a Seção 15.10 traz referências adicionais, incluindo a Tabela 16, com a lista das PEPs sobre tipagem aprovadas ou em discussão até o final de 2021.
"Awesome Python Typing" é uma ótima coleção de links para ferramentas e referências.
9. Decoradores e Clausuras
Houve uma certa quantidade de reclamações sobre a escolha do nome "decorador" para esse recurso. A mais frequente foi sobre o nome não ser consistente com seu uso no livro da GoF.[103] O nome decorator provavelmente se origina de seu uso no âmbito dos compiladores—uma árvore sintática é percorrida e anotada.
Decoradores de função nos permitem "marcar" funções no código-fonte, para aprimorar de alguma forma seu comportamento.
É um mecanismo muito poderoso. Por exemplo, o decorador @functools.cache
armazena um mapeamento de argumentos para resultados,
e depois usa esse mapeamento para evitar computar novamente o resultado quando a função é chamada com argumentos já vistos. Isso pode acelerar muito uma aplicação.
Mas para dominar esse recurso é preciso antes entender clausuras (closures)—o nome dado à estrutura onde uma função captura variáveis presentes no escopo onde a função é definida, necessárias para a execução da função futuramente.[104]
A palavra reservada mais obscura de Python é nonlocal
, introduzida no Python 3.0.
É perfeitamente possível ter uma vida produtiva e lucrativa programando em Python sem jamais usá-la,
seguindo uma dieta estrita de orientação a objetos centrada em classes.
Entretanto, caso queira implementar seus próprios decoradores de função,
precisa entender clausuras, e então a necessidade de nonlocal
fica evidente.
Além de sua aplicação aos decoradores, clausuras também são essenciais para qualquer tipo de programação utilizando callbacks, e para codar em um estilo funcional quando isso fizer sentido.
O objetivo último deste capítulo é explicar exatamente como funcionam os decoradores de função, desde simples decoradores de registro até os complicados decoradores parametrizados. Mas antes de chegar a esse objetivo, precisamos tratar de:
-
Como Python analisa a sintaxe de decoradores
-
Como Python decide se uma variável é local
-
Porque clausuras existem e como elas funcionam
-
Qual problema é resolvido por
nonlocal
Após criar essa base, poderemos então enfrentar os outros tópicos relativos aos decoradores:
-
A implementação de um decorador bem comportado
-
Os poderosos decoradores na biblioteca padrão:
@cache
,@lru_cache
, e@singledispatch
-
A implementação de um decorador parametrizado
9.1. Novidades nesse capítulo
O decorador de caching functools.cache
—introduzido no Python 3.9—é mais simples que o tradicional functools.lru_cache
, então falo primeiro daquele. Este último é tratado na Seção 9.9.2, incluindo a forma simplificada introduzida no Python 3.8.
A Seção 9.9.3 foi expandida e agora inclui dicas de tipo, a forma recomendada de usar functools.singledispatch
desde Python 3.7.
A Seção 9.10 agora inclui um exemplo baseado em classes, o Exemplo 173.
Transferi o #ch_design_patterns para o final da Parte II: Funções como objetos, para melhorar a fluidez do livro. E a Seção 10.3 também aparece agora naquele capítulo, juntamente com outras variantes do padrão de projeto Estratégia usando invocáveis.
Começamos com uma introdução muito suave aos decoradores, e dali seguiremos para o restante dos tópicos listados no início do capítulo.
9.2. Introdução aos decoradores
Um decorador é um invocável que recebe outra função como um argumento (a função decorada).
Um decorador pode executar algum processamento com a função decorada, e ou a devolve ou a substitui por outra função ou por um objeto invocável.[105]
Em outras palavras, supondo a existência de um decorador chamado decorate
, esse código:
@decorate
def target():
print('running target()')
tem o mesmo efeito de:
def target():
print('running target()')
target = decorate(target)
O resultado final é o mesmo: após a execução de qualquer dos dois trechos, o nome target
está vinculado a qualquer que seja a função devolvida por decorate(target)
—que tanto pode ser a função inicialmente chamada target
quanto uma outra função diferente.
Para confirmar que a função decorada é substituída, veja a sessão de console no Exemplo 147.
>>> def deco(func):
... def inner():
... print('running inner()')
... return inner (1)
...
>>> @deco
... def target(): (2)
... print('running target()')
...
>>> target() (3)
running inner()
>>> target (4)
<function deco.<locals>.inner at 0x10063b598>
-
deco
devolve seu objeto funçãoinner
. -
target
é decorada pordeco
. -
Invocar a
target
decorada causa, na verdade, a execução deinner
. -
A inspeção revela que
target
é agora uma referência ainner
.
Estritamente falando, decoradores são apenas açúcar sintático. Como vimos, é sempre possível chamar um decorador como um invocável normal, passando outra função como parâmetro. Algumas vezes isso inclusive é conveniente, especialmente quando estamos fazendo metaprogramação—mudando o comportamento de um programa durante a execução.
Três fatos essenciais nos dão um bom resumo dos decoradores:
-
Um decorador é uma função ou outro invocável.
-
Um decorador pode substituir a função decorada por outra, diferente.
-
Decoradores são executados imediatamente quando um módulo é carregado.
Vamos agora nos concentrar nesse terceiro ponto.
9.3. Quando Python executa decoradores
Uma característica fundamental dos decoradores é serem executados logo após a função decorada ser definida. Isso normalmente acontece no tempo de importação (isto é, quando um módulo é carregado pelo Python). Observe registration.py no Exemplo 148.
registry = [] # (1)
def register(func): # (2)
print(f'running register({func})') # (3)
registry.append(func) # (4)
return func # (5)
@register # (6)
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3(): # (7)
print('running f3()')
def main(): # (8)
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main() # (9)
-
registry
vai manter referências para funções decoradas por@register
. -
register
recebe uma função como argumento. -
Exibe a função que está sendo decorada, para fins de demonstração.
-
Insere
func
emregistry
. -
Devolve
func
: precisamos devolver uma função; aqui devolvemos a mesma função recebida como argumento. -
f1
ef2
são decoradas por@register
. -
f3
não é decorada. -
main
mostraregistry
, depois chamaf1()
,f2()
, ef3()
. -
main()
só é invocada se registration.py for executado como um script.
O resultado da execução de registration.py se parece com isso:
$ python3 registration.py running register(<function f1 at 0x100631bf8>) running register(<function f2 at 0x100631c80>) running main() registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>] running f1() running f2() running f3()
Observe que register
roda (duas vezes) antes de qualquer outra função no módulo.
Quando register
é chamada, ela recebe o objeto função a ser decorado como argumento—por exemplo,
<function f1 at 0x100631bf8>
.
Após o carregamento do módulo, a lista registry
contém
referências para as duas funções decoradas:
f1
e f2
. Essa funções, bem como f3
, são executadas apenas quando chamadas explicitamente por main
.
Se registration.py for importado (e não executado como um script), a saída é essa:
>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)
Nesse momento, se você inspecionar registry
, verá isso:
>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]
O ponto central do Exemplo 148 é enfatizar que decoradores de função são executados assim que o módulo é importado, mas as funções decoradas só rodam quando são invocadas explicitamente. Isso ressalta a diferença entre o que pythonistas chamam de tempo de importação e tempo de execução.
9.4. Decoradores de registro
Considerando a forma como decoradores são normalmente usados em código do mundo real, o Exemplo 148 é incomum por duas razões:
-
A função do decorador é definida no mesmo módulo das funções decoradas. Em geral, um decorador real é definido em um módulo e aplicado a funções de outros módulos.
-
O decorador
register
devolve a mesma função recebida como argumento. Na prática, a maior parte dos decoradores define e devolve uma função interna.
Apesar do decorador register
no Exemplo 148 devolver a função decorada inalterada, a técnica não é inútil.
Decoradores parecidos são usados por muitos frameworks Python para adicionar funções a um registro central—por exemplo,
um registro mapeando padrões de URLs para funções que geram respostas HTTP.
Tais decoradores de registro podem ou não modificar as funções decoradas.
Vamos ver um decorador de registro em ação na Seção 10.3 (do Capítulo 10).
A maioria dos decoradores modificam a função decorada. Eles normalmente fazem isso definindo e devolvendo uma função interna para substituir a função decorada. Código que usa funções internas quase sempre depende de clausuras para operar corretamente. Para entender as clausuras, precisamos dar um passo atrás e revisar como o escopo de variáveis funciona no Python.
9.5. Regras de escopo de variáveis
No Exemplo 149, definimos e testamos uma função que lê duas variáveis:
uma variável local a
—definida como parâmetro de função—e a variável b
, que não é definida em lugar algum na função.
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined
O erro obtido não é surpreendente. Continuando do Exemplo 149, se atribuirmos um valor a um b
global e então chamarmos f1
, funciona:
>>> b = 6
>>> f1(3)
3
6
Agora vamos ver um exemplo que pode ser surpreendente.
Dê uma olhada na função f2
, no Exemplo 150. As primeiras duas linhas são as mesmas da f1
do Exemplo 149, e então ela faz uma atribuição a b
. Mas para com um erro no segundo print
, antes da atribuição ser executada.
b
é local, porque um valor é atribuído a ela no corpo da função>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
Observe que o a saída começa com 3
, provando que o comando print(a)
foi executado. Mas o segundo, print(b)
, nunca roda. Quando vi isso pela primeira vez me espantei, pensava que o 6
deveria ser exibido, pois há uma variável global b
, e a atribuição para a b
local ocorre após print(b)
.
Mas o fato é que, quando Python compila o corpo da função, ele decide que b
é uma variável local, por ser atribuída dentro da função. O bytecode gerado reflete essa decisão, e tentará obter b
no escopo local. Mais tarde, quando a chamada f2(3)
é realizada, o corpo de f2
obtém e exibe o valor da variável local a
, mas ao tentar obter o valor da variável local b
, descobre que b
não está vinculado a nada.
Isso não é um bug, mas uma escolha de projeto: Python não exige que você declare variáveis, mas assume que uma variável atribuída no corpo de uma função é local. Isso é muito melhor que o comportamento de Javascript, que também não requer declarações de variáveis, mas se você esquecer de declarar uma variável como local (com var
), pode acabar alterando uma variável global sem nem saber.
Se queremos que o interpretador trate b
como uma variável global e também atribuir um novo valor a ela dentro da função, usamos a declaração global
:
>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
Nos exemplos anteriores, vimos dois escopos em ação:
- O escopo global de módulo
-
Composto por nomes atribuídos a valores fora de qualquer bloco de classe ou função.
- O escopo local da função f3
-
Composto por nomes atribuídos a valores como parâmetros, ou diretamente no corpo da função.
Há um outro escopo de onde variáveis podem vir, chamado nonlocal, e ele é fundamental para clausuras; vamos tratar disso em breve.
Após ver mais de perto como o escopo de variáveis funciona no Python, podemos enfrentar as clausuras na próxima seção, Seção 9.6. Se você tiver curiosidade sobre as diferenças no bytecode das funções no Exemplo 149 e no Exemplo 150, veja o quadro a seguir.
9.6. Clausuras
Na blogosfera, as clausuras são algumas vezes confundidas com funções anônimas. Muita gente confunde os dois conceitos por causa da história paralela dos dois recursos: definir funções dentro de outras funções não é tão comum ou conveniente, até existirem funções anônimas. E clausuras só importam a partir do momento em que você tem funções aninhadas. Daí que muitos aprendem as duas ideias ao mesmo tempo.
Na verdade, uma clausura é uma função—vamos chamá-la de f
—com um escopo estendido, incorporando variáveis referenciadas no corpo de f
que não são nem variáveis globais nem variáveis locais de f
. Tais variáveis devem vir do escopo local de uma função externa que englobe f
.
Não interessa aqui se a função é anônima ou não; o que importa é que ela pode acessar variáveis não-globais definidas fora de seu corpo.
É um conceito difícil de entender, melhor ilustrado por um exemplo.
Imagine uma função avg
, para calcular a média de uma série de valores que cresce continuamente; por exemplo, o preço de fechamento de uma commodity através de toda a sua história. A cada dia, um novo preço é acrescentado, e a média é computada levando em conta todos os preços até ali.
Começando do zero, avg
poderia ser usada assim:
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
Da onde vem avg
, e onde ela mantém o histórico com os valores anteriores?
Para começar, o Exemplo 153 mostra uma implementação baseada em uma classe.
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
A classe Averager
cria instâncias invocáveis:
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
O Exemplo 154, a seguir, é uma implementação funcional, usando a função de ordem superior make_averager
.
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
Quando invocada, make_averager
devolve um objeto função averager
. Cada vez que um averager
é invocado, ele insere o argumento recebido na série, e calcula a média atual, como mostra o Exemplo 155.
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(15)
12.0
Note as semelhanças entre os dois exemplos: chamamos Averager()
ou make_averager()
para obter um objeto invocável avg
, que atualizará a série histórica e calculará a média atual. No Exemplo 153, avg
é uma instância de Averager
, no Exemplo 154 é a função interna averager
. Nos dois casos, basta chamar avg(n)
para incluir n
na série e obter a média atualizada.
É óbvio onde o avg
da classe Averager
mantém o histórico: no atributo de instância self.series
. Mas onde a função avg
no segundo exemplo encontra a series
?
Observe que series
é uma variável local de make_averager
, pois a atribuição series = []
acontece no corpo daquela função. Mas quando avg(10)
é chamada, make_averager
já retornou, e seu escopo local há muito deixou de existir.
Dentro de averager
, series
é uma variável livre. Esse é um termo técnico para designar uma variável que não está vinculada no escopo local. Veja a Figura 23.
averager
estende o escopo daquela função para incluir a vinculação da variável livre series
.Inspecionar o objeto averager
devolvido mostra como Python mantém os nomes de variáveis locais e livres no atributo __code__
, que representa o corpo compilado da função. O Exemplo 156 demonstra isso.
make_averager
no Exemplo 154>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
O valor de series
é mantido no atributo __closure__
da função devolvida, avg
. Cada item em avg.__closure__
corresponde a um nome em __code__
. Esses itens são cells
, e tem um atributo chamado cell_contents
, onde o valor real pode ser encontrado. O Exemplo 157 mostra esses atributos.
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
Resumindo: uma clausura é uma função que retém os vínculos das variáveis livres que existem quando a função é definida, de forma que elas possam ser usadas mais tarde, quando a função for invocada mas o escopo de sua definição não estiver mais disponível.
Note que a única situação na qual uma função pode ter de lidar com variáveis externas não-globais é quando ela estiver aninhada dentro de outra função, e aquelas variáveis sejam parte do escopo local da função externa.
9.7. A declaração nonlocal
Nossa implementação anterior de make_averager
não era eficiente. No Exemplo 154, armazenamos todos os valores na série histórica e calculamos sua sum
cada vez que averager
é invocada. Uma implementação melhor armazenaria apenas o total e número de itens até aquele momento, e calcularia a média com esses dois números.
O Exemplo 158 é uma implementação errada, apenas para ilustrar um ponto. Você consegue ver onde o código quebra?
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
Se você testar o Exemplo 158, eis o resultado:
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>
O problema é que a instrução count += 1
significa o mesmo que count = count + 1
, quando count
é um número ou qualquer tipo imutável. Então estamos efetivamente atribuindo um valor a count
no corpo de averager
, e isso a torna uma variável local. O mesmo problema afeta a variável total
.
Não tivemos esse problema no Exemplo 154, porque nunca atribuimos nada ao nome series
; apenas chamamos series.append
e invocamos sum
e len
nele. Nos valemos, então, do fato de listas serem mutáveis.
Mas com tipos imutáveis, como números, strings, tuplas, etc., só é possível ler, nunca atualizar. Se você tentar revinculá-las, como em count = count + 1
, estará criando implicitamente uma variável local count
. Ela não será mais uma variável livre, e assim não será armazenada na clausura.
A palavra reservada nonlocal
foi introduzida no Python 3 para contornar esse problema. Ela permite declarar uma variável como variável livre, mesmo quando ela for atribuída dentro da função. Se um novo valor é atribuído a uma variável nonlocal
, o vínculo armazenado na clausura é modificado. Uma implemetação correta da nossa última versão de make_averager
se pareceria com o Exemplo 159.
nonlocal
)def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
Após estudar o nonlocal
, podemos resumir como a consulta de variáveis funciona no Python.
9.7.1. A lógica da consulta de variáveis
Quando uma função é definida, o compilador de bytecode de Python determina como encontrar uma variável x
que aparece na função, baseado nas seguintes regras:[106]
-
Se há uma declaração
global x
,x
vem de e é atribuída à variável globalx
do módulo.[107] -
Se há uma declaração
nonlocal x
,x
vem de e atribuída à variável localx
na função circundante mais próxima de ondex
for definida. -
Se
x
é um parâmetro ou tem um valor atribuído a si no corpo da função, entãox
é uma variável local. -
Se
x
é referenciada mas não atribuída, e não é um parâmetro:-
x
será procurada nos escopos locais do corpos das funções circundantes (os escopos nonlocal). -
Se
x
não for encontrada nos escopos circundantes, será lida do escopo global do módulo. -
Se
x
não for encontrada no escopo global, será lida de__builtins__.__dict__
.
-
Tendo visto as clausuras de Python, podemos agora de fato implementar decoradores com funções aninhadas.
9.8. Implementando um decorador simples
O Exemplo 160 é um decorador que cronometra cada invocação da função decorada e exibe o tempo decorrido, os argumentos passados, e o resultado da chamada.
import time
def clock(func):
def clocked(*args): # (1)
t0 = time.perf_counter()
result = func(*args) # (2)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked # (3)
-
Define a função interna
clocked
para aceitar qualquer número de argumentos posicionais. -
Essa linha só funciona porque a clausura para
clocked
engloba a variável livrefunc
. -
Devolve a função interna para substituir a função decorada.
O Exemplo 161 demonstra o uso do decorador clock
.
clock
import time
from clockdeco0 import clock
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
O resultado da execução do Exemplo 161 é o seguinte:
$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12363791s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000095s] factorial(1) -> 1
[0.00002408s] factorial(2) -> 2
[0.00003934s] factorial(3) -> 6
[0.00005221s] factorial(4) -> 24
[0.00006390s] factorial(5) -> 120
[0.00008297s] factorial(6) -> 720
6! = 720
9.8.1. Como isso funciona
Lembre-se que esse código:
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
na verdade faz isso:
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)
Então, nos dois exemplos, clock
recebe a função factorial
como seu argumento func
(veja o Exemplo 160).
Ela então cria e devolve a função clocked
, que o interpretador Python atribui a factorial
(no primeiro exemplo, por baixo dos panos).
De fato, se você importar o módulo clockdeco_demo
e verificar o __name__
de factorial
, verá isso:
>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'clocked'
>>>
Então factorial
agora mantém uma referência para a função clocked
.
Daqui por diante, cada vez que factorial(n)
for chamada, clocked(n)
será executada.
Essencialmente, clocked
faz o seguinte:
-
Registra o tempo inicial
t0
. -
Chama a função
factorial
original, salvando o resultado. -
Computa o tempo decorrido.
-
Formata e exibe os dados coletados.
-
Devolve o resultado salvo no passo 2.
Esse é o comportamento típico de um decorador: ele substitui a função decorada com uma nova função que aceita os mesmos argumentos e (normalmente) devolve o que quer que a função decorada deveria devolver, enquanto realiza também algum processamento adicional.
👉 Dica
|
Em Padrões de Projetos, de Gamma et al., a descrição curta do padrão decorador começa com: "Atribui dinamicamente responsabilidades adicionais a um objeto." Decoradores de função se encaixam nessa descrição. Mas, no nível da implementação, os decoradores de Python guardam pouca semelhança com o decorador clássico descrito no Padrões de Projetos original. O Ponto de vista fala um pouco mais sobre esse assunto. |
O decorador clock
implementado no Exemplo 160 tem alguns defeitos: ele não suporta argumentos nomeados, e encobre o __name__
e o __doc__
da função decorada.
O Exemplo 162 usa o decorador functools.wraps
para copiar os atributos relevantes de func
para clocked
.
E nessa nova versão os argumentos nomeados também são tratados corretamente.
clock
melhoraimport time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = [repr(arg) for arg in args]
arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
arg_str = ', '.join(arg_lst)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked
O functools.wraps
é apenas um dos decoradores prontos para uso da biblioteca padrão. Na próxima seção veremos o decorador mais impressionante oferecido por functools
: cache
.
9.9. Decoradores na biblioteca padrão
Python tem três funções embutidas projetadas para decorar métodos:
property
, classmethod
e staticmethod
.
Vamos discutir property
na Seção 22.4 e os outros na Seção 11.5.
No Exemplo 162 vimos outro decorador importante: functools.wraps
, um auxiliar na criação de decoradores bem comportados.
Três dos decoradores mais interessantes da biblioteca padrão são cache
, lru_cache
e singledispatch
—todos do módulo functools
. Falaremos deles a seguir.
9.9.1. Memoização com functools.cache
O decorador functools.cache
implementa memoização:[108]
uma técnica de otimização que funciona salvando os resultados de invocações anteriores de uma função dispendiosa, evitando repetir o processamento para argumentos previamente utilizados.
👉 Dica
|
O |
Uma boa demonstração é aplicar @cache
à função recursiva, e dolorosamente lenta, que gera o enésimo número da sequência de Fibonacci, como mostra o Exemplo 163.
from clockdeco import clock
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
Aqui está o resultado da execução de fibo_demo.py. Exceto pela última linha, toda a saída é produzida pelo decorador clock
:
$ python3 fibo_demo.py
[0.00000042s] fibonacci(0) -> 0
[0.00000049s] fibonacci(1) -> 1
[0.00006115s] fibonacci(2) -> 1
[0.00000031s] fibonacci(1) -> 1
[0.00000035s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00001084s] fibonacci(2) -> 1
[0.00002074s] fibonacci(3) -> 2
[0.00009189s] fibonacci(4) -> 3
[0.00000029s] fibonacci(1) -> 1
[0.00000027s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000959s] fibonacci(2) -> 1
[0.00001905s] fibonacci(3) -> 2
[0.00000026s] fibonacci(0) -> 0
[0.00000029s] fibonacci(1) -> 1
[0.00000997s] fibonacci(2) -> 1
[0.00000028s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000031s] fibonacci(1) -> 1
[0.00001019s] fibonacci(2) -> 1
[0.00001967s] fibonacci(3) -> 2
[0.00003876s] fibonacci(4) -> 3
[0.00006670s] fibonacci(5) -> 5
[0.00016852s] fibonacci(6) -> 8
8
O desperdício é óbvio: fibonacci(1)
é chamada oito vezes, fibonacci(2)
cinco vezes, etc.
Mas acrescentar apenas duas linhas, para usar cache
, melhora muito o desempenho. Veja o Exemplo 164.
import functools
from clockdeco import clock
@functools.cache # (1)
@clock # (2)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
print(fibonacci(6))
-
Essa linha funciona com Python 3.9 ou posterior. Veja a Seção 9.9.2 para uma alternativa que suporta versões anteriores de Python.
-
Esse é um exemplo de decoradores empilhados:
@cache
é aplicado à função devolvida por@clock
.
👉 Dica
|
Decoradore empilhados
Para entender os decoradores empilhados, lembre-se que a
é o mesmo que isso:
Em outras palavras, o decorador |
Usando o cache
no Exemplo 164, a função fibonacci
é chamada apenas uma vez para cada valor de n
:
$ python3 fibo_demo_lru.py
[0.00000043s] fibonacci(0) -> 0
[0.00000054s] fibonacci(1) -> 1
[0.00006179s] fibonacci(2) -> 1
[0.00000070s] fibonacci(3) -> 2
[0.00007366s] fibonacci(4) -> 3
[0.00000057s] fibonacci(5) -> 5
[0.00008479s] fibonacci(6) -> 8
8
Em outro teste, para calcular fibonacci(30)
, o Exemplo 164 fez as 31 chamadas necessárias em 0,00017s (tempo total), enquanto o Exemplo 163 sem cache, demorou 12,09s em um notebook Intel Core i7, porque chamou fibonacci(1)
832.040 vezes, para um total de 2.692.537 chamadas.
Todos os argumentos recebidos pela função decorada devem ser hashable, pois o lru_cache
subjacente usa um dict
para armazenar os resultados, e as chaves são criadas a partir dos argumentos posicionais e nomeados usados nas chamados.
Além de tornar viáveis esses algoritmos recursivos tolos, @cache
brilha de verdade em aplicações que precisam buscar informações de APIs remotas.
⚠️ Aviso
|
O |
9.9.2. Usando o lru_cache
O decorador functools.cache
é, na realidade, um mero invólucro em torno da antiga função functools.lru_cache
, que é mais flexível e também compatível com Python 3.8 e outras versões anteriores.
A maior vantagem de @lru_cache
é a possibilidade de limitar seu uso de memória através do parâmetro maxsize
, que tem um default bastante conservador de 128—significando que o cache pode manter no máximo 128 registros simultâneos.
LRU é a sigla de Least Recently Used (literalmente "Usado Menos Recentemente"). Significa que registros que há algum tempo não são lidos, são descartados para dar lugar a novos itens.
Desde Python 3.8, lru_cache
pode ser aplicado de duas formas.
Abaixo vemos o modo mais simples em uso:
@lru_cache
def costly_function(a, b):
...
A outra forma—disponível desde Python 3.2—é invocá-lo como uma função,
com ()
:
@lru_cache()
def costly_function(a, b):
...
Nos dois casos, os parâmetros default seriam utilizados. São eles:
maxsize=128
-
Estabelece o número máximo de registros a serem armazenados. Após o cache estar cheio, o registro menos recentemente usado é descartado, para dar lugar a cada novo item. Para um desempenho ótimo,
maxsize
deve ser uma potência de 2. Se você passarmaxsize=None
, a lógica LRU é desabilitada e o cache funciona mais rápido, mas os itens nunca são descartados, podendo levar a um consumo excessivo de memória. É assim que o@functools.cache
funciona. typed=False
-
Determina se os resultados de diferentes tipos de argumentos devem ser armazenados separadamente, Por exemplo, na configuração default, argumentos inteiros e de ponto flutuante considerados iguais são armazenados apenas uma vez. Assim, haverá apenas uma entrada para as chamadas
f(1)
ef(1.0)
. Setyped=True
, aqueles argumentos produziriam registros diferentes, possivelmente armazenando resultados distintos.
Eis um exemplo de invocação de @lru_cache
com parâmetros diferentes dos defaults:
@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
...
Vamos agora examinar outro decorador poderoso: functools.singledispatch
.
9.9.3. Funções genéricas com despacho único
Imagine que estamos criando uma ferramenta para depurar aplicações web. Queremos gerar código HTML para tipos diferentes de objetos Python.
Poderíamos começar com uma função como essa:
import html
def htmlize(obj):
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
Isso funcionará para qualquer tipo de Python, mas agora queremos estender a função para gerar HTML específico para determinados tipos. Alguns exemplos seriam:
str
-
Substituir os caracteres de mudança de linha na string por
'<br/>\n'
e usar tags<p>
tags em vez de<pre>
. int
-
Mostrar o número em formato decimal e hexadecimal (com um caso especial para
bool
). list
-
Gerar uma lista em HTML, formatando cada item de acordo com seu tipo.
float
eDecimal
-
Mostrar o valor como de costume, mas também na forma de fração (por que não?).
O comportamento que desejamos aparece no Exemplo 165.
htmlize()
gera HTML adaptado para diferentes tipos de objetos>>> htmlize({1, 2, 3}) # (1)
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre><built-in function abs></pre>'
>>> htmlize('Heimlich & Co.\n- a game') # (2)
'<p>Heimlich & Co.<br/>\n- a game</p>'
>>> htmlize(42) # (3)
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}])) # (4)
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
>>> htmlize(True) # (5)
'<pre>True</pre>'
>>> htmlize(fractions.Fraction(2, 3)) # (6)
'<pre>2/3</pre>'
>>> htmlize(2/3) # (7)
'<pre>0.6666666666666666 (2/3)</pre>'
>>> htmlize(decimal.Decimal('0.02380952'))
'<pre>0.02380952 (1/42)</pre>'
-
A função original é registrada para
object
, então ela serve para capturar e tratar todos os tipos de argumentos que não foram capturados pelas outras implementações. -
Objetos
str
também passam por escape de HTML, mas são cercados por<p></p>
, com quebras de linha<br/>
inseridas antes de cada'\n'
. -
Um
int
é exibido nos formatos decimal e hexadecimal, dentro de um bloco<pre></pre>
. -
Cada item na lista é formatado de acordo com seu tipo, e a sequência inteira é apresentada como uma lista HTML.
-
Apesar de ser um subtipo de
int
,bool
recebe um tratamento especial. -
Mostra
Fraction
como uma fração. -
Mostra
float
eDecimal
com a fração equivalemte aproximada.
9.9.3.1. Despacho único de funções
Como não temos no Python a sobrecarga de métodos ao estilo de Java, não podemos simplesmente criar variações de htmlize
com assinaturas diferentes para cada tipo de dado que queremos tratar de forma distinta. Uma solução possível em Python seria transformar htmlize
em uma função de despacho, com uma cadeia de if/elif/…
ou match/case/…
chamando funções especializadas como htmlize_str
, htmlize_int
, etc.
Isso não é extensível pelos usuários de nosso módulo, e é desajeitado:
com o tempo, a despachante htmlize
de tornaria grande demais, e o acoplamento entre ela e as funções especializadas seria excessivamente sólido.
O decorador functools.singledispatch
permite que diferentes módulos contribuam para a solução geral, e que você forneça facilmente funções especializadas, mesmo para tipos pertencentes a pacotes externos que não possam ser editados.
Se você decorar um função simples com @singledispatch
, ela se torna o ponto de entrada para uma função genérica:
Um grupo de funções que executam a mesma operação de formas diferentes, dependendo do tipo do primeiro argumento.
É isso que signifca o termo despacho único. Se mais argumentos fossem usados para selecionar a função específica, teríamos um despacho múltiplo.
O Exemplo 166 mostra como funciona.
⚠️ Aviso
|
|
@singledispatch
cria uma @htmlize.register
personalizada, para empacotar várias funções em uma função genéricafrom functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
@singledispatch # (1)
def htmlize(obj: object) -> str:
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
@htmlize.register # (2)
def _(text: str) -> str: # (3)
content = html.escape(text).replace('\n', '<br/>\n')
return f'<p>{content}</p>'
@htmlize.register # (4)
def _(seq: abc.Sequence) -> str:
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
@htmlize.register # (5)
def _(n: numbers.Integral) -> str:
return f'<pre>{n} (0x{n:x})</pre>'
@htmlize.register # (6)
def _(n: bool) -> str:
return f'<pre>{n}</pre>'
@htmlize.register(fractions.Fraction) # (7)
def _(x) -> str:
frac = fractions.Fraction(x)
return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
@htmlize.register(decimal.Decimal) # (8)
@htmlize.register(float)
def _(x) -> str:
frac = fractions.Fraction(x).limit_denominator()
return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'
-
@singledispatch
marca a função base, que trata o tipoobject
. -
Cada função especializada é decorada com
@«base».register
. -
O tipo do primeiro argumento passado durante a execução determina quando essa definição de função em particular será utilizada. O nome das funções especializadas é irrelevante;
_
é uma boa escolha para deixar isso claro.[109] -
Registra uma nova função para cada tipo que precisa de tratamento especial, com uma dica de tipo correspondente no primeiro parâmetro.
-
As ABCs em
numbers
são úteis para uso em conjunto comsingledispatch
.[110] -
bool
é um subtipo-denumbers.Integral
, mas a lógica desingledispatch
busca a implementação com o tipo correspondente mais específico, independente da ordem na qual eles aparecem no código. -
Se você não quiser ou não puder adicionar dicas de tipo à função decorada, você pode passar o tipo para o decorador
@«base».register
. Essa sintaxe funciona em Python 3.4 ou posterior. -
O decorador
@«base».register
devolve a função sem decoração, então é possível empilhá-los para registrar dois ou mais tipos na mesma implementação.[111]
Sempre que possível, registre as funções especializadas para tratar ABCs (classes abstratas), tais como numbers.Integral
e abc.MutableSequence
, ao invés das implementações concretas como int
e list
.
Isso permite ao seu código suportar uma variedade maior de tipos compatíveis.
Por exemplo, uma extensão de Python pode fornecer alternativas para o tipo int
com número fixo de bits como subclasses de numbers.Integral
.[112]
👉 Dica
|
Usar ABCs ou |
Uma qualidade notável do mecanismo de singledispatch
é que você pode registrar funções especializadas em qualquer lugar do sistema, em qualquer módulo. Se mais tarde você adicionar um módulo com um novo tipo definido pelo usuário, é fácil acrescentar uma nova função específica para tratar aquele tipo. E é possível também escrever funções personalizadas para classes que você não escreveu e não pode modificar.
O singledispatch
foi uma adição muito bem pensada à biblioteca padrão, e oferece muitos outros recursos que não me cabe descrever aqui. Uma boa referência é a PEP 443—Single-dispatch generic functions (EN) mas ela não menciona o uso de dicas de tipo, acrescentado posteriormente. A documentação do módulo functools
foi aperfeiçoada e oferece um tratamento mais atualizado, com vários exemplos na seção referente ao singledispatch
(EN).
✒️ Nota
|
O |
Vimos alguns decoradores recebendo argumentos, por exemplo @lru_cache()
e o
htmlize.register(float)
criado por @singledispatch
no Exemplo 166.
A próxima seção mostra como criar decoradores que aceitam parâmetros.
9.10. Decoradores parametrizados
Ao analisar um decorador no código-fonte, Python passa a função decorada como primeiro argumento para a função do decorador. Mas como fazemos um decorador aceitar outros argumentos? A resposta é: criar uma fábrica de decoradores que recebe aqueles argumentos e devolve um decorador, que é então aplicado à função a ser decorada. Confuso? Com certeza. Vamos começar com um exemplo baseado no decorador mais simples que vimos: register
no Exemplo 167.
registry = []
def register(func):
print(f'running register({func})')
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
9.10.1. Um decorador de registro parametrizado
Para tornar mais fácil a habilitação ou desabilitação do registro executado por register
, faremos esse último aceitar um parâmetro opcional active
que, se False
, não registra a função decorada. Conceitualmente, a nova função register
não é um decorador mas uma fábrica de decoradores. Quando chamada, ela devolve o decorador que será realmente aplicado à função alvo.
register
precisa ser invocado como uma funçãoregistry = set() # (1)
def register(active=True): # (2)
def decorate(func): # (3)
print('running register'
f'(active={active})->decorate({func})')
if active: # (4)
registry.add(func)
else:
registry.discard(func) # (5)
return func # (6)
return decorate # (7)
@register(active=False) # (8)
def f1():
print('running f1()')
@register() # (9)
def f2():
print('running f2()')
def f3():
print('running f3()')
-
registry
é agora umset
, tornando mais rápido acrescentar ou remover funções. -
register
recebe um argumento nomeado opcional. -
A função interna
decorate
é o verdadeiro decorador; observe como ela aceita uma função como argumento. -
Registra
func
apenas se o argumentoactive
(obtido da clausura) forTrue
. -
Se
not active
efunc in registry
, remove a função. -
Como
decorate
é um decorador, ele deve devolver uma função. -
register
é nossa fábrica de decoradores, então devolvedecorate
. -
A fábrica
@register
precisa ser invocada como uma função, com os parâmetros desejados. -
Mesmo se nenhum parâmetro for passado, ainda assim
register
deve ser chamado como uma função—@register()
—isto é, para devolver o verdadeiro decorador,decorate
.
O ponto central aqui é que register()
devolve decorate
, que então é aplicado à função decorada.
O código do Exemplo 168 está em um módulo registration_param.py. Se o importarmos, veremos o seguinte:
>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x10063c1e0>)
running register(active=True)->decorate(<function f2 at 0x10063c268>)
>>> registration_param.registry
[<function f2 at 0x10063c268>]
Veja como apenas a função f2
aparece no registry
; f1
não aparece porque active=False
foi passado para a fábrica de decoradores register
, então o decorate
aplicado a f1
não adiciona essa função a registry
.
Se, ao invés de usar a sintaxe @
, usarmos register
como uma função regular, a sintaxe necessária para decorar uma função f
seria register()(f)
, para inserir f
ao registry
, ou register(active=False)(f)
, para não inseri-la (ou removê-la). Veja o Exemplo 169 para uma demonstração da adição e remoção de funções do registry
.
>>> from registration_param import *
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry # (1)
{<function f2 at 0x10073c268>}
>>> register()(f3) # (2)
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry # (3)
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2) # (4)
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry # (5)
{<function f3 at 0x10073c158>}
-
Quando o módulo é importado,
f2
é inserida noregistry
. -
A expressão
register()
devolvedecorate
, que então é aplicado af3
. -
A linha anterior adicionou
f3
aoregistry
. -
Essa chamada remove
f2
doregistry
. -
Confirma que apenas
f3
permanece noregistry
.
O funcionamento de decoradores parametrizados é bastante complexo, e esse que acabamos de discutir é mais simples que a maioria. Decoradores parametrizados em geral substituem a função decorada, e sua construção exige um nível adicional de aninhamento. Vamos agora explorar a arquitetura de uma dessas pirâmides de funções.
9.10.2. Um decorador parametrizado de cronometragem
Nessa seção vamos revisitar o decorador clock
, acrescentando um recurso: os usuários podem passar uma string de formatação, para controlar a saída do relatório sobre função cronometrada. Veja o Exemplo 170.
✒️ Nota
|
Para simplificar, o Exemplo 170 está baseado na implementação inicial de |
clock
parametrizadoimport time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # (1)
def decorate(func): # (2)
def clocked(*_args): # (3)
t0 = time.perf_counter()
_result = func(*_args) # (4)
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # (5)
result = repr(_result) # (6)
print(fmt.format(**locals())) # (7)
return _result # (8)
return clocked # (9)
return decorate # (10)
if __name__ == '__main__':
@clock() # (11)
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
-
clock
é a nossa fábrica de decoradores parametrizados. -
decorate
é o verdadeiro decorador. -
clocked
envolve a função decorada. -
_result
é o resultado efetivo da função decorada. -
_args
mantém os verdadeiros argumentos declocked
, enquantoargs
é astr
usada para exibição. -
result
é astr
que representa_result
, para exibição. -
Usar
**locals()
aqui permite que qualquer variável local declocked
seja referenciada emfmt
.[113] -
clocked
vai substituir a função decorada, então ela deve devolver o mesmo que aquela função devolve. -
decorate
devolveclocked
. -
clock
devolvedecorate
. -
Nesse auto-teste,
clock()
é chamado sem argumentos, então o decorador aplicado usará o formato default,str
.
Se você rodar o Exemplo 170 no console, o resultado é o seguinte:
$ python3 clockdeco_param.py
[0.12412500s] snooze(0.123) -> None
[0.12411904s] snooze(0.123) -> None
[0.12410498s] snooze(0.123) -> None
Para exercitar a nova funcionalidade, vamos dar uma olhada em dois outros módulos que usam o clockdeco_param
, o Exemplo 171 e o Exemplo 172, e nas saídas que eles geram.
import time
from clockdeco_param import clock
@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
Saída do Exemplo 171:
$ python3 clockdeco_param_demo1.py
snooze: 0.12414693832397461s
snooze: 0.1241159439086914s
snooze: 0.12412118911743164s
import time
from clockdeco_param import clock
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
Saída do Exemplo 172:
$ python3 clockdeco_param_demo2.py
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
snooze(0.123) dt=0.124s
✒️ Nota
|
Lennart Regebro—um dos revisores técnicos da primeira edição—argumenta seria melhor programar decoradores como classes implementando |
A próxima seção traz um exemplo no estilo recomendado por Regebro e Dumpleton.
9.10.3. Um decorador de cronometragem em forma de classe
Como um último exemplo, o Exemplo 173 mostra a implementação de um decorador parametrizado clock
, programado como uma classe com __call__
.
Compare o Exemplo 170 com o Exemplo 173.
Qual você prefere?
clock
, implementado como uma classeimport time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
class clock: # (1)
def __init__(self, fmt=DEFAULT_FMT): # (2)
self.fmt = fmt
def __call__(self, func): # (3)
def clocked(*_args):
t0 = time.perf_counter()
_result = func(*_args) # (4)
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(self.fmt.format(**locals()))
return _result
return clocked
-
Ao invés de uma função externa
clock
, a classeclock
é nossa fábrica de decoradores parametrizados. A nomeei com umc
minúsculo, para deixar claro que essa implementação é uma substituta direta para aquela no Exemplo 170. -
O argumento passado em
clock(my_format)
é atribuído ao parâmetrofmt
aqui. O construtor da classe devolve uma instância declock
, commy_format
armazenado emself.fmt
. -
__call__
torna a instância declock
invocável. Quando chamada, a instância substitui a função decorada comclocked
. -
clocked
envolve a função decorada.
Isso encerra nossa exploração dos decoradores de função. Veremos os decoradores de classe no Capítulo 24.
9.11. Resumo do capítulo
Percorremos um terreno acidentado nesse capítulo. Tentei tornar a jornada tão suave quanto possível, mas entramos definitivamente nos domínios da meta-programação.
Partimos de um decorador simples @register
, sem uma função interna, e terminamos com um @clock()
parametrizado envolvendo dois níveis de funções aninhadas.
Decoradores de registro, apesar de serem essencialmente simples, tem aplicações reais nos frameworks Python. Vamos aplicar a ideia de registro em uma implementação do padrão de projeto Estratégia, no Capítulo 10.
Entender como os decoradores realmente funcionam exigiu falar da diferença entre tempo de importação e tempo de execução. Então mergulhamos no escopo de variáveis, clausuras e a nova declaração nonlocal
. Dominar as clausuras e nonlocal
é valioso não apenas para criar decoradores, mas também para escrever programas orientados a eventos para GUIs ou E/S assíncrona com callbacks, e para adotar um estilo funcional quando fizer sentido.
Decoradores parametrizados quase sempre implicam em pelo menos dois níveis de funções aninhadas, talvez mais se você quiser usar @functools.wraps
, e produzir um decorador com um suporte melhor a técnicas mais avançadas. Uma dessas técnicas é o empilhamento de decoradores, que vimos no Exemplo 164. Para decoradores mais sofisticados, uma implementação baseada em classes pode ser mais fácil de ler e manter.
Como exemplos de decoradores parametrizados na biblioteca padrão, visitamos os poderosos @cache
e @singledispatch
, do módulo functools
.
9.12. Leitura complementar
O item #26 do livro Effective Python, 2nd ed. (EN) (Addison-Wesley), de Brett Slatkin, trata das melhores práticas para decoradores de função, e recomenda sempre usar functools.wraps
—que vimos no Exemplo 162.[114]
Graham Dumpleton tem, em seu blog, uma série de posts abrangentes (EN) sobre técnicas para implementar decoradores bem comportados, começando com "How you implemented your Python decorator is wrong" (A forma como você implementou seu decorador em Python está errada). Seus conhecimentos profundos sobre o assunto também estão muito bem demonstrados no módulo wrapt
, que Dumpleton escreveu para simplificar a implementação de decoradores e invólucros (wrappers) dinâmicos de função, que suportam introspecção e se comportam de forma correta quando decorados novamente, quando aplicados a métodos e quando usados como descritores de atributos. O Capítulo 23 na Parte III: Classes e protocolos é sobre descritores.
"Metaprogramming" (Metaprogramação) (EN), o capítulo 9 do Python Cookbook, 3ª ed. de David Beazley e Brian K. Jones (O’Reilly), tem várias receitas ilustrando desde decoradores elementares até alguns muito sofisticados, incluindo um que pode ser invocado como um decorador regular ou como uma fábrica de decoradores, por exemplo, @clock
ou @clock()
. É a "Recipe 9.6. Defining a Decorator That Takes an Optional Argument" (Receita 9.6. Definindo um Decorador Que Recebe um Argumento Opcional) desse livro de receitas.
Michele Simionato criou um pacote com objetivo de "simplificar o uso de decoradores para o programador comum, e popularizar os decoradores através da apresentação de vários exemplos não-triviais", de acordo com a documentação. Ele está disponível no PyPI, em decorator package (pacote decorador) (EN).
Criada quando os decoradores ainda eram um recurso novo no Python, a página wiki Python Decorator Library (EN) tem dezenas de exemplos. Como começou há muitos anos, algumas das técnicas apresentadas foram suplantadas, mas ela ainda é uma excelente fonte de inspiração.
"Closures in Python" (Clausuras em Python) (EN) é um post de blog curto de Fredrik Lundh, explicando a terminologia das clausuras.
A PEP 3104—Access to Names in Outer Scopes (Acesso a Nomes em Escopos Externos) (EN) descreve a introdução da declaração nonlocal
, para permitir a re-vinculação de nomes que não são nem locais nem globais. Ela também inclui uma excelente revisão de como essa questão foi resolvida em outras linguagens dinâmicas (Perl, Ruby, JavaScript, etc.) e os prós e contras das opções de design disponíveis para Python.
Em um nível mais teórico, a PEP 227—Statically Nested Scopes (Escopos Estaticamente Aninhados) (EN) documenta a introdução do escopo léxico como um opção no Python 2.1 e como padrão no Python 2.2, explicando a justificativa e as opções de design para a implementação de clausuras no Python.
A PEP 443 (EN) traz a justificativa e uma descrição detalhada do mecanismo de funções genéricas de despacho único. Um post de Guido van Rossum de março de 2005 "Five-Minute Multimethods in Python" (Multi-métodos em Python em Cinco Minutos) (EN), mostra os passos para uma implementação de funcões genéricas (também chamadas multi-métodos) usando decoradores. O código de multi-métodos de Guido é interessante, mas é apenas um exemplo didático. Para conhecer uma implementação de funções genéricas de despacho múltiplo moderna e pronta para uso em produção, veja a Reg de Martijn Faassen–autor de Morepath, um framework web guiado por modelos e orientado a REST.
10. Padrões de projetos com funções de primeira classe
Conformidade a padrões não é uma medida de virtude.[115]
co-autor do clássico "Padrões de Projetos"
Em engenharia de software, um padrão de projeto é uma receita genérica para solucionar um problema de design frequente. Não é preciso conhecer padrões de projeto para acompanhar esse capítulo, vou explicar os padrões usados nos exemplos.
O uso de padrões de projeto em programação foi popularizado pelo livro seminal Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos (Addison-Wesley), de Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides—também conhecidos como "the Gang of Four" (A Gangue dos Quatro). O livro é um catálogo de 23 padrões, cada um deles composto por arranjos de classes e exemplificados com código em C++, mas assumidos como úteis também em outras linguagens orientadas a objetos.
Apesar dos padrões de projeto serem independentes da linguagem, isso não significa que todo padrão se aplica a todas as linguagens. Por exemplo, o Capítulo 17 vai mostrar que não faz sentido emular a receita do padrão Iterator (Iterador) (EN) no Python, pois esse padrão está embutido na linguagem e pronto para ser usado, na forma de geradores—que não precisam de classes para funcionar, e exigem menos código que a receita clássica.
Os autores de Padrões de Projetos reconhecem, na introdução, que a linguagem usada na implementação determina quais padrões são relevantes:
A escolha da linguagem de programação é importante, pois ela influencia nosso ponto de vista. Nossos padrões supõe uma linguagem com recursos equivalentes aos de Smalltalk e do C++—e essa escolha determina o que pode e o que não pode ser facilmente implementado. Se tivéssemos presumido uma linguagem procedural, poderíamos ter incluído padrões de projetos chamados "Herança", "Encapsulamento" e "Polimorfismo". Da mesma forma, alguns de nossos padrões são suportados diretamente por linguagens orientadas a objetos menos conhecidas. CLOS, por exemplo, tem multi-métodos, reduzindo a necessidade de um padrão como o Visitante.[116]
Em sua apresentação de 1996, "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (EN), Peter Norvig afirma que 16 dos 23 padrões no Padrões de Projeto original se tornam "invisíveis ou mais simples" em uma linguagem dinâmica (slide 9). Ele está falando das linguagens Lisp e Dylan, mas muitos dos recursos dinâmicos relevantes também estão presentes no Python. Em especial, no contexto de linguagens com funções de primeira classe, Norvig sugere repensar os padrões clássicos conhecidos como Estratégia (Strategy), Comando (Command), Método Template (Template Method) e Visitante (Visitor).
O objetivo desse capítulo é mostrar como—em alguns casos—as funções podem realizar o mesmo trabalho das classes, com um código mais legível e mais conciso. Vamos refatorar uma implementaçao de Estratégia usando funções como objetos, removendo muito código redundante. Vamos também discutir uma abordagem similar para simplificar o padrão Comando.
10.1. Novidades nesse capítulo
Movi este capítulo para o final da Parte II, para poder então aplicar o decorador de registro na Seção 10.3, e também usar dicas de tipo nos exemplos. A maior parte das dicas de tipo usadas nesse capítulo não são complicadas, e ajudam na legibilidade.
10.2. Estudo de caso: refatorando Estratégia
Estratégia é um bom exemplo de um padrão de projeto que pode ser mais simples em Python, usando funções como objetos de primeira classe. Na próxima seção vamos descrever e implementar Estratégia usando a estrutura "clássica" descrita em Padrões de Projetos. Se você estiver familiarizado com o padrão clássico, pode pular direto para Seção 10.2.2, onde refatoramos o código usando funções, reduzindo significativamente o número de linhas.
10.2.1. Estratégia clássica
O diagrama de classes UML na Figura 24 retrata um arranjo de classes exemplificando o padrão Estratégia.
O padrão Estratégia é resumido assim em Padrões de Projetos:
Define uma família de algoritmos, encapsula cada um deles, e os torna intercambiáveis. Estratégia permite que o algoritmo varie de forma independente dos clientes que o usam.
Um exemplo claro de Estratégia, aplicado ao domínio do ecommerce, é o cálculo de descontos em pedidos de acordo com os atributos do cliente ou pela inspeção dos itens do pedido.
Considere uma loja online com as seguintes regras para descontos:
-
Clientes com 1.000 ou mais pontos de fidelidade recebem um desconto global de 5% por pedido.
-
Um desconto de 10% é aplicado a cada item com 20 ou mais unidades no mesmo pedido.
-
Pedidos com pelo menos 10 itens diferentes recebem um desconto global de 7%.
Para simplificar, vamos assumir que apenas um desconto pode ser aplicado a cada pedido.
O diagrama de classes UML para o padrão Estratégia aparece na Figura 24. Seus participantes são:
- Contexto (Context)
-
Oferece um serviço delegando parte do processamento para componentes intercambiáveis, que implementam algoritmos alternativos. No exemplo de ecommerce, o contexto é uma classe
Order
, configurada para aplicar um desconto promocional de acordo com um de vários algoritmos. - Estratégia (Strategy)
-
A interface comum dos componentes que implementam diferentes algoritmos. No nosso exemplo, esse papel cabe a uma classe abstrata chamada
Promotion
. - Estratégia concreta (Concrete strategy)
-
Cada uma das subclasses concretas de Estratégia.
FidelityPromo
,BulkPromo
, eLargeOrderPromo
são as três estratégias concretas implementadas.
O código no Exemplo 174 segue o modelo da Figura 24. Como descrito em Padrões de Projetos, a estratégia concreta é escolhida pelo cliente da classe de contexto. No nosso exemplo, antes de instanciar um pedido, o sistema deveria, de alguma forma, selecionar o estratégia de desconto promocional e passá-la para o construtor de Order
. A seleção da estratégia está fora do escopo do padrão.
Order
com estratégias de desconto intercambiáveisfrom abc import ABC, abstractmethod
from collections.abc import Sequence
from decimal import Decimal
from typing import NamedTuple, Optional
class Customer(NamedTuple):
name: str
fidelity: int
class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal
def total(self) -> Decimal:
return self.price * self.quantity
class Order(NamedTuple): # the Context
customer: Customer
cart: Sequence[LineItem]
promotion: Optional['Promotion'] = None
def total(self) -> Decimal:
totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))
def due(self) -> Decimal:
if self.promotion is None:
discount = Decimal(0)
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
class Promotion(ABC): # the Strategy: an abstract base class
@abstractmethod
def discount(self, order: Order) -> Decimal:
"""Return discount as a positive dollar amount"""
class FidelityPromo(Promotion): # first Concrete Strategy
"""5% discount for customers with 1000 or more fidelity points"""
def discount(self, order: Order) -> Decimal:
rate = Decimal('0.05')
if order.customer.fidelity >= 1000:
return order.total() * rate
return Decimal(0)
class BulkItemPromo(Promotion): # second Concrete Strategy
"""10% discount for each LineItem with 20 or more units"""
def discount(self, order: Order) -> Decimal:
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""
def discount(self, order: Order) -> Decimal:
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
Observe que no Exemplo 174, programei Promotion
como uma classe base abstrata (ABC), para usar o decorador @abstractmethod
e deixar o padrão mais explícito.
O Exemplo 175 apresenta os doctests usados para demonstrar e verificar a operação de um módulo implementando as regras descritas anteriormente.
Order
com a aplicação de diferentes promoções >>> joe = Customer('John Doe', 0) # (1)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = (LineItem('banana', 4, Decimal('.5')), # (2)
... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, Decimal(5)))
>>> Order(joe, cart, FidelityPromo()) # (3)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo()) # (4)
<Order total: 42.00 due: 39.90>
>>> banana_cart = (LineItem('banana', 30, Decimal('.5')), # (5)
... LineItem('apple', 10, Decimal('1.5')))
>>> Order(joe, banana_cart, BulkItemPromo()) # (6)
<Order total: 30.00 due: 28.50>
>>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) # (7)
... for sku in range(10))
>>> Order(joe, long_cart, LargeOrderPromo()) # (8)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>
-
Dois clientes:
joe
tem 0 pontos de fidelidade,ann
tem 1.100. -
Um carrinho de compras com três itens.
-
A promoção
FidelityPromo
não dá qualquer desconto parajoe
. -
ann
recebe um desconto de 5% porque tem pelo menos 1.000 pontos. -
O
banana_cart
contém 30 unidade do produto"banana"
e 10 maçãs. -
Graças à
BulkItemPromo
,joe
recebe um desconto de $1,50 no preço das bananas. -
O
long_cart
tem 10 itens diferentes, cada um custando $1,00. -
joe
recebe um desconto de 7% no pedido total, por causa daLargerOrderPromo
.
O Exemplo 174 funciona perfeitamente bem, mas a mesma funcionalidade pode ser implementada com menos linhas de código em Python, se usarmos funções como objetos. Veremos como fazer isso na próxima seção.
10.2.2. Estratégia baseada em funções
Cada estratégia concreta no Exemplo 174 é uma classe com um único método, discount
.
Além disso, as instâncias de estratégia não tem nenhum estado (nenhum atributo de instância).
Você poderia dizer que elas se parecem muito com funções simples, e estaria certa.
O Exemplo 176 é uma refatoração do Exemplo 174,
substituindo as estratégias concretas por funções simples e removendo a classe abstrata Promo
.
São necessários apenas alguns pequenos ajustes na classe Order
.[117]
Order
com as estratégias de descontos implementadas como funçõesfrom collections.abc import Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, Callable, NamedTuple
class Customer(NamedTuple):
name: str
fidelity: int
class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal
def total(self):
return self.price * self.quantity
@dataclass(frozen=True)
class Order: # the Context
customer: Customer
cart: Sequence[LineItem]
promotion: Optional[Callable[['Order'], Decimal]] = None # (1)
def total(self) -> Decimal:
totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))
def due(self) -> Decimal:
if self.promotion is None:
discount = Decimal(0)
else:
discount = self.promotion(self) # (2)
return self.total() - discount
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# (3)
def fidelity_promo(order: Order) -> Decimal: # (4)
"""5% discount for customers with 1000 or more fidelity points"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
def bulk_item_promo(order: Order) -> Decimal:
"""10% discount for each LineItem with 20 or more units"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
def large_order_promo(order: Order) -> Decimal:
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
-
Essa dica de tipo diz:
promotion
pode serNone
, ou pode ser um invocável que recebe umaOrder
como argumento e devolve umDecimal
. -
Para calcular o desconto, chama o invocável
self.promotion
, passandoself
como um argumento. Veja a razão disso logo abaixo. -
Nenhuma classe abstrata.
-
Cada estratégia é uma função.
👉 Dica
|
Por que self.promotion(self)?
Na classe A Seção 23.4 vai explicar o mecanismo que vincula automaticamente métodos a instâncias. Mas isso não se aplica a |
O código no Exemplo 176 é mais curto que o do Exemplo 174. Usar a nova Order
é também um pouco mais simples, como mostram os doctests no Exemplo 177.
Order
com as promoções como funções >>> joe = Customer('John Doe', 0) # (1)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo) # (2)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo) # (3)
<Order total: 30.00 due: 28.50>
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
... for item_code in range(10)]
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
-
Mesmos dispositivos de teste do Exemplo 174.
-
Para aplicar uma estratégia de desconto a uma
Order
, basta passar a função de promoção como argumento. -
Uma função de promoção diferente é usada aqui e no teste seguinte.
Observe os textos explicativos do Exemplo 177—não há necessidade de instanciar um novo objeto promotion
com cada novo pedido: as funções já estão disponíveis para serem usadas.
É interessante notar que no Padrões de Projetos, os autores sugerem que:
"Objetos Estratégia muitas vezes são bons "peso mosca" (flyweight)".[118]
Uma definição do padrão Peso Mosca em outra parte daquele texto afirma:
"Um peso mosca é um objeto compartilhado que pode ser usado em múltiplos contextos simultaneamente."[119]
O compartilhamento é recomendado para reduzir o custo da criação de um novo objeto concreto de estratégia, quando a mesma estratégia é aplicada repetidamente a cada novo contexto—no nosso exemplo, a cada nova instância de Order
.
Então, para contornar uma desvantagem do padrão Estratégia—seu custo durante a execução—os autores recomendam a aplicação de mais outro padrão.
Enquanto isso, o número de linhas e custo de manutenção de seu código vão se acumulando.
Um caso de uso mais espinhoso, com estratégias concretas complexas mantendo estados internos, pode exigir a combinação de todas as partes dos padrões de projeto Estratégia e Peso Mosca. Muitas vezes, porém, estratégias concretas não tem estado interno; elas lidam apenas com dados vindos do contexto. Neste caso, não tenha dúvida, use as boas e velhas funções ao invés de escrever classes de um só metodo implementando uma interface de um só método declarada em outra classe diferente. Uma função pesa menos que uma instância de uma classe definida pelo usuário, e não há necessidade do Peso Mosca, pois cada função da estratégia é criada apenas uma vez por processo Python, quando o módulo é carregado. Uma função simples também é um "objeto compartilhado que pode ser usado em múltiplos contextos simultaneamente".
Uma vez implementado o padrão Estratégia com funções, outras possibilidades nos ocorrem. Suponha que você queira criar uma "meta-estratégia", que seleciona o melhor desconto disponível para uma dada Order
.
Nas próximas seções vamos estudar as refatorações adicionais para implementar esse requisito, usando abordagens que se valem de funções e módulos vistos como objetos.
10.2.3. Escolhendo a melhor estratégia: uma abordagem simples
Dados os mesmos clientes e carrinhos de compras dos testes no Exemplo 177, vamos agora acrescentar três testes adicionais ao Exemplo 178.
best_promo
aplica todos os descontos e devolve o maior >>> Order(joe, long_cart, best_promo) # (1)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) # (2)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo) # (3)
<Order total: 42.00 due: 39.90>
-
best_promo
selecionou alarger_order_promo
para o clientejoe
. -
Aqui
joe
recebeu o desconto debulk_item_promo
, por comprar muitas bananas. -
Encerrando a compra com um carrinho simples,
best_promo
deu à cliente fielann
o desconto dafidelity_promo
.
A implementação de best_promo
é muito simples. Veja o Exemplo 179.
best_promo
encontra o desconto máximo iterando sobre uma lista de funçõespromos = [fidelity_promo, bulk_item_promo, large_order_promo] # (1)
def best_promo(order: Order) -> Decimal: # (2)
"""Compute the best discount available"""
return max(promo(order) for promo in promos) # (3)
-
promos
: lista de estratégias implementadas como funções. -
best_promo
recebe uma instância deOrder
como argumento, como as outras funções*_promo
. -
Usando uma expressão geradora, aplicamos cada uma das funções de
promos
aorder
, e devolvemos o maior desconto encontrado.
O Exemplo 179 é bem direto: promos
é uma list
de funções. Depois que você se acostuma à ideia de funções como objetos de primeira classe, o próximo passo é notar que construir estruturas de dados contendo funções muitas vezes faz todo sentido.
Apesar do Exemplo 179 funcionar e ser fácil de ler, há alguma duplicação que poderia levar a um bug sutil: para adicionar uma nova estratégia, precisamos escrever a função e lembrar de incluí-la na lista promos
. De outra forma a nova promoção só funcionará quando passada explicitamente como argumento para Order
, e não será considerada por best_promotion
.
Vamos examinar algumas soluções para essa questão.
10.2.4. Encontrando estratégias em um módulo
Módulos também são objetos de primeira classe no Python, e a biblioteca padrão oferece várias funções para lidar com eles. A função embutida globals
é descrita assim na documentação de Python:
globals()
-
Devolve um dicionário representando a tabela de símbolos globais atual. Isso é sempre o dicionário do módulo atual (dentro de uma função ou método, esse é o módulo onde a função ou método foram definidos, não o módulo de onde são chamados).
O Exemplo 180 é uma forma um tanto hacker de usar globals
para ajudar best_promo
a encontrar automaticamente outras funções *_promo
disponíveis.
promos
é construída a partir da introspecção do espaço de nomes global do módulofrom decimal import Decimal
from strategy import Order
from strategy import (
fidelity_promo, bulk_item_promo, large_order_promo # (1)
)
promos = [promo for name, promo in globals().items() # (2)
if name.endswith('_promo') and # (3)
name != 'best_promo' # (4)
]
def best_promo(order: Order) -> Decimal: # (5)
"""Compute the best discount available"""
return max(promo(order) for promo in promos)
-
Importa as funções de promoções, para que fiquem disponíveis no espaço de nomes global.[120]
-
Itera sobre cada item no
dict
devolvido porglobals()
. -
Seleciona apenas aqueles valores onde o nome termina com o sufixo
_promo
e… -
…filtra e remove a própria
best_promo
, para evitar uma recursão infinita quandobest_promo
for invocada. -
Nenhuma mudança em
best_promo
.
Outra forma de coletar as promoções disponíveis seria criar um módulo e colocar nele todas as funções de estratégia, exceto best_promo
.
No Exemplo 181, a única mudança significativa é que a lista de funções de estratégia é criada pela introspecção de um módulo separado chamado promotions
. Veja que o Exemplo 181 depende da importação do módulo promotions
bem como de inspect
, que fornece funções de introspecção de alto nível.
promos
é construída a partir da introspecção de um novo módulo, promotions
from decimal import Decimal
import inspect
from strategy import Order
import promotions
promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order: Order) -> Decimal:
"""Compute the best discount available"""
return max(promo(order) for promo in promos)
A função inspect.getmembers
devolve os atributos de um objeto—neste caso, o módulo promotions
—opcionalmente filtrados por um predicado (uma função booleana). Usamos
inspect.isfunction
para obter apenas as funções do módulo.
O Exemplo 181 funciona independente dos nomes dados às funções; tudo o que importa é que o módulo promotions
contém apenas funções que, dado um pedido, calculam os descontos. Claro, isso é uma suposição implícita do código. Se alguém criasse uma função com uma assinatura diferente no módulo promotions
, best_promo
geraria um erro ao tentar aplicá-la a um pedido.
Poderíamos acrescentar testes mais estritos para filtrar as funções, por exemplo inspecionando seus argumentos. O ponto principal do Exemplo 181 não é oferecer uma solução completa, mas enfatizar um uso possível da introspecção de módulo.
Uma alternativa mais explícita para coletar dinamicamente as funções de desconto promocional seria usar um decorador simples. É nosso próximo tópico.
10.3. Padrão Estratégia aperfeiçoado com um decorador
Lembre-se que nossa principal objeção ao Exemplo 179 foi a repetição dos nomes das funções em suas definições e na lista promos
, usada pela função best_promo
para determinar o maior desconto aplicável. A repetição é problemática porque alguém pode acrescentar uma nova função de estratégia promocional e esquecer de adicioná-la manualmente à lista promos
—caso em que best_promo
vai silenciosamente ignorar a nova estratégia, introduzindo no sistema um bug sutil. O Exemplo 182 resolve esse problema com a técnica vista na Seção 9.4.
promos
é preenchida pelo decorador promotion
Promotion = Callable[[Order], Decimal]
promos: list[Promotion] = [] # (1)
def promotion(promo: Promotion) -> Promotion: # (2)
promos.append(promo)
return promo
def best_promo(order: Order) -> Decimal:
"""Compute the best discount available"""
return max(promo(order) for promo in promos) # (3)
@promotion # (4)
def fidelity(order: Order) -> Decimal:
"""5% discount for customers with 1000 or more fidelity points"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
@promotion
def bulk_item(order: Order) -> Decimal:
"""10% discount for each LineItem with 20 or more units"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
@promotion
def large_order(order: Order) -> Decimal:
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
-
A lista
promos
é global no módulo, e começa vazia. -
promotion
é um decorador de registro: ele devolve a funçãopromo
inalterada, após inserí-la na listapromos
. -
Nenhuma mudança é necessária em
best_promo
, pois ela se baseia na listapromos
. -
Qualquer função decorada com
@promotion
será adicionada apromos
.
Essa solução tem várias vantagens sobre aquelas apresentadas anteriormente:
-
As funções de estratégia de promoção não precisam usar nomes especiais—não há necessidade do sufixo
_promo
. -
O decorador
@promotion
realça o propósito da função decorada, e também torna mais fácil desabilitar temporariamente uma promoção: basta transformar a linha do decorador em comentário. -
Estratégias de desconto promocional podem ser definidas em outros módulos, em qualquer lugar do sistema, desde que o decorador
@promotion
seja aplicado a elas.
Na próxima seção vamos discutir Comando (Command)—outro padrão de projeto que é algumas vezes implementado via classes de um só metodo, quando funções simples seriam suficientes.
10.4. O padrão Comando
Comando é outro padrão de projeto que pode ser simplificado com o uso de funções passadas como argumentos. A Figura 25 mostra o arranjo das classes nesse padrão.
PasteCommand
, o receptor é Document. Para OpenCommand
, o receptor á a aplicação.O objetivo de Comando é desacoplar um objeto que invoca uma operação (o invoker ou remetente) do objeto fornecedor que implementa aquela operação (o receiver ou receptor). No exemplo em Padrões de Projetos, cada remetente é um item de menu em uma aplicação gráfica, e os receptors são o documento sendo editado ou a própria aplicação.
A ideia é colocar um objeto Command
entre os dois, implementando uma interface com um único método, execute
, que chama algum método no receptor para executar a operação desejada. Assim, o remetente não precisa conhecer a interface do receptor, e receptors diferentes podem ser adaptados com diferentes subclasses de Command
. O remetente é configurado com um comando concreto, e o opera chamando seu método execute
. Observe na Figura 25 que MacroCommand
pode armazenar um sequência de comandos; seu método execute()
chama o mesmo método em cada comando armazenado.
Citando Padrões de Projetos, "Comandos são um substituto orientado a objetos para callbacks." A pergunta é: precisamos de um substituto orientado a objetos para callbacks? Algumas vezes sim, mas nem sempre.
Em vez de dar ao remetente uma instância de Command
, podemos simplesmente dar a ele uma função. Em vez de chamar command.execute()
, o remetente pode apenas chamar command()
. O MacroCommand
pode ser programado como uma classe que implementa __call__
. Instâncias de MacroCommand
seriam invocáveis, cada uma mantendo uma lista de funções para invocação futura, como implementado no Exemplo 183.
MacroCommand
tem uma lista interna de comandosclass MacroCommand:
"""A command that executes a list of commands"""
def __init__(self, commands):
self.commands = list(commands) # (1)
def __call__(self):
for command in self.commands: # (2)
command()
-
Criar uma nova lista com os itens do argumento
commands
garante que ela seja iterável e mantém uma cópia local de referências a comandos em cada instância deMacroCommand
. -
Quando uma instância de
MacroCommand
é invocada, cada comando emself.commands
é chamado em sequência.
Usos mais avançados do padrão Comando—para implementar "desfazer", por exemplo—podem exigir mais que uma simples função de callback. Mesmo assim, Python oferece algumas alternativas que merecem ser consideradas:
-
Uma instância invocável como
MacroCommand
no Exemplo 183 pode manter qualquer estado que seja necessário, e oferecer outros métodos além de__call__
. -
Uma clausura pode ser usada para manter o estado interno de uma função entre invocações.
Isso encerra nossa revisão do padrão Comando usando funções de primeira classe.
Por alto, a abordagem aqui foi similar à que aplicamos a Estratégia:
substituir as instâncias de uma classe participante que implementava uma interface de método único por invocáveis.
Afinal, todo invocável de Python implementa uma interface de método único, e esse método se chama
__call__
.
10.5. Resumo do Capítulo
Como apontou Peter Norvig alguns anos após o surgimento do clássico Padrões de Projetos, "16 dos 23 padrões tem implementações qualitativamente mais simples em Lisp ou Dylan que em C++, pelo menos para alguns usos de cada padrão" (slide 9 da apresentação de Norvig, "Design Patterns in Dynamic Languages" presentation (Padrões de Projetos em Linguagens Dinâmicas)). Python compartilha alguns dos recursos dinâmicos das linguagens Lisp e Dylan, especialmente funções de primeira classe, nosso foco nesse capítulo.
Na mesma palestra citada no início deste capítulo, refletindo sobre o 20º aniversário de Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos, Ralph Johnson afirmou que um dos defeitos do livro é: "Excesso de ênfase nos padrões como linhas de chegada, em vez de como etapas em um processo de design".[121] Neste capítulo usamos o padrão Estratégia como ponto de partida: uma solução que funcionava, mas que simplificamos usando funções de primeira classe.
Em muitos casos, funções ou objetos invocáveis oferecem um caminho mais natural para implementar callbacks em Python que a imitação dos padrões Estratégia ou Comando como descritos por Gamma, Helm, Johnson, e Vlissides em Padrões de Projetos. A refatoração de Estratégia e a discussão de Comando nesse capítulo são exemplos de uma ideia mais geral: algumas vezes você pode encontrar uma padrão de projeto ou uma API que exigem que seus componentes implementem uma interface com um único método, e aquele método tem um nome que soa muito genérico, como "executar", "rodar" ou "fazer". Tais padrões ou APIs podem frequentemente ser implementados em Python com menos código repetitivo, usando funções como objetos de primeira classe.
10.6. Leitura complementar
A "Receita 8.21. Implementando o Padrão Visitante" (Receipt 8.21. Implementing the Visitor Pattern) no Python Cookbook, 3ª ed. (EN), mostra uma implementação elegante do padrão Visitante, na qual uma classe NodeVisitor
trata métodos como objetos de primeira classe.
Sobre o tópico mais geral de padrões de projetos, a oferta de leituras para o programador Python não é tão numerosa quando aquela disponível para as comunidades de outras linguagens.
Learning Python Design Patterns ("Aprendendo os Padrões de Projeto de Python"), de Gennadiy Zlobin (Packt), é o único livro inteiramente dedicado a padrões em Python que encontrei. Mas o trabalho de Zlobin é muito breve (100 páginas) e trata de apenas 8 dos 23 padrões de projeto originais.
Expert Python Programming ("Programação Avançada em Python"), de Tarek Ziadé (Packt), é um dos melhores livros de Python de nível intermediário, e seu capítulo final, "Useful Design Patterns" (Padrões de Projetos Úteis), apresenta vários dos padrões clássicos de uma perspectiva pythônica.
Alex Martelli já apresentou várias palestras sobre padrões de projetos em Python. Há um vídeo de sua apresentação na EuroPython (EN) e um conjunto de slides em seu site pessoal (EN). Ao longo dos anos, encontrei diferentes jogos de slides e vídeos de diferentes tamanhos, então vale a pena tentar uma busca mais ampla com o nome dele e as palavras "Python Design Patterns". Um editor me contou que Martelli está trabalhando em um livro sobre esse assunto. Eu certamente comprarei meu exemplar assim que estiver disponível.
Há muitos livros sobre padrões de projetos no contexto de Java mas, dentre todos eles, meu preferido é Head First Design Patterns ("Mergulhando de Cabeça nos Padrões de Projetos"), 2ª ed., de Eric Freeman e Elisabeth Robson (O’Reilly). Esse volume explica 16 dos 23 padrões clássicos. Se você gosta do estilo amalucado da série Head First e precisa de uma introdução a esse tópico, vai adorar esse livro. Ele é centrado em Java, mas a segunda edição foi atualizada para refletir a introdução de funções de primeira classe naquela linguagem, tornando alguns dos exemplos mais próximos de código que escreveríamos em Python.
Para um olhar moderno sobre padrões, do ponto de vista de uma linguagem dinâmica com duck typing e funções de primeira classe, Design Patterns in Ruby ("Padrões de Projetos em Ruby") de Russ Olsen (Addison-Wesley) traz muitas ideias aplicáveis também ao Python. A despeito de suas muitas diferenças sintáticas, no nível semântico Python e Ruby estão mais próximos entre si que de Java ou do C++.
Em "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (slides), Peter Norvig mostra como funções de primeira classe (e outros recursos dinâmicos) tornam vários dos padrões de projeto originais mais simples ou mesmo desnecessários.
A "Introdução" do Padrões de Projetos original, de Gamma et al. já vale o preço do livro—mais até que o catálogo de 23 padrões, que inclui desde receitas muito importantes até algumas raramente úteis. Alguns princípios de projetos de software muito citados, como "Programe para uma interface, não para uma implementação" e "Prefira a composição de objetos à herança de classe", vem ambos daquela introdução.
A aplicação de padrões a projetos se originou com o arquiteto Christopher Alexander et al., e foi apresentada no livro A Pattern Language ("Uma Linguagem de Padrões") (Oxford University Press). A ideia de Alexander é criar um vocabulário padronizado, permitindo que equipes compartilhem decisões comuns em projetos de edificações. M. J. Dominus wrote “‘Design Patterns’ Aren’t” (Padrões de Projetos Não São), uma curiosa apresentação de slides acompanhada de um texto, argumentando que a visão original de Alexander sobre os padrões é mais profunda e mais humanista e também aplicável à engenharia de software.
Parte III: Classes e protocolos
11. Um objeto pythônico
Para uma biblioteca ou framework, ser pythônica significa tornar tão fácil e tão natural quanto possível que uma programadora Python descubra como realizar uma tarefa.[123]
criador de frameworks Python e JavaScript
Graças ao Modelo de Dados de Python, nossos tipos definidos pelo usuário podem se comportar de forma tão natural quanto os tipos embutidos. E isso pode ser realizado sem herança, no espírito do duck typing: implemente os métodos necessários e seus objetos se comportarão da forma esperada.
Nos capítulos anteriores, estudamos o comportamento de vários objetos embutidos. Vamos agora criar classes definidas pelo usuário que se portam como objetos Python reais. As classes na sua aplicação provavelmente não precisam nem devem implementar tantos métodos especiais quanto os exemplos nesse capítulo. Mas se você estiver escrevendo uma biblioteca ou um framework, os programadores que usarão suas classes talvez esperem que elas se comportem como as classes fornecidas pelo Python. Satisfazer tal expectativa é um dos jeitos de ser "pythônico".
Esse capítulo começa onde o Capítulo 1 terminou, mostrando como implementar vários métodos especiais comumente vistos em objetos Python de diferentes tipos.
Veremos como:
-
Suportar as funções embutidas que convertem objetos para outros tipos (por exemplo,
repr()
,bytes()
,complex()
, etc.) -
Implementar um construtor alternativo como um método da classe
-
Estender a mini-linguagem de formatação usada pelas f-strings, pela função embutida
format()
e pelo métodostr.format()
-
Fornecer acesso a atributos apenas para leitura
-
Tornar um objetos hashable, para uso em sets e como chaves de
dict
-
Economizar memória com
__slots__
Vamos fazer tudo isso enquanto desenvolvemos Vector2d
, um tipo simples de vetor euclidiano bi-dimensional.
No Capítulo 12, o mesmo código servirá de base para uma classe de vetor N-dimensional.
A evolução do exemplo será interrompida para discutirmos dois tópicos conceituais:
-
Como e quando usar os decoradores
@classmethod
e@staticmethod
-
Atributos privados e protegidos no Python: uso, convenções e limitações
11.1. Novidades nesse capítulo
Acrescentei uma nova epígrafe e também algumas palavras ao segundo parágrafo do capítulo, para falar do conceito de "pythônico"—que na primeira edição era discutido apenas no final do livro.
A Seção 11.6 foi atualizada para mencionar as f-strings, introduzidas no Python 3.6.
É uma mudança pequena, pois as f-strings suportam a mesma mini-linguagem de formatação que a função embutida format()
e o método str.format()
,
então quaisquer métodos __format__
implementados antes vão funcionar também com as f-strings.
O resto do capítulo quase não mudou—os métodos especiais são praticamente os mesmos desde Python 3.0, e as ideias centrais apareceram no Python 2.2.
Vamos começar pelos métodos de representação de objetos.
11.2. Representações de objetos
Todas as linguagens orientadas a objetos tem pelo menos uma forma padrão de se obter uma representação de qualquer objeto como uma string. Python tem duas formas:
repr()
-
Devolve uma string representando o objeto como o desenvolvedor quer vê-lo. É o que aparece quando o console de Python ou um depurador mostram um objeto.
str()
-
Devolve uma string representando o objeto como o usuário quer vê-lo. É o que aparece quando se passa um objeto como argumento para
print()
.
Os métodos especiais __repr__
e __str__
suportam repr()
e str()
, como vimos no Capítulo 1.
Existem dois métodos especiais adicionais para suportar representações alternativas de objetos, __bytes__
e __format__
. O método __bytes__
é análogo a __str__
: ele é chamado por bytes()
para obter um objeto representado como uma sequência de bytes.
Já __format__
é usado por f-strings, pela função embutida format()
e pelo método str.format()
. Todos eles chamam obj.format(format_spec)
para obter versões de exibição de objetos usando códigos de formatação especiais. Vamos tratar de __bytes__
na próxima seção e de __format__
logo depois.
⚠️ Aviso
|
Se você está vindo de Python 2, lembre-se que no Python 3 |
11.3. A volta da classe Vector
Para
demonstrar os vários métodos usados para gerar representações de objetos,
vamos criar uma classe Vector2d
, similar à que vimos no Capítulo 1.
O Exemplo 184 ilustra o comportamento básico que esperamos de uma instância de Vector2d
.
Vector2d
têm várias representações >>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y) # (1)
3.0 4.0
>>> x, y = v1 # (2)
>>> x, y
(3.0, 4.0)
>>> v1 # (3)
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1)) # (4)
>>> v1 == v1_clone # (5)
True
>>> print(v1) # (6)
(3.0, 4.0)
>>> octets = bytes(v1) # (7)
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1) # (8)
5.0
>>> bool(v1), bool(Vector2d(0, 0)) # (9)
(True, False)
-
Os componentes de um
Vector2d
podem ser acessados diretamente como atributos (não é preciso invocar métodos getter). -
Um
Vector2d
pode ser desempacotado para uma tupla de variáveis. -
O
repr
de umVector2d
emula o código-fonte usado para construir a instância. -
Usar
eval
aqui mostra que orepr
de umVector2d
é uma representação fiel da chamada a seu construtor.[124] -
Vector2d
suporta a comparação com==
; isso é útil para testes. -
print
chamastr
, que no caso deVector2d
exibe um par ordenado. -
bytes
usa o método__bytes__
para produzir uma representação binária. -
abs
usa o método__abs__
para devolver a magnitude doVector2d
. -
bool
usa o método__bool__
para devolverFalse
se oVector2d
tiver magnitude zero, caso contrário esse método devolveTrue
.
O Vector2d
do Exemplo 184 é implementado em vector2d_v0.py (no Exemplo 185).
O código está baseado no Exemplo 2, exceto pelos métodos para os operadores +
e *
, que veremos mais tarde no Capítulo 16. Vamos acrescentar o método para ==
, já que ele é útil para testes.
Nesse ponto, Vector2d
usa vários métodos especiais para oferecer operações que um pythonista espera encontrar em um objeto bem projetado.
from array import array
import math
class Vector2d:
typecode = 'd' # (1)
def __init__(self, x, y):
self.x = float(x) # (2)
self.y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y)) # (3)
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self) # (4)
def __str__(self):
return str(tuple(self)) # (5)
def __bytes__(self):
return (bytes([ord(self.typecode)]) + # (6)
bytes(array(self.typecode, self))) # (7)
def __eq__(self, other):
return tuple(self) == tuple(other) # (8)
def __abs__(self):
return math.hypot(self.x, self.y) # (9)
def __bool__(self):
return bool(abs(self)) # (10)
-
typecode
é um atributo de classe, usado na conversão de instâncias deVector2d
de/parabytes
. -
Converter
x
ey
parafloat
em__init__
captura erros mais rápido, algo útil quandoVector2d
é chamado com argumentos inadequados. -
__iter__
torna umVector2d
iterável; é isso que faz o desempacotamento funcionar (por exemplo,x, y = my_vector
). Vamos implementá-lo aqui usando uma expressão geradora para produzir os componentes, um após outro.[125] -
O
__repr__
cria uma string interpolando os componentes com{!r}
, para obter seusrepr
; comoVector2d
é iterável,*self
alimentaformat
com os componentesx
ey
. -
Dado um iterável
Vector2d
, é fácil criar umatuple
para exibição como um par ordenado. -
Para gerar
bytes
, convertemos o typecode parabytes
e concatenamos… -
…
bytes
convertidos a partir de umarray
criada iterando sobre a instância. -
Para comparar rapidamente todos os componentes, cria tuplas a partir dos operandos. Isso funciona para operandos que sejam instâncias de
Vector2d
, mas tem problemas. Veja o alerta abaixo. -
A magnitude é o comprimento da hipotenusa do triângulo retângulo de catetos formados pelos componentes
x
ey
. -
__bool__
usaabs(self)
para computar a magnitude, então a converte parabool
; assim,0.0
se tornaFalse
, qualquer valor diferente de zero éTrue
.
⚠️ Aviso
|
O método |
Temos um conjunto bastante completo de métodos básicos, mas ainda precisamos de uma maneira de reconstruir um Vector2d
a partir da representação binária produzida por bytes()
.
11.4. Um construtor alternativo
Já que podemos exportar um Vector2d
na forma de bytes, naturalmente precisamos de um método para importar um Vector2d
de uma sequência binária. Procurando na biblioteca padrão por algo similar, descobrimos que array.array
tem um método de classe chamado .frombytes
, adequado a nossos propósitos—já o vimos na Seção 2.10.1. Adotamos o mesmo nome e usamos sua funcionalidade em um método de classe para Vector2d
em vector2d_v1.py (no Exemplo 186).
frombytes
, acrescentado à definição de Vector2d
em vector2d_v0.py (no Exemplo 185) @classmethod # (1)
def frombytes(cls, octets): # (2)
typecode = chr(octets[0]) # (3)
memv = memoryview(octets[1:]).cast(typecode) # (4)
return cls(*memv) # (5)
-
O decorador
classmethod
modifica um método para que ele possa ser chamado diretamente em uma classe. -
Nenhum argumento
self
; em vez disso, a própria classe é passada como primeiro argumento—por convenção chamadocls
. -
Lê o
typecode
do primeiro byte. -
Cria uma
memoryview
a partir da sequência bináriaoctets
, e usa otypecode
para convertê-la.[126] -
Desempacota a
memoryview
resultante da conversão no par de argumentos necessários para o construtor.
Acabei de usar um decorador classmethod
, e ele é muito específico de Python. Vamos então falar um pouco disso.
11.5. classmethod versus staticmethod
O decorador classmethod
não é mencionado no tutorial de Python, nem tampouco o staticmethod
. Qualquer um que tenha aprendido OO com Java pode se perguntar porque Python tem esses dois decoradores, e não apenas um deles.
Vamos começar com classmethod
. O Exemplo 186 mostra seu uso: definir um método que opera na classe, e não em suas instâncias. O classmethod
muda a forma como o método é chamado, então recebe a própria classe como primeiro argumento, em vez de uma instância. Seu uso mais comum é em construtores alternativos, como frombytes
no Exemplo 186. Observe como a última linha de frombytes
de fato usa o argumento cls
, invocando-o para criar uma nova instância: cls(*memv)
.
O decorador staticmethod
, por outro lado, muda um método para que ele não receba qualquer primeiro argumento especial. Essencialmente, um método estático é apenas uma função simples que por acaso mora no corpo de uma classe, em vez de ser definida no nível do módulo. O Exemplo 187 compara a operação de classmethod
e staticmethod
.
classmethod
e staticmethod
>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args # (1)
... @staticmethod
... def statmeth(*args):
... return args # (2)
...
>>> Demo.klassmeth() # (3)
(<class '__main__.Demo'>,)
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
>>> Demo.statmeth() # (4)
()
>>> Demo.statmeth('spam')
('spam',)
-
klassmeth
apenas devolve todos os argumentos posicionais. -
statmeth
faz o mesmo. -
Não importa como ele seja invocado,
Demo.klassmeth
recebe sempre a classeDemo
como primeiro argumento. -
Demo.statmeth
se comporta exatamente como uma boa e velha função.
✒️ Nota
|
O decorador |
Agora que vimos para que serve o classmethod
(e que o staticmethod
não é muito útil), vamos voltar para a questão da representação de objetos e entender como gerar uma saída formatada.
11.6. Exibição formatada
As f-strings, a função embutida format()
e o método str.format()
delegam a formatação efetiva para cada tipo, chamando seu método .__format__(format_spec)
. O format_spec
especifica a formatação desejada, e é:
-
O segundo argumento em
format(my_obj, format_spec)
, ou -
O que quer que apareça após os dois pontos (
:
) em um campo de substituição delimitado por{}
dentro de uma f-string ou ofmt
emfmt.str.format()
Por exemplo:
>>> brl = 1 / 4.82 # BRL to USD currency conversion rate
>>> brl
0.20746887966804978
>>> format(brl, '0.4f') # (1)
'0.2075'
>>> '1 BRL = {rate:0.2f} USD'.format(rate=brl) # (2)
'1 BRL = 0.21 USD'
>>> f'1 USD = {1 / brl:0.2f} BRL' # (3)
'1 USD = 4.82 BRL'
-
A formatação especificada é
'0.4f'
. -
A formatação especificada é
'0.2f'
. Orate
no campo de substituição não é parte da especificação de formato. Ele determina qual argumento nomeado de.format()
entra no campo de substituição. -
Novamente, a especificação é
'0.2f'
. A expressão1 / brl
não é parte dela.
O segundo e o terceiro textos explicativos apontam um fato importante: uma string de formatação tal como’{0.mass:5.3e}'` na verdade usa duas notações separadas. O '0.mass'
à esquerda dos dois pontos é a parte field_name
da sintaxe de campo de substituição, e pode ser uma expressão arbitrária em uma f-string. O '5.3e'
após os dois pontos é a especificação do formato. A notação usada na especificação do formato é chamada Mini-Linguagem de Especificação de Formato.
👉 Dica
|
Se f-strings, |
Alguns tipos embutidos tem seus próprios códigos de apresentação na Mini-Linguagem de Especificação de Formato. Por exemplo—entre muitos outros códigos—o tipo int
suporta b
e x
, para saídas em base 2 e base 16, respectivamente, enquanto float
implementa f
, para uma exibição de ponto fixo, e %
, para exibir porcentagens:
>>> format(42, 'b')
'101010'
>>> format(2 / 3, '.1%')
'66.7%'
A Mini-Linguagem de Especificação de Formato é extensível, porque cada classe interpreta o argumento format_spec
como quiser. Por exemplo, as classes no módulo datetime
usam os mesmos códigos de formatação nas funções strftime()
e em seus métodos __format__
. Veja abaixo alguns exemplos de uso da função format()
e do método str.format()
:
>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now, '%H:%M:%S')
'18:49:05'
>>> "It's now {:%I:%M %p}".format(now)
"It's now 06:49 PM"
Se a classe não possuir um __format__
, o método herdado de object
devolve str(my_object)
. Como Vector2d
tem um __str__
, isso funciona:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
Entretanto, se você passar um especificador de formato, object.__format__
gera um TypeError
:
>>> format(v1, '.3f')
Traceback (most recent call last):
...
TypeError: non-empty format string passed to object.__format__
Vamos corrigir isso implementando nossa própria mini-linguagem de formatação. O primeiro passo será presumir que o especificador de formato fornecido pelo usuário tem por objetivo formatar cada componente float
do vetor. Esse é o resultado esperado:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
O Exemplo 188 implementa __format__
para produzir as formatações vistas acima.
Vector2d.__format__
, versão #1 # inside the Vector2d class
def __format__(self, fmt_spec=''):
components = (format(c, fmt_spec) for c in self) # (1)
return '({}, {})'.format(*components) # (2)
-
Usa a função embutida
format
para aplicar ofmt_spec
a cada componente do vetor, criando um iterável de strings formatadas. -
Insere as strings formatadas na fórmula
'(x, y)'
.
Agora vamos acrescentar um código de formatação personalizado à nossa mini-linguagem: se o especificador de formato terminar com 'p'
, vamos exibir o vetor em coordenadas polares: <r, θ>
,
onde r
é a magnitute e θ (theta) é o ângulo em radianos. O restante do especificador de formato (o que quer que venha antes do 'p'
) será usado como antes.
👉 Dica
|
Ao escolher a letra para um código personalizado de formato, evitei sobrepor códigos usados por outros tipos. Na Mini-Linguagem de Especificação de Formato vemos que inteiros usam os códigos |
Para gerar coordenadas polares, já temos o método __abs__
para a magnitude. Vamos então escrever um método angle
simples, usando a função math.atan2()
, para obter o ângulo. Eis o código:
# inside the Vector2d class
def angle(self):
return math.atan2(self.y, self.x)
Com isso, podemos agora aperfeiçoar nosso __format__
para gerar coordenadas polares. Veja o Exemplo 189.
Vector2d.__format__
, versão #2, agora com coordenadas polares def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'): # (1)
fmt_spec = fmt_spec[:-1] # (2)
coords = (abs(self), self.angle()) # (3)
outer_fmt = '<{}, {}>' # (4)
else:
coords = self # (5)
outer_fmt = '({}, {})' # (6)
components = (format(c, fmt_spec) for c in coords) # (7)
return outer_fmt.format(*components) # (8)
-
O formato termina com
'p'
: usa coordenadas polares. -
Remove o sufixo
'p'
defmt_spec
. -
Cria uma
tuple
de coordenadas polares:(magnitude, angle)
. -
Configura o formato externo com chaves de ângulo.
-
Caso contrário, usa os componentes
x, y
deself
para coordenadas retângulares. -
Configura o formato externo com parênteses.
-
Gera um iterável cujos componentes são strings formatadas.
-
Insere as strings formatadas no formato externo.
Com o Exemplo 189, obtemos resultados como esses:
>>> format(Vector2d(1, 1), 'p')
'<1.4142135623730951, 0.7853981633974483>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'
Como mostrou essa seção, não é difícil estender a Mini-Linguagem de Especificação de Formato para suportar tipos definidos pelo usuário.
Vamos agora passar a um assunto que vai além das aparências: tornar nosso Vector2d
hashable, para podermos criar conjuntos de vetores ou usá-los como chaves em um dict
.
11.7. Um Vector2d hashable
Da forma como ele está definido até agora, as instâncias de nosso Vector2d
não são hashable, então não podemos colocá-las em um set
:
>>> v1 = Vector2d(3, 4)
>>> hash(v1)
Traceback (most recent call last):
...
TypeError: unhashable type: 'Vector2d'
>>> set([v1])
Traceback (most recent call last):
...
TypeError: unhashable type: 'Vector2d'
Para tornar um Vector2d
hashable, precisamos implementar __hash__
(__eq__
também é necessário, mas já temos esse método). Além disso, precisamos tornar imutáveis as instâncias do vetor, como vimos na Seção 3.4.1.
Nesse momento, qualquer um pode fazer v1.x = 7
, e não há nada no código sugerindo que é proibido modificar um Vector2d
. O comportamento que queremos é o seguinte:
>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 7
Traceback (most recent call last):
...
AttributeError: can't set attribute
Faremos isso transformando os componentes x
e y
em propriedades apenas para leitura no Exemplo 190.
Vector2d
imutável são exibidas aqui; a listagem completa está no Exemplo 194class Vector2d:
typecode = 'd'
def __init__(self, x, y):
self.__x = float(x) # (1)
self.__y = float(y)
@property # (2)
def x(self): # (3)
return self.__x # (4)
@property # (5)
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x, self.y)) # (6)
# remaining methods: same as previous Vector2d
-
Usa exatamente dois sublinhados como prefixo (com zero ou um sublinhado como sufixo), para tornar um atributo privado.[128]
-
O decorador
@property
marca o método getter de uma propriedade. -
O método getter é nomeado de acordo com o nome da propriedade pública que ele expõe:
x
. -
Apenas devolve
self.__x
. -
Repete a mesma fórmula para a propriedade
y
. -
Todos os métodos que apenas leem os componentes
x
ey
podem permanecer como estavam, lendo as propriedades públicas através deself.x
eself.y
em vez de usar os atributos privados. Então essa listagem omite o restante do código da classe.
✒️ Nota
|
|
Agora que nossos vetores estão razoavelmente protegidos contra mutação acidental, podemos implementar o método __hash__
.
Ele deve devolver um int
e, idealmente, levar em consideração os hashs dos atributos do objeto usados também no método __eq__
, pois objetos que são considerados iguais ao serem comparados devem ter o mesmo hash.
A documentação do método especial __hash__
sugere computar o hash de uma tupla com os componentes, e é isso que fazemos no Exemplo 191.
# inside class Vector2d:
def __hash__(self):
return hash((self.x, self.y))
Com o acréscimo do método __hash__
, temos agora vetores hashable:
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> hash(v1), hash(v2)
(1079245023883434373, 1994163070182233067)
>>> {v1, v2}
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}
👉 Dica
|
Não é estritamente necessário implementar propriedades ou proteger de alguma forma os atributos de instância para criar um tipo hashable.
Só é necessário implementar corretamente |
Se você estiver criando um tipo com um valor numérico escalar que faz sentido, você pode também implementar os métodos __int__
e __float__
, invocados pelos construtores int()
e float()
, que são usados, em alguns contextos, para coerção de tipo. Há também o método __complex__
, para suportar o construtor embutido complex()
. Talvez Vector2d
pudesse oferecer o __complex__
, mas deixo isso como um exercício para vocês.
11.8. Suportando o pattern matching posicional
Até aqui, instâncias de Vector2d
são compatíveis com o pattern matching com instâncias de classe—vistos na Seção 5.8.2.
No Exemplo 192, todos aqueles padrões nomeados funcionam como esperado.
Vector2d
—requer Python 3.10def keyword_pattern_demo(v: Vector2d) -> None:
match v:
case Vector2d(x=0, y=0):
print(f'{v!r} is null')
case Vector2d(x=0):
print(f'{v!r} is vertical')
case Vector2d(y=0):
print(f'{v!r} is horizontal')
case Vector2d(x=x, y=y) if x==y:
print(f'{v!r} is diagonal')
case _:
print(f'{v!r} is awesome')
Entretanto, se tentamos usar um padrão posicional, como esse:
case Vector2d(_, 0):
print(f'{v!r} is horizontal')
o resultado é esse:
TypeError: Vector2d() accepts 0 positional sub-patterns (1 given)
Para fazer Vector2d
funcionar com padrões posicionais, precisamos acrescentar um atributo de classe chamado __match_args__
, listando os atributos de instância na ordem em que eles serão usados no pattern matching posicional.
class Vector2d:
__match_args__ = ('x', 'y')
# etc...
Agora podemos economizar alguma digitação ao escrever padrões para usar contra sujeitos
Vector2d
, como se vê no Exemplo 193.
Vector2d
—requer Python 3.10def positional_pattern_demo(v: Vector2d) -> None:
match v:
case Vector2d(0, 0):
print(f'{v!r} is null')
case Vector2d(0):
print(f'{v!r} is vertical')
case Vector2d(_, 0):
print(f'{v!r} is horizontal')
case Vector2d(x, y) if x==y:
print(f'{v!r} is diagonal')
case _:
print(f'{v!r} is awesome')
O atributo de classe __match_args__
não precisa incluir todos os atributos públicos de instância.
Em especial, se o __init__
da classe tem argumentos obrigatórios e opcionais, que são depois vinculados a atributos de instância, pode ser razoável nomear apenas os argumentos obrigatórios em
__match_args__
, omitindo os opcionais.
Vamos dar um passo atrás e revisar tudo o que programamos até aqui no Vector2d
.
11.9. Listagem completa Vector2d, versão 3
Já estamos trabalhando no Vector2d
há algum tempo, mostrando apenas trechos isolados. O Exemplo 194 é uma listagem completa e consolidada
de vector2d_v3.py, incluindo os doctests que usei durante o desenvolvimento.
"""
A two-dimensional vector class
>>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y)
3.0 4.0
>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> print(v1)
(3.0, 4.0)
>>> octets = bytes(v1)
>>> octets
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
>>> abs(v1)
5.0
>>> bool(v1), bool(Vector2d(0, 0))
(True, False)
Test of ``.frombytes()`` class method:
>>> v1_clone = Vector2d.frombytes(bytes(v1))
>>> v1_clone
Vector2d(3.0, 4.0)
>>> v1 == v1_clone
True
Tests of ``format()`` with Cartesian coordinates:
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
Tests of the ``angle`` method::
>>> Vector2d(0, 0).angle()
0.0
>>> Vector2d(1, 0).angle()
0.0
>>> epsilon = 10**-8
>>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
True
>>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
True
Tests of ``format()`` with polar coordinates:
>>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS
'<1.414213..., 0.785398...>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'
Tests of `x` and `y` read-only properties:
>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 123
Traceback (most recent call last):
...
AttributeError: can't set attribute 'x'
Tests of hashing:
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> len({v1, v2})
2
"""
from array import array
import math
class Vector2d:
__match_args__ = ('x', 'y')
typecode = 'd'
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(array(self.typecode, self)))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __hash__(self):
return hash((self.x, self.y))
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'):
fmt_spec = fmt_spec[:-1]
coords = (abs(self), self.angle())
outer_fmt = '<{}, {}>'
else:
coords = self
outer_fmt = '({}, {})'
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(*components)
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)
Recordando, nessa seção e nas anteriores vimos alguns dos métodos especiais essenciais que você pode querer implementar para obter um objeto completo.
✒️ Nota
|
Você deve implementar esses métodos especiais apenas se sua aplicação precisar deles. Os usuários finais não se importam se os objetos que compõem uma aplicação são pythônicos ou não. Por outro lado, se suas classes são parte de uma biblioteca para ser usada por outros programadores Python, você não tem como adivinhar como eles vão usar seus objetos. E eles estarão esperando ver esses comportamentos pythônicos que descrevemos aqui. |
Como programado no Exemplo 194,
Vector2d
é um exemplo didático com uma lista extensiva de métodos especiais relacionados à representação de objetos, não um modelo para qualquer classe definida pelo usuário.
Na próxima seção, deixamos o Vector2d
de lado por um tempo para discutir o design e as desvantagens do mecanismo de atributos privados no Python—o prefixo de duplo sublinhado em self.__x
.
11.10. Atributos privados e "protegidos" no Python
Em Python, não há como criar variáveis privadas como as criadas com o modificador private
em Java. O que temos no Python é um mecanismo simples para prevenir que um atributo "privado" em uma subclasse seja acidentalmente sobrescrito.
Considere o seguinte cenário: alguém escreveu uma classe chamada Dog
, que usa um atributo de instância mood
internamente, sem expô-lo. Você precisa criar a uma subclasse Beagle
de Dog
. Se você criar seu próprio atributo de instância mood
, sem saber da colisão de nomes, vai afetar o atributo mood
usado pelos métodos herdados de Dog
. Isso seria bem complicado de depurar.
Para prevenir esse tipo de problema, se você nomear o atributo de instância no formato __mood
(dois sublinhados iniciais e zero ou no máximo um sublinhado no final), Python armazena o nome no __dict__
da instância, prefixado com um sublinhado seguido do nome da classe. Na classe Dog
, por exemplo, __mood
se torna _Dog__mood
e em Beagle
ele será _Beagle__mood
.
Esse recurso da linguagem é conhecido pela encantadora alcunha de desfiguração de nome ("name mangling").
O Exemplo 195 mostra o resultado na classe Vector2d
do Exemplo 190.
_
e o nome da classe>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}
>>> v1._Vector2d__x
3.0
A desfiguração do nome é sobre alguma proteção, não sobre segurança: ela foi projetada para evitar acesso acidental, não ataques maliciosos. A Figura 26 ilustra outro dispositivo de proteção.
Qualquer um que saiba como os nomes privados são modificados pode ler o atributo privado diretamente, como mostra a última linha do Exemplo 195—isso na verdade é útil para depuração e serialização. Isso também pode ser usado para atribuir um valor a um componente privado de um Vector2d
, escrevendo v1._Vector2d__x = 7
. Mas se você estiver fazendo isso com código em produção, não poderá reclamar se alguma coisa explodir.
A funcionalidade de desfiguração de nomes não é amada por todos os pythonistas, nem tampouco a aparência estranha de nomes escritos como self.__x
. Muitos preferem evitar essa sintaxe e usar apenas um sublinhado no prefixo para "proteger" atributos da forma convencional
(por exemplo, self._x
). Críticos da desfiguração automática com o sublinhado duplo sugerem que preocupações com modificações acidentais a atributos deveriam ser tratadas através de convenções de nomenclatura. Ian Bicking—criador do pip, do virtualenv e de outros projetos—escreveu:
Nunca, de forma alguma, use dois sublinhados como prefixo. Isso é irritantemente privado. Se colisão de nomes for uma preocupação, use desfiguração explícita de nomes em seu lugar (por exemplo,
_MyThing_blahblah
). Isso é essencialmente a mesma coisa que o sublinhado duplo, mas é transparente onde o sublinhado duplo é obscuro.[129]
O prefixo de sublinhado único não tem nenhum significado especial para o interpretador Python, quando usado em nomes de atributo. Mas essa é uma convenção muito presente entre programadores Python: tais atributos não devem ser acessados de fora da classe.[130] É fácil respeitar a privacidade de um objeto que marca seus atributos com um único _
, da mesma forma que é fácil respeitar a convenção de tratar como constantes as variáveis com nomes inteiramente em maiúsculas.
Atributos com um único _
como prefixo são chamados "protegidos" em algumas partes da documentação de Python.[131] A prática de "proteger" atributos por convenção com a forma self._x
é muito difundida, mas chamar isso de atributo "protegido" não é tão comum. Alguns até falam em atributo "privado" nesses casos.
Concluindo: os componentes de Vector2d
são "privados" e nossas instâncias de Vector2d
são "imutáveis"—com aspas irônicas—pois não há como tornar uns realmente privados e outras realmente imutáveis.[132]
Vamos agora voltar à nossa classe Vector2d
. Na próxima seção trataremos de um atributo (e não um método) especial que afeta o armazenamento interno de um objeto, com um imenso impacto potencial sobre seu uso de memória, mas pouco efeito sobre sua interface pública: __slots__
.
11.11. Economizando memória com __slots__
Por default, Python armazena os atributos de cada instância em um dict
chamado __dict__
.
Como vimos em Seção 3.9, um dict
ocupa um espaço significativo de memória, mesmo com as otimizações mencionadas naquela seção.
Mas se você definir um atributo de classe chamado __slots__
, que mantém uma sequência de nomes de atributos, Python usará um modelo alternativo de armazenamento para os atributos de instância:
os atributos nomeados em __slots__
serão armazenados em um array de referências oculto, que usa menos memória que um dict
.
Vamos ver como isso funciona através de alguns exemplos simples, começando pelo Exemplo 196.
Pixel
usa __slots__
>>> class Pixel:
... __slots__ = ('x', 'y') # (1)
...
>>> p = Pixel() # (2)
>>> p.__dict__ # (3)
Traceback (most recent call last):
...
AttributeError: 'Pixel' object has no attribute '__dict__'
>>> p.x = 10 # (4)
>>> p.y = 20
>>> p.color = 'red' # (5)
Traceback (most recent call last):
...
AttributeError: 'Pixel' object has no attribute 'color'
-
__slots__
deve estar presente quando a classe é criada; acrescentá-lo ou modificá-lo posteriormente não tem qualquer efeito. Os nomes de atributos podem estar em umatuple
ou em umalist
. Prefiro usar umatuple
, para deixar claro que não faz sentido modificá-la. -
Cria uma instância de
Pixel
, pois os efeitos de__slots__
são vistos nas instâncias. -
Primeiro efeito: instâncias de
Pixel
não têm um__dict__
. -
Define normalmente os atributos
p.x
ep.y
. -
Segundo efeito: tentar definir um atributo não listado em
__slots__
gera umAttributeError
.
Até aqui, tudo bem. Agora vamos criar uma subclasse de Pixel
, no Exemplo 197, para ver o lado contraintuitivo de __slots__
.
OpenPixel
é uma subclasse de Pixel
>>> class OpenPixel(Pixel): # (1)
... pass
...
>>> op = OpenPixel()
>>> op.__dict__ # (2)
{}
>>> op.x = 8 # (3)
>>> op.__dict__ # (4)
{}
>>> op.x # (5)
8
>>> op.color = 'green' # (6)
>>> op.__dict__ # (7)
{'color': 'green'}
-
OpenPixel
não declara qualquer atributo próprio. -
Surpresa: instâncias de
OpenPixel
têm um__dict__
. -
Se você definir o atributo
x
(nomeado no__slots__
da classe basePixel
)… -
…ele não será armazenado no
__dict__
da instância… -
…mas sim no array oculto de referências na instância.
-
Se você definir um atributo não nomeado no
__slots__
… -
…ele será armazenado no
__dict__
da instância.
O Exemplo 197 mostra que o efeito de __slots__
é herdado apenas parcialmente por uma subclasse.
Para se assegurar que instâncias de uma subclasse não tenham o __dict__
, é preciso declarar
__slots__
novamente na subclasse.
Se você declarar __slots__ = ()
(uma tupla vazia), as instâncias da subclasse não terão um
__dict__
e só aceitarão atributos nomeados no __slots__
da classe base.
Se você quiser que uma subclasse tenha atributos adicionais, basta nomeá-los em __slots__
, como mostra o Exemplo 198.
ColorPixel
, another subclass of Pixel
>>> class ColorPixel(Pixel):
... __slots__ = ('color',) # (1)
>>> cp = ColorPixel()
>>> cp.__dict__ # (2)
Traceback (most recent call last):
...
AttributeError: 'ColorPixel' object has no attribute '__dict__'
>>> cp.x = 2
>>> cp.color = 'blue' # (3)
>>> cp.flavor = 'banana'
Traceback (most recent call last):
...
AttributeError: 'ColorPixel' object has no attribute 'flavor'
-
Em resumo, o
__slots__
da superclasse é adicionado ao__slots__
da classe atual. Não esqueça que tuplas com um único elemento devem ter uma vírgula no final. -
Instâncias de
ColorPixel
não tem um__dict__
. -
Você pode definir atributos declarados no
__slots__
dessa classe e nos de suas superclasses, mas nenhum outro.
Curiosamente, também é possível colocar o nome '__dict__'
em __slots__
.
Neste caso, as instâncias vão manter os atributos nomeados em __slots__
num array de referências da instância,
mas também vão aceitar atributos criados dinamicamente, que serão armazenados no habitual
__dict__
.
Isso é necessário para usar o decorador @cached_property
(tratado na Seção 22.3.5).
Naturalmente, incluir __dict__
em __slots__
pode desviar completamente do objetivo deste último,
dependendo do número de atributos estáticos e dinâmicos em cada instância, e de como eles são usados.
Otimização descuidada é pior que otimização prematura: adiciona complexidade sem colher qualquer benefício.
Outro atributo de instância especial que você pode querer manter é __weakref__
, necessário para que objetos suportem referências fracas (mencionadas brevemente na Seção 6.6).
Esse atributo existe por default em instâncias de classes definidas pelo usuário.
Entretanto, se a classe define __slots__
, e é necessário que as instâncias possam ser alvo de referências fracas, então é preciso incluir __weakref__
entre os atributos nomeados em
__slots__
.
Vejamos agora o efeito da adição de __slots__
a Vector2d
.
11.11.1. Uma medida simples da economia gerada por __slots__
Exemplo 199 mostra a implementação de __slots__
em Vector2d
.
__slots__
é a única adição a Vector2d
class Vector2d:
__match_args__ = ('x', 'y') # (1)
__slots__ = ('__x', '__y') # (2)
typecode = 'd'
# methods are the same as previous version
-
__match_args__
lista os nomes dos atributos públicos, para pattern matching posicional. -
__slots__
, por outro lado, lista os nomes dos atributos de instância, que neste caso são atributos privados.
Para medir a economia de memória, escrevi o script mem_test.py.
Ele recebe, como argumento de linha de comando, o nome de um módulo com uma variante da classe Vector2d
, e usa uma compreensão de lista para criar uma list
com 10.000.000 de instâncias de Vector2d
.
Na primeira execução, vista no Exemplo 200, usei vector2d_v3.Vector2d
(do Exemplo 190); na segunda execução usei a versão com __slots__
do Exemplo 199.
Vector2d
, usando a classe definida no módulo nomeado$ time python3 mem_test.py vector2d_v3
Selected Vector2d type: vector2d_v3.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 6,983,680
Final RAM usage: 1,666,535,424
real 0m11.990s
user 0m10.861s
sys 0m0.978s
$ time python3 mem_test.py vector2d_v3_slots
Selected Vector2d type: vector2d_v3_slots.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 6,995,968
Final RAM usage: 577,839,104
real 0m8.381s
user 0m8.006s
sys 0m0.352s
Como revela o Exemplo 200, o uso de RAM do script cresce para 1,55 GB quando o __dict__
de instância é usado em cada uma das 10 milhões de instâncias de Vector2d
, mas isso se reduz a 551 MB quando Vector2d
tem um atributo __slots__
. A versão com __slots__
também é mais rápida. O script mem_test.py neste teste lida basicamente com o carregamento do módulo, a medição da memória utilizada e a formatação de resultados. O código-fonte pode ser encontrado no repositório fluentpython/example-code-2e.
👉 Dica
|
Se você precisa manipular milhões de objetos com dados numéricos, deveria na verdade estar usando os arrays do NumPy (veja a Seção 2.10.3), que são eficientes no de uso de memória, e também tem funções para processamento numérico extremamente otimizadas, muitas das quais operam sobre o array inteiro ao mesmo tempo. Projetei a classe |
11.11.2. Resumindo os problemas com __slots__
O atributo de classe __slots__
pode proporcionar uma economia significativa de memória se usado corretamente, mas existem algumas ressalvas:
-
É preciso lembrar de redeclarar
__slots__
em cada subclasse, para evitar que suas instâncias tenham um__dict__
. -
Instâncias só poderão ter os atributos listados em
__slots__
, a menos que__dict__
seja incluído em__slots__
(mas isso pode anular a economia de memória). -
Classe que usam
__slots__
não podem usar o decorador@cached_property
, a menos que nomeiem__dict__
explicitamente em__slots__
. -
Instâncias não podem ser alvo de referências fracas, a menos que
__weakref__
seja incluído em__slots__
.
O último tópico do capítulo trata da sobreposição de um atributo de classe em instâncias e subclasses.
11.12. Sobrepondo atributos de classe
Um recurso característico de Python é a forma como atributos de classe podem ser usados como valores default para atributos de instância. Vector2d
contém o atributo de classe typecode
. Ele é usado duas vezes no método __bytes__
, mas é lido intencionalmente como self.typecode
. As instâncias de Vector2d
são criadas sem um atributo typecode
próprio, então self.typecode
vai, por default, se referir ao atributo de classe Vector2d.typecode
.
Mas se incluirmos um atributo de instância que não existe, estamos criando um novo atributo de instância—por exemplo, um atributo de instância typecode
—e o atributo de classe com o mesmo nome permanece intocado. Entretanto, daí em diante, sempre que algum código referente àquela instância contiver self.typecode
, o typecode
da instância será usado, na prática escondendo o atributo de classe de mesmo nome. Isso abre a possibilidade de personalizar uma instância individual com um typecode
diferente.
O Vector2d.typecode
default é 'd'
: isso significa que cada componente do vetor será representado como um número de ponto flutuante de dupla precisão e 8 bytes de tamanho quando for exportado para bytes
. Se definirmos o typecode
de uma instância Vector2d
como 'f'
antes da exportação, cada componente será exportado como um número de ponto flutuante de precisão simples e 4 bytes de tamanho.. O Exemplo 201 demonstra isso.
✒️ Nota
|
Estamos falando do acréscimo de um atributo de instância, assim o Exemplo 201 usa a implementação de |
typecode
, que antes era herdado da classe>>> from vector2d_v3 import Vector2d
>>> v1 = Vector2d(1.1, 2.2)
>>> dumpd = bytes(v1)
>>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'
>>> len(dumpd) # (1)
17
>>> v1.typecode = 'f' # (2)
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf) # (3)
9
>>> Vector2d.typecode # (4)
'd'
-
A representação default em
bytes
tem 17 bytes de comprimento. -
Define
typecode
como'f'
na instânciav1
. -
Agora
bytes
tem 9 bytes de comprimento. -
Vector2d.typecode
não foi modificado; apenas a instânciav1
usa otypecode
'f'
.
Isso deixa claro porque a exportação para bytes
de um Vector2d
tem um prefixo typecode
: queríamos suportar diferentes formatos de exportação.
Para modificar um atributo de classe, é preciso redefini-lo diretamente na classe, e não através de uma instância. Poderíamos modificar o typecode
default para todas as instâncias (que não tenham seu próprio typecode
) assim:
>>> Vector2d.typecode = 'f'
Porém, no Python, há uma maneira idiomática de obter um efeito mais permanente, e de ser mais explícito sobre a modificação. Como atributos de classe são públicos, eles são herdados por subclasses. Então é uma prática comum fazer a subclasse personalizar um atributo da classe. As views baseadas em classes do Django usam amplamente essa técnica. O Exemplo 202 mostra como se faz.
ShortVector2d
é uma subclasse de Vector2d
, que apenas sobrepõe o typecode
default>>> from vector2d_v3 import Vector2d
>>> class ShortVector2d(Vector2d): # (1)
... typecode = 'f'
...
>>> sv = ShortVector2d(1/11, 1/27) # (2)
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035) # (3)
>>> len(bytes(sv)) # (4)
9
-
Cria
ShortVector2d
como uma subclasse deVector2d
apenas para sobrepor o atributo de classetypecode
. -
Cria
sv
, uma instância deShortVector2d
, para demonstração. -
Verifica o
repr
desv
. -
Verifica que a quantidade de bytes exportados é 9, e não 17 como antes.
Esse exemplo também explica porque não escrevi explicitamente o class_name
em Vector2d.__repr__
, optando por obtê-lo de type(self).__name__
, assim:
# inside class Vector2d:
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
Se eu tivesse escrito o class_name
explicitamente, subclasses de Vector2d
como ShortVector2d
teriam que sobrescrever __repr__
só para mudar o class_name
. Lendo o nome do type
da instância, tornei __repr__
mais seguro de ser herdado.
Aqui termina nossa conversa sobre a criação de uma classe simples, que se vale do modelo de dados para se adaptar bem ao restante de Python: oferecendo diferentes representações do objeto, fornecendo um código de formatação personalizado, expondo atributos somente para leitura e suportando hash()
para se integrar a conjuntos e mapeamentos.
11.13. Resumo do capítulo
O objetivo desse capítulo foi demonstrar o uso dos métodos especiais e as convenções na criação de uma classe pythônica bem comportada.
Será vector2d_v3.py (do Exemplo 194) mais pythônica que vector2d_v0.py (do Exemplo 185)? A classe Vector2d
em vector2d_v3.py com certeza utiliza mais recursos de Python. Mas decidir qual das duas implementações de Vector2d
é mais adequada, a primeira ou a última, depende do contexto onde a classe será usada. O "Zen of Python" (Zen de Python), de Tim Peter, diz:
Simples é melhor que complexo.
Um objeto deve ser tão simples quanto seus requerimentos exigem—e não um desfile de recursos da linguagem. Se o código for parte de uma aplicação, ele deveria se concentrar naquilo que for necessário para suportar os usuários finais, e nada mais.
Se o código for parte de uma biblioteca para uso por outros programadores, então é razoável implementar métodos especiais que suportam comportamentos esperados por pythonistas.
Por exemplo, __eq__
pode não ser necessário para suportar um requisito do negócio, mas torna a classe mais fácil de testar.
Minha meta, ao expandir o código do Vector2d
, foi criar um contexto para a discussão dos métodos especiais e das convenções de programação em Python.
Os exemplos neste capítulo demonstraram vários dos métodos especiais vistos antes na Tabela 1 (do Capítulo 1):
-
Métodos de representação de strings e bytes:
__repr__
,__str__
,__format__
e__bytes__
-
Métodos para reduzir um objeto a um número:
__abs__
,__bool__
e__hash__
-
O operador
__eq__
, para suportar testes e hashing (juntamente com__hash__
)
Quando suportamos a conversão para bytes
, também implementamos um construtor alternativo, Vector2d.frombytes()
, que nos deu um contexto para falar dos decoradores @classmethod
(muito conveniente) e @staticmethod
(não tão útil: funções a nível do módulo são mais simples). O método frombytes
foi inspirado pelo método de mesmo nome na classe array.array
.
Vimos que a Mini-Linguagem de Especificação de Formato é extensível, ao implementarmos um método __format__
que analisa uma format_spec
fornecida à função embutida format(obj, format_spec)
ou dentro de campos de substituição '{:«format_spec»}'
em f-strings ou ainda strings usadas com o método str.format()
.
Para preparar a transformação de instâncias de Vector2d
em hashable, fizemos um esforço para torná-las imutáveis, ao menos prevenindo modificações acidentais, programando os atributos x
e y
como privados, e expondo-os como propriedades apenas para leitura. Nós então implementamos
__hash__
usando a técnica recomendada, aplicar o operador xor aos hashes dos atributos da instância.
Discutimos a seguir a economia de memória e as ressalvas de se declarar um atributo __slots__
em Vector2d
. Como o uso de __slots__
tem efeitos colaterais, ele só faz real sentido quando é preciso processar um número muito grande de instâncias—pense em milhões de instâncias, não apenas milhares. Em muitos destes casos, usar a pandas pode ser a melhor opção.
O último tópico tratado foi a sobreposição de um atributo de classe acessado através das instâncias (por exemplo, self.typecode
). Fizemos isso primeiro criando um atributo de instância, depois criando uma subclasse e sobrescrevendo o atributo no nível da classe.
Por todo o capítulo, apontei como escolhas de design nos exemplos foram baseadas no estudo das APIs dos objetos padrão de Python. Se esse capítulo pode ser resumido em uma só frase, seria essa:
Para criar objetos pythônicos, observe como se comportam objetos reais de Python.
11.14. Leitura complementar
Este capítulo tratou de vários dos métodos especiais do modelo de dados, então naturalmente as referências primárias são as mesmas do Capítulo 1, onde tivemos uma ideia geral do mesmo tópico. Por conveniência, vou repetir aquelas quatro recomendações anteriores aqui, e acrescentar algumas outras:
- O capítulo "Modelo de Dados" em A Referência da Linguagem Python
-
A maioria dos métodos usados nesse capítulo estão documentados em "3.3.1. Personalização básica".
- Python in a Nutshell, 3ª ed., de Alex Martelli, Anna Ravenscroft, e Steve Holden
-
Trata com profundidade dos métodos especiais .
- Python Cookbook, 3ª ed., de David Beazley e Brian K. Jones
-
Práticas modernas de Python demonstradas através de receitas. Especialmente o Capítulo 8, "Classes and Objects" (Classes e Objetos), que contém várias receitas relacionadas às discussões deste capítulo.
- Python Essential Reference, 4ª ed., de David Beazley
-
Trata do modelo de dados em detalhes, apesar de falar apenas de Python 2.6 e do 3.0 (na quarta edição). Todos os conceitos fundamentais são os mesmos, e a maior parte das APIs do Modelo de Dados não mudou nada desde Python 2.2, quando os tipos embutidos e as classes definidas pelo usuário foram unificados.
Em 2015—o ano que terminei a primeira edição de Python Fluente—Hynek Schlawack começou a desenvolver o pacote attrs
. Da documentação de attrs
:
attrs
é um pacote Python que vai trazer de volta a alegria de criar classes, liberando você do tedioso trabalho de implementar protocolos de objeto (também conhecidos como métodos dunder)
Mencionei attrs
como uma alternativa mais poderosa ao @dataclass
na Seção 5.10.
As fábricas de classes de dados do Capítulo 5, assim como attrs
, equipam suas classes automaticamente com vários métodos especiais. Mas saber como programar métodos especiais ainda é essencial para entender o que aqueles pacotes fazem, para decidir se você realmente precisa deles e para—quando necessário—sobrescrever os métodos que eles geram.
Vimos neste capítulo todos os métodos especiais relacionados à representação de objetos, exceto
__index__
e __fspath__
.
Discutiremos __index__
no Capítulo 12, na Seção 12.5.2.
Não vou tratar de __fspath__
. Para aprender sobre esse método, veja a PEP 519—Adding a file system path protocol (Adicionando um protocolo de caminho de sistema de arquivos) (EN).
Uma percepção precoce da necessidade de strings de representação diferentes para objetos apareceu em Smalltalk. O artigo de 1996 "How to Display an Object as a String: printString and displayString" (Como Mostrar um Objeto como uma String: printString and displayString) (EN), de Bobby Woolf, discute a implementação dos métodos printString
e displayString
naquela linguagem. Foi desse artigo que peguei emprestado as expressivas descrições "como o desenvolvedor quer vê-lo" e "como o usuário quer vê-lo" para definir repr()
e str()
, na Seção 11.2.
12. Métodos especiais para sequências
Não queira saber se aquilo é-um pato: veja se ele grasna-como-um pato, anda-como-um pato, etc., etc., dependendo de qual subconjunto exato de comportamentos de pato você precisa para usar em seus jogos de linguagem. (
comp.lang.python
, Jul. 26, 2000)
Neste capítulo, vamos criar uma classe Vector
, para representar um vetor multidimensional—um avanço significativo sobre o Vector2D
bidimensional do Capítulo 11.
Vector
vai se comportar como uma simples sequência imutável padrão de Python.
Seus elementos serão números de ponto flutuante, e ao final do capítulo a classe suportará o seguinte:
-
O protocolo de sequência básico:
__len__
e__getitem__
-
Representação segura de instâncias com muitos itens
-
Suporte adequado a fatiamento, produzindo novas instâncias de
Vector
-
Hashing agregado, levando em consideração o valor de cada elemento contido na sequência
-
Um extensão personalizada da linguagem de formatação
Também vamos implementar, com __getattr__
, o acesso dinâmico a atributos, como forma de substituir as propriedades apenas para leitura que usamos no Vector2d
—apesar disso não ser típico de tipos sequência.
Nossa apresentação voltada para o código será interrompida por uma discussão conceitual sobre a ideia de protocolos como uma interface informal. Vamos discutir a relação entre protocolos e duck typing, e as implicações práticas disso na criação de seus próprios tipos
12.1. Novidades nesse capítulo
Não ocorreu qualquer grande modificação neste capítulo. Há uma breve discussão nova sobre o typing.Protocol
em um quadro de dicas, no final da Seção 12.4.
Na Seção 12.5.2, a implementação do __getitem__
no Exemplo 210 está mais concisa e robusta que o exemplo na primeira edição, graças ao duck typing e ao operator.index
.
Essa mudança foi replicada para as implementações seguintes de Vector
aqui e no Capítulo 16.
Vamos começar.
12.2. Vector: Um tipo sequência definido pelo usuário
Nossa estratégia na implementação de Vector
será usar composição, não herança. Vamos armazenar os componentes em um array de números de ponto flutuante, e implementar os métodos necessários para que nossa classe Vector
se comporte como uma sequência plana imutável.
Mas antes de implementar os métodos de sequência, vamos desenvolver uma implementação básica de Vector
compatível com nossa classe Vector2d
, vista anteriormente—exceto onde tal compatibilidade não fizer sentido.
12.3. Vector versão #1: compatível com Vector2d
A primeira versão de Vector
deve ser tão compatível quanto possível com nossa classe Vector2d
desenvolvida anteriormente.
Entretanto, pela própria natureza das classes, o construtor de Vector
não é compatível com o construtor de Vector2d
. Poderíamos fazer Vector(3, 4)
e Vector(3, 4, 5)
funcionarem, recebendo argumentos arbitrários com *args
em __init__
. Mas a melhor prática para um construtor de sequências é receber os dados através de um argumento iterável, como fazem todos os tipos embutidos de sequências. O Exemplo 205 mostra algumas maneiras de instanciar objetos do nosso novo Vector
.
Vector.__init__
e Vector.__repr__
>>> Vector([3.1, 4.2])
Vector([3.1, 4.2])
>>> Vector((3, 4, 5))
Vector([3.0, 4.0, 5.0])
>>> Vector(range(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
Exceto pela nova assinatura do construtor, me assegurei que todos os testes realizados com Vector2d
(por exemplo, Vector2d(3, 4)
) são bem sucedidos e produzem os mesmos resultados com um Vector
de dois componentes, como Vector([3, 4])
.
⚠️ Aviso
|
Quando um |
O Exemplo 206 lista a implementação de nossa primeira versão de Vector
(esse exemplo usa como base o código mostrado no #ex_vector2d_v0 e no #ex_vector2d_v1 do Capítulo 11).
from array import array
import reprlib
import math
class Vector:
typecode = 'd'
def __init__(self, components):
self._components = array(self.typecode, components) # (1)
def __iter__(self):
return iter(self._components) # (2)
def __repr__(self):
components = reprlib.repr(self._components) # (3)
components = components[components.find('['):-1] # (4)
return f'Vector({components})'
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components)) # (5)
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.hypot(*self) # (6)
def __bool__(self):
return bool(abs(self))
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv) # (7)
-
O atributo de instância "protegido"
self._components
vai manter umarray
com os componentes doVector
. -
Para permitir iteração, devolvemos um itereador sobre self._components.[134]
-
Usa
reprlib.repr()
para obter um representação de tamanho limitado deself._components
(por exemplo,array('d', [0.0, 1.0, 2.0, 3.0, 4.0, …])
). -
Remove o prefixo
array('d',
e o)
final, antes de inserir a string em uma chamada ao construtor deVector
. -
Cria um objeto
bytes
diretamente deself._components
. -
Desde Python 3.8,
math.hypot
aceita pontos N-dimensionais. Já usei a seguinte expressão antes:math.sqrt(sum(x * x for x in self))
. -
A única mudança necessária no
frombytes
anterior é na última linha: passamos amemoryview
diretamente para o construtor, sem desempacotá-la com*
, como fazíamos antes.
O modo como usei reprlib.repr
pede alguma elaboração. Essa função produz representações seguras de estruturas grandes ou recursivas, limitando a tamanho da string devolvida e marcando o corte com '…'
. Eu queria que o repr
de um Vector
se parecesse com Vector([3.0, 4.0, 5.0])
e não com Vector(array('d', [3.0, 4.0, 5.0]))
, porque a existência de um array
dentro de um Vector
é um detalhe de implementação. Como essas chamadas ao construtor criam objetos Vector
idênticos, preferi a sintaxe mais simples, usando um argumento list
.
Ao escrever o __repr__
, poderia ter produzido uma versão para exibição simplificada de components
com essa expressão: reprlib.repr(list(self._components))
. Isso, entretanto, geraria algum desperdício, pois eu estaria copiando cada item de self._components
para uma list
apenas para usar a list
no repr
. Em vez disso, decidi aplicar reprlib.repr
diretamente no array self._components
, e então remover os caracteres fora dos []
. É isso o que faz a segunda linha do
__repr__
no Exemplo 206.
👉 Dica
|
Por seu papel na depuração, chamar |
Observe que os métodos __str__
, __eq__
, e __bool__
são idênticos a suas versões em Vector2d
, e apenas um caractere mudou em frombytes
(um *
foi removido na última linha). Isso é um dos benefícios de termos tornado o Vector2d
original iterável.
Aliás, poderíamos ter criado Vector
como uma subclasse de Vector2d
, mas escolhi não fazer isso por duas razões. Em primeiro lugar, os construtores incompatíveis de fato tornam a relação de super/subclasse desaconselhável. Eu até poderia contornar isso como um tratamento engenhoso dos parâmetros em
__init__
, mas a segunda razão é mais importante: queria que Vector
fosse um exemplo independente de uma classe que implementa o protocolo de sequência. É o que faremos a seguir, após uma discussão sobre o termo protocolo.
12.4. Protocolos e o duck typing
Já no Capítulo 1, vimos que não é necessário herdar de qualquer classe em especial para criar um tipo sequência completamente funcional em Python; basta implementar os métodos que satisfazem o protocolo de sequência. Mas de que tipo de protocolo estamos falando?
No contexto da programação orientada a objetos, um protocolo é uma interface informal, definida apenas na documentação (e não no código). Por exemplo, o protocolo de sequência no Python implica apenas no métodos __len__
e __getitem__
. Qualquer classe Spam
, que implemente esses métodos com a assinatura e a semântica padrões, pode ser usada em qualquer lugar onde uma sequência for esperada. É irrelevante se Spam
é uma subclasse dessa ou daquela outra classe; tudo o que importa é que ela fornece os métodos necessários. Vimos isso no Exemplo 1, reproduzido aqui no Exemplo 207.
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
A classe FrenchDeck
, no Exemplo 207, pode tirar proveito de muitas facilidades de Python por implementar o protocolo de sequência, mesmo que isso não esteja declarado em qualquer ponto do código.
Um programador Python experiente vai olhar para ela e entender que aquilo é uma sequência, mesmo sendo apenas uma subclasse de object
.
Dizemos que ela é uma sequênca porque ela se comporta como uma sequência, e é isso que importa.
Isso ficou conhecido como duck typing (literalmente "tipagem pato"), após o post de Alex Martelli citado no início deste capítulo.
Como protocolos são informais e não obrigatórios, muitas vezes é possível resolver nosso problema implementando apenas parte de um protocolo, se sabemos o contexto específico em que a classe será utilizada. Por exemplo, apenas __getitem__
basta para suportar iteração; não há necessidade de fornecer um __len__
.
👉 Dica
|
Com a PEP 544—Protocols: Structural subtyping (static duck typing) (Protocolos:sub-tipagem estrutural (duck typing estático)) (EN),
o Python 3.8 suporta classes protocolo: subclasses de |
Vamos agora implementar o protocolo sequência em Vector
, primeiro sem suporte adequado ao fatiamento, que acrescentaremos mais tarde.
12.5. Vector versão #2: Uma sequência fatiável
Como vimos no exemplo da classe FrenchDeck
, suportar o protocolo de sequência é muito fácil se você puder delegar para um atributo sequência em seu objeto, como nosso array self._components
. Esses __len__
e __getitem__
de uma linha são um bom começo:
class Vector:
# many lines omitted
# ...
def __len__(self):
return len(self._components)
def __getitem__(self, index):
return self._components[index]
Após tais acréscimos, agora todas as seguintes operações funcionam:
>>> v1 = Vector([3, 4, 5])
>>> len(v1)
3
>>> v1[0], v1[-1]
(3.0, 5.0)
>>> v7 = Vector(range(7))
>>> v7[1:4]
array('d', [1.0, 2.0, 3.0])
Como se vê, até o fatiamento é suportado—mas não muito bem. Seria melhor se uma fatia de um Vector
fosse também uma instância de Vector
, e não um array
. A antiga classe FrenchDeck
tem um problema similar: quando ela é fatiada, o resultado é uma list
. No caso de Vector
, muito da funcionalidade é perdida quando o fatiamento produz arrays simples.
Considere os tipos sequência embutidos: cada um deles, ao ser fatiado, produz uma nova instância de seu próprio tipo, e não de algum outro tipo.
Para fazer Vector
produzir fatias como instâncias de Vector
, não podemos simplesmente delegar o fatiamento para array
.
Precisamos analisar os argumentos recebidos em __getitem__
e fazer a coisa certa.
Vejamos agora como Python transforma a sintaxe my_seq[1:3]
em argumentos para my_seq.__getitem__(...)
.
12.5.1. Como funciona o fatiamento
Uma demonstração vale mais que mil palavras, então dê uma olhada no Exemplo 208.
__getitem__
e fatias>>> class MySeq:
... def __getitem__(self, index):
... return index # (1)
...
>>> s = MySeq()
>>> s[1] # (2)
1
>>> s[1:4] # (3)
slice(1, 4, None)
>>> s[1:4:2] # (4)
slice(1, 4, 2)
>>> s[1:4:2, 9] # (5)
(slice(1, 4, 2), 9)
>>> s[1:4:2, 7:9] # (6)
(slice(1, 4, 2), slice(7, 9, None))
-
Para essa demonstração, o método
__getitem__
simplesmente devolve o que for passado a ele. -
Um único índice, nada de novo.
-
A notação
1:4
se tornaslice(1, 4, None)
. -
slice(1, 4, 2)
significa comece em 1, pare em 4, ande de 2 em 2. -
Surpresa: a presença de vírgulas dentro do
[]
significa que__getitem__
recebe uma tupla. -
A tupla pode inclusive conter vários objetos
slice
.
Vamos agora olhar mais de perto a própria classe slice
, no Exemplo 209.
slice
>>> slice # (1)
<class 'slice'>
>>> dir(slice) # (2)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__',
'__hash__', '__init__', '__le__', '__lt__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'indices', 'start', 'step', 'stop']
-
slice
é um tipo embutido (que já vimos antes na Seção 2.7.2). -
Inspecionando uma
slice
descobrimos os atributos de dadosstart
,stop
, estep
, e um métodoindices
.
No Exemplo 209, a chamada dir(slice)
revela um atributo indices
, um método pouco conhecido mas muito interessante. Eis o que diz help(slice.indices)
:
S.indices(len) → (start, stop, stride)
-
Supondo uma sequência de tamanho
len
, calcula os índicesstart
(início) estop
(fim), e a extensão dostride
(passo) da fatia estendida descrita porS
. Índices fora dos limites são recortados, exatamente como acontece em uma fatia normal.
Em outras palavras, indices
expõe a lógica complexa implementada nas sequências embutidas, para lidar graciosamente com índices inexistentes ou negativos e com fatias maiores que a sequência original. Esse método produz tuplas "normalizadas" com os inteiros não-negativos start
, stop
, e stride
ajustados para uma sequência de um dado tamanho.
Aqui estão dois exemplos, considerando uma sequência de len == 5
, por exemplo 'ABCDE'
:
>>> slice(None, 10, 2).indices(5) # (1)
(0, 5, 2)
>>> slice(-3, None, None).indices(5) # (2)
(2, 5, 1)
-
'ABCDE'[:10:2]
é o mesmo que'ABCDE'[0:5:2]
. -
'ABCDE'[-3:]
é o mesmo que'ABCDE'[2:5:1]
.
No código de nosso Vector
não vamos precisar do método slice.indices()
,
pois quando recebermos uma fatia como argumento vamos delegar seu tratamento para o array
interno _components
.
Mas quando você não puder contar com os serviços de uma sequência subjacente,
esse método ajuda evita a necessidade de implementar uma lógica sutil.
Agora que sabemos como tratar fatias, vamos ver a implementação aperfeiçoada de Vector.__getitem__
.
12.5.2. Um __getitem__ que trata fatias
O Exemplo 210 lista os dois métodos necessários para fazer Vector
se comportar como uma sequência: __len__
e __getitem__
(com o último implementado para tratar corretamente o fatiamento).
__len__
e __getitem__
adicionados à classe Vector
, de vector_v1.py (no Exemplo 206) def __len__(self):
return len(self._components)
def __getitem__(self, key):
if isinstance(key, slice): # (1)
cls = type(self) # (2)
return cls(self._components[key]) # (3)
index = operator.index(key) # (4)
return self._components[index] # (5)
-
Se o argumento
key
é umaslice
… -
…obtém a classe da instância (isto é,
Vector
) e… -
…invoca a classe para criar outra instância de
Vector
a partir de uma fatia do array_components
. -
Se podemos obter um
index
dekey
… -
…devolve o item específico de
_components
.
A função operator.index()
chama o método especial __index__
.
A função e o método especial foram definidos na PEP 357—Allowing Any Object to be Used for Slicing (Permitir que Qualquer Objeto seja Usado para Fatiamento) (EN), proposta por Travis Oliphant, para permitir que qualquer um dos numerosos tipos de inteiros na NumPy fossem usados como argumentos de índices e fatias. A diferença principal entre operator.index()
e int()
é que o primeiro foi projetado para esse propósito específico. Por exemplo, int(3.14)
devolve 3
, mas operator.index(3.14)
gera um TypeError
, porque um float
não deve ser usado como índice.
✒️ Nota
|
O uso excessivo de |
Após a adição do código do Exemplo 210 à classe Vector
class, temos o comportamento apropriado para fatiamento, como demonstra o Exemplo 211 .
Vector.__getitem__
aperfeiçoado, do Exemplo 210 >>> v7 = Vector(range(7))
>>> v7[-1] # (1)
6.0
>>> v7[1:4] # (2)
Vector([1.0, 2.0, 3.0])
>>> v7[-1:] # (3)
Vector([6.0])
>>> v7[1,2] # (4)
Traceback (most recent call last):
...
TypeError: 'tuple' object cannot be interpreted as an integer
-
Um índice inteiro recupera apenas o valor de um componente, um
float
. -
Uma fatia como índice cria um novo
Vector
. -
Um fatia de
len == 1
também cria umVector
. -
Vector
não suporta indexação multidimensional, então tuplas de índices ou de fatias geram um erro.
12.6. Vector versão #3: acesso dinâmico a atributos
Ao evoluir Vector2d
para Vector
, perdemos a habilidade de acessar os componentes do vetor por nome (por exemplo, v.x
, v.y
). Agora estamos trabalhando com vetores que podem ter um número grande de componentes. Ainda assim, pode ser conveniente acessar os primeiros componentes usando letras como atalhos, algo como x
, y
, z
em vez de v[0]
, v[1]
, and v[2]
.
Aqui está a sintaxe alternativa que queremos oferecer para a leitura dos quatro primeiros componentes de um vetor:
>>> v = Vector(range(10))
>>> v.x
0.0
>>> v.y, v.z, v.t
(1.0, 2.0, 3.0)
No Vector2d
, oferecemos acesso somente para leitura a x
e y
através do decorador @property
(veja o Exemplo 190). Poderíamos incluir quatro propriedades no Vector
, mas isso seria tedioso. O método especial __getattr__
nos fornece uma opção melhor.
O método __getattr__
é invocado pelo interpretador quando a busca por um atributo falha.
Simplificando, dada a expressão my_obj.x
, Python verifica se a instância de my_obj
tem um atributo chamado x
;
em caso negativo, a busca passa para a classe (my_obj.__class__
) e depois sobe pelo diagrama de herança.[135] Se por fim o atributo x
não for encontrado, o método __getattr__
, definido na classe de my_obj
, é chamado com self
e o nome do atributo em formato de string (por exemplo, 'x'
).
O Exemplo 212 lista nosso método __getattr__
. Ele basicamente verifica se o atributo desejado é uma das letras xyzt
. Em caso positivo, devolve o componente correspondente do vetor.
__getattr__
acrescentado à classe Vector
__match_args__ = ('x', 'y', 'z', 't') # (1)
def __getattr__(self, name):
cls = type(self) # (2)
try:
pos = cls.__match_args__.index(name) # (3)
except ValueError: # (4)
pos = -1
if 0 <= pos < len(self._components): # (5)
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' # (6)
raise AttributeError(msg)
-
Define
__match_args__
para permitir pattern matching posicional sobre os atributos dinâmicos suportados por__getattr__
.[136] -
Obtém a classe de
Vector
, para uso posterior. -
Tenta obter a posição de
name
em__match_args__
. -
.index(name)
gera umValueError
quandoname
não é encontrado; definepos
como-1
. (Eu preferiria usar algo comostr.find
aqui, mastuple
não implementa esse método.) -
Se
pos
está dentro da faixa de componentes disponíveis, devolve aquele componente. -
Se chegamos até aqui, gera um
AttributeError
com uma mensagem de erro padrão.
Não é difícil implementar __getattr__
, mas neste caso não é o suficiente. Observe a interação bizarra no Exemplo 213.
v.x
não gera um erro, mas introduz uma inconsistência>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x # (1)
0.0
>>> v.x = 10 # (2)
>>> v.x # (3)
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0]) # (4)
-
Acessa o elemento
v[0]
comov.x
. -
Atribui um novo valor a
v.x
. Isso deveria gera uma exceção. -
Ler
v.x
obtém o novo valor,10
. -
Entretanto, os componentes do vetor não mudam.
Você consegue explicar o que está acontecendo?
Em especial, por que v.x
devolve 10
na segunda consulta (<3>), se aquele valor não está presente no array de componentes do vetor?
Se você não souber responder de imediato, estude a explicação de __getattr__
que aparece logo antes do Exemplo 212.
A razão é um pouco sutil, mas é um alicerce fundamental para entender grande parte do que veremos mais tarde no livro.
Após pensar um pouco sobre essa questão, siga em frente e leia a explicação para o que aconteceu.
A inconsistência no Exemplo 213 ocorre devido à forma como __getattr__
funciona: Python só chama esse método como último recurso, quando o objeto não contém o atributo nomeado. Entretanto, após atribuirmos v.x = 10
, o objeto v
agora contém um atributo x
,
e então __getattr__
não será mais invocado para obter v.x
: o interpretador vai apenas devolver o valor 10
, que agora está vinculado a v.x
. Por outro lado, nossa implementação de
__getattr__
não leva em consideração qualquer atributo de instância diferente de
self._components
, de onde ele obtém os valores dos "atributos virtuais" listados em __match_args__
.
Para evitar essa inconsistência, precisamos modificar a lógica de definição de atributos em nossa classe Vector
.
Como você se lembra, nos nossos últimos exemplos de Vector2d
no Capítulo 11, tentar atribuir valores aos atributos de instância .x
ou .y
gerava um AttributeError
. Em Vector
, queremos produzir a mesma exceção em resposta a tentativas de atribuição a qualquer nome de atributo com um única letra, só para evitar confusão. Para fazer isso, implementaremos __setattr__
, como listado no Exemplo 214.
__setattr__
na classe Vector
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1: # (1)
if name in cls.__match_args__: # (2)
error = 'readonly attribute {attr_name!r}'
elif name.islower(): # (3)
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = '' # (4)
if error: # (5)
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value) # (6)
-
Tratamento especial para nomes de atributos com uma única letra.
-
Se
name
está em__match_args__
, configura mensagens de erro específicas. -
Se
name
é uma letra minúscula, configura a mensagem de erro sobre todos os nomes de uma única letra. -
Caso contrário, configura uma mensagem de erro vazia.
-
Se existir uma mensagem de erro não-vazia, gera um
AttributeError
. -
Caso default: chama
__setattr__
na superclasse para obter o comportamento padrão.
👉 Dica
|
A função |
Ao escolher a menssagem de erro para mostrar com AttributeError
, primeiro eu verifiquei o comportamento do tipo embutido complex
, pois ele é imutável e tem um par de atributos de dados real
and imag
. Tentar mudar qualquer um dos dois em uma instância de complex
gera um AttributeError
com a mensagem "can’t set attribute"
("não é possível [re]-definir o atributo"). Por outro lado, a tentativa de modificar um atributo protegido por uma propriedade, como fizemos no Seção 11.7, produz a mensagem "read-only attribute"
("atributo apenas para leitura"). Eu me inspirei em ambas as frases para definir a string error
em __setitem__
, mas fui mais explícito sobre os atributos proibidos.
Observe que não estamos proibindo a modificação de todos os atributos, apenas daqueles com nomes compostos por uma única letra minúscula, para evitar conflitos com os atributos apenas para leitura suportados, x
, y
, z
, e t
.
⚠️ Aviso
|
Sabendo que declarar |
Mesmo não suportando escrita nos componentes de Vector
, aqui está uma lição importante deste exemplo: muitas vezes, quando você implementa __getattr__
, é necessário também escrever o __setattr__
, para evitar comportamentos inconsistentes em seus objetos.
Para permitir a modificação de componentes, poderíamos implementar __setitem__
, para permitir v[0] = 1.1
, e/ou __setattr__
, para fazer v.x = 1.1
funcionar. Mas Vector
permanecerá imutável, pois queremos torná-lo hashable, na próxima seção.
12.7. Vector versão #4: o hash e um == mais rápido
Vamos novamente implementar um método __hash__
.
Juntamente com o __eq__
existente, isso tornará as instâncias de Vector
hashable.
O __hash__
do Vector2d
(no Exemplo 191) computava o hash de uma tuple
construída com os dois componentes, self.x
and self.y
.
Nós agora podemos estar lidando com milhares de componentes, então criar uma tuple
pode ser caro demais. Em vez disso, vou aplicar sucessivamente o operador ^
(xor) aos hashes de todos os componentes, assim: v[0] ^ v[1] ^ v[2]
. É para isso que serve a função functools.reduce
. Anteriormente afirmei que reduce
não é mais tão popular quanto antes,[137] mas computar o hash de todos os componentes do vetor é um bom caso de uso para ela. A Figura 27 ilustra a ideia geral da função reduce
.
reduce
, sum
, any
, all
—produzem um único resultado agregado a partir de uma sequência ou de qualquer objeto iterável finito.Até aqui vimos que functools.reduce()
pode ser substituída por sum()
. Vamos agora explicar exatamente como ela funciona. A ideia chave é reduzir uma série de valores a um valor único. O primeiro argumento de reduce()
é uma função com dois argumentos, o segundo argumento é um iterável. Vamos dizer que temos uma função fn
, que recebe dois argumentos, e uma lista lst
. Quando chamamos reduce(fn, lst)
, fn
será aplicada ao primeiro par de elementos de lst
—fn(lst[0], lst[1])
—produzindo um primeiro resultado, r1
. Então fn
é aplicada a r1
e ao próximo elemento—fn(r1, lst[2])
—produzindo um segundo resultado, r2
. Agora fn(r2, lst[3])
é chamada para produzir r3
… e assim por diante, até o último elemento, quando finalmente um único elemento, rN
, é produzido e devolvido.
Aqui está como reduce
poderia ser usada para computar 5!
(o fatorial de 5):
>>> 2 * 3 * 4 * 5 # the result we want: 5! == 120
120
>>> import functools
>>> functools.reduce(lambda a,b: a*b, range(1, 6))
120
Voltando a nosso problema de hash, o Exemplo 215 demonstra a ideia da computação de um xor agregado, fazendo isso de três formas diferente: com um loop for
e com dois modos diferentes de usar reduce
.
>>> n = 0
>>> for i in range(1, 6): # (1)
... n ^= i
...
>>> n
1
>>> import functools
>>> functools.reduce(lambda a, b: a^b, range(6)) # (2)
1
>>> import operator
>>> functools.reduce(operator.xor, range(6)) # (3)
1
-
xor agregado com um loop
for
e uma variável de acumulação. -
functools.reduce
usando uma função anônima. -
functools.reduce
substituindo alambda
personalizada poroperator.xor
.
Das alternativas apresentadas no Exemplo 215, a última é minha favorita, e o loop for
vem a seguir. Qual sua preferida?
Como visto na Seção 7.8.1, operator
oferece a funcionalidade de todos os operadores infixos de Python em formato de função, diminuindo a necessidade do uso de lambda
.
Para escrever Vector.__hash__
no meu estilo preferido precisamos importar os módulos functools
e operator
. Exemplo 216 apresenta as modificações relevantes.
__hash__
adicionados à classe Vector
de vector_v3.pyfrom array import array
import reprlib
import math
import functools # (1)
import operator # (2)
class Vector:
typecode = 'd'
# many lines omitted in book listing...
def __eq__(self, other): # (3)
return tuple(self) == tuple(other)
def __hash__(self):
hashes = (hash(x) for x in self._components) # (4)
return functools.reduce(operator.xor, hashes, 0) # (5)
# more lines omitted...
-
Importa
functools
para usarreduce
. -
Importa
operator
para usarxor
. -
Não há mudanças em
__eq__
; listei-o aqui porque é uma boa prática manter__eq__
e__hash__
próximos no código-fonte, pois eles precisam trabalhar juntos. -
Cria uma expressão geradora para computar sob demanda o hash de cada componente.
-
Alimenta
reduce
comhashes
e a funçãoxor
, para computar o código hash agregado; o terceiro argumento,0
, é o inicializador (veja o próximo aviso).
⚠️ Aviso
|
Ao usar |
Da forma como está implementado, o método __hash__
no Exemplo 216 é um exemplo perfeito de uma computação de map-reduce (mapeia e reduz). Veja a (Figura 28).