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
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
etuple
quanto ao desempenho e ao armazenamento. -
Ressalvas sobre tuplas com elementos mutáveis, e como detectá-los se necessário.
Movi a discussão sobre tuplas nomeadas para a [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. |
A biblioteca padrão oferece uma boa seleção de tipos de sequências, implementadas em C:
- Sequências contêiner
-
Podem armazenar itens de tipos diferentes, incluindo contêineres aninhados e objetos de qualquer tipo. Alguns exemplos:
list
,tuple
, ecollections.deque
. - Sequências planas
-
Armazenam itens de algum tipo simples, mas não outras coleções ou referências a objetos. Alguns exemplos:
str
,bytes
, earray.array
.
Uma sequência contêiner mantém referências para os objetos que contém, que podem ser de qualquer tipo, enquanto uma sequência plana armazena o valor de seu conteúdo em seu próprio espaço de memória, e não como objetos Python distintos. Veja a Figura 1.
tuple
e um array
, cada uma com três itens. As células em cinza representam o cabeçalho de cada objeto Python na memória. A tuple
tem um array de referências para seus itens. Cada item é um objeto Python separado, possivelmente contendo também referências aninhadas a outros objetos Python, como aquela lista de dois itens. Por outro lado, um array
Python é um único objeto, contendo um array da linguagem C com três números de ponto flutuante`.Dessa forma, sequências planas são mais compactas, mas estão limitadas a manter valores primitivos como bytes e números inteiros e de ponto flutuante.
Note
|
Todo objeto Python na memória tem um cabeçalho com metadados. O objeto Python mais simples, um
No Python 64-bits, cada um desses campos ocupa 8 bytes.
Por isso um array de números de ponto flutuante é muito mais compacto que uma tupla de números de ponto flutuante: o array é um único objeto contendo apenas o valor dos números,
enquanto a tupla consiste de vários objetos—a própria tupla e cada objeto |
Outra forma de agrupar as sequências é por mutabilidade:
- Sequências mutáveis
-
Por exemplo,
list
,bytearray
,array.array
ecollections.deque
. - Sequências imutáveis
-
Por exemplo,
tuple
,str
, ebytes
.
A Figura 2 ajuda a visualizar como as sequências mutáveis herdam todos os métodos das sequências imutáveis e implementam vários métodos adicionais.
Os tipos embutidos concretos de sequências na verdade não são subclasses das classes base abstratas (ABCs) Sequence
e MutableSequence
, mas sim subclasses virtuais registradas com aquelas ABCs—como veremos no [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
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.
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. |
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
... codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]
Qualquer um que saiba um pouco de Python consegue ler o Exemplo 1. Entretanto, após aprender sobre as listcomps, acho o Exemplo 2 mais legível, porque deixa sua intenção explícita.
Um loop for
pode ser usado para muitas coisas diferentes: percorrer uma sequência para contar ou encontrar itens, computar valores agregados (somas, médias), ou inúmeras outras tarefas.
O código no Exemplo 1 está criando uma lista.
Uma listcomp, por outro lado, é mais clara. Seu objetivo é sempre criar uma nova lista.
Naturalmente, é possível abusar das compreensões de lista para escrever código verdadeiramente incompreensível. Já vi código Python usando listcomps apenas para repetir um bloco de código por seus efeitos colaterais. Se você não vai fazer alguma coisa com a lista criada, não deveria usar essa sintaxe. Além disso, tente manter o código curto. Se uma compreensão ocupa mais de duas linhas, provavelmente seria melhor quebrá-la ou reescrevê-la como um bom e velho loop for
. Avalie qual o melhor caminho: em Python, como em português, não existem regras absolutas para se escrever bem.
Tip
|
Dica de sintaxe
No código Python, quebras de linha são ignoradas dentro de pares de |
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
-
x
não foi sobrescrito: continua vinculado a'ABC'
. -
last
permanece. -
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 fazem tudo que as funções map
e filter
fazem, sem os malabarismos exigidos pela funcionalidade limitada do lambda
do Python.
Considere o Exemplo 3.
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]
Eu acreditava que map
e filter
eram mais rápidas que as listcomps equivalentes, mas Alex Martelli
assinalou que não é o caso—pelo menos não nos exemplos acima.
O script listcomp_speed.py no
repositório de código do Python Fluente é um teste de velocidade simples, comparando listcomp com filter/map
.
Vou falar mais sobre map
e filter
no [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.
Listcomps podem criar listas a partir do produto cartesiano de dois ou mais iteráveis. Os itens resultantes de um produto cartesiano são tuplas criadas com os itens de cada iterável na entrada, e a lista resultante tem o tamanho igual ao produto da multiplicação dos tamanhos dos iteráveis usados. Veja a Figura 3.
Por exemplo, imagine que você precisa produzir uma lista de camisetas disponíveis em duas cores e três tamanhos. O Exemplo 4 mostra como produzir tal lista usando uma listcomp. O resultado tem seis itens.
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes] (1)
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
('white', 'M'), ('white', 'L')]
>>> for color in colors: (2)
... for size in sizes:
... print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes (3)
... for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
('black', 'L'), ('white', 'L')]
-
Isso gera uma lista de tuplas ordenadas por cor, depois por tamanho.
-
Observe que a lista resultante é ordenada como se os loops
for
estivessem aninhados na mesma ordem que eles aparecem na listcomp. -
Para ter os itens ordenados por tamanho e então por cor, apenas rearranje as cláusulas
for
; adicionar uma quebra de linha listcomp torna mais fácil ver como o resultado será ordenado.
No [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.
Para inicializar tuplas, arrays e outros tipos de sequências, você também poderia começar de uma listcomp, mas uma genexp (expressão geradora) economiza memória, pois ela produz itens um de cada vez usando o protocolo iterador, em vez de criar uma lista inteira apenas para alimentar outro construtor.
As genexps usam a mesma sintaxe das listcomps, mas são delimitadas por parênteses em vez de colchetes.
O Exemplo 5 demonstra o uso básico de genexps para criar uma tupla e um array.
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) (1)
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) (2)
array('I', [36, 162, 163, 165, 8364, 164])
-
Se a expressão geradora é o único argumento em uma chamada de função, não há necessidade de duplicar os parênteses circundantes.
-
O construtor de
array
espera dois argumentos, então os parênteses em torno da expressão geradora são obrigatórios. O primeiro argumento do construtor dearray
define o tipo de armazenamento usado para os números no array, como veremos na 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
.
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in (f'{c} {s}' for c in colors for s in sizes): (1)
... print(tshirt)
...
black S
black M
black L
white S
white M
white L
-
A expressão geradora produz um item por vez; uma lista com todas as seis variações de camisetas nunca aparece neste exemplo.
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.
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 podem conter registros: cada item na tupla contém os dados de um campo, e a posição do item indica seu significado.
Se você pensar em uma tupla apenas como uma lista imutável, a quantidade e a ordem dos elementos pode ou não ter alguma importância, dependendo do contexto. Mas quando usamos uma tupla como uma coleção de campos, o número de itens em geral é fixo e sua ordem é sempre importante.
O Exemplo 7 mostras tuplas usadas como registros. Observe que, em todas as expressões, ordenar a tupla destruiria a informação, pois o significado de cada campo é dado por sua posição na tupla.
>>> lax_coordinates = (33.9425, -118.408056) (1)
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014) (2)
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), (3)
... ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids): (4)
... print('%s/%s' % passport) (5)
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids: (6)
... print(country)
...
USA
BRA
ESP
-
Latitude e longitude do Aeroporto Internacional de Los Angeles.
-
Dados sobre Tóquio: nome, ano, população (em milhares), crescimento populacional (%) e área (km²).
-
Uma lista de tuplas no formato (código_de_país, número_do_passaporte).
-
Iterando sobre a lista,
passport
é vinculado a cada tupla. -
O operador de formatação
%
entende as tuplas e trata cada item como um campo separado. -
O loop
for
sabe como recuperar separadamente os itens de uma tupla—isso é chamado "desempacotamento" ("unpacking"). Aqui não estamos interessados no segundo item, então o atribuímos a_
, uma variável descartável, usada apenas para coletar valores que não serão usados.
Tip
|
Em geral, usar |
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
.
O interpretador Python e a biblioteca padrão fazem uso extensivo das tuplas como listas imutáveis, e você deveria seguir o exemplo. Isso traz dois benefícios importantes:
- Clareza
-
Quando você vê uma
tuple
no código, sabe que seu tamanho nunca mudará. - Desempenho
-
Uma
tuple
usa menos memória que umalist
de mesmo tamanho, e permite ao Python realizar algumas otimizações.
Entretanto, lembre-se que a imutabilidade de uma tuple
só se aplica às referências ali contidas.
Referências em 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.
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 mesmat
. Não há necessidade de cópia. Por outro lado, dada uma listal
, o construtorlist(l)
precisa criar uma nova cópia del
. -
Devido a seu tamanho fixo, uma instância de
tuple
tem alocado para si o espaço exato de memória que precisa. Em contrapartida, instâncias delist
tem alocadas para si memória adicional, para amortizar o custo de acréscimos futuros. -
As referências para os itens em uma tupla são armazenadas em um array na struct da tupla, enquanto uma lista mantém um ponteiro para um array de referências armazenada em outro lugar. Essa indireção é necessária porque, quando a lista cresce além do espaço alocado naquele momento, o Python precisa realocar o array de referências para criar espaço. A indireção adicional torna o cache da CPU menos eficiente.
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.
list
ou tuple
(os métodos implementados por object
foram omitidos para economizar espaço)
list |
tuple |
||
---|---|---|---|
|
● |
● |
s + s2—concatenação |
|
● |
s += s2—concatenação no mesmo lugar |
|
|
● |
Acrescenta um elemento após o último |
|
|
● |
Apaga todos os itens |
|
|
● |
● |
|
|
● |
Cópia rasa da lista |
|
|
● |
● |
Conta as ocorrências de um elemento |
|
● |
Remove o item na posição |
|
|
● |
Acrescenta itens do iterável |
|
|
● |
● |
s[p]—obtém o item na posição |
|
● |
Suporte a serialização otimizada com |
|
|
● |
● |
Encontra a posição da primeira ocorrência de |
|
● |
Insere elemento |
|
|
● |
● |
Obtém o iterador |
|
● |
● |
len(s)—número de itens |
|
● |
● |
s * n—concatenação repetida |
|
● |
s *= n—concatenação repetida no mesmo lugar |
|
|
● |
● |
n * s—concatenação repetida inversa[2] |
|
● |
Remove e devolve o último item ou o item na posição opcional |
|
|
● |
Remove a primeira ocorrência do elemento |
|
|
● |
Reverte, no lugar, a ordem dos itens |
|
|
● |
Obtém iterador para examinar itens, do último para o primeiro |
|
|
● |
s[p] = e—coloca |
|
|
● |
Ordena os itens no lugar, com os argumentos nomeados opcionais |
Vamos agora examinar um tópico importante para a programação Python idiomática: tuplas, listas e desempacotamento iterável.
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.
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)
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.
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.
link:code/02-array-seq/metro_lat_lon.py[role=include]
-
Cada tupla contém um registro com quatro campos, o último deles um par de coordenadas.
-
Ao atribuir o último campo a uma tupla aninhada, desempacotamos as coordenadas.
-
O teste
lon ⇐ 0:
seleciona apenas cidades no hemisfério ocidental.
A saída do Exemplo 8 é:
| latitude | longitude
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
São Paulo | -23.5478 | -46.6358
O alvo da atribuição de um desempacotamento pode também ser uma lista, mas bons casos de uso aqui são raros.
Aqui está o único que conheço: se você tem uma consulta de banco de dados que devolve um único registro (por exemplo, se o código SQL tem a instrução LIMIT 1
), daí é possível desempacotar e ao mesmo tempo se assegurar que há apenas um resultado com o seguinte código:
>>> [record] = query_returning_single_row()
Se o registro contiver apenas um campo, é possível obtê-lo diretamente, assim:
>>> [[field]] = query_returning_single_row_with_single_field()
Ambos os exemplos acima podem ser escritos com tuplas, mas não esqueça da peculiaridade sintática, tuplas com um único item devem ser escritas com uma vírgula final.
Então o primeiro alvo seria (record,)
e o segundo ((field,),)
.
Nos dois casos, esquecer aquela vírgula causa um bug silencioso.[4]
Agora vamos estudar pattern matching, que suporta maneiras ainda mais poderosas para desempacotar 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:
Robot
imaginária def handle_command(self, message):
match message: # (1)
case ['BEEPER', frequency, times]: # (2)
self.beep(times, frequency)
case ['NECK', angle]: # (3)
self.rotate_neck(angle)
case ['LED', ident, intensity]: # (4)
self.leds[ident].set_brightness(ident, intensity)
case ['LED', ident, red, green, blue]: # (5)
self.leds[ident].set_color(ident, red, green, blue)
case _: # (6)
raise InvalidCommand(message)
-
A expressão após a palavra-chave
match
é o sujeito (subject). O sujeito contém os dados que o Python vai comparar aos padrões em cada instruçãocase
. -
Esse padrão casa com qualquer sujeito que seja uma sequência de três itens. O primeiro item deve ser a string
BEEPER
. O segundo e o terceiro itens podem ser qualquer coisa, e serão vinculados às variáveisfrequency
etimes
, nessa ordem. -
Isso casa com qualquer sujeito com dois itens, se o primeiro for
'NECK'
. -
Isso vai casar com uma sujeito de três itens começando com
LED
. Se o número de itens não for correspondente, o Python segue para o próximocase
. -
Outro padrão de sequência começando com
'LED'
, agora com cinco itens—incluindo a constante'LED'
. -
Esse é o
case
default. Vai casar com qualquer sujeito que não tenha sido capturado por um dos padrões precedentes. A variável_
é especial, como logo veremos.
Olhando superficialmente, match/case
se parece instrução switch/case
da linguagem C—mas isso é só uma pequena parte da sua funcionalidade.[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
.
link:code/02-array-seq/match_lat_lon.py[role=include]
-
O sujeito desse
match
érecord
—isto é, cada uma das tuplas emmetro_areas
. -
Uma instrução
case
tem duas partes: um padrão e uma guarda opcional, com a palavra-chaveif
.
Em geral, um padrão de sequência casa com o sujeito se estas três condições forem verdadeiras:
-
O sujeito é uma sequência, e
-
O sujeito e o padrão tem o mesmo número de itens, e
-
Cada item correspondente casa, incluindo os itens aninhados.
Por exemplo, o padrão [name, _, _, (lat, lon)]
no Exemplo 10
casa com uma sequência de quatro itens, e o último item tem que ser uma sequência de dois itens.
Padrões de sequência podem ser escritos como tuplas e listas, mas a sintaxe usada não faz diferença: em um padrão de sequência, colchetes e parênteses tem o mesmo significado. Escrevi o padrão como uma lista com uma tupla aninhada de dois itens para evitar a repetição de colchetes ou parênteses no Exemplo 10.
Um padrão de sequência pode casar com instâncias da maioria das subclasses reais ou virtuais de collections.abc.Sequence
, com a exceção de str
, bytes
, e bytearray
.
Warning
|
Instâncias de 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 |
---|---|
|
|
|
|
|
|
|
|
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 |
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 |
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.
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.
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.
match/case
—requer Python ≥ 3.10link: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]
-
Casa se o sujeito for uma sequência de dois itens começando com
'quote'
. -
Casa se o sujeito for uma sequência de quatro itens começando com
'if'
. -
Casa se o sujeito for uma sequência com três ou mais itens começando com
'lambda'
. A guarda assegura quebody
não esteja vazio. -
Casa se o sujeito for uma sequência de três itens começando com
'define'
, seguido de uma instância deSymbol
. -
é uma boa prática ter um
case
para capturar todo o resto. Neste exemplo, seexp
não casar com nenhum dos padrões, a expressão está mal-formada, então gera umSyntaxError
.
Sem o último case
, para pegar tudo que tiver passado pelos anteriores, todo o bloco match
não faz nada quando o sujeito não casa com algum case
—e isso pode ser uma falha silenciosa.
Norvig deliberadamente evitou a checagem e o tratamento de erros em lis.py, para manter o código fácil de entender.
Com pattern matching, podemos acrescentar mais verificações e ainda manter o programa legível.
Por exemplo, no padrão 'define'
,
o código original não se assegura que name
é uma instância de Symbol
—isso exigiria um bloco if
, uma chamada a isinstance
, e mais código.
O Exemplo 12 é mais curto e mais seguro que o Exemplo 11.
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.
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.
case
para tratá-las
Sintaxe do Scheme | Padrão de sequência |
---|---|
|
|
|
|
|
|
|
|
|
|
Espero que a refatoração do evaluate
de Norvig com pattern matching
tenha convencido você que match/case
pode tornar seu código mais legível e mais seguro.
Note
|
Veremos mais do lis.py na [pattern_matching_case_study_sec],
quando vamos revisar o exemplo completo de |
Isso conclui nossa primeira passagem por desempacotamento, desestruturação e pattern matching com sequências. Vamos tratar de outros tipos de padrões mais adiante, em outros capítulos.
Todo programador Python sabe que sequências podem ser fatiadas usando a sintaxe s[a:b]
.
Vamos agora examinar alguns fatos menos conhecidos sobre fatiamento.
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].
A convenção pythônica de excluir o último item em fatias e faixas funciona bem com a indexação iniciada no zero usada no Python, no C e em muitas outras linguagens. Algumas características convenientes da convenção são:
-
É fácil ver o tamanho da fatia ou da faixa quando apenas a posição final é dada: tanto
range(3)
quantomy_list[:3]
produzem três itens. -
É fácil calcular o tamanho de uma fatia ou de uma faixa quando o início e o fim são dados: basta subtrair
fim-início
. -
É fácil cortar uma sequência em duas partes em qualquer índice
x
, sem sobreposição: simplesmente escrevamy_list[:x]
emy_list[x:]
. Por exemplo:>>> l = [10, 20, 30, 40, 50, 60] >>> l[:2] # split at 2 [10, 20] >>> l[2:] [30, 40, 50, 60] >>> l[:3] # split at 3 [10, 20, 30] >>> l[3:] [40, 50, 60]
Os melhores argumentos a favor desta convenção foram escritos pelo cientista da computação holandês Edsger W. Dijkstra (veja a última referência na Leitura complementar).
Agora vamos olhar mais de perto a forma como o Python interpreta a notação de fatiamento.
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.
>>> 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.
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.
Sequências mutáveis podem ser transplantadas, extirpadas e, de forma geral, modificadas no lugar com o uso da notação de fatias no lado esquerdo de um comando de atribuição ou como alvo de um comando del
.
Os próximos exemplos dão uma ideia do poder dessa notação:
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100 (1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]
-
Quando o alvo de uma atribuição é uma fatia, o lado direito deve ser um objeto iterável, mesmo que tenha apenas um item.
Todo programador sabe que a concatenação é uma operação frequente com sequências.
Tutoriais introdutórios de Python explicam o uso de +
e *
para tal propósito,
mas há detalhes sutis em seu funcionamento, como veremos a seguir.
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 próxima seção fala das armadilhas ao se tentar usar *
para inicializar uma lista de listas.
Algumas vezes precisamos inicializar uma lista com um certo número de listas aninhadas—para, por exemplo, distribuir estudantes em uma lista de equipes, ou para representar casas no tabuleiro de um jogo. A melhor forma de fazer isso é com uma compreensão de lista, como no Exemplo 14.
>>> board = [['_'] * 3 for i in range(3)] (1)
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' (2)
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
-
Cria uma lista de três listas, cada uma com três itens. Inspeciona a estrutura criada.
-
Coloca um "X" na linha 1, coluna 2, e verifica o resultado.
Um atalho tentador mas errado seria fazer algo como o Exemplo 15.
>>> weird_board = [['_'] * 3] * 3 (1)
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' (2)
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
-
A lista externa é feita de três referências para a mesma lista interna. Enquanto ela não é modificada, tudo parece correr bem.
-
Colocar um "O" na linha 1, coluna 2, revela que todas as linhas são apelidos do mesmo objeto.
O problema com o Exemplo 15 é que ele se comporta, essencialmente, como o código abaixo:
row = ['_'] * 3
board = []
for i in range(3):
board.append(row) (1)
-
A mesma
row
é anexada três vezes aoboard
.
Por outro lado, a compreensão de lista no Exemplo 14 equivale ao seguinte código:
>>> board = []
>>> for i in range(3):
... row = ['_'] * 3 # (1)
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board # (2)
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]
-
Cada iteração cria uma nova
row
e a acrescenta aoboard
. -
Como esperado, apenas a linha 2 é modificada.
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.
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)
-
O ID da lista inicial.
-
Após a multiplicação, a lista é o mesmo objeto, com novos itens anexados.
-
O ID da tupla inicial.
-
Após a multiplicação, uma nova tupla foi criada.
A concatenação repetida de sequências imutáveis é ineficiente, pois ao invés de apenas acrescentar novos itens, o interpretador tem que copiar toda a sequência alvo para criar um novo objeto com os novos itens concatenados.[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.
Tente responder sem usar o console: qual o resultado da avaliação das duas expressões no Exemplo 16?[11]
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
O que acontece a seguir? Escolha a melhor alternativa:
-
t
se torna(1, 2, [30, 40, 50, 60])
. -
É gerado um
TypeError
com a mensagem'tuple' object does not support item assignment
(o objeto tupla não suporta atribuição de itens). -
Nenhuma das alternativas acima..
-
Ambas as alternativas, A e B.
Quando vi isso, tinha certeza que a resposta era B, mas, na verdade é D, "Ambas as alternativas, A e B"! O Exemplo 17 é a saída real em um console rodando Python 3.10.[12]
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
O Online Python Tutor (EN) é uma ferramenta online fantástica para visualizar em detalhes o funcionamento do Python. A Figura 5 é uma composição de duas capturas de tela, mostrando os estados inicial e final da tupla t
do Exemplo 17.
Se olharmos o bytecode gerado pelo Python para a expressão s[a] += b
(Exemplo 18), fica claro como isso acontece.
s[a] += b
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
3 LOAD_NAME 1 (a)
6 DUP_TOP_TWO
7 BINARY_SUBSCR (1)
8 LOAD_NAME 2 (b)
11 INPLACE_ADD (2)
12 ROT_THREE
13 STORE_SUBSCR (3)
14 LOAD_CONST 0 (None)
17 RETURN_VALUE
-
Coloca o valor de
s[a]
noTOS
(Top Of Stack, topo da pilha de execução_). -
Executa
TOS += b
. Isso é bem sucedido seTOS
se refere a um objeto mutável (no Exemplo 17 é uma lista). -
Atribui
s[a] = TOS
. Isso falha ses
é imutável (a tuplat
no Exemplo 17).
Esse exemplo é um caso raro—em meus 20 anos usando Python, nunca vi esse comportamento estranho estragar o dia de alguém.
Há três lições para tirar daqui:
-
Evite colocar objetos mutáveis em tuplas.
-
A atribuição aumentada não é uma operação atômica—acabamos de vê-la gerar uma exceção após executar parte de seu trabalho.
-
Inspecionar o bytecode do Python não é muito difícil, e pode ajudar a ver o que está acontecendo por debaixo dos panos.
Após testemunharmos as sutilezas do uso de +
e *
para concatenação, podemos mudar de assunto e tratar de outra operação essencial com sequências: ordenação.
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 |
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, ekey=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 |
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)
-
Isso produz uma lista de strings ordenadas alfabeticamente.[15]
-
Inspecionando a lista original, vemos que ela não mudou.
-
Isso é a ordenação "alfabética" anterior, invertida.
-
Uma nova lista de strings, agora ordenada por tamanho. Como o algoritmo de ordenação é estável, "grape" e "apple," ambas com tamanho 5, estão em sua ordem original.
-
Essas são strings ordenadas por tamanho em ordem descendente. Não é o inverso do resultado anterior porque a ordenação é estável e então, novamente, "grape" aparece antes de "apple."
-
Até aqui, a ordenação da lista
fruits
original não mudou. -
Isso ordena a lista no mesmo lugar, devolvendo
None
(que o console omite). -
Agora
fruits
está ordenada.
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.
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, |
O restante desse capítulo discute tipos mutáveis de sequências que, em muitos casos, podem substituir as listas. Começamos pelos arrays.
Se uma lista contém apenas números, uma array.array
é um substituto mais eficiente.
Arrays suportam todas as operações das sequências mutáveis (incluindo .pop
, .insert
, e .extend
), bem como métodos adicionais para carregamento e armazenamento rápidos, tais como
.frombytes
e .tofile
.
Um array do Python quase tão enxuto quanto um array do C.
Como mostrado na Figura 1, um array
de valores float
não mantém instâncias completas de float
, mas apenas pacotes de bytes representando seus valores em código de máquina—de forma similar a um array de double
na linguagem C.
Ao criar um array
, você fornece um código de tipo (typecode), uma letra que determina o tipo C subjacente usado para armazenar cada item no array.
Por exemplo, b é o código de tipo para o que o C chama de signed char
, um inteiro variando de -128 a 127.
Se você criar uma array('b')
, então cada item será armazenado em um único byte e será interpretado como um inteiro. Para grandes sequências de números, isso economiza muita memória.
E o Python não permite que você insira qualquer número que não corresponda ao tipo do array.
O Exemplo 19 mostra a criação, o armazenamento e o carregamento de um array de 10 milhões de números de ponto flutuante aleatórios.
>>> from array import array (1)
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7))) (2)
>>> floats[-1] (3)
0.07802343889111107
>>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp) (4)
>>> fp.close()
>>> floats2 = array('d') (5)
>>> fp = open('floats.bin', 'rb')
>>> floats2.fromfile(fp, 10**7) (6)
>>> fp.close()
>>> floats2[-1] (7)
0.07802343889111107
>>> floats2 == floats (8)
True
-
Importa o tipo
array
. -
Cria um array de números de ponto flutuante de dupla precisão (código de tipo
'd'
) a partir de qualquer objeto iterável—nesse caso, uma expressão geradora. -
Inspeciona o último número no array.
-
Salva o array em um arquivo binário.
-
Cria um array vazio de números de ponto flutuante de dupla precisão
-
Lê 10 milhões de números do arquivo binário.
-
Inspeciona o último número no array.
-
Verifica a igualdade do conteúdo dos arrays
Como você pode ver, array.tofile
e array.fromfile
são fáceis de usar.
Se você rodar o exemplo, verá que são também muito rápidos.
Um pequeno experimento mostra que array.fromfile
demora aproximadamente 0,1 segundos para carregar 10 milhões de números de ponto flutuante de dupla precisão de um arquivo binário criado com array.tofile
.
Isso é quase 60 vezes mais rápido que ler os números de um arquivo de texto,
algo que também exige passar cada linha para a função embutida float
.
Salvar o arquivo com array.tofile
é umas sete vezes mais rápido que escrever um número de ponto flutuante por vez em um arquivo de texto.
Além disso, o tamanho do arquivo binário com 10 milhões de números de dupla precisão é de 80.000.000 bytes (8 bytes por número, zero excesso), enquanto o arquivo de texto ocupa 181.515.739 bytes para os mesmos dados.
Para o caso específico de arrays numéricas representando dados binários, tal como bitmaps de imagens, o Python tem os tipos bytes
e bytearray
, discutidos na [strings_bytes_files].
Vamos encerrar essa seção sobre arrays com a Tabela 3,
comparando as características de list
e array.array
.
list
ou array
(os métodos descontinuados de array e aqueles implementados também pir object foram omitidos para preservar espaço)
list | array | ||
---|---|---|---|
|
● |
● |
|
|
● |
● |
|
|
● |
● |
Acrescenta um elemento após o último |
|
● |
Permuta os bytes de todos os itens do array para conversão de endianness (ordem de interpretação bytes) |
|
|
● |
Apaga todos os itens |
|
|
● |
● |
|
|
● |
Cópia rasa da lista |
|
|
● |
Suporte a |
|
|
● |
● |
Conta as ocorrências de um elemento |
|
● |
Suporte otimizado a |
|
|
● |
● |
Remove item na posição |
|
● |
● |
Acrescenta itens a partir do iterável |
|
● |
Acrescenta itens de uma sequência de bytes, interpretada como valores em código de máquina empacotados |
|
|
● |
Acrescenta |
|
|
● |
Acrescenta itens de lista; se um deles causar um |
|
|
● |
● |
|
|
● |
● |
Encontra a posição da primeira ocorrência de |
|
● |
● |
Insere elemento |
|
● |
Tamanho em bytes de cada item do array |
|
|
● |
● |
Obtém iterador |
|
● |
● |
|
|
● |
● |
|
|
● |
● |
|
|
● |
● |
n * s—concatenação repetida invertida[17] |
|
● |
● |
Remove e devolve o item na posição |
|
● |
● |
Remove a primeira ocorrência do elemento |
|
● |
● |
Reverte a ordem dos itens no mesmo lugar |
|
● |
Obtém iterador para percorrer itens do último até o primeiro |
|
|
● |
● |
s[p] = e—coloca |
|
● |
Ordena itens no mesmo lugar, com os argumentos de palavra-chave opcionais |
|
|
● |
Devolve itens como pacotes de valores em código de máquina em um objeto |
|
|
● |
Grava itens como pacotes de valores em código de máquina no arquivo binário |
|
|
● |
Devolve os itens como objetos numéricos em uma |
|
|
● |
String de um caractere identificando o tipo em C dos itens |
Tip
|
Até o Python 3.10, o tipo a = array.array(a.typecode, sorted(a)) Para manter a ordem de um array ordenado ao acrescentar novos itens, use a função |
Se você trabalha muito com arrays e não conhece memoryview
,
está perdendo oportunidades. Veja o próximo tópico.
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.
>>> from array import array
>>> octets = array('B', range(6)) # (1)
>>> m1 = memoryview(octets) # (2)
>>> m1.tolist()
[0, 1, 2, 3, 4, 5]
>>> m2 = m1.cast('B', [2, 3]) # (3)
>>> m2.tolist()
[[0, 1, 2], [3, 4, 5]]
>>> m3 = m1.cast('B', [3, 2]) # (4)
>>> m3.tolist()
[[0, 1], [2, 3], [4, 5]]
>>> m2[1,1] = 22 # (5)
>>> m3[1,1] = 33 # (6)
>>> octets # (7)
array('B', [0, 1, 2, 33, 22, 5])
-
Cria um array de 6 bytes (código de tipo
'B'
). -
Cria uma
memoryview
a partir daquele array, e a exporta como uma lista. -
Cria uma nova
memoryview
a partir da anterior, mas com2
linhas e3
colunas. -
Ainda outra
memoryview
, agora com3
linhas e2
colunas. -
Sobrescreve o byte em
m2
, na linha1
, coluna1
com22
. -
Sobrescreve o byte em
m3
, na linha1
, coluna1
com33
. -
Mostra o array original, provando que a memória era compartilhada entre
octets
,m1
,m2
, em3
.
O fantástico poder de memoryview
também pode ser usado para o mal.
O Exemplo 21 mostra como mudar um único byte de um item em um array de inteiros de 16 bits.
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) (1)
>>> len(memv)
5
>>> memv[0] (2)
-2
>>> memv_oct = memv.cast('B') (3)
>>> memv_oct.tolist() (4)
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 (5)
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) (6)
-
Cria uma
memoryview
a partir de um array de 5 inteiros com sinal de 16 bits (código de tipo'h'
). -
memv
vê os mesmos 5 itens no array. -
Cria
memv_oct
, transformando os elementos dememv
em bytes (código de tipo'B'
). -
Exporta os elementos de
memv_oct
como uma lista de 10 bytes, para inspeção. -
Atribui o valor
4
ao byte com offset5
. -
Observe a mudança em
numbers
: um4
no byte mais significativo de um inteiro de 2 bytes sem sinal é1024
.
Note
|
Você pode ver um exemplo de inspeção de uma |
Enquanto isso, se você está fazendo processamento numérico avançado com arrays, deveria estar usando as bibliotecas NumPy. Vamos agora fazer um breve passeio por elas.
Por todo esse livro, procuro destacar o que já existe na biblioteca padrão do Python, para que você a aproveite ao máximo. Mas a NumPy é tão maravilhosa que exige um desvio.
Por suas operações avançadas de arrays e matrizes, o Numpy é a razão pela qual o Python se tornou uma das principais linguagens para aplicações de computação científica. A Numpy implementa tipos multidimensionais e homogêneos de arrays e matrizes, que podem conter não apenas números, mas também registros definidos pelo usuário. E fornece operações eficientes ao nível desses elementos.
A SciPy é uma biblioteca criada usando a NumPy, e oferece inúmeros algoritmos de computação científica, incluindo álgebra linear, cálculo numérico e estatística. A SciPy é rápida e confiável porque usa a popular base de código C e Fortran do Repositório Netlib. Em outras palavras, a SciPy dá a cientistas o melhor de dois mundos: um prompt iterativo e as APIs de alto nível do Python, junto com funções estáveis e de eficiência comprovada para processamento de números, otimizadas em C e Fortran
O Exemplo 22, uma amostra muito rápida da Numpy, demonstra algumas operações básicas com arrays bi-dimensionais.
numpy.ndarray
>>> import numpy as np (1)
>>> a = np.arange(12) (2)
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape (3)
(12,)
>>> a.shape = 3, 4 (4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[2] (5)
array([ 8, 9, 10, 11])
>>> a[2, 1] (6)
9
>>> a[:, 1] (7)
array([1, 5, 9])
>>> a.transpose() (8)
array([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])
-
Importa a NumPy, que precisa ser instalada previamente (ela não faz parte da biblioteca padrão do Python). Por convenção,
numpy
é importada comonp
. -
Cria e inspeciona uma
numpy.ndarray
com inteiros de0
a11
. -
Inspeciona as dimensões do array: essa é um array com uma dimensão e 12 elementos.
-
Muda o formato do array, acrescentando uma dimensão e depois inspecionando o resultado.
-
Obtém a linha no índice
2
-
Obtém elemento na posição
2, 1
. -
Obtém a coluna no índice
1
-
Cria um novo array por transposição (permutando as colunas com as linhas)
A NumPy também suporta operações de alto nível para carregar, salvar e operar sobre todos os elementos de uma numpy.ndarray
:
>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt') (1)
>>> floats[-3:] (2)
array([ 3016362.69195522, 535281.10514262, 4566560.44373946])
>>> floats *= .5 (3)
>>> floats[-3:]
array([ 1508181.34597761, 267640.55257131, 2283280.22186973])
>>> from time import perf_counter as pc (4)
>>> t0 = pc(); floats /= 3; pc() - t0 (5)
0.03690556302899495
>>> numpy.save('floats-10M', floats) (6)
>>> floats2 = numpy.load('floats-10M.npy', 'r+') (7)
>>> floats2 *= 6
>>> floats2[-3:] (8)
memmap([ 3016362.69195522, 535281.10514262, 4566560.44373946])
-
Carrega 10 milhões de números de ponto flutuante de um arquivo de texto.
-
Usa a notação de fatiamento de sequência para inspecionar os três últimos números.
-
Multiplica cada elemento no array
floats
por.5
e inspeciona novamente os três últimos elementos. -
Importa o cronômetro de medida de tempo em alta resolução (disponível desde o Python 3.3).
-
Divide cada elemento por
3
; o tempo decorrido para dividir os 10 milhões de números de ponto flutuante é menos de 40 milissegundos. -
Salva o array em um arquivo binário .npy.
-
Carrega os dados como um arquivo mapeado na memória em outro array; isso permite o processamento eficiente de fatias do array, mesmo que ele não caiba inteiro na memória.
-
Inspeciona os três últimos elementos após multiplicar cada elemento por
6
.
Mas isso foi apenas um aperitivo.
A NumPy e a SciPy são bibliotecas formidáveis, e estão na base de outras ferramentas fantásticas, como a Pandas (EN)—que implementa tipos eficientes de arrays capazes de manter dados não-numéricos, e fornece funções de importação/exportação em vários formatos diferentes, como .csv, .xls, dumps SQL, HDF5, etc.—e a scikit-learn (EN), o conjunto de ferramentas para Aprendizagem de Máquina mais usado atualmente. A maior parte das funções da NumPy e da SciPy são implementadas em C ou C++, e conseguem aproveitar todos os núcleos de CPU disponíveis, pois podem liberar a GIL (Global Interpreter Lock, Trava Global do Interpretador) do Python. O projeto Dask suporta a paralelização do processamento da NumPy, da Pandas e da scikit-learn para grupos (clusters) de máquinas. Esses pacotes merecem 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).
Os métodos .append
e .pop
tornam uma list
usável como uma pilha (stack) ou uma fila (queue) (usando .append
e .pop(0)
, se obtém um comportamento FIFO).
Mas inserir e remover da cabeça de uma lista (a posição com índice 0) é caro, pois a lista toda precisa ser deslocada na memória.
A classe collections.deque
é uma fila de duas pontas e segura para usar com threads, projetada para inserção e remoção rápida nas duas pontas. É também a estrutura preferencial se você precisa manter uma lista de "últimos itens vistos" ou coisa semelhante, pois um deque
pode ser delimitado—isto é, criado com um tamanho máximo fixo. Se um deque
delimitado está cheio, quando se adiciona um novo item, o item na ponta oposta é descartado.
O Exemplo 23 mostra algumas das operações típicas com um deque
.
deque
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10) (1)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3) (2)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1) (3)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33]) (4)
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40]) (5)
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
-
O argumento opcional
maxlen
determina o número máximo de itens permitidos nessa instância dedeque
; isso estabelece o valor de um atributo de instânciamaxlen
, somente de leitura. -
Rotacionar com
n > 0
retira itens da direita e os recoloca pela esquerda; quandon < 0
, os itens são retirados pela esquerda e anexados pela direita. -
Acrescentar itens a um
deque
cheio (len(d) == d.maxlen
) elimina itens da ponta oposta. Observe, na linha seguinte, que o0
foi descartado. -
Acrescentar três itens à direita derruba
-1
,1
, e2
da extremidade esquerda. -
Observe que
extendleft(iter)
acrescenta cada item sucessivo do argumentoiter
do lado esquerdo dodeque
, então a posição final dos itens é invertida.
A Tabela 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.
list
ou deque
(aqueles também implementados por object
foram omitidos para preservar espaço)
list | deque | ||
---|---|---|---|
|
● |
s + s2—concatenação |
|
|
● |
● |
s += s2—concatenação no mesmo lugar |
|
● |
● |
Acrescenta um elemento à direita (após o último) |
|
● |
Acrescenta um elemento à esquerda (antes do primeiro) |
|
|
● |
● |
Apaga todos os itens |
|
● |
|
|
|
● |
Cópia rasa da lista |
|
|
● |
Suporte a |
|
|
● |
● |
Conta ocorrências de um elemento |
|
● |
● |
Remove item na posição |
|
● |
● |
Acrescenta item do iterável |
|
● |
Acrescenta item do iterável |
|
|
● |
● |
s[p]—obtém item ou fatia na posição |
|
● |
Encontra a primeira ocorrência de |
|
|
● |
Insere elemento |
|
|
● |
● |
Obtém iterador |
|
● |
● |
len(s)—número de itens |
|
● |
s * n—concatenação repetida |
|
|
● |
s *= n—concatenação repetida no mesmo lugar |
|
|
● |
n * s—concatenação repetida invertida[18] |
|
|
● |
● |
Remove e devolve último item[19] |
|
● |
Remove e devolve primeiro item |
|
|
● |
● |
Remove primeira ocorrência do elemento |
|
● |
● |
Inverte a ordem do itens no mesmo lugar |
|
● |
● |
Obtém iterador para percorrer itens, do último para o primeiro |
|
● |
Move |
|
|
● |
● |
s[p] = e—coloca |
|
● |
Ordena os itens no mesmo lugar, com os argumentos de palavra-chave opcionais |
Além de deque
, outros pacotes da biblioteca padrão do Python implementam filas:
queue
-
Fornece as classes sincronizadas (isto é, seguras para se usar com múltiplas threads)
SimpleQueue
,Queue
,LifoQueue
, ePriorityQueue
. Essas classes podem ser usadas para comunicação segura entre threads. Todas, excetoSimpleQueue
, podem ser delimitadas passando um argumentomaxsize
maior que 0 ao construtor. Entretanto, elas não descartam um item para abrir espaço, como fazdeque
. Em vez disso, quando a fila está lotada, a inserção de um novo item bloqueia quem tentou inserir—isto é, ela espera até alguma outra thread criar espaço retirando um item da fila, algo útil para limitar o número de threads ativas. multiprocessing
-
Implementa sua própria
SimpleQueue
, não-delimitada, eQueue
, delimitada, muito similares àquelas no pacotequeue
, mas projetadas para comunicação entre processos. Uma fila especializada,multiprocessing.JoinableQueue
, é disponibilizada para gerenciamento de tarefas. asyncio
-
Fornece
Queue
,LifoQueue
,PriorityQueue
, eJoinableQueue
com APIs inspiradas pelas classes nos módulosqueue
emultiprocessing
, mas adaptadas para gerenciar tarefas em programação assíncrona. heapq
-
Diferente do últimos três módulos,
heapq
não implementa a classe queue, mas oferece funções comoheappush
eheappop
, que permitem o uso de uma sequência mutável como uma fila do tipo heap ou como uma fila de prioridade.
Aqui termina nossa revisão das alternativas ao tipo list
, e também nossa exploração dos tipos sequência em geral—exceto pelas especificidades de str
e das sequências binárias, que tem seu próprio capítulo ([strings_bytes_files]).
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.
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).
O post de Eli Bendersky em seu blog, "Less copies in Python with the buffer protocol and memoryviews" (Menos cópias em Python, com o protocolo de buffer e mamoryviews) inclui um pequeno tutorial sobre memoryview
.
Há muitos livros tratando da NumPy no mercado, e muitos não mencionam "NumPy" no título. Dois exemplos são o Python Data Science Handbook, escrito por Jake VanderPlas e de acesso aberto, e a segunda edição do Python for Data Analysis, de Wes McKinney.
"A Numpy é toda sobre vetorização". Essa é a frase de abertura do livro de acesso aberto From Python to NumPy, de Nicolas P. Rougier. Operações vetorizadas aplicam funções matemáticas a todos os elementos de um array sem um loop explícito escrito em Python. Elas podem operar em paralelo, usando instruções especiais de vetor presentes em CPUs modernas, tirando proveito de múltiplos núcleos ou delegando para a GPU, dependendo da biblioteca. O primeiro exemplo no livro de Rougier mostra um aumento de velocidade de 500 vezes, após a refatoração de uma bela classe pythônica, usando um método gerador, em uma pequena e feroz função que chama um par de funções de vetor da NumPy.
Para aprender a usar deque
(e outras coleções), veja os exemplos e as receitas práticas em
"Tipos de dados de contêineres", na documentação do Python.
A melhor defesa da convenção do Python de excluir o último item em faixas e fatias foi escrita pelo próprio Edsger W.
Dijkstra, em uma nota curta intitulada "Why Numbering Should Start at Zero" (Porque a Numeração Deve Começar em Zero).
O assunto da nota é notação matemática, mas ela é relevante para o Python porque
Dijkstra explica, com humor e rigor, porque uma sequência como 2, 3, …, 12 deveria sempre ser expressa como 2 ≤ i < 13.
Todas as outras convenções razoáveis são refutadas, bem como a ideia de deixar cada usuário escolher uma convenção.
O título se refere à indexação baseada em zero, mas a nota na verdade é sobre porque é desejável que 'ABCDE'[1:3]
signifique 'BC'
e não 'BCD'
, e porque faz todo sentido escrever range(2, 13)
para produzir 2, 3, 4, …, 12.
E, por sinal, a nota foi escrita à mão, mas é linda e totalmente legível.
A letra de Dijkstra é tão cristalina que alguém criou uma fonte a partir de suas anotações.
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
.
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.
eval
no código original; a renomeei para evitar que fosse confundida com a função embutida eval
do Python.
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
.
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.
t[2].extend([50,60])
, sem erros. Eu sei disso, mas a intenção aqui é mostrar o comportamento estranho do operador +=
nesse caso.
self
no corpo do método.