yield from – O que é? Entendendo geradores em Python

yield from

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.

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

Aproveite para acompanhar o Ciência Programada nas redes sociais.

Gostou desse artigo? Ele faz parte do Python Drops, 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!

Compartilhe:

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima