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

小卤蛋 · 07月29日

yield关键字在Python中的用途是什么?

例如,我试图理解这段代码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)编写的,Jochen Schulz是一个很好的用于度量空间的Python库。这是完整源代码的链接:Module mspace

回答(21)
时间
C
Cathy
21 · 2020-07-29 09:35:18

(我下面的回答仅从使用Python生成器的角度讲,而不是生成器机制基础实现,它涉及堆栈和堆操作的一些技巧。)

在python函数中yield使用when 代替a return时,该函数变成了一个特殊的名称generator function该函数将返回一个generator类型的对象yield关键字是一个标志,通知蟒蛇编译器将特殊对待这样的功能。普通函数将在返回一些值后终止。但是在编译器的帮助下,可以将 generator函数视为可恢复的。也就是说,将恢复执行上下文,并且将从上次运行继续执行。在您显式调用return之前,它将引发StopIteration异常(这也是迭代器协议的一部分),或到达函数的结尾。我发现了很多关于引用的generator,但是这一个从中functional programming perspective是最易消化的。

(现在,我想根据我自己的理解来讨论其背后的原理generatoriterator基础。我希望这可以帮助您掌握迭代器和生成器基本动机。这种概念也出现在其他语言中,例如C#。)

据我了解,当我们要处理一堆数据时,通常先将数据存储在某个地方,然后再逐一处理。但是这种幼稚的方法是有问题的。如果数据量巨大,则预先存储它们是很昂贵的。因此data,为什么不直接存储自身,为什么不metadata间接存储某种形式,即the logic how the data is computed

有两种包装此类元数据的方法。

  1. 面向对象的方法,我们包装元数据as a class这就是所谓的iterator实现迭代器协议的人(即__next__()__iter__()方法)。这也是常见的迭代器设计模式
  2. 在功能方法上,我们包装了元数据as a function这就是所谓的generator function但是在后台,返回的generator object静态IS-A迭代器仍然存在,因为它也实现了迭代器协议。

无论哪种方式,都会创建一个迭代器,即某个可以为您提供所需数据的对象。OO方法可能有点复杂。无论如何,要使用哪一个取决于您。

理查德
20 · 2020-07-29 09:35:18

以下是一些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)
启人
19 · 2020-07-29 09:35:18

这是一个简单的示例:

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

18 · 2020-07-29 09:35:18

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

伊芙妮
17 · 2020-07-29 09:35:18

产量是一个对象

一个 return函数中的将返回单个值。

如果要让函数返回大量值,请使用yield

更重要的yield是一个障碍

就像CUDA语言中的barrier一样,它只有完成后才能转移控制权。

也就是说,它将从头开始运行您函数中的代码,直到命中为止 yield然后,它将返回循环的第一个值。

然后,其他所有调用将再次运行您在函数中编写的循环,返回下一个值,直到没有任何值可返回为止。

蓝染大人
16 · 2020-07-29 09:35:18

yield就像函数的返回元素一样。区别在于,yield元素将功能转换为生成器。生成器的行为就像函数一样,直到“屈服”为止。生成器将停止运行,直到下一次调用为止,并从与启动时完全相同的位置继续运行。您可以通过调用来获得所有“屈服”值的序列list(generator())

若合
15 · 2020-07-29 09:35:17

所有好的答案,但是对于新手来说有点困难。

我想你已经学会了 return声明。

作为一个比喻,returnyield是一对双胞胎。return表示“返回并停止”,而“收益”则表示“返回但继续”

  1. 尝试使用获取num_list return
def num_list(n):
    for i in range(n):
        return i

运行:

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

看,您只会得到一个数字,而不是列表。return永远不要让你高高兴兴,只实现一次就退出。

  1. 来了 yield

替换returnyield

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时间进行比较你可以理解returnreturn one of them,和yield作为return all of them这称为iterable

  1. 我们可以yield用以下步骤重写语句return
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输出”中检索一次。因此,它具有一个新的名称generator对象,如中所示Out[11]: <generator object num_list at 0x10327c990>

最后,作为一个隐喻,它可以:

  • return并且yield是双胞胎
  • list并且generator是双胞胎
留姬
14 · 2020-07-29 09:35:17

这是简单语言的示例。我将提供高级人类概念与低级Python概念之间的对应关系。

我想对数字序列进行运算,但是我不想为创建该序列而烦恼自己,我只想着重于自己想做的运算。因此,我执行以下操作:

  • 我打电话给你,告诉你我想要一个以特定方式产生的数字序列,让您知道算法是什么。
    此步骤对应于def生成器函数,即包含yield
  • 稍后,我告诉您,“好,准备告诉我数字的顺序”。
    此步骤对应于调用生成器函数,该函数返回生成器对象。请注意,您还没有告诉我任何数字。你只要拿起纸和铅笔。
  • 我问你,“告诉我下一个号码”,然后你告诉我第一个号码;之后,您等我问您下一个电话号码。记住您的位置,已经说过的电话号码以及下一个电话号码是您的工作。我不在乎细节。
    此步骤对应于调用.next()生成器对象。
  • …重复上一步,直到…
  • 最终,您可能会走到尽头。你不告诉我电话号码;您只是大声喊道:“抱马!我做完了!没有数字了!”
    此步骤对应于生成器对象结束其工作并引发StopIteration异常。生成器函数不需要引发异常。函数结束或发出时,它将自动引发return

这就是生成器的功能(包含的函数yield);它开始执行,在执行时暂停yield,并在要求输入.next()值时从上一个点继续执行。它在设计上与Python的迭代器协议非常吻合,该协议描述了如何顺序请求值。

迭代器协议最著名的用户是forPython中命令。因此,无论何时执行以下操作:

for item in sequence:

不管sequence是列表,字符串,字典还是如上所述的生成器对象,都没有关系结果是一样的:您从一个序列中逐个读取项目。

注意,def包含一个yield关键字的函数并不是创建生成器的唯一方法;这是创建一个的最简单方法。

有关更准确的信息,请阅读Python文档中有关迭代器类型yield语句生成器的信息。

神乐
13 · 2020-07-29 09:35:17

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

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

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

for line in getNextLines():
    doSomeThing(line)

执行控制转移陷阱

执行控制将从getNextLines()转移到 for yield时循环中。因此,每次调用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"
伊芙妮
12 · 2020-07-29 09:35:17

一个简单的例子来了解它是什么: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

输出为:

1 2 1 2 1 2 1 2
11 · 2020-07-29 09:35:17

在描述如何使用生成器的许多很棒的答案中,我还没有给出一种答案。这是编程语言理论的答案:

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

编程语言理论中的延续是一种更为基础的计算,但是由于它们很难推理而且也很难实现,因此并不经常使用。但是关于延续是什么的想法很简单:只是尚未完成的计算状态。在此状态下,将保存变量的当前值,尚未执行的操作等。然后,在程序的稍后某个点,可以调用延续,以便将程序的变量重置为该状态,并执行保存的操作。

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

在延续传递样式(CPS)中,延续只是普通的函数(仅在函数是第一类的语言中),程序员可以对其进行显式管理并传递给子例程。用这种风格,程序状态由闭包(以及恰好在其中编码的变量)表示,而不是驻留在栈中某个位置的变量。管理控制流的函数接受连续作为参数(在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)

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

在不失一般性的前提下,本文的其余部分将连续性概念化为CPS,因为它很容易理解和阅读。


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

def f():
  while True:
    yield 4

这显然是一个合理的迭代器,其行为已得到很好的定义-每次生成器对其进行迭代时,它都将返回4(并且永远如此)。但是,在考虑迭代器(即for x in collection: do_something(x)时,可能不会想到可迭代的原型类型此示例说明了生成器的功能:如果有什么是迭代器,生成器可以保存其迭代状态。

重申一下:连续可以保存程序堆栈的状态,而生成器可以保存迭代的状态。这意味着延续比生成器强大得多,但是生成器也非常简单。它们对于语言设计者来说更容易实现,对程序员来说也更容易使用(如果您有时间要燃烧,请尝试阅读并理解有关延续和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关键字。

宝儿
10 · 2020-07-29 09:35:16

我本来打算发布“阅读Beazley的“ Python:基本参考”的第19页,以快速了解生成器”,但是已经有许多其他人已经发布了不错的描述。

另外,请注意,它们yield可以在协程中用作生成器函数的双重用法。尽管它与您的代码段用法不同,(yield)但是可以用作函数中的表达式。当调用者使用该send()方法向该方法发送值时,协程将执行直到(yield)遇到一条语句。

生成器和协程是设置数据流类型应用程序的一种很酷的方法。我认为有必要了解该yield语句在函数中的其他用法

王者一打九
9 · 2020-07-29 09:35:16

TL; DR

代替这个:

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

做这个:

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

每当您发现自己从头开始建立清单时,就yield逐一列出

这是我第一次屈服。


yield是一种含糖的方式来表达

建立一系列的东西

相同的行为:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

不同的行为:

收益是单次通过:您只能迭代一次。当一个函数包含一个yield时,我们称其为Generator函数迭代器就是它返回的内容。这些术语在揭示。我们失去了容器的便利性,但获得了按需计算且任意长的序列的功效。

产量懒惰,它推迟了计算。当您调用函数时,其中包含yield的函数实际上根本不会执行。它返回一个迭代器对象对象记住它从何处中断。每次您调用next()迭代器(这在for循环中发生)时,执行都会向前推进到下一个收益。return引发StopIteration并结束系列(这是for循环的自然结束)。

产量多才多艺数据不必全部存储在一起,可以一次存储一次。它可以是无限的。

>>> 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(square_yield(4))
[0, 1, 4, 9]

单词的出色选择,yield因为两种含义都适用:

产量 —生产或提供(如在农业中)

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

屈服 -让步或放弃(如在政治权力中一样)

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

神乐
8 · 2020-07-29 09:35:16

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

yield from <expr>

PEP 380-委托给子生成器的语法

提出了一种语法,供生成器将其部分操作委托给另一生成器。这允许包含“ yield”的一段代码被分解出来并放置在另一个生成器中。另外,允许子生成器返回一个值,并且该值可用于委派生成器。

当一个生成器重新产生另一个生成器的值时,新语法还为优化提供了一些机会。

此外,将引入(自Python 3.5起):

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

为了避免将协程与常规生成器混淆(今天yield两者都使用)。

飞云
7 · 2020-07-29 09:35:16

yield关键字被减少到两个简单的事实:

  1. 如果编译器在函数内部的任何位置检测到yield关键字,则该函数不再通过该语句返回相反,它立即返回一个懒惰的“等候名单”对象称为发电机return
  2. 生成器是可迭代的。什么是可迭代的就像是listor或setor range或dict-view一样,它带有用于以特定顺序访问每个元素内置协议

简而言之:生成器是一个懒惰的,增量待定的list,并且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),也可以yield在while循环中使用关键字来实现。

请注意:生成器实际上可以用于更多事情,例如实现协程或不确定性编程或其他优雅的事情。但是,我在这里提出的“惰性列表”观点是您会发现的最常见用法。


幕后花絮

这就是“ Python迭代协议”的工作方式。就是说,当你做什么的时候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))那些绝对需要克隆生成器的人(例如,正在进行骇人的骇人的元编程的人)可以itertools.tee在绝对必要的情况下使用,因为可复制的迭代器Python PEP标准建议已被推迟。

路易
6 · 2020-07-29 09:35:16

收益给您一个发电机。

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

如您所见,在第一种情况下,foo将整个列表立即保存在内存中。对于包含5个元素的列表来说,这并不是什么大问题,但是如果您想要500万个列表,该怎么办?这不仅是一个巨大的内存消耗者,而且在调用该函数时还花费大量时间来构建。

在第二种情况下,bar只需为您提供一个生成器。生成器是可迭代的-意味着您可以在for循环等中使用它,但是每个值只能被访问一次。所有的值也不会同时存储在存储器中。生成器对象“记住”您上次调用它时在循环中的位置-这样,如果您使用的是一个迭代的(例如)计数为500亿,则不必计数为500亿立即存储500亿个数字以进行计算。

再次,这是一个非常人为的示例,如果您真的想计数到500亿,则可能会使用itertools。:)

这是生成器最简单的用例。如您所说,它可以用来编写有效的排列,使用yield可以将内容推入调用堆栈,而不是使用某种堆栈变量。生成器还可以用于特殊的树遍历以及所有其他方式。

理查德
5 · 2020-07-29 09:35:16

对于那些偏爱简单工作示例的人,请在此交互式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
G
Gil村村
4 · 2020-07-29 09:35:15

它正在返回发电机。我对Python并不是特别熟悉,但是我相信,如果您熟悉Python,它与C#的迭代器块是一样的东西

关键思想是,编译器/解释器/无论做什么都做一些技巧,以便就调用者而言,他们可以继续调用next(),并且将继续返回值- 就像Generator方法已暂停一样现在显然您无法真正“暂停”方法,因此编译器将构建状态机,以供您记住当前位置以及局部变量等。这比自己编写迭代器要容易得多。

武藏
3 · 2020-07-29 09:35:15

还有另外一件事要提到:yield的函数实际上不必终止。我写了这样的代码:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

然后我可以在其他代码中使用它:

for f in fib():
    if some_condition: break
    coolfuncs(f);

它确实有助于简化某些问题,并使某些事情更易于使用。

阿良
2 · 2020-07-29 09:35:15

理解的捷径 yield

当您看到带有yield语句的函数时,请应用以下简单技巧,以了解将发生的情况:

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

这个技巧可能会让您对函数背后的逻辑yield有所了解,但是实际发生的事情与基于列表的方法发生的事情明显不同。在许多情况下,yield方法也将具有更高的内存效率和更快的速度。在其他情况下,即使原始函数运行正常,此技巧也会使您陷入无限循环。请继续阅读以了解更多信息...

不要混淆您的Iterable,Iterators和Generators

首先,迭代器协议 -当您编写时

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

Python执行以下两个步骤:

  1. 获取以下项的迭代器mylist

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

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

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

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

事实是,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

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

因此,生成器对象有点像适配器-一端通过公开__iter__()next()保持for循环满意的方法展示了迭代器协议但是,在另一端,它恰好运行该函数以从中获取下一个值,并将其放回暂停模式。

为什么使用发电机?

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

N
Near
1 · 2020-07-29 09:35:15

yield就像return-它返回您告诉的内容(作为生成器)。不同之处在于,下次调用生成器时,执行将从上一次调用yield语句开始。与return不同的是,在产生良率时不会清除堆栈帧,但是会将控制权转移回调用方,因此下次调用该函数时,其状态将恢复。

就您的代码而言,该函数get_child_candidates的作用类似于迭代器,以便在扩展列表时,它一次将一个元素添加到新列表中。

list.extend调用迭代器,直到耗尽为止。在您发布的代码示例的情况下,只返回一个元组并将其附加到列表中会更加清楚。

你的回答

加载中...
⌘+Return 发表
发表

温馨提示:登录后可发表评论或回复

关闭,朕知道了

扫码关注微信公众号或小程序