七、python学习之多任务-协程

版权声明:浅弋、璃鱼的原创 https://blog.csdn.net/qq_35709559/article/details/82351402

一、迭代:

1.迭代的概念:

使用for循环便利的过程叫做迭代。比如:使用for循环遍历列表取值的过程

2.可迭代对象:

可使用for循环便利取值的对象叫做可迭代对象。比如:列表、元组、字典、集合、字符串、range

3.判断对象是否为可迭代对象:

使用isinstance(obj, type):内建方法

from collections import Iterable
var = isinstance([1, 5, 6, 7], Iterable)    # ==> bool
# 如果obj是type一个实例,返回True;否则,返回False

代码:

#!/usr/bin/env python
# coding=utf-8

# 导包
from collections import Iterable    # Iterable:可迭代对象

# 判断列表是否为可迭代对象
ret = isinstance([1,2,3,'haha','哈哈'], Iterable)
print("列表是否为可迭代对象:", ret)

# 判断元组是否为可迭代对象
ret = isinstance(("你好", 666, 3.14), Iterable)
print("元组是否为可迭代对象:", ret)

# 判断字典是否为可迭代对象
ret = isinstance({"name":"张三","age":18}, Iterable)
print("字典是否为可迭代对象:", ret)

# 判断集合是否为可迭代对象
ret = isinstance({"你好", 666, 3.14}, Iterable)
print("集合是否为可迭代对象:", ret)

# 判断字符串是否为可迭代对象
ret = isinstance("好好学习,good good study", Iterable)
print("字符串是否为可迭代对象:", ret)

# 判断range是否为可迭代对象
ret = isinstance(range(5), Iterable)
print("range是否为可迭代对象:", ret)

4.自定义可迭代对象:

在类里面定义__iter__方法创建的对象就是可迭代对象

代码:

#!/usr/bin/env python
# coding=utf-8

from collections import Iterable


class MyList(object):

    def __init__(self):
        # 定义列表容器保存用户数据
        self.my_list = list()

    # 定义添加数据方法
    def add_data(self, data):
        self.my_list.append(data)

    # 在类中提供__iter__方法,保证创建的对象是可迭代对象
    def __iter__(self):
        pass


if __name__ == '__main__':
    # 通过自定义的可迭代类型创建自定义的可迭代对象
    my_iterable = MyList()
    my_iterable.add_data(1)
    my_iterable.add_data(2)

    ret = isinstance(my_iterable, Iterable)
    print(ret)

    for value in my_iterable:
        print(value)

运行结果:

自定义类是否为可迭代对象: True
Traceback (most recent call last):
  File "xxx/xxx/代码/02_自定义可迭代对象.py", line 32, in <module>
    for value in my_iterable:
TypeError: iter() returned non-iterator of type 'NoneType'

分析:程序执行到32行,for循环遍历出报错,“类型错误:iter()返回类型为“NoneType”的非迭代器”,原因是因为在__iter__方法中需要返回一个迭代器。可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器,然后通过迭代器获取对象中的数据。

 二、迭代器:

1.自定义迭代器对象:

自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象

代码:

#!/usr/bin/env python
# coding=utf-8

from collections import Iterable
from collections import Iterator


# 自定义可迭代类型: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

    def __init__(self):
        self.my_list = list()

    # 添加指定元素
    def append_item(self, item):
        self.my_list.append(item)

    def __iter__(self):
        # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
        my_iterator = MyIterator(self.my_list)

        # 判一下
        ret = isinstance(my_iterator, Iterator)
        print("my_iterator是迭代器对象:", ret)

        return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
class MyIterator(object):

    def __init__(self, my_list):
        self.my_list = my_list
        # 记录当前获取数据的下标
        self.current_index = 0
        # 判断当前对象是否是迭代器
        result = isinstance(self, Iterator)
        print("MyIterator创建的对象是否是迭代器:", result)

    def __iter__(self):
        return self

    # 获取迭代器中下一个值
    def __next__(self):
        if self.current_index < len(self.my_list):
            self.current_index += 1
            return self.my_list[self.current_index - 1]
        else:
            # 数据取完了,需要抛出一个停止迭代的异常
            raise StopIteration


if __name__ == '__main__':
    my_list = MyList()
    my_list.append_item(1)
    my_list.append_item(2)
    result = isinstance(my_list, Iterable)
    
    print("my_list是可迭代对象:", result)
    
    for value in my_list:
        print(value)

运行结果:

my_list是可迭代对象: True
MyIterator创建的对象是否是迭代器: True
my_iterator是迭代器对象: True
1
2

分析:迭代器和可迭代对象是两个类;

检查是否为迭代器:需要导入迭代器类

from collections import Iterator    # 迭代器类

可迭代类型(MyList)中的__iter__实例化一个迭代器并返回,这是可迭代类型就具备了 MyIterator类型的迭代器,这时就可以进行遍历操作。迭代器在数据取完的时候需要抛出一个停止迭代异常

raise StopIteration

2.iter()函数与next()函数:

iter()函数: 获取可迭代对象的迭代器,会调用可迭代对象的__iter__方法

next()函数:获取迭代器中下一个值,会调用迭代器对象的__next__方法

3. for循环的本质:

<1>当使用for循环遍历的是可迭代对象,那么for循环内部会通过iter函数获取迭代器,然后循环调用next函数获取迭代器中下一个值。

<2>当使用for循环遍历的是迭代器对象,那么直接循环使用next函数获取迭代器中的下一个值。

<3>for循环内部自动捕获停止迭代的异常,而while循环内部没有自己捕获。

<4>最终取值操作都是通过迭代器完成的。

4. 迭代器的应用场景:斐波那契数列--不死神兔:

迭代器完成斐波那契的好处:

1. 节省内存空间,因为每次根据算法只生成一个值

2.生成数列的个数没有上限控制

代码:

#!/usr/bin/env python
# coding=utf-8


class Fibonacci(object):
    def __init__(self, num):
        # num: 表示根据个数生成fibonacci数列
        self.num = num
        # 保存fib数列的前两个数
        self.first = 0
        self.second = 1
        # 记录fib数列的下标
        self.current_index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < self.num:
            ret = self.first
            self.first, self.second = self.second, self.first + self.second
            # 生成数据完成以后下标加1
            self.current_index += 1
            return ret
        else:
            # 表示数列生成完成
            raise StopIteration

if __name__ == '__main__':
    # 创建生成100个fib数列对象(迭代器)
    fib = Fibonacci(100)
    for _ in range(10):
        # 使用for循环,使用next()函数打印前10个
        print(next(fib))

运行结果:

      

三、生成器:

1.生成器的概念:

生成器是一类特殊的迭代器,他不需要再向上面的类一样写__iter__()和__next__()方法。使用更加的方便,但是依然可以使用next函数和for循环取值。

2.创建生成器:

方法一:

#!/usr/bin/env python
# coding=utf-8

# 列表推导式
my_list = [i + 2 for i in range(5)]
print("列表推导式:", my_list)

# 创建一个简单的生成器
my_generator = (i + 2 for i in range(5))
print("简单的生成器:")
for _ in range(5):
    print(next(my_generator), end=" ")

小结:由代码可以看出,简单的生成器的定义方法跟列表推导式类似,是将列表推导式的“[]”换成了“()”,获得的返回值由列表(list)实例对象变成了生成器(generater)对象。

方法二:使用yield关键字

代码:使用yield关键字创建生成器,完成斐波那契数列


#!/usr/bin/env python
# coding=utf-8

# 定义生成器
def fibonacci(num):
    # num: 表示生成器根据个数创建指定个数的fib数列
    first = 0
    second = 1
    # 记录生成数列的下标
    current_index = 0
    # 验证yield执行过程
    print("===1===")
    # 循环判断条件是否成立
    while current_index < num:
        ret = first
        # 根据公式
        first, second = second, first + second
        current_index += 1
        print("===2===")
        yield ret    # 在函数里面使用yield表示是一个生成器
        print("===3===")

if __name__ == '__main__':
    fib = Fibonacci(10)

    # 循环遍历
    for _ in range(2):
        print(next(fib))
    print("======华丽的分割线======")
    for _ in range(5):
        print(next(fib))

运行结果:

===1===
===2===
0
===3===
===2===
1
======华丽的分割线======
===3===
===2===
1
===3===
===2===
2
===3===
===2===
3
===3===
===2===
5
===3===
===2===
8

小结:当前代码执行到yield关键字的时候代码会暂停,然后把结果返回给外界;

当再次启动生成器的时候会在暂停的位置继续往下执行;

在函数里面使用yield表示是一个生成器

3.生成器使用return关键字:
    代码:

#!/usr/bin/env python
# coding=utf-8


# 定义生成器
def fibonacci(num):
    # num: 表示生成器根据个数创建指定个数的fib数列
    first = 0
    second = 1
    # 记录生成数列的下标
    current_index = 0
    # 验证yield执行过程
    print("===1===")
    # 循环判断条件是否成立
    while current_index < num:
        ret = first
        # 根据公式
        first, second = second, first + second
        current_index += 1
        print("===2===")
        yield ret
        print("===3===")
        # 当再次启动生成器执行return代码的时候,生成器生成数据结束,抛出停止迭代异常
        return "ok"


if __name__ == '__main__':
    fib = fibonacci(10)

    value = next(fib)
    print(value)

    value = next(fib)
    print(value)

运行结果:

===1===
===2===
0
===3===
Traceback (most recent call last):
  File "G:/xxx/代码/07_使用return关键字.py", line 31, in <module>
    value = next(fib)
StopIteration: ok

小结:
    生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,会携带信息抛出停止迭代异常。

4.yield和return的对比:
    <1>使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)

    <2>代码执行到yield会暂停,然后把结果返回出去,下次启动
    生成器会在暂停的位置继续往下执行

    <3>每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值

    <4>return只能返回一次值,代码执行到return语句就停止迭代,抛出停止迭代异常

5.使用send方法启动生成器并传参:
代码:

#!/usr/bin/env python
# coding=utf-8


# 定义生成器
def fibonacci(num):
    # num: 表示生成器根据个数创建指定个数的fib数列
    first = 0
    second = 1
    # 记录生成数列的下标
    current_index = 0
    # 验证yield执行过程
    print("===1===")
    # 循环判断条件是否成立
    while current_index < num:
        ret = first
        # 根据公式
        first, second = second, first + second
        current_index += 1
        print("===2===")
        params = yield ret
        print(params)
        print("===3===")


if __name__ == '__main__':
    fib = fibonacci(10)

    value = fib.send(None)
    print(value)

    value = fib.send("哈哈哈")
    print(value)

运行结果:

===1===
===2===
0
哈哈哈
===3===
===2===
1

小结:

next和send的区别:
    next函数启动生成器不能传入参数;
    send方法启动生成器可以传入参数,但第一次只能传None。

四、协程-yield

1.协程的概念:

协程,又称微线程,纤程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行。

通俗理解只要在def里面只看到一个yield关键字表示就是协程

2.协程yield的代码实现:

代码:

#!/usr/bin/env python
# coding=utf-8
import time


# 跳舞任务
def dance():
    for i in range(3):
        print("跳舞中...")
        time.sleep(0.2)
        yield


# 唱歌任务
def sing():
    for i in range(3):
        print("唱歌中...")
        time.sleep(0.2)
        yield


if __name__ == '__main__':
    g1 = dance()
    g2 = sing()
    # yield本质上就是一个生成器
    # print(g1, g2)

    for i in range(3):
        next(g1)
        next(g2)

运行结果:

跳舞中...
唱歌中...
跳舞中...
唱歌中...
跳舞中...
唱歌中...

小结:协程之间执行任务按照一定顺序交替执行

五、协程-greenlet:

1.greentlet的介绍:

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单。

使用如下命令安装greenlet模块:

pip3 install greenlet

greenlet的使用:

<1>创建协程:

# 导包
import greenlet

# 创建协程
g1 = greenlet.greenlet(任务名, 参数)

<2>切换到指定协程执行对应的任务:

g1.switch()

<3>获取当前的协程对象:

ret = greenlet.getcurrent()

代码:

#!/usr/bin/env python
# coding=utf-8

import greenlet
import time


def work1():
    # 获取当前的协程对象
    result = greenlet.getcurrent()
    print(result)
    for i in range(3):
        print("work1工作中...")
        time.sleep(0.2)
        # 切换到第二个协程执行对应的任务
        g2.switch()


def work2():
    # 获取当前的协程对象
    result = greenlet.getcurrent()
    print(result)
    for i in range(3):
        print("work2工作中...")
        time.sleep(0.2)
        # 切换到第一个协程执行对应的任务
        g1.switch()


if __name__ == '__main__':
    # 创建协程1指定完成对应的任务
    g1 = greenlet.greenlet(work1)
    # 创建协程2指定完成对应的任务
    g2 = greenlet.greenlet(work2)

    print(g1, g2)
    # 切换到指定协程执行对应的任务
    g1.switch()

运行结果:

<greenlet.greenlet object at 0x00000200A9B2EA60> <greenlet.greenlet object at 0x00000200A9B2EAF8>
<greenlet.greenlet object at 0x00000200A9B2EA60>
work1工作中...
<greenlet.greenlet object at 0x00000200A9B2EAF8>
work2工作中...
work1工作中...
work2工作中...
work1工作中...
work2工作中...

2.使用greenlet的优点:

明确了当前运行的协程。

六、协程-gevent:

1.gevent的介绍:

greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。

gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

2.gevent的使用:

下载安装:

pip3 install gevent

gevent的使用:

<1>创建协程:

# 导包
import gevent

# 创建协程
g1 = gevent.spawn(任务名, 参数1, 参数2, ...)
# spawn(function, *args, **kwargs) -> Greenlet

<2>主进程等待协程执行完成:

# 主进程等待协程
g1.join()

代码:

#!/usr/bin/env python
# coding=utf-8

import gevent
import time


# 定义跳舞任务
def dance():
    # 打印当前协程
    print("跳舞:", gevent.getcurrent())
    for i in range(3):
        print("跳舞中...")
        time.sleep(0.2)


# 定义唱歌任务
def sing():
    # 打印当前协程
    print("唱歌:", gevent.getcurrent())
    for i in range(3):
        print("唱歌中...")
        time.sleep(0.2)


if __name__ == '__main__':
    # 创建协程
    g1 = gevent.spawn(dance)
    g2 = gevent.spawn(sing)
    print("main:", g1, g2)

    g1.join()
    g2.join()

运行结果:

main: <Greenlet "Greenlet-0" at 0x29d3d448d48: dance> <Greenlet "Greenlet-1" at 0x29d3d8ab248: sing>
跳舞: <Greenlet "Greenlet-0" at 0x29d3d448d48: dance>
跳舞中...
跳舞中...
跳舞中...
唱歌: <Greenlet "Greenlet-1" at 0x29d3d8ab248: sing>
唱歌中...
唱歌中...
唱歌中...

从运行结果中可以看出,程序的运行是按照协程的创建顺序执行的,执行完g1协程之后,再开始执行g2协程,这不是多任务执行的结果,,问题出在哪了呢?注意:默认系统耗时(time.sleep(n))在geven包中不认为是耗时操作,gevent有自带的耗时方法(gevent.sleeo(n))。但是如果一个完成了功能的代码,或者遇到无法使用gevent自带耗时方法表示的系统耗时怎么办呢??

3.给程序打补丁:

3.1 为什么要打补丁?

让gevent框架识别耗时操作,比如:time.sleep,网络请求延时

3.2 monkey.patch_all():猴子补丁

用来在运行时动态修改已有的代码,而不需要修改原始代码

#!/usr/bin/env python
# coding=utf-8

from gevent import monkey
import gevent
import time

# 定义跳舞任务
def dance():
    # 打印当前协程
    print("跳舞:", gevent.getcurrent())
    for i in range(3):
        print("跳舞中...")
        time.sleep(0.2)


# 定义唱歌任务
def sing():
    # 打印当前协程
    print("唱歌:", gevent.getcurrent())
    for i in range(3):
        print("唱歌中...")
        time.sleep(0.2)


if __name__ == '__main__':

    # 猴子补丁
    monkey.patch_all()

    # 创建协程
    g1 = gevent.spawn(dance)
    g2 = gevent.spawn(sing)
    print("main:", g1, g2)

    # g1.join()
    # g2.join()
    while True:
        # 模拟循环等待接收客户端的连接请求
        time.sleep(0.2)

运行结果:

main: <Greenlet "Greenlet-0" at 0x1adf918db48: dance> <Greenlet "Greenlet-1" at 0x1adf918dc48: sing>
跳舞: <Greenlet "Greenlet-0" at 0x1adf918db48: dance>
跳舞中...
唱歌: <Greenlet "Greenlet-1" at 0x1adf918dc48: sing>
唱歌中...
跳舞中...
唱歌中...
跳舞中...
唱歌中...

分析:分析运行结果,代码不再是执行完g1之后在执行g2了,而是交替执行,如此就完成了多任务。

注意:在代码中将g.join()换成了while True死循环,目的是模拟服务端循环等待客户端的请求。不需要加join操作的前提是:主线程需要一直运行,比如死循环;循环的代码中必须有耗时操作。

七、进程、线程、协程的对比:

1.进程、线程、协程的关系:

一个进程至少有一个线程,一个线程里可以有多个协程。

2.进程、线程、协程的关系:

进程是系统分配资源的基本单位;线程是CPU调度的基本单位

先有进程, 进程里面可以有多个线程,线程里面可以有多个协程

进程、线程、协程都是可以完成多任务的,但是进程和线程完成多任务是无序的,协程是按照一定顺序交替执行

从资源开销来说,进程需要的资源最多,线程次之(512k),协程最少(5k)

多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

八、使用gevent完成图片的下载:

代码:

#!/usr/bin/env python
# coding=utf-8

# 导包
import gevent
from gevent import monkey
import urllib.request   # 网络请求模块
import time


# 打补丁:让gevent使用网络请求的耗时操作,让协程自动完成下载任务
monkey.patch_all()


# 定义文件下载功能,下载对应的网路文件
def download_file(file_url, file_name):
    try:
        print(file_url)
        # 根据url向网络发起请求
        response = urllib.request.urlopen(file_url)
        # 把文件写入本地
        with open(file_name, 'wb') as file:
            while True:
                file_data = response.read(1024)
                if file_data:
                    file.write(file_data)
                    file.flush()
                else:
                    break
    except Exception as e:
        print("文件下载异常:", e)
    else:
        print("文件下载完成:",file_name)


if __name__ == '__main__':
    # 准备文件的url
    file1_url = ""
    file2_url = ""
    file3_url = ""

    # 创建协程并指派任务
    g1 = gevent.spawn(download_file, file1_url, "./2b.jpg")
    g2 = gevent.spawn(download_file, file2_url, "./hh.jpg")
    g3 = gevent.spawn(download_file, file3_url, "./aaa.jpg")

    # 主线程等待所有的协程执行完以后再退出
    gevent.joinall([g1, g2, g3])

猜你喜欢

转载自blog.csdn.net/qq_35709559/article/details/82351402