首页 文章

“yield”关键字有什么作用?

提问于
浏览
8739

Python中 yield 关键字的用途是什么?它有什么作用?

例如,我试图理解这个代码1:

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

这是来电者:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

调用方法 _get_child_candidates 时会发生什么?列表是否返回?单个元素?它又被召唤了吗?后续通话何时停止?


1.代码来自Jochen Schulz(jrschulz),他为度量空间创建了一个很棒的Python库 . 这是完整源代码的链接:模块mspace .

30 回答

  • 273

    yield 关键字简化为两个简单的事实:

    • 如果编译器在函数内的任何位置检测到 yield 关键字,则该函数不再通过 return 语句返回 . Instead ,它 immediately 返回 lazy "pending list" object 称为生成器

    • 生成器是可迭代的 . 什么是可迭代的?它类似于 listsetrange 或dict-view,具有用于按特定顺序访问每个元素的内置协议 .

    简而言之: a generator is a lazy, incrementally-pending listyield statements allow you to use function notation to program the list values 生成器应逐渐吐出 .

    generator = myYieldingFunction(...)
    x = list(generator)
    
       generator
           v
    [x[0], ..., ???]
    
             generator
                 v
    [x[0], x[1], ..., ???]
    
                   generator
                       v
    [x[0], x[1], x[2], ..., ???]
    
                           StopIteration exception
    [x[0], x[1], x[2]]     done
    
    list==[x[0], x[1], x[2]]
    

    示例

    让我们定义一个函数 makeRange ,'s just like Python' s range . 呼叫 makeRange(n) 退回发电机:

    def makeRange(n):
        # return 0,1,2,...,n-1
        i = 0
        while i < n:
            yield i
            i += 1
    
    >>> makeRange(5)
    <generator object makeRange at 0x19e4aa0>
    

    要强制生成器立即返回其挂起值,您可以将其传递给 list() (就像您可以任意迭代一样):

    >>> list(makeRange(5))
    [0, 1, 2, 3, 4]
    

    比较“只返回列表”的示例

    上面的例子可以被认为只是创建一个你追加并返回的列表:

    # list-version                   #  # generator-version
    def makeRange(n):                #  def makeRange(n):
        """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
        TO_RETURN = []               #>
        i = 0                        #      i = 0
        while i < n:                 #      while i < n:
            TO_RETURN += [i]         #~         yield i
            i += 1                   #          i += 1  ## indented
        return TO_RETURN             #>
    
    >>> makeRange(5)
    [0, 1, 2, 3, 4]
    

    但是有一个主要的区别;见最后一节 .


    如何使用发电机

    可迭代是列表推导的最后一部分,并且所有生成器都是可迭代的,因此它们经常被使用:

    #                   _ITERABLE_
    >>> [x+10 for x in makeRange(5)]
    [10, 11, 12, 13, 14]
    

    为了更好地感受生成器,您可以使用 itertools 模块(确保在保证时使用 chain.from_iterable 而不是 chain ) . 例如,您甚至可以使用生成器来实现无限长的惰性列表,如 itertools.count() . 您可以实现自己的 def enumerate(iterable): zip(count(), iterable) ,或者在while循环中使用 yield 关键字实现 .

    请注意:生成器实际上可以用于更多的东西,例如implementing coroutines或非确定性编程或其他优雅的东西 . 但是,我在这里提出的观点是你会发现的最常见的用途 .


    在幕后

    这就是"Python iteration protocol"的工作原理 . 也就是说,当你做 list(makeRange(5)) 时会发生什么 . 这就是我之前描述的"lazy, incremental list" .

    >>> x=iter(range(5))
    >>> next(x)
    0
    >>> next(x)
    1
    >>> next(x)
    2
    >>> next(x)
    3
    >>> next(x)
    4
    >>> next(x)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    

    内置函数 next() 只调用对象 .next() 函数,它是"iteration protocol"的一部分,可以在所有迭代器上找到 . 您可以手动使用 next() 函数(以及迭代协议的其他部分)来实现花哨的东西,通常以牺牲可读性为代价,因此尽量避免这样做......


    Minutiae

    通常情况下,大多数人不会关心以下区别,可能想在这里停止阅读 .

    在Python语言中,iterable是"understands the concept of a for-loop"像列表 [1,2,3] 的任何对象,迭代器是所请求的for循环的特定实例,如 [1,2,3].__iter__() . 生成器与任何迭代器完全相同,除了它的编写方式(使用函数语法) .

    从列表中请求迭代器时,它会创建一个新的迭代器 . 但是,当您从迭代器(您很少这样做)请求迭代器时,它只会为您提供自身的副本 .

    因此,万一你没有做到这样的事情......

    > x = myRange(5)
    > list(x)
    [0, 1, 2, 3, 4]
    > list(x)
    []
    

    ...然后记住发电机是一个迭代器;也就是说,它是一次性的 . 如果要重复使用它,则应再次调用 myRange(...) . 如果需要使用结果两次,请将结果转换为列表并将其存储在变量 x = list(myRange(5)) 中 . 那些绝对需要克隆生成器的人(例如,谁正在做可怕的hackish元编程)如果绝对必要的话可以使用itertools.tee,因为可复制的迭代器Python PEP标准提案已被推迟 .

  • 57

    像每个答案所暗示的那样, yield 用于创建序列生成器 . 它用于动态生成一些序列 . 例如,在网络上逐行读取文件时,可以使用 yield 函数,如下所示:

    def getNextLines():
       while con.isOpen():
           yield con.read()
    

    您可以在代码中使用它,如下所示:

    for line in getNextLines():
        doSomeThing(line)
    

    Execution Control Transfer gotcha

    执行yield时,执行控件将从getNextLines()传送到 for 循环 . 因此,每次调用getNextLines()时,执行都从上次暂停的位置开始 .

    因此简而言之,具有以下代码的功能

    def simpleYield():
        yield "first time"
        yield "second time"
        yield "third time"
        yield "Now some useful value {}".format(12)
    
    for i in simpleYield():
        print i
    

    将打印

    "first time"
    "second time"
    "third time"
    "Now some useful value 12"
    
  • 191

    Grokking收益率的快捷方式

    当你看到一个带有 yield 语句的函数时,应用这个简单的技巧来理解会发生什么:

    • 在函数开头插入一行 result = [] .

    • result.append(expr) 替换每个 yield expr .

    • 在底部插入一条 return result 行功能 .

    • 耶 - 没有 yield 陈述!阅读并找出代码 .

    • 将功能与原始定义进行比较 .

    这个技巧可以让你了解函数背后的逻辑,但 yield 实际发生的情况与基于列表的方法中发生的情况明显不同 . 在许多情况下,yield方法将更高效,更快 . 在其他情况下,即使原始函数工作得很好,这个技巧也会让你陷入无限循环 . 请继续阅读以了解更多信息...

    不要混淆你的Iterables,Iterators和Generators

    首先, iterator protocol - 你写的时候

    for x in mylist:
        ...loop body...
    

    Python执行以下两个步骤:

    • 获取 mylist 的迭代器:

    调用 iter(mylist) - >返回一个带有 next() 方法的对象(或Python 3中的 __next__() ) .

    [这是大多数人忘记告诉你的步骤]

    • 使用迭代器循环遍历项目:

    继续在步骤1返回的迭代器上调用 next() 方法 . 将 next() 的返回值赋给 x 并执行循环体 . 如果从 next() 中引发异常 StopIteration ,则表示迭代器中没有更多值,并且退出循环 .

    事实是Python只要想要循环对象的内容就执行上述两个步骤 - 所以它可能是一个for循环,但它也可能是像 otherlist.extend(mylist) 这样的代码(其中 otherlist 是一个Python列表) .

    这里 mylist 是一个可迭代的,因为它实现了迭代器协议 . 在用户定义的类中,您可以实现 __iter__() 方法以使类的实例可迭代 . 此方法应返回迭代器 . 迭代器是一个带有 next() 方法的对象 . 可以在同一个类上实现 __iter__()next() ,并使 __iter__() 返回 self . 这适用于简单的情况,但是当您希望两个迭代器同时循环遍历同一个对象时 .

    所以这是迭代器协议,许多对象实现了这个协议:

    • 内置列表,词典,元组,集合,文件 .

    • 用户定义的实现 __iter__() 的类 .

    • 发电机 .

    注意 for 循环没有't know what kind of object it'处理它 - 它只是遵循迭代器协议,并且很高兴得到项目,因为它调用 next() . 内置列表逐个返回它们的项目,字典逐个返回键,文件一个接一个地返回行等 . 然后生成器返回......那就是 yield 所在的位置:

    def f123():
        yield 1
        yield 2
        yield 3
    
    for item in f123():
        print item
    

    而不是 yield 语句,如果在 f123() 中有三个 return 语句,则只会执行第一个语句,并且该函数将退出 . 但 f123() 不是普通的功能 . 调用 f123() 时,它不会返回yield语句中的任何值!它返回一个生成器对象 . 此外,该功能并没有真正退出 - 它进入暂停状态 . 当 for 循环尝试遍历生成器对象时,该函数在之前返回的 yield 之后的最后一行从其挂起状态恢复,执行下一行代码,在本例中为 yield 语句,并将其返回为下一个项目 . 这一过程发生,直到函数退出,此时生成器引发 StopIteration ,循环退出 .

    所以生成器对象有点像适配器 - 在一端它展示了迭代器协议,通过暴露 __iter__()next() 方法来保持 for 循环满意 . 然而,在另一端,它运行该功能足以从中获取下一个值,并将其重新置于挂起模式 .

    为什么使用发电机?

    通常你可以编写一些我之前提到过的代码 . 这并不适用于所有情况,例如如果你有无限循环,或者当你有一个非常长的列表时它可能会低效地使用内存 . 另一种方法是实现一个新的可迭代类 SomethingIter ,它将状态保存在实例成员中,并在它的 next() (或Python3中的 __next__() )方法中执行下一个逻辑步骤 . 根据逻辑, next() 方法中的代码可能看起来非常复杂并且容易出错 . 这里的发电机提供了一个简洁的解决方案

  • 142

    我将发布“阅读Beazley的'Python:Essential Reference'第19页,以便快速描述发生器”,但是很多其他人已经发布了很好的描述 .

    另外,请注意 yield 可以在协同程序中用作它们在生成器函数中使用的对偶 . 虽然它与您的代码片段的用法不同,但 (yield) 可以用作函数中的表达式 . 当调用者使用 send() 方法向方法发送值时,协程将执行,直到遇到下一个 (yield) 语句 .

    生成器和协同程序是设置数据流类型应用程序的一种很酷的方法 . 我认为在函数中知道 yield 语句的其他用法是值得的 .

  • 82

    所有伟大的答案,但是新手有点困难 .

    我假设你已经学会了 return 声明 .

    作为类比, returnyield 是双胞胎 . return 表示'return and stop'而'yield` means '返回,但继续'

    尝试使用return返回num_list .

    def num_list(n):
        for i in range(n):
            return i
    

    运行:

    In [5]: num_list(3)
    Out[5]: 0
    

    看,你只得到一个数字而不是它们的列表 . return 永远不会让你高兴,只执行一次并退出 .

    收益率

    return 替换为 yield

    In [10]: def num_list(n):
        ...:     for i in range(n):
        ...:         yield i
        ...:
    
    In [11]: num_list(3)
    Out[11]: <generator object num_list at 0x10327c990>
    
    In [12]: list(num_list(3))
    Out[12]: [0, 1, 2]
    

    现在,你赢了所有的数字 .

    比较 return 运行一次并停止, yield 运行您计划的时间 . 您可以将 return 解释为 return one of them ,将 yield 解释为 return all of them . 这叫做 iterable .

    还有一步我们可以用return重写yield语句

    In [15]: def num_list(n):
        ...:     result = []
        ...:     for i in range(n):
        ...:         result.append(i)
        ...:     return result
    
    In [16]: num_list(3)
    Out[16]: [0, 1, 2]
    

    这是 yield 的核心 .

    列表 return 输出与对象 yield 输出之间的区别是:

    您将始终从列表对象获取[0,1,2],但只能从“对象 yield 输出”中检索一次 . 因此,它具有 Out[11]: <generator object num_list at 0x10327c990> 中显示的新名称 generator 对象 .

    总之,作为一个隐喻它的隐喻:

    • returnyield 是双胞胎

    • listgenerator 是双胞胎

  • 138

    这是 yield 所做的心理形象 .

    我喜欢将一个线程视为具有堆栈(即使它没有以这种方式实现) .

    当调用普通函数时,它将其局部变量放在堆栈上,进行一些计算,然后清除堆栈并返回 . 其局部变量的值再也看不到了 .

    使用 yield 函数,当其代码开始运行时(即在调用函数之后,返回一个生成器对象,然后调用其 next() 方法),它同样将其局部变量放入堆栈并计算一段时间 . 但是,当它到达 yield 语句时,在清除其部分堆栈并返回之前,它会获取其局部变量的快照并将它们存储在生成器对象中 . 它还会在其代码中写下它当前所处的位置(即特定的 yield 语句) .

    所以它是发电机悬挂的一种冻结功能 .

    当随后调用 next() 时,它会将函数的所有物检索到堆栈中并重新设置动画 . 该功能继续从它停止的地方进行计算,不知道它刚刚在冷库中度过了一个永恒的事实 .

    比较以下示例:

    def normalFunction():
        return
        if False:
            pass
    
    def yielderFunction():
        return
        if False:
            yield 12
    

    当我们调用第二个函数时,它的行为与第一个函数的行为非常不同 . yield 语句可能无法访问,但如果's present anywhere, it changes the nature of what we'正在处理 .

    >>> yielderFunction()
    <generator object yielderFunction at 0x07742D28>
    

    为了便于阅读,使用 yielder 前缀来调用 yielderFunction() 不是't run its code, but makes a generator out of the code. (Maybe it'是个好主意 . )

    >>> gen = yielderFunction()
    >>> dir(gen)
    ['__class__',
     ...
     '__iter__',    #Returns gen itself, to make it work uniformly with containers
     ...            #when given to a for loop. (Containers return an iterator instead.)
     'close',
     'gi_code',
     'gi_frame',
     'gi_running',
     'next',        #The method that runs the function's body.
     'send',
     'throw']
    

    gi_codegi_frame 字段是存储冻结状态的位置 . 用 dir(..) 探索它们,我们可以确认我们上面的心理模型是可信的 .

  • 1709

    对于那些喜欢最小工作示例的人,请冥想这个交互式的Python会话:

    >>> def f():
    ...   yield 1
    ...   yield 2
    ...   yield 3
    ... 
    >>> g = f()
    >>> for i in g:
    ...   print i
    ... 
    1
    2
    3
    >>> for i in g:
    ...   print i
    ... 
    >>> # Note that this time nothing was printed
    
  • 102

    还有另一个 yield 用法和含义(自Python 3.3起):

    yield from <expr>
    

    来自PEP 380 -- Syntax for Delegating to a Subgenerator

    建议生成器将其部分操作委托给另一个生成器 . 这允许将包含'yield'的一段代码分解出来并放在另一个生成器中 . 此外,允许子生成器返回一个值,该值可供委派生成器使用 . 当一个生成器重新生成另一个生成器生成的值时,新语法也会为优化提供一些机会 .

    此外this将介绍(自Python 3.5):

    async def new_coroutine(data):
       ...
       await blocking_action()
    

    避免协程与常规生成器混淆(今天 yield 用于两者) .

  • 42

    总之, yield 语句将您的函数转换为一个工厂,该工厂生成一个名为 generator 的特殊对象,它包裹原始函数的主体 . 当 generator 被迭代时,它会执行你的函数,直到它到达下一个 yield 然后暂停执行并计算传递给 yield 的值 . 它在每次迭代时重复此过程,直到执行路径退出函数 . 例如,

    def simple_generator():
        yield 'one'
        yield 'two'
        yield 'three'
    
    for i in simple_generator():
        print i
    

    简单的输出

    one
    two
    three
    

    电源来自使用带有计算序列的循环的发生器,发生器每次执行循环停止以“产生”下一个计算结果,这样它就可以动态计算列表,其好处是内存保存用于特别大的计算

    假设你想创建一个自己的 range 函数,它产生一个可迭代的数字范围,你可以这样做,

    def myRangeNaive(i):
        n = 0
        range = []
        while n < i:
            range.append(n)
            n = n + 1
        return range
    

    并像这样使用它;

    for i in myRangeNaive(10):
        print i
    

    但这是低效的,因为

    • 您创建一个只使用一次的数组(这会浪费内存)

    • 这段代码实际上循环遍历该数组两次! :(

    幸运的是,Guido和他的团队足够慷慨地开发发电机,所以我们可以做到这一点;

    def myRangeSmart(i):
        n = 0
        while n < i:
           yield n
           n = n + 1
        return
    
    for i in myRangeSmart(10):
        print i
    

    现在每个人迭代一个名为 next() 的生成器上的函数执行该函数,直到它到达'yield'语句,在该语句中它停止并且'yields'该值或到达函数的结尾 . 在这种情况下,在第一次调用时, next() 执行到yield语句并产生'n',在下一次调用时它将执行增量语句,跳回到'while',对其进行评估,如果为true,它将停止并再次产生'n' ,它将继续这种方式,直到while条件返回false并且生成器跳转到函数的末尾 .

  • 80

    这是一个简单的基于_134822的方法,用于计算斐波纳契数列,解释如下:

    def fib(limit=50):
        a, b = 0, 1
        for i in range(limit):
           yield b
           a, b = b, a+b
    

    当你在REPL中输入它然后尝试调用它时,你会得到一个神秘的结果:

    >>> fib()
    <generator object fib at 0x7fa38394e3b8>
    

    这是因为您希望创建一个生成器,即根据需要生成值的对象,向Python发出 yield 信号 .

    那么,你如何生成这些值?这可以通过使用内置函数 next 直接完成,也可以通过将其提供给消耗值的构造来间接完成 .

    使用内置的 next() 函数,直接调用 .next / __next__ ,强制生成器生成一个值:

    >>> g = fib()
    >>> next(g)
    1
    >>> next(g)
    1
    >>> next(g)
    2
    >>> next(g)
    3
    >>> next(g)
    5
    

    间接地,如果您将 fib 提供给 for 循环, list 初始值设定项, tuple 初始化程序或任何其他需要生成/生成值的对象的东西,那么您将生成生成器,直到它不再生成值(并且它回报):

    results = []
    for i in fib(30):       # consumes fib
        results.append(i) 
    # can also be accomplished with
    results = list(fib(30)) # consumes fib
    

    同样,使用 tuple 初始值设定项:

    >>> tuple(fib(5))       # consumes fib
    (1, 1, 2, 3, 5)
    

    生成器与函数的不同之处在于它是惰性的 . 它通过维护本地状态并允许您随时恢复来实现此目的 .

    当您第一次通过调用它来调用 fib 时:

    f = fib()
    

    Python编译函数,遇到 yield 关键字并简单地返回一个生成器对象 . 似乎不是很有帮助 .

    然后,当您请求它直接或间接生成第一个值时,它会执行它找到的所有语句,直到遇到 yield ,然后它会返回您提供给 yield 的值并暂停 . 有关更好地演示此示例的示例,让我们使用一些 print 调用(如果在Python 2上,则替换为 print "text" ):

    def yielder(value):
        """ This is an infinite generator. Only use next on it """ 
        while 1:
            print("I'm going to generate the value for you")
            print("Then I'll pause for a while")
            yield value
            print("Let's go through it again.")
    

    现在,输入REPL:

    >>> gen = yielder("Hello, yield!")
    

    你有一个生成器对象现在正在等待命令让它生成一个值 . 使用 next 并查看打印内容:

    >>> next(gen) # runs until it finds a yield
    I'm going to generate the value for you
    Then I'll pause for a while
    'Hello, yield!'
    

    未加引号的结果是印刷的 . 引用的结果是从 yield 返回的结果 . 现在再次致电 next

    >>> next(gen) # continues from yield and runs again
    Let's go through it again.
    I'm going to generate the value for you
    Then I'll pause for a while
    'Hello, yield!'
    

    发电机记得它在 yield value 暂停并从那里恢复 . 打印下一条消息并再次执行搜索 yield 语句以暂停它(由于 while 循环) .