iterator - Python中的yield关键字有什么作用?

  显示原文与译文双语对照的内容

yield 关键字在 python 中的用法? 它的用途?

例如我试图理解这里代码( ** ):


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 库。 这是指向完整源的链接: 模块 mspace

时间:

要理解什么是 yield,你必须理解哪些生成器是。 在生成iterables之前。

Iterables

当你创建一个列表时,你可以逐个读取它的项目,它被称为迭代:


>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3

Mylist是一个 iterable 。当你使用列表理解时,你会创建一个列表,这样一个 iterable:


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

你可以使用 "for... in..." 上的所有内容是一个 iterable: 列表,字符串,文件。。这些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

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,然后返回循环的第一个值。 然后,每次调用都会再次运行你在函数中写入的循环,并返回下一个值,直到没有返回值。

一旦函数运行,生成器被认为是空的,但不会再产生屈服。 可能是因为循环已经结束,或者你不再满足于 "if/else"

你的代码解释了

生成器:


# Here you create the method of the node object that will return the generator
def node._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)
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 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.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() 方法是一个列表对象方法,它需要一个iterable并将它的值添加到列表。

通常我们会向它传递一个列表:


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

但在你的代码中,它会得到一个生成器,这很好,因为:

  1. 不需要两次读取值。
  2. 你可以有很多孩子,你不希望它们都存储在内存中。

它之所以工作,是因为 python 不关心方法的参数是否是列表。 python 需要 iterables,因此它可以处理字符串,列表,元组和生成器 ! 这叫做duck类型,是 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,你最好的朋友

itertools模块包含操作iterables的特殊函数。 希望复制生成器? 链两个生成器用一个衬垫在嵌套列表中分组值? 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)]

理解迭代的内部机制

迭代是一个表示 iterables ( 实现 __iter__() 方法) 和迭代器( 实现 __next__() 方法)的过程。 Iterables是任何可以从中获取迭代器的对象。 迭代器是允许你迭代iterables的对象。

关于在本文中的更多关于loop循环如何工作

指向 Grokkingyield的快捷方式

当你看到带有 yield 语句的函数时,请应用这个简单的技巧来了解将发生什么:

  1. 在函数的开始处插入一行 result = []
  2. 将每个 yield expr 替换为 result.append(expr)
  3. 在函数的底部插入一行 return result
  4. yield - 不再有语句 ! 阅读并找出代码。
  5. 比较函数与原始定义。

这个技巧可能会给你一个想法的背后的逻辑函数,但实际上 yield 显著不同,基于列表中会发生什么。 在许多情况下,收益率方法将更多的内存效率和更快的内存。 在其他情况下这个技巧会让你陷入无限循环,即使原来的函数就可以了。 阅读以了解更多信息。。

不要混淆你的Iterables,迭代器和生成器

首先,当你写的时候,的迭代器协议


for x in mylist:
. . .loop body...

python 执行以下两个步骤:

  1. 获取 mylist的迭代器:

    调用 iter(mylist) -> 返回一个具有 next() 方法( 或者 __next__() 在 python 3中)的对象。

    [This is the step most people forget to tell you about ]

  2. 使用迭代器在项目上循环:

    继续调用 next() 方法从 1步返回的迭代器。 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

而不是 yield 语句,如果你有三个 return 语句 f123() 只有第一个会执行,函数将退出。 但 f123() 不是普通函数。 当 f123() 叫做不返回任何值的产量报表! 它返回一个生成器对象。 另外,函数并没有真正退出- 它进入挂起状态。 当 for 循环试图循环发电机对象,函数恢复挂起状态,直到下一次运行 yield 声明并返回,作为下一个项目。 这种情况发生在函数退出之前,此时发电机提出了 StopIteration, 循环退出。

生成器对象是有点像一个适配器一端,它展现了迭代器协议,通过公开 __iter__()next() 方法保持 for 循环快乐。 然而,另一端运行函数就足以让下一个值,并将其在暂停模式。

为什么使用生成器?

通常你可以编写不使用生成器但实现相同逻辑的代码。 一个选项是使用我前面提到的临时列表'戏法'。 为 比如 不会工作在所有情况下,如果你有无限循环,或者它可能低效利用的内存当你有一个很长的名单。 另一种方法是实现一个新的iterable类 SomethingIter,它保持实例成员的状态,并执行下一个逻辑步骤,它是 next() ( 或者 __next__() 在 python 3中) 方法。 根据逻辑, next() 方法内的代码可能会看起来非常复杂,容易 Bug 。 这里的发电机提供了一个干净方便的解决方案。

以这种方式思考:

对于具有 next() 方法的对象来说,迭代器只是一个复杂的探测术语。 因此yield-ed函数最终变成了类似这样的东西:

原始版本:


def some_function():
 for i in xrange(4):
 yield i

for i in some_function():
 print i

这基本上就是 python 解释器对上面代码执行的操作:


class it:
 def __init__(self):
 #start at -1 so that we get 0 when we add 1 below.
 self.count = -1
 #the __iter__ method will be called once by the for loop.
 #the rest of the magic happens on the object returned by this method.
 #in this case it is the object itself.
 def __iter__(self):
 return self
 #the next method will be called repeatedly by the for loop
 #until it raises StopIteration.
 def next(self):
 self.count += 1
 if self.count <4:
 return self.count
 else:
 #a StopIteration exception is raised
 #to signal that the iterator is done.
 #This is caught implicitly by the for loop.
 raise StopIteration 

def some_func():
 return it()

for i in some_func():
 print i

为了更深入地了解场景背后发生了什么,可以将For循环rewritten重写为:


iterator = some_func()
try:
 while 1:
 print iterator.next()
except StopIteration:
 pass

这是否更有意义,或者只是让你更困惑? : )

的编辑: 我应该注意这是一个用于演示目的的oversimplification 。 : )

编辑 2: 忘记抛出StopIteration异常

有一个额外的东西需要提及: 一个函数实际上不需要终止。 我已经编写了这样的代码:


def fib():
 last, cur = 0, 1
 while True: 
 yield cur
 last, cur = cur, last + cur

然后,我可以在其他代码中使用它:


for f in fib():
 if some_condition: break
 coolfuncs(f);

它确实有助于简化一些问题,并使一些事情更容易处理。

收益率就像返回。 它会返回你告诉它的一切。 唯一的区别是,下次调用函数时,执行从最后一次对yield语句的调用开始。

在代码的情况下,函数 get_child_candidates 充当一个迭代器,当你扩展列表时,它每次向新列表添加一个元素。

list.extend 调用一个迭代器直到它被耗尽。 对于你所发布的代码示例,只返回一个元组并将它的附加到列表中就会更加清晰。

对于那些喜欢最少的工作示例的人,可以在这个交互式的 python 会话中进行冥想:


>>> def f():
... yield 1
... yield 2
... yield 3
... 
>>> g = f()
>>> for i in g:
... print i
... 
1
2
3
>>> for i in g:
... print i
... 
>>> # Note that this time nothing was printed

收益率给你一个生成器。


def get_odd_numbers(i):
 return range(1, i, 2)
def yield_odd_numbers(i):
 for x in range(1, i, 2):
 yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

可以看到,在第一个案例中,foo同时保存内存中的整个列表。 对于有 5个元素的列表来说,这不是一件大事,但是如果你想要一个 5万的列表? 这不仅是一个巨大的内存消耗者,它还花费了大量的时间来构建函数。 在第二个例子中,bar只给你一个生成器。 生成器是一个 iterable--which,意味着你可以在循环中使用它,但是每个值只能访问一次。 所有的值也不是同时存储在内存中,发电机对象"记忆"循环的最后一次你叫it--this方式,如果你正在使用一个iterable( say ) 数到 50十亿,你不必数到 50十亿一次性和存储 50十亿数字计数。 再次,这是一个很不自然的例子中,你可能会出现使用itertools如果你真的想要数到 50十亿。 : )

这是生成器最简单的用例。 就像你所说的,它可以用来编写高效的排列,使用yield来通过调用堆栈推动事情,而不是使用某种堆栈变量。 生成器也可以用于特殊的树遍历,以及其他各种方式。

这是返回一个发电机。我 python 不是特别熟悉,但我相信这是同一种东西迭代器 C# 块如果你熟悉这些。

有一个 IBM条解释合理( 用于 python ) 据我所看到的。

关键想法是 compiler/interpreter/whatever 做一些欺骗,调用者而言,他们可以保持调用 next() 并将返回值-仿佛停顿了一下发电机方法。 显然,你不能真正地"暂停"方法,所以编译器会为你构建一个状态机,以便记住你当前所处的位置和局部变量等。 这比编写迭代器容易得多。

...