Skip to content

Latest commit

 

History

History
2276 lines (1842 loc) · 129 KB

cap02.adoc

File metadata and controls

2276 lines (1842 loc) · 129 KB

Uma coleção de sequências

Como vocês podem ter notado, várias das operações mencionadas funcionam da mesma forma com textos, listas e tabelas. Coletivamente, textos, listas e tabelas são chamados de 'trens' (trains). [...] O comando `FOR` também funciona, de forma geral, em trens.

Leo Geurts, Lambert Meertens, e Steven Pembertonm, ABC Programmer's Handbook, p. 8. (Bosko Books)

Antes de criar o Python, Guido foi um dos desenvolvedores da linguagem ABC—um projeto de pesquisa de 10 anos para criar um ambiente de programação para iniciantes. A ABC introduziu várias ideias que hoje consideramos "pythônicas": operações genéricas com diferentes tipos de sequências, tipos tupla e mapeamento embutidos, estrutura [do código] por indentação, tipagem forte sem declaração de variáveis, entre outras. O Python não é assim tão amigável por acidente.

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

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

A maior parte da discussão deste capítulo se aplica às sequências em geral, desde a conhecida list até os tipos str e bytes, adicionados no Python 3. Tópicos específicos sobre listas, tuplas, arrays e filas também foram incluídos, mas os detalhes sobre strings Unicode e sequências de bytes são tratados no [strings_bytes_files]. 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 [user_defined_sequences].

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

Novidades neste capítulo

A atualização mais importante desse capítulo é a Pattern matching com sequências, primeira abordagem das instruções match/case introduzidas no Python 3.10.

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

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

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

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

Movi a discussão sobre tuplas nomeadas para a [classic_named_tuples_sec] no [data_class_ch], onde elas são comparadas com typing.NamedTuple e @dataclass.

Note

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.

Uma visão geral das sequências embutidas

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

Sequências contêiner

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

Sequências planas

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

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

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

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

Note

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

  • ob_refcnt: a contagem de referências ao objeto

  • ob_type: um ponteiro para o tipo do objeto

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

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

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

Sequências mutáveis

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

Sequências imutáveis

Por exemplo, tuple, str, e bytes.

A Figura 2 ajuda a visualizar como as sequências mutáveis herdam todos os métodos das sequências imutáveis e implementam vários métodos adicionais. Os tipos embutidos concretos de sequências na verdade não são subclasses das classes base abstratas (ABCs) Sequence e MutableSequence, mas sim subclasses virtuais registradas com aquelas ABCs—como veremos no [ifaces_prot_abc]. Por serem subclasses virtuais, tuple e list passam nesses testes:

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

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

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

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

Tip

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.

Compreensões de lista e legibilidade

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

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

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

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

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

Tip
Dica de sintaxe

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

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

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

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

  2. last permanece.

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

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

Listcomps versus map e filter

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

Considere o Exemplo 3.

Exemplo 3. A mesma lista, criada por uma listcomp e por uma composição de map/filter
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]

Eu acreditava que map e filter eram mais rápidas que as listcomps equivalentes, mas Alex Martelli assinalou que não é o caso—pelo menos não nos exemplos acima. O script listcomp_speed.py no repositório de código do Python Fluente é um teste de velocidade simples, comparando listcomp com filter/map.

Vou falar mais sobre map e filter no [functions_as_objects]. 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.

Produtos cartesianos

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

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

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

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

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

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

No [ex_pythonic_deck] (em [data_model]), 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.

Expressões geradoras

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

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

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

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

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

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

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

Note

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

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

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.

Tuplas como registros

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

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

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

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

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

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

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

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

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

Tip

Em geral, usar _ como variável descartável (dummy variable) é só uma convenção. É apenas um nome de variável estranho mas válido. Entretanto, em uma instrução match/case, o _ é um coringa que corresponde a qualquer valor, mas não está vinculado a um valor. Veja a Pattern matching com sequências. E no console do Python, o resultado do comando anterior é atribuído a _—a menos que o resultado seja None.

Muitas vezes pensamos em registros como estruturas de dados com campos nomeados. O [data_class_ch] apresenta duas formas de criar tuplas com campos nomeados.

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

Note

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 Desempacotando sequências e iteráveis 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.

Tuplas como listas imutáveis

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

Clareza

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

Desempenho

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

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

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

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

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

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

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 [tuple-relative-immutable].

Apesar dessa ressalva, as tuplas são frequentemente usadas como listas imutáveis. Elas oferecem algumas vantagens de desempenho, explicadas por uma dos desenvolvedores principais do Python, Raymond Hettinger, em uma resposta à questão "Are tuples more efficient than lists in Python?" (As tuplas são mais eficientes que as listas no Python?) no StackOverflow. Em resumo, Hettinger escreveu:

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

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

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

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

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 1, tuple suporta todos os métodos de list que não envolvem adicionar ou remover itens, com uma exceção—tuple não possui o método __reversed__. Entretanto, isso é só uma otimização; reversed(my_tuple) funciona sem esse método.

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

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

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

s.append(e)

Acrescenta um elemento após o último

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.count(e)

Conta as ocorrências de um elemento

s.__delitem__(p)

Remove o item na posição p

s.extend(it)

Acrescenta itens do iterável it

s.__getitem__(p)

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

s.__getnewargs__()

Suporte a serialização otimizada com pickle

s.index(e)

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

s.insert(p, e)

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

s.__iter__()

Obtém o iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

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

s.__rmul__(n)

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

s.pop([p])

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

s.remove(e)

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

s.reverse()

Reverte, no lugar, a ordem dos itens

s.__reversed__()

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

s.__setitem__(p, e)

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

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

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

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

Desempacotando sequências e iteráveis

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

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.

Usando * para recolher itens em excesso

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

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

>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])

No contexto da atribuição paralela, o prefixo * pode ser aplicado a exatamente uma variável, mas pode aparecer em qualquer posição:

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

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

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

Desempacotamento aninhado

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

Exemplo 8. Desempacotando tuplas aninhadas para acessar a longitude
link:code/02-array-seq/metro_lat_lon.py[role=include]
  1. Cada tupla contém um registro com quatro campos, o último deles um par de coordenadas.

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

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

A saída do Exemplo 8 é:

                |  latitude | longitude
Mexico City     |   19.4333 |  -99.1333
New York-Newark |   40.8086 |  -74.0204
São Paulo       |  -23.5478 |  -46.6358

O alvo da atribuição de um desempacotamento pode também ser uma lista, mas bons casos de uso aqui são raros. Aqui está o único que conheço: se você tem uma consulta de banco de dados que devolve um único registro (por exemplo, se o código SQL tem a instrução LIMIT 1), daí é possível desempacotar e ao mesmo tempo se assegurar que há apenas um resultado com o seguinte código:

>>> [record] = query_returning_single_row()

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

>>> [[field]] = query_returning_single_row_with_single_field()

Ambos os exemplos acima podem ser escritos com tuplas, mas não esqueça da peculiaridade sintática, tuplas com um único item devem ser escritas com uma vírgula final. Então o primeiro alvo seria (record,) e o segundo ((field,),). Nos dois casos, esquecer aquela vírgula causa um bug silencioso.[4]

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

Pattern matching com sequências

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

Note

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

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

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

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

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

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

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

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

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

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

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

Exemplo 10. Desestruturando tuplas aninhadas—requer Python ≥ 3.10
link:code/02-array-seq/match_lat_lon.py[role=include]
  1. O sujeito desse match é record—isto é, cada uma das tuplas em metro_areas.

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

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

  1. O sujeito é uma sequência, e

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

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

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

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

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

Warning

Instâncias de str, bytes, e bytearray não são tratadas como sequências no contexto de um match/case. Um sujeito de match de um desses tipos é tratado como um valor "atômico"—assim como o inteiro 987 é tratado como um único valor, e não como uma sequência de dígitos. Tratar aqueles três tipos como sequências poderia causar bugs devido a casamentos não intencionais. Se você quer usar um objeto daqueles tipos como um sujeito sequência, converta-o na instrução match. Por exemplo, veja tuple(phone) no trecho abaixo, que poderia ser usado para separar números de telefone por regiões do mundo com base no prefixo DDI:

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

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

list     memoryview    array.array
tuple    range         collections.deque

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

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

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

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

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

Variável Valor atribuído

name

'Shanghai'

lat

31.1

lon

121.3

coord

(31.1, 121.3)

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

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

As expressões str(name) e float(lat) se parecem com chamadas a construtores, que usaríamos para converter name e lat para str e float. Mas no contexto de um padrão, aquela sintaxe faz uma verificação de tipo durante a execução do programa: o padrão acima vai casar com uma sequência de quatro itens, na qual o item 0 deve ser uma str e o item 3 deve ser um par de números de ponto flutuante. Além disso, a str no item 0 será vinculada à variável name e os números no item 3 serão vinculados a lat e lon, respectivamente. Assim, apesar de imitar a sintaxe de uma chamada de construtor, o significado de str(name) é totalmente diferente no contexto de um padrão. O uso de classes arbitrárias em padrões será tratado na [pattern_instances_sec].

Por outro lado, se queremos casar qualquer sujeito sequência começando com uma str e terminando com uma sequência aninhada com dois números de ponto flutuante, podemos escrever:

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

O *_ casa com qualquer número de itens, sem vinculá-los a uma variável. Usar *extra em vez de *_ vincularia os itens a extra como uma list com 0 ou mais itens.

A instrução de guarda opcional começando com if só é avaliada se o padrão casar, e pode se referir a variáveis vinculadas no padrão, como no Exemplo 10:

        match record:
            case [name, _, _, (lat, lon)] if lon <= 0:
                print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')

O bloco aninhado com o comando print só será executado se o padrão casar e a expressão guarda for verdadeira.

Tip

A desestruturação com padrões é tão expressiva que, algumas vezes, um match com um único case pode tornar o código mais simples. Guido van Rossum tem uma coleção de exemplos de case/match, incluindo um que ele chamou de "A very deep iterable and type match with extraction" (Um match de iterável e tipo muito profundo, com extração) (EN).

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

Casando padrões de sequência em um interpretador

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

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

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

Exemplo 11. Casando padrões sem match/case
link:code/02-array-seq/lispy/py3.9/lis.py[role=include]
    # ... lines omitted
link:code/02-array-seq/lispy/py3.9/lis.py[role=include]
    # ... more lines omitted

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

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

Exemplo 12. Pattern matching com match/case—requer Python ≥ 3.10
link:code/02-array-seq/lispy/py3.10/lis.py[role=include]
    # ... lines omitted
link:code/02-array-seq/lispy/py3.10/lis.py[role=include]
        # ... more lines omitted
link:code/02-array-seq/lispy/py3.10/lis.py[role=include]
  1. Casa se o sujeito for uma sequência de dois itens começando com 'quote'.

  2. Casa se o sujeito for uma sequência de quatro itens começando com 'if'.

  3. Casa se o sujeito for uma sequência com três ou mais itens começando com 'lambda'. A guarda assegura que body não esteja vazio.

  4. Casa se o sujeito for uma sequência de três itens começando com 'define', seguido de uma instância de Symbol.

  5. é uma boa prática ter um case para capturar todo o resto. Neste exemplo, se exp não casar com nenhum dos padrões, a expressão está mal-formada, então gera um SyntaxError.

Sem o último case, para pegar tudo que tiver passado pelos anteriores, todo o bloco match não faz nada quando o sujeito não casa com algum case—e isso pode ser uma falha silenciosa.

Norvig deliberadamente evitou a checagem e o tratamento de erros em lis.py, para manter o código fácil de entender. Com pattern matching, podemos acrescentar mais verificações e ainda manter o programa legível. Por exemplo, no padrão 'define', o código original não se assegura que name é uma instância de Symbol—isso exigiria um bloco if, uma chamada a isinstance, e mais código. O Exemplo 12 é mais curto e mais seguro que o Exemplo 11.

Padrões alternativos para lambda

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

(lambda (parms…) body1 body2…)

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

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

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

['lambda', 'x', ['*', 'x', 2]]

A lista aninhada após a palavra-chave lambda do Scheme contém os nomes do parâmetros formais da função, e deve ser uma lista mesmo que contenha apenas um elemento. Ela pode também ser uma lista vazia, se função não receber parâmetros—como a random.random() do Python.

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

        case ['lambda', [*parms], *body] if body:
            return Procedure(parms, body, env)

Em um padrão de sequência, o * pode aparecer apenas uma vez por sequência. Aqui temos duas sequências: a externa e a interna.

Acrescentando os caracteres [*] em torno de parms fez o padrão mais parecido com a sintaxe do Scheme da qual ele trata, e nos deu uma verificação estrutural adicional.

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

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

(define (name parm…) body1 body2…)

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

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

        case ['define', [Symbol() as name, *parms], *body] if body:
            env[name] = Procedure(parms, body, env)

Eu colocaria esse case após o case da outra forma de define no Exemplo 12. A ordem desses cases de define é irrelevante nesse exemplo, pois nenhum sujeito pode casar com esses dois padrões: o segundo elemento deve ser um Symbol na forma original de define, mas deve ser uma sequência começando com um Symbol no atalho de define para definição de função.

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

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

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

(quote exp)

['quote', exp]

(if test conseq alt)

['if', test, conseq, alt]

(lambda (parms…) body1 body2…)

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

(define name exp)

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

(define (name parms…) body1 body2…)

['define', [Symbol() as name, *parms], *body] if body

Espero que a refatoração do evaluate de Norvig com pattern matching tenha convencido você que match/case pode tornar seu código mais legível e mais seguro.

Note

Veremos mais do lis.py na [pattern_matching_case_study_sec], quando vamos revisar o exemplo completo de match/case em evaluate. Se você quiser aprender mais sobre o lys.py de Norvig, leia seu maravilhoso post "(How to Write a (Lisp) Interpreter (in Python))" (Como Escrever um Interpretador (Lisp) em (Python)).

Isso conclui nossa primeira passagem por desempacotamento, desestruturação e pattern matching com sequências. Vamos tratar de outros tipos de padrões mais adiante, em outros capítulos.

Todo programador Python sabe que sequências podem ser fatiadas usando a sintaxe s[a:b]. Vamos agora examinar alguns fatos menos conhecidos sobre fatiamento.

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 [user_defined_sequences], mantendo nossa filosofia de tratar de classes prontas para usar nessa parte do livro, e da criação de novas classes na [classes_protocols_part].

Por que fatias e faixas excluem o último item?

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

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

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

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

    >>> l = [10, 20, 30, 40, 50, 60]
    >>> l[:2]  # split at 2
    [10, 20]
    >>> l[2:]
    [30, 40, 50, 60]
    >>> l[:3]  # split at 3
    [10, 20, 30]
    >>> l[3:]
    [40, 50, 60]

Os melhores argumentos a favor desta convenção foram escritos pelo cientista da computação holandês Edsger W. Dijkstra (veja a última referência na Leitura complementar).

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

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 [data_model], 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 [how_slicing_works], para avaliar a expressão seq[start:stop:step], o Python chama seq.__getitem__(slice(start, stop, step)). Mesmo se você não for implementar seus próprios tipos de sequência, saber dos objetos fatia é útil, porque eles permitem que você atribua nomes às fatias, da mesma forma que planilhas permitem dar nomes a faixas de células.

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

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

Voltaremos aos objetos slice quando formos discutir a criação de suas próprias coleções, na [sliceable_sequence]. 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.

Fatiamento multidimensional e reticências

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

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

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

As reticências—escritas como três pontos finais (...) e não como (Unicode U+2026)—são reconhecidas como um símbolo pelo parser do Python. Esse símbolo é um apelido para o objeto Ellipsis, a única instância da classe ellipsis.[9] Dessa forma, ele pode ser passado como argumento para funções e como parte da especificação de uma fatia, como em f(a, ..., z) ou a[i:...]. O NumPy usa ... como atalho ao fatiar arrays com muitas dimensões; por exemplo, se x é um array com quatro dimensões, x[i, ...] é um atalho para x[i, :, :, :,]. Veja "NumPy quickstart" (EN) para saber mais sobre isso.

No momento em que escrevo isso, desconheço usos de Ellipsis ou de índices multidimensionais na biblioteca padrão do Python. Se você souber de algum, me avise. Esses recursos sintáticos existem para suportar tipos definidos pelo usuário ou extensões como o NumPy.

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

Atribuindo a fatias

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

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

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

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.

Warning

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

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

Criando uma lista de listas

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

Exemplo 14. Uma lista com três listas de tamanho 3 pode representar um tabuleiro de jogo da velha
>>> board = [['_'] * 3 for i in range(3)]  (1)
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'  (2)
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
  1. Cria uma lista de três listas, cada uma com três itens. Inspeciona a estrutura criada.

  2. Coloca um "X" na linha 1, coluna 2, e verifica o resultado.

Um atalho tentador mas errado seria fazer algo como o Exemplo 15.

Exemplo 15. Uma lista com três referências para a mesma lista é inútil
>>> weird_board = [['_'] * 3] * 3  (1)
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' (2)
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
  1. A lista externa é feita de três referências para a mesma lista interna. Enquanto ela não é modificada, tudo parece correr bem.

  2. Colocar um "O" na linha 1, coluna 2, revela que todas as linhas são apelidos do mesmo objeto.

O problema com o Exemplo 15 é que ele se comporta, essencialmente, como o código abaixo:

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

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

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

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

Tip

Se o problema ou a solução mostrados nessa seção não estão claros para você, não se preocupe. O [mutability_and_references] 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.

Atribuição aumentada com sequências

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

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

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

>>> a += b

Se a implementar __iadd__, esse método será chamado. No caso de sequências mutáveis (por exemplo, list, bytearray, array.array), a será modificada no lugar (isto é, o efeito ser similar a a.extend(b)). Porém, quando a não implementa __iadd__, a expressão a = b` tem o mesmo efeito de `a = a + b`: a expressão `a + b` é avaliada antes, produzindo um novo objeto, que então é vinculado a `a`. Em outras palavras, a identidade do objeto vinculado a `a` pode ou não mudar, dependendo da disponibilidade de `+__iadd__.

Em geral, para sequências mutáveis, é razoável supor que __iadd__ está implementado e que += acontece no mesmo lugar. Para sequências imutáveis, obviamente não há forma disso acontecer.

Isso que acabei de escrever sobre =` também se aplica a `*=`, que é implementado via `+__imul__. Os métodos especiais __iadd__ e __imul__ são tratados no [operator_overloading]. Aqui está uma demonstração de *= com uma sequência mutável e depois com uma sequência imutável:

>>> l = [1, 2, 3]
>>> id(l)
4311953800  (1)
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800  (2)
>>> t = (1, 2, 3)
>>> id(t)
4312681568  (3)
>>> t *= 2
>>> id(t)
4301348296  (4)
  1. O ID da lista inicial.

  2. Após a multiplicação, a lista é o mesmo objeto, com novos itens anexados.

  3. O ID da tupla inicial.

  4. Após a multiplicação, uma nova tupla foi criada.

A concatenação repetida de sequências imutáveis é ineficiente, pois ao invés de apenas acrescentar novos itens, o interpretador tem que copiar toda a sequência alvo para criar um novo objeto com os novos itens concatenados.[10]

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.

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

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

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

O que acontece a seguir? Escolha a melhor alternativa:

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

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

  3. Nenhuma das alternativas acima..

  4. Ambas as alternativas, A e B.

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

Exemplo 17. O resultado inesperado: o item t2 é modificado e uma exceção é gerada
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

O Online Python Tutor (EN) é uma ferramenta online fantástica para visualizar em detalhes o funcionamento do Python. A Figura 5 é uma composição de duas capturas de tela, mostrando os estados inicial e final da tupla t do Exemplo 17.

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

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

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

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

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

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

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

  • Evite colocar objetos mutáveis em tuplas.

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

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

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

list.sort versus a função embutida sorted

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

Note

A convenção de devolver None para sinalizar mudanças no mesmo lugar tem uma desvantagem: não podemos cascatear chamadas a esses métodos. Em contraste, métodos que devolvem novos objetos (todos os métodos de str, por exemplo) podem ser cascateados no estilo de uma interface fluente. Veja o artigo "Fluent interface" (EN) da Wikipedia em inglês para uma descrição mais detalhada deste tópico.

A função embutida sorted, por outro lado, cria e devolve uma nova lista. Ela aceita qualquer objeto iterável como um argumento, incluindo sequências imutáveis e geradores (veja o [iterables2generators]). Independente do tipo do iterável passado a sorted, ela sempre cria e devolve uma nova lista.

Tanto list.sort quanto sorted podem receber dois argumentos de palavra-chave opcionais:

reverse

Se True, os itens são devolvidos em ordem decrescente (isto é, invertendo a comparação dos itens). O default é False.

key

Uma função com um argumento que será aplicada a cada item, para produzir sua chave de ordenação. Por exemplo, ao ordenar uma lista de strings, key=str.lower pode ser usada para realizar uma ordenação sem levar em conta maiúsculas e minúsculas, e key=len irá ordenar as strings pela quantidade de caracteres. O default é a função identidade (isto é, os itens propriamente ditos são comparados).

Tip

Também se pode usar o parâmetro de palavra-chave opcional key com as funções embutidas min() e max(), e com outras funções da biblioteca padrão (por exemplo, itertools.groupby() e heapq.nlargest()).

Aqui estão alguns exemplos para esclarecer o uso dessas funções e dos argumentos de palavra-chave. Os exemplos também demonstram que o algoritmo de ordenação do Python é estável (isto é, ele preserva a ordem relativa de itens que resultam iguais na comparação):[14]

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

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

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

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

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

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

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

  8. Agora fruits está ordenada.

Warning

Por default, o Python ordena as strings lexicograficamente por código de caractere. Isso quer dizer que as letras maiúsculas ASCII virão antes das minúsculas, e que os caracteres não-ASCII dificilmente serão ordenados de forma razoável. A [sorting_unicode_sec] 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 do Python. Aquele módulo também inclui a função bisect.insort, que você pode usar para assegurar que suas sequências ordenadas permaneçam ordenadas. Há uma introdução ilustrada ao módulo bisect no post "Managing Ordered Sequences with Bisect" (Gerenciando Sequências Ordenadas com Bisect) (EN) em fluentpython.com, o website que complementa este livro.

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

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[16] mais eficiente.

Tip

Se seu código frequentemente verifica se um item está presente em uma coleção (por exemplo, item in my_collection), considere usar um set para my_collection, especialmente se ela contiver um número grande de itens. Um set é otimizado para verificação rápida de presença de itens. Eles também são iteráveis, mas não são coleções, porque a ordenação de itens de sets não é especificada. Vamos falar deles no [dicts-a-to-z].

O restante desse capítulo discute tipos mutáveis de sequências que, em muitos casos, podem substituir as listas. Começamos pelos arrays.

Arrays

Se uma lista contém apenas números, uma array.array é um substituto mais eficiente. Arrays suportam todas as operações das sequências mutáveis (incluindo .pop, .insert, e .extend), bem como métodos adicionais para carregamento e armazenamento rápidos, tais como .frombytes e .tofile.

Um array do Python quase tão enxuto quanto um array do C. Como mostrado na Figura 1, um array de valores float não mantém instâncias completas de float, mas apenas pacotes de bytes representando seus valores em código de máquina—de forma similar a um array de double na linguagem C. Ao criar um array, você fornece um código de tipo (typecode), uma letra que determina o tipo C subjacente usado para armazenar cada item no array. Por exemplo, b é o código de tipo para o que o C chama de signed char, um inteiro variando de -128 a 127. Se você criar uma array('b'), então cada item será armazenado em um único byte e será interpretado como um inteiro. Para grandes sequências de números, isso economiza muita memória. E o Python não permite que você insira qualquer número que não corresponda ao tipo do array.

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

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

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

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

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

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

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

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

  8. Verifica a igualdade do conteúdo dos arrays

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

Para o caso específico de arrays numéricas representando dados binários, tal como bitmaps de imagens, o Python tem os tipos bytes e bytearray, discutidos na [strings_bytes_files].

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

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

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

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

s.append(e)

Acrescenta um elemento após o último

s.byteswap()

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

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.__copy__()

Suporte a copy.copy

s.count(e)

Conta as ocorrências de um elemento

s.__deepcopy__()

Suporte otimizado a copy.deepcopy

s.__delitem__(p)

Remove item na posição p

s.extend(it)

Acrescenta itens a partir do iterável it

s.frombytes(b)

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

s.fromfile(f, n)

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

s.fromlist(l)

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

s.__getitem__(p)

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

s.index(e)

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

s.insert(p, e)

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

s.itemsize

Tamanho em bytes de cada item do array

s.__iter__()

Obtém iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

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

s.__rmul__(n)

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

s.pop([p])

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

s.remove(e)

Remove a primeira ocorrência do elemento e por valor

s.reverse()

Reverte a ordem dos itens no mesmo lugar

s.__reversed__()

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

s.__setitem__(p, e)

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

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

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

s.tobytes()

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

s.tofile(f)

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

s.tolist()

Devolve os itens como objetos numéricos em uma list

s.typecode

String de um caractere identificando o tipo em C dos itens

Tip

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

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

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

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

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 NumPy). Travis Oliphant, autor principal da NumPy, responde assim à questão "When should a memoryview be used?" Quando se deve usar uma memoryview?:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Note

Você pode ver um exemplo de inspeção de uma memoryview com o pacote struct em fluentpython.com: "Parsing binary records with struct" Analisando registros binários com struct (EN).

Enquanto isso, se você está fazendo processamento numérico avançado com arrays, deveria estar usando as bibliotecas NumPy. Vamos agora fazer um breve passeio por elas.

NumPy

Por todo esse livro, procuro destacar o que já existe na biblioteca padrão do Python, para que você a aproveite ao máximo. Mas a NumPy é tão maravilhosa que exige um desvio.

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

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

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

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

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

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

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

  5. Obtém a linha no índice 2

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

  7. Obtém a coluna no índice 1

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

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

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

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

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

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

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

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

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

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

Mas isso foi apenas um aperitivo.

A NumPy e a SciPy são bibliotecas formidáveis, e estão na base de outras ferramentas fantásticas, como a Pandas (EN)—que implementa tipos eficientes de arrays capazes de manter dados não-numéricos, e fornece funções de importação/exportação em vários formatos diferentes, como .csv, .xls, dumps SQL, HDF5, etc.—e a scikit-learn (EN), o conjunto de ferramentas para Aprendizagem de Máquina mais usado atualmente. A maior parte das funções da NumPy e da SciPy são implementadas em C ou C++, e conseguem aproveitar todos os núcleos de CPU disponíveis, pois podem liberar a GIL (Global Interpreter Lock, Trava Global do Interpretador) do Python. O projeto Dask suporta a paralelização do processamento da NumPy, da Pandas e da scikit-learn para grupos (clusters) de máquinas. Esses pacotes merecem livros inteiros. Este não é um desses livros, mas nenhuma revisão das sequências do Python estaria completa sem pelo menos uma breve passagem pelos arrays da NumPy.

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

Deques e outras filas

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

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

Exemplo 23. Usando um deque
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)  (1)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3)  (2)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1)  (3)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33])  (4)
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40])  (5)
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
  1. O argumento opcional maxlen determina o número máximo de itens permitidos nessa instância de deque; isso estabelece o valor de um atributo de instância maxlen, somente de leitura.

  2. Rotacionar com n > 0 retira itens da direita e os recoloca pela esquerda; quando n < 0, os itens são retirados pela esquerda e anexados pela direita.

  3. Acrescentar itens a um deque cheio (len(d) == d.maxlen) elimina itens da ponta oposta. Observe, na linha seguinte, que o 0 foi descartado.

  4. Acrescentar três itens à direita derruba -1, 1, e 2 da extremidade esquerda.

  5. Observe que extendleft(iter) acrescenta cada item sucessivo do argumento iter do lado esquerdo do deque, então a posição final dos itens é invertida.

A Tabela 4 compara os métodos específicos de list e deque (omitindo aqueles que também aparecem em object).

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

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

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

s.__add__(s2)

s + s2—concatenação

s.__iadd__(s2)

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

s.append(e)

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

s.appendleft(e)

Acrescenta um elemento à esquerda (antes do primeiro)

s.clear()

Apaga todos os itens

s.__contains__(e)

e in s

s.copy()

Cópia rasa da lista

s.__copy__()

Suporte a copy.copy (cópia rasa)

s.count(e)

Conta ocorrências de um elemento

s.__delitem__(p)

Remove item na posição p

s.extend(i)

Acrescenta item do iterável i pela direita

s.extendleft(i)

Acrescenta item do iterável i pela esquerda

s.__getitem__(p)

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

s.index(e)

Encontra a primeira ocorrência de e

s.insert(p, e)

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

s.__iter__()

Obtém iterador

s.__len__()

len(s)—número de itens

s.__mul__(n)

s * n—concatenação repetida

s.__imul__(n)

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

s.__rmul__(n)

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

s.pop()

Remove e devolve último item[19]

s.popleft()

Remove e devolve primeiro item

s.remove(e)

Remove primeira ocorrência do elemento e por valor

s.reverse()

Inverte a ordem do itens no mesmo lugar

s.__reversed__()

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

s.rotate(n)

Move n itens de um lado para o outro

s.__setitem__(p, e)

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

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

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

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

queue

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

multiprocessing

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

asyncio

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

heapq

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

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

Resumo do capítulo

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

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

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

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

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

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

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

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

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

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

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

Leitura complementar

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

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

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

A PEP 3132—​Extended Iterable Unpacking (Desempacotamento Iterável Estendido) (EN) é a fonte canônica para ler sobre o novo uso da sintaxe *extra no lado esquerdo de atribuições paralelas. Se você quiser dar uma olhada no processo de evolução do Python, "Missing *-unpacking generalizations" (As generalizações esquecidas de * no desempacotamento) (EN) é um tópico do bug tracker propondo melhorias na notação de desempacotamento iterável. PEP 448—​Additional Unpacking Generalizations (Generalizações de Desempacotamento Adicionais) (EN) foi o resultado de discussões ocorridas naquele tópico.

Como mencionei na Pattern matching com sequências, o texto introdutório "Correspondência de padrão estrutural", de Carol Willing, no "O que há de novo no Python 3.10", é uma ótima introdução a esse novo grande recurso, em mais ou menos 1.400 palavras (isso é menos de 5 páginas quando o Firefox converte o HTML em PDF). A PEP 636—Structural Pattern Matching: Tutorial (Casamento de Padrões Estrutural: Tutorial) (EN) também é boa, mas mais longa. A mesma PEP 636 inclui o "Appendix A—Quick Intro" (Apêndice A-Introdução Rápida) (EN). Ele é menor que a introdução de Willing, porque omite as considerações gerais sobre os motivos pelos quais o pattern matching é bom para você. SE você precisar de mais argumentos para se convencer ou convencer outros que o pattern matching é bom para o Python, leia as 22 páginas de PEP 635—Structural Pattern Matching: Motivation and Rationale (_Casamento de Padrões Estrutural: Motivação e Justificativa) (EN).

Há muitos livros tratando da NumPy no mercado, e muitos não mencionam "NumPy" no título. Dois exemplos são o Python Data Science Handbook, escrito por Jake VanderPlas e de acesso aberto, e a segunda edição do Python for Data Analysis, de Wes McKinney.

"A Numpy é toda sobre vetorização". Essa é a frase de abertura do livro de acesso aberto From Python to NumPy, de Nicolas P. Rougier. Operações vetorizadas aplicam funções matemáticas a todos os elementos de um array sem um loop explícito escrito em Python. Elas podem operar em paralelo, usando instruções especiais de vetor presentes em CPUs modernas, tirando proveito de múltiplos núcleos ou delegando para a GPU, dependendo da biblioteca. O primeiro exemplo no livro de Rougier mostra um aumento de velocidade de 500 vezes, após a refatoração de uma bela classe pythônica, usando um método gerador, em uma pequena e feroz função que chama um par de funções de vetor da NumPy.

Para aprender a usar deque (e outras coleções), veja os exemplos e as receitas práticas em "Tipos de dados de contêineres", na documentação do Python.

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

Ponto de Vista

A natureza das tuplas

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

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

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

Sequências planas versus sequências contêineres

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

Alguns objetos contêm referências a outros objetos; eles são chamados de contêineres.

Usei o termo "sequência contêiner" para ser específico, porque existem contêineres em Python que não são sequências, como dict e set. Sequências contêineres podem ser aninhadas porque elas podem conter objetos de qualquer tipo, incluindo seu próprio tipo.

Por outro lado, sequências planas são tipos de sequências que não podem ser aninhadas, pois só podem conter valores atômicos como inteiros, números de ponto flutuante ou caracteres.

Adotei o termo sequência plana porque precisava de algo para contrastar com "sequência contêiner."

Apesar dos uso anterior da palavra "containers" na documentação oficial, há uma classe abstrata em collections.abc chamada Container. Aquela ABC tem apenas um método, __contains__—o método especial por trás do operador in. Isso significa que arrays e strings, que não são contêineres no sentido tradicional, são subclasses virtuais de Container, porque implementam __contains__. Isso é só mais um exemplo de humanos usando uma mesma palavra para significar coisas diferentes. Nesse livro, vou escrever "contêiner" com minúscula e em português para "um objeto que contém referências para outros objetos" e Container com a inicial maiúscula em fonte mono espaçada para me referir a collections.abc.Container.

Listas bagunçadas

Textos introdutórios de Python costumam enfatizar que listas podem conter objetos de diferentes tipos, mas na prática esse recurso não é muito útil: colocamos itens em uma lista para processá-los mais tarde, o que implica o suporte, da parte de todos os itens, a pelo menos alguma operação em comum (isto é, eles devem todos "grasnar", independente de serem ou não 100% patos, geneticamente falando), Por exemplo, não é possível ordenar uma lista em Python 3 a menos que os itens ali contidos sejam comparáveis:

>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
>>> sorted(l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() < int()

Diferente das listas, as tuplas muitas vezes mantêm itens de tipos diferentes. Isso é natural: se cada item em uma tupla é um campo, então cada campo pode ter um tipo diferente.

'key' é brilhante

O argumento opcional key de list.sort, sorted, max, e min é uma grande ideia. Outras linguagens forçam você a fornecer uma função de comparação com dois argumentos, como a função descontinuada do Python 2 cmp(a, b). Usar key é mais simples e mais eficiente. É mais simples porque basta definir uma função de um único argumento que recupera ou calcula o critério a ser usado para ordenar seus objetos; isso é mais fácil que escrever uma função de dois argumentos para devolver –1, 0, 1. Também é mais eficiente, porque a função key é invocada apenas uma vez por item, enquanto a comparação de dois argumentos é chamada a cada vez que o algoritmo de ordenação precisa comparar dois itens. Claro, o Python também precisa comparar as chaves ao ordenar, mas aquela comparação é feita em código C otimizado, não em uma função Python escrita por você.

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

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

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

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

O Timsort foi usado no CPython pela primeira vez em 2002. Desde 2009, o Timsort também é usado para ordenar arrays tanto no Java padrão quanto no Android, um fato que ficou muito conhecido quando a Oracle usou parte do código relacionado ao Timsort como evidência da violação da propriedade intelectual da Sun pelo Google. Por exemplo, veja essa ordem do Juiz William Alsup (EN) de 2012. Em 2021, a Suprema Corte dos Estados Unidos decidiu que o uso do código do Java pelo Google é "fair use"[20]

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


1. Agradeço à leitora Tina Lapine por apontar essa informação.
2. Operadores invertidos são explicados no [operator_overloading].
3. Também usado para sobrescrever uma sub-sequência. Veja a Atribuindo a fatias.
4. Agradeço ao revisor técnico Leonardo Rochael por esse exemplo.
5. NT: A tradução em português da documentação do Python adotou o termo "correspondência de padrões" no lugar de pattern matching. Escolhemos manter o termo em inglês, pois é usado nas comunidades brasileiras de linguagens que implementam pattern matching há muitos anos, como por exemplo Scala, Elixir e Haskell. Naturalmente mantivemos os títulos originais nos links externos.
6. Na minha opinião, uma sucessão if/elif/elif/…​/else funciona muito bem como no lugar de switch/case. E ela não sofre dos problemas de fallthrough (cascateamento) (EN) e de dangling else (o else errante) (EN), que alguns projetistas de linguagens copiaram irracionalmente do C—décadas após sabermos que tais problemas são a causa de inúmeros bugs.
7. A última é chamada eval no código original; a renomeei para evitar que fosse confundida com a função embutida eval do Python.
8. Na Views de memória vamos mostrar que views da memória construídas de forma especial podem ter mais de uma dimensão.
9. Não, eu não escrevi ao contrário: o nome da classe ellipsis realmente se escreve só com minúsculas, e a instância é um objeto embutido chamado Ellipsis, da mesma forma que bool é em minúsculas mas suas instâncias são True e False.
10. str é uma exceção a essa descrição. Como criar strings com += em loops é tão comum em bases de código reais, o CPython foi otimizado para esse caso de uso. Instâncias de str são alocadas na memória com espaço extra, então a concatenação não exige a cópia da string inteira a cada operação.
11. Agradeço a Leonardo Rochael e Cesar Kawakami por compartilharem esse enigma na Conferência PythonBrasil de 2013.
12. Alguns leitores sugeriram que a operação no exemplo pode ser realizada com t[2].extend([50,60]), sem erros. Eu sei disso, mas a intenção aqui é mostrar o comportamento estranho do operador += nesse caso.
13. Receptor (receiver) é o alvo de uma chamada a um método, o objeto vinculado a self no corpo do método.
14. O principal algoritmo de ordenação do Python se chama Timsort, em homenagem a seu criador, Tim Peters. Para curiosidades sobre o Timsort, veja o Ponto de Vista.
15. As palavras nesse exemplo estão ordenadas em ordem alfabética porque são 100% constituídas de caracteres ASCII em letras minúsculas. Veja o aviso após o exemplo.
16. Sigla em inglês para "First in, first out" (Primeiro a entrar, primeiro a sair—o comportamento padrão de filas.
17. Operadores invertidos são explicados no [operator_overloading].
18. Operadores invertidos são explicados no [operator_overloading].
19. a_list.pop(p) permite remover da posição p, mas deque não suporta essa opção.
20. NT: Conceito da lei de copyright norte-americana que permite, em determinadas circunstâncias, o uso sem custo de partes da propriedade intelectual de outros. Em geral traduzido como "uso razoável" ou "uso aceitável". Essa doutrina não faz parte da lei brasileira.