Skip to content

Latest commit

 

History

History
1355 lines (1063 loc) · 90.5 KB

cap03.adoc

File metadata and controls

1355 lines (1063 loc) · 90.5 KB

Dicionários e conjuntos

O Python é feito basicamente de dicionários cobertos por muitas camadas de açúcar sintático

— Lalo Martins
pioneiro do nomadismo digital e pythonista

Usamos dicionários em todos os nossos programas Python. Se não diretamente em nosso código, então indiretamente, pois o tipo dict é um elemento fundamental da implementação do Python. Atributos de classes e de instâncias, espaços de nomes de módulos e argumentos nomeados de funções são alguns dos elementos fundamentais do Python representados na memória por dicionários. O __builtins__.__dict__ armazena todos os tipos, funções e objetos embutidos.

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

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

Aqui está um breve esquema do capítulo:

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

  • Métodos comuns dos tipos de mapeamentos

  • Tratamento especial para chaves ausentes

  • Variantes de dict na biblioteca padrão

  • Os tipos set e frozenset

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

Novidades nesse capítulo

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

A implementação interna de dict e set ainda está alicerçada em tabelas de hash, mas o código de dict teve duas otimizações importantes, que economizam memória e preservam o ordem de inserção das chaves. As seções Consequências práticas da forma como dict funciona e Consequências práticas da forma de funcionamento dos conjuntos resumem o que você precisa saber sobre isso para usar bem as estruturas efetadas.

Note

Após acrescentar mais de 200 páginas a essa segunda edição, transferi a seção opcional "Internals of sets and dicts" (As entranhas dos sets e dos dicts) (EN) para o fluentpython.com, o site que complementa o livro. O post de 18 páginas (EN) foi atualizado e expandido, e inclui explicações e diagramas sobre:

  • O algoritmo de tabela de hash e as estruturas de dados, começando por seu uso em set, que é mais simples de entender.

  • A otimização de memória que preserva a ordem de inserção de chaves em instâncias de dict (desde o Python 3.6) .

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

A sintaxe moderna dos dicts

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

Compreensões de dict

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

Exemplo 1. Exemplos de compreensões de dict
>>> dial_codes = [                                                  # (1)
...     (880, 'Bangladesh'),
...     (55,  'Brazil'),
...     (86,  'China'),
...     (91,  'India'),
...     (62,  'Indonesia'),
...     (81,  'Japan'),
...     (234, 'Nigeria'),
...     (92,  'Pakistan'),
...     (7,   'Russia'),
...     (1,   'United States'),
... ]
>>> country_dial = {country: code for code, country in dial_codes}  # (2)
>>> country_dial
{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62,
'Japan': 81, 'Nigeria': 234, 'Pakistan': 92, 'Russia': 7, 'United States': 1}
>>> {code: country.upper()                                          # (3)
...     for country, code in sorted(country_dial.items())
...     if code < 70}
{55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA', 1: 'UNITED STATES'}
  1. Um iterável de pares chave-valor como dial_codes pode ser passado diretamente para o construtor de dict, mas…​

  2. …​aqui permutamos os pares: country é a chave, e code é o valor.

  3. Ordenando country_dial por nome, revertendo novamente os pares, colocando os valores em maiúsculas e filtrando os itens com code < 70.

Se você já está acostumada com as listcomps, as dictcomps são um próximo passo natural. Caso contrário, a propagação da sintaxe de compreensão mostra que agora é mais valioso que nunca se tornar fluente nessa técnica.

Desempacotando mapeamentos

A PEP 448—Additional Unpacking Generalizations (Generalizações de Desempacotamento Adicionais) melhorou o suporte ao desempacotamento de mapeamentos de duas formas, desde o Python 3.5.

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

>>> def dump(**kwargs):
...     return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}

Em segundo lugar, ** pode ser usado dentro de um literal dict—também múltiplas vezes:

>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}

Nesse caso, chaves duplicadas são permitidas. Cada ocorrência sobrescreve ocorrências anteriores—observe o valor mapeado para x no exemplo.

Essa sintaxe também pode ser usada para mesclar mapas, mas isso pode ser feito de outras formas. Siga comigo.

Fundindo mapeamentos com |

Desde a versão 3.9, Python suporta o uso de | e |= para mesclar mapeamentos. Isso faz todo sentido, já que estes são também os operadores de união de conjuntos.

O operador | cria um novo mapeamento:

>>> d1 = {'a': 1, 'b': 3}
>>> d2 = {'a': 2, 'b': 4, 'c': 6}
>>> d1 | d2
{'a': 2, 'b': 4, 'c': 6}

O tipo do novo mapeamento normalmente será o mesmo do operando da esquerda—no exemplo, d1—mas ele pode ser do tipo do segundo operando se tipos definidos pelo usuário estiverem envolvidos na operação, dependendo das regras de sobrecarga de operadores, que exploraremos no [operator_overloading].

Para atualizar mapeamentos existentes no mesmo lugar, use |=. Retomando o exemplo anterior, ali d1 não foi modificado. Mas aqui sim:

>>> d1
{'a': 1, 'b': 3}
>>> d1 |= d2
>>> d1
{'a': 2, 'b': 4, 'c': 6}
Tip

Se você precisa manter código rodando no Python 3.8 ou anterior, a seção "Motivation" (Motivação) (EN) da PEP 584—Add Union Operators To dict (Acrescentar Operadores de União a dict) (EN) inclui um bom resumo das outras formas de mesclar mapeamentos.

Agora vamos ver como o pattern matching se aplica aos mapeamentos.

Pattern matching com mapeamentos

A instrução match/case suporta sujeitos que sejam objetos mapeamento. Padrões para mapeamentos se parecem com literais dict, mas podem casar com instâncias de qualquer subclasse real ou virtual de collections.abc.Mapping.[1]

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

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

Exemplo 2. creator.py: get_creators() extrai o nome dos criadores em registros de mídia
link:code/03-dict-set/py3.10/creator.py[role=include]
  1. Casa com qualquer mapeamento na forma 'type': 'book', 'api' :2, e uma chave 'authors' mapeada para uma sequência. Devolve os itens da sequência, como uma nova list.

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

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

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

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

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

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

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

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

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

link:code/03-dict-set/py3.10/creator.py[role=include]

Observe que a ordem das chaves nos padrões é irrelevante, mesmo se o sujeito for um OrderedDict como b2.

Diferente de patterns de sequência, patterns de mapeamento funcionam com matches parciais. Nos doctests, os sujeitos b1 e b2 incluem uma chave 'title', que não aparece em nenhum padrão 'book', mas mesmo assim casam.

Não há necessidade de usar **extra para casar pares chave-valor adicionais, mas se você quiser capturá-los como um dict, pode prefixar uma variável com **. Ela precisa ser a última do padrão, e **_ é proibido, pois seria redundante. Um exemplo simples:

>>> food = dict(category='ice cream', flavor='vanilla', cost=199)
>>> match food:
...     case {'category': 'ice cream', **details}:
...         print(f'Ice cream details: {details}')
...
Ice cream details: {'flavor': 'vanilla', 'cost': 199}

Na Tratamento automático de chaves ausentes, vamos estudar o defaultdict e outros mapeamentos onde buscas com chaves via __getitem__ (isto é, d[chave]) funcionam porque itens ausentes são criados na hora. No contexto do pattern matching, um match é bem sucedido apenas se o sujeito já possui as chaves necessárias no início do bloco match.

Tip

O tratamento automático de chaves ausentes não é acionado porque o pattern matching sempre usa o método d.get(key, sentinel)—onde o sentinel default é um marcador com valor especial, que não pode aparecer nos dados do usuário.

Vistas a sintaxe e a estrutura, vamos estudar a API dos mapeamentos.

A API padrão dos tipos de mapeamentos

O módulo collections.abc contém as ABCs Mapping e MutableMapping, descrevendo as interfaces de dict e de tipos similares. Veja a Figura 1.

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

>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
>>> isinstance(my_dict, abc.MutableMapping)
True
Tip

Usar isinstance com uma ABC é muitas vezes melhor que verificar se um argumento de função é do tipo concreto dict, porque daí tipos alternativos de mapeamentos podem ser usados. Vamos discutir isso em detalhes no [ifaces_prot_abc].

Diagrama de classes UML para `Mapping` e `MutableMapping`
Figura 1. Diagrama de classe simplificado para MutableMapping e suas superclasses de collections.abc (as setas de herança apontam das subclasses para as superclasses; nomes em itálico indicam classes e métodos abstratos

Para implementar uma mapeamento personalizado, é mais fácil estender collections.UserDict, ou envolver um dict por composição, ao invés de criar uma subclasse dessas ABCs. A classe collections.UserDict e todas as classes concretas de mapeamentos da biblioteca padrão encapsulam o dict básico em suas implementações, que por sua vez é criado sobre uma tabela de hash. Assim, todas elas compartilham a mesma limitação, as chaves precisam ser hashable (os valores não precisam ser hashable, só as chaves). Se você precisa de uma recapitulação, a próxima seção explica isso.

O que é hashable?

Aqui está parte da definição de hashable, adaptado do Glossário do Python:

Um objeto é hashable se tem um código de hash que nunca muda durante seu ciclo de vida (precisa ter um método hash()) e pode ser comparado com outros objetos (precisa ter um método eq()). Objetos hashable que são comparados como iguais devem ter o mesmo código de hash.[2]

Tipos numéricos e os tipos planos imutáveis str e bytes são todos hashable. Tipos contêineres são hashable se forem imutáveis e se todos os objetos por eles contidos forem também hashable. Um frozenset é sempre hashable, pois todos os elementos que ele contém devem ser, por definição, hashable. Uma tuple é hashable apenas se todos os seus itens também forem. Observe as tuplas tt, tl, and tf:

>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110

O código de hash de um objeto pode ser diferente dependendo da versão do Python, da arquitetura da máquina, e pelo sal acrescentado ao cálculo do hash por razões de segurança.[3] O código de hash de um objeto corretamente implementado tem a garantia de ser constante apenas dentro de um processo Python.

Tipos definidos pelo usuário são hashble por default, pois seu código de hash é seu id(), e o método __eq__() herdado da classe objetct apenas compara os IDs dos objetos. Se um objeto implementar seu próprio __eq__(), que leve em consideração seu estado interno, ele será hashable apenas se seu __hash__() sempre devolver o mesmo código de hash. Na prática, isso exige que __eq__() e __hash__() levem em conta apenas atributos de instância que nunca mudem durante a vida do objeto.

Vamos agora revisar a API dos tipos de mapeamento mais comumente usado no Python: dict, defaultdict, e OrderedDict.

Revisão dos métodos mais comuns dos mapeamentos

A API básica para mapeamentos é muito variada. A Tabela 1 mostra os métodos implementados por dict e por duas variantes populares: defaultdict e OrderedDict, ambas classes definidas no módulo collections.

Tabela 1. Métodos do tipos de mapeamento dict, collections.defaultdict, e collections.OrderedDict (métodos comuns de object omitidos por concisão); argumentos opcionais então entre […]
dict defaultdict OrderedDict  

d.clear()

Remove todos os itens.

d.__contains__(k)

k in d.

d.copy()

Cópia rasa.

d.__copy__()

Suporte a copy.copy(d).

d.default_factory

Chamável invocado por __missing__ para definir valores ausentes.[4]

d.__delitem__(k)

del d[k]—remove item com chave k

d.fromkeys(it, [initial])

Novo mapeamento com chaves no iterável it, com um valor inicial opcional (o default é None).

d.get(k, [default])

Obtém item com chave k, devolve default ou None se k não existir.

d.__getitem__(k)

d[k]—obtém item com chave k.

d.items()

Obtém uma view dos itens—pares (chave, valor).

d.__iter__()

Obtém iterador das chaves.

d.keys()

Obtém view das chaves.

d.__len__()

len(d)—número de itens.

d.__missing__(k)

Chamado quando __getitem__ não consegue encontrar a chave.

d.move_to_end(k, [last])

Move k para primeira ou última posição (last é True por default).

d.__or__(other)

Suporte a d1 | d2 para criar um novo `dict`, fundindo d1 e d2 (Python ≥ 3.9).

d.__ior__(other)

Suporte a d1 |= d2 para atualizar d1 com d2 (Python ≥ 3.9).

d.pop(k, [default])

Remove e devolve valor em k, ou default ou None, se k não existir.

d.popitem()

Remove e devolve, na forma (chave, valor), o último item inserido.[5]

d.__reversed__()

Suporte a reverse(d)—devolve um iterador de chaves, da última para a primeira a serem inseridas.

d.__ror__(other)

Suporte a other | dd—operador de união invertido (Python ≥ 3.9)[6]

d.setdefault(k, [default])

Se k in d, devolve d[k]; senão, atribui d[k] = default e devolve isso.

d.__setitem__(k, v)

d[k] = v—coloca v em k

d.update(m, [**kwargs])

Atualiza d com itens de um mapeamento ou iterável de pares (chave, valor).

d.values()

Obtém uma view dos valores.

A forma como d.update(m) lida com seu primeiro argumento, m, é um excelente exemplo de duck typing (tipagem pato): ele primeiro verifica se m possui um método keys e, em caso afirmativo, assume que m é um mapeamento. Caso contrário, update() reverte para uma iteração sobre m, presumindo que seus item são pares (chave, valor). O construtor da maioria dos mapeamentos do Python usa internamente a lógica de update(), o que quer dizer que eles podem ser inicializados por outros mapeamentos ou a partir de qualquer objeto iterável que produza pares (chave, valor).

Um método sutil dos mapeamentos é setdefault(). Ele evita buscas redundantes de chaves quando precisamos atualizar o valor em um item no mesmo lugar. A próxima seção mostra como ele pode ser usado.

Inserindo ou atualizando valores mutáveis

Alinhada à filosofia de falhar rápido do Python, a consulta a um dict com d[k] gera um erro quando k não é uma chave existente. Pythonistas sabem que d.get(k, default) é uma alternativa a d[k] sempre que receber um valor default é mais conveniente que tratar um KeyError. Entretanto, se você está buscando um valor mutável e quer atualizá-lo, há um jeito melhor.

Considere um script para indexar texto, produzindo um mapeamento no qual cada chave é uma palavra, e o valor é uma lista das posições onde aquela palavra ocorre, como mostrado no Exemplo 3.

Exemplo 3. Saída parcial do Exemplo 4 processando o texto "Zen of Python"; cada linha mostra uma palavra e uma lista de ocorrências na forma de pares (line_number, column_number) (número da linha, _número da coluna)
$ python3 index0.py zen.txt
a [(19, 48), (20, 53)]
Although [(11, 1), (16, 1), (18, 1)]
ambiguity [(14, 16)]
and [(15, 23)]
are [(21, 12)]
aren [(10, 15)]
at [(16, 38)]
bad [(19, 50)]
be [(15, 14), (16, 27), (20, 50)]
beats [(11, 23)]
Beautiful [(3, 1)]
better [(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8), (18, 25)]
...

O Exemplo 4 é um script aquém do ideal, para mostrar um caso onde dict.get não é a melhor maneira de lidar com uma chave ausente. Ele foi adaptado de um exemplo de Alex Martelli.[7]

Exemplo 4. index0.py usa dict.get para obter e atualizar uma lista de ocorrências de palavras de um índice (uma solução melhor é apresentada no Exemplo 5)
link:code/03-dict-set/index0.py[role=include]
  1. Obtém a lista de ocorrências de word, ou [] se a palavra não for encontrada.

  2. Acrescenta uma nova localização a occurrences.

  3. Coloca a occurrences modificada no dict index; isso exige uma segunda busca em index.

  4. Não estou chamando str.upper no argumento key= de sorted, apenas passando uma referência àquele método, para que a função sorted possa usá-lo para normalizar as palavras antes de ordená-las.[8]

As três linhas tratando de occurrences no Exemplo 4 podem ser substituídas por uma única linha usando dict.setdefault. O Exemplo 5 fica mais próximo do código apresentado por Alex Martelli.

Exemplo 5. index.py usa dict.setdefault para obter e atualizar uma lista de ocorrências de uma palavra em uma única linha de código; compare com o Exemplo 4
link:code/03-dict-set/index.py[role=include]
  1. Obtém a lista de ocorrências de word, ou a define como [], se não for encontrada; setdefault devolve o valor, então ele pode ser atualizado sem uma segunda busca.

Em outras palavras, o resultado final desta linha…​

my_dict.setdefault(key, []).append(new_value)

…​é o mesmo que executar…​

if key not in my_dict:
    my_dict[key] = []
my_dict[key].append(new_value)

…​exceto que este último trecho de código executa pelo menos duas buscas por key—três se a chave não for encontrada—enquanto setdefault faz tudo isso com uma única busca.

Uma questão relacionada, o tratamento de chaves ausentes em qualquer busca (e não apenas para inserção de valores), é o assunto da próxima seção.

Tratamento automático de chaves ausentes

Algumas vezes é conveniente que os mapeamentos devolvam algum valor padronizado quando se busca por uma chave ausente. Há duas abordagem principais para esse fim: uma é usar um defaultdict em vez de um dict simples. A outra é criar uma subclasse de dict ou de qualquer outro tipo de mapeamento e acrescentar um método __missing__. Vamos ver as duas soluções a seguir.

defaultdict: outra perspectiva sobre as chaves ausentes

Uma instância de collections.defaultdict cria itens com um valor default sob demanda, sempre que uma chave ausente é buscada usando a sintaxe d[k]. O Exemplo 6 usa defaultdict para fornecer outra solução elegante para o índice de palavras do Exemplo 5.

Funciona assim: ao instanciar um defaultdict, você fornece um chamável que produz um valor default sempre que __getitem__ recebe uma chave inexistente como argumento.

Por exemplo, dado um defaultdict criado por dd = defaultdict(list), se 'new-key' não estiver em dd, a expressão dd['new-key'] segue os seguintes passos:

  1. Chama list() para criar uma nova lista.

  2. Insere a lista em dd usando 'new-key' como chave.

  3. Devolve uma referência para aquela lista.

O chamável que produz os valores default é mantido em um atributo de instância chamado default_factory.

Exemplo 6. index_default.py: usando um defaultdict em vez do método setdefault
link:code/03-dict-set/index_default.py[role=include]
  1. Cria um defaultdict com o construtor de list como default_factory.

  2. Se word não está inicialmente no index, o default_factory é chamado para produzir o valor ausente, que neste caso é uma list vazia, que então é atribuída a index[word] e devolvida, de forma que a operação .append(location) é sempre bem sucedida.

Se nenhum default_factory é fornecido, o KeyError usual é gerado para chaves ausente.

Warning

O default_factory de um defaultdict só é invocado para fornecer valores default para chamadas a __getitem__, não para outros métodos. Por exemplo, se dd é um defaultdict e k uma chave ausente, dd[k] chamará default_factory para criar um valor default, mas dd.get(k) vai devolver None e k in dd é False.

O mecanismo que faz defaultdict funcionar, chamando default_factory, é o método especial __missing__, um recurso discutido a seguir.

O método __missing__

Por trás da forma como os mapeamentos lidam com chaves ausentes está o método muito apropriadamente chamado __missing__.[9]. Esse método não é definido na classe base dict, mas dict está ciente de sua possibilidade: se você criar uma subclasse de dict e incluir um método __missing__, o dict.__getitem__ padrão vai chamar seu método sempre que uma chave não for encontrada, em vez de gerar um KeyError.

Suponha que você queira um mapeamento onde as chaves são convertidas para str quando são procuradas. Um caso de uso concreto seria uma biblioteca para dispositivos IoT (Internet of Things, Internet das Coisas)[10], onde uma placa programável com portas genéricas programáveis (por exemplo, uma Raspberry Pi ou uma Arduino) é representada por uma classe "Placa" com um atributo minha_placa.portas, que é uma mapeamento dos identificadores das portas físicas para objetos de software portas. O identificador da porta física pode ser um número ou uma string como "A0" ou "P9_12". Por consistência, é desejável que todas as chaves em placa.portas seja strings, mas também é conveniente buscar uma porta por número, como em meu-arduino.porta[13], para evitar que iniciantes tropecem quando quiserem fazer piscar o LED na porta 13 de seus Arduinos. O Exemplo 7 mostra como tal mapeamento funcionaria.

Exemplo 7. Ao buscar por uma chave não-string, StrKeyDict0 a converte para str quando ela não é encontrada
link:code/03-dict-set/strkeydict0.py[role=include]

O Exemplo 8 implementa a classe StrKeyDict0, que passa nos doctests acima.

Tip

Uma forma melhor de criar uma mapeamento definido pelo usuário é criar uma subclasse de collections.UserDict em vez de dict (como faremos no Exemplo 9). Aqui criamos uma subclasse de dict apenas para mostrar que __missing__ é suportado pelo método embutido dict.__getitem__.

Exemplo 8. StrKeyDict0 converte chaves não-string para string no momento da consulta (vejas os testes no Exemplo 7)
link:code/03-dict-set/strkeydict0.py[role=include]
  1. StrKeyDict0 herda de dict.

  2. Verifica se key já é uma str. Se é, e está ausente, gera um KeyError.

  3. Cria uma str de key e a procura.

  4. O método get delega para __getitem__ usando a notação self[key]; isso dá oportunidade para nosso __missing__ agir.

  5. Se um KeyError foi gerado, __missing__ já falhou, então devolvemos o default.

  6. Procura pela chave não-modificada (a instância pode conter chaves não-str), depois por uma str criada a partir da chave.

Considere por um momento o motivo do teste isinstance(key, str) ser necessário na implementação de __missing__.

Sem aquele teste, nosso método __missing__ funcionaria bem com qualquer chave kstr ou não—sempre que str(k) produzisse uma chave existente. Mas se str(k) não for uma chave existente, teríamos uma recursão infinita. Na última linha de __missing__, self[str(key)] chamaria __getitem__, passando aquela chave str, e __getitem__, por sua vez, chamaria __missing__ novamente.

O método __contains__ também é necessário para que o comportamento nesse exemplo seja consistente, pois a operação k in d o chama, mas o método herdado de dict não invoca __missing__ com chaves ausentes. Há um detalhe sutil em nossa implementação de __contains__: não verificamos a existência da chave da forma pythônica normal—k in d—porque str(key) in self chamaria __contains__ recursivamente. Evitamos isso procurando a chave explicitamente em self.keys().

Uma busca como k in my_dict.keys() é eficiente em Python 3 mesmo para mapeamentos muito grandes, porque dict.keys() devolve uma view, que é similar a um set, como veremos na Operações de conjuntos em views de dict. Entretanto, lembre-se que k in my_dict faz o mesmo trabalho, e é mais rápido porque evita a busca nos atributos para encontrar o método .keys.

Eu tinha uma razão específica para usar self.keys() no método __contains__ do Exemplo 8. A verificação da chave não-modificada key in self.keys() é necessária por correção, pois StrKeyDict0 não obriga todas as chaves no dicionário a serem do tipo str. Nosso único objetivo com esse exemplo simples foi fazer a busca "mais amigável", e não forçar tipos.

Warning

Classes definidas pelo usuário derivadas de mapeamentos da biblioteca padrão podem ou não usar __missing__ como alternativa em sua implementação de __getitem__, get, ou __contains__, como explicado na próxima seção.

O uso inconsistente de __missing__ na biblioteca padrão

Considere os seguintes cenários, e como eles afetam a busca de chaves ausentes:

subclasse de dict

Uma subclasse de dict que implemente apenas __missing__ e nenhum outro método. Nesse caso, __missing__ pode ser chamado apenas em d[k], que usará o __getitem__ herdado de dict.

subclasse de collections.UserDict

Da mesma forma, uma subclasse de UserDict que implemente apenas __missing__ e nenhum outro método. O método get herdado de UserDict chama __getitem__. Isso significa que __missing__ pode ser chamado para tratar de consultas com d[k] e com d.get(k).

subclasse de abc.Mapping com o __getitem__ mais simples possível

Uma subclasse mínima de abc.Mapping, implementando __missing__ e os métodos abstratos obrigatórios, incluindo uma implementação de __getitem__ que não chama __missing__. O método __missing__ nunca é acionado nessa classe.

subclasse de abc.Mapping com __getitem__ chamando __missing__

Uma subclasse mínima de abc.Mapping, implementando __missing__ e os métodos abstratos obrigatórios, incluindo uma implementação de __getitem__ que chama __missing__. O método __missing__ é acionado nessa classe para consultas por chaves ausentes feitas com d[k], d.get(k), e k in d.

Veja missing.py no repositório de exemplos de código para demonstrações dos cenários descritos acima.

Os quatro cenários que acabo de descrever supõem implementações mínimas. Se a sua subclasse implementa __getitem__, get, e __contains__, então você pode ou não fazer tais métodos usarem __missing__, dependendo de suas necessidades. O ponto aqui é mostrar que é preciso ter cuidado ao criar subclasses dos mapeamentos da biblioteca padrão para usar __missing__, porque as classes base suportam comportamentos default diferentes. Não se esqueça que o comportamento de setdefault e update também é afetado pela consulta de chaves. E por fim, dependendo da lógica de seu __missing__, pode ser necessário implementar uma lógica especial em __setitem__, para evitar inconsistências ou comportamentos surpreeendentes. Veremos um exemplo disso na Criando subclasses de UserDict em vez de dict.

Até aqui tratamos dos tipos de mapeamentos dict e defaultdict, mas a biblioteca padrão traz outras implementações de mapeamentos, que discutiremos a seguir.

Variações de dict

Nessa seção falaremos brevemente sobre os tipos de mapeamentos incluídos na biblioteca padrão diferentes de defaultdict, já visto na defaultdict: outra perspectiva sobre as chaves ausentes.

collections.OrderedDict

Agora que o dict embutido também mantém as chaves ordenadas (desde o Python 3.6), o motivo mais comum para usar OrderedDict é escrever código compatível com versões anteriores do Python. Dito isso, a documentação lista algumas diferenças entre dict e OrderedDict que ainda persistem e que cito aqui—apenas reordenando os itens conforme sua relevância no uso diário:

  • A operação de igualdade para OrderedDict verifica a igualdade da ordenação.

  • O método popitem() de OrderedDict tem uma assinatura diferente, que aceita um argumento opcional especificando qual item será devolvido.

  • OrderedDict tem um método move_to_end(), que reposiciona de um elemento para uma ponta do dicionário de forma eficiente.

  • O dict comum foi projetado para ser muito bom nas operações de mapeamento. Monitorar a ordem de inserção era uma preocupação secundária.

  • OrderedDict foi projetado para ser bom em operações de reordenamento. Eficiência espacial, velocidade de iteração e o desempenho de operações de atualização eram preocupações secundárias.

  • Em termos do algoritmo, um OrderedDict lida melhor que um dict com operações frequentes de reordenamento. Isso o torna adequado para monitorar acessos recentes (em um cache LRU[11], por exemplo).

collections.ChainMap

Uma instância de ChainMap mantém uma lista de mapeamentos que podem ser consultados como se fossem um mapeamento único. A busca é realizada em cada mapa incluído, na ordem em que eles aparecem na chamada ao construtor, e é bem sucedida assim que a chave é encontrada em um daqueles mapeamentos. Por exemplo:

>>> d1 = dict(a=1, b=3)
>>> d2 = dict(a=2, b=4, c=6)
>>> from collections import ChainMap
>>> chain = ChainMap(d1, d2)
>>> chain['a']
1
>>> chain['c']
6

A instância de ChainMap não cria cópias dos mapeamentos, mantém referências para eles. Atualizações ou inserções a um ChainMap afetam apenas o primeiro mapeamento passado. Continuando do exemplo anterior:

>>> chain['c'] = -1
>>> d1
{'a': 1, 'b': 3, 'c': -1}
>>> d2
{'a': 2, 'b': 4, 'c': 6}

Um ChainMap é útil na implementação de linguagens com escopos aninhados, onde cada mapeamento representa um contexto de escopo, desde o escopo aninhado mais interno até o mais externo. A seção "Objetos ChainMap", na documentação de collections, apresenta vários exemplos do uso de Chainmap, incluindo esse trecho inspirado nas regras básicas de consulta de variáveis no Python:

import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))

O [environment_class_ex] mostra uma subclasse de ChainMap usada para implementar um interpretador parcial da linguagem de programação Scheme.

collections.Counter

Um mapeamento que mantém uma contagem inteira para cada chave. Atualizar uma chave existente adiciona à sua contagem. Isso pode ser usado para contar instâncias de objetos hashable ou como um multiset ("conjunto múltiplo"), discutido adiante nessa seção. Counter implementa os operadores + e - para combinar contagens, e outros métodos úteis tal como o most_common([n]), que devolve uma lista ordenada de tuplas com os n itens mais comuns e suas contagens; veja a documentação.

Aqui temos um Counter usado para contar as letras em palavras:

>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.update('aaaaazzz')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>>> ct.most_common(3)
[('a', 10), ('z', 3), ('b', 2)]

Observe que as chaves 'b' e 'r' estão empatadas em terceiro lugar, mas ct.most_common(3) mostra apenas três contagens.

Para usar collections.Counter como um conjunto múltiplo, trate cada chave como um elemento de um conjunto, e a contagem será o número de ocorrências daquele elemento no conjunto.

shelve.Shelf

O módulo shelve na biblioteca padrão fornece armazenamento persistente a um mapeamento de chaves em formato string para objetos Python serializados no formato binário pickle. O nome curioso, shelve, faz sentido quando você percebe que potes de pickle são armazenadas em prateleiras.[12]

A função de módulo shelve.open devolve uma instância de shelve.Shelf—um banco de dados DBM simples de chave-valor, baseado no módulo dbm, com as seguintes características:

  • shelve.Shelf é uma subclasse de abc.MutableMapping, então fornece os métodos essenciais esperados de um tipo mapeamento.

  • Além disso, shelve.Shelf fornece alguns outros métodos de gerenciamento de E/S, como sync e close.

  • Uma instância de Shelf é um gerenciador de contexto, então é possível usar um bloco with para garantir que ela seja fechada após o uso.

  • Chaves e valores são salvos sempre que um novo valor é atribuído a uma chave.

  • As chaves devem ser strings.

  • Os valores devem ser objetos que o módulo pickle possa serializar.

A documentação para os módulos shelve, dbm (EN), e pickle traz mais detalhes e também algumas ressalvas.

Warning

O pickle do Python é fácil de usar nos caso mais simples, mas tem vários inconvenientes. Leia o "Pickle’s nine flaws", de Ned Batchelder, antes de adotar qualquer solução envolvendo pickle. Em seu post, Ned menciona outros formatos de serialização que podem ser considerados como alternativas.

As classes OrderedDict, ChainMap, Counter, e Shelf podem ser usadas diretamente, mas também podem ser personalizadas por subclasses. UserDict, por outro lado, foi planejada apenas como uma classe base a ser estendida.

Criando subclasses de UserDict em vez de dict

É melhor criar um novo tipo de mapeamento estendendo collections.UserDict em vez de dict. Percebemos isso quando tentamos estender nosso StrKeyDict0 do Exemplo 8 para assegurar que qualquer chave adicionada ao mapeamento seja armazenada como str.

A principal razão pela qual é melhor criar uma subclasse de UserDict em vez de dict é que o tipo embutido tem alguns atalhos de implementação, que acabam nos obrigando a sobrepor métodos que poderíamos apenas herdar de UserDict sem maiores problemas.[13]

Observe que UserDict não herda de dict, mas usa uma composição: a classe tem uma instância interna de dict, chamada data, que mantém os itens propriamente ditos. Isso evita recursão indesejada quando escrevemos métodos especiais, como __setitem__, e simplifica a programação de __contains__, quando comparado com o Exemplo 8.

Graças a UserDict, o StrKeyDict (Exemplo 9) é mais conciso que o StrKeyDict0 (Exemplo 8), mais ainda faz melhor: ele armazena todas as chaves como str, evitando surpresas desagradáveis se a instância for criada ou atualizada com dados contendo chaves de outros tipos (que não string).

Exemplo 9. StrKeyDict sempre converte chaves que não sejam strings para str na inserção, atualização e busca
link:code/03-dict-set/strkeydict.py[role=include]
  1. StrKeyDict estende UserDict.

  2. __missing__ é exatamente igual ao do Exemplo 8.

  3. __contains__ é mais simples: podemos assumir que todas as chaves armazenadas são str, e podemos operar sobre self.data em vez de invocar self.keys(), como fizemos em StrKeyDict0.

  4. __setitem__ converte qualquer key para uma str. Esse método é mais fácil de sobrepor quando podemos delegar para o atributo self.data.

Como UserDict estende abc.MutableMapping, o restante dos métodos que fazem de StrKeyDict uma mapeamento completo são herdados de UserDict, MutableMapping, ou Mapping. Estes últimos contém vários métodos concretos úteis, apesar de serem classes base abstratas (ABCs). Os seguinte métodos são dignos de nota:

MutableMapping.update

Esse método poderoso pode ser chamado diretamente, mas também é usado por __init__ para criar a instância a partir de outros mapeamentos, de iteráveis de pares (chave, valor), e de argumentos nomeados. Como usa self[chave] = valor para adicionar itens, ele termina por invocar nossa implementação de __setitem__.

Mapping.get

No StrKeyDict0 (Exemplo 8), precisamos codificar nosso próprio get para devolver os mesmos resultados de __getitem__, mas no Exemplo 9 herdamos Mapping.get, que é implementado exatamente como StrKeyDict0.get (consulte o código-fonte do Python).

Tip

Antoine Pitrou escreveu a PEP 455—​Adding a key-transforming dictionary to collections (Acrescentando um dicionário com transformação de chaves a collections) (EN) e um patch para aperfeiçoar o módulo collections com uma classe TransformDict, que é mais genérico que StrKeyDict e preserva as chaves como fornecidas antes de aplicar a transformação. A PEP 455 foi rejeitada em maio de 2015—veja a mensagem de rejeição (EN) de Raymond Hettinger. Para experimentar com a TransformDict, extraí o patch de Pitrou do issue18986 (EN) para um módulo independente (03-dict-set/transformdict.py disponível no repositório de código da segunda edição do Fluent Python).

Sabemos que existem tipos de sequências imutáveis, mas e mapeamentos imutáveis? Bem, não há um tipo real desses na biblioteca padrão, mas um substituto está disponível. É o que vem a seguir.

Mapeamentos imutáveis

Os tipos de mapeamentos disponíveis na biblioteca padrão são todos mutáveis, mas pode ser necessário impedir que os usuários mudem um mapeamento por acidente. Um caso de uso concreto pode ser encontrado, novamente, em uma biblioteca de programação de hardware como a Pingo, mencionada na O método __missing__: o mapeamento board.pins representa as portas de GPIO (General Purpose Input/Output, Entrada/Saída Genérica) em um dispositivo. Dessa forma, seria útil evitar atualizações descuidadas de board.pins, pois o hardware não pode ser modificado via software: qualquer mudança no mapeamento o tornaria inconsistente com a realidade física do dispositivo.

O módulo types oferece uma classe invólucro (wrapper) chamada MappingProxyType que, dado um mapeamento, devolve uma instância de mappingproxy, que é um proxy somente para leitura (mas dinâmico) do mapeamento original. Isso significa que atualizações ao mapeamento original são refletidas no mappingproxy, mas nenhuma mudança pode ser feita através desse último. Veja uma breve demonstração no Exemplo 10.

Exemplo 10. MappingProxyType cria uma instância somente de leitura de mappingproxy a partir de um dict
>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1]  (1)
'A'
>>> d_proxy[2] = 'x'  (2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d[2] = 'B'
>>> d_proxy  (3)
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>
  1. Os items em d podem ser vistos através de d_proxy.

  2. Não é possível fazer modificações através de d_proxy.

  3. d_proxy é dinâmica: qualquer mudança em d é refletida ali.

Isso pode ser usado assim na prática, no cenário da programação de hardware: o construtor em uma subclasse concreta Board preencheria um mapeamento privado com os objetos porta, e o exporia aos clientes da API via um atributo público .portas, implementado como um mappingproxy. Dessa forma os clientes não poderiam acrescentar, remover ou modificar as portas por acidente.

A seguir veremos views—que permitem operações de alto desempenho em um dict, sem cópias desnecessárias dos dados.

Views de dicionários

Os métodos de instância de dict .keys(), .values(), e .items() devolvem instâncias de classes chamadas dict_keys, dict_values, e dict_items, respectivamente. Essas views de dicionário são projeções somente para leitura de estruturas de dados internas usadas na implemetação de dict. Elas evitam o uso de memória adicional dos métodos equivalentes no Python 2, que devolviam listas, duplicando dados já presentes no dict alvo. E também substituem os métodos antigos que devolviam iteradores.

O Exemplo 11 mostra algumas operações básicas suportadas por todas as views de dicionários.

Exemplo 11. O método .values() devolve uma view dos valores em um dict
>>> d = dict(a=10, b=20, c=30)
>>> values = d.values()
>>> values
dict_values([10, 20, 30])  (1)
>>> len(values)  (2)
3
>>> list(values)  (3)
[10, 20, 30]
>>> reversed(values)  (4)
<dict_reversevalueiterator object at 0x10e9e7310>
>>> values[0] (5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict_values' object is not subscriptable
  1. O repr de um objeto view mostra seu conteúdo.

  2. Podemos consultar a len de uma view.

  3. Views são iteráveis, então é fácil criar listas a partir delas.

  4. Views implementam __reversed__, devolvendo um iterador personalizado.

  5. Não é possível usar [] para obter itens individuais de uma view.

Um objeto view é um proxy dinâmico. Se o dict fonte é atualizado, as mudanças podem ser vistas imediatamente através de uma view existente. Continuando do Exemplo 11:

>>> d['z'] = 99
>>> d
{'a': 10, 'b': 20, 'c': 30, 'z': 99}
>>> values
dict_values([10, 20, 30, 99])

As classes dict_keys, dict_values, e dict_items são internas: elas não estão disponíveis via __builtins__ ou qualquer módulo da biblioteca padrão, e mesmo que você obtenha uma referência para uma delas, não pode usar essa referência para criar uma view do zero no seu código Python:

>>> values_class = type({}.values())
>>> v = values_class()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot create 'dict_values' instances

A classe dict_values é a view de dicionário mais simples—ela implementa apenas os métodos especiais __len__, __iter__, e __reversed__. Além desses métodos, dict_keys e dict_items implementam vários métodos dos sets, quase tantos quanto a classe frozenset. Após vermos os conjuntos (sets), teremos mais a dizer sobre dict_keys e dict_items, na Operações de conjuntos em views de dict.

Agora vamos ver algumas regras e dicas baseadas na forma como dict é implementado debaixo dos panos.

Consequências práticas da forma como dict funciona

A implementação da tabela de hash do dict do Python é muito eficiente, mas é importante entender os efeitos práticos desse design:

  • Chaves devem ser objetos hashable. Eles devem implementar métodos __hash__ e __eq__ apropriados, como descrito na O que é hashable?.

  • O acesso aos itens através da chave é muito rápido. Mesmo que um dict tenha milhões de chaves, o Python pode localizar uma chave diretamente, computando o código hash da chave e derivando um deslocamento do índice na tabela de hash, com um possível ônus de um pequeno número de tentativas até encontrar a entrada correspondente.

  • A ordenação das chaves é preservada, como efeito colateral de um layout de memória mais compacto para dict no CPython 3.6, que se tornou um recurso oficial da linguagem no 3.7.

  • Apesar de seu novo layout compacto, os dicts apresentam, inevitavelmente, um uso adicional significativo de memória. A estrutura de dados interna mais compacta para um contêiner seria um array de ponteiros para os itens.[14] Comparado a isso, uma tabela de hash precisa armazenar mais dados para cada entrada e, para manter a eficiência, o Python precisa manter pelo menos um terço das linhas da tabela de hash vazias.

  • Para economizar memória, evite criar atributos de instância fora do método __init__.

Essa última dica, sobre atributos de instância, é consequência do comportamento default do Python, de armazenar atributos de instância em um atributo __dict__ especial, que é um dict vinculado a cada instância.[15] Desde a implementação da PEP 412—Key-Sharing Dictionary (Dicionário de Compartilhamento de Chaves) (EN), no Python 3.3, instâncias de uma classe podem compartilhar uma tabela de hash comum, armazenada com a classe. Essa tabela de hash comum é compartilhada pelo __dict__ de cada nova instância que, quando __init__ retorna, tenha os mesmos nomes de atributos que a primeira instância daquela classe a ser criada. O __dict__ de cada instância então pode manter apenas seus próprios valores de atributos como uma simples array de ponteiros. Acrescentar um atributo de instância após o __init__ obriga o Python a criar uma nova tabela de hash só para o __dict__ daquela instância (que era o comportamento default antes do Python 3.3). De acordo com a PEP 412, essa otimização reduz o uso da memória entre 10% e 20% em programas orientados as objetos. Os detalhes das otimizações do layout compacto e do compartilhamento de chaves são bastante complexos. Para saber mais, por favor leio o texto "Internals of sets and dicts" (EN) em fluentpython.com.

Agora vamos estudar conjuntos(sets).

Teoria dos conjuntos

Conjuntos não são novidade no Python, mais ainda são um tanto subutilizados. O tipo set e seu irmão imutável, frozenset, surgiram inicialmente como módulos na biblioteca padrão do Python 2.3, e foram promovidos a tipos embutidos no Python 2.6.

Note

Nesse livro, uso a palavra "conjunto" para me referir tanto a set quanto a frozenset.

Um conjunto é uma coleção de objetos únicos. Uma grande utilidade dos conjuntos é descartar itens duplicados:

>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
>>> set(l)
{'eggs', 'spam', 'bacon'}
>>> list(set(l))
['eggs', 'spam', 'bacon']
Tip

Para remover elementos duplicados preservando a ordem da primeira ocorrência de cada item, você pode fazer isso com um dict simples, assim:

>>> dict.fromkeys(l).keys()
dict_keys(['spam', 'eggs', 'bacon'])
>>> list(dict.fromkeys(l).keys())
['spam', 'eggs', 'bacon']

Elementos de um conjunto devem ser hashable. O tipo set não é hashable, então não é possível criar um set com instâncias aninhadas de set. Mas frozenset é hashable, então você pode ter instâncias de frozenset dentro de um set.

Além de impor a unicidade de cada elemento, os tipos conjunto implementam muitas operações entre conjuntos como operadores infixos. Assim, dados dois conjuntos a e b, a | b devolve sua união, a & b calcula a intersecção, a - b a diferença, e a ^ b a diferença simétrica. Quando bem utilizadas, as operações de conjuntos podem reduzir tanto a contagem de linhas quanto o tempo de execução de programas Python, ao mesmo tempo em que tornam o código mais legível e mais fácil de entender—pela remoção de loops e lógica condicional.

Por exemplo, imagine que você tem um grande conjunto de endereços de email (o "palheiro"—haystack) e um conjunto menor de endereços (as "agulhas"—needles`), e precisa contar quantas agulhas existem no palheiro. Graças à interseção de set (o operador &), é possível codar isso em uma expressão simples (veja o Exemplo 12).

Exemplo 12. Conta as ocorrências de agulhas (needles) em um palheiro (haystack), ambos do tipo set
found = len(needles & haystack)

Sem o operador de intersecção, seria necessário escrever o Exemplo 13 para realizar a mesma tarefa executa pelo Exemplo 12.

Exemplo 13. Conta as ocorrências de agulhas (needles) em um palheiro (haystack); mesmo resultado final do Exemplo 12
found = 0
for n in needles:
    if n in haystack:
        found += 1

O Exemplo 12 é um pouco mais rápido que o Exemplo 13. Por outro lado, o Exemplo 13 funciona para quaisquer objetos iteráveis needles e haystack, enquanto o Exemplo 12 exige que ambos sejam conjuntos. Mas se você não tem conjuntos à mão, pode sempre criá-los na hora, como mostra o Exemplo 14.

Exemplo 14. Conta as ocorrências de agulhas (needles) em um palheiro (haystack); essas linhas funcionam para qualquer tipo iterável
found = len(set(needles) & set(haystack))

# another way:
found = len(set(needles).intersection(haystack))

Claro, há o custo extra envolvido na criação dos conjuntos no Exemplo 14, mas se ou as needles ou o haystack já forem um set, a alternativa no Exemplo 14 pode ser mais barata que o Exemplo 13.

Qualquer dos exemplos acima é capaz de buscar 1000 elementos em um haystack de 10 milhões de itens em cerca de 0,3 milisegundos—isso é cerca de 0,3 microsegundos por elemento.

Além do teste de existência extremamente rápido (graças à tabela de hash subjacente), os tipos embutidos set e frozenset oferecem uma rica API para criar novos conjuntos ou, no caso de set, para modificar conjuntos existentes. Vamos discutir essas operações em breve, após uma observação sobre sintaxe.

Sets literais

A sintaxe de literais set{1}, {1, 2}, etc.—parece muito com a notação matemática, mas tem uma importante exceção: não há notação literal para o set vazio, então precisamos nos lembrar de escrever set().

Warning
Peculiaridade sintática

Para criar um set vazio, usamos o construtor sem argumentos: set(). Se você escrever {}, vai criar um dict vazio—isso não mudou no Python 3.

No Python 3, a representação padrão dos sets como strings sempre usa a notação {…}, exceto para o conjunto vazio:

>>> s = {1}
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s.pop()
1
>>> s
set()

A sintaxe do set literal, como {1, 2, 3}, é mais rápida e mais legível que uma chamada ao construtor (por exemplo, set([1, 2, 3])). Essa última forma é mais lenta porque, para avaliá-la, o Python precisa buscar o nome set para obter seu construtor, daí criar uma lista e, finalmente, passá-la para o construtor. Por outro lado, para processar um literal como {1, 2, 3}, o Python roda um bytecode especializado, BUILD_SET.[16]

Não há sintaxe especial para representar literais frozenset—eles só podem ser criados chamando seu construtor. Sua representação padrão como string no Python 3 se parece com uma chamada ao construtor de frozenset com um argumento set`. Observe a saída na sessão de console a seguir:

>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

E por falar em sintaxe, a ideia das listcomps foi adaptada para criar conjuntos também.

Compreensões de conjuntos

Compreensões de conjuntos (setcomps) apareceram há bastante tempo, no Python 2.7, junto com as dictcomps que vimos na Compreensões de dict. O Exemplo 15 mostra procedimento.

Exemplo 15. Cria um conjunto de caracteres Latin-1 que tenham a palavra "SIGN" em seus nomes Unicode
>>> from unicodedata import name  (1)
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')}  (2)
{'§', '=', '¢', '#', '¤', '<', '¥', 'µ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}
  1. Importa a função name de unicodedata para obter os nomes dos caracteres.

  2. Cria um conjunto de caracteres com códigos entre 32 e 255 que contenham a palavra 'SIGN' em seus nomes.

A ordem da saída muda a cada processo Python, devido ao hash "salgado", mencionado na O que é hashable?.

Questões de sintaxe à parte, vamos considerar agora o comportamento dos conjuntos.

Consequências práticas da forma de funcionamento dos conjuntos

Os tipos set e frozenset são ambos implementados com um tabela de hash. Isso tem os seguintes efeitos:

  • Elementos de conjuntos tem que ser objetos hashable. Eles precisam implementar métodos __hash__ e __eq__ adequados, como descrido na O que é hashable?.

  • O teste de existência de um elemento é muito eficiente. Um conjunto pode ter milhões de elementos, mas um elemento pode ser localizado diretamente, computando o código hash da chave e derivando um deslocamento do índice, com o possível ônus de um pequeno número de tentativas até encontrar a entrada correspondente ou exaurir a busca.

  • Conjuntos usam mais memória se comparados aos simples ponteiros de um array para seus elementos—que é uma estrutura mais compacta, mas também muito mais lenta para buscas se seu tamanho cresce além de uns poucos elementos.

  • A ordem dos elementos depende da ordem de inserção, mas não de forma útil ou confiável. Se dois elementos são diferentes mas tem o mesmo código hash, sua posição depende de qual elemento foi inserido primeiro.

  • Acrescentar elementos a um conjunto muda a ordem dos elementos existentes. Isso ocorre porque o algoritmo se torna menos eficiente se a tabela de hash estiver com mais de dois terços de ocupação, então o Python pode ter que mover e redimensionar a tabela conforme ela cresce. Quando isso acontece, os elementos são reinseridos e sua ordem relativa pode mudar.

Veja o post "Internals of sets and dicts" (EN) no fluentpython.com para maiores detalhes.

Agora vamos revisar a vasta seleção de operações oferecidas pelos conjuntos.

Operações de conjuntos

A Figura 2 dá uma visão geral dos métodos disponíveis em conjuntos mutáveis e imutáveis. Muitos deles são métodos especiais que sobrecarregam operadores, tais como & and >=. A Tabela 2 mostra os operadores matemáticos de conjuntos que tem operadores ou métodos correspondentes no Python. Note que alguns operadores e métodos realizam mudanças no mesmo lugar sobre o conjunto alvo (por exemplo, &=, difference_update, etc.). Tais operações não fazem sentido no mundo ideal dos conjuntos matemáticos, e também não são implementadas em frozenset.

Tip

Os operadores infixos na Tabela 2 exigem que os dois operandos sejam conjuntos, mas todos os outros métodos recebem um ou mais argumentos iteráveis. Por exemplo, para produzir a união de quatro coleções, a, b, c, e d, você pode chamar a.union(b, c, d), onde a precisa ser um set, mas b, c, e d podem ser iteráveis de qualquer tipo que produza itens hashable. Para criar um novo conjunto com a união de quatro iteráveis, desde o Python 3.5 você pode escrever {*a, *b, *c, *d} ao invés de atualizar um conjunto existente, graças à PEP 448—Additional Unpacking Generalizations (Generalizações de Desempacotamento Adicionais).

Diagrama de classe UML para `Set` e `MutableSet`
Figura 2. Diagrama de classes UML simplificado para MutableSet e suas superclasses em collections.abc (nomes em itálico são classes e métodos abstratos; métodos de operadores reversos foram omitidos por concisão).
Tabela 2. Operações matemáticas com conjuntos: esses métodos ou produzem um novo conjunto ou atualizam o conjunto alvo no mesmo lugar, se ele for mutável
Math symbol Python operator Method Description

S ∩ Z

s & z

s.__and__(z)

Intersecção de s e z

z & s

s.__rand__(z)

Operador & invertido

s.intersection(it, …)

Intersecção de s e todos os conjuntos construídos a partir de iteráveis it, etc.

s &= z

s.__iand__(z)

s atualizado com a intersecção de s e z

s.intersection_update(it, …)

s atualizado com a intersecção de s e todos os conjuntos construídos a partir de iteráveis it, etc.

S ∪ Z

s | z

s.__or__(z)

União de s e z

z | s

s.__ror__(z)

| invertido

s.union(it, …)

União de s e todos os conjuntos construídos a partir de iteráveis it, etc.

s |= z

s.__ior__(z)

s atualizado com a união de s e z

s.update(it, …)

s atualizado com a união de s e todos os conjuntos construídos a partir de iteráveis it, etc.

S \ Z

s - z

s.__sub__(z)

Complemento relativo ou diferença entre s e z

z - s

s.__rsub__(z)

Operador - invertido

s.difference(it, …)

Diferença entre s e todos os conjuntos construídos a partir de iteráveis it, etc.

s -= z

s.__isub__(z)

s atualizado com a diferença entre s e z

s.difference_update(it, …)

s atualizado com a diferença entre s e todos os conjuntos construídos a partir de iteráveis it, etc.

S ∆ Z

s ^ z

s.__xor__(z)

Diferença simétrica (o complemento da intersecção s & z)

z ^ s

s.__rxor__(z)

Operador ^ invertido

s.symmetric_difference(it)

Complemento de s & set(it)

s ^= z

s.__ixor__(z)

s atualizado com a diferença simétrica de s e z

s.symmetric_difference_update(it, …)

s atualizado com a diferença simétrica de s e todos os conjuntos construídos a partir de iteráveis it, etc.

A Tabela 3 lista predicados de conjuntos: operadores e métodos que devolvem True ou False.

Tabela 3. Operadores e métodos de comparação de conjuntos que devolvem um booleano
Math symbol Python operator Method Description

S ∩ Z = ∅

s.isdisjoint(z)

s e z são disjuntos (não tem elementos em comum)

e ∈ S

e in s

s.__contains__(e)

Elemento e é membro de s

S ⊆ Z

s <= z

s.__le__(z)

s é um subconjunto do conjunto z

s.issubset(it)

s é um subconjunto do conjunto criado a partir do iterável it

S ⊂ Z

s < z

s.__lt__(z)

s é um subconjunto próprio[17] do conjunto z

S ⊇ Z

s >= z

s.__ge__(z)

s é um superconjunto do conjunto z

s.issuperset(it)

s é um superconjunto do conjunto criado a partir do iterável it

S ⊃ Z

s > z

s.__gt__(z)

s é um superconjunto próprio do conjunto z

Além de operadores e métodos derivados da teoria matemática dos conjuntos, os tipos conjunto implementam outros métodos para tornar seu uso prático, resumidos na Tabela 4.

Tabela 4. Métodos adicionais de conjuntos
set frozenset  

s.add(e)

Adiciona elemento e a s

s.clear()

Remove todos os elementos de s

s.copy()

Cópia rasa de s

s.discard(e)

Remove elemento e de s, se existir

s.__iter__()

Obtém iterador de s

s.__len__()

len(s)

s.pop()

Remove e devolve um elemento de s, gerando um KeyError se s estiver vazio

s.remove(e)

Remove elemento e de s, gerando um KeyError se e não existir em s

Isso encerra nossa visão geral dos recursos dos conjuntos. Como prometido na Views de dicionários, vamos agora ver como dois dos tipos de views de dicionários se comportam de forma muito similar a um frozenset.

Operações de conjuntos em views de dict

A Tabela 5 mostra como os objetos view devolvidos pelos métodos .keys() e .items() de dict são notavelmente similares a um frozenset.

Tabela 5. Métodos implementados por frozenset, dict_keys, e dict_items
frozenset dict_keys dict_items Description

s.__and__(z)

s & z (interseção de s e z)

s.__rand__(z)

operador & invertido

s.__contains__()

e in s

s.copy()

Cópia rasa de s

s.difference(it, …)

Diferença entre s e os iteráveis it, etc.

s.intersection(it, …)

Intersecção de s e dos iteráveis it, etc.

s.isdisjoint(z)

s e z são disjuntos (não tem elementos em comum)

s.issubset(it)

s é um subconjunto do iterável it

s.issuperset(it)

s é um superconjunto do iterável it

s.__iter__()

obtém iterador para s

s.__len__()

len(s)

s.__or__(z)

s | z (união de s e z)

s.__ror__()

Operador | invertido

s.__reversed__()

Obtém iterador para s com a ordem invertida

s.__rsub__(z)

Operador - invertido

s.__sub__(z)

s - z (diferença entre s e z)

s.symmetric_difference(it)

Complemento de s & set(it)

s.union(it, …)

União de s e dos iteráveis`it`, etc.

s.__xor__()

s ^ z (diferença simétrica de s e z)

s.__rxor__()

Operador ^ invertido

Especificamente, dict_keys e dict_items implementam os métodos especiais para suportar as poderosas operações de conjuntos & (intersecção), | (união), - (diferença), and ^ (diferença simétrica).

Por exemplo, usando & é fácil obter as chaves que aparecem em dois dicionários:

>>> d1 = dict(a=1, b=2, c=3, d=4)
>>> d2 = dict(b=20, d=40, e=50)
>>> d1.keys() & d2.keys()
{'b', 'd'}

Observe que o valor devolvido por & é um set. Melhor ainda: os operadores de conjuntos em views de dicionários são compatíveis com instâncias de set. Veja isso:

>>> s = {'a', 'e', 'i'}
>>> d1.keys() & s
{'a'}
>>> d1.keys() | s
{'a', 'c', 'b', 'd', 'i', 'e'}
Warning

Uma view obtida de dict_items só funciona como um conjunto se todos os valores naquele dict são hashable. Tentar executar operações de conjuntos sobre uma view devolvida por dict_items que contenha valores não-hashable gera um TypeError: unhashable type 'T', sendo T o tipo do valor incorreto.

Por outro lado, uma view devolvida por dict_keys sempre pode ser usada como um conjunto, pois todas as chaves são hashable—por definição.

Usar operações de conjunto com views pode evitar a necessidade de muitos loops e ifs quando seu código precisa inspecionar o conteúdo de dicionários. Deixe a eficiente implementação do Python em C trabalhar para você!

Com isso, encerramos esse capítulo.

Resumo do capítulo

Dicionários são a pedra fundamental do Python. Ao longo dos anos, a sintaxe literal familiar, {k1: v1, k2: v2}, foi aperfeiçoada para suportar desempacotamento com ** e pattern matching, bem como com compreensões de dict.

Além do dict básico, a biblioteca padrão oferece mapeamentos práticos prontos para serem usados, como o defaultdict, o ChainMap, e o Counter, todos definidos no módulo collections. Com a nova implementação de dict, o OrderedDict não é mais tão útil quanto antes, mas deve permanecer na biblioteca padrão para manter a compatibilidade retroativa—e por suas características específicas ausentes em dict, tal como a capacidade de levar em consideração o ordenamento das chaves em uma comparação ==. Também no módulo collections está o UserDict, uma classe base fácil de usar na criação de mapeamentos personalizados.

Dois métodos poderosos disponíveis na maioria dos mapeamentos são setdefault e update. O método setdefault pode atualizar itens que mantenham valores mutáveis—por exemplo, em um dict de valores list—evitando uma segunda busca pela mesma chave. O método update permite inserir ou sobrescrever itens em massa a partir de qualquer outro mapeamento, desde iteráveis que forneçam pares (chave, valor) até argumentos nomeados. Os construtores de mapeamentos também usam update internamente, permitindo que instâncias sejam inicializadas a partir de outros mapeamentos, de iteráveis e de argumentos nomeados. Desde o Python 3.9 também podemos usar o operador |= para atualizar uma mapeamento e o operador | para criar um novo mapeamento a partir a união de dois mapeamentos.

Um gancho elegante na API de mapeamento é o método __missing__, que permite personalizar o que acontece quando uma chave não é encontrada ao se usar a sintaxe d[k] syntax, que invoca __getitem__.

O módulo collections.abc oferece as classes base abstratas Mapping e MutableMapping como interfaces padrão, muito úteis para checagem de tipo durante a execução. O MappingProxyType, do módulo types, cria uma fachada imutável para um mapeamento que você precise proteger de modificações acidentais. Existem também ABCs para Set e MutableSet.

Views de dicionários foram uma grande novidade no Python 3, eliminando o uso desnecessário de memória dos métodos .keys(), .values(), e .items() do Python 2, que criavam listas duplicando os dados na instância alvo de dict. Além disso, as classes dict_keys e dict_items suportam os operadores e métodos mais úteis de frozenset.

Leitura complementar

Na documentação da Biblioteca Padrão do Python, a seção "collections—Tipos de dados de contêineres" inclui exemplos e receitas práticas para vários tipos de mapeamentos. O código-fonte do Python para o módulo, Lib/collections/__init__.py, é uma excelente referência para qualquer um que deseje criar novos tipos de mapeamentos ou entender a lógica dos tipos existentes. O capítulo 1 do Python Cookbook, 3rd ed. (O’Reilly), de David Beazley e Brian K. Jones traz 20 receitas práticas e perpicazes usando estruturas de dados—a maioria mostrando formas inteligentes de usar dict.

Greg Gandenberger defende a continuidade do uso de collections.OrderedDict, com os argumentos de que "explícito é melhor que implícito," compatibilidade retroativa, e o fato de algumas ferramentas e bibliotecas presumirem que a ordenação das chaves de um dict é irrelevante—nesse post: "Python Dictionaries Are Now Ordered. Keep Using OrderedDict" (Os dicionários do Python agora são ordenados. Continue a usar OrderedDict) (EN).

A PEP 3106—​Revamping dict.keys(), .values() and .items() (Renovando dict.keys(), .values() e .items()) (EN) foi onde Guido van Rossum apresentou o recurso de views de dicionário para o Python 3. No resumo, ele afirma que a ideia veio da Java Collections Framework.

O PyPy foi o primeiro interpretador Python a implementar a proposta de Raymond Hettinger de dicts compactos, e eles escreverem em seu blog sobre isso, em "Faster, more memory efficient and more ordered dictionaries on PyPy" (Dicionários mais rápidos, mais eficientes em termos de memória e mais ordenados no PyPy) (EN), reconhecendo que um layout similar foi adotado no PHP 7, como descrito em PHP’s new hashtable implementation (A nova implementação de tabelas de hash do PHP) (EN). É sempre muito bom quando criadores citam trabalhos anteriores de outros.

Na PyCon 2017, Brandon Rhodes apresentou "The Dictionary Even Mightier" (O dicionário, ainda mais poderoso) (EN), uma continuação de sua apresentação animada clássica "The Mighty Dictionary" (O poderoso dicionário) (EN)—incluindo colisões de hash animadas! Outro vídeo atual mas mais aprofundado sobre o funcionamento interno do dict do Python é "Modern Dictionaries" (Dicionários modernos) (EN) de Raymond Hettinger, onde ele nos diz que após inicialmente fracassar em convencer os desenvolvedores principais do Python sobre os dicts compactos, ele persuadiu a equipe do PyPy, eles os adotaram, a ideia ganhou força, e finalmente foi adicionada ao CPython 3.6 por INADA Naoki. Para saber todos os detalhes, dê uma olhada nos extensos comentários no código-fonte do CPython para Objects/dictobject.c (EN) e no documento de design em Objects/dictnotes.txt (EN).

A justificativa para a adição de conjuntos ao Python está documentada na PEP 218—​Adding a Built-In Set Object Type (Adicionando um objeto embutido de tipo conjunto). Quando a PEP 218 foi aprovada, nenhuma sintaxe literal especial foi adotada para conjuntos. Os literais set foram criados para o Python 3 e implementados retroativamente no Python 2.7, assim como as compreensões de dict e set. Na PyCon 2019, eu apresentei "Set Practice: learning from Python’s set types" (A Prática dos Conjuntos: aprendendo com os tipos conjunto do Python) (EN), descrevendo casos de uso de conjuntos em programas reais, falando sobre o design de sua API, e sobre a implementação da uintset, uma classe de conjunto para elementos inteiros, usando um vetor de bits ao invés de uma tabela de hash, inspirada por um exemplo do capítulo 6 do excelente The Go Programming Language (A Linguagem de Programação Go) (EN), de Alan Donovan e Brian Kernighan (Addison-Wesley).

A revista Spectrum, do IEEE, tem um artigo sobre Hans Peter Luhn, um prolífico inventor, que patenteou um conjunto de cartões interligados que permitiam selecionar receitas de coquetéis a partir dos ingredientes disponíveis, entre inúmeras outras invenções, incluindo…​ tabelas de hash! Veja "Hans Peter Luhn and the Birth of the Hashing Algorithm" (Hans Peter Luhn e o Nascimento do Algoritmo de Hash).

Ponto de Vista

Açúcar sintático

Meu amigo Geraldo Cohen certa vez observou que o Python é "simples e correto."

Puristas de linguagens de programação gostam de desprezar a sintaxe como algo desimportante.

Syntactic sugar causes cancer of the semicolon.[18]

— Alan Perlis

A sintaxe é a interface de usuário de uma linguagem de programação, então tem muita importância na prática.

Antes de encontrar o Python, fiz um pouco de programação para a web usando Perl e PHP. A sintaxe para mapeamentos nessas linguagens é muito útil, e eu sinto imensa saudade dela sempre que tenho que usar Java ou C.

Uma boa sintaxe para mapeamentos literais é muito conveniente para configuração, para implementações guiadas por tabelas, e para conter dados para prototipagem e testes. Essa foi uma das lições que os projetistas do Go aprenderam com as linguagens dinâmicas. A falta de uma boa forma de expressar dados estruturados no código empurrou a comunidade Java a adotar o prolixo e excessivamente complexo XML como formato de dados.

JSON foi proposto como "The Fat-Free Alternative to XML" (A alternativa sem gordura ao XML) e se tornou um imenso sucesso, substituindo o XML em vários contextos. Uma sintaxe concisa para listas e dicionários resulta em um excelente formato para troca de dados.

O PHP e o Ruby imitaram a sintaxe de hash do Perl, usando => para ligar chaves a valores. O JavaScript usa : como o Python. Por que usar dois caracteres, quando um já é legível o bastante?

O JSON veio do JavaScript, mas por acaso também é quase um subconjunto exato da sintaxe do Python. O JSON é compatível com o Python, exceto pela grafia dos valores true, false, e null.

Armin Ronacher tuitou que gosta de brincar com o espaço de nomes global do Python, para acrescentar apelidos compatíveis com o JSON para o True, o False, e o None do Python, pois daí ele pode colar trechos de JSON diretamente no console. Sua ideia básica:

>>> true, false, null = True, False, None
>>> fruit = {
...     "type": "banana",
...     "avg_weight": 123.2,
...     "edible_peel": false,
...     "species": ["acuminata", "balbisiana", "paradisiaca"],
...     "issues": null,
... }
>>> fruit
{'type': 'banana', 'avg_weight': 123.2, 'edible_peel': False,
'species': ['acuminata', 'balbisiana', 'paradisiaca'], 'issues': None}

A sintaxe que todo mundo agora usa para trocar dados é a sintaxe de dict e list do Python. E então temos uma sintaxe agradável com a conveniência da preservação da ordem de inserção.

Simples e correto.


1. Uma subclasse virtual é qualquer classe registrada com uma chamada ao método .register() de uma ABC, como explicado na [virtual_subclass_sec]. Um tipo implementado através da API Python/C também é aceitável, se um bit de marcação específico estiver acionado. Veja Py_TPFLAGS_MAPPING (EN).
2. O verbete para "hashable" no Glossário do Python usa o termo "valor de hash" em vez de código de hash. Prefiro código de hash porque "valor" é um conceito frequentemente usado no contexto de mapeamentos, onde itens são compostos de chavas e valores. Então pode ser confuso se referir ao código de hash como um valor. Nesse livro usarei apenas código de hash.
3. Veja a PEP 456—Secure and interchangeable hash algorithm (Algoritmo de hash seguro e intercambiável_) (EN) para saber mais sobre as implicações de segurança e as soluções adotadas.
4. default_factory não é um método, mas um atributo chamável definido pelo usuário quando um defaultdict é instanciado.
5. OrderedDict.popitem(last=False) remove o primeiro item inserido (FIFO). O argumento nomeado last não é suportado por dict ou defaultdict, pelo menos até o Python 3.10b3.
6. Operadores invertidos são tratados no [operator_overloading].
7. O script original aparece no slide 41 da apresentação de Martelli, "Re-learning Python" (Reaprendendo Python) (EN). O script é, na verdade, uma demonstração de dict.setdefault, como visto no nosso Exemplo 5.
8. Isso é um exemplo do uso de um método como uma função de primeria classe, o assunto do [functions_as_objects].
9. NT: "Missing" significa ausente, perdido ou desaparecido
10. Uma biblioteca dessas é a Pingo.io, que não está mais em desenvolvimento ativo.
11. NT: Least Recently Used, Menos Recentemente Usado, esquema de cache que descarta o item armazenado que esteja há mais tempo sem requisições
12. NT: "to shelve" é "colocar na prateleira", "pickle" também significa "conserva" e "pickles" é literalmente picles. O trocadilho dos desenvolvedores do Python é sobre colocar pickles em shelves.
13. O problema exato de se criar subclasses de dict e de outros tipos embutidos é tratado na [subclass_builtin_woes].
14. É assim que as tuplas são armazenadas.
15. A menos que classe tenha umm atributo __slots__, como explicado na [slots_section].
16. Isso pode ser interessante, mas não é super importante. Essa diferença de velocidade vai ocorrer apenas quando um conjunto literal for avaliado, e isso acontece no máximo uma vez por processo Python—quando um módulo é compilado pela primeira vez. Se você estiver curiosa, importe a função dis do módulo dis, e a use para inspecionar os bytecodes de um set literal—por exemplo, dis('{1}')—e uma chamada ao construtor setdis('set([1])')
17. NT: Na teoria dos conjuntos, A é um subconjunto próprio de B se A é subconjunto de B e A é diferente de B.
18. NT: Explicando o trocadilho intraduzível: "colon", em inglês, designa "a parte central do intestino grosso"; "semicolon", por outro lado, é "ponto e vírgula". A frase diz, literalmente, "Açúcar sintático causa câncer no ponto e vírgula", que faz sentido em inglês pela proximidade ortográfica das duas palavras.