python中yield语句详解——译自stack overflow

原题目链接: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 方法被调用时,发生了什么?一个列表被返回了吗?还是一个元素被返回了?它又被再次调用了吗?接下来的调用什么时候停止?

下面是得票最多的答案:来自e-satis

为了理解yield做了什么,你必须知道什么是生成器,而在此之前你需要了解什么是迭代器

迭代

当你创建了一个列表,你可以一个接一个地读取它的元素。像这样一个接一个读取它元素的行为被称做迭代:

>>>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…”结构来进行操作的对象都是可迭代的:列表,字符串,文件……
这些迭代对象用起来十分顺手因为你可以按你想的那样读取它们,但是你需要将所有元素的值都存储在内存中,如果你有很多数据,显然你并不会愿意这么做。

生成器

生成器属于迭代器,但你只可以在生成器上迭代一次。因为生成器把所有的值存储在内存中,它们动态地生成这些值。

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

  # 每一次你使用生成器对象这里的代码都会被调用:

  # 如果在node对象左边仍然有一个child
  # 并且 如果distance满足条件, 返回下一个child
  if self._leftchild and distance - max_dist < self._median:
      yield self._leftchild

  # 如果在node对象右边仍然有一个child
  # 并且 如果distance满足条件, 返回下一个child
  if self._rightchild and distance + max_dist >= self._median:
      yield self._rightchild

  # 当函数执行到这里,生成器可以被认为是空的
  # 生成器中不再有两个值:left child和right child

调用:

# 创建一个空列表和一个包含当前对象引用的列表
result, candidates = list(), [self]

# 对candidates的循环 (一开始他们只包含一个元素)
while candidates:

    # 获得最后一个candidate并把它从列表中移除
    node = candidates.pop()

    # 获得obj和candidate之间的distance
    distance = node._get_dist(obj)

    # 如果distance满足条件, 你就可以拓展result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # 在candidates中添加candidate的children
    # 所以循环将一直执行直到其遍历了candidate的children的children…
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

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

  1. 循环在列表上迭代但同时列表又在扩展自身,这是种比较简洁的方法来遍历所有的嵌套数据,虽然其有一定的危险性因为你可能使用一个无限循环。在你的代码中,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))囊括了生成器的所有的值,但是while语句一直在创建新的生成器对象而这些生成器对象会产生不同的值。
  2. extend()方法是列表对象的方法,其需要一个可迭代对象作为参数并把这个可迭代对象的值加入列表。

一般我们向它传递一个列表:

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

但在你的代码里这个方法获得了一个生成器,这很好因为:

1、你不需要读取这些值两次。
2、可能会有很多children对象但你不想把他们都存放在内存中。

它能工作是因为python不关心extend方法的参数是否是一个列表,python期望这个方法的参数是可迭代对象所以字符串、列表、元组和生成器都可以!这叫做填鸭式输入(duck typing),这也是python如此cool的一个原因。但这又是另一个话题了……

你可以就此打住了,也可以再读一点来看看生成器的一个高级用法:

控制生成器的输出

>>> class Bank(): # 我们来建立一家银行并搭一些ATM机
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # 一切正常时,你想要多少,ATM机就给你多少
>>> 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 # 金融危机来了,没钱了!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # 甚至对新的ATM机来说也是这样
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # 问题是,即使危机过去了,ATM机还是空的
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # 重新搭一个来投入商业运营
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

这对很多东西来说都很有用例如控制某个资源的入口。

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循环如何工作

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值