生成器、协程

生成器、协程

1 协程和生成器

  • 从语句上看,协程和生成器类似,都是包含了yield关键字。

  • 不同之处在于协程中yield关键词通常出现在=右边,可以产出值a(y = yield a)或不产出值时为None(y = yield)。调用方可以用send函数发送值给协程。

  • 激活协程时在yield处暂停,等待调用方发送数据,下次继续在yield暂停。从根本上看,yield是流程控制的工具,可以实现协作式多任务。

  • python中异步IO操作是通过asyncio模块来实现的。

2 生成器Generator

在 Python 中,使用了 yield 的函数被称为生成器(generator)。

  • 只有一个.__next__()方法:
    跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

  • 只记录当前位置:
    在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

  • 调用一个生成器函数,返回的是一个迭代器对象。

2.1 列表生成式

列表生成式在调用列表之前创建完整的列表,如果列表参数庞大,会占用大量的内存空间。

L = [x*2 for x in range(10)]
print(L)
print(L[3])
print(len(L))

# 输出:
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
6
10

2.2 生成器

generator保存的是算法,每次调用next(G),就计算出G的下一个元素的值,直到计算到最后一个元素。没有更多的元素时,抛出StopIteration的错误。

# 创建L和G的区别仅在于最外层的[]和(),L是一个list,而G是一个generator。
G = (i * 2 for i in range(10))
print(G)
print(G.__next__())
print(G.__next__())
print(G.__next__())
print(G.__next__())
for value in G:
    print('value', value)
print('G[5]', G[5])

# 输出
<generator object <genexpr> at 0x000001CF71EF0430>
0
2
4
6
value 8
value 10
value 12
value 14
value 16
value 18
Traceback (most recent call last):
  File "generator01.py", line 9, in <module>
    print('G[5]', G[5])
TypeError: 'generator' object is not subscriptable

2.3 斐波拉契数列(Fibonacci)

2.3.1 斐波拉契数列函数写法

# Description:打印斐波拉契数列(Fibonacci):
# 除第一个和第二个数外,任意一个数都可由前两个数相加得到:
# 1, 1, 2, 3, 5, 8, 13, 21, 34, ...


# 斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
def fib(number):
    x, y = 0, 1

    for i in range(number):
        print(y)
        x, y = y, x + y


fib(10)

注意,赋值语句:

x, y = y, x + y

相当于:

z = (y, x + y) # z是一个tuple
x = z[0]
y = z[1]
# 结论:不必显式写出临时变量z就可以赋值。

2.3.2 yield 方式生成斐波拉契数列函数

  • 如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator
def fib(number):
    x, y = 0, 1

    for i in range(number):
        yield y
        x, y = y, x + y


f = fib(10)
print(f)
print(f.__next__())
print(f.__next__())
print('-----out-----')
print(f.__next__())
print(f.__next__())
print('-----start loop-----')
for i in f:
    print(i)

# 输出:
<generator object fib at 0x000001BF8A080430>
1
1
-----out-----
2
3
-----start loop-----
5
8
13
21
34
55

2.4 yield 生成器返回值特点

  • 生成器返回值为yield的返回值,没有更多的元素时,抛出StopIteration的错误。
def fib(max):
    x, y = 0, 1
    for i in range(max):
        yield y
        x, y = y, x + y
    return 'done'


g = fib(5)
while True:
    try:
        z = next(g)
        print('z:', z)
    except StopIteration as error_info:
        print('Generator return value:', error_info.value)  # 错误时抛出返回值。
        break

# 输出
z: 1
z: 1
z: 2
z: 3
z: 5
Generator return value: done

2.5 yield from

  • yield from内部会自动捕获StopIteration异常,并把异常对象的value属性变成yield from表达式的值。
def for_test():
    for i in range(3):
        yield i


def yield_from_test():
    yield from range(3)


print(list(for_test()))
print(list(yield_from_test()))

# 输出:
[0, 1, 2]
[0, 1, 2]
  • yield from x 表达式内部首先是调用iter(x),然后再调用next(),因此x是任何的可迭代对象。
  • yield from 的主要功能就是打开双向通道,作用是把最外层的调用方和最内层的子生成器连接起来,同时子生成器也可使用yield from调用另一个子生成器,一直嵌套下去直到遇到yield表达式结束链式。
def test(name):
    print('in test(): ', name)
    x = yield name  # 调用next()时,产出yield右边的值后暂停;调用send()时,产出值赋给x,并往下运行
    print('send值:', x)
    return 'fcarey'


def grouper2():
    result2 = yield from test('fcarey')  # 在此处暂停,等待子生成器的返回后继续往下执行
    print('result2的值:', result2)
    return result2


def grouper():
    result = yield from grouper2()  # 在此处暂停,等待子生成器的返回后继续往下执行
    print('result的值:', result)
    return result


def main():
    g = grouper()
    next(g)
    try:
        g.send(10)
    except StopIteration as e:
        print('返回值:', e.value)


if __name__ == '__main__':
    main()

3 协程

3.1 协程介绍

  • 异步IO:就是发起一个IO操作(如:网络请求,文件读写等),这些操作一般是比较耗时的,不用等待它结束,可以继续做其他事情,结束时会发来通知。
  • 协程:又称为微线程,协程英文名Coroutine。在一个线程中执行,执行函数时可以随时中断,由程序(用户)自身控制,执行效率极高,与多线程比较,没有切换线程的开销和多线程锁机制。
  • 符合什么条件就能称之为协程
    • 必须在只有一个单线程里实现并发
    • 修改共享数据不需加锁
    • 用户程序里自己保存多个控制流的上下文栈
    • 一个协程遇到IO操作自动切换到其它协程

3.1.1 存在yield函数运行过程

以示例来说明:

def Foo():
    print('starting eating baozi...')
    while True:
        baozi = yield 'caibao'
        print('\033[34;1m fcarey\033[0m is eating baozi %s' % baozi)


if __name__ == '__main__':
    f = Foo()
    print('第一次', next(f))
    print('第二次', next(f))

# 输出
starting eating baozi...
第一次 caibao
 fcarey is eating baozi None
第二次 caibao


# 单步输出
def Foo():
    print('starting eating baozi...')
    while True:
        baozi = yield 'caibao'
        print('\033[34;1m fcarey\033[0m is eating baozi %s' % baozi)


if __name__ == '__main__':
    f = Foo()
    print(f)
    print('step 1 :', f.__next__())
    print('*' * 25)
    print('step 2 :', f.__next__())
    print('*' * 25)
    # `send()`方法的作用是恢复generator并发送一个值给当前yield表达式。
print('step 3 :', f.send('roubao'))

# 输出
starting eating baozi...
step 1 : caibao
*************************
 fcarey is eating baozi None
step 2 : caibao
*************************
 fcarey is eating baozi roubao
step 3 : caibao
# 调用next()时,返回yield右边的值后暂停;调用send()时,返回值赋给baozi,并往下运行
  1. 程序执行后,会得到一个生成器f,<generator object Foo at 0x000001D120FB2180>
  2. 调用f.__next__()指针下移一位,执行Foo函数,打印starting eating baozi...再进入while循环,执行yield语句,会暂停Foo函数执行、返回caibao并跳出此函数。
  3. 此时由于程序已经跳出此函数,变量'baozi'并没有被赋值。
  4. 执行print('*'*25)
  5. 调用f.__next__()指针下移一位,Foo函数将从上个暂停的指针位置执行函数,baozi没有被赋值,返回None;由于while循环,执行yield语句,会暂停Foo函数执行、返回caibao并跳出此函数
  6. 执行print('*'*25)
  7. 调用f.send(3),由于send()方法的作用是恢复generator并发送一个值给当前yield表达式。Foo函数将从上个暂停的指针位置执行函数,send()roubao赋值给baozi变量,返回roubao,由于while循环,再执行yield语句,会暂停Foo函数执行、返回caibao并跳出此函数

3.1.2 通过yield实现协程并发运算的效果

def consumer(name):
    print("%s 准备吃包子啦!" %name)
    while True:
       baozi = yield
       print("包子[%s]来了,被[%s]吃了!" %(baozi,name))

b1 = 'big baozi'
c = consumer('abc')
c.__next__()
c.__next__()
c.send(b1)
c.__next__()

# 输出
abc 准备吃包子啦!
包子[None]来了,被[abc]吃了!
包子[big baozi]来了,被[abc]吃了!
包子[None]来了,被[abc]吃了!

结论:

  • yield : 保存当前状态,并返回当前值
  • next()调用yield但不会给yield传值
  • send()调用yield会给yield传值

3.2 Greenlet 协程模块

greenlet是一个用C实现的协程模块。相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为 generator

from greenlet import greenlet


def test1():
    print(12)
    gr2.switch()
    print(34)
    gr2.switch()

def test2():
    print(56)
    gr1.switch()
    print(78)
    gr1.switch()

# 生成一个协程
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

# 输出结果:
12
56
34
78

3.3 Gevent 协程模块

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

3.3.1 协程间自动切换

import gevent


def Foo():
    print('runnig Foo')
    # gevent.sleep()触发切换
    gevent.sleep(2)
    print('explicit running in Foo again')


def Bar():
    print('running Bar')
    gevent.sleep(1)
    print('explicit running in Bar again')


def Fun():
    print('running Fun')
    gevent.sleep(0)
    print('explicit running in Fun again')


#  列表形式,产生3个协程,实现协程之间自动切换
gevent.joinall([
    gevent.spawn(Foo),
    gevent.spawn(Bar),
    gevent.spawn(Fun),
])

# 输出:
runnig Foo
running Bar
running Fun
explicit running in Fun again
explicit running in Bar again
explicit running in Foo again

3.3.2 同步与异步的性能区别

异步遇到IO阻塞时会自动切换任务

from urllib import request
import gevent
import time
from gevent import monkey

# 默认urllib无法与gevent协同,需要打个补丁,将当前程序所有I/O操作打标记
monkey.patch_all()

def Fun(url):
    print('GET: %s' % url)
    req = request.urlopen(url)
    data = req.read()
    print('%d bytes receied from %s.' % (len(data), url))


urls = ['https://www.python.org',
        'https://www.yahoo.com',
        'https://www.baidu.com',
        ]

time_start = time.time()
for url in urls:
    Fun(url)

print('同步cost', time.time() - time_start)

async_time_start = time.time()
gevent.joinall([
    gevent.spawn(Fun, 'https://www.python.org'),
    gevent.spawn(Fun, 'https://www.yahoo.com'),
    gevent.spawn(Fun, 'https://www.baidu.com'),
])

print("异步cost", time.time() - async_time_start)

# 输出
GET: https://www.python.org
50435 bytes receied from https://www.python.org.
GET: https://www.yahoo.com
3369 bytes receied from https://www.yahoo.com.
GET: https://www.baidu.com
227 bytes receied from https://www.baidu.com.
同步cost 2.240593194961548
GET: https://www.python.org
GET: https://www.yahoo.com
GET: https://www.baidu.com
227 bytes receied from https://www.baidu.com.
3369 bytes receied from https://www.yahoo.com.
50435 bytes receied from https://www.python.org.
异步cost 1.5009031295776367

3.3.3 实现单线程下的多socket并发

server端

import socket
import gevent

from gevent import monkey
monkey.patch_all()


def server(port):
    ser = socket.socket()
    ser.bind(('localhost', port))
    ser.listen(1024)
    while True:
        conn, addr = ser.accept()
        gevent.spawn(handle_request, conn)

def handle_request(conn):
    try:
        while True:
            data = conn.recv(1024)
            print('rece: ', data)
            conn.send(data.upper())
            if not data:
                conn.shutdown(socket.SHUT_WR)

    except Exception as ex:
        print(ex)
    finally:
        conn.close()

if __name__ == '__main__':
    server(6666)

client端

#单次连接
import socket
HOST = 'localhost'
PORT = 6666
cli = socket.socket()
cli.connect((HOST, PORT))

while True:
    msg = input('>>: ').strip().encode('utf-8')
    if len(msg) == 0: continue
    cli.send(msg)
    data = cli.recv(1024)
    print(data.decode('utf-8'))


# 并发
import socket
import threading

HOST = 'localhost'
PORT = 6666

def sock_conn():

    client = socket.socket()
    client.connect((HOST, PORT))
    client.send( ("hello %s" %count).encode("utf-8"))
    data = client.recv(1024)

    print("[%s]recv from server:" % threading.get_ident(),data.decode()) #结果
    client.close()


for i in range(100):
    t = threading.Thread(target=sock_conn)
    t.start()

3.4 协程异步IO(asyncio)模块

一定要看:Python中协程异步IO(asyncio)详解 - 知乎 (zhihu.com)

3.4.1 asyncio 中的概念

  1. 事件循环:管理所有的事件,在整个程序运行过程中不断循环执行并追踪事件发生的顺序将它们放在队列中,空闲时调用相应的事件处理者来处理这些事件。

  2. Future对象:表示尚未完成的计算,还未完成的结果

  3. Task:是Future的子类,作用是在运行某个任务的同时可以并发的运行多个任务。asyncio.Task是用于实现协作式多任务的库,且Task对象不能用户手动实例化,通过下面2个函数创建:

    loop.create_task()
    asyncio.ensure_future()
    
  4. run_until_complete():阻塞调用,直到协程运行结束才返回。参数是future,传入协程对象时内部会自动变为future

  5. asyncio.sleep():模拟IO操作,这样的休眠不会阻塞事件循环,前面加上await后会把控制权交给主事件循环,在休眠(IO操作)结束后恢复这个协程。

    若在协程中需要有延时操作,应该使用 await asyncio.sleep(),而不是使用time.sleep(),因为使用time.sleep()后会释放GIL(全局解释器锁),阻塞整个主线程,从而阻塞整个事件循环。

  6. 创建Taskloop.create_task():接收一个协程,返回一个asyncio.Task的实例,也是asyncio.Future的实例。返回值可直接传入run_until_complete()

  7. 获取协程返回值

    1. task.result(),只有运行完毕后才能获取,若没有运行完毕,result()方法不会阻塞去等待结果,而是抛出 asyncio.InvalidStateError 错误。
    2. 通过add_done_callback()回调:
  8. 控制任务:通过asyncio.wait()可以控制多任务,asyncio.wait()是一个协程,不会阻塞,立即返回,返回的是协程对象。传入的参数是future或协程构成的可迭代对象。最后将返回值传给run_until_complete()加入事件循环

  9. 动态添加协程:方案是创建一个线程,使事件循环在线程内永久运行,相关函数:

    loop.call_soon_threadsafe() :与 call_soon()类似,等待此函数返回后马上调用回调函数,返回值是一个 asyncio.Handle 对象,此对象内只有一个方法为 cancel()方法,用来取消回调函数。
    
    loop.call_soon() : 与call_soon_threadsafe()类似,call_soon_threadsafe() 是线程安全的
    
    loop.call_later():延迟多少秒后执行回调函数
    
    loop.call_at():在指定时间执行回调函数,这里的时间统一使用 loop.time() 来替代 time.sleep()
    
    asyncio.run_coroutine_threadsafe(): 动态的加入协程,参数为一个回调函数和一个loop对象,返回值为future对象,通过future.result()获取回调函数返回值
    
    

3.4.2 异步协程调用过程

  • 异步协程
    • 在函数(特殊的函数)定义的时候,如果使用了async修饰的话,则改函数调用后会返回一个协程对象,并且函数内部的实现语句不会被立即执行
    • 在特殊函数内部的实现中不可以出现不支持异步的模块代码
  • 用户可主动控制程序,在认为耗时IO处添加await(yield from)。在asyncio库中,协程使用@asyncio.coroutine装饰,使用yield from来驱动,在python3.5后作了如下更改:
    • @asyncio.coroutine -> async
    • yield from -> await
import asyncio


# 返回async修饰的函数return值,需要使用回调函数

def call_back(task):
    print(task.result())


async def test():
    print("it's test()")
    return "test()"


# async修饰的函数,调用之后返回一个协程对象
c = test()

# 创建一个循环对象
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop=loop)

# 将协程对象注册到循环对象中,启动loop
loop.run_until_complete(c)
# 输出
it's test()

# task 的使用
task = loop.create_task(c)
# 将 callback 方法传递给了封装好的 task 对象,这样当 task 执行完毕之后就调用 callback 方法;
# 同时 task 对象还会作为参数传递给 callback 方法,调用 task 对象的 result 方法就可以获取返回结果。
task.add_done_callback(call_back)
# 将task注册到循环对象中,启动loop
loop.run_until_complete(task)
print(task)
# 输出
it's test()
test()
<Task finished name='Task-1' coro=<test() done, defined at test.py:10> result='test()'>

# future 的使用
future = asyncio.ensure_future(c, loop=loop)
# 将callback 方法传递给了封装好的 future 对象,这样当 future 执行完毕之后就调用 callback 方法;
# 同时 future 对象还会作为参数传递给 callback 方法,调用 future 对象的 result 方法就可以获取返回结果。
future.add_done_callback(call_back)
loop.run_until_complete(future)

3.4.3 多任务处理

import asyncio
import time

start = time.time()


# 在特殊函数内部的实现中不可以出现不支持异步的模块代码
async def get_url(url):
    await asyncio.sleep(2)
    print('得到url:', url)


urls = [
    'www.1.com',
    'www.2.com',
    'www.3.com',
]

# 创建一个循环对象
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop=loop)

# 将协程对象注册到future循环对象中,启动loop
tasks = []
for url in urls:
    c = get_url(url)
    task = asyncio.ensure_future(c, loop=loop)
    tasks.append(task)

# 启动loop
loop.run_until_complete(asyncio.wait(tasks))

print(time.time() - start)

# 输出
得到url: www.1.com
得到url: www.2.com
得到url: www.3.com
2.0074543952941895

3.4.4 协程处理request请求

  • 服务端
from flask import Flask, render_template
import time

app = Flask(__name__)


@app.route('/site1')
def index_bobo():
    time.sleep(2)
    return render_template('test.html')


@app.route('/site2')
def index_jay():
    time.sleep(2)
    return render_template('test.html')


@app.route('/site3')
def index_tom():
    time.sleep(2)
    return render_template('test.html')


if __name__ == '__main__':
    app.run(threaded=True)

  • 客户端
import requests
# aiohttp:支持异步网络请求的模块
import aiohttp
import time
import asyncio


start = time.time()
urls = [
    'http://127.0.0.1:5000/site1',
    'http://127.0.0.1:5000/site2',
    'http://127.0.0.1:5000/site3',
]

"""
# 在特殊函数内部的实现中不可以出现不支持异步的模块代码,如此处request是不支持的
# 最终导致运行后没有异步处理效果
async def get_request(url):
    page_text = requests.get(url).text
    print(page_text)
    return page_text
# 输出
site1
site2
site3
6.022470474243164
"""


async def get_page(url):
    async with aiohttp.ClientSession() as acs:
        async with await acs.get(url=url) as page_response:
            page_text = await page_response.text()  # read()返回的是byte类型的数据
            print('page_text')
    return page_text
"""
# 输出
site2
site3
site1
2.007105588912964
"""

def call_back(task):
    page_text = task.result()
    page_etree = etree.HTML(page_text)
    page_content = page_etree.xpath('//li/text()')
    print(page_content)

# 创建循环对象
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop=loop)

# 将协程对象注册到future循环对象中,启动loop
tasks = []
for url in urls:
    c = get_request(url)
    future = asyncio.ensure_future(c, loop=loop)
    future.add_done_callback(call_back)
    future.append(task)

loop.run_until_complete(asyncio.wait(tasks))
print(time.time() - s)

posted @ 2022-12-05 12:03  f_carey  阅读(21)  评论(0编辑  收藏  举报