Python复习(八)-协程

迭代器

迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

1. 可迭代对象

我们把可以通过for...in...这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable)

# 2. 如何判断一个对象是否可迭代
# 可以使用 isinstance() 判断一个对象是否是 iterable 对象:
from collections import Iterable

print(isinstance([], Iterable))

print(isinstance({}, Iterable))

print(isinstance('abc', Iterable))

print(isinstance(100, Iterable))
True
True
True
False

可迭代对象的本质

对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for...in...中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator)。

可迭代对象的本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。

可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.

那么也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。

from collections import Iterable

class Mylist(object):
    def __init__(self):
        self.list = []
    def add(self, num):
        self.list.append(num)
    def __iter__(self):
        "返回一个迭代器"
        pass
mylist = Mylist()
print(isinstance(mylist, Iterable))
True

iter()函数与next()函数

list、tuple等都是可迭代对象,我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下一条数据。iter()函数实际上就是调用了可迭代对象的__iter__方法。

l = [1, 2, 3, 4]
list_iter = iter(l)

print(next(list_iter))
print(next(list_iter))
print(next(list_iter))
print(next(list_iter))
print(next(list_iter))
1
2
3
4
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-9-602dcc0b92e2> in <module>
      6 print(next(list_iter))
      7 print(next(list_iter))
----> 8 print(next(list_iter))
StopIteration: 

注意,当我们已经迭代完最后一个数据之后,再次调用next()函数会抛出StopIteration的异常,来告诉我们所有数据都已迭代完成,不用再执行next()函数了

2. 如何判断一个对象是否是迭代器

# 可以使用 isinstance() 判断一个对象是否是 Iterator 对象:
from collections import Iterator

print(isinstance([], Iterator))

print(isinstance(iter([]), Iterator))

print(isinstance('abc', Iterator))

print(isinstance(iter('abc'), Iterator))
False
True
False
True

迭代器iterator

通过上面,我们发现:一个实现了__iter__方法和__next__方法的对象,就是迭代器

class Mylist(object):
    def __init__(self):
        self.list = []
    def add(self, num):
        self.list.append(num)
    def __iter__(self):
        return Myiterator(self.list)

class Myiterator(object):
    def __init__(self, mylist):
        self.mylist = mylist
        self.current = 0
    def __next__(self):
        if self.current < len(self.mylist):
            item = self.mylist[self.current]
            self.current += 1
            return item
        else:
            raise StopIteration
    def __iter__(self):
        return self

mylist = Mylist()
mylist.add(1)
mylist.add(2)
mylist.add(3)
mylist.add(4)
for i in mylist:
    print(i)
1
2
3
4

for...in...循环的本质

for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

# 迭代器的应用场景-斐波拉契数列(Fibonacci)
'''
列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
'''
class Fib():
    def __init__(self, n):
        self._n = n
        self._current = 0
        self.a = 0
        self.b = 1
    def __next__(self):
        if self._current < self._n:
            self.a, self.b = self.b, self.a + self.b
            self._current += 1
            return self.a
        else:
            raise StopIteration
    def __iter__(self):
        return self
fib = Fib(10)
for i in fib:
    print(i)
# 除了for循环能接收可迭代对象,list、tuple等也能接收
l = list(Fib(10))
print(l)
t = tuple(Fib(10))
print(t)
1
1
2
3
5
8
13
21
34
55
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
(1, 1, 2, 3, 5, 8, 13, 21, 34, 55)

生成器

利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器

# 创建生成器1
l = [i * 2 for i in range(5)]
print(type(l))
# [] -> ()
g = (i * 2 for i in range(5))
print(type(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
# print(next(g)) 此处报 StopIteration
<class 'list'>
<class 'generator'>
0
2
4
6
8
g = (i * 2 for i in range(5))
for i in g:
    print(i)
0
2
4
6
8
# 创建生成器2 - 斐波那契数列
def fib(n):
    current = 0
    a, b = 0, 1
    while current < n:
        a, b = b, a + b
        current += 1
        yield a
    return 'Done'
F = fib(5)
print(next(F))
print(next(F))
print(next(F))
print(next(F))
print(next(F))
print(next(F))
1
1
2
3
5

---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-37-6056bd787944> in <module>
     14 print(next(F))
     15 print(next(F))
---> 16 print(next(F))
StopIteration: Done

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。

简单来说:只要在def中有yield关键字的 就称为 生成器

for n in fib(5):
    print(n)
1
1
2
3
5

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:

def fib(n):
    current = 0
    a, b = 0, 1
    while current < n:    
        a, b = b, a + b
        current += 1
        yield a
    return 'Done'
F = fib(5)
while True:
    try:
        x = next(F)
        print("Value: %d" % x)
    except StopIteration as e:
        print("Return: %s" % e.value)
        break
Value: 1
Value: 1
Value: 2
Value: 3
Value: 5
Return: Done

使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
yield关键字有两点作用:

  • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
  • 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用

可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)。

Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)

# 使用send 唤醒
# 除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。
# 使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据

def create_num(all_num):
    a, b = 0, 1
    current_num = 0
    while current_num < all_num:
        ret = yield a
        print("ret>>>>", ret)
        a, b = b, a+b
        current_num += 1

obj = create_num(10)
print(next(obj))
print(obj.send('Taoy'))
print(next(obj))

print("- " * 20)
print(next(obj))
print(obj.__next__()) # 不常用
0
ret>>>> Taoy
1
ret>>>> None
1
- - - - - - - - - - - - - - - - - - - - 
ret>>>> None
2
ret>>>> None
3

协程

协程,又称微线程,纤程。英文名Coroutine

协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定

协程和线程差异

在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

# 简单实现协程

import time

def work1():
    while True:
        print("--- work1 ---")
        yield
        time.sleep(0.5)
def work2():
    while True:
        print("--- work2 ---")
        yield
        time.sleep(0.5)
w1 = work1()
w2 = work2()
i = 0
for i in range(5):
    next(w1)
    w2.__next__()
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---

greenlet

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

from greenlet import greenlet
import time

def work1():
    for i in range(5):
        print("--- work1 ---")
        gr2.switch()
        time.sleep(0.5)
def work2():
    for i in range(5):
        print("--- work2 ---")
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(work1)
gr2 = greenlet(work2)

gr1.switch()
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
--- work1 ---
--- work2 ---
# 协程 gevent

# 参考 https://www.liaoxuefeng.com/wiki/897692888725344/966405998508320

进程、线程、协程对比:

  • 有一个老板想要开个工厂进行生产某件商品(例如剪子)
  • 他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
  • 只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程
  • 这个老板为了提高生产率,想到3种办法:

    • 在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式
    • 老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式
    • 老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式

简单总结

  • 进程是资源分配的单位
  • 线程是操作系统调度的单位
  • 进程切换需要的资源很最大,效率很低
  • 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  • 协程切换任务资源很小,效率高
  • 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

结语

python的基础复习就先到这里了, 还有一些高级应用以后再整理

本文链接:

https://www.betao.cn/archives/python-review08.html
1 + 8 =
快来做第一个评论的人吧~