“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 库。这是完整源代码的链接: Module mspace

原文由 Alex. S. 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 678
2 个回答

要了解 yield 做了什么,您必须了解 生成器 是什么。在你了解生成器之前,你必须了解 iterables

可迭代对象

创建列表时,您可以一一阅读其项目。一项一项地读取它的项目称为迭代:

 >>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist 是一个 可迭代 的 .当您使用列表推导时,您会创建一个列表,因此是一个可迭代的:

 >>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

您可以使用“ for... in... ”的所有内容都是可迭代的; lists , strings , 文件…

这些可迭代对象很方便,因为您可以随心所欲地读取它们,但是您将所有值存储在内存中,当您有很多值时,这并不总是您想要的。

发电机

生成器是迭代器,一种 只能迭代一次 的可迭代对象。生成器不会将所有值存储在内存中, 它们会即时生成值

 >>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

除了您使用 () 而不是 [] 之外,它是一样的。但是,您 不能 第二次执行 for i in mygenerator 因为生成器只能使用一次:它们计算 0,然后忘记它并计算 1,然后一个接一个地结束计算 4。

屈服

yield 是一个类似于 return 的关键字,除了该函数将返回一个生成器。

 >>> def create_generator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = create_generator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

这是一个无用的示例,但是当您知道您的函数将返回大量值而您只需要读取一次时,它会很方便。

要掌握 yield ,你必须明白, 当你调用函数时,你写在函数体中的代码是不会运行的。 该函数只返回生成器对象,这有点棘手。

然后,您的代码将在每次 for 使用生成器时从中断处继续。

现在最困难的部分:

第一次 for 调用从您的函数创建的生成器对象,它将从头开始运行您的函数中的代码,直到它命中 yield ,然后它将返回第一个值的循环。然后,每个后续调用将运行您在函数中编写的循环的另一次迭代并返回下一个值。这将一直持续到生成器被认为是空的,这发生在函数运行时没有命中 yield 。这可能是因为循环已经结束,或者因为您不再满足 "if/else"


你的代码解释

发电机:

 # Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

呼叫者:

 # Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

此代码包含几个智能部分:

  • 循环在一个列表上进行迭代,但在迭代循环时列表会扩展。这是一种遍历所有这些嵌套数据的简洁方法,即使它有点危险,因为您最终可能会陷入无限循环。在这种情况下, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) 耗尽了生成器的所有值,但是 while 不断创建新的生成器对象,这些生成器对象将产生与以前的值不同的值,因为它没有应用于同一节点。

  • extend() 方法是一个列表对象方法,它需要一个可迭代对象并将其值添加到列表中。

通常我们传递一个列表给它:

 >>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

但是在您的代码中,它有一个生成器,这很好,因为:

  1. 您不需要读取两次值。
  2. 您可能有很多孩子,并且您不希望他们都存储在内存中。

它之所以有效,是因为 Python 不关心方法的参数是否为列表。 Python 需要可迭代对象,因此它可以处理字符串、列表、元组和生成器!这被称为鸭子类型,这也是 Python 如此酷的原因之一。但这是另一个故事,另一个问题……

你可以在这里停下来,或者阅读一下以了解生成器的高级用法:

控制发电机耗尽

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

注意: 对于 Python 3,请使用 print(corner_street_atm.__next__())print(next(corner_street_atm))

它可以用于控制对资源的访问等各种事情。

Itertools,你最好的朋友

itertools 模块包含操作可迭代对象的特殊函数。曾经想复制一个生成器吗?链接两个发电机?使用单线对嵌套列表中的值进行分组? Map / Zip 不创建另一个列表?

然后只是 import itertools

一个例子?让我们看看四马比赛的可能到达顺序:

 >>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

了解迭代的内在机制

迭代是一个暗示可迭代(实现 __iter__() 方法)和迭代器(实现 __next__() 方法)的过程。可迭代对象是您可以从中获取迭代器的任何对象。迭代器是允许您迭代可迭代对象的对象。

在这篇文章中有更多关于 for 循环如何工作 的内容。

原文由 e-satis 发布,翻译遵循 CC BY-SA 4.0 许可协议

理解的捷径 yield

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

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

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

不要混淆 Iterables、Iterators 和 Generators

一、 迭代器协议——当你写的时候

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

Python 执行以下两个步骤:

  1. 获取 mylist 的迭代器:

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

\[这是大多数人忘记告诉你的步骤\]
  1. 使用迭代器遍历项目:

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

事实上,Python 会在它想要 遍历 对象内容的任何时候执行上述两个步骤——因此它可能是一个 for 循环,但它也可能是类似 otherlist.extend(mylist) 的代码(其中 otherlist 是一个 Python 列表)。

这里 mylist 是一个 可迭代 对象,因为它实现了迭代器协议。在用户定义的类中,您可以实现 __iter__() 方法来使您的类的实例可迭代。此方法应返回一个 _迭代器_。迭代器是一个具有 next() 方法的对象。 It is possible to implement both __iter__() and next() on the same class, and have __iter__() return 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 happy loop47677 .然而,在另一端,它运行该函数刚好足以从中获取下一个值,并将其放回挂起模式。

为什么要使用生成器?

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

原文由 user28409 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题