Você já viu o termo yield from em algum código Python e ficou imaginando o que
era? Nesse artigo vamos nos aprofundar ainda mais em geradores e entender, com
exemplos, o que significa o yield from e como podemos utilizá-lo para deixar
nossos códigos ainda mais eficientes.
Origem #
No artigo sobre
geradores
vimos que a origem dos mesmos foi no contexto de corrotinas, mas que são
aplicáveis em diversos outros contextos, de forma que nem o conhecimento sobre
corrotinas é necessário para aplicá-los. O yield from foi oficializado na PEP
380, ainda de forma bastante
técnica, voltado para esse contexto específico. Mas vamos pegar trechos dessa
PEP e trazer para mais perto de exemplos simples.
O resumo da PEP 380 traz o seguinte texto:
A syntax is proposed for a generator to delegate part of its operations to another generator. This allows a section of code containing ‘yield’ to be factored out and placed in another generator. Additionally, the subgenerator is allowed to return with a value, and the value is made available to the delegating generator. The new syntax also opens up some opportunities for optimisation when one generator re-yields values produced by another.
O ponto chave é o escrito na primeira frase: um gerador delegando parte de suas operações para outro gerador.
Outra citação é na documentação de expressões da linguagem, onde temos:
When
yield from <expr>is used, it treats the supplied expression as a subiterator. (…) the supplied expression must be an iterable.
Ou seja, podemos converter um iterável em iterador e gerar a partir dele. Interessante. E, caso precise lembrar dos conceitos, veja esse artigo sobre iteradores e iteráveis com diversos exemplos.
Vamos ver como aplicar o apresentado em alguns exemplos simples.
Exemplos simples #
Recentemente escrevi sobre sequências
infinitas
e no referido artigo vimos o uso do método islice. Vamos usar todo o
conhecimento adquirido aqui.
Consumindo de dois geradores #
Considere que temos dois geradores de sequências infinitas, um para inteiros positivos pares e outro para inteiros positivos ímpares:
from itertools import islice
def pares_positivos():
valor = 0
while True:
yield valor
valor += 2
def impares_positivos():
valor = 1
while True:
yield valor
valor += 2
Suponha agora que queremos um gerador de inteiros positivos, sem discriminar se par ou ímpar. Podemos aproveitar os geradores já criados:
def inteiros_positivos():
for par, impar in zip(pares_positivos(), impares_positivos()):
yield par
yield impar
Vamos verificar os 10 primeiros valores gerados:
gen_int = inteiros_positivos()
tuple(islice(gen_int, 10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
Funcionou… mas como? Vamos começar vendo a documentação da função zip:
help(zip)
Help on class zip in module builtins:
class zip(object)
| zip(*iterables) --> A zip object yielding tuples until an input is exhausted.
|
| >>> list(zip('abcdefg', range(3), range(4)))
| [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
|
| The zip object yields n-length tuples, where n is the number of iterables
| passed as positional arguments to zip(). The i-th element in every tuple
| comes from the i-th iterable argument to zip(). This continues until the
| shortest argument is exhausted.
|
| Methods defined here:
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __iter__(self, /)
| Implement iter(self).
|
| __next__(self, /)
| Implement next(self).
|
| __reduce__(...)
| Return state information for pickling.
|
| ----------------------------------------------------------------------
| Static methods defined here:
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
Veja que zip recebe
iteráveis
e gera tuplas até que esses iteráveis sejam consumidos. Assim:
zip(pares_positivos(), impares_positivos())
<zip at 0x7f25b857f300>
next(zip(pares_positivos(), impares_positivos()))
(0, 1)
Vejamos os 5 primeiros pares gerados:
tuple(islice(zip(pares_positivos(), impares_positivos()), 5))
((0, 1), (2, 3), (4, 5), (6, 7), (8, 9))
Então, basicamente o loop for no corpo do gerador passa por cada item de cada
tupla, disponibilizando-o (yield) para chamadas ao gerador.
Agora, se a função zip gera tuplas, e tuplas são iteráveis, podemos passar
essas tuplas para yield from segundo a documentação vista anteriormente.
Assim, podemos redefinir nosso gerador como:
def inteiros_positivos():
for tupla in zip(pares_positivos(), impares_positivos()):
yield from tupla
gen_int = inteiros_positivos()
tuple(islice(gen_int, 10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
Bem simples e sucinto.
Consumindo de outro gerador #
Vamos agora entender melhor a parte da PEP que fala sobre delegar parte das operações para um gerador. Vamos criar um gerador de inteiros positivos divisíveis por 10. Sabemos que para ser divisível por 10, um número deve terminar em 0 e, portanto, é um número par. Assim, faz sentido usar o gerador já criado de números pares e gerar apenas os divíveis por 10 sob demanda:
def inteiros_positivos_divisiveis_por_dez():
par = pares_positivos()
yield from (p for p in par if (p % 10 == 0))
Verifique que criamos uma expressão
geradora
e estamos gerando a partir dela com yield from.
gen_dez = inteiros_positivos_divisiveis_por_dez()
tuple(islice(gen_dez, 10))
(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
“Achatando” listas de listas #
Considere que você tem uma lista de listas contendo números e gostaria de gerar estes números como se fossem pertencentes a apenas uma lista. Esse processo se chama flatten em inglês, algo como achatar as sublistas na lista principal.
lista_de_sublistas = [[1, 2, 3], [4, 5, 6], [7], [8, 9]]
Uma forma de escrever um gerador seria:
def flatten(lista):
for sublista in lista:
for item in sublista:
yield item
gen = flatten(lista_de_sublistas)
tuple(gen)
(1, 2, 3, 4, 5, 6, 7, 8, 9)
Agora, cada sublista é um iterável e, portanto, podemos passar diretamente para
yield from:
def flatten(lista):
for sublista in lista:
yield from sublista
gen = flatten(lista_de_sublistas)
tuple(gen)
(1, 2, 3, 4, 5, 6, 7, 8, 9)
Esse achatamento tem diversas aplicações reais e não necessariamente essa
abordagem é a mais eficiente, leia mais
aqui.
Mas é uma boa forma de enxergar mais aplicações de yield from.
Conclusões #
Curtiu conhecer mais sobre geradores? É uma forma bem eficiente de lidar com diversas situações onde não é possível, ou não é saudável do ponto de vista de recursos, armazenar previamente todos os valores.
Gostou desse artigo? Ele faz parte do Drops de programação, um conjunto de posts mais curtos voltados para fundamentos falando sobre alguns aspectos da linguagem Python e de programação em geral. Você pode ler mais desses artigos buscando a tag “drops” aqui no site.
Até a próxima!