迭代器
可迭代對(duì)象
什么是可迭代對(duì)象
可迭代對(duì)象就是對(duì)象的類(lèi)中實(shí)現(xiàn)了__iter__方法的對(duì)象。對(duì)于可迭代對(duì)象,可以使用for循環(huán)直接從中取得元素。原生數(shù)據(jù)中的list, set, tuple, str都是可迭代對(duì)象。
如何判斷對(duì)象是否可以迭代
判斷對(duì)象是否可以迭代,可以用from collections import Iterable,然后用isinstance標(biāo)識(shí)符將對(duì)象與之對(duì)比,如果可以迭代,就會(huì)返回True。
如何讓自定義的類(lèi)成為可迭代對(duì)象
通過(guò)定義類(lèi)的__iter__方法,可以讓自定義的類(lèi)創(chuàng)建的對(duì)象可迭代,示例如下:
from collections import Iterable
class myInt(int):
""""""
def __init__(self, num):
self._num = num
def __iter__(self):
for i in range(self._num):
yield i
def main():
some_int = myInt(10)
print("myInt is iterable: ", isinstance(some_int, Iterable))
# 輸出myInt is iterable: True
for i in some_int:
print(i, end=' ')
print()
if __name__ == '__main__':
main()
迭代器的創(chuàng)建和調(diào)用
迭代器是訪問(wèn)元素集合的一種方式,迭代器是一個(gè)可以記住遍歷位置的對(duì)象。迭代器對(duì)象從集合的第一個(gè)元素開(kāi)始訪問(wèn),直到所有元素訪問(wèn)結(jié)束。迭代器只能前進(jìn),不能后退。類(lèi)似于C++中的forward_iterator。
一個(gè)迭代器有兩個(gè)基本要素:__iter__方法,__next__方法。__iter__方法返回迭代器自身,__next__方法返回集合中的下一個(gè)值,如果集合中沒(méi)有更多元素,則拋出StopIteration異常。
在實(shí)際使用迭代器時(shí),一般讓可迭代對(duì)象類(lèi)的__iter__方法返回其迭代器,而迭代器的__next__方法設(shè)定訪問(wèn)元素的順序,一個(gè)例子如下:
from collections import Iterable
from collections import Iterator
class MyList(object): # 定義可迭代對(duì)象類(lèi)
def __init__(self, num):
self.data = num # 迭代上邊界
def __iter__(self):
return MyListIterator(self.data)
class MyListIterator(object): # 定義迭代器類(lèi)
def __init__(self, data):
self.data = data # 迭代上邊界
self.current = 0 # 當(dāng)前迭代值
self.step = 2 # 迭代步長(zhǎng)
def __iter__(self):
return self
def __next__(self):
while self.current <= self.data - self.step:
self.current += self.step
return self.current
raise StopIteration # 迭代超出上邊界時(shí)拋出異常
def main():
smObj = MyList(8)
print("MyList() is iterable: ", isinstance(smObj, Iterable))
smObj_iterator = iter(smObj)
print("iter(Mylist()) is iterator: ", isinstance(smObj_iterator, Iterator))
for i in smObj:
print(i, end=' ')
print()
if __name__ == '__main__':
main()
在for循環(huán)進(jìn)行調(diào)用時(shí),會(huì)調(diào)用smObj中的 __iter__方法,返回一個(gè)迭代器后,調(diào)用迭代器中的__next__方法獲取返回值。
這里要注意,如果不手動(dòng)設(shè)定越界后拋出StepIteration,for循環(huán)是不會(huì)終止的,而是不斷會(huì)取到None。
迭代器的作用
使用迭代器,可以只記錄在集合中生成下一個(gè)元素的方式,而不去記錄生成的具體結(jié)果。這樣在生成一個(gè)大集合時(shí),可以只占用很小的內(nèi)存空間,更加memory-efficient。
例如用迭代器生成斐波那契數(shù)列:
class Fibonacci(object):
def __init__(self, n):
self.prev_last_item = 0
self.last_item = 1
self.current = 0
self.upper_bound = n
def __iter__(self):
return self
def __next__(self):
if self.current < 2:
tmp = self.current
self.current += 1
return tmp
elif self.current < self.upper_bound:
self.current += 1
tmp = self.prev_last_item + self.last_item
self.prev_last_item, self.last_item = self.last_item, tmp
return tmp
else:
raise StopIteration
def main():
# 用生成器打印斐波那契數(shù)列的前n項(xiàng)
n = 20
fib = Fibonacci(n)
for i in fib:
print(i)
if __name__ == '__main__':
main()
除了for以外的迭代器使用
除了使用for循環(huán)以外,其實(shí)在進(jìn)行類(lèi)型轉(zhuǎn)換時(shí),也會(huì)使用迭代器。如:
fib = Fibonacci(15)
print(list(fib)) # 轉(zhuǎn)換為列表,實(shí)際上會(huì)調(diào)用迭代器
print(tuple(fib)) # 轉(zhuǎn)換為元組,實(shí)際上會(huì)調(diào)用迭代器
所以實(shí)際上在這個(gè)過(guò)程中,是先生成一個(gè)空列表或者空元組,再調(diào)用迭代器的__next__方法,依次向內(nèi)填充數(shù)據(jù)。
生成器
什么是生成器
生成器就是一種特殊的迭代器。它是一個(gè)返回迭代器的函數(shù),只能用于迭代操作。
創(chuàng)建生成器的方式
- 第一種方式:將列表推導(dǎo)式的
[]換成()
def main():
G = (i * i for i in range(10))
print(G) # 輸出<generator object main.<locals>.<genexpr> at 0x10ddb7228>
if __name__ == '__main__':
main()
要從生成器中取值,可以使用next()函數(shù)。
- 第二種方式:在函數(shù)中使用
yield關(guān)鍵字
當(dāng)函數(shù)中包含yield關(guān)鍵字時(shí),那么創(chuàng)建的就是一個(gè)生成器,而非普通函數(shù)。函數(shù)會(huì)順序執(zhí)行,遇到return或者最后一行函數(shù)語(yǔ)句就返回。但是generator會(huì)在每次調(diào)用next()時(shí)執(zhí)行,遇到yield語(yǔ)句是暫停并返回一個(gè)值,再次執(zhí)行時(shí),<u>從上次返回的yield語(yǔ)句處執(zhí)行。</u>
例如:
from time import sleep
def Fibonacci(num):
a, b = 0, 1
cnt = 0
while cnt < num:
print("Before yield, current cnt: {}".format(cnt))
yield a
print("After yield")
cnt += 1
a, b = b, a + b
def main():
G = Fibonacci(10)
print(G) # <generator object Fibonacci at 0x104079228>
next(G) # Before yield, current cnt: 0
sleep(1)
next(G) # After yield
# Before yield, current cnt: 1
if __name__ == '__main__':
main()
可以看到在執(zhí)行next時(shí),是不會(huì)執(zhí)行到函數(shù)的最后一句的,而是到yield就暫停并返回了。
獲得生成器函數(shù)的返回值
在生成器函數(shù)中,如果要獲得其返回值,需要用異常捕獲到方式,如下所示:
from time import sleep
def Fibonacci(num):
a, b = 0, 1
cnt = 0
while cnt < num:
yield a
cnt += 1
a, b = b, a + b
return "Done!"
def main():
G = Fibonacci(10)
try:
while True:
ret = next(G)
print(ret)
except Exception as ret:
print(ret.value)
if __name__ == '__main__':
main()
這樣就會(huì)在最后打印出函數(shù)的返回值Done
使用send操作生成器
除了使用next()函數(shù)啟動(dòng)生成器以外,還可以用send方法來(lái)使生成器運(yùn)行。它與next()的不同在于可以向生成器內(nèi)傳入一些值,形如generator().send(someValue)。這樣就使得我們可以在生成器運(yùn)行的過(guò)程中操作它,極大增加了生成器的靈活性。
例如:
def generator(num):
"""一個(gè)生成器,生成斐波那契數(shù)列"""
a, b = 0, 1
cnt = 0
while cnt <= num:
yield a
a, b = b, a + b
cnt += 1
def main():
g = generator(20)
try:
while True:
print(g.send(None)) # 此時(shí)并未傳值,效果等同于next(g)
except StopIteration:
pass
if __name__ == '__main__':
main()
如果說(shuō)希望在打印的過(guò)程中,讓生成器重啟,可以修改上面的代碼:
def generator(num):
"""一個(gè)生成器,生成斐波那契數(shù)列"""
a, b = 0, 1
cnt = 0
while cnt <= num:
val = yield a # val用來(lái)接收send傳入的值
# 根據(jù)val是否有傳入值,來(lái)決定是否重置
if val is None:
a, b = b, a + b
else:
a, b = 0, 1
cnt = val
cnt += 1
def main():
g = generator(5)
reset = 0
try:
while True:
print(g.send(None)) # 此時(shí)并未傳值,效果等同于next(g)
reset += 1
# 在打印兩項(xiàng)之后,重新啟動(dòng)生成器
if reset == 2:
print(g.send(0)) # 讓函數(shù)內(nèi)的cnt重置為0
except StopIteration:
pass
if __name__ == '__main__':
main()
此時(shí)的結(jié)果為:
0
1
0
1
1
2
3
可以看到按照我們的需要,重啟了生成器。
這里注意,yield返回的值對(duì)應(yīng)next函數(shù)的返回值,而send方法傳入的值對(duì)應(yīng)的是yield語(yǔ)句執(zhí)行完成,下次重新從這里開(kāi)始運(yùn)行后,yield表達(dá)式的值。也就是說(shuō),在運(yùn)行時(shí)用send傳入值時(shí),實(shí)際上第一次暫停,是在someVal = yield a的等式右邊,這里a被返回給了next函數(shù)作為返回值,而yield a表達(dá)式的值會(huì)等于send(someVal)中傳入的值。
另外需要注意的是,如果需要用到send方法傳入值,一定不能在生成器第一次運(yùn)行時(shí)(沒(méi)有調(diào)用過(guò)next或者是send(None))進(jìn)行傳值,因?yàn)楹瘮?shù)還沒(méi)有運(yùn)行到yield語(yǔ)句,沒(méi)有東西可以接收傳入值。
協(xié)程 - yield實(shí)現(xiàn)多任務(wù)
我們需要注意到yield關(guān)鍵字的重要特點(diǎn)之一是可以暫停函數(shù),利用這個(gè)特點(diǎn),可以用yield實(shí)現(xiàn)多任務(wù)并發(fā)。這里的原理非常類(lèi)似操作系統(tǒng)的時(shí)間片輪轉(zhuǎn)。
如以下例子:
def task1(num):
while True:
print("Now in task1 - %d" % num)
num = yield
def task2(num):
while True:
print("Now in test2 - %d" % num)
num = yield
def main():
current_num = 0
num = 10
g1 = task1(current_num)
g2 = task2(current_num)
next(g1)
next(g2)
while current_num < num:
current_num += 1
g1.send(current_num)
g2.send(current_num)
if __name__ == '__main__':
main()
輸出結(jié)果如下:
Now in task1 - 0
Now in test2 - 0
Now in task1 - 1
Now in test2 - 1
Now in task1 - 2
Now in test2 - 2
Now in task1 - 3
Now in test2 - 3
Now in task1 - 4
Now in test2 - 4
Now in task1 - 5
Now in test2 - 5
Now in task1 - 6
Now in test2 - 6
Now in task1 - 7
Now in test2 - 7
Now in task1 - 8
Now in test2 - 8
Now in task1 - 9
Now in test2 - 9
Now in task1 - 10
Now in test2 - 10
協(xié)程的特點(diǎn)在于調(diào)用的資源非常少,僅相當(dāng)于調(diào)用了一個(gè)函數(shù)。但是弱點(diǎn)在于無(wú)法合理利用計(jì)算機(jī)的多CPU,實(shí)際上就是用一個(gè)線程模擬了多任務(wù)的同時(shí)進(jìn)行。