Quando vejo padrões em meus programas, considero isso um mau sinal. A forma de um programa deve refletir apenas o problema que ele precisa resolver. Qualquer outra regularidade no código é, pelo menos para mim, um sinal que estou usando abstrações que não são poderosas o suficiente—muitas vezes estou gerando à mão as expansões de alguma macro que preciso escrever.[1]
hacker de Lisp e investidor
A iteração é fundamental para o processamento de dados: programas aplicam computações sobre séries de dados, de pixels a nucleotídeos. Se os dados não cabem na memória, precisamos buscar esses itens de forma preguiçosa—um de cada vez e sob demanda. É isso que um iterador faz. Este capítulo mostra como o padrão de projeto Iterator ("Iterador") está embutido na linguagem Python, de modo que nunca será necessário programá-lo manualmente.
Todas as coleções padrão do Python são iteráveis. Um iterável é um objeto que fornece um iterador, que o Python usa para suportar operações como:
-
loops
for
-
Compreensões de lista, dict e set
-
Desempacotamento para atribuições
-
Criação de instâncias de coleções
Este capítulo cobre os seguintes tópicos:
-
Como o Python usa a função embutida
iter()
para lidar com objetos iteráveis -
Como implementar o padrão Iterator clássico no Python
-
Como o padrão Iterator clássico pode ser substituído por uma função geradora ou por uma expressão geradora
-
Como funciona uma função geradora, em detalhes, com descrições linha a linha
-
Aproveitando o poder das funções geradoras de uso geral da biblioteca padrão
-
Usando expressões
yield from
para combinar geradoras -
Porque geradoras e corrotinas clássicas se parecem, mas são usadas de formas muito diferentes e não devem ser misturadas
A Subgeradoras com yield from aumentou de uma para seis páginas.
Ela agora inclui experimentos simples, demonstrando o comportamento de geradoras com yield from
, e um exemplo de código para percorrer uma árvore de dados, desenvolvido passo a passo.
Novas seções explicam as dicas de tipo para os tipos Iterable
, Iterator
e Generator
.
A última grande seção do capítulo, Corrotinas clássicas, é agora uma introdução de 9 páginas a um tópico que ocupava um capítulo de 40 páginas na primeira edição. Atualizei e transferi o capítulo Classic Coroutines (Corrotinas Clássicas) para um post no site que acompanha o livro, porque ele era o capítulo mais difícil para os leitores, mas seu tema se tornou menos relevante após a introdução das corrotinas nativas no Python 3.5 (estudaremos as corrotinas nativas no [async_ch]).
Vamos começar examinando como a função embutida iter()
torna as sequências iteráveis.
Vamos começar nossa exploração de iteráveis implementando uma classe Sentence
: seu construtor recebe uma string de texto e daí podemos iterar sobre a "sentença" palavra por palavra. A primeira versão vai implementar o protocolo de sequência e será iterável, pois todas as sequências são iteráveis—como sabemos desde o [data_model].
Agora veremos exatamente porque isso acontece.
O Exemplo 1 mostra uma classe Sentence
que extrai palavras de um texto por
índice.
Sentence
como uma sequência de palavraslink:code/17-it-generator/sentence.py[role=include]
-
.findall
devolve a lista com todos os trechos não sobrepostos correspondentes à expressão regular, como uma lista de strings. -
self.words
mantém o resultado de.findall
, então basta devolver a palavra em um dado índice. -
Para completar o protocolo de sequência, implementamos
__len__
, apesar dele não ser necessário para criar um iterável. -
reprlib.repr
é uma função utilitária para gerar representações abreviadas, em forma de strings, de estruturas de dados que podem ser muito grandes.[2]
Por default, reprlib.repr
limita a string gerada a 30 caracteres. Veja como Sentence
é usada na sessão de console do Exemplo 2.
Sentence
>>> s = Sentence('"The time has come," the Walrus said,') # (1)
>>> s
Sentence('"The time ha... Walrus said,') # (2)
>>> for word in s: # (3)
... print(word)
The
time
has
come
the
Walrus
said
>>> list(s) # (4)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
-
Uma sentença criada a partir de uma string.
-
Observe a saída de
__repr__
gerada porreprlib.repr
, usando…
. -
Instâncias de
Sentence
são iteráveis; veremos a razão em seguida. -
Sendo iteráveis, objetos
Sentence
podem ser usados como entrada para criar listas e outros tipos iteráveis.
Nas próximas páginas vamos desenvolver outras classes Sentence
que passam nos testes do Exemplo 2.
Entretanto, a implementação no Exemplo 1 difere das outras por ser também uma sequência, e então é possível obter palavras usando um índice:
>>> s[0]
'The'
>>> s[5]
'Walrus'
>>> s[-1]
'said'
Programadores Python sabem que sequências são iteráveis. Agora vamos descobrir exatamente o porquê disso.
Sempre que o Python precisa iterar sobre um objeto x
, ele automaticamente invoca iter(x)
.
A função embutida iter
:
-
Verifica se o objeto implementa o método
__iter__
, e o invoca para obter um iterador. -
Se
__iter__
não for implementado, mas__getitem__
sim, entãoiter()
cria um iterador que tenta buscar itens pelo índice, começando de 0 (zero). -
Se isso falhar, o Python gera um
TypeError
, normalmente dizendo'C' object is not iterable
(objeto 'C' não é iterável), ondeC
é a classe do objeto alvo.
Por isso todas as sequências do Python são iteráveis: por definição, todas elas implementam __getitem__
.
Na verdade, todas as sequências padrão também implementam __iter__
, e as suas próprias sequências também deviam implementar esse método, porque a iteração via __getitem__
existe para manter a compatibilidade retroativa, e pode desaparecer em algum momento—apesar dela não ter sido descontinuada no Python 3.10, e eu duvidar que vá ser removida algum dia.
Como mencionado na [python_digs_seq_sec], essa é uma forma extrema de duck typing: um objeto é considerado iterável não apenas quando implementa o método especial __iter__
, mas também quando implementa __getitem__
. Veja isso:
>>> class Spam:
... def __getitem__(self, i):
... print('->', i)
... raise IndexError()
...
>>> spam_can = Spam()
>>> iter(spam_can)
<iterator object at 0x10a878f70>
>>> list(spam_can)
-> 0
[]
>>> from collections import abc
>>> isinstance(spam_can, abc.Iterable)
False
Se uma classe fornece __getitem__
, a função embutida iter()
aceita uma instância daquela classe como iterável e cria um iterador a partir da instância.
A maquinaria de iteração do Python chamará __getitem__
com índices, começando de 0, e entenderá um IndexError
como sinal de que não há mais itens.
Observe que, apesar de spam_can
ser iterável (seu método __getitem__
poderia fornecer itens), ela não é reconhecida assim por uma chamada a isinstance
contra abc.Iterable
.
Na abordagem da goose typing, a definição para um iterável é mais simples, mas não tão flexível: um objeto é considerado iterável se implementa o método __iter__
.
Não é necessário ser subclasse ou se registar, pois abc.Iterable
implementa o __subclasshook__
, como visto na [subclasshook_sec]. Eis uma demonstração:
>>> class GooseSpam:
... def __iter__(self):
... pass
...
>>> from collections import abc
>>> issubclass(GooseSpam, abc.Iterable)
True
>>> goose_spam_can = GooseSpam()
>>> isinstance(goose_spam_can, abc.Iterable)
True
Tip
|
Desde o Python 3.10, a forma mais precisa de verificar se um objeto |
Verificar explicitamente se um objeto é iterável pode não valer a pena, se você for iterar sobre o objeto logo após a verificação.
Afinal, quando se tenta iterar sobre um não-iterável, a exceção gerada pelo Python é bastante clara: TypeError: 'C' object is not iterable
(TypeError: o objeto 'C' não é iterável).
Se você puder fazer algo mais além de gerar um TypeError
, então faça isso em um bloco try/except
ao invés de realizar uma verificação explícita.
A verificação explícita pode fazer sentido se você estiver mantendo o objeto para iterar sobre ele mais tarde; nesse caso, capturar o erro mais cedo torna a depuração mais fácil.
A função embutida iter()
é usada mais frequentemente pelo Python que no nosso código.
Há uma segunda maneira de usá-la, mas não é muito conhecida.
Podemos chamar iter()
com dois argumentos, para criar um iterador a partir de uma função ou de qualquer objeto invocável.
Nessa forma de uso, o primeiro argumento deve ser um invocável que será chamado repetidamente (sem argumentos) para produzir valores, e o segundo argumento é um valor sentinela (EN): um marcador que, quando devolvido por um invocável, faz o iterador gerar um StopIteration
ao invés de produzir o valor sentinela.
O exemplo a seguir mostra como usar iter
para rolar um dado de seis faces até que o valor 1
seja sorteado:
>>> def d6():
... return randint(1, 6)
...
>>> d6_iter = iter(d6, 1)
>>> d6_iter
<callable_iterator object at 0x10a245270>
>>> for roll in d6_iter:
... print(roll)
...
4
3
6
3
Observe que a função iter
devolve um callable_iterator
.
O loop for
no exemplo pode rodar por um longo tempo, mas nunca vai devolver 1
, pois esse é o valor sentinela. Como é comum com iteradores, o objeto d6_iter
se torna inútil após ser exaurido. Para recomeçar, é necessário reconstruir o iterador, invocando novamente iter()
.
A documentação de iter
inclui a seguinte explicação e código de exemplo:
Uma aplicação útil da segunda forma de
iter()
é para construir um bloco de leitura. Por exemplo, ler blocos de comprimento fixo de um arquivo binário de banco de dados até que o final do arquivo seja atingido:
from functools import partial
with open('mydata.db', 'rb') as f:
read64 = partial(f.read, 64)
for block in iter(read64, b''):
process_block(block)
Para deixar o código mais claro, adicionei a atribuição read64
, que não está no
exemplo original.
A função partial()
é necessária porque o invocável passado a iter()
não pode requerer argumentos.
No exemplo, um objeto bytes
vazio é a sentinela, pois é isso que f.read
devolve quando não há mais bytes para ler.
A próxima seção detalha a relação entre iteráveis e iteradores.
Da explicação na Porque sequências são iteráveis: a função iter podemos extrapolar a seguinte definição:
- iterável
-
Qualquer objeto a partir do qual a função embutida
iter
consegue obter um iterador. Objetos que implementam um método__iter__
devolvendo um iterador são iteráveis. Sequências são sempre iteráveis, bem como objetos que implementam um método__getitem__
que aceite índices iniciando em 0.
É importante deixar clara a relação entre iteráveis e iteradores: o Python obtém iteradores de iteráveis.
Aqui está um simples loop for
iterando sobre uma str
. A str
'ABC'
é o iterável aqui. Você não vê, mas há um iterador por trás das cortinas:
>>> s = 'ABC'
>>> for char in s:
... print(char)
...
A
B
C
Se não existisse uma instrução for
e fosse preciso emular o mecanismo do for
à mão com um loop while
, isso é o que teríamos que escrever:
>>> s = 'ABC'
>>> it = iter(s) # (1)
>>> while True:
... try:
... print(next(it)) # (2)
... except StopIteration: # (3)
... del it # (4)
... break # (5)
...
A
B
C
-
Cria um iterador
it
a partir de um iterável. -
Chama
next
repetidamente com o iterador, para obter o item seguinte. -
O iterador gera
StopIteration
quando não há mais itens. -
Libera a referência a
it
—o obleto iterador é descartado. -
Sai do loop.
StopIteration
sinaliza que o iterador foi exaurido.
Essa exceção é tratada internamente pela função embutida iter()
, que é parte da lógica dos loops for
e de outros contextos de iteração, como compreensões de lista, desempacotamento iterável, etc.
A interface padrão do Python para um iterador tem dois métodos:
__next__
-
Devolve o próximo item em uma série, gerando
StopIteration
se não há mais nenhum. __iter__
-
Devolve
self
; isso permite que iteradores sejam usado quando um iterável é esperado. Por exemplo, em um loopfor
loop.
Essa interface está formalizada na ABC collections.abc.Iterator
,
que declara o método abstrato __next__
,
e é uma subclasse de Iterable—onde o método abstrato __iter__
é declarado.
Veja a Figura 1.
Iterable
e Iterator
. Métodos em itálico são abstratos. Um Iterable.__iter__
concreto deve devolver uma nova instância de Iterator
. Um Iterator
concreto deve implementar __next__
. O método Iterator.__iter__
apenas devolve a própria instância.O código-fonte de collections.abc.Iterator
aparece no Exemplo 3.
abc.Iterator
; extraído de Lib/_collections_abc.pyclass Iterator(Iterable):
__slots__ = ()
@abstractmethod
def __next__(self):
'Return the next item from the iterator. When exhausted, raise StopIteration'
raise StopIteration
def __iter__(self):
return self
@classmethod
def __subclasshook__(cls, C): # (1)
if cls is Iterator:
return _check_methods(C, '__iter__', '__next__') # (2)
return NotImplemented
-
__subclasshook__
suporta a verificação de tipo estrutural comisinstance
eissubclass
. Vimos isso na [subclasshook_sec]. -
_check_methods
percorre o parâmetro__mro__
da classe, para verificar se os métodos estão implementados em sua classe base. Ele está definido no mesmo módulo, Lib/_collections_abc.py. Se os métodos estiverem implementados, a classeC
será reconhecida como uma subclasse virtual deIterator
. Em outras palavras,issubclass(C, Iterable)
devolveráTrue
.
Warning
|
O método abstrato da ABC |
O código-fonte do módulo Lib/types.py no Python 3.9 tem um comentário dizendo:
# Iteradores no Python não são uma questão de tipo, mas sim de protocolo. Um número # grande e variável de tipos embutidos implementa *alguma* forma de # iterador. Não verifique o tipo! Em vez disso, use `hasattr` para # verificar [a existência] de ambos os atributos "__iter__" e "__next__".
E de fato, é exatamente o que o método __subclasshook__
da ABC abc.Iterator
faz.
Tip
|
Dado o conselho de Lib/types.py e a lógica implementada em Lib/_collections_abc.py, a melhor forma de verificar se um objeto |
Voltando à nossa classe Sentence
no Exemplo 1, usando o console do Python é possivel ver claramente como o iterador é criado por iter()
e consumido por next()
:
>>> s3 = Sentence('Life of Brian') # (1)
>>> it = iter(s3) # (2)
>>> it # doctest: +ELLIPSIS
<iterator object at 0x...>
>>> next(it) # (3)
'Life'
>>> next(it)
'of'
>>> next(it)
'Brian'
>>> next(it) # (4)
Traceback (most recent call last):
...
StopIteration
>>> list(it) # (5)
[]
>>> list(iter(s3)) # (6)
['Life', 'of', 'Brian']
-
Cria uma sentença
s3
com três palavras. -
Obtém um iterador a partir de
s3
. -
next(it)
devolve a próxima palavra. -
Não há mais palavras, então o iterador gera uma exceção
StopIteration
. -
Uma vez exaurido, um itereador irá sempre gerar
StopIteration
, o que faz parecer que ele está vazio.. -
Para percorrer a sentença novamente é preciso criar um novo iterador.
Como os únicos métodos exigidos de um iterador são __next__
e __iter__
, não há como verificar se há itens restantes, exceto invocando next()
e capturando StopIteration
.
Além disso, não é possível "reiniciar" um iterador.
Se for necessário começar de novo, é preciso invocar iter()
no iterável que criou o iterador original.
Invocar iter()
no próprio iterador também não funciona, pois—como já mencionado—a implementação de Iterator.__iter__
apenas devolve self
, e isso não reinicia um iterador exaurido.
Essa interface mínima é bastante razoável porque, na realidade, nem todos os itereadores são reiniciáveis. Por exemplo, se um iterador está lendo pacotes da rede, não há como "rebobiná-lo".[3]
A primeira versão de Sentence
, no Exemplo 1, era iterável graças ao tratamento especial dispensado pela função embutida às sequências.
A seguir vamos implementar variações de Sentence
que implementam __iter__
para devolver iteradores.
As próximas variantes de Sentence
implementam o protocolo iterável padrão, primeiro implementando o padrão de projeto Iterable e depois com funções geradoras.
A próxima implementação de Sentence
segue a forma do padrão de projeto Iterator clássico, do livro Padrões de Projeto.
Observe que isso não é Python idiomático, como as refatorações seguintes deixarão claro.
Mas é útil para mostrar a distinção entre uma coleção iterável e um iterador que trabalha com ela.
A classe Sentence
no Exemplo 4 é iterável por implementar o método especial __iter__
,
que cria e devolve um SentenceIterator
. É assim que um iterável e um iterador se relacionam.
Sentence
implementada usando o padrão Iteratorlink:code/17-it-generator/sentence_iter.py[role=include]
-
O método
__iter__
é o único acréscimo à implementação anterior deSentence
. Essa versão não inclui um__getitem__
, para deixar claro que a classe é iterável por implementar__iter__
. -
__iter__
atende ao protocolo iterável instanciando e devolvendo um iterador. -
SentenceIterator
mantém uma referência para a lista de palavras. -
self.index
determina a próxima palavra a ser recuperada. -
Obtém a palavra em
self.index
. -
Se não há palavra em
self.index
, gera umaStopIteration
. -
Incrementa
self.index
. -
Devolve a palavra.
-
Implementa
self.__iter__
.
Veja que não é de fato necessário implementar __iter__
em SentenceIterator
para esse exemplo funcionar, mas é o correto a fazer: supõe-se que iteradores implementem tanto __next__
quanto
__iter__
, e fazer isso permite ao nosso iterador passar no teste
issubclass(SentenceIterator, abc.Iterator)
.
Se tivéssemos tornado SentenceIterator
uma subclasse de abc.Iterator
, teríamos herdado o método concreto abc.Iterator.__iter__
.
É um bocado de trabalho (pelo menos para nós, programadores mimados pelo Python).
Observe que a maior parte do código em SentenceIterator
serve para gerenciar o estado interno do iterador. Logo veremos como evitar essa burocracia.
Mas antes, um pequeno desvio para tratar de um atalho de implementação que pode parecer tentador, mas é apenas errado.
Uma causa comum de erros na criação de iteráveis é confundir os dois.
Para deixar claro: iteráveis tem um método __iter__
que instancia um novo iterador a cada invocação.
Iteradores implementam um método __next__
, que devolve itens individuais, e um método
__iter__
, que devolve self
.
Assim, iteradores também são iteráveis, mas iteráveis não são iteradores.
Pode ser tentador implementar __next__
além de __iter__
na classe Sentence
, tornando cada instância de Sentence
ao mesmo tempo um iterável e um iterador de si mesma.
Mas raramente isso é uma boa ideia. Também é um anti-padrão comum, de acordo com Alex Martelli, que possui vasta experiência revisando código no Google.
A seção "Aplicabilidade" do padrão de projeto Iterator no livro Padrões de Projeto diz:
Use o padrão Iterator
para acessar o conteúdo de um objeto agregado sem expor sua representação interna.
para suportar travessias múltiplas de objetos agregados.
para fornecer uma interface uniforme para atravessar diferentes estruturas agregadas (isto é, para suportar iteração polimórfica).
Para "suportar travessias múltiplas", deve ser possível obter múltiplos iteradores independentes de uma mesma instância iterável, e cada iterador deve manter seu próprio estado interno. Assim, uma implementação adequada do padrão exige que cada invocação de iter(my_iterable)
crie um novo iterador independente. É por essa razão que precisamos da classe SentenceIterator
neste exemplo.
Agora que demonstramos de forma apropriada o padrão Iterator clássico, vamos em frente.
O Python incorporou a instrução yield
da linguagem CLU, de Barbara Liskov, para não termos que "escrever à mão" o código implementando iteradores.
As próximas seções apresentam versões mais idiomáticas de Sentence
.
Uma implementação pythônica da mesma funcionalidade usa uma geradora, evitando todo o trabalho para implementar a classe SentenceIterator
. A explicação completa da geradora está logo após o Exemplo 5.
Sentence
implementada usando uma geradoralink:code/17-it-generator/sentence_gen.py[role=include]
-
Itera sobre
self.words
. -
Produz a
word
atual. -
Um
return
explícito não é necessário; a função pode apenas seguir em frente e retornar automaticamente. De qualquer das formas, uma função geradora não geraStopIteration
: ela simplesmente termina quando acaba de produzir valores.[4] -
Não há necessidade de uma classe iteradora separada!
Novamente temos aqui uma implementação diferente de Sentence
que passa nos testes do Exemplo 2.
No código de Sentence
do Exemplo 4, __iter__
chamava o construtor SentenceIterator
para criar e devolver um iterador. Agora o iterador do Exemplo 5 é na verdade um objeto gerador, criado automaticamente quando o método __iter__
é invocado, porque aqui __iter__
é uma função geradora.
Segue abaixo uma explicação completa das geradoras.
Qualquer função do Python contendo a instrução yield
em seu corpo é uma função geradora: uma função que, quando invocada, devolve um objeto gerador. Em outras palavras, um função geradora é uma fábrica de geradores.
Tip
|
O único elemento sintático distinguindo uma função comum de uma função geradora é o fato dessa última conter a instrução |
>>> def gen_123(): ... yield 1 # (1) ... yield 2 ... yield 3 ... >>> gen_123 # doctest: +ELLIPSIS <function gen_123 at 0x...> # (2) >>> gen_123() # doctest: +ELLIPSIS <generator object gen_123 at 0x...> # (3) >>> for i in gen_123(): # (4) ... print(i) 1 2 3 >>> g = gen_123() # (5) >>> next(g) # (6) 1 >>> next(g) 2 >>> next(g) 3 >>> next(g) # (7) Traceback (most recent call last): ... StopIteration
-
O corpo de uma função geradora muitas vezes contém
yield
dentro de um loop, mas não necessariamente; aqui eu apenas repetiyield
três vezes. -
Olhando mais de perto, vemos que
gen_123
é um objeto função. -
Mas quando invocado,
gen_123()
devolve um objeto gerador. -
Objetos geradores implementam a interface
Iterator
, então são também iteráveis. -
Atribuímos esse novo objeto gerador a
g
, para podermos experimentar seu funcionamento. -
Como
g
é um iterador, chamarnext(g)
obtém o próximo item produzido poryield
. -
Quando a função geradora retorna, o objeto gerador gera uma
StopIteration
.
Uma função geradora cria um objeto gerador que encapsula o corpo da função.
Quando invocamos next()
no objeto gerador,
a execução avança para o próximo yield
no corpo da função,
e a chamada a next()
resulta no valor produzido quando o corpo da função é suspenso.
Por fim, o objeto gerador externo criado pelo Python gera uma StopIteration
quando a função retorna, de acordo com o protocolo Iterator
.
Tip
|
Acho útil ser rigoroso ao falar sobre valores obtidos a partir de um gerador. É confuso dizer que um gerador "devolve" valores. Funções devolvem valores. A chamada a uma função geradora devolve um gerador. Um gerador produz (yields) valores. Um gerador não "devolve" valores no sentido comum do termo: a instrução |
O Exemplo 7 torna a iteração entre um loop for
e o corpo da função mais explícita.
>>> def gen_AB():
... print('start')
... yield 'A' # (1)
... print('continue')
... yield 'B' # (2)
... print('end.') # (3)
...
>>> for c in gen_AB(): # (4)
... print('-->', c) # (5)
...
start (6)
--> A (7)
continue (8)
--> B (9)
end. (10)
>>> (11)
-
A primeira chamada implícita a
next()
no loopfor
em vai exibir'start'
e parar no primeiroyield
, produzindo o valor'A'
. -
A segunda chamada implícita a
next()
no loopfor
vai exibir'continue'
e parar no segundoyield
, produzindo o valor'B'
. -
A terceira chamada a
next()
vai exibir'end.'
e continuar até o final do corpo da função, fazendo com que o objeto gerador crie umaStopIteration
. -
Para iterar, o mecanismo do
for
faz o equivalente ag = iter(gen_AB())
para obter um objeto gerador, e daínext(g)
a cada iteração. -
O loop exibe
-→
e o valor devolvido pornext(g)
. Esse resultado só aparece após a saída das chamadasprint
dentro da função geradora. -
O texto
start
vem deprint('start')
no corpo da geradora. -
yield 'A'
no corpo da geradora produz o valor 'A' consumido pelo loopfor
, que é atribuído à variávelc
e resulta na saída-→ A
. -
A iteração continua com a segunda chamada a
next(g)
, avançando no corpo da geradora deyield 'A'
parayield 'B'
. O textocontinue
é gerado pelo segundoprint
no corpo da geradora. -
yield 'B'
produz o valor 'B' consumido pelo loopfor
, que é atribuído à variávelc
do loop, que então exibe-→ B
. -
A iteração continua com uma terceira chamada a
next(it)
, avançando para o final do corpo da função. O textoend.
é exibido por causa do terceiroprint
no corpo da geradora. -
Quando a função geradora chega ao final, o objeto gerador cria uma
StopIteration
. O mecanismo do loopfor
captura essa exceção, e o loop encerra naturalmente.
Espero agora ter deixado claro como Sentence.__iter__
no Exemplo 5 funciona: __iter__
é uma função geradora que, quando chamada, cria um objeto gerador que implementa a interface Iterator
, então a classe SentenceIterator
não é mais necessária.
A segunda versão de Sentence
é mais concisa que a primeira, mas não é tão preguiçosa quanto poderia ser. Atualmente, a preguiça é considerada uma virtude, pelo menos em linguagens de programação e APIs. Uma implementação preguiçosa adia a produção de valores até o último momento possível. Isso economiza memória e também pode evitar o desperdício de ciclos da CPU.
Vamos criar a seguir classes Sentence
preguiçosas.
As últimas variações de Sentence
são preguiçosas, se valendo de um função preguiçosa do módulo re
.
A interface Iterator
foi projetada para ser preguiçosa: next(my_iterator)
produz um item por vez.
O oposto de preguiçosa é ávida: avaliação preguiçosa e ávida são termos técnicos da teoria das linguagens de programação[7].
Até aqui, nossas implementações de Sentence
não são preguiçosas, pois o __init__
cria avidamemente uma lista com todas as palavras no texto, vinculando-as ao atributo self.words
.
Isso exige o processamento do texto inteiro, e a lista pode acabar usando tanta memória quanto o próprio texto (provavelmente mais: vai depender de quantos caracteres que não fazem parte de palavras existirem no texto).
A maior parte desse trabalho será inútil se o usuário iterar apenas sobre as primeiras palavras.
Se você está se perguntado se "Existiria uma forma preguiçosa de fazer isso em Python?", a resposta muitas vezes é "Sim".
A função re.finditer
é uma versão preguiçosa de re.findall
. Em vez de uma lista, re.finditer
devolve uma geradora que produz instâncias de re.MatchObject
sob demanda.
Se existirem muitos itens, re.finditer
economiza muita memória.
Com ela, nossa terceira versão de Sentence
agora é preguiçosa:
ela só lê a próxima palavra do texto quando necessário.
O código está no Exemplo 8.
Sentence
implementada usando uma função geradora que invoca a função geradora re.finditer
link:code/17-it-generator/sentence_gen2.py[role=include]
-
Não é necessário manter uma lista
words
. -
finditer
cria um iterador sobre os termos encontrados comRE_WORD
emself.text
, produzindo instâncias deMatchObject
. -
match.group()
extraí o texto da instância deMatchObject
.
Geradores são um ótimo atalho, mas o código pode ser ainda mais conciso com uma expressão geradora.
Podemos substituir funções geradoras simples como aquela na última classe `Sentence (no Exemplo 8) por uma expressão geradora. Assim como uma compreensão de lista cria listas, uma expressão geradora cria objetos geradores. O Exemplo 9 compara o comportamento nos dois casos.
gen_AB
é usada primeiro por uma compreensão de lista, depois por uma expressão geradora>>> def gen_AB(): # (1)
... print('start')
... yield 'A'
... print('continue')
... yield 'B'
... print('end.')
...
>>> res1 = [x*3 for x in gen_AB()] # (2)
start
continue
end.
>>> for i in res1: # (3)
... print('-->', i)
...
--> AAA
--> BBB
>>> res2 = (x*3 for x in gen_AB()) # (4)
>>> res2
<generator object <genexpr> at 0x10063c240>
>>> for i in res2: # (5)
... print('-->', i)
...
start # (6)
--> AAA
continue
--> BBB
end.
-
Está é a mesma função
gen_AB
do Exemplo 7. -
A compreensão de lista itera avidamente sobre os itens produzidos pelo objeto gerador devolvido por
gen_AB()
:'A'
e'B'
. Observe a saída nas linhas seguintes:start
,continue
,end.
-
Esse loop
for
itera sobre a listares1
criada pela compreensão de lista. -
A expressão geradora devolve
res2
, um objeto gerador. O gerador não é consumido aqui. -
Este gerador obtém itens de
gen_AB
apenas quando o loopfor
itera sobreres2
. Cada iteração do loopfor
invoca, implicitamente,next(res2)
, que por sua vez invocanext()
sobre o objeto gerador devolvido porgen_AB()
, fazendo este último avançar até o próximoyield
. -
Observe como a saída de
gen_AB()
se intercala com a saída doprint
no loopfor
.
Podemos usar uma expressão geradora para reduzir ainda mais o código na classe Sentence
. Veja o Exemplo 10.
Sentence
implementada usando uma expressão geradoralink:code/17-it-generator/sentence_genexp.py[role=include]
A única diferença com o Exemplo 8 é o método __iter__
, que aqui não é uma função geradora (ela não contém uma instrução yield
) mas usa uma expressão geradora para criar um gerador e devolvê-lo. O resultado final é o mesmo: quem invoca __iter__
recebe um objeto gerador.
Expressões geradoras são "açúcar sintático": elas pode sempre ser substituídas por funções geradoras, mas algumas vezes são mais convenientes. A próxima seção trata do uso de expressões geradoras.
Eu usei várias expressões geradoras quando implementamos a classe Vector
no [ex_vector_v5]. Cada um destes métodos contém uma expressão geradora: __eq__
, __hash__
, __abs__
, angle
, angles
, format
, __add__
, e __mul__
.
Em todos aqueles métodos, uma compreensão de lista também funcionaria, com um custo adicional de memória para armazenar os valores da lista intermediária.
No Exemplo 10, vimos que uma expressão geradora é um atalho sintático para criar um gerador sem definir e invocar uma função. Por outro lado, funções geradoras são mais flexíveis: podemos programar uma lógica complexa, com múltiplos comandos, e podemos até usá-las como corrotinas, como veremos na Corrotinas clássicas.
Nos casos mais simples, uma expressão geradora é mais fácil de ler de relance, como mostra o exemplo de Vector
.
Minha regra básica para escolher qual sintaxe usar é simples: se a expressão geradora exige mais que um par de linhas, prefiro escrever uma função geradora, em nome da legibilidade.
Tip
|
Dica de sintaxe
Quando uma expressão geradora é passada como único argumento a uma função ou a um construtor, não é necessário escrever um conjunto de parênteses para a chamada da função e outro par cercando a expressão geradora. Um único par é suficiente, como na chamada a def __mul__(self, scalar):
if isinstance(scalar, numbers.Real):
return Vector(n * scalar for n in self)
else:
return NotImplemented Entretanto, se existirem mais argumentos para a função após a expressão geradora, é preciso cercar a expressão com parênteses para evitar um |
Os exemplos de Sentence
vistos até aqui mostram geradores fazendo o papel do padrão Iterator clássico: obter itens de uma coleção.
Mas podemos também usar geradores para produzir valores independente de uma fonte de dados.
A próxima seção mostra um exemplo.
Mas antes, um pequena discussão sonre os conceitos sobrepostos de iterador e gerador.
Na documentação e na base de código oficiais do Python, a terminologia em torno de iteradores e geradores é inconsistente e está em evolução. Adotei as seguintes definições:
- iterador
-
Termo geral para qualquer objeto que implementa um método
__next__
. Iteradores são projetados para produzir dados a serem consumidos pelo código cliente, isto é, o código que controla o iterador através de um loopfor
ou outro mecanismo de iteração, ou chamandonext(it)
explicitamente no iterador—apesar desse uso explícito ser menos comum. Na prática, a maioria dos iteradores que usamos no Python são geradores. - gerador
-
Um iterador criado pelo compilador Python. Para criar um gerador, não implementamos
__next__
. Em vez disso, usamos a palavra reservadayield
para criar uma função geradora, que é uma fábrica de objetos geradores. Uma expressão geradora é outra maneira de criar um objeto gerador. Objetos geradores fornecem__next__
, então são iteradores. Desde o Python 3.5, também temos geradores assíncronos, declarados comasync def
. Vamos estudá-los no [async_ch].
O Glossário do Python introduziu recentemente o termo iterador gerador para se referir a objetos geradores criados por funções geradoras, enquanto o verbete para expressão geradora diz que ela devolve um "iterador".
Mas, de acordo com o interpretador Python, os objetos devolvidos em ambos os casos são objetos geradores:
>>> def g():
... yield 0
...
>>> g()
<generator object g at 0x10e6fb290>
>>> ge = (c for c in 'XYZ')
>>> ge
<generator object <genexpr> at 0x10e936ce0>
>>> type(g()), type(ge)
(<class 'generator'>, <class 'generator'>)
O padrão Iterator clássico está todo baseado em uma travessia: navegar por alguma estrutura de dados. Mas uma interface padrão baseada em um método para obter o próximo item em uma série também é útil quando os itens são produzidos sob demanda, ao invés de serem obtidos de uma coleção. Por exemplo, a função embutida range
gera uma progressão aritmética (PA) de inteiros delimitada. E se precisarmos gerar uma PA com números de qualquer tipo, não apenas inteiros?
O Exemplo 11 mostra alguns testes no console com uma classe ArithmeticProgression
, que vermos em breve.
A assinatura do construtor no Exemplo 11 é ArithmeticProgression(begin, step[, end])
.
A assinatura completa da função embutida range
é range(start, stop[, step])
.
Escolhi implementar uma assinatura diferente porque o step
é obrigatório, mas end
é opcional em uma progressão aritmética.
Também mudei os nomes dos argumentos de start/stop
para begin/end
, para deixar claro que optei por uma assinatura diferente.
Para cada teste no Exemplo 11, chamo list()
com o resultado para inspecionar o valores gerados.
ArithmeticProgression
link:code/17-it-generator/aritprog_v1.py[role=include]
Observe que o tipo dos números na progressão aritmética resultante segue o tipo de begin + step
, de acordo com as regras de coerção numérica da aritmética do Python. No Exemplo 11, você pode ver listas de números int
, float
, Fraction
, e Decimal
.
O Exemplo 12 mostra a implementação da classe ArithmeticProgression
.
ArithmeticProgression
link:code/17-it-generator/aritprog_v1.py[role=include]
-
__init__
exige dois argumentos:begin
estep
;end
é opcional, se forNone
, a série será ilimitada. -
Obtém o tipo somando
self.begin
eself.step
. Por exemplo, se um forint
e o outrofloat
, oresult_type
seráfloat
. -
Essa linha cria um
result
com o mesmo valor numérico deself.begin
, mas coagido para o tipo das somas subsequentes.[8] -
Para melhorar a legibilidade, o sinalizador
forever
seráTrue
se o atributoself.end
forNone
, resultando em uma série ilimitada. -
Esse loop roda
forever
ou até o resultado ser igual ou maior queself.end
. Quando esse loop termina, a função retorna. -
O
result
atual é produzido. -
O próximo resultado em potencial é calculado. Ele pode nunca ser produzido, se o loop
while
terminar.
Na última linha do Exemplo 12,
em vez de somar self.step
ao result
anterior a cada passagem do loop,
optei por ignorar o result
existente: cada novo result
é criado somando self.begin
a self.step
multiplicado por index
.
Isso evita o efeito cumulativo de erros após a adição sucessiva de números de ponto flutuante.
Alguns experimentos simples tornam clara a diferença:
>>> 100 * 1.1
110.00000000000001
>>> sum(1.1 for _ in range(100))
109.99999999999982
>>> 1000 * 1.1
1100.0
>>> sum(1.1 for _ in range(1000))
1100.0000000000086
A classe ArithmeticProgression
do Exemplo 12 funciona como esperado, é outro exemplo do uso de uma função geradora para implementar o método especial __iter__
.
Entretanto, se o único objetivo de uma classe é criar um gerador pela implementação de __iter__
,
podemos substituir a classe por uma função geradora. Pois afinal, uma função geradora é uma fábrica de geradores.
O Exemplo 13 mostra uma função geradora chamada aritprog_gen
, que realiza a mesma tarefa da ArithmeticProgression
, mas com menos código. Se, em vez de chamar ArithmeticProgression
, você chamar aritprog_gen
, os testes no Exemplo 11 são todos bem sucedidos.[9]
aritprog_gen
link:code/17-it-generator/aritprog_v2.py[role=include]
O Exemplo 13 é elegante, mas lembre-se sempre: há muitos geradores prontos para uso na biblioteca padrão, e a próxima seção vai mostrar uma implementação mais curta, usando o módulo itertools
.
O módulo itertools
no Python 3.10 contém 20 funções geradoras, que podem ser combinadas de várias maneiras interessantes.
Por exemplo, a função itertools.count
devolve um gerador que produz números. Sem argumentos, ele produz uma série de inteiros começando de 0
. Mas você pode fornecer os valores opcionais start
e step
, para obter um resultado similar ao das nossas funções aritprog_gen
:
>>> import itertools
>>> gen = itertools.count(1, .5)
>>> next(gen)
1
>>> next(gen)
1.5
>>> next(gen)
2.0
>>> next(gen)
2.5
Warning
|
|
Por outro lado, temos também a função itertools.takewhile
: ela devolve um gerador que consome outro gerador e para quando um dado predicado é avaliado como False
. Então podemos combinar os dois e escrever o seguinte:
>>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
>>> list(gen)
[1, 1.5, 2.0, 2.5]
Se valendo de takewhile
e count
, o Exemplo 14 é ainda mais conciso.
aritprog_gen
anterioreslink:code/17-it-generator/aritprog_v3.py[role=include]
Observe que aritprog_gen
no Exemplo 14 não é uma função geradora: não há um yield
em seu corpo.
Mas ela devolve um gerador, exatamente como faz uma função geradora.
Entretanto, lembre-se que itertools.count
soma o step
repetidamente,
então a série de números de ponto flutuante que ela produz não é tão precisa quanto a do Exemplo 13.
O importante no Exemplo 14 é: ao implementar geradoras, olhe o que já está disponível na biblioteca padrão, caso contrário você tem uma boa chance de reinventar a roda. Por isso a próxima seção trata de várias funções geradoras prontas para usar.
A biblioteca padrão oferece muitas geradoras, desde objetos de arquivo de texto forncendo iteração linha por linha até a incrível função os.walk
, que produz nomes de arquivos enquanto cruza uma árvore de diretórios, tornando buscas recursivas no sistema de arquivos tão simples quanto um loop for
.
A função geradora os.walk
é impressionante, mas nesta seção quero me concentrar em funções genéricas que recebem iteráveis arbitrários como argumento e devolvem geradores que produzem itens selecionados, calculados ou reordenados. Nas tabelas a seguir, resumi duas dúzias delas, algumas embutidas, outras dos módulos itertools
e functools
. Por conveniência, elas estão agrupadas por sua funcionalidade de alto nível, independente de onde são definidas.
O primeiro grupo contém funções geradoras de filtragem: elas produzem um subconjunto dos itens produzidos pelo iterável de entrada, sem mudar os itens em si. Como takewhile
, a maioria das funções listadas na Tabela 1 recebe um predicate
, uma função booleana de um argumento que será aplicada a cada item no iterável de entrada, para determinar se aquele item será incluído na saída.
Módulo | Função | Descrição |
---|---|---|
|
|
Consome dois iteráveis em paralelo; produz itens de |
|
|
Consome |
(Embutida) |
|
Aplica |
|
|
Igual a |
|
|
Produz itens de uma fatia de |
|
|
Produz itens enquanto |
A seção de console no Exemplo 15 demonstra o uso de todas as funções na Tabela 1.
>>> def vowel(c):
... return c.lower() in 'aeiou'
...
>>> list(filter(vowel, 'Aardvark'))
['A', 'a', 'a']
>>> import itertools
>>> list(itertools.filterfalse(vowel, 'Aardvark'))
['r', 'd', 'v', 'r', 'k']
>>> list(itertools.dropwhile(vowel, 'Aardvark'))
['r', 'd', 'v', 'a', 'r', 'k']
>>> list(itertools.takewhile(vowel, 'Aardvark'))
['A', 'a']
>>> list(itertools.compress('Aardvark', (1, 0, 1, 1, 0, 1)))
['A', 'r', 'd', 'a']
>>> list(itertools.islice('Aardvark', 4))
['A', 'a', 'r', 'd']
>>> list(itertools.islice('Aardvark', 4, 7))
['v', 'a', 'r']
>>> list(itertools.islice('Aardvark', 1, 7, 2))
['a', 'd', 'a']
O grupo seguinte contém os geradores de mapeamento: eles produzem itens computados a partir de cada item individual no iterável de entrada—ou iteráveis, nos casos de map
e starmap
.[10] As geradoras na Tabela 2 produzem um resultado por item dos iteráveis de entrada. Se a entrada vier de mais de um iterável, a saída para assim que o primeiro iterável de entrada for exaurido.
Módulo | Função | Descrição |
---|---|---|
|
|
Produz somas cumulativas; se |
(embutida) |
|
Produz tuplas de dois itens na forma |
(embutida) |
|
Aplica |
|
|
Aplica |
O Exemplo 16 demonstra alguns usos de itertools.accumulate
.
itertools.accumulate
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> import itertools
>>> list(itertools.accumulate(sample)) # (1)
[5, 9, 11, 19, 26, 32, 35, 35, 44, 45]
>>> list(itertools.accumulate(sample, min)) # (2)
[5, 4, 2, 2, 2, 2, 2, 0, 0, 0]
>>> list(itertools.accumulate(sample, max)) # (3)
[5, 5, 5, 8, 8, 8, 8, 8, 9, 9]
>>> import operator
>>> list(itertools.accumulate(sample, operator.mul)) # (4)
[5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0]
>>> list(itertools.accumulate(range(1, 11), operator.mul))
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] # (5)
-
Soma acumulada.
-
Mínimo corrente.
-
Máximo corrente.
-
Produto acumulado.
-
Fatoriais de
1!
a10!
.
As funções restantes da Tabela 2 são demonstradas no Exemplo 17.
>>> list(enumerate('albatroz', 1)) # (1)
[(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')]
>>> import operator
>>> list(map(operator.mul, range(11), range(11))) # (2)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> list(map(operator.mul, range(11), [2, 4, 8])) # (3)
[0, 4, 16]
>>> list(map(lambda a, b: (a, b), range(11), [2, 4, 8])) # (4)
[(0, 2), (1, 4), (2, 8)]
>>> import itertools
>>> list(itertools.starmap(operator.mul, enumerate('albatroz', 1))) # (5)
['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> list(itertools.starmap(lambda a, b: b / a,
... enumerate(itertools.accumulate(sample), 1))) # (6)
[5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333,
5.0, 4.375, 4.888888888888889, 4.5]
-
Número de letras na palavra, começando por
1
. -
Os quadrados dos inteiros de
0
a10
. -
Multiplicando os números de dois iteráveis em paralelo; os resultados cessam quando o iterável menor termina.
-
Isso é o que faz a função embutida
zip
. -
Repete cada letra na palavra de acordo com a posição da letra na palavra, começando por
1
. -
Média corrente.
A seguir temos o grupo de geradores de fusão—todos eles produzem itens a partir de múltiplos iteráveis de entrada. chain
e chain.from_iterable
consomem os iteráveis de entrada em sequência (um após o outro), enquanto product
, zip
, e zip_longest
consomem os iteráveis de entrada em paralelo. Veja a Tabela 3.
Módulo | Função | Descrição |
---|---|---|
|
|
Produz todos os itens de |
|
|
Produz todos os itens de cada iterável produzido por |
|
|
Produto cartesiano: produz tuplas de N elementos criadas combinando itens de cada iterável de entrada, como loops |
(embutida) |
|
Produz tuplas de N elementos criadas a partir de itens obtidos dos iteráveis em paralelo, terminando silenciosamente quando o menor iterável é exaurido, a menos que |
|
|
Produz tuplas de N elementos criadas a partir de itens obtidos dos iteráveis em paralelo, terminando apenas quando o último iterável for exaurido, preenchendo os itens ausentes com o |
O Exemplo 18 demonstra o uso das funções geradoras itertools.chain
e zip
, e de suas pares. Lembre-se que o nome da função zip
vem do zíper ou fecho-éclair (nenhuma relação com a compreensão de dados). Tanto zip
quanto itertools.zip_longest
foram apresentadas no [zip_box].
>>> list(itertools.chain('ABC', range(2))) # (1)
['A', 'B', 'C', 0, 1]
>>> list(itertools.chain(enumerate('ABC'))) # (2)
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> list(itertools.chain.from_iterable(enumerate('ABC'))) # (3)
[0, 'A', 1, 'B', 2, 'C']
>>> list(zip('ABC', range(5), [10, 20, 30, 40])) # (4)
[('A', 0, 10), ('B', 1, 20), ('C', 2, 30)]
>>> list(itertools.zip_longest('ABC', range(5))) # (5)
[('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
>>> list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # (6)
[('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
-
chain
é normalmente invocada com dois ou mais iteráveis. -
chain
não faz nada de útil se invocada com um único iterável. -
Mas
chain.from_iterable
pega cada item do iterável e os encadeia em sequência, desde que cada item seja também iterável. -
Qualquer número de iteráveis pode ser consumido em paralelo por
zip
, mas a geradora sempre para assim que o primeiro iterável acaba. No Python ≥ 3.10, se o argumentostrict=True
for passado e um iterável terminar antes dos outros, umValueError
é gerado. -
itertools.zip_longest
funciona comozip
, exceto por consumir todos os iteráveis de entrada, preenchendo as tuplas de saída comNone
onde necessário. -
O argumento nomeado
fillvalue
especifica um valor de preenchimento personalizado.
A geradora itertools.product
é uma forma preguiçosa para calcular produtos cartesianos, que criamos usando compreensões de lista com mais de uma instrução for
na [cartesian_product_sec]. Expressões geradoras com múltiplas instruções for
também podem ser usadas para produzir produtos cartesianos de forma preguiçosa. O Exemplo 19 demonstra itertools.product
.
itertools.product
>>> list(itertools.product('ABC', range(2))) # (1)
[('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]
>>> suits = 'spades hearts diamonds clubs'.split()
>>> list(itertools.product('AK', suits)) # (2)
[('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'),
('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')]
>>> list(itertools.product('ABC')) # (3)
[('A',), ('B',), ('C',)]
>>> list(itertools.product('ABC', repeat=2)) # (4)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'),
('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
>>> list(itertools.product(range(2), repeat=3))
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0),
(1, 0, 1), (1, 1, 0), (1, 1, 1)]
>>> rows = itertools.product('AB', range(2), repeat=2)
>>> for row in rows: print(row)
...
('A', 0, 'A', 0)
('A', 0, 'A', 1)
('A', 0, 'B', 0)
('A', 0, 'B', 1)
('A', 1, 'A', 0)
('A', 1, 'A', 1)
('A', 1, 'B', 0)
('A', 1, 'B', 1)
('B', 0, 'A', 0)
('B', 0, 'A', 1)
('B', 0, 'B', 0)
('B', 0, 'B', 1)
('B', 1, 'A', 0)
('B', 1, 'A', 1)
('B', 1, 'B', 0)
('B', 1, 'B', 1)
-
O produto cartesiano de uma
str
com três caracteres e umrange
com dois inteiros produz seis tuplas (porque3 * 2
é6
). -
O produto de duas cartas altas (
'AK'
) e quatro naipes é uma série de oito tuplas. -
Dado um único iterável,
product
produz uma série de tuplas de um elemento—muito pouco útil. -
O argumento nomeado
repeat=N
diz à função para consumir cada iterável de entradaN
vezes.
Algumas funções geradoras expandem a entrada, produzindo mais de um valor por item de entrada. Elas estão listadas na Tabela 4.
Module | Function | Description |
---|---|---|
|
|
Produz combinações de |
|
|
Produz combinações de |
|
|
Produz números começando em |
|
|
Produz itens de |
|
|
Produz pares sobrepostos sucessivos, obtidos do iterável de entrada[12] |
|
|
Produz permutações de |
|
|
Produz um dado item repetidamente e, a menos que um número de |
As funções count
e repeat
de itertools
devolvem geradores que conjuram itens do nada:
nenhum deles recebe um iterável como parâmetro.
Vimos itertools.count
na Progressão aritmética com itertools.
O gerador cycle
faz uma cópia do iterável de entrada e produz seus itens repetidamente.
O Exemplo 20 ilustra o uso de count
, cycle
, pairwise
e repeat
.
count
, cycle
, pairwise
, e repeat
>>> ct = itertools.count() # (1)
>>> next(ct) # (2)
0
>>> next(ct), next(ct), next(ct) # (3)
(1, 2, 3)
>>> list(itertools.islice(itertools.count(1, .3), 3)) # (4)
[1, 1.3, 1.6]
>>> cy = itertools.cycle('ABC') # (5)
>>> next(cy)
'A'
>>> list(itertools.islice(cy, 7)) # (6)
['B', 'C', 'A', 'B', 'C', 'A', 'B']
>>> list(itertools.pairwise(range(7))) # (7)
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]
>>> rp = itertools.repeat(7) # (8)
>>> next(rp), next(rp)
(7, 7)
>>> list(itertools.repeat(8, 4)) # (9)
[8, 8, 8, 8]
>>> list(map(operator.mul, range(11), itertools.repeat(5))) # (10)
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
-
Cria
ct
, uma geradoracount
. -
Obtém o primeiro item de
ct
. -
Não posso criar uma
list
a partir dect
, poisct
nunca para. Então pego os próximos três itens. -
Posso criar uma
list
de uma geradoracount
se ela for limitada porislice
outakewhile
. -
Cria uma geradora
cycle
a partir de'ABC'
, e obtém seu primeiro item,'A'
. -
Uma
list
só pode ser criada se limitada porislice
; os próximos sete itens são obtidos aqui. -
Para cada item na entrada,
pairwise
produz uma tupla de dois elementos com aquele item e o próximo—se existir um próximo item. Disponível no Python ≥ 3.10. -
Cria uma geradora
repeat
que vai produzir o número7
para sempre. -
Uma geradora
repeat
pode ser limitada passando o argumentotimes
: aqui o número8
será produzido4
vezes. -
Um uso comum de
repeat
: fornecer um argumento fixo emmap
; aqui ela fornece o multiplicador5
.
A funções geradoras combinations
, combinations_with_replacement
e permutations
--juntamente com product
—são chamadas geradoras combinatórias na página de documentação do itertools
.
Também há um relação muito próxima entre itertools.product
e o restante das funções combinatórias, como mostra o Exemplo 21.
>>> list(itertools.combinations('ABC', 2)) # (1)
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(itertools.combinations_with_replacement('ABC', 2)) # (2)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
>>> list(itertools.permutations('ABC', 2)) # (3)
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
>>> list(itertools.product('ABC', repeat=2)) # (4)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'),
('C', 'A'), ('C', 'B'), ('C', 'C')]
-
Todas as combinações com
len()==2
a partir dos itens em'ABC'
; a ordem dos itens nas tuplas geradas é irrelevante (elas poderiam ser conjuntos). -
Todas as combinação com
len()==2
a partir dos itens em'ABC'
, incluindo combinações com itens repetidos. -
Todas as permutações com
len()==2
a partir dos itens em'ABC'
; a ordem dos itens nas tuplas geradas é relevante. -
Produto cartesiano de
'ABC'
e'ABC'
(esse é o efeito derepeat=2
).
O último grupo de funções geradoras que vamos examinar nessa seção foram projetados para produzir todos os itens dos iteráveis de entrada, mas rearranjados de alguma forma. Aqui estão duas funções que devolvem múltiplos geradores: itertools.groupby
e itertools.tee
. A outra geradora nesse grupo, a função embutida reversed
, é a única geradora tratada nesse capítulo que não aceita qualquer iterável como entrada, apenas sequências. Faz sentido: como reversed
vai produzir os itens do último para o primeiro, só funciona com uma sequência de tamanho conhecido. Mas ela evita o custo de criar uma cópia invertida da sequência produzindo cada item quando necessário. Coloquei a função itertools.product
junto com as geradoras de fusão, na Tabela 3, porque todas aquelas consomem mais de um iterável, enquanto todas as geradoras na Tabela 5 aceitam no máximo um iterável como entrada.
Módulo | Função | Descrição |
---|---|---|
|
|
Produz tuplas de 2 elementos na forma |
(embutida) |
|
Produz os itens de |
|
|
Produz uma tupla de n geradores, cada um produzindo os itens do iterável de entrada de forma independente |
O Exemplo 22 demonstra o uso de itertools.groupby
e da função embutida reversed
. Observe que itertools.groupby
assume que o iterável de entrada está ordenado pelo critério de agrupamento, ou que pelo menos os itens estejam agrupados por aquele critério—mesmo que não estejam completamente ordenados.
O revisor técnico Miroslav Šedivý sugeriu esse caso de uso:
você pode ordenar objetos datetime
em ordem cronológica, e então groupby
por dia da semana, para obter o grupo com os dados de segunda-feira, seguidos pelos dados de terça, etc., e então da segunda (da semana seguinte) novamente, e assim por diante.
itertools.groupby
>>> list(itertools.groupby('LLLLAAGGG')) # (1)
[('L', <itertools._grouper object at 0x102227cc0>),
('A', <itertools._grouper object at 0x102227b38>),
('G', <itertools._grouper object at 0x102227b70>)]
>>> for char, group in itertools.groupby('LLLLAAAGG'): # (2)
... print(char, '->', list(group))
...
L -> ['L', 'L', 'L', 'L']
A -> ['A', 'A',]
G -> ['G', 'G', 'G']
>>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear',
... 'bat', 'dolphin', 'shark', 'lion']
>>> animals.sort(key=len) # (3)
>>> animals
['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark',
'giraffe', 'dolphin']
>>> for length, group in itertools.groupby(animals, len): # (4)
... print(length, '->', list(group))
...
3 -> ['rat', 'bat']
4 -> ['duck', 'bear', 'lion']
5 -> ['eagle', 'shark']
7 -> ['giraffe', 'dolphin']
>>> for length, group in itertools.groupby(reversed(animals), len): # (5)
... print(length, '->', list(group))
...
7 -> ['dolphin', 'giraffe']
5 -> ['shark', 'eagle']
4 -> ['lion', 'bear', 'duck']
3 -> ['bat', 'rat']
>>>
-
groupby
produz tuplas de(key, group_generator)
. -
Tratar geradoras
groupby
envolve iteração aninhada: neste caso, o loopfor
externo e o construtor delist
interno. -
Ordena
animals
por tamanho. -
Novamente, um loop sobre o par
key
egroup
, para exibirkey
e expandir ogroup
em umalist
. -
Aqui a geradora
reverse
itera sobreanimals
da direita para a esquerda.
A última das funções geradoras nesse grupo é iterator.tee
, que apresenta um comportamento singular: ela produz múltiplos geradores a partir de um único iterável de entrada, cada um deles produzindo todos os itens daquele iterável. Esse geradores podem ser consumidos de forma independente, como mostra o Exemplo 23.
itertools.tee
produz múltiplos geradores, cada um produzindo todos os itens do gerador de entrada>>> list(itertools.tee('ABC'))
[<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>]
>>> g1, g2 = itertools.tee('ABC')
>>> next(g1)
'A'
>>> next(g2)
'A'
>>> next(g2)
'B'
>>> list(g1)
['B', 'C']
>>> list(g2)
['C']
>>> list(zip(*itertools.tee('ABC')))
[('A', 'A'), ('B', 'B'), ('C', 'C')]
Observe que vários exemplos nesta seção usam combinações de funções geradoras. Essa é uma excelente característica dessas funções: como recebem como argumentos e devolvem geradores, elas podem ser combinadas de muitas formas diferentes.
Vamos agora revisar outro grupo de funções da biblioteca padrão que lidam com iteráveis.
Todas as funções na Tabela 6 recebem um iterável e devolvem um resultado único.
Elas são conhecidas como funções de "redução", "dobra" (folding) ou "acumulação".
Podemos implementar cada uma das funções embutidas listadas a seguir com functools.reduce
, mas elas existem embutidas por resolverem algums casos de uso comuns de forma mais fácil.
Já vimos uma explicação mais aprofundada sobre functools.reduce
na [multi_hashing].
Nos casos de all
e any
, há uma importante otimização não suportada por functools.reduce
: all
e any
conseguem criar um curto-circuito—isto é, elas param de consumir o iterador assim que o resultado esteja determinado.
Veja o último teste com any
no Exemplo 24.
Módulo | Função | Descrição |
---|---|---|
(embutida) |
|
Devolve |
(embutida) |
|
Devolve |
(embutida) |
|
Devolve o valor máximo entre os itens de |
(embutida) |
|
Devolve o valor mínimo entre os itens de |
|
|
Devolve o resultado da aplicação de |
(embutida) |
|
A soma de todos os itens em |
O Exemplo 24 exemplifica a operação de all
e de any
.
all
e any
para algumas sequências>>> all([1, 2, 3])
True
>>> all([1, 0, 3])
False
>>> all([])
True
>>> any([1, 2, 3])
True
>>> any([1, 0, 3])
True
>>> any([0, 0.0])
False
>>> any([])
False
>>> g = (n for n in [0, 0.0, 7, 8])
>>> any(g) # (1)
True
>>> next(g) # (2)
8
-
any
iterou sobreg
atég
produzir7
; neste momentoany
parou e devolveuTrue
. -
É por isso que
8
ainda restava.
Outra função embutida que recebe um iterável e devolve outra coisa é sorted
.
Diferente de reversed
, que é uma função geradora, sorted
cria e devolve uma nova list
.
Afinal, cada um dos itens no iterável de entrada precisa ser lido para que todos possam ser ordenados, e a ordenação acontece em uma list
; sorted
então apenas devolve aquela list
após terminar seu processamento.
Menciono sorted
aqui porque ela consome um iterável arbitrário.
Claro, sorted
e as funções de redução só funcionam com iteráveis que terminam em algum momento.
Caso contrário, eles seguirão coletando itens e nunca devolverão um resultado.
Note
|
Se você chegou até aqui, já viu o conteúdo mais importante e útil deste capítulo.
As seções restantes tratam de recursos avançados de geradores, que a maioria de nós não vê ou precisa com muita frequência, tal como a instrução Há também seções sobre dicas de tipo para iteráveis, iteradores e corrotinas clássicas. |
A sintaxe yield from
fornece uma nova forma de combinar geradores. É nosso próximo assunto.
A sintaxe da expressão yield from
foi introduzida no Python 3.3, para permitir que um gerador delegue tarefas a um subgerador.
Antes da introdução de yield from
, usávamos um loop for
quando um gerador precisava produzir valores de outro gerador:
>>> def sub_gen():
... yield 1.1
... yield 1.2
...
>>> def gen():
... yield 1
... for i in sub_gen():
... yield i
... yield 2
...
>>> for x in gen():
... print(x)
...
1
1.1
1.2
2
Podemos obter o mesmo resultado usando yield from
, como se vê no Exemplo 25.
yield from
>>> def sub_gen():
... yield 1.1
... yield 1.2
...
>>> def gen():
... yield 1
... yield from sub_gen()
... yield 2
...
>>> for x in gen():
... print(x)
...
1
1.1
1.2
2
No Exemplo 25, o loop for
é o código cliente,
gen
é o gerador delegante e sub_gen
é o subgerador.
Observe que yield from
suspende gen
, e sub_gen
toma o controle até se exaurir.
Os valores produzidos por sub_gen
passam através de gen
diretamente para o loop for
cliente.
Enquanto isso, gen
está suspenso e não pode ver os valores que passam por ele.
gen
continua apenas quando sub_gen
termina.
Quando o subgerador contém uma instrução return
com um valor, aquele valor pode ser capturado pelo gerador delegante, com o uso de yield from
como parte de uma expressão.
Veja a demonstração no Exemplo 26.
yield from
recebe o valor devolvido pelo subgerador>>> def sub_gen():
... yield 1.1
... yield 1.2
... return 'Done!'
...
>>> def gen():
... yield 1
... result = yield from sub_gen()
... print('<--', result)
... yield 2
...
>>> for x in gen():
... print(x)
...
1
1.1
1.2
<-- Done!
2
Agora que já vimos o básico sobre yield from
, vamos estudar alguns exemplos simples mas práticos de sua utilização.
Vimos na Tabela 3 que itertools
fornece uma geradora chain
, que produz itens a partir de vários iteráveis,
iterando sobre o primeiro, depois sobre o segundo, e assim por diante, até o último.
Abaixo está uma implementação caseira de chain
, com loops for
aninhados, em Python:[15]
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = 'ABC'
>>> r = range(3)
>>> list(chain(s, r))
['A', 'B', 'C', 0, 1, 2]
A geradora chain
, no código acima, está delegando para cada iterável it
, controlando cada it
no loop for
interno.
Aquele loop interno pode ser substituído por uma expressão yield from
, como mostra a seção de console a seguir:
>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
O uso de yield from
neste exemplo está correto, e o código é mais legível, mas parece açúcar sintático, com pouco ganho real.
Vamos então desenvolver um exemplo mais interessante.
Nessa seção, veremos yield from
em um script para percorrer uma estrutura de árvore.
Vou desenvolvê-lo bem devagar.
A estrutura de árvore nesse exemplo é a hierarquia das exceções do Python. Mas o padrão pode ser adaptado para exibir uma árvore de diretórios ou qualquer outra estrutura de árvore.
Começando de BaseException
no nível zero, a hierarquia de exceções tem cinco níveis de profundidade no Python 3.10. Nosso primeiro pequeno passo será exibir o nível zero.
Dada uma classe raiz, a geradora tree
no Exemplo 27 produz o nome dessa classe e para.
link:code/17-it-generator/tree/step0/tree.py[role=include]
A saída do Exemplo 27 tem apenas uma linha:
BaseException
O próximo pequeno passo nos leva ao nível 1.
A geradora tree
irá produzir o nome da classe raiz e os nomes de cada subclasse direta.
Os nomes das subclasses são indentados para explicitar a hierarquia.
Esta é a saída que queremos:
$ python3 tree.py
BaseException
Exception
GeneratorExit
SystemExit
KeyboardInterrupt
O Exemplo 28 produz a saída acima.
link:code/17-it-generator/tree/step1/tree.py[role=include]
-
Para suportar a saída indentada, produz o nome da classe e seu nível na hierarquia.
-
Usa o método especial
__subclasses__
para obter uma lista de subclasses. -
Produz o nome da subclasse e o nível (
1
). -
Cria a string de indentação de
4
espaços vezes olevel
. No nível zero, isso será uma string vazia.
No Exemplo 29, refatorei tree
para separar o caso especial da classes raiz de suas subclasses, que agora são processadas na geradora sub_tree
.
Em yield from
, a geradora tree
é suspensa, e sub_tree
passa a produzir valores.
tree
produz o nome da classe raiz, e entao delega para sub_tree
link:code/17-it-generator/tree/step2/tree.py[role=include]
-
Delega para
sub_tree
, para produzir os nomes das subclasses. -
Produz o nome de cada subclasse e o nível (
1
). Por causa doyield from sub_tree(cls)
dentro detree
, esses valores escapam completamente à geradoratree
… -
… e são recebidos aqui diretamente.
Seguindo com nosso método de pequenos passos, vou escrever o código mais simples que consigo imaginar para chegar ao nível 2.
Para percorrer uma árvore primeiro em produndidade (depth-first), após produzir cada nó do nível 1, quero produzir os filhotes daquele nó no nível 2 antes de voltar ao nível 1.
Um loop for
aninhado cuida disso, como no Exemplo 30.
sub_tree
percorre os níveis 1 e 2, primeiro em profundidadelink:code/17-it-generator/tree/step3/tree.py[role=include]
Este é o resultado da execução de step3/tree.py, do Exemplo 30:
$ python3 tree.py
BaseException
Exception
TypeError
StopAsyncIteration
StopIteration
ImportError
OSError
EOFError
RuntimeError
NameError
AttributeError
SyntaxError
LookupError
ValueError
AssertionError
ArithmeticError
SystemError
ReferenceError
MemoryError
BufferError
Warning
GeneratorExit
SystemExit
KeyboardInterrupt
Você pode já ter percebido para onde isso segue, mas vou insistir mais uma vez nos pequenos passos:
vamos atingir o nível 3, acrescentando ainda outro loop for
aninhado.
Não há qualquer alteração no restante do programa, então o Exemplo 31 mostra apenas a geradora sub_tree
.
sub_tree
de tree/step4/tree.pylink:code/17-it-generator/tree/step4/tree.py[role=include]
Há um padrão claro no Exemplo 31.
Entramos em um loop for
para obter as subclasses do nível N.
A cada passagem do loop, produzimos uma subclasse do nível N, e então iniciamos outro loop for
para visitar o nível N+1.
Na Reinventando chain, vimos como é possível substituir um loop for
aninhado controlando uma geradora com yield from
sobre a mesma geradora.
Podemos aplicar aquela ideia aqui, se fizermos sub_tree
aceitar um parâmetro level
, usando yield from
recursivamente e
passando a subclasse atual como nova classe raiz com o número do nível seguinte.
Veja o Exemplo 32.
sub_tree
recursiva vai tão longe quanto a memória permitirlink:code/17-it-generator/tree/step5/tree.py[role=include]
O Exemplo 32 pode percorrer árvores de qualquer profundidade, limitado apenas pelo limite de recursão do Python. O limite default permite 1.000 funções pendentes.
Qualquer bom tutorial sobre recursão enfatizará a importância de ter um caso base,
para evitar uma recursão infinita.
Um caso base é um ramo condicional que retorna sem fazer uma chamada recursiva.
O caso base é frequentemente implementado com uma instrução if
.
No Exemplo 32, sub_tree
não tem um if
,
mas há uma condicional implícita no loop for
:
Se cls.subclasses()
devolver uma lista vazia, o corpo do loop não é executado,
e assim a chamada recursiva não ocorre.
O caso base ocorre quando a classe cls
não tem subclasses.
Nesse caso, sub_tree
não produz nada, apenas retorna.
O Exemplo 32 funciona como planejado,
mas podemos fazê-la mais concisa recordando do padrão que observamos quando
alcançamos o nível 3 (no Exemplo 31):
produzimos uma subclasse de nível N, e então iniciamos um loop for
aninhado para visitar o nível N+1.
No Exemplo 32, substituímos o loop aninhado por yield from
.
Agora podemos fundir tree
e sub_tree
em uma única geradora.
O Exemplo 33 é o último passo deste exemplo.
tree
passam um argumento level
incrementadolink:code/17-it-generator/tree/step6/tree.py[role=include]
No início da Subgeradoras com yield from, vimos como yield from
conecta a subgeradora diretamente ao código cliente, escapando da geradora delegante.
Aquela conexão se torna realmente importante quando geradoras são usadas como corrotinas, e não apenas produzem mas também consomem valores do código cliente, como veremos na Corrotinas clássicas.
Após esse primeiro encontro com yield from
, vamos olhar as dicas de tipo para iteráveis e iteradores.
A bilbioteca padrão do Python contém muitas funções que aceitam argumentos iteráveis.
Em seu código, tais funções podem ser anotadas como a função zip_replace
, vista no [replacer_ex],
usando collections.abc.Iterable
(ou typing.Iterable
, se você precisa suporta o Python 3.8 ou anterior, como explicado no [legacy_deprecated_typing_box]). Veja o Exemplo 34.
link:code/08-def-type-hints/replacer.py[role=include]
-
Define um apelido (alias) de tipo; isso não é obrigatório, mas torna a próxima dica de tipo mais legível. Desde o Python 3.10,
FromTo
deve ter uma dica de tipo detyping.TypeAlias
, para esclarecer a razão para essa linha:FromTo: TypeAlias = tuple[str, str]
. -
Anota
changes
para aceitar umIterable
de tuplasFromTo
.
Tipos Iterator
não aparecem com a mesma frequência de tipos Iterable
, mas eles também são simples de escrever.
O Exemplo 35 mostra a conhecida geradora Fibonacci, anotada.
fibonacci
devolve um gerador de inteiroslink:code/17-it-generator/fibo_gen.py[role=include]
Observe que o tipo Iterator
é usado para geradoras programadas como funções com yield
,
bem como para iteradores escritos "a mão", como classes que implementam __next__
.
Há também o tipo collections.abc.Generator
(e o decontinuado typing.Generator
correspondente)
que podemos usar para anotar objetos geradores,
mas ele é verboso e redundane para geradoras usadas como iteradores,
que não recebem valores via .send()
.
O Exemplo 36, quando verificado com o Mypy, revela que o tipo Iterator
é, na verdade, um caso especial simplificado do tipo Generator
.
link:code/17-it-generator/iter_gen_type.py[role=include]
-
Uma expressão geradora que produz palavras reservadas do Python com menos de
5
caracteres. -
O Mypy infere:
typing.Generator[builtins.str*, None, None]
.[16] -
Isso também produz strings, mas acrescentei uma dica de tipo explícita.
-
Tipo revelado:
typing.Iterator[builtins.str]
.
abc.Iterator[str]
é consistente-com abc.Generator[str, None, None]
,
assim o Mypy não reporta erros na verificação de tipos no Exemplo 36.
Iterator[T]
é um atalho para Generator[T, None, None]
.
Ambas as anotações significam "uma geradora que produz itens do tipo T
, mas não consome ou devolve valores."
Geradoras capazes de consumir e devolver valores são corrotinas, nosso próximo tópico.
Note
|
A PEP 342—Coroutines via Enhanced Generators (Corrotinas via geradoras aprimoradas) introduziu Após o lançamento do Python 3.5, a tendência é usar "corrotina" como sinônimo de "corrotina nativa". Mas a PEP 342 não está descontinuada, e as corrotinas clássicas ainda funcionam como originalmente projetadas, apesar de não serem mais suportadas por |
Entender as corrotinas clássicas no Python é mais confuso porque elas são, na verdade, geradoras usadas de uma forma diferente. Vamos então dar um passo atrás e examinar outro recurso do Python que pode ser usado de duas maneiras.
Vimos na [tuples_more_than_lists_sec] que é possível usar instâncias de tuple
como registros ou como sequências imutáveis.
Quando usadas como um registro, se espera que uma tupla tenha um número específico de itens, e cada item pode ter um tipo diferente.
Quando usadas como listas imutáveis, uma tupla pode ter qualquer tamanho, e se espera que todos os itens sejam do mesmo tipo.
Por essa razão, há duas formas de anotar tuplas com dicas de tipo:
# Um registro de cidade, como nome, país e população:
city: tuple[str, str, int]
# Uma sequência imutável de nomes de domínios:
domains: tuple[str, ...]
Algo similar ocorre com geradoras.
Elas normalmente são usadas como iteradores, mas podem também ser usadas como corrotinas.
Na verdade, corrotina é uma função geradora, criada com a palavra-chave yield
em seu corpo.
E um objeto corrotina é um objeto gerador, fisicamente.
Apesar de compartilharem a mesma implementação subjacente em C, os casos de uso de geradoras e corrotinas em Python são tão diferentes que há duas formas de escrever dicas de tipo para elas:
# A variável `readings` pode ser delimitada a um iterador
# ou a um objeto gerador que produz itens `float`:
readings: Iterator[float]
# A variável `sim_taxi` pode ser delimitada a uma corrotina
# representando um táxi em uma simulação de eventos discretos.
# Ela produz eventos, recebe um `float` de data/hora, e devolve
# o número de viagens realizadas durante a simulação:
sim_taxi: Generator[Event, float, int]
Para aumentar a confusão, os autores do módulo typing
decidiram nomear
aquele tipo Generator
, quando ele de fato descreve a API de um
objeto gerador projetado para ser usado como uma corrotina,
enquanto geradoras são mais frequentemente usadas como iteradores simples.
A
documentação do módulo typing
(EN) descreve assim os parâmetros de tipo formais de Generator
:
Generator[YieldType, SendType, ReturnType]
O SendType
só é relevante quando a geradora é usada como uma corrotina.
Aquele parâmetro de tipo é o tipo de x
na chamada gen.send(x)
.
É um erro invocar .send()
em uma geradora escrita para se comportar como um iterador em vez de uma corrotina.
Da mesma forma, ReturnType
só faz sentido para anotar uma corrotina,
pois iteradores não devolvem valores como funções regulares.
A única operação razoável em uma geradora usada como um iterador é
invocar next(it)
direta ou indiretamente, via loops for
e
outras formas de iteração. O YieldType
é o tipo do valor
devolvido em uma chamada a next(it)
.
O tipo Generator
tem os mesmo parâmetros de tipo de typing.Coroutine
:
Coroutine[YieldType, SendType, ReturnType]
A
documentação de typing.Coroutine
diz literalmente:
"A variância e a ordem das variáveis de tipo correspondem às de Generator
."
Mas typing.Coroutine
(descontinuada)
e collections.abc.Coroutine
(genérica a partir do Python 3.9)
foram projetadas para anotar apenas corrotinas nativas, e não corrotinas clássicas.
Se você quiser usar dicas de tipo com corrotinas clássicas,
vai sofrer com a confusão advinda de anotá-las como
Generator[YieldType, SendType, ReturnType]
.
David Beazley criou algumas das melhores palestras e algumas das oficinas mais abrangentes sobre corrotinas clássicas. No material de seu curso na PyCon 2009 há um slide chamado "Keeping It Straight" (Cada Coisa em Seu Lugar), onde se lê:
Geradoras produzem dados para iteração
Corrotinas são consumidoras de dados
Para evitar que seu cérebro exploda, não misture os dois conceitos
Corrotinas não tem relação com iteração
Nota: Há uma forma de fazer
yield
produzir um valor em uma corrotina, mas isso não está ligado à iteração.[17]
Vamos ver agora como as corrotinas clássicas funcionam.
Quando discutimos clausuras no [closures_and_decorators],
estudamos objetos para computar uma média móvel.
O [ex_average_oo] mostra uma classe e o [ex_average_fixed] apresenta uma função de ordem superior devolvendo uma função que mantem as variáveis total
e count
entre invocações, em uma clausura.
O Exemplo 37 mostra como fazer o mesmo com uma corrotina.[18]
link:code/17-it-generator/coroaverager.py[role=include]
-
Essa função devolve uma geradora que produz valores
float
, aceita valoresfloat
via.send()
, e não devolve um valor útil.[19] -
Esse loop infinito significa que a corrotina continuará produzindo médias enquanto o código cliente enviar valores.
-
O comando
yield
aqui suspende a corrotina, produz um resultado para o cliente e—mais tarde—recebe um valor enviado pelo código de invocação para a corrotina, iniciando outra iteração do loop infinito.
Em uma corrotina, total
e count
podem ser variáveis locais:
atributos de instância ou uma clausura não são necessários para manter o contexto
enquanto a corrotina está suspensa, esperando pelo próximo .send()
.
Por isso as corrotinas são substitutas atraentes para callbacks em programação assíncrona—elas mantêm o estado local entre ativações.
O Exemplo 38 executa doctests mostrando a corrotina averager
em operação.
link:code/17-it-generator/coroaverager.py[role=include]
-
Cria o objeto corrotina.
-
Inicializa a corrotina. Isso produz o valor inicial de
average
: 0.0. -
Agora estamos conversando: cada chamada a
.send()
produz a média atual.
No Exemplo 38, a chamada next(coro_avg)
faz a corrotina avançar até o yield
, produzindo o valor inicial de average
.
Também é possível inicializar a corrotina chamando coro_avg.send(None)
—na verdade é isso que a função embutida next()
faz.
Mas você não pode enviar qualquer valor diferente de None
,
pois a corrotina só pode aceitar um valor enviado quando está suspensa, em uma linha de yield
.
Invocar next()
ou .send(None)
para avançar até o primeiro yield
é conhecido como "preparar (priming) a corrotina".
Após cada ativação, a corrotina é suspensa exatamente na palavra-chave yield
, e espera que um valor seja enviado.
A linha coro_avg.send(10)
fornece aquele valor, ativando a corrotina.
A expressão yield
se resolve para o valor 10, que é atribuído à variável term
.
O restante do loop atualiza as variáveis total
, count
, e average
.
A próxima iteração no loop while
produz average
, e a corrotina é novamente suspensa na palavra-chave yield
.
O leitor atento pode estar ansioso para saber como a execução de uma instância de averager
(por exemplo, coro_avg
) pode ser encerrada, pois seu corpo é um loop infinito.
Em geral, não precisamos encerrar uma geradora, pois ela será coletada como lixo assim que não existirem mais referências válidas para ela.
Se for necessário encerrá-la explicitamente, use o método .close()
, como mostra o Exemplo 39.
link:code/17-it-generator/coroaverager.py[role=include]
-
coro_avg
é a instância criada no Exemplo 38. -
O método
.close()
gera uma exceçãoGeneratorExit
na expressãoyield
suspensa. Se não for tratada na função corrotina, a exceção a encerra.GeneratorExit
é capturada pelo objeto gerador que encapsula a corrotina—por isso não a vemos. -
Invocar
.close()
em uma corrotina previamente encerrada não tem efeito. -
Tentar usar
.send()
em uma corrotina encerrada gera umaStopIteration
.
Além do método .send()
, a
PEP 342—Coroutines via Enhanced Generators (Corrotinas via geradoras aprimoradas) também introduziu uma forma de uma corrotina devolver um valor.
A próxima seção mostra como fazer isso.
Vamos agora estudar outra corrotina para computar uma média. Essa versão não vai produzir resultados parciais. Em vez disso, ela devolve uma tupla com o número de termos e a média. Dividi a listagem em duas partes, no Exemplo 40 e no Exemplo 41.
link:code/17-it-generator/coroaverager2.py[role=include]
-
A corrotina
averager2
no Exemplo 41 vai devolver uma instância deResult
. -
Result
é, na verdade, uma subclasse detuple
, que tem um método.count()
, que não preciso aqui. O comentário# type: ignore
evita que o Mypy reclame sobre a existência do campocount
.[20] -
Uma classe para criar um valor sentinela com um
__repr__
legível. -
O valor sentinela que vou usar para fazer a corrotina parar de coletar dados e devolver uma resultado.
-
Vou usar esse apelido de tipo para o segundo parâmetro de tipo devolvido pela corrotina
Generator
, o parâmetroSendType
.
A definição de SendType
também funciona no Python 3.10 mas, se não for necessário suportar versões mais antigas, é melhor escrever a anotação assim, após importar TypeAlias
de typing
:
SendType: TypeAlias = float | Sentinel
Usar |
em vez de typing.Union
é tão conciso e legível que eu provavelmente não criaria aquele apelido de tipo. Em vez disso, escreveria a assinatura de averager2
assim:
def averager2(verbose: bool=False) -> Generator[None, float | Sentinel, Result]:
Vamos agora estudar o código da corrotina em si (no Exemplo 41).
link:code/17-it-generator/coroaverager2.py[role=include]
-
Para essa corrotina, o tipo produzido é
None
, porque ela não produz dados. Ela recebe dados do tipoSendType
e devolve uma tuplaResult
quando termina o processamento. -
Usar
yield
assim só faz sentido em corrotinas, que são projetadas para consumir dados. Isso produzNone
, mas recebe umterm
de.send(term)
. -
Se
term
é umSentinel
, sai do loop. Graças a essa verificação comisinstance
… -
…Mypy me permite somar
term
atotal
sem sinalizar um erro (que eu não poderia somar umfloat
a um objeto que pode ser umfloat
ou umSentinel
). -
Essa linha só será alcançada de um
Sentinel
for enviado para a corrotina.
Vamos ver agora como podemos usar essa corrotina, começando por um exemplo simples, que sequer produz um resultado (no Exemplo 42).
.cancel()
link:code/17-it-generator/coroaverager2.py[role=include]
-
Lembre-se que
averager2
não produz resultados parciais. Ela produzNone
, que o console do Python omite. -
Invocar
.close()
nessa corrotina a faz parar, mas não devolve um resultado, pois a exceçãoGeneratorExit
é gerada na linhayield
da corrotina, então a instruçãoreturn
nunca é alcançada.
Vamos então fazê-la funcionar, no Exemplo 43.
StopIteration
com um Result
link:code/17-it-generator/coroaverager2.py[role=include]
-
Enviar o valor sentinela
STOP
faz a corrotina sair do loop e devolver umResult
. O objeto gerador que encapsula a corrotina gera então umaStopIteration
. -
A instância de
StopIteration
tem um atributovalue
vinculado ao valor do comandoreturn
que encerrou a corrotina. -
Acredite se quiser!
Essa ideia de "contrabandear" o valor devolvido para fora de uma corrotina dentro de uma exceção StopIteration
é um truque bizarro. Entretanto, esse truque é parte da
PEP 342—Coroutines via Enhanced Generators (Corrotinas via geradoras aprimoradas) (EN), e está documentada com a exceção StopIteration
e na seção "Expressões yield"
do capítulo 6 de
A Referência da Linguagem Python.
Uma geradora delegante pode obter o valor devolvido por uma corrotina diretamente, usando a sintaxe
yield from
, como demonstrado no Exemplo 44.
StopIteration
com um Result
link:code/17-it-generator/coroaverager2.py[role=include]
-
res
vai coletar o valor devolvido poraverager2
; o mecanismo deyield from
recupera o valor devolvido quando trata a exceçãoStopIteration
, que marca o encerramento da corrotina. QuandoTrue
, o parâmetroverbose
faz a corrotina exibir o valor recebido, tornando sua operação visível. -
Preste atenção na saída desta linha quando a geradora for executada.
-
Devolve o resultado. Isso também estará encapsulado em
StopIteration
. -
Cria o objeto corrotina delegante.
-
Esse loop vai controlar a corrotina delegante.
-
O primeiro valor enviado é
None
, para preparar a corrotina; o último é a sentinela, para pará-la. -
Captura
StopIteration
para obter o valor devolvido porcompute
. -
Após as linhas exibidas por
averager2
ecompute
, recebemos a instância deResult
.
Mesmo com esses exemplos aqui, que não fazem muita coisa, o código é difícil de entender.
Controlar a corrotina com chamadas .send()
e recuperar os resultados é complicado, exceto com yield from
—mas só podemos usar essa sintaxe dentro de uma geradora/corrotina,
que no fim precisa ser controlada por algum código não-trivial, como mostra o Exemplo 44.
Os exemplos anteriores mostram que o uso direto de corrotinas é incômodo e confuso.
Acrescente o tratamento de exceções e o método de corrotina .throw()
, e os exemplos ficam ainda mais complicados.
Não vou tratar de .throw()
nesse livro porque—como .send()
—ele só é útil para controlar corrotinas "manualmente", e não recomendo fazer isso, a menos que você esteja criando um novo framework baseado em corrotinas do zero .
Note
|
Se você estiver interessado em um tratamento mais aprofundado de corrotinas clássicas—incluindo o método |
Na prática, realizar trabalho produtivo com corrotinas exige o suporte de um framework especializada.
É isso que asyncio
oferecia para corrotinas clássicas lá atrás, no Python 3.3.
Com o advento das corrotinas nativas no Python 3.5, os desenvolvedores principais do Python estão gradualmente eliminando o suporte a corrotinas clássicas no asyncio
.
Mas os mecanismos subjacentes são muito similares.
A sintaxe async def
torna a corrotinas nativas mais fáceis de identificar no código,
um grande benefício por si só.
Internamente, as corrotinas nativas usam await
em vez de yield from
para delegar a outras corrotinas.
O [async_ch] é todo sobre esse assunto.
Vamos agora encerrar o capítulo com uma seção alucinante sobre co-variância e contra-variância em dicas de tipo para corrotinas.
Anteriomente, na [contravariant_types_sec],
mencionei typing.Generator
como um dos poucos tipos da biblioteca padrão com um parâmetro de tipo contra-variante.
Agora que estudamos as corrotinas clássicas, estamos prontos para entender esse tipo genérico.
T_co = TypeVar('T_co', covariant=True)
V_co = TypeVar('V_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
# muitas linhas omitidas
class Generator(Iterator[T_co], Generic[T_co, T_contra, V_co],
extra=_G_base):
Essa declaração de tipo genérico significa que uma dica de tipo de Generator
requer aqueles três parâmetros de tipo que vimos antes:
my_coro : Generator[YieldType, SendType, ReturnType]
Pelas variáveis de tipo nos parâmetros formais, vemos que YieldType
e ReturnType
são covariantes, mas SendType
é contra-variante.
Para entender a razão disso, considere que YieldType
e ReturnType
são tipos de "saída".
Ambos descrevem dados que saem do objeto corrotina—isto é, o objeto gerador quando usado como um objeto corrotina..
Faz sentido que esses parâmetros sejam covariantes, pois qualquer código esperando uma corrotina que produz números de ponto flutuante pode usar uma corrotina que produz inteiros.
Por isso Generator
é covariante em seu parâmetro YieldType
.
O mesmo raciocínio se aplica ao parâmetro ReturnType
—também covariante.
Usando a notação introduzida na [covariant_types_sec], a covariância do primeiro e do terceiro parâmetros pode ser expressa pelos símbolos :>
apontando para a mesma direção:
float :> int
Generator[float, Any, float] :> Generator[int, Any, int]
YieldType
e ReturnType
são exemplos da primeira regra apresentada na [variance_rules_sec]:
Se um parâmetro de tipo formal define um tipo para dados que saem de um objeto, ele pode ser covariante.
Por outro lado, SendType
é um parâmetro de "entrada":
ele é o tipo do argumento value
para o método .send(value)
do objeto corrotina.
Código cliente que precise enviar números de ponto flutuante para uma corrotina não consegue usar uma corrotina que receba int
como o SendType
, porque float
não é um subtipo de int
.
Em outras palavras, float
não é consistente-com int
.
Mas o cliente pode usar uma corrotina que tenha complex
como SendType
,
pois float
é um subtipo de complex
,
e portanto float
é consistente-com complex
.
A notação :>
torna visível a contra-variância do segundo parâmetro:
float :> int
Generator[Any, float, Any] <: Generator[Any, int, Any]
Este é um exemplo da segunda regra geral da variância:
Se um parâmetro de tipo formal define um tipo para dados que entram em um objeto após sua construção inicial, ele pode ser contra-variante.
Essa alegre discussão sobre variância encerra o capítulo mais longo do livro.
A iteração está integrada tão profundamente à linguagem que eu gosto de dizer que o Python groks iteradores[22] A integração do padrão Iterator na semântica do Python é um exemplo perfeito de como padrões de projeto não são aplicáveis a todas as linguagens de programação. No Python, um Iterator clássico, implementado "à mão", como no Exemplo 4, não tem qualquer função prática, exceto como exemplo didático.
Neste capítulo, criamos algumas versões de uma classe para iterar sobre palavras individuais em arquivos de texto (que podem ser muito grandes).
Vimos como o Python usa a função embutida iter()
para criar iteradores a partir de objetos similares a sequências.
Criamos um iterador clássico como uma classe com __next__()
, e então usamos geradoras,
para tornar cada refatoração sucessiva da classe Sentence
mais concisa e legível.
Daí criamos uma geradora de progressões aritméticas, e mostramos como usar o módulo itertools
para torná-la mais simples.
A isso se seguiu uma revisão da maioria das funções geradoras de uso geral na biblioteca padrão.
A seguir estudamos expressões yield from
no contexto de geradoras simples, com os exemplos chain
e tree
.
A última seção de nota foi sobre corrotinas clássicas, um tópico de importância decrescente após a introducão das corrotinas nativas, no Python 3.5.
Apesar de difíceis de usar na prática, corrotinas clássicas são os alicerces das corrotinas nativas, e a expressão yield from
é uma precursora direta de await
.
Dicas de tipo para os tipos Iterable
, Iterator
, e Generator
também foram abordadas—com esse último oferecendo um raro exemplo concreto de um parâmetro de tipo contra-variante.
Uma explicação técnica detalhada sobre geradoras aparece na A Referência da Linguagem Python, em "6.2.9. Expressões yield". A PEP onde as funções geradoras foram definidas é a PEP 255—Simple Generators (Geradoras Simples).
A documentação do módulo itertools
é excelente, especialmente por todos os exemplos incluídos. Apesar das funções daquele módulo serem implementadas em C, a documentação mostra como algumas delas poderiam ser escritas em Python, frequentemente se valendo de outras funções no módulo. Os exemplos de utilização também são ótimos; por exemplo, há um trecho mostrando como usar a função accumulate
para amortizar um empréstimo com juros, dada uma lista de pagamentos ao longo do tempo. Há também a seção "Receitas com itertools", com funções adicionais de alto desempenho, usando as funções de itertools
como base.
Além da bilbioteca padrão do Python, recomendo o pacote More Itertools, que continua a bela tradição do itertools
, oferecendo geradoras poderosas, acompanhadas de muitos exemplos e várias receitas úteis.
"Iterators and Generators" (Iteradores e Geradoras), o capítulo 4 de Python Cookbook, 3ª ed., de David Beazley e Brian K. Jones (O’Reilly), traz 16 receitas sobre o assunto, de muitos ângulos diferentes, concentradas em aplicações práticas. O capítulo contém algumas receitas esclarecedoras com yield from
.
Sebastian Rittau—atualmente um dos principais colaboradores do typeshed—explica porque iteradores devem ser iteráveis. Ele observou, em 2006, que "Java: Iterators are not Iterable" (Java:Iteradores não são Iteráveis).
A sintaxe de yield from
é explicada, com exemplos, na seção "What’s New in Python 3.3" (Novidades no Python 3.3) da PEP 380—Syntax for Delegating to a Subgenerator (Sintaxe para Delegar para um Subgerador). Meu artigo
"Classic Coroutines" (Corrotinas Clássicas) (EN)
no fluentpython.com explica yield from
em profundidade, incluindo pseudo-código em Python de sua implementação (em C).
David Beazley é a autoridade final sobre geradoras e corrotinas no Python. O Python Cookbook, 3ª ed., (O’Reilly), que ele escreveu com Brian Jones, traz inúmeras receitas com corrotinas. Os tutoriais de Beazley sobre esse tópico nas PyCon são famosos por sua profundidade e abrangência. O primeiro foi na PyCon US 2008: "Generator Tricks for Systems Programmers" (Truques com Geradoras para Programadores de Sistemas) (EN). A PyCon US 2009 assisitiu ao lendário "A Curious Course on Coroutines and Concurrency" (Um Curioso Curso sobre Corrotinas e Concorrência) (EN) (links de vídeo difíceis de encontrar para todas as três partes: parte 1, parte 2, e parte 3). Seu tutorial na PyCon 2014 em Montreal foi "Generators: The Final Frontier" (Geradoras: A Fronteira Final), onde ele apresenta mais exemplos de concorrência—então é, na verdade, mais relacionado aos tópicos do [async_ch]. Dave não consegue deixar de explodir cérebros em suas aulas, então, na última parte de "A Fronteira Final", corrotinas substituem o padrão clássico Visitor em um analisador de expressões aritméticas.
Corrotinas permitem organizar o código de novas maneiras e, assim como a recursão e o polimorfismo (despacho dinâmico), demora um certo tempo para se acostumar com suas possibilidades. Um exemplo interessante de um algoritmo clássico reescrito com corrotinas aparece no post "Greedy algorithm with coroutines" (O Algoritmo guloso com corrotinas), de James Powell.
O Effective Python, 1ª ed. (Addison-Wesley), de Brett Slatkin, tem um excelente capítulo curto chamado "Consider Coroutines to Run Many Functions Concurrently" (Considere as Corrotinas para Executar Muitas Funções de Forma Concorrente).
Esse capítulo não aparece na segunda edição de Effective Python, mas ainda está disponível online como um capítulo de amostra (EN).
Slatkin apresenta o melhor exemplo que já vi do controle de corrotinas com yield from
:
uma implementaçào do Jogo da Vida, de John Conway, no qual corrotinas gerenciam o estado de cada célula conforme o jogo avança.
Refatorei o código do exemplo do Jogo da Vida—separando funções e classes que implementam o jogo dos trechos de teste no código original de Slatkin.
Também reescrevi os testes como doctests, então você pode ver o resultados de várias corrotinas e classes sem executar o script. The exemplo refatorado está publicado como um GitHub gist.
A interface Iterador minimalista do Python
Na seção "Implementação" do padrão Iterator,[23], a Guange dos Quatro escreveu:
A interface mínima de Iterator consiste das operações First, Next, IsDone, e CurrentItem.
Entretanto, essa mesma frase tem uma nota de rodapé, onde se lê:
Podemos tornar essa interface ainda menor, fundindo Next, IsDone, e CurrentItem em uma única operação que avança para o próximo objeto e o devolve. Se a travessia estiver encerrada, essa operação daí devolve um valor especial (0, por exemplo), que marca o final da iteração.
Isso é próximo do que temos em Python: um único método __next__
, faz o serviço. Mas em vez de uma sentinela, que poderia passar desapercebida por enganoo ou distração, a exceção StopIteration
sinaliza o final da iteração. Simples e correto: esse é o jeito do Python.
Geradoras conectáveis
Qualquer um que gerencie grandes conjuntos de dados encontra muitos usos para geradoras. Essa é a história da primeira vez que criei uma solução prática baseada em geradoras.
Muitos anos atrás, eu trabalhava na BIREME, uma biblioteca digital operada pela OPAS/OMS (Organização Pan-Americana da Saúde/Organização Mundial da Saúde) em São Paulo, Brasil. Entre os conjuntos de dados bibliográficos criados pela BIREME estão o LILACS (Literatura Latino-Americana e do Caribe em Ciências da Saúde) and SciELO (Scientific Electronic Library Online), dois bancos de dados abrangentes, indexando a literatura de pesquisa em ciências da saúde produzida na região.
Desde o final dos anos 1980, o sistema de banco de dados usado para gerenciar o LILACS é o CDS/ISIS, um banco de dados não-relacional de documentos, criado pela UNESCO. Uma de minhas tarefas era pesquisar alternativas para uma possível migração do LILACS—e depois do SciELO, muito maior—para um banco de dados de documentos moderno e de código aberto, tal como o CouchDB ou o MongoDB. Naquela época escrevi um artigo explicando o modelo de dados semi-estruturado e as diferentes formas de representar dados CDS/ISIS com registros do tipo JSON: "From ISIS to CouchDB: Databases and Data Models for Bibliographic Records" (Do ISIS ao CouchDBL Bancos de Dados e Modelos de Dados para Registros Bibliográficos) (EN).
Como parte daquela pesquisa, escrevi um script Python para ler um arquivo CDS/ISIS e escrever um arquivo JSON adequado para importação pelo CouchDB ou pelo MongoDB. Inicialmente, o arquivo lia arquivos no formato ISO-2709, exportados pelo CDS/ISIS. A leitura e a escrita tinham de ser feitas de forma incremental, pois os conjuntos de dados completos eram muito maiores que a memória principal. Isso era bastante fácil: cada iteração do loop for
principal lia um registro do arquivo .iso, o manipulava e escrevia no arquivo de saída .json.
Entretanto, por razões operacionais, foi considerado necessário que o isis2json.py suportasse outro formato de dados do CDS/ISIS: os arquivos binários .mst, usados em produção na BIREME—para evitar uma exportação dispendiosa para ISO-2709. Agora eu tinha um problema: as bibliotecas usadas para ler arquivos ISO-2709 e .mst tinham APIs muito diferentes. E o loop de escrita JSON já era complicado, pois o script aceitava, na linha de comando, muitas opções para reestruturar cada registro de saída. Ler dados usando duas APIs diferentes no mesmo loop for
onde o JSON era produzido seria muito difícil de manejar.
A solução foi isolar a lógica de leitura em um par de funções geradoras: uma para cada formato de entrada suportado. No fim, dividi o script isis2json.py em quatro funções. Você pode ver o código-fonte em Python 2, com suas dependências, no repositório fluentpython/isis2json no GitHub.[24]
Aqui está uma visão geral em alto nível de como o script está estruturado:
main
-
A função
main
usaargparse
para ler opções de linha de comando que configuram a estrutura dos registros de saída. Baseado na extensão do nome do arquivo de entrada, uma função geradora é selecionada para ler os dados e produzir os registros, um por vez. iter_iso_records
-
Essa função geradora lê arquivos .iso (que se presume estarem no formato ISO-2709). Ela aceita dois argumento: o nome do arquivo e
isis_json_type
, uma das opções relacionadas à estrutura do registro. Cada iteração de seu loopfor
lê um registro, cria umdict
vazio, o preenche com dados dos campos, e produz odict
. iter_mst_records
-
Essa outra função geradora lê arquivos .mst.[25] Se você examinar o código-fonte de isis2json.py, vai notar que ela não é tão simples quanto
iter_iso_records
, mas sua interface e estrutura geral é a mesma: a função recebe como argumentos um nome de arquivo e umisis_json_type
, e entra em um loopfor
, que cria e produz por iteração umdict
, representando um único registro. write_json
-
Essa função executa a escrita efetiva de registros JSON, um por vez. Ela recebe numerosos argumentos, mas o primeiro—input_gen—é uma referência para uma função geradora:
iter_iso_records
ouiter_mst_records
. O loopfor
principal itera sobre os dicionários produzidos pela geradorainput_gen
selecionada, os reestrutura de diferentes formas, determinadas pelas opções de linha de comando, e anexa o registro JSON ao arquivo de saída.
Me aproveitando das funções geradoras, pude dissociar a leitura da escrita. Claro, a maneira mais simples de dissociar as duas operações seria ler todos os registros para a memória e então escrevê-los no disco. Mas essa não era uma opção viável, pelo tamanho dos conjuntos de dados. Usando geradoras, a leitura e a escrita são intercaladas, então o script pode processar arquivos de qualquer tamanho. Além disso, a lógica especial para ler um registro em formatos de entrada diferentes está isolada da lógica de reestruturação de cada registro para escrita.
Agora, se precisarmos que isis2json.py suporte um formato de entrada adicional—digamos, MARCXML, uma DTD[26] usada pela Biblioteca do Congresso norte-americano para representar dados ISO-2709—será fácil acrescentar uma terceira função geradora para implementar a lógica de leitura, sem mudar nada na complexa função write_json
.
Não é ciência de foguete, mas é um exemplo real onde as geradoras permitiram um solução eficiente e flexível para processar bancos de dados como um fluxo de registros, mantendo o uso de memória baixo e independente do tamanho do conjunto de dados.
return iter(self.words)
. Ele está certo: o resultado da invocação de self.words.iter()
também seria um iterador, como deve ser. Entretanto, usei um loop for
com yield
aqui para introduzir a sintaxe de uma função geradora, que exige a instrução yield
, como veremos na próxima seção. Durante a revisão da segunda edição deste livro, Leonardo Rochael sugeriu ainda outro atalho para o corpo de __iter__
: yield from self.words
. Também vamos falar de yield from
mais adiante neste mesmo capítulo.
gen
ao nomear funções geradoras, mas essa não é uma prática comum. E claro que não é possível fazer isso ao implementar um iterável: o método especial obrigatório deve se chamar __iter__
.
coerce()
, mas ela não existe mais no Python 3. Foi considerada desnecessária, pois as regras de coerção numérica estão implícitas nos métodos dos operadores aritméticos. Então, a melhor forma que pude imaginar para forçar o valor inicial para o mesmo tipo do restante da série foi realizar a adição e usar seu tipo para converter o resultado. Perguntei sobre isso na Python-list e recebi uma excelente resposta de Steven D’Aprano (EN).
map
.
strict
é novo, surgiu no Python 3.10. Quando strict=True
, um ValueError
é gerado se qualquer iterável tiver um tamanho diferente. O default é False
, para manter a compatibilidade retroativa.
itertools.pairwise
foi introduzido no Python 3.10.
max(arg1, arg2, …, [key=?])
, devolvendo então o valor máximo entre os argumentos passados.
min(arg1, arg2, …, [key=?])
, devolvendo então o valor mínimo entre os argumentos passados.
typing
.
None
quanto typing.NoReturn
como parâmetro de tipo devolvido pela geradora—mas ele também aceita str
naquela posição, então aparentemente o Mypy não consegue, neste momento, analisar completamente o código da corrotina.
count
é o melhor nome para a variável local na corrotina, e é o nome que usei para essa variável em exemplos similares ao longo do livro, então faz sentido usar o mesmo nome no campo de Result
. Não hesitei em usar # type: ignore
para evitar as limitações e os aborrecimentos de verificadores de tipo estáticos, quando se submeteer à ferramenta tornaria o código pior ou desnecessariamente complicado.
typing.Generator
e outros tipos que correspondem a ABCs em`collections.abc` foram refatorados e encapsulads em torno da ABC correspondente, então seus parâmetros genéricos não são visíveis no código-fonte de typing.py . Por isso estou fazendo referência ao código-fonte do Python 3.6 aqui.