30.python-協程-yield

迭代器

迭代是訪問集合元素的一種方式。迭代器是一個可以記住遍歷的位置的對象。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。迭代器只能往前不會後退。

可迭代對象

我們已經知道可以對list、tuple、str等類型的數據使用for...in...的循環語法從其中依次拿到數據進行使用,我們把這樣的過程稱爲遍歷,也叫迭代

但是,是否所有的數據類型都可以放到for...in...的語句中,然後讓for...in...每次從中取出一條數據供我們使用,即供我們迭代嗎?

>>> for i in 100:
...     print(i)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
# int整型不是iterable,即int整型不是可以迭代的

# 我們自定義一個容器MyList用來存放數據,可以通過add方法向其中添加數據
>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...
>>> mylist = MyList()
>>> mylist.add(1)
>>> mylist.add(2)
>>> mylist.add(3)
>>> for num in mylist:
...     print(num)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'MyList' object is not iterable
>>>
# MyList容器的對象也是不能迭代的

我們自定義了一個容器類型MyList,在將一個存放了多個數據的MyList對象放到for...in...的語句中,發現for...in...並不能從中依次取出一條數據返回給我們,也就說我們隨便封裝了一個可以存放多條數據的類型卻並不能被迭代使用。

我們把可以通過for...in...這類語句迭代讀取一條數據供我們使用的對象稱之爲可迭代對象(Iterable)**。

可以使用 isinstance() 判斷一個對象是否是 Iterable 對象:

isinstance([], Iterable)
True

可迭代對象的本質

可迭代對象的本質就是可以向我們提供一個這樣的中間“人”即迭代器幫助我們對其進行迭代遍歷使用。

可迭代對象通過__iter__方法向我們提供一個迭代器,我們在迭代一個可迭代對象的時候,實際上就是先獲取該對象提供的一個迭代器,然後通過這個迭代器來依次獲取對象中的每一個數據.

那麼也就是說,一個具備了__iter__方法的對象,就是一個可迭代對象。

>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...     def __iter__(self):
...             """返回一個迭代器"""
...             # 我們暫時忽略如何構造一個迭代器對象
...             pass
...
>>> mylist = MyList()
>>> from collections import Iterable
>>> isinstance(mylist, Iterable)
True
>>>
# 這回測試發現添加了__iter__方法的mylist對象已經是一個可迭代對象了

iter()函數與next()函數

list、tuple等都是可迭代對象,我們可以通過iter()函數獲取這些可迭代對象的迭代器。然後我們可以對獲取到的迭代器不斷使用next()函數來獲取下一條數據。iter()函數實際上就是調用了可迭代對象的__iter__方法。

>>> li = [11, 22, 33, 44, 55]
>>> li_iter = iter(li)
>>> next(li_iter)
11
>>> next(li_iter)
22
>>> next(li_iter)
33
>>> next(li_iter)
44
>>> next(li_iter)
55
>>> next(li_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

注意,當我們已經迭代完最後一個數據之後,再次調用next()函數會拋出StopIteration的異常,來告訴我們所有數據都已迭代完成,不用再執行next()函數了。

如何判斷一個對象是否是迭代器:

可以使用 isinstance() 判斷一個對象是否是 Iterator 對象:

In [58]: isinstance(iter([]), Iterator)
Out[58]: True

 

通過上面的分析,我們已經知道,迭代器是用來幫助我們記錄每次迭代訪問到的位置,當我們對迭代器使用next()函數的時候,迭代器會向我們返回它所記錄位置的下一個位置的數據。實際上,在使用next()函數的時候,調用的就是迭代器對象的__next__方法(Python3中是對象的__next__方法,Python2中是對象的next()方法)。所以,我們要想構造一個迭代器,就要實現它的__next__方法。但這還不夠,python要求迭代器本身也是可迭代的,所以我們還要爲迭代器實現__iter__方法,而__iter__方法要返回一個迭代器,迭代器自身正是一個迭代器,所以迭代器的__iter__方法返回自身即可。

一個實現了__iter__方法和__next__方法的對象,就是迭代器。

class MyList(object):
    """自定義的一個可迭代對象"""
    def __init__(self):
        self.items = []

    def add(self, val):
        self.items.append(val)

    def __iter__(self):
        myiterator = MyIterator(self)
        return myiterator


class MyIterator(object):
    """自定義的供上面可迭代對象使用的一個迭代器"""
    def __init__(self, mylist):
        self.mylist = mylist
        # current用來記錄當前訪問到的位置
        self.current = 0

    def __next__(self):
        if self.current < len(self.mylist.items):
            item = self.mylist.items[self.current]
            self.current += 1
            return item
        else:
            raise StopIteration

    def __iter__(self):
        return self


if __name__ == '__main__':
    mylist = MyList()
    mylist.add(1)
    mylist.add(2)
    mylist.add(3)
    mylist.add(4)
    mylist.add(5)
    for num in mylist:
        print(num)

for...in...循環的本質

for item in Iterable 循環的本質就是先通過iter()函數獲取可迭代對象Iterable的迭代器,然後對獲取到的迭代器不斷調用next()方法來獲取下一個值並將其賦值給item,當遇到StopIteration的異常後循環結束。

 

生成器

利用迭代器,我們可以在每次迭代獲取數據(通過next()方法)時按照特定的規律進行生成。但是我們在實現一個迭代器時,關於當前迭代到的狀態需要我們自己記錄,進而才能根據當前狀態生成下一個數據。爲了達到記錄當前狀態,並配合next()函數進行迭代使用,我們可以採用更簡便的語法,即生成器(generator)。生成器是一類特殊的迭代器

創建生成器方法

要創建一個生成器,有很多種方法。第一種方法很簡單,只要把一個列表生成式的 [ ] 改成 ( )


In [15]: L = [ x*2 for x in range(5)]

In [16]: L
Out[16]: [0, 2, 4, 6, 8]

In [17]: G = ( x*2 for x in range(5))

In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>

In [19]:

創建 L 和 G 的區別僅在於最外層的 [ ] 和 ( ) , L 是一個列表,而 G 是一個生成器。我們可以直接打印出列表L的每一個元素,而對於生成器G,我們可以按照迭代器的使用方法來使用,即可以通過next()函數、for循環、list()等方法使用。


In [19]: next(G)
Out[19]: 0

In [20]: next(G)
Out[20]: 2

In [21]: next(G)
Out[21]: 4

In [22]: next(G)
Out[22]: 6

In [23]: next(G)
Out[23]: 8

In [24]: next(G)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-380e167d6934> in <module>()
----> 1 next(G)

StopIteration:

In [25]:
In [26]: G = ( x*2 for x in range(5))

In [27]: for x in G:
   ....:     print(x)
   ....:     
0
2
4
6
8

In [28]:

 創建生成器方法2

generator非常強大。如果推算的算法比較複雜,用類似列表生成式的 for 循環無法實現的時候,還可以用函數來實現。

我們仍然用上一節提到的斐波那契數列來舉例,回想我們在上一節用迭代器的實現方式:

class FibIterator(object):
    """斐波那契數列迭代器"""
    def __init__(self, n):
        """
        :param n: int, 指明生成數列的前n個數
        """
        self.n = n
        # current用來保存當前生成到數列中的第幾個數了
        self.current = 0
        # num1用來保存前前一個數,初始值爲數列中的第一個數0
        self.num1 = 0
        # num2用來保存前一個數,初始值爲數列中的第二個數1
        self.num2 = 1

    def __next__(self):
        """被next()函數調用來獲取下一個數"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器的__iter__返回自身即可"""
        return self

注意,在用迭代器實現的方式中,我們要藉助幾個變量(n、current、num1、num2)來保存迭代的狀態。現在我們用生成器來實現一下。


In [30]: def fib(n):
   ....:     current = 0
   ....:     num1, num2 = 0, 1
   ....:     while current < n:
   ....:         num = num1
   ....:         num1, num2 = num2, num1+num2
   ....:         current += 1
   ....:         yield num
   ....:     return 'done'
   ....:

In [31]: F = fib(5)

In [32]: next(F)
Out[32]: 1

In [33]: next(F)
Out[33]: 1

In [34]: next(F)
Out[34]: 2

In [35]: next(F)
Out[35]: 3

In [36]: next(F)
Out[36]: 5

In [37]: next(F)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-37-8c2b02b4361a> in <module>()
----> 1 next(F)

StopIteration: done

在使用生成器實現的方式中,我們將原本在迭代器__next__方法中實現的基本邏輯放到一個函數中來實現,但是將每次迭代返回數值的return換成了yield,此時新定義的函數便不再是函數,而是一個生成器了。簡單來說:只要在def中有yield關鍵字的 就稱爲 生成器

此時按照調用函數的方式( 案例中爲F = fib(5) )使用生成器就不再是執行函數體了,而是會返回一個生成器對象( 案例中爲F ),然後就可以按照使用迭代器的方式來使用生成器了。


In [38]: for n in fib(5):
   ....:     print(n)
   ....:     
1
1
2
3
5

In [39]:

但是用for循環調用generator時,發現拿不到generator的return語句的返回值。如果想要拿到返回值,必須捕獲StopIteration錯誤,返回值包含在StopIteration的value中:

In [39]: g = fib(5)

In [40]: while True:
   ....:     try:
   ....:         x = next(g)
   ....:         print("value:%d"%x)      
   ....:     except StopIteration as e:
   ....:         print("生成器返回值:%s"%e.value)
   ....:         break
   ....:     
value:1
value:1
value:2
value:3
value:5
生成器返回值:done

In [41]:

    總結

  • 使用了yield關鍵字的函數不再是函數,而是生成器。(使用了yield的函數就是生成器)
  • yield關鍵字有兩點作用:
    • 保存當前運行狀態(斷點),然後暫停執行,即將生成器(函數)掛起
    • 將yield關鍵字後面表達式的值作爲返回值返回,此時可以理解爲起到了return的作用
  • 可以使用next()函數讓生成器從斷點處繼續執行,即喚醒生成器(函數)
  • Python3中的生成器可以使用return返回最終運行的返回值,而Python2中的生成器不允許使用return返回一個返回值(即可以使用return從生成器中退出,但return後不能有任何表達式)。

  使用send喚醒

我們除了可以使用next()函數來喚醒生成器繼續執行外,還可以使用send()函數來喚醒執行。使用send()函數的一個好處是可以在喚醒的同時向斷點處傳入一個附加數據。

例子:執行到yield時,gen函數作用暫時保存,返回i的值; temp接收下次c.send("python"),send發送過來的值,c.next()等價c.send(None)

In [10]: def gen():
   ....:     i = 0
   ....:     while i<5:
   ....:         temp = yield i
   ....:         print(temp)
   ....:         i+=1
   ....:

使用send


In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:

    使用next函數

In [11]: f = gen()

In [12]: next(f)
Out[12]: 0

In [13]: next(f)
None
Out[13]: 1

In [14]: next(f)
None
Out[14]: 2

In [15]: next(f)
None
Out[15]: 3

In [16]: next(f)
None
Out[16]: 4

In [17]: next(f)
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-17-468f0afdf1b9> in <module>()
----> 1 next(f)

StopIteration:

使用__next__()方法(不常使用)

In [18]: f = gen()

In [19]: f.__next__()
Out[19]: 0

In [20]: f.__next__()
None
Out[20]: 1

In [21]: f.__next__()
None
Out[21]: 2

In [22]: f.__next__()
None
Out[22]: 3

In [23]: f.__next__()
None
Out[23]: 4

In [24]: f.__next__()
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-39ec527346a9> in <module>()
----> 1 f.__next__()

 

協程

協程,又稱微線程,纖程。英文名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)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()

運行結果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

 

greenlet

爲了更好使用協程來完成多任務,python中的greenlet模塊對其封裝,從而使得切換任務變的更加簡單

安裝方式

使用如下命令安裝greenlet模塊:

sudo pip3 install greenlet
#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

#切換到gr1中運行
gr1.switch()

運行效果

---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...

 

gevent

greenlet已經實現了協程,但是這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個比greenlet更強大的並且能夠自動切換任務的模塊gevent

其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網絡、文件操作等)操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。

由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent爲我們自動切換協程,就保證總有greenlet在運行,而不是等待IO

安裝

pip3 install gevent

1. gevent的使用

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

運行結果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

可以看到,3個greenlet是依次運行而不是交替運行

2. gevent切換執行


import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用來模擬一個耗時操作,注意不是time模塊中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

運行結果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

3. 給程序打補丁

from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

運行結果

work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9
from gevent import monkey
import gevent
import random
import time

# 有耗時操作時需要
monkey.patch_all()  # 將程序中用到的耗時操作的代碼,換爲gevent中自己實現的模塊

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

運行結果

work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9

 

進程、線程、協程對比

請仔細理解如下的通俗描述

  • 有一個老闆想要開個工廠進行生產某件商品(例如剪子)
  • 他需要花一些財力物力製作一條生產線,這個生產線上有很多的器件以及材料這些所有的 爲了能夠生產剪子而準備的資源稱之爲:進程
  • 只有生產線是不能夠進行生產的,所以老闆的找個工人來進行生產,這個工人能夠利用這些材料最終一步步的將剪子做出來,這個來做事情的工人稱之爲:線程
  • 這個老闆爲了提高生產率,想到3種辦法:
    1. 在這條生產線上多招些工人,一起來做剪子,這樣效率是成倍増長,即單進程 多線程方式
    2. 老闆發現這條生產線上的工人不是越多越好,因爲一條生產線的資源以及材料畢竟有限,所以老闆又花了些財力物力購置了另外一條生產線,然後再招些工人這樣效率又再一步提高了,即多進程 多線程方式
    3. 老闆發現,現在已經有了很多條生產線,並且每條生產線上已經有很多工人了(即程序是多進程的,每個進程中又有多個線程),爲了再次提高效率,老闆想了個損招,規定:如果某個員工在上班時臨時沒事或者再等待某些條件(比如等待另一個工人生產完謀道工序 之後他才能再次工作) ,那麼這個員工就利用這個時間去做其它的事情,那麼也就是說:如果一個線程等待某些條件,可以充分利用這個時間去做其它事情,其實這就是:協程方式

簡單總結

  1. 進程是資源分配的單位
  2. 線程是操作系統調度的單位
  3. 進程切換需要的資源很最大,效率很低
  4. 線程切換需要的資源一般,效率一般(當然了在不考慮GIL的情況下)
  5. 協程切換任務資源很小,效率高
  6. 多進程、多線程根據cpu核數不一樣可能是並行的,但是協程是在一個線程中 所以是併發
發佈了88 篇原創文章 · 獲贊 88 · 訪問量 49萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章