Na prática, quais são os principais usos da nova sintaxe "yield from" no Python 3.3?

407

Estou tendo dificuldade para envolver meu cérebro em torno do PEP 380 .

  1. Quais são as situações em que a "produção de" é útil?
  2. Qual é o caso de uso clássico?
  3. Por que é comparado com micro-threads?

[atualização]

Agora eu entendo a causa das minhas dificuldades. Eu usei geradores, mas nunca usei corotinas (introduzidas pelo PEP-342 ). Apesar de algumas semelhanças, geradores e corotinas são basicamente dois conceitos diferentes. Entender as corotinas (não apenas os geradores) é a chave para entender a nova sintaxe.

As rotinas IMHO são o recurso Python mais obscuro ; a maioria dos livros faz com que pareça inútil e desinteressante.

Obrigado pelas ótimas respostas, mas agradecimentos especiais ao agf e seu comentário, relacionados às apresentações de David Beazley . David balança.

Paulo Scardine
fonte
7
Vídeo da apresentação de dabeaz.com/coroutines de David Beazley : youtube.com/watch?v=Z_OAlIhXziw
jcugat 15/09/16

Respostas:

571

Vamos tirar uma coisa do caminho primeiro. A explicação que yield from gé equivalente for v in g: yield v nem começa a fazer justiça ao que yield fromse trata. Porque, convenhamos, se tudo o que yield fromfaz é expandir o forloop, ele não garante a adição yield fromà linguagem e impede que um monte de novos recursos sejam implementados no Python 2.x.

O que yield fromfaz é estabelecer uma conexão bidirecional transparente entre o chamador e o subgerador :

  • A conexão é "transparente" no sentido de que também propagará tudo corretamente, não apenas os elementos que estão sendo gerados (por exemplo, as exceções são propagadas).

  • A conexão é "bidirecional" no sentido de que os dados podem ser enviados de e para um gerador.

( Se estivéssemos falando sobre o TCP, yield from gpode significar "agora desconecte temporariamente o soquete do meu cliente e reconecte-o a esse outro soquete do servidor". )

BTW, se você não sabe ao certo o que significa enviar dados para um gerador , você precisa descartar tudo e ler primeiro sobre as corotinas - elas são muito úteis (contrastam com as sub - rotinas ), mas infelizmente são menos conhecidas no Python. O curioso curso de Dave Beazley sobre corotinas é um excelente começo. Leia os slides 24-33 para obter uma cartilha rápida.

Lendo dados de um gerador usando rendimento de

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

Em vez de iterar manualmente reader(), podemos justamente yield from.

def reader_wrapper(g):
    yield from g

Isso funciona e eliminamos uma linha de código. E provavelmente a intenção é um pouco mais clara (ou não). Mas nada mudou a vida.

Enviando dados para um gerador (corotina) usando o rendimento de - Parte 1

Agora vamos fazer algo mais interessante. Vamos criar uma corotina chamada writerque aceita dados enviados a ele e grava em um soquete, fd, etc.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

Agora, a pergunta é: como a função do wrapper deve lidar com o envio de dados para o gravador, para que quaisquer dados enviados ao wrapper sejam enviados de forma transparente ao writer()?

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

O wrapper precisa aceitar os dados que são enviados para ele (obviamente) e também deve manipular o StopIterationquando o loop for estiver esgotado. Evidentemente, apenas fazer for x in coro: yield xnão serve. Aqui está uma versão que funciona.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

Ou, nós poderíamos fazer isso.

def writer_wrapper(coro):
    yield from coro

Isso economiza 6 linhas de código, torna muito mais legível e simplesmente funciona. Magia!

Enviando dados para um rendimento de gerador de - Parte 2 - Tratamento de exceções

Vamos torná-lo mais complicado. E se nosso escritor precisar lidar com exceções? Digamos que as writeralças SpamExceptionae ***sejam impressas se encontrarem uma.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

E se não mudarmos writer_wrapper? Funciona? Vamos tentar

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

Hum, não está funcionando porque x = (yield)apenas gera a exceção e tudo chega a um impasse. Vamos fazê-lo funcionar, mas manipulando exceções manualmente e enviando-as ou lançando-as no subgerador ( writer)

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

Isso funciona.

# Result
>>  0
>>  1
>>  2
***
>>  4

Mas o mesmo acontece!

def writer_wrapper(coro):
    yield from coro

O yield frommanipula de forma transparente o envio ou lançamento de valores no subgerador.

Isso ainda não cobre todos os casos de canto. O que acontece se o gerador externo estiver fechado? E o caso em que o subgerador retorna um valor (sim, no Python 3.3+, os geradores podem retornar valores), como o valor retornado deve ser propagado? Que yield fromlida com transparência em todos os casos de canto é realmente impressionante . yield fromfunciona magicamente e lida com todos esses casos.

Pessoalmente, considero yield fromuma má escolha de palavra-chave porque não torna aparente a natureza bidirecional . Foram propostas outras palavras-chave (como delegateforam rejeitadas, porque adicionar uma nova palavra-chave ao idioma é muito mais difícil do que combinar as existentes.

Em resumo, é melhor pensar yield fromcomo um transparent two way channelentre o chamador eo sub-gerador.

Referências:

  1. PEP 380 - Sintaxe para delegar a um subgerador (Ewing) [v3.3, 13-02-2009]
  2. PEP 342 - Corotinas através de geradores aprimorados (GvR, Eby) [v2.5, 10-05-2005]
Praveen Gollakota
fonte
3
@PraveenGollakota, na segunda parte da sua pergunta, Enviando dados para um gerador (corotina) usando o rendimento da - Parte 1 , e se você tiver mais do que corotinas para encaminhar o item recebido? Como um cenário de emissora ou assinante, em que você fornece várias corotinas ao invólucro em seu exemplo e os itens devem ser enviados para todos ou para um subconjunto deles?
21768 Kevin Ghaboosi #
3
@PraveenGollakota, parabéns pela ótima resposta. Os pequenos exemplos permitem-me experimentar as coisas em repl. O link para o curso de Dave Beazley foi um bônus!
Bigyan
11
fazer except StopIteration: passINSIDE o while True:loop não é uma representação precisa de yield from coro- que não é um loop infinito e depois de coroesgotado (ou seja, gera StopIteration), writer_wrapperexecutará a próxima instrução. Após a última declaração que vai-se auto-raise StopIterationcomo qualquer gerador exausta ...
Aprillion
11
... por isso, se writercontido for _ in range(4)em vez de while True, em seguida, após a impressão >> 3que também auto-raise StopIteratione este seria por cabo de auto yield frome, em seguida, writer_wrapperseria auto-raise é própria StopIteratione porque wrap.send(i)não está dentro trydo bloco, seria realmente levantou neste ponto ( ie traceback só irá relatar a linha com wrap.send(i), e não algo de dentro do gerador)
Aprillion
3
Ao ler " nem sequer começa a fazer justiça ", sei que cheguei à resposta certa. Obrigado pela ótima explicação!
Hot.PxL
89

Quais são as situações em que a "produção de" é útil?

Toda situação em que você tem um loop como este:

for x in subgenerator:
  yield x

Como o PEP descreve, esta é uma tentativa bastante ingênua de usar o subgerador, faltam vários aspectos, especialmente o manuseio adequado dos mecanismos .throw()/ .send()/ .close()introduzidos pelo PEP 342 . Para fazer isso corretamente, é necessário um código bastante complicado .

Qual é o caso de uso clássico?

Considere que você deseja extrair informações de uma estrutura de dados recursiva. Digamos que queremos obter todos os nós das folhas em uma árvore:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Ainda mais importante é o fato de que até o momento yield fromnão havia um método simples de refatorar o código do gerador. Suponha que você tenha um gerador (sem sentido) como este:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Agora você decide fatorar esses loops em geradores separados. Sem yield from, isso é feio, até o ponto em que você pensará duas vezes se realmente deseja fazê-lo. Com yield from, é realmente bom olhar para:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Por que é comparado com micro-threads?

Acho que o que esta seção do PEP está falando é que todo gerador tem seu próprio contexto de execução isolado. Juntamente com o fato de a execução ser alternada entre o gerador-iterador e o chamador usando yielde __next__(), respectivamente, isso é semelhante aos encadeamentos, em que o sistema operacional alterna o encadeamento em execução de tempos em tempos, juntamente com o contexto de execução (pilha, registradores, ...)

O efeito disso também é comparável: o gerador-iterador e o chamador progridem em seu estado de execução ao mesmo tempo, suas execuções são intercaladas. Por exemplo, se o gerador fizer algum tipo de cálculo e o chamador imprimir os resultados, você os verá assim que estiverem disponíveis. Esta é uma forma de simultaneidade.

yield fromPorém, essa analogia não é nada específico - é uma propriedade geral dos geradores em Python.

Niklas B.
fonte
Hoje, a refatoração de geradores é dolorosa .
21412 Josh Lee
11
Costumo usar muito as ferramentas para refatorar geradores (coisas como itertools.chain), não é grande coisa. Gosto de ceder, mas ainda não vejo como é revolucionário. Provavelmente é, já que Guido é louco por isso, mas devo estar perdendo o quadro geral. Eu acho que é ótimo para send (), pois isso é difícil de refatorar, mas eu não uso isso com bastante frequência.
e-satis
Suponho que get_list_values_as_xxxsejam geradores simples com uma única linha for x in input_param: yield int(x)e os outros dois respectivamente com strefloat
madtyn
@NiklasB. re "extrair informações de uma estrutura de dados recursiva". Estou apenas entrando no Py para obter dados. Você poderia dar uma facada nesse Q ?
22419 alancalvitti
33

Onde quer que você invocar um gerador de dentro de um gerador que você precisa de uma "bomba" de re yieldos valores: for v in inner_generator: yield v. Como o PEP aponta, há complexidades sutis a isso que a maioria das pessoas ignora. Controle de fluxo não local como throw()é um exemplo dado no PEP. A nova sintaxe yield from inner_generatoré usada onde quer que você tenha escrito o forloop explícito antes. Porém, não é apenas um açúcar sintático: ele lida com todos os casos de canto que são ignorados pelo forloop. Ser "açucarado" encoraja as pessoas a usá-lo e, assim, obter os comportamentos certos.

Esta mensagem no tópico de discussão fala sobre essas complexidades:

Com os recursos adicionais do gerador introduzidos pelo PEP 342, esse não é mais o caso: conforme descrito no PEP de Greg, a iteração simples não suporta send () e throw () corretamente. A ginástica necessária para oferecer suporte a send () e throw () na verdade não é tão complexa quando você as divide, mas também não é trivial.

Não posso falar de uma comparação com micro-threads, exceto observar que os geradores são um tipo de paralelismo. Você pode considerar o gerador suspenso como um encadeamento que envia valores yieldpara um encadeamento consumidor. A implementação real pode ser nada parecida com isso (e a implementação real é obviamente de grande interesse para os desenvolvedores do Python), mas isso não diz respeito aos usuários.

A nova yield fromsintaxe não adiciona nenhum recurso adicional ao idioma em termos de encadeamento, apenas facilita o uso correto dos recursos existentes. Ou, mais precisamente, torna mais fácil para um consumidor iniciante de um gerador interno complexo escrito por um especialista passar por esse gerador sem interromper nenhum de seus complexos recursos.

Ben Jackson
fonte
23

Um pequeno exemplo o ajudará a entender um dos yield fromcasos de uso: obter valor de outro gerador

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))
ospider
fonte
2
Só queria sugerir que a impressão no final ficaria um agradável pouco sem a conversão para uma lista -print(*flatten([1, [2], [3, [4]]]))
yoniLavi
6

yield from basicamente interage iteradores de maneira eficiente:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Como você pode ver, ele remove um loop Python puro. Isso é praticamente tudo o que faz, mas encadear iteradores é um padrão bastante comum no Python.

Threads são basicamente um recurso que permite que você pule de funções em pontos completamente aleatórios e volte ao estado de outra função. O supervisor de encadeamento faz isso com muita frequência, portanto, o programa parece executar todas essas funções ao mesmo tempo. O problema é que os pontos são aleatórios; portanto, é necessário usar o bloqueio para impedir que o supervisor interrompa a função em um ponto problemático.

Os geradores são bem parecidos com os threads nesse sentido: eles permitem que você especifique pontos específicos (sempre que eles yield) onde você pode entrar e sair. Quando usados ​​dessa maneira, os geradores são chamados de corotinas.

Leia estes excelentes tutoriais sobre corotinas em Python para obter mais detalhes

Jochen Ritzel
fonte
10
Essa resposta é enganosa porque elimina o recurso destacado de "yield from", como mencionado acima: suporte para send () e throw ().
26713 Justin W
2
@ Justin W: Eu acho que tudo o que você leu antes é realmente enganador, porque você não entendeu que throw()/send()/close()são yieldrecursos que yield fromobviamente precisam ser implementados corretamente, pois é para simplificar o código. Tais trivialidades não têm nada a ver com o uso.
Jochen Ritzel
5
Você está contestando a resposta de Ben Jackson acima? Minha leitura da sua resposta é que é essencialmente o açúcar sintático que segue a transformação do código que você forneceu. A resposta de Ben Jackson refuta especificamente essa afirmação.
26713 Justin W
@JochenRitzel Você nunca precisa escrever sua própria chainfunção, porque itertools.chainjá existe. Use yield from itertools.chain(*iters).
Acumenus
4

No uso aplicado para a corotina de E / S assíncrona , yield fromtem um comportamento semelhante awaitao de uma função de corotina . Ambos são usados ​​para suspender a execução da corotina.

Para o Asyncio, se não houver necessidade de oferecer suporte a uma versão mais antiga do Python (ou seja,> 3.5), async def/ awaité a sintaxe recomendada para definir uma corotina. Assim, yield fromnão é mais necessário em uma rotina.

Mas no exterior geral da asyncio, yield from <sub-generator>ainda tem algum outro uso na iteração do sub-gerador como mencionado na resposta anterior.

Yeo
fonte
1

Esse código define uma função fixed_sum_digitsretornando um gerador enumerando todos os seis dígitos, de forma que a soma dos dígitos seja 20.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Tente escrever sem yield from. Se você encontrar uma maneira eficaz de fazer isso, me avise.

Eu acho que para casos como este: visitar árvores, yield fromtorna o código mais simples e mais limpo.

jimifiki
fonte
0

Simplificando, yield fromfornece recursão final para as funções do iterador.

DomQ
fonte