迭代器
可迭代对象
什么是可迭代对象
可迭代对象就是对象的类中实现了__iter__
方法的对象。对于可迭代对象,可以使用for
循环直接从中取得元素。原生数据中的list, set, tuple, str
都是可迭代对象。
如何判断对象是否可以迭代
判断对象是否可以迭代,可以用from collections import Iterable
,然后用isinstance
标识符将对象与之对比,如果可以迭代,就会返回True。
如何让自定义的类成为可迭代对象
通过定义类的__iter__
方法,可以让自定义的类创建的对象可迭代,示例如下:
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()
迭代器的创建和调用
迭代器是访问元素集合的一种方式,迭代器是一个可以记住遍历位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有元素访问结束。迭代器只能前进,不能后退。类似于C++中的forward_iterator。
一个迭代器有两个基本要素:__iter__
方法,__next__
方法。__iter__
方法返回迭代器自身,__next__
方法返回集合中的下一个值,如果集合中没有更多元素,则抛出StopIteration
异常。
在实际使用迭代器时,一般让可迭代对象类的__iter__
方法返回其迭代器,而迭代器的__next__
方法设定访问元素的顺序,一个例子如下:
from collections import Iterable
from collections import Iterator
class MyList(object): # 定义可迭代对象类
def __init__(self, num):
self.data = num # 迭代上边界
def __iter__(self):
return MyListIterator(self.data)
class MyListIterator(object): # 定义迭代器类
def __init__(self, data):
self.data = data # 迭代上边界
self.current = 0 # 当前迭代值
self.step = 2 # 迭代步长
def __iter__(self):
return self
def __next__(self):
while self.current <= self.data - self.step:
self.current += self.step
return self.current
raise StopIteration # 迭代超出上边界时抛出异常
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
循环进行调用时,会调用smObj
中的 __iter__
方法,返回一个迭代器后,调用迭代器中的__next__
方法获取返回值。
这里要注意,如果不手动设定越界后抛出StepIteration
,for
循环是不会终止的,而是不断会取到None
。
迭代器的作用
使用迭代器,可以只记录在集合中生成下一个元素的方式,而不去记录生成的具体结果。这样在生成一个大集合时,可以只占用很小的内存空间,更加memory-efficient。
例如用迭代器生成斐波那契数列:
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():
# 用生成器打印斐波那契数列的前n项
n = 20
fib = Fibonacci(n)
for i in fib:
print(i)
if __name__ == '__main__':
main()
除了for以外的迭代器使用
除了使用for
循环以外,其实在进行类型转换时,也会使用迭代器。如:
fib = Fibonacci(15)
print(list(fib)) # 转换为列表,实际上会调用迭代器
print(tuple(fib)) # 转换为元组,实际上会调用迭代器
所以实际上在这个过程中,是先生成一个空列表或者空元组,再调用迭代器的__next__
方法,依次向内填充数据。
生成器
什么是生成器
生成器就是一种特殊的迭代器。它是一个返回迭代器的函数,只能用于迭代操作。
创建生成器的方式
- 第一种方式:将列表推导式的
[]
换成()
def main():
G = (i * i for i in range(10))
print(G) # 输出<generator object main.<locals>.<genexpr> at 0x10ddb7228>
if __name__ == '__main__':
main()
要从生成器中取值,可以使用next()
函数。
- 第二种方式:在函数中使用
yield
关键字
当函数中包含yield
关键字时,那么创建的就是一个生成器,而非普通函数。函数会顺序执行,遇到return
或者最后一行函数语句就返回。但是generator会在每次调用next()
时执行,遇到yield
语句是暂停并返回一个值,再次执行时,<u>从上次返回的yield
语句处执行。</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()
可以看到在执行next
时,是不会执行到函数的最后一句的,而是到yield
就暂停并返回了。
获得生成器函数的返回值
在生成器函数中,如果要获得其返回值,需要用异常捕获到方式,如下所示:
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()
这样就会在最后打印出函数的返回值Done
使用send操作生成器
除了使用next()
函数启动生成器以外,还可以用send
方法来使生成器运行。它与next()
的不同在于可以向生成器内传入一些值,形如generator().send(someValue)
。这样就使得我们可以在生成器运行的过程中操作它,极大增加了生成器的灵活性。
例如:
def generator(num):
"""一个生成器,生成斐波那契数列"""
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)) # 此时并未传值,效果等同于next(g)
except StopIteration:
pass
if __name__ == '__main__':
main()
如果说希望在打印的过程中,让生成器重启,可以修改上面的代码:
def generator(num):
"""一个生成器,生成斐波那契数列"""
a, b = 0, 1
cnt = 0
while cnt <= num:
val = yield a # val用来接收send传入的值
# 根据val是否有传入值,来决定是否重置
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)) # 此时并未传值,效果等同于next(g)
reset += 1
# 在打印两项之后,重新启动生成器
if reset == 2:
print(g.send(0)) # 让函数内的cnt重置为0
except StopIteration:
pass
if __name__ == '__main__':
main()
此时的结果为:
0
1
0
1
1
2
3
可以看到按照我们的需要,重启了生成器。
这里注意,yield
返回的值对应next
函数的返回值,而send
方法传入的值对应的是yield
语句执行完成,下次重新从这里开始运行后,yield
表达式的值。也就是说,在运行时用send
传入值时,实际上第一次暂停,是在someVal = yield a
的等式右边,这里a
被返回给了next
函数作为返回值,而yield a
表达式的值会等于send(someVal)
中传入的值。
另外需要注意的是,如果需要用到send
方法传入值,一定不能在生成器第一次运行时(没有调用过next
或者是send(None)
)进行传值,因为函数还没有运行到yield
语句,没有东西可以接收传入值。
协程 - yield实现多任务
我们需要注意到yield
关键字的重要特点之一是可以暂停函数,利用这个特点,可以用yield
实现多任务并发。这里的原理非常类似操作系统的时间片轮转。
如以下例子:
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()
输出结果如下:
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
协程的特点在于调用的资源非常少,仅相当于调用了一个函数。但是弱点在于无法合理利用计算机的多CPU,实际上就是用一个线程模拟了多任务的同时进行。