Python中的yield关键字

Python中的yield关键字

来源: https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do

这是stackoverflow上一个关于yield关键字的问题以及它被推荐次数最高的一个答案

问题:

Python中的yield关键字是什么?它是用来做什么的?
例如,下面的这份代码:

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方法被调用时,发生了什么?是一个list对象被返回了?还是一个单独的元素?或者是它再次被调用了?这个调用什么时候会结束呢?


答案

在理解yield关键字之前,需要先理什么是迭代器(iterables)

迭代器(Iterables)

当创建一个列表后,你就可以一个接着一个地读取它的元素, 这种操作就叫做迭代(iteration):

>>> 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 ...语法的对象,都是一个可迭代对象: 例如 listsstringfile等等
由于可以随意地读取这些对象中的元素,这些可迭代对象使用起来非常方便,但是这样做会把可迭代对象所有的值都存在内存中,当有很多个元素的时候,这样做可能并不合适。

生成器(Generators)

生成器是可以迭代的,但它是一个只可以读取一次的迭代器。生成器并不会把所有的值都存在内存中,而是实时地生成数据

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

除了把[]换成()之外,生成器看起来和列表推导式没有什么不同。但是,调用之后,你不能再次使用for i in mygenerator,因为生成器只可以被迭代一次:它计算出0,然后计算出1,同时丢弃掉0,然后最终计算出4,丢弃掉1,一个接着一个。

Yield 关键字

yield是一个类似与return的关键字,只是这个函数会返回的是一个生成器(Generator)

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

这个例子并没有什么用处,但是,我们可以看出,createGenerator函数会返回一大批你只需要读取一次的值。

为了理解yield,你必须理解: 当你调用这个函数时,你写在函数体内部的代码并没有运行。这个函数只是返回了一个生成器对象(这可能有一点tricky)。
然后,每次当你使用for进行迭代的时候,你的代码才会执行。

接下来是关键的部分
使用for第一次调用从你的函数中创建出的生成器对象时,将会执行从函数起始位置到yield所在位置的代码并返回这个循环(函数内部定义)的第一个值。随后的每一次调用(for)都会继续执行你的函数内部的下一轮循环,并返回下一个值,直到没有值可以返回为止。

原文:
The first time the for calls the generator object created from your function, it will run the code in your function from the beginning until it hits yield, then it'll return the first value of the loop. Then, each other call will run the loop you have written in the function one more time, and return the next value, until there is no value to return.

如果生成器内部没有yield关键字,那么这个生成器将会被认为是空的。这可能是因为循环结束了,或者是没有满足if/else判断条件

代码解析

  • 生成器
# Here you create the method of the node object that will return the generator
# 在node对象中创建一个返回生成器的方法
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 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 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)
# 在candidates中循环(在初始时值含有一个元素。根节点)
while candidates:

    # Get the last candidate and remove it from the list
    # 获取最近的一个candidate并将它从列表中移除
    node = candidates.pop()

    # Get the distance between obj and the candidate
    # 获取obj和candidate的距离
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    # 如果距离符合要求,填入result中
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidates 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列表中candidate的子节点添加到candidates列表中
    # 以确保遍历了candidate所有的子节点以及子节点的子节点。。。
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

这份代码包含了几个巧妙的地方:

  • 我们对一个列表进行迭代的同时,列表还在不断地扩展。尽管这样做有可能导致无限迭代,但是它是一个简单的遍历所有数据的方式。在这个case中,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))穷尽了生成器的所有值,但是while循环一直在创建新的生成器对象,由于传入的节点不同,这些生成器对象会产生不同的值。
  • extend() 是一个迭代器方法,作用于迭代器本身并把值添加到列表中

通常,我们会传一个list参数

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

但是,上面的代码是一个生成器,这样做很巧妙,因为:

  1. 你不需要读取每个值两次
  2. 你可以有很多子对象,但是不必将他们都存储在内存里面

这份代码是可以运行的,因为Python并不关心方法的参数是否是一个list对象,Python只期望它是一个可迭代的对象,所以参数可以是列表、元组、字符串、生成器...这叫做duck typing,这也是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
...

这能被用来做资源访问权限控制

Itertools,你最好的朋友

itertools模块包含了很多特殊的迭代方法,你是否曾经想过复制一个迭代器?链接两个迭代器?将嵌套的列表分组?执行不创建新列表的zip/map操作?

所有的这些,只需要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__() 方法)的过程。可迭代对象是通过它可以获取到一个迭代器的任一对象。迭代器是那些允许你迭代可迭代对象的对象。
更多细节可以阅读 http://effbot.org/zone/python-for-statement.htm

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容