为了理解
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 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 are 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 the distance is ok, then you can fill in the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# Add the children of the candidate to the candidate's list
# so the loop will keep running until it has 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]
但在你的代码中,它得到了一个生成器,这是很好的,因为:
- 你不需要两次读取值。
- 你可能有很多子元素,而且你不希望它们都存储在内存中。
并且它能工作是因为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
循环中了解更多信息。