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 from
se trata. Porque, convenhamos, se tudo o que yield from
faz é expandir o for
loop, 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 from
faz é 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 g
pode 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 writer
que 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 StopIteration
quando o loop for estiver esgotado. Evidentemente, apenas fazer for x in coro: yield x
nã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 writer
alças SpamException
ae ***
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 from
manipula 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 from
lida com transparência em todos os casos de canto é realmente impressionante . yield from
funciona magicamente e lida com todos esses casos.
Pessoalmente, considero yield from
uma má escolha de palavra-chave porque não torna aparente a natureza bidirecional . Foram propostas outras palavras-chave (como delegate
foram rejeitadas, porque adicionar uma nova palavra-chave ao idioma é muito mais difícil do que combinar as existentes.
Em resumo, é melhor pensar yield from
como um transparent two way channel
entre o chamador eo sub-gerador.
Referências:
- PEP 380 - Sintaxe para delegar a um subgerador (Ewing) [v3.3, 13-02-2009]
- PEP 342 - Corotinas através de geradores aprimorados (GvR, Eby) [v2.5, 10-05-2005]