【问题标题】:Why does a generator function not use the idle time to prepare the next yield?为什么生成器函数不使用空闲时间来准备下一个产量?
【发布时间】:2017-08-31 22:31:27
【问题描述】:

在当今多核、多线程 CPU(我笔记本中的 CPU 有两个内核,每个内核两个线程)的编程世界中,编写能够利用提供的硬件功能的代码越来越有意义。像 go(lang) 这样的语言的诞生是为了让程序员更容易通过生成多个“独立”进程来加速应用程序,以便稍后再次同步它们。

在与 Python 中的生成器函数联系的上下文中,我预计此类函数将使用在后续项目请求之间传递的空闲时间来准备下一个收益以立即交付,但似乎不是这样 - 至少所以我对运行下面提供的代码所得到的结果的解释。

更让我困惑的是,即使生成器已经交付了所有项目,生成器函数的调用者也必须等到函数完成处理所有剩余的指令。

是否有任何我目前看不到的明确原因,为什么要使用生成器 函数不在 yield 请求之间的空闲时间运行代码 超过请求的产量,直到满足下一个产量指令,并且 甚至让调用者等待,以防所有物品都已送达?

这里是我使用的代码:

import time
startTime = time.time()
time.sleep(1)
def generatorFunctionF():
    print("# here: generatorFunctionF() lineNo #1", time.time()-startTime)
    for i in range(1,4):
        print("# now: time.sleep(1)", time.time()-startTime)
        time.sleep(1)
        print("# before yield", i, time.time()-startTime)
        yield i # yield i
        print("# after  yield", i, time.time()-startTime)
    print("# now: time.sleep(5)", time.time()-startTime)
    time.sleep(5)
    print("# end followed by 'return'", time.time()-startTime)
    return
#:def

def standardFunctionF():
    print("*** before: 'gFF = generatorFunctionF()'", time.time()-startTime) 
    gFF = generatorFunctionF()
    print("*** after:  'gFF = generatorFunctionF()'", time.time()-startTime) 
    print("*** before print(next(gFF)", time.time()-startTime)
    print(next(gFF))
    print("*** after  print(next(gFF)", time.time()-startTime)
    print("*** before time.sleep(3)", time.time()-startTime)
    time.sleep(3)
    print("*** after  time.sleep(3)", time.time()-startTime)
    print("*** before print(next(gFF)", time.time()-startTime)
    print(next(gFF))
    print("*** after  print(next(gFF)", time.time()-startTime)
    print("*** before list(gFF)", time.time()-startTime)
    print("*** list(gFF): ", list(gFF), time.time()-startTime)
    print("*** after:  list(gFF)", time.time()-startTime)
    print("*** before time.sleep(3)", time.time()-startTime)
    time.sleep(3)
    print("*** after  time.sleep(3)", time.time()-startTime)
    return "*** endOf standardFunctionF"

print()
print(standardFunctionF)
print(standardFunctionF())

给予:

>python3.6 -u "aboutIteratorsAndGenerators.py"

<function standardFunctionF at 0x7f97800361e0>
*** before: 'gFF = generatorFunctionF()' 1.001169204711914
*** after:  'gFF = generatorFunctionF()' 1.0011975765228271
*** before print(next(gFF) 1.0012099742889404
# here: generatorFunctionF() lineNo #1 1.0012233257293701
# now: time.sleep(1) 1.0012412071228027
# before yield 1 2.0023491382598877
1
*** after  print(next(gFF) 2.002397298812866
*** before time.sleep(3) 2.0024073123931885
*** after  time.sleep(3) 5.005511283874512
*** before print(next(gFF) 5.005547761917114
# after  yield 1 5.005556106567383
# now: time.sleep(1) 5.005565881729126
# before yield 2 6.006666898727417
2
*** after  print(next(gFF) 6.006711006164551
*** before list(gFF) 6.0067174434661865
# after  yield 2 6.006726026535034
# now: time.sleep(1) 6.006732702255249
# before yield 3 7.0077736377716064
# after  yield 3 7.0078125
# now: time.sleep(5) 7.007838010787964
# end followed by 'return' 12.011908054351807
*** list(gFF):  [3] 12.011950254440308
*** after:  list(gFF) 12.011966466903687
*** before time.sleep(3) 12.011971473693848
*** after  time.sleep(3) 15.015069007873535
*** endOf standardFunctionF
>Exit code: 0

【问题讨论】:

  • 不确定问题的第二部分(关于“必须等到生成器完成”)是什么意思。请澄清你的意思。
  • 不要忘记generators 的相同机制可以是coroutines,例如x = yield 10,这在yielding 10 之后暂停,但分配发生在下一个send(5)next(...)。你可能想看看asyncio
  • 这种行为会干扰定时数据,例如每天的服务器查询。按需交付的新数据通常比急切获取然后等待下一个请求到达的陈旧数据更受欢迎。
  • @BrenBarn:在生成器函数的代码离开循环后,生成器函数中可能会有更多命令。在我提供的示例代码中,其余代码不包含任何 yield 关键字,但是调用者必须等待项目的交付,直到代码被处理(在代码示例中 time.sleep(5) 秒长)。跨度>
  • @Claudio:我认为您误解了生成器是什么。它们是在 yield 语句(和 return 语句)之间暂停的函数。这就是他们所做的。它们并不意味着是在后台进行处理的某种优化方式。如果你写time.sleep(5),那么当代码运行时它会休眠 5 秒。没有花哨的前瞻来查看将要发生什么;当您推进发电机时,它只会恢复。

标签: python multithreading iterator generator multicore


【解决方案1】:

因为yield之间的代码可能会有副作用。您不仅在“想要下一个值”时推进生成器,而且在想要通过继续运行代码来推进生成器时推进生成器。

【讨论】:

  • 我很清楚,一些奇怪的代码可能会产生副作用。但是如果我能从代码中清楚地看到自己不会有任何东西,那么在我看来,解释器应该也能看到它,并在这种情况下提供立即交付的功能。
  • @Claudio:即使你认为你可以看到没有副作用,你也可能错了。外部代码可以更改生成器函数使用的全局变量的值,例如 time。在 Python 这样的动态语言中,确定代码是否会产生副作用是极其困难的,因为 Python 几乎任何东西的值几乎随时都可能发生变化。
  • @Claudio 我们可以急切地评估一个简单的for x in a: yield x 吗?您将如何证明这一点?
  • @Claudio:是的,任何代码都可能有副作用,生成器函数可能包含任何代码。这就是为什么没有代码(在生成器中或其他任何地方)在后台运行的原因,除非您通过启动新线程或新进程明确使其这样做。
【解决方案2】:

关于 Python 中生成器函数的预期特性的问题应该从更广泛的主题的角度来看待

隐式并行

这里是excerpt from Wikipedia“在计算机科学中,隐式并行性是编程语言的一个特征,它允许编译器或解释器自动利用由某些语言结构表达的计算所固有的并行性。”

问题的本质有什么重要的原因,为什么一个生成器函数没有在yield之间的空闲时间预取下一项?其实是问

“Python 作为编程语言是否支持隐式并行?”

尽管(问题作者对 的引用表达了意见):“生成器函数不应该提供这种“智能”行为没有任何合理的理由.”,在 Python 作为编程语言的背景下,问题的实际正确答案(已经在 cmets 中给出,但没有如此清楚地揭示问题的核心)是:

Python 生成器函数不应在后台智能地预取下一项以便稍后立即交付的重要原因是 Python 作为编程语言 不支持隐式并行。


这就是说,如果可以在 Python 中以显式方式提供预期的功能,那么在这种情况下进行探索肯定很有趣?是的,这是可能的。让我们在此上下文中演示一个生成器函数,该生成器函数能够通过将此功能显式编程到此类函数中,从而在后台隐式预取下一项:

from multiprocessing import Process
import time

def generatorFetchingItemsOnDemand():
    for i in range(1, 4):
        time.sleep(2)
        print("# ...ItemsOnDemand spends 2 seconds for delivery of item")
        yield i

def generatorPrefetchingItemsForImmediateDelivery():
    with open('tmpFile','w') as tmpFile:
        tmpFile.write('')
        tmpFile.flush()

    def itemPrefetcher():
        for i in range(1, 4):
            time.sleep(2)
            print("### itemPrefetcher spends 2 seconds for prefetching an item")
            with open('tmpFile','a') as tmpFile:
                tmpFile.write(str(i)+'\n')
                tmpFile.flush()

    p = Process(target=itemPrefetcher)
    p.start()

    for i in range(1, 4):
        with open('tmpFile','r') as tmpFile:
            lstFileLines = tmpFile.readlines()
        if len(lstFileLines) < i: 
            while len(lstFileLines) < i:
                time.sleep(0.1)
                with open('tmpFile','r') as tmpFile:
                    lstFileLines = tmpFile.readlines()

        yield int(lstFileLines[i-1])
#:def

def workOnAllItems(intValue):
    startTime = time.time()
    time.sleep(2)
    print("workOn(", intValue, "): took", (time.time()-startTime), "seconds")
    return intValue

print("===============================")        
genPrefetch = generatorPrefetchingItemsForImmediateDelivery()
startTime = time.time()
for item in genPrefetch:
    workOnAllItems(item)
print("using genPrefetch workOnAllItems took", (time.time()-startTime), "seconds")
print("-------------------------------")        
print()
print("===============================")        
genOnDemand = generatorFetchingItemsOnDemand()
startTime = time.time()
for item in genOnDemand:
    workOnAllItems(item)
print("using genOnDemand workOnAllItems took", (time.time()-startTime), "seconds")
print("-------------------------------")        

提供的代码使用文件系统进行进程间通信,因此,如果您想在自己的编程中重新使用这个概念,以用现有的其他更快的进程间通信机制替换它,请随意。以此处演示的方式实现生成器函数,实现了问题作者期望生成器函数应该做的事情,并有助于加快应用程序的速度(这里从 12 秒缩短到 8 秒):

>python3.6 -u "generatorPrefetchingItemsForImmediateDelivery.py"
===============================
### itemPrefetcher spends 2 seconds for prefetching an item
### itemPrefetcher spends 2 seconds for prefetching an item
workOn( 1 ): took 2.0009119510650635 seconds
### itemPrefetcher spends 2 seconds for prefetching an item
workOn( 2 ): took 2.0010197162628174 seconds
workOn( 3 ): took 2.00161075592041 seconds
using genPrefetch workOnAllItems took 8.013896942138672 seconds
-------------------------------

===============================
# ...ItemsOnDemand spends 2 seconds for delivery of item
workOn( 1 ): took 2.0011563301086426 seconds
# ...ItemsOnDemand spends 2 seconds for delivery of item
workOn( 2 ): took 2.001920461654663 seconds
# ...ItemsOnDemand spends 2 seconds for delivery of item
workOn( 3 ): took 2.0002224445343018 seconds
using genOnDemand workOnAllItems took 12.007976293563843 seconds
-------------------------------
>Exit code: 0

【讨论】:

    【解决方案3】:

    生成器被设计为一种更简单、更短、更易于理解的用于编写迭代器的语法。那是他们的用例。想要使迭代器更短、更容易理解的人希望在他们编写的每个迭代器中引入线程同步的麻烦。这与设计目标背道而驰。

    因此,生成器基于coroutines 和协作多任务的概念,而不是线程。设计权衡是不同的;生成器牺牲并行执行以换取更容易推理的语义。

    此外,为每个生成器使用单独的线程确实效率低下,并且确定何时并行化是一个难题。大多数生成器实际上并不值得在另一个线程中执行。哎呀,即使在没有 GIL 的 Python 实现(如 Jython 或 Grumpy)中,它们也不值得在另一个线程中执行。

    如果您想要并行运行的东西,这已经通过启动线程或进程并通过队列与其通信来处理。

    【讨论】:

    • Hmmm ... 正如link 此处所述,“许多人报告说,即使在齐心协力自学该主题后,也难以理解生成器和 yield 关键字。”(包括我)我很难接受:“生成器被设计成一种更简单、更短、更易于理解的用于编写迭代器的语法。”。
    • @Claudio: Well, they were. 对于新手来说,yield 可能很难理解,但它比手动编写高度有状态或递归迭代器要容易得多。如果这种语法也自动引入线程,您认为那些新手会更轻松吗?
    • GOMAXPROCSGo 1.5(2015 年 8 月)中不再默认为 1
    • @LukeShu:嗯,你说得对。在我写这个答案时,我一定是在使用过时的文档(或过时的内存)。
    猜你喜欢
    • 2015-01-18
    • 1970-01-01
    • 2015-06-02
    • 2012-08-06
    • 2012-08-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-15
    相关资源
    最近更新 更多