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




15 Answers

Grokking yield捷径

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

  1. 在函数的开头插入行result = []
  2. result.append(expr)替换每个yield expr
  3. 在函数底部插入一行return result
  4. 耶 - 没有更多的yield声明! 阅读并找出代码。
  5. 比较功能与原始定义。

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

不要混淆你的Iterables,Iterators和Generators

首先, 迭代器协议 - 当你写

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

Python执行以下两个步骤:

  1. 获取mylist的迭代器:

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

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

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

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

事实上,Python在任何时候想要循环对象的内容时执行上述两个步骤 - 所以它可能是for循环,但它也可能是像otherlist.extend(mylist)代码( otherlist列表是Python列表) 。

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

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

  1. 内置列表,词典,元组,集,文件。
  2. 用户定义的实现__iter__()
  3. 发电机。

请注意, for循环不知道它正在处理什么类型的对象 - 它只是遵循迭代器协议,并且很乐意在它调用next()获得项目。 内置列表逐个返回它们的项目,字典逐个返回 ,文件一个接一个地返回等。然后生成器返回......那就是yield来源:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

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

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

为什么要使用发电机?

通常,您可以编写不使用生成器但实现相同逻辑的代码。 一种选择是使用我之前提到的临时列表'技巧'。 这在所有情况下都不起作用,例如,如果你有无限循环,或者当你有一个很长的列表时,它可能会使内存的使用效率低下。 另一种方法是实现一个新的可迭代类SomethingIter ,它将状态保存在实例成员中,并在它的next() (或Python 3中的__next__() )方法中执行下一个逻辑步骤。 根据逻辑, next()方法中的代码可能看起来非常复杂并且容易出错。 这里的发电机提供了一个简洁的解决方案

python yield返回值

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




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

  1. 如果编译器在函数内的任何位置检测到yield关键字,则该函数不再通过return语句return相反 ,它会立即返回一个称为生成器的惰性“挂起列表”对象
  2. 生成器是可迭代的。 什么是可迭代的 ? 它类似于listsetrange或字典视图,具有用于按特定顺序访问每个元素内置协议

简而言之: 生成器是一个惰性的,递增挂起的列表yield语句允许您使用函数表示法来编程生成器应逐渐吐出的列表值

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就像Python的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关键字执行此操作。

请注意:生成器实际上可以用于更多的事情,例如实现协同程序或非确定性编程或其他优雅的东西。 但是,我在这里提出的“懒惰列表”观点是您将找到的最常见的用途。

在幕后

这就是“Python迭代协议”的工作原理。 也就是说, list(makeRange(5))时会发生什么list(makeRange(5)) 。 这就是我之前描述的“懒惰的增量列表”。

>>> 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()函数,它是“迭代协议”的一部分,可以在所有迭代器上找到。 您可以手动使用next()函数(以及迭代协议的其他部分)来实现花哨的东西,通常以牺牲可读性为代价,因此尽量避免这样做......

细节

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

在Python中, 可迭代是任何“理解for循环的概念”的对象,如列表[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标准提案已被推迟。




yield就像return- 它返回你告诉它的任何东西(作为一个发生器)。区别在于下次调用生成器时,执行从最后一次调用yield语句开始。与返回不同,当产生收益时不会清除堆栈帧,但是控制被转移回调用者,因此其状态将在下次函数时恢复。

对于代码,该函数get_child_candidates的作用类似于迭代器,因此当您扩展列表时,它会一次向新列表添加一个元素。

list.extend调用迭代器直到它耗尽。对于您发布的代码示例,只返回一个元组并将其附加到列表中将更加清晰。




对于那些喜欢最小工作示例的人,请冥想这个交互式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



TL; DR

而不是这个:

def squares_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

做这个:

def squares_the_yield_way(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

每当你发现自己从头开始建立一个列表时,yield每一件都是。

这是我第一次有收益的“啊哈”时刻。

yield 是一种含糖的方式

建立一系列的东西

相同的行为:

>>> for square in squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

不同的行为:

收益是单程:你只能迭代一次。当函数有一个yield时,我们将其称为生成函数。和iterator是它返回。这是揭示。我们失去了容器的便利性,但却获得了任意长篇系列的力量。

收益是懒惰的,它推迟了计算。当你调用它时,一个带有yield的函数实际上根本不会执行。它返回的迭代器对象使用magic来维护函数的内部上下文。每次调用next()迭代器(这发生在for循环中)执行时,前进到下一个yield。(return提升StopIteration和结束系列。)

产量是多才多艺的。它可以做无限循环:

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

如果你需要多次通过并且系列不是太长,只需要打电话list()

>>> list(squares_the_yield_way(4))
[0, 1, 4, 9]

yield因为这两个含义都适用,所以这个词的选择很棒

产量 - 生产或提供(如农业)

...提供系列中的下一个数据。

收益 - 放弃或放弃(如政治权力)

...放弃CPU执行直到迭代器前进。




在我描述如何使用发电机的许多重要答案中,有一种我认为尚未给出的答案。这是编程语言理论的答案:

yieldPython中的语句返回一个生成器。 Python中的生成器是一个返回continuation的函数(特别是一种coroutine,但continuation代表了更常用的机制来理解正在发生的事情)。

编程语言理论的延续是一种更为基础的计算,但它们并不经常使用,因为它们极难推理并且也很难实现。但是,延续的概念很简单:计算的状态还没有完成。在此状态下,将保存变量的当前值,尚未执行的操作等。然后在程序的某个时刻,可以调用continuation,以便程序的变量重置为该状态,并执行保存的操作。

以这种更一般的形式,可以以两种方式实现连续。在call/cc方式,程序的堆栈字面上保存,然后调用延续时,堆栈恢复。

在连续传递样式(CPS)中,continuation只是普通函数(仅在函数是第一类的语言中),程序员明确地管理它并传递给子例程。在这种风格中,程序状态由闭包(以及碰巧在其中编码的变量)表示,而不是驻留在堆栈中某处的变量。管理控制流的函数接受继续作为参数(在CPS的某些变体中,函数可以接受多个延续)并通过简单地调用它们并在之后返回来调用它们来操纵控制流。延续传递样式的一个非常简单的例子如下:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

在这个(非常简单的)示例中,程序员将实际写入文件的操作保存到一个延续中(这可能是一个非常复杂的操作,需要写出许多细节),然后传递该延续(即,作为第一个 - class closure)到另一个执行更多处理的运算符,然后在必要时调用它。(我在实际的GUI编程中经常使用这种设计模式,因为它节省了我的代码行,或者更重要的是,在GUI事件触发后管理控制流。)

本文的其余部分将不失一般性地将延续概念化为CPS,因为它更容易理解和阅读。


现在让我们谈谈Python中的生成器。生成器是延续的特定子类型。虽然continuation通常能够保存计算的状态(即程序的调用堆栈),但生成器只能通过迭代器保存迭代状态。虽然这个定义对于某些发电机的使用情况略有误导。例如:

def f():
  while True:
    yield 4

这显然是一个合理的迭代,其行为很明确 - 每次生成器迭代它,它返回4(并且永远这样做)。但是,在考虑迭代器(即,for x in collection: do_something(x))时,它可能不是想到的典型迭代类型。这个例子说明了生成器的强大功能:如果有什么是迭代器,生成器可以保存其迭代的状态。

重申:Continuations可以保存程序堆栈的状态,生成器可以保存迭代状态。这意味着continuation比生成器更强大,但是生成器也很多,更容易。它们对于语言设计者来说更容易实现,并且程序员更容易使用它们(如果你有时间刻录,尝试阅读和理解这个页面关于continuation和call / cc)。

但是您可以轻松地实现(和概念化)生成器作为连续传递样式的简单特定情况:

无论什么时候yield被调用,它都会告诉函数返回一个延续。再次调用该函数时,它从它停止的任何地方开始。因此,在伪伪代码(即,不是伪代码,而不是代码)中,生成器的next方法基本如下:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

其中yield关键字实际上是真正的生成器函数的语法糖,基本上类似于:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

请记住,这只是伪代码,Python中生成器的实际实现更复杂。但是,作为了解正在发生的事情的练习,尝试使用延续传递样式来实现生成器对象而不使用yield关键字。




虽然很多答案都说明了为什么要使用a yield创建生成器,但有更多的用途yield。制作协程非常容易,它可以在两个代码块之间传递信息。我不会重复已经给出的关于使用yield创建生成器的任何精细示例。

为了帮助理解yield以下代码中的功能,您可以用手指在任何具有代码的代码中跟踪循环yield。每当你的手指击中时yield,你必须等待a next或a send进入。当a next被调用时,你会遍历代码,直到你点击yield... yield评估右侧的代码并返回给调用者...然后你等待。当next被再次调用,您通过代码进行另一次循环。但是,您会注意到在协程中,yield也可以与send... 一起使用,它会将调用者的值发送让步函数中。如果send给出了a ,那么yield接收发送的值,并将其从左侧吐出...然后通过代码的跟踪进行直到yield再次击中(在结束时返回值,就像next被调用一样)。

例如:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()



下面是一些如何实际实现生成器的Python示例,就好像Python没有为它们提供语法糖:

作为Python生成器:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

使用词法闭包而不是生成器

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

使用对象闭包而不是生成器(因为ClosuresAndObjectsAreEquivalent

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)



从编程的角度来看,迭代器实现为thunks

要将并发执行的迭代器,生成器和线程池等实现为thunks(也称为匿名函数),可以使用发送给具有调度程序的闭包对象的消息,并且调度程序将回答“消息”。

http://en.wikipedia.org/wiki/Message_passing

next ”是发送到闭包的消息,由“ iter ”调用创建。

有很多方法可以实现这个计算。我使用了变异,但通过返回当前值和下一个yielder,很容易做到没有变异。

这是一个使用R6RS结构的演示,但语义与Python完全相同。它是相同的计算模型,只需要在Python中重写它就需要改变语法。

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->



这是一个简单的例子:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

输出:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

我不是一个Python开发人员,但它看起来我yield保持程序流的位置和下一个循环从“yield”位置开始。似乎它正在等待那个位置,就在此之前,将值返回到外部,然后下一次继续工作。

这似乎是一个有趣而且很好的能力:D




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

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

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

for line in getNextLines():
    doSomeThing(line)

执行控制转移问题

执行foryield时,执行控件将从getNextLines()传送到循环。因此,每次调用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"



总之,该yield语句将您的函数转换为一个工厂,该工厂生成一个名为a的特殊对象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'语句,在该语句中它停止并“产生”该值或到达函数的末尾。在这种情况下,在第一次调用时,next()执行yield语句并生成'n',在下一次调用时它将执行increment语句,跳回'while',计算它,如果为true,它将停止并且再次屈服'n',它将继续这样,直到while条件返回false并且生成器跳转到函数的末尾。




许多人使用return而不是yield,但在某些情况下yield可以更有效,更容易使用。

这是一个yield绝对最适合的例子:

返回(在功能中)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

产量(功能)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

调用函数

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

这两个函数都做同样的事情,但yield使用三行而不是五行,并且有一个较少的变量需要担心。

这是代码的结果:

正如您所看到的,两个函数都做同样的事情。唯一的区别是return_dates()给出一个列表并yield_dates()给出一个生成器。

一个现实生活中的例子就像是逐行读取文件或者只是想制作一个生成器。




yield关键字简单地收集返回结果。想想yield就好return +=




另一个TL; DR

列表上的迭代器next()返回列表的下一个元素

迭代器生成器next()将动态计算下一个元素(执行代码)

您可以看到yield / generator作为一种从外部手动运行控制流的方法(如继续循环一步),通过调用next,无论流程如何复杂。

注意:生成器不是正常功能。它记住了先前的状态,如局部变量(堆栈)。有关详细说明,请参阅其他答案或文章。生成器只能迭代一次。你可以没有yield,但它不会那么好,所以它可以被认为是'非常好'的语言糖。




Related