[英]Python `yield from`, or return a generator?
我寫了這段簡單的代碼:
def mymap(func, *seq):
return (func(*args) for args in zip(*seq))
我應該使用上面的“return”語句返回一個生成器,還是使用這樣的“yield from”指令:
def mymap(func, *seq):
yield from (func(*args) for args in zip(*seq))
除了“回報”和“收益”之間的技術差異之外,一般情況下哪種方法更好?
不同之處在於您的第一個mymap
只是一個普通函數,在這種情況下是一個返回生成器的工廠。 調用該函數后,主體內的所有內容都會立即執行。
def gen_factory(func, seq):
"""Generator factory returning a generator."""
# do stuff ... immediately when factory gets called
print("build generator & return")
return (func(*args) for args in seq)
第二個mymap
也是一個工廠,但它本身也是一個生成器,從內部自建的子生成器產生。 因為它本身就是一個生成器,所以直到第一次調用 next(generator) 時才會開始執行主體。
def gen_generator(func, seq):
"""Generator yielding from sub-generator inside."""
# do stuff ... first time when 'next' gets called
print("build generator & yield")
yield from (func(*args) for args in seq)
我認為下面的例子會更清楚。 我們定義了數據包,這些數據包應該用函數處理,捆綁在我們傳遞給生成器的作業中。
def add(a, b):
return a + b
def sqrt(a):
return a ** 0.5
data1 = [*zip(range(1, 5))] # [(1,), (2,), (3,), (4,)]
data2 = [(2, 1), (3, 1), (4, 1), (5, 1)]
job1 = (sqrt, data1)
job2 = (add, data2)
現在我們在像 IPython 這樣的交互式 shell 中運行以下代碼來查看不同的行為。 gen_factory
立即打印出來,而gen_generator
僅在next()
被調用后才打印出來。
gen_fac = gen_factory(*job1)
# build generator & return <-- printed immediately
next(gen_fac) # start
# Out: 1.0
[*gen_fac] # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]
gen_gen = gen_generator(*job1)
next(gen_gen) # start
# build generator & yield <-- printed with first next()
# Out: 1.0
[*gen_gen] # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]
為了給像gen_generator
這樣的構造提供一個更合理的用例示例,我們將對其進行一些擴展,並通過將 yield 分配給變量來從中創建一個協程,這樣我們就可以使用send()
將作業注入正在運行的生成器中。
此外,我們創建了一個輔助函數,它將運行一個作業中的所有任務,並在完成后請求一個新的任務。
def gen_coroutine():
"""Generator coroutine yielding from sub-generator inside."""
# do stuff... first time when 'next' gets called
print("receive job, build generator & yield, loop")
while True:
try:
func, seq = yield "send me work ... or I quit with next next()"
except TypeError:
return "no job left"
else:
yield from (func(*args) for args in seq)
def do_job(gen, job):
"""Run all tasks in job."""
print(gen.send(job))
while True:
result = next(gen)
print(result)
if result == "send me work ... or I quit with next next()":
break
現在我們使用輔助函數do_job
和兩個作業運行gen_coroutine
。
gen_co = gen_coroutine()
next(gen_co) # start
# receive job, build generator & yield, loop <-- printed with first next()
# Out:'send me work ... or I quit with next next()'
do_job(gen_co, job1) # prints out all results from job
# 1
# 1.4142135623730951
# 1.7320508075688772
# 2.0
# send me work... or I quit with next next()
do_job(gen_co, job2) # send another job into generator
# 3
# 4
# 5
# 6
# send me work... or I quit with next next()
next(gen_co)
# Traceback ...
# StopIteration: no job left
回到你的問題,一般來說哪個版本是更好的方法。 IMO 之類的gen_factory
東西只有在您需要為將要創建的多個發電機做同樣的事情時才有意義,或者在您的發電機構建過程足夠復雜以證明使用工廠而不是使用發電機就地構建單個發電機的情況下才有意義理解。
上面對gen_generator
函數(第二個mymap
)的描述指出“它本身就是一個生成器”。 這有點含糊,從技術上講也不太正確,但有助於在這個棘手的設置中推理函數的差異,其中gen_factory
還返回一個生成器,即由生成器理解構建的生成器。
事實上,任何函數(不僅僅是來自這個問題的那些帶有生成器推導式的函數!)在調用時內部有一個yield
,只返回一個生成器對象,該對象從函數體中構造出來。
type(gen_coroutine) # function
gen_co = gen_coroutine(); type(gen_co) # generator
所以我們在上面觀察到的gen_generator
和gen_coroutine
的整個動作發生在這些生成器對象中,里面有yield
函數之前已經吐出來了。
答案是:返回一個生成器。 它更快:
marco@buzz:~$ python3.9 -m pyperf timeit --rigorous --affinity 3 --value 6 --loops=4096 -s '
a = range(1000)
def f1():
for x in a:
yield x
def f2():
return f1()
' 'tuple(f2())'
........................................
Mean +- std dev: 72.8 us +- 5.8 us
marco@buzz:~$ python3.9 -m pyperf timeit --rigorous --affinity 3 --value 6 --loops=4096 -s '
a = range(1000)
def f1():
for x in a:
yield x
def f2():
yield from f1()
' 'tuple(f2())'
........................................
WARNING: the benchmark result may be unstable
* the standard deviation (12.6 us) is 10% of the mean (121 us)
Try to rerun the benchmark with more runs, values and/or loops.
Run 'python3.9 -m pyperf system tune' command to reduce the system jitter.
Use pyperf stats, pyperf dump and pyperf hist to analyze results.
Use --quiet option to hide these warnings.
Mean +- std dev: 121 us +- 13 us
如果你閱讀PEP 380 ,引入yield from
主要原因是將一個生成器的一部分代碼用於另一個生成器,而不必復制代碼或更改 API:
上面介紹的大多數語義背后的基本原理源於能夠重構生成器代碼的願望。 應該可以將包含一個或多個 yield 表達式的一段代碼移到一個單獨的函數中(使用通常的技術來處理對周圍范圍內變量的引用等),並使用從表達中產生。
最重要的區別(我不知道yield from generator
是否優化)是return
和yield from
的上下文不同。
[ins] In [1]: def generator():
...: yield 1
...: raise Exception
...:
[ins] In [2]: def use_generator():
...: return generator()
...:
[ins] In [3]: def yield_generator():
...: yield from generator()
...:
[ins] In [4]: g = use_generator()
[ins] In [5]: next(g); next(g)
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-5-3d9500a8db9f> in <module>
----> 1 next(g); next(g)
<ipython-input-1-b4cc4538f589> in generator()
1 def generator():
2 yield 1
----> 3 raise Exception
4
Exception:
[ins] In [6]: g = yield_generator()
[ins] In [7]: next(g); next(g)
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-7-3d9500a8db9f> in <module>
----> 1 next(g); next(g)
<ipython-input-3-3ab40ecc32f5> in yield_generator()
1 def yield_generator():
----> 2 yield from generator()
3
<ipython-input-1-b4cc4538f589> in generator()
1 def generator():
2 yield 1
----> 3 raise Exception
4
Exception:
生成器使用yield
,函數使用return
。
生成器通常用在for
循環中,用於重復迭代生成器自動提供的值,但也可以在其他上下文中使用,例如在list()函數中創建列表 - 再次從生成器自動提供的值。
函數被調用以提供返回值,每次調用只有一個值。
我更喜歡帶有yield from
的版本,因為它可以更輕松地處理異常和上下文管理器。
以文件行的生成器表達式為例:
def with_return(some_file):
with open(some_file, 'rt') as f:
return (line.strip() for line in f)
for line in with_return('/tmp/some_file.txt'):
print(line)
return
版本引發ValueError: I/O operation on closed file.
因為在return
語句之后文件不再打開。
另一方面,版本的yield from
按預期工作:
def with_yield_from(some_file):
with open(some_file, 'rt') as f:
yield from (line.strip() for line in f)
for line in with_yield_from('/tmp/some_file.txt'):
print(line)
真的要視情況而定。 yield
主要適用於您只想迭代返回值然后操作它們的情況。 return
主要適用於您希望將函數生成的所有值存儲在內存中而不是僅迭代一次的情況。 請注意,您只能迭代生成器(收益返回)一次,有些算法絕對不適合。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.