文章目录
迭代器、生成器与协程
迭代器 iterator 与生成器 generator 是 Python 中处理可迭代对象的常用方法
-
Python 中典型的容器 container (容器是用来储存多个值或对象的一种数据结构)有 list, set, dictionary, OrderedDictionary, bytearray, array, string, frozenset, tuple, bytes
-
容器中部分对象 list, dictionary, string, tuple… 是可迭代对象 Iterable ,不仅具有
__iter__()
方法还额外具有__getitem__()
方法 -
具有
__iter__()
方法即实现了可迭代协议,是可迭代对象,__iter__()
方法必须能够返回一个对象,通常是返回对象自身,因为对象通常还具有__next__()
方法以实现了迭代器协议,此处通常定义要执行什么并返回一个输出值,以实现迭代器 Iterator 因此迭代器的__iter__()
方法的返回值是一个至少包含done
与value
两个属性的对象 -
具有
__getitem__()
方法(属于序列和映射协议)称具有迭代性质,虽然具有同样的功能但并非相关协议的标准实现 -
通过内建函数 iter() 将具有迭代性质的对象转换成迭代器,将自动添加标准的
__iter__()
与__next__()
方法并通过 StopIteration 异常标识迭代完成 -
通过 collections.abc 里的 Iterable 和 Iterator 与 isinstance 方法可判断一个对象是否是可迭代的和是否是迭代器对象
-
使用 yield 关键字进行返回的函数是生成器函数。生成器本质上是一种特殊的迭代器,它可以动态地按需产生数据,并通过实现迭代器协议来逐个访问所产生的数据。因此,使用一个生成器函数创建一个生成器对象时,该对象也就同时具备了迭代器对象的特性。生成器函数执行到 yield 时,会在 yield 处中断执行,并返回 yield 后携带的表达式,当下次调用生成器函数时会回到中断位置并继续执行至迭代终止
-
可通过 yield from 后接生成器或行为类似生成器的对象如 range() 等实现嵌套的效果,总体上看
yield from iterator
语义等同yield x for x in iterator
(yield from 可以看做是协程中的 await 关键字,等待后续对象执行的意思) -
生成器可以中断和恢复,并且生成器可以通过 send() 向生成器内部发送信息,生成器和外部调用的交互中表现出非阻塞和异步的特性(其内部依然是阻塞和同步的,但在流式处理中还是非常有用的)总体上还是阻塞的和同步的
-
当生成器未初始化时第一次 send() 需要使用 send(None) 此时将会从头开始执行生成器函数直到执行到 yield 中断该生成器,后续可以在程序中设置任意合适的值由 send() 主动恢复到中断该生成器处继续执行,这和协程允许多个入口点进行挂起和恢复复杂的控制流程的定义类似,因此可以实现生成器协程(一种很早期的协程和并发基本没关系),事实上早期的协程关键字 async 就是一个实现了生成器初始化的装饰器
async 的内涵如下所示
def coroutine(func):
def start(*args, **kwargs):
cr = func(*args, **kwargs)
cr.send(None)
return cr
return start
- 生成器与迭代器的主要区别在于迭代器访问容器中的数据需物理存在(数据在迭代开始时通常已经加载到内存),而生成器允许数据仅逻辑存在(只有在需要时才会生成值)
- 列表推导式 [i for i in old_list],使用生成器表达式 (i for i in old_list) 可获得同样的结果,生成器表达式是调用时才会迭代求出结果,且每次只迭代一次。面对 sum([i for i in 1e16]) 时至少需要一次开辟 1e16 Byte 的空间,内存压力很大,而面对 sum((i for i in 1e16)) 时 sum() 只需一个个值逐个计算(生成器中会包含中断,迭代是一段一段)内存压力很小
协程 coroutines 是一种可以被暂停和恢复执行的函数,允许在不同任务之间切换执行权实现非抢占式的并发。协程与传统的线程和多进程不同,协程提供了一种在单个线程中处理并发操作的方法,使你可以在没有多线程竞态条件和上下文切换的开销的情况下执行多个操作,协程在Python中经历了几个发展阶段
- 生成器基础的协程,在早期,人们开始使用带有 yield 的生成器函数作为原始的协程。这些函数可以使用 yield 暂停和恢复它们的执行
- 增强的生成器协程,yield from 语法的引入在 Python 3.3 中允许生成器委派部分操作给另一个生成器
- 异步协程,Python 3.5 引入了 async/await 语法,使得异步编程变得更加直观。可以使用 async def 定义一个协程函数,并使用 await 暂停协程的执行,等待另一个异步操作完成
迭代器
1、使用 iter()
函数
iter(object, sentinel)
# 第一个参数的解释基于第二个参数
# 当第二个参数不存在,第一个参数必须是支持迭代协议的容器类型对象,例如字典等
# 或者是支持序列协议的序列类型对象,例如列表等,如果都不支持则报错
# 当第二个参数存在即哨兵参数存在,则第一个参数必须是可调用对象 (callable) 即函数等
# 以此种方式创建的迭代器对象将会调用 object
# 如果可调用对象调用后返回值与哨兵对象值相同,则结束调用
2、通过实现 __iter__()
和 __next__()
方法
class ListIterator:
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.data):
raise StopIteration
result = self.data[self.index]
self.index += 1
return result
3、仅具有 __iter__()
方法而没有 __next__()
或 __getitem__()
时,使用 iter() 函数创建迭代器将发生错误,因为仅具有 __iter__()
方法并不是迭代器对象也不是可调用对象
class OnlyIter():
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
self.data = -1
return self
if __name__ == '__main__':
test = OnlyIter(1)
print(test.data, test.__dir__(), sep='\n')
# 1
# ['__getattribute__', '__iter__', ..., '__class__']
try:
iter(test).__dir__()
except TypeError as te:
print(te, test.data, sep='\n')
# iter() returned non-iterator of type 'OnlyIter'
# -1
from collections.abc import Iterable, Iterator
print(isinstance(test, Iterable), isinstance(test, Iterator), sep=' ')
# True False
4、next()
返回容器中的下一个元素
next(iterator, default)
# 调用对象的 __next__() 方法
# 第一个参数的解释基于第二个参数
# 当第二个参数不存在,第一个参数的全部元素迭代完成后将 raise StopIteration 异常
# 当第二个参数存在,第一个参数的全部元素迭代完成后将返回 default 值
5、仅具有 __next__()
方法的对象调用方法 iter() 转化创建迭代器对象将发生错误
class OnlyNext():
def __init__(self, data):
self.data = data
def __next__(self):
self.data = -1
return self
if __name__ == '__main__':
test = OnlyNext(1)
print(test.__dir__(), test.data, sep='\n')
# 1
# [..., '__next__', ..., '__getattribute__', ..., '__class__']
try:
iter(test).__dir__()
except TypeError as te:
print(te, test.data, sep='\n')
# -1
# 'OnlyNext' object is not iterable
from collections.abc import Iterable, Iterator
print(isinstance(test, Iterable), isinstance(test, Iterator), sep=' ')
# False False
6、调用方法 iter() 可将仅具有 __getitem__()
方法的可迭代性质的对象转化为具有 __iter__()
和 __next__()
的迭代器对象
class OnlyGetitem:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
if isinstance(index, int):
return self.data[index]
elif isinstance(index, slice):
start, stop, step = index.indices(len(self.data))
return [self.data[i] for i in range(start, stop, step)]
else:
raise TypeError("Invalid index type")
if __name__ == '__main__':
test = OnlyGetitem([1, 2, 3, 4, 5])
print(test.__dir__())
# [..., '__getitem__', ..., '__getattribute__', ..., '__class__']
print(iter(test).__dir__())
# ['__getattribute__', '__iter__', '__next__', ..., '__class__']
from collections.abc import Iterable, Iterator
print(isinstance(test, Iterable), isinstance(test, Iterator), sep=' ')
# False False
print(isinstance(iter(test), Iterable), isinstance(iter(test), Iterator), sep=' ')
# True True
7、值得注意的是在上述的对象中都具有的 __getattribute__()
实际上定义了类的属性和方法这一概念,类无论调用属性或方法,都是先强制调用 __getattribute__()
,然后返回属性的值或者方法的引用
class Test:
def __init__(self):
self.a = 1
self.b = 2
def __getattribute__(self, name):
if name == 'a':
print('%s' % name)
return '%s is get' % name
else:
return self.something # 返回值为 self.** 将导致无限递归
def something(self):
print('......')
if __name__ == '__main__':
test = Test()
print(test.a)
# a
# a is get
try:
print(test.b)
except RecursionError as re:
print(re)
# maximum recursion depth exceeded in comparison
8、for in obj 实际上是 for in iter(obj) ,即使用 next() 方法迭代并自动捕获 StopIteration 异常终止迭代
# 官例1:
"""
with open('mydata.txt') as fp:
for line in iter(fp.readline, ''):
print(line)
# for 循环的每一次会调用 fp.readline 方法,将方法调用产生的值与哨兵参数比较,相同则结束迭代,否则将方法调用产生的值返回并绑定到 line 变量
"""
# 官例2:
"""
def worker(inputs, outputs):
for func, args in iter(inputs.get, 'STOP'):
result = calculate(func, args)
outputs.put(result)
# 函数中 inputs 和 outputs 均是任务队列,每次循环从 inputs 队列中取出任务,若取出的任务不是 'STOP' 则执行取出的任务,否则停止
"""
生成器
使用 yield
关键字创建,使用 from
关键字嵌套
def generator1():
yield from range(5)
def generator2():
for i in generator1():
yield i * 10
if __name__ == '__main__':
test = generator2()
print(test)
# <generator object generator2 at 0x12eef4f20>
for i in test:
print(i)
# 0
# 10
# 20
# 30
# 40
使用 send()
向生成器发送信息
def my_generator():
print("Start")
yield 1
print("Middle")
yield 2
print("End")
yield
gen = my_generator()
result1 = gen.send(None) # 启动生成器,执行到第一个 yield,输出 "Start"
result2 = gen.send(None) # 执行到第二个 yield,输出 "Middle"
gen.send(None) # 输出 "End",执行最后一个 yield,结束程序
print(result1) # 输出 1
print(result2) # 输出 2
通过生成器实现生产者消费者协程模型
def consumer():
r = ''
while True:
n = yield r # yield的返回值为r接受值赋予n
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
协程
通过以下代码理解协程的运行逻辑与基本用法
import threading
import asyncio
import datetime
import time
"""
0. async 定义的函数是协程函数调用协程函数返回协程对象
1. await 可以处理可等待对象,可等待对象有协程对象(corotine)、任务对象(task,用于顶层操作,必须作用于协程对象)和未来对象(future,底层操作 task 的父类)
2. 协程的核心是事件循环,目的是合理的调度可等待对象的执行逻辑从而优化io等待时间
2. 事件循环的最小调度单元是任务,通过主动的将协程对象封装成的协程任务并注册到事件循环并通过内部的调度算法调度执行
3. 运行协程函数需要使用特定的接口调用如 asyncio.run(main()),它会启动一个事件循环并将 main() 作为任务注册到事件循环中调度执行
"""
# example 1
async def display_date():
loop = asyncio.get_running_loop()
end_time = loop.time() + 5.0
while True:
print(datetime.datetime.now())
print(loop.time() + 1.0)
if (loop.time() + 1.0) >= end_time:
break
await asyncio.sleep(1) # 可等待对象将
print(datetime.datetime.now())
# 1. asyncio.run(display_date()) 创建了一个事件循环并将中 display_date() 作为 task 注册
# 2. display_date() 有一个可等待对象(awaitable) asyncio.sleep(1) 执行就会阻塞当前 task 1秒,阻塞过程中由于没有其他的 task 所以 cpu 也在等待
asyncio.run(display_date())
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
# 1. 使用 async 创建协程函数 main() 和 say_after(delay, what)
# 2. 使用 asyncio.run(main()) 创建了一个事件循环(eventloop)并将中 main() 作为第一个 task 调度执行
# 3. 使用了 await 创建了可等待对象 say_after(1, 'hello') 和 say_after(2, 'hello')
# 4. 可以发现当前事件循环中只有 main() 无法异步调度,并且 main() 中有两个 awaitable 存在异步调度的改进空间
asyncio.run(main()) # 花费时间为3秒
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
await task1
await task2
print(f"finished at {time.strftime('%X')}")
# 执行这段协程程序的逻辑如下
# 1. 使用 async 创建协程函数 say_after(delay, what) 和 main()。
# 2. 使用 asyncio.run(main()) 创建一个新的事件循环,并将程序的控制权交给这个事件循环。
# 3. main() 将作为事件循环的第一个任务,获得程序的控制权开始执行。虽然它是在事件循环上下文中执行的,但直到遇到一个 await 语句,控制权都不会返回给事件循环。
# 4. 在 main() 内部,使用 asyncio.create_task() 创建了两个任务 task1 和 task2。这两个任务被添加到事件循环的任务队列中,并标记为准备执行。事件循环被通知了这些任务,但直到 main() 释放控制权,事件循环才真正开始执行它们。
# 5. 在创建完这两个任务之后,main() 继续执行 print() 语句。
# 6. 当 main() 执行到 await task1 时,它将控制权交还给事件循环,等待 task1 完成。事件循环此时开始执行队列中的任务。task1 和 task2 几乎同时开始,并都进入它们各自的 await asyncio.sleep() 语句。
# 7. 在 task1 的休眠时间结束后,它继续执行并打印 "hello"。此时 main() 中的 await task1 也完成了,所以 main() 继续执行,直到下一个 await,即 await task2。
# 8. await task2 需要等待 task2 完成。但因为 task2 已经开始了,并且已经等待了大约1秒,所以此时只需再等待约1秒。
# 9. task2 完成其休眠后,继续执行并打印 "world"。此时 main() 中的 await task2 也完成了。
# 10. main() 打印 "finished" 信息。
# 11. main() 完成执行,程序控制权返回给事件循环。但事件循环中没有更多的任务要执行,所以它结束,并随之结束了整个程序。
asyncio.run(main()) # 花费时间为2秒
# 协程函数的并发是在一个线程内通过事件循环实现的,因此一个线程在同一时间也只能有一个活跃的事件循环
# 1. asyncio.wait() set 形式返回任务执行的详情以及future值(即协程最后return的值)
async def hello1():
print(f"Hello world 01 begin,my thread is: {threading.currentThread()}")
await asyncio.sleep(3)
print("Hello again 01 end")
async def hello2():
print(f"Hello world 02 begin,my thread is: {threading.currentThread()}")
await asyncio.sleep(2)
print("Hello again 02 end")
async def hello3():
print(f"Hello world 03 begin,my thread is: {threading.currentThread()}")
await asyncio.sleep(1)
print("Hello again 03 end")
async def main():
time_start = time.time()
result = await asyncio.wait([hello1(), hello2(), hello3()])
print(result)
time_end = time.time()
print(f"Take time: {time_end - time_start}")
asyncio.run(main())
# 2. asyncio.gather() 列表形式返回 future值
async def hello1():
print(f"Hello world 01 begin,my thread is: {threading.currentThread()}")
await asyncio.sleep(3)
print("Hello again 01 end")
async def hello2():
print(f"Hello world 02 begin,my thread is: {threading.currentThread()}")
await asyncio.sleep(2)
print("Hello again 02 end")
async def hello3():
print(f"Hello world 03 begin,my thread is: {threading.currentThread()}")
await asyncio.sleep(1)
print("Hello again 03 end")
async def main():
time_start = time.time()
result = await asyncio.gather(hello1(), hello2(), hello3()) # 使用gather来并发执行协程
print(result)
time_end = time.time()
print(f"Take time: {time_end - time_start}")
asyncio.run(main())
实践
斐波那契数列 f(0) = 0, f(1) = 1, f(n) = f(n-1) + f(n-2) (n >= 2)
- 迭代器实现
class Fib(object):
def __init__(self, num):
super(Fib, self).__init__()
self.x = 0
self.y = 1
self.max = num
def __iter__(self):
return self
def __next__(self):
fib = self.x
if fib > self.max:
raise StopIteration
self.x, self.y = self.y, self.x + self.y # f(n), f(n+1) = f(n+1), f(n) + f(n+1)
return fib
def main():
fib = Fib(50)
print(fib)
for ans in fib:
print(ans)
if __name__ == '__main__':
main()
# <__main__.Fib object at 0x12f0c1130>
# 0
# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34
- 生成器实现
def fab(num):
count, a, b = 0, 0, 1
while count < num:
yield a
a, b = b, a + b
count += 1
def main():
for ans in fab(10):
print(ans)
if __name__ == '__main__':
print(fab(10))
# <generator object fab at 0x13317c820>
print(fab(10).__dir__())
# ['__getattribute__', '__iter__', '__next__', ..., '__class__']
main()
# 0
# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34
- 协程实现
import asyncio
async def fib(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
await asyncio.sleep(0.1)
async def print_dots():
for _ in range(20): # 打印20个点
print(".", end="", flush=True)
await asyncio.sleep(0.05)
async def main():
task1 = asyncio.create_task(print_dots())
# 直接在这里迭代fib函数(协程生成器)
async for value in fib(10):
print(value)
# 等待print_dots完成
await task1
asyncio.run(main())
# 0
# ..1
# ..1
# ..2
# ..3
# ..5
# ..8
# ..13
# ..21
# ..34
# ..