【问题标题】:Return or yield from a function that calls a generator?从调用生成器的函数返回或产生?
【发布时间】:2020-04-23 01:28:59
【问题描述】:

我有一个生成器 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_all return 还是yield?我希望这两种方法的用户都使用相同的方法,即

for x in generate_all()

应该等于

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

【问题讨论】:

  • 两者都有使用的理由。对于这个例子,return 效率更高
  • 这让我想起了我曾经提出的一个类似问题:“yield from iterable” vs “return iter(iterable)”。虽然不是专门关于生成器,但它基本上与生成器相同,并且迭代器在 python 中非常相似。也可以在这里使用答案提出的比较字节码的策略。

标签: python function return generator yield


【解决方案1】:

生成器是惰性求值的,所以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 模块中找到更多帮助器。在这种简单的情况下,我会简单地求助于这些,并且仅在不平凡的情况下编写您自己的生成器。

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

【讨论】:

    【解决方案2】:

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

    对于简单的迭代器,yield from iterable 本质上只是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())
    

    它非常简洁,还具有许多其他优点,例如能够链接任意/不同的可迭代对象!

    【讨论】:

    • 哦,你的意思是list的命名?这是一个不好的例子,不是粘贴在问题中的真实代码,我可能应该对其进行编辑。
    • 是的 - 不用担心,我对最初询问甚至无法运行的示例代码感到内疚..
    • 第一个也可以是单线 :)。 yield from map(do_something, iterable) 甚至 yield from (do_something(x) for x in iterable)
    • 只有在你自己做一些事情时才需要委托,而不仅仅是返回新的生成器。如果您只是返回新的生成器,则不需要委托。所以yield from 是毫无意义的,除非你的包装器做了 something else generator-y.
    【解决方案3】:

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

    return generator(list)
    

    yield from generator(list)
    

    后面的差不多

    for i in generator(list):
        yield i
    

    return 语句返回您正在寻找的生成器。 yield fromyield 语句将您的整个函数转换为返回生成器的东西,该生成器通过您要查找的生成器。

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

    【讨论】:

      【解决方案4】:

      你会return它。

      yielding* 将导致 generate_all() 评估为生成器本身,并在该外部生成器上调用 next 将返回第一个函数返回的内部生成器,这不是您想要的。

      *不包括yield from

      【讨论】:

        【解决方案5】:

        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"
        

        【讨论】:

        • 我相信yield fromreturn 之间存在细微差别,当生成器throws 的使用者在其中发生异常时,以及其他受堆栈跟踪影响的操作。
        猜你喜欢
        • 2016-05-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多