Return 或从调用生成器的函数中产生?

Return or yield from a function that calls a generator?

我有一个生成器 generator 以及一个方便的方法 - generate_all.

def generator(some_list):
  for i in some_list:
    yield do_something(i)

def generate_all():
  some_list = get_the_list()
  return generator(some_list) # <-- Is this supposed to be return or yield?

应该generate_allreturn还是yield?我希望这两种方法的用户使用相同的方法,即

for x in generate_all()

应该等于

some_list = get_the_list()
for x in generate(some_list)

您可能正在寻找 Generator Delegation (PEP380)

For simple iterators, yield from iterable is essentially just a shortened form of for item in iterable: yield item

def generator(iterable):
  for i in iterable:
    yield do_something(i)

def generate_all():
  yield from generator(get_the_list())

它非常简洁,还有许多其他优点,例如能够链接 arbitrary/different 可迭代对象!

return generator(list) 为所欲为。但请注意

yield from generator(list)

是等效的,但在 generator 耗尽后有机会产生更多的值。例如:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"

在这种特殊情况下,以下两个语句在功能上似乎是等效的:

return generator(list)

yield from generator(list)

后面跟

差不多
for i in generator(list):
    yield i

return声明returns你要找的生成器。 yield fromyield 语句将您的整个函数变成 returns 生成器,它通过您正在寻找的那个。

从用户的角度来看,没有区别。然而,在内部,return 可以说更有效,因为它不会将 generator(list) 包装在多余的直通生成器中。如果您计划对包装生成器的元素进行 any 处理,当然可以使用某种形式的 yield

你会 return 它。

yielding* 会导致 generate_all() 评估生成器本身,并且在该外部生成器上调用 next 会 return 内部生成器 return 由第一个函数编辑,这不是您想要的。

* 不包括 yield from

生成器是惰性求值的,因此 returnyield 在调试代码或抛出异常时会有不同的行为。

使用 return 时,您的 generator 中发生的任何异常都不会知道 generate_all,这是因为当 generator 真正执行时,您已经离开了generate_all 函数。在 yield 的情况下,回溯中将包含 generate_all

def generator(some_list):
    for i in some_list:
        raise Exception('exception happened :-)')
        yield i

def generate_all():
    some_list = [1,2,3]
    return generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-3-b19085eab3e1> in <module>
      8     return generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-3-b19085eab3e1> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

如果它使用 yield from:

def generate_all():
    some_list = [1,2,3]
    yield from generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-4-be322887df35> in <module>
      8     yield from generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-4-be322887df35> in generate_all()
      6 def generate_all():
      7     some_list = [1,2,3]
----> 8     yield from generator(some_list)
      9 
     10 for item in generate_all():

<ipython-input-4-be322887df35> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

然而,这是以性能为代价的。额外的生成器层确实有一些开销。所以 return 通常会比 yield from ...(或 for item in ...: yield item)快一点。在大多数情况下,这无关紧要,因为无论您在生成器中做什么,通常都会支配 运行 时间,因此附加层不会引人注意。

然而 yield 有一些额外的优势:你不局限于单个迭代器,你还可以轻松地产生额外的项目:

def generator(some_list):
    for i in some_list:
        yield i

def generate_all():
    some_list = [1,2,3]
    yield 'start'
    yield from generator(some_list)
    yield 'end'

for item in generate_all():
    print(item)
start
1
2
3
end

在您的情况下,操作非常简单,我不知道是否有必要为此创建多个函数,可以轻松地使用内置 map 或生成器表达式来代替:

map(do_something, get_the_list())          # map
(do_something(i) for i in get_the_list())  # generator expression

两者应该是相同的(除了发生异常时的一些差异)以供使用。如果他们需要一个更具描述性的名称,那么您仍然可以将它们包装在一个函数中。

有多个帮助程序可以在内置的可迭代对象上包装非常常见的操作,并且可以在内置的 itertools 模块中找到更多的帮助程序。在这种简单的情况下,我会简单地求助于这些,并且仅在非平凡的情况下编写您自己的生成器。

但我假设您的实际代码更复杂,因此可能不适用,但我认为如果不提及替代方案,这将不是一个完整的答案。