要了解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
,文件。..
这些iterables很方便,因为您可以随心所欲地读取它们,但是您将所有值存储在内存中,当您有很多值时,这并不总是您想要的。
发电机
生成器是迭代器,一种可迭代的你只能迭代一次。 生成器不会将所有值存储在内存中,它们会动态生成值:
>>> 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
时。 这可能是因为循环已经结束,或者因为你不再满足an"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]
但是在你的代码中,它得到了一个生成器,这很好,因为:
- 您不需要读取两次值。
- 你可能有很多孩子,你不希望他们都存储在内存中。
它的工作原理是因为Python不关心方法的参数是否是列表。 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
...
注意:对于Python3,使用print(corner_street_atm.__next__())
或print(next(corner_street_atm))
它对于控制对资源的访问等各种事情都很有用。
Itertools,你最好的朋友
itertools模块包含用于操作iterables的特殊函数。 曾经想复制一个发电机吗?
链两个发电机? 在一个单行嵌套列表中分组值? 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)]
理解迭代的内在机制
迭代是一个隐含iterables(实现__iter__()
方法)和iterators(实现__next__()
方法)的过程。
Iterables是您可以从中获取迭代器的任何对象。 迭代器是允许您迭代可迭代对象的对象。
在这篇文章中有更多关于for
循环如何工作的内容。
理解的捷径
yield
当你看到一个带有
yield
语句的函数时,应用这个简单的技巧来理解会发生什么:result = []
。yield expr
替换为result.append(expr)
。return result
。yield
语句! 阅读并找出代码。这个技巧可能会让你了解函数背后的逻辑,但是
yield
实际发生的事情与基于列表的方法有很大的不同。 在许多情况下,yield方法的内存效率更高,速度也更快。 在其他情况下,这个技巧会让你陷入无限循环,即使原始函数工作得很好。 继续阅读以了解更多。..不要混淆你的迭代器,迭代器和生成器
首先,迭代器协议-当你写
Python执行以下两个步骤:
获取
mylist
的迭代器:Call
iter(mylist)
->这返回一个带有next()
方法(或Python3中的__next__()
)的对象。[这是大多数人忘记告诉你的步骤]
使用迭代器循环遍历项:
在从步骤1返回的迭代器上继续调用
next()
方法。 来自next()
的返回值被分配给x
并执行循环体。 如果从next()
内引发异常next()
,则意味着迭代器中没有更多值并且退出循环。事实是Python在想要循环对象的内容时执行上述两个步骤-因此它可能是for循环,但也可能是像
otherlist.extend(mylist)
这样的代码(其中otherlist
是Python列表)。这里
mylist
是一个可迭代的,因为它实现了迭代器协议。 在用户定义的类中,您可以实现__iter__()
方法以使类的实例可迭代。 此方法应返回一个迭代器。 迭代器是具有next()
方法的对象。 可以在同一个类上实现__iter__()
和next()
,并具有__iter__()
returnself
。 这将适用于简单的情况,但当您希望两个迭代器在同一时间循环相同的对象时则不行。这就是迭代器协议,很多对象都实现了这个协议:
__iter__()
的用户定义类。请注意,一个
for
循环不知道它正在处理什么样的对象-它只是遵循迭代器协议,并且很乐意在它调用next()
时获得一个又一个的项目。 内置列表一个接一个地返回它们的项目,字典一个接一个地返回键,文件一个接一个地返回行等等。 发电机返回。.. 这就是yield
的用武之地:而不是
yield
语句,如果在f123()
中有三个f123()
语句,只有第一个语句会被执行,并且函数将退出。 但f123()
不是普通的功能。 当f123()
被调用时,它不会返回yield语句中的任何值! 它返回一个生成器对象。 此外,该函数并没有真正退出-它进入暂停状态。 当for
循环试图在生成器对象上循环时,函数在它先前返回的yield
之后的下一行从其暂停状态恢复,执行下一行代码,在这种情况下,是一个yield
语句,并将其作为下一项返回。 这种情况发生在函数退出之前,此时生成器引发next()
,循环退出。所以generator对象有点像一个适配器-在一端它展示了迭代器协议,通过公开
__iter__()
和next()
方法来保持for
循环的快乐。 然而,在另一端,它运行函数足以从中获取下一个值,并将其放回挂起模式。为什么要使用发电机?
通常,您可以编写不使用生成器但实现相同逻辑的代码。 一种选择是使用我之前提到的临时列表'技巧'。 这在所有情况下都不起作用,例如,如果你有无限循环,或者当你有一个非常长的列表时,它可能会使内存使用效率低下。 另一种方法是实现一个新的可迭代类SomethingIter,它保持实例成员中的状态,并在它的
next()
(或Python3中的__next__()
)方法中执行下一个逻辑步骤。 根据逻辑,next()
方法中的代码最终可能看起来非常复杂并且容易出现错误。 在这里,发电机提供了一个干净和简单的解决方案。