在理解yield之前,要首先明白什么是generator,在理解generator之前首先要理解可迭代的概念。
可迭代(iterables)
在你创建一个list的时候,可以逐个读取其中的元素,该逐个读取的过程称作迭代:
>>> myList=[1,2,3] >>> for item in myList: print item 1 2 3 >>>
如上所示,myList是可以迭代的。
当使用列表推导式(list comprehension)创建了一个列表时,它就是一个可迭代对象:
>>> myList=[x*x for x in range(3)] >>> for item in myList: print item 0 1 4 >>>
任何可以在其上使用for...in...语句的对象都是可迭代的,例如:lists,strings,files等等。
这些可迭代对象使用非常方便因为它能尽你所愿地读取其中的元素,但是是将所有的值存储在内存中,当它包含大量元素的时候这并不一定总是你想要的。
生成器(Generators)
生成器是可以迭代的,但是只能迭代一次,因为他们并不是将所有的内容放在内存中。而是实时地生成(on the fly):
>>> myGenerator=(x*x for x in range(3)) >>> for item in myGenerator: print item 0 1 4 >>>
这里虽然只是使用了()代替了[],但是你不可以使用for item in myGenerator进行第二次迭代。
因为myGenerator是迭代器,只能使用一次。先算出0,然后忘记0再算出1,然后忘记1再算出4,如此一个接着一个进行计算。
>>> myList=[1,2,3] >>> for item in myList: print item 1 2 3 >>> myGenerator=(x*x for x in range(3)) >>> for item in myGenerator: print item 0 1 4 >>> for item in myList: print item 1 2 3 >>> for item in myGenerator: print item >>>
yield
yield是类似return的关键字,只不过函数返回的是generator。
>>> def createGenerator(): myList = range(3) for item in myList: yield item*item >>> myGenerator=createGenerator() # create a generator >>> print myGenerator <generator object createGenerator at 0x0000000002B20CA8> # mygenerator is an object! >>> for item in myGenerator: print item 0 1 4 >>>
这个列子用途并不大,但是当你知道你的函数将返回很大一批数量的值并且只需要读取一次的时候就很方便了。
为了弄懂yield,你要明白:当你调用函数的时候,函数体中的代码不会被运行;函数只是返回一个生成器(generator)对象。
在for语句部分,使用生成器时,函数体中的代码每次都会被运行。
for第一次调用生成器对象时,代码将会从函数的开始处运行直到遇到yield为止,然后返回此次循环的第一个值,接着循环地执行函数体,返回下一个值,直到没有值返回为止。
一旦函数运行再也没有遇到yield时,生成器就被认为是空的。这有可能是因为循环终止,或者因为没有满足任何if/else。
代码示例
生成器(generator):
#创建node对象的方法_get_child_candidates(),该方法会返回generator def node._get_child_candidates(self, distance, min_dist, max_dist): #每次使用生成器对象的时候都会调用的代码 #如果node对象还有左孩子(left child)并且distance是符合要求的,返回下一个child if self._leftchild and distance - max_dist < self._median: yield self._leftchild #如果node对象还有右孩子(right child)并且distance是符合要求的,返回下一个child if self._rightchild and distance + max_dist >= self._median: yield self._rightchild #函数运行到这里,生成器就会被认为是空的,已经没有children存在
调用器(caller):
#创建一个空的列表、一个包含当前对象的引用的列表 result, candidates = list(), [self] #在candidates上循环 (一开始只包含一个元素) 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
这个代码包含了几个小部分:
-我们对一个链表进行迭代,但是迭代中链表还在不断的扩展。它是一个迭代这些嵌套的数据的简洁方式,即使这样有点危险,因为可能导致无限迭代。 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]
但是在代码中的是一个生成器,这是很好的,因为:
-不必读两次所有的值
-可以有很多children,但不必叫他们都存储在内存里面。
并且这很有效,因为Python不关心一个方法的参数是不是个列表。Python只希望它是个可以迭代的,所以这个参数可以是列表,元组,字符串,生成器...
这叫做 duck typing,这也是为何Python如此棒的原因之一,但这已经是另外一个问题了...
你可以在这里停下,来看看生成器的一些高级用法:
控制生成器的耗尽
>>> class Bank(): # 创建银行,构建ATM机,只要没有危机,就可以不断地每次从中取100 ... 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 # 危机来临,没有更多的钱了 >>> 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 # 危机过后,银行还是空的,因为该函数之前已经不满足while条件 >>> print(corner_street_atm.next()) <type 'exceptions.StopIteration'> >>> brand_new_atm = hsbc.create_atm() # 必须构建一个新的atm,恢复取钱业务 >>> for cash in brand_new_atm: ... print cash $100 $100 $100 $100 $100 $100 $100 $100 $100 ...
对于类似资源的访问控制等场景,生成器显得很实用。
Itertools,很实用的工具
Itertools模块包含特殊的函数,用于操作可迭代对象。
可否想过复制一个生成器?把两个生成器进行连接?在内嵌列表中一行代码处理分组?不会创建另外一个列表的Map/Zip函数?
你要做的就是import itertools 。
举个例子,我们来看看4匹马赛跑到达终点所有可能的顺序:
>>> import itertools >>> horses=[1,2,3,4] >>> races=itertools.permutations(horses) >>> print races <itertools.permutations object at 0x0000000002F09A40> >>> 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__() 方法)的过程。
可迭代对象是你可以从其获取到一个迭代器的任一对象。迭代器是那些允许你迭代可迭代对象的对象。