Skip to content

Latest commit

 

History

History
313 lines (223 loc) · 14 KB

301.md

File metadata and controls

313 lines (223 loc) · 14 KB

#Python中yield关键词的作用是什么?

原问题地址:http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do-in-python

##问题:

在Python中yield关键字的作用是什么?它是做什么用的?

例如,我正试图理解这段代码:

def node._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 = list(), [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方法的时候发生了什么呢?返回某一个列表?返回某一个元素?它是不是再次被调用了?后续调用什么时候才会停下来?

注:这里的代码是Jochen Schulz(jrschulz)写的。他建立了一个很棒的度量空间的Python库。点击这个链接就可以看到完整的源代码:Module mspace

##答案 1:

要了解yield是做什么用的,你就必须了解什么是生成器。在了解生成器之前,先要了解可迭代。

###可迭代

当你创建一个列表后,可以逐个读取它的元素。对一个列表的逐个读取被称为迭代。

>>> 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..."的对象都是可迭代的,如:列表, 字符串, 文件…

这些可迭代对象使用起来很方便,因为你可以随心所欲。但是所有的值都存在了内存中,当有大量数据的时候,这就不是你想要的结果了。

###生成器

生成器是迭代器,但只能遍历一次。这是因为并非所有的值都在内存中,它们被实时生成:

>>> 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

Yield是一个关键词,它的用法就像return,只不过它返回一个生成器。

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

这段代码示例没什么用处,但是当你知道函数要返回大量的并且仅需要读一次的数据时,就会觉得它是方便的。

要掌握yield,你就必须明白,当你调用这个函数时,写在函数体里的代码并没有运行。该函数只返回到生成器对象,这可有点棘手。

接下来,每当for使用生成器的时候,你的代码就会被运行。

现在的困难部分是:

第一次用for调用你的函数所创建的生成器对象时,它将会从头开始运行函数中的代码,直到遇到yield,才会返回该循环中的第一个值。然后,每次调用都会再运行一次函数中你所写的循环,并返回下一个值,直到没有什么值可以返回。

一旦函数在运行过程中不再遇到yield,生成器就会被认为是空的。这可能是因为循环已经结束,或者因为你不再满足"if/else"条件了。

###代码说明

生成器:

#这里是你创建node对象的方法,它将返回生成器
def node._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 = list(), [self]

#候选项的循环(它们一开始只包含一个元素)
while candidates:

#找到最后的候选项并且从列表中删除它
    node = candidates.pop()

#获取obj(当前对象)和候选项之间的距离
    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))

返回结果

此代码包含几个巧妙的部分:

  • 该循环迭代一个列表,但在迭代过程中列表会扩大。这是运行所有的嵌套数据的一个简洁的方法。只不过这样做有点危险,因为你最终可能会遇到死循环。在这个示例中,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))会读取这个生成器的所有的值,但while循环不断创造新的生成器对象,它们会从以前的生成器对象中产生出不同的值,因为所应用的节点不同。

  • 列表的extend()方法,允许可迭代对象并把值添加到列表中。

通常我们给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            # 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
...

它可以有利于各种事情,如:控制对于资源的访问。

###迭代工具,你最好的朋友

itertools模块有一些处理可迭代对象的专用方法。你是否曾经希望复制一个生成器?链接两个生成器?用一行代码把嵌套列表中的值进行分组?在无需创建另一个列表进行Map/Zip

那么,import itertools吧。

一个例子,让我们看看4匹赛马的抵达顺序:

>>> 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__()方法)。可迭代对象是可以获得迭代器的任何对象。迭代器允许你对可迭代对象进行迭代。

##答案 2

###深刻理解yield的捷径

(关于Grokking

当你看到一个用yield语句的函数时,应用这个简单的技巧来了解将会发生的情况:

  1. 在函数的开始处插入一行result = []
  2. 把每一处yield语句替换为result.append(expr)
  3. 在函数的末端插入一行return result
  4. 耶!再也没有用yield语句了。阅读并理解代码。
  5. 把这个函数与初始定义进行比较。

这个技巧可能会使你对函数背后的逻辑有所了解,但使用yield和使用列表的函数执行起来是有着显著的不同。在很多情况下,使用yield会使得内存效率更高、更快。在其他情况下,即使原函数运行良好,这个技巧也会使你陷入死循环中。继续阅读以便加深了解…

###不要把你的可迭代对象、迭代器和生成器弄混

首先,迭代器协议。当你书写下面内容的时候

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()中没有使用yield语句,而是使用了三个return 语句,那么只有第一个会被执行,函数将会退出。但f123()不是普通的函数。当你调用f123()的时候,它不会到返回到yield语句中的任何一个值!它返回到一个生成器对象。此外,该函数并没有真正退出,它进入了一个挂起(暂停)状态。当for 循环尝试对生成器对象进行循环时,该函数将从它的挂起状态恢复运行,直到下一个yield语句,并同前运行。这种情况一直持续到函数退出为止,最后,生成器就会抛出StopIteration异常,循环也随之结束。

因此,生成器对象有点儿像适配器。它的一端展示了迭代器协议,利用__iter__()next()方法保持for循环的畅通。然而,在另一端,它所运行的函数只能够支持它获取下一个值,并把它返回到挂起模式。

###为什么要使用生成器?

通常,你在写代码时,不使用生成器也能实现相同的逻辑。一个选择是使用我之前提到过的临时列表的“把戏”。这种做法并非在所有的情况下都适用。例如,如果你遇到了死循环,或者当你有一个很长的列表时,它可能会导致内存使用效率低下。另一种方法是使用一个新的可迭代的类型。SomethingIter适用于实例成员,并在它的next() 或者Python 3中的__next__()方法中执行下一个逻辑步骤。根据逻辑,在next()方法内部的代码可能看起来很复杂,也容易出现错误。生成器可以为这种情况提供一个干净且容易的解决方案。


打赏帐号:[email protected](支付宝),qiwsir(微信号)