10. 迭代器和生成器/協程/列表生成式

[TOC]

迭代器

為什么要用迭代器?

提供了一種不依賴索引的取值方式,使一些不具有索引屬性的對象也能遍歷輸出
相比列表,迭代器的惰性計算更加節約內存。
但是它無法有針對性地指定取某個值,并且只能向后取值。

>>> from collections import Iterable
>>> isinstance([],Iterable)
True

>>> from collections import Iterator
>>> isinstance((x for x in range(10)),Iterator)
True
>>> isinstance([],Iterator)
False
>>> isinstance({},Iterator)
False
>>> isinstance('abx',Iterator)
False

注意這兩個單詞:‘Iterable’和‘Iterator’

老師區分可迭代對象和迭代器的方法,就是對象是否內置.__iter__方法。而這個方法的運行,也就是xxx.__iter__(),被賦值給一個i。i = xxx.__iter__
那么這個i就成為一個迭代器。這個過程也可以表示為i = iter(xxx)
迭代器本身就有一個i.__next__方法,其實就相當于next(i)

  • Iterable:表示這個對象是“可以被迭代的”。比如說list、dict、str都是Iterable,但是根據上面代碼可以發現,list、dict、str并不能算迭代器。

  • Iterator:迭代器對象。定義迭代器對象的時候,要有“數據流”的概念。迭代器和可迭代對象的差別就在于,迭代器是一個數據流,可以看成是一個有序序列,我們不知道序列的長度,只能通過next()函數對他進行不斷迭代,直到爆出StopIteration的錯誤提示.

其實這個StopIteration并不能算一個錯誤,只是一個提示,表示這個迭代器已經被迭代完了。這時候就需要使用try……except……函數來規避掉這個錯誤,在出現StopIteration的時候自動跳出迭代。

可迭代對象轉換成迭代器: iter()

小結:

凡是可以使用for循環的,都是Iterabale;
凡是可以使用next()方法的都是Iterator,迭代器代表一個惰性計算(無限有限皆可)序列,比方說:全體自然數集合就是一個Iterator;
一個迭代器被iter()之后,仍然是一個迭代器

for x in [1,2,3,4,5,6]:
    print (x)

print('======================')

it = iter([1,2,3,4,5,6])
while True:
    try:
        print(next(it))
    except StopIteration:
        break

以上兩段代碼輸出結果相同,所以我們就能理解for方法的運行原理了

生成器

  • 關鍵字:yield

為什么要使用生成器,什么是生成器?

可循環的類型(Iteratable)比如list,占用內存空間較大,當你想要其中的一個元素的時候只能將list放在內存里,根據索引去獲取,生成器本質就是一個迭代器,但是我們知道迭代器是可以循環出來的,每次都可以只輸出一個值,節約了內存空間。
如果一個迭代對象,他后面的每個值都是可以根據一定計算獲得的。我們是否可以在循環的過程中不斷推算出后續的元素,這樣就不必創建list,節省了大部分空間。
這種一邊循環一邊計算的機制,就是生成器:generator

如何創建一個生成器

  • 將列表生成式的[]改成()
>>> l = [x*x for x in range(10)]
>>> l
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000006CC7E0D1A8>

列表生成后,list可以直接打印出來,但是generator的迭代器屬性,每個元素需要用next()函數獲取

 >>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9

以上這種調用方式,手工操作部分太多了,所以正確的調用應該是使用for循環:

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> for i in g:
...     print(i)
...
16
25
36
49
64
81

啊哈哈哈哈,側面反映了迭代器的單項輸出性質,只能迭代下一個。這里還可以注意到,用for調用generator的時候,不需要擔心StopIteration錯誤

舉個栗子:斐波拉契數列

函數生成斐波拉契數列,并注釋邏輯:

# 函數生成斐波拉契數列
def fibonacci(max):
    n, a, b = 0, 0, 1   #這里的n用于計數,取最大數用
                        #a是前置數,用于配合b獲取最開始的兩個數字
                        #b是第一個數
    while n < max:      #當n未達到max的時候
        print(b)        #打印數字b,后面可以看到,b的值是前兩個數相加的和
        a, b = b, a + b #對a,b分別進行賦值,a是原來的b,b是前兩數相加獲得后的和
        n = n + 1       #n的計數加一
    return 'done'

這時候執行fibonacci(num),得到的結果就是一個最大長度為num的fibonacci數列了。
而想要把這個函數改成一個generator,其實只要通過一句yield就可以:

# 函數生成斐波拉契生成器
def fibonacci(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
        return 'done'
print(fibonacci(10))

結果為:

<generator object fibonacci at 0x000000D75546D1A8>

Process finished with exit code 0

這里的fibonacci已經不再是一個函數了,而是一個生成器,他的結果需要被循環出來,而他本身就作為一個方法被儲存在某個變量里。

f = fibonacci(6)
for i in f :
    print(i)

用yield返回結果的執行流程

使用了yield之后,函數就變成了生成器。我們原先認為函數運行到return、或者函數的最后一行語句就返回。但是添加了yield之后的生成器不一樣,在每次調用了next()執行,遇到yield就返回,再次執行時,從上次返回的yield開始繼續執行

在一個函數中,可以有多個yield,但是這種不太常見,在我的理解里,yield就有點像IDE當中的debug斷點,在指定斷點輸出一個值。但是這個斷點越多越復雜,反而會不好,因為他最終返回的生成器是一個同類數據流,當你這個數據流中的數據出現兩種,就不太適合被再次加工了。
所以一般用一個yield來定義一束數據流(生成器),然后通過生成器具有的迭代器特性來逐個輸出處理數據。起到節約內存的作用。

注意:yield有時候會作為一個傳參工具(下文會細說,搭配.send()使用,被稱為協程函數),這時候的yield會被放在一個函數中比較靠前位置,但是函數本身暫時沒有可以輸出的值,這時候就需要提前使用next()方法,將生成器初始化到yield的位置
比較優秀的方法,是使用裝飾器,提前將函數next()或者g.send(None)一次,并且方便后面的生成器運行調用:

def init(func):
    def wrapper(*args,**kwargs):
        res = func(*args,**kwargs)
        next(res)
        return res
    return wrapper

@init

如果上次的yield是最后一個,并且生成器是一個被while包圍的函數,就從上次結束的yield處繼續下次循環,仍然遇到這個yield就輸出返回。

>>> def odd():
...     print('step1')
...     yield 1
...     print('step2')
...     yield 2
...     print('step3')
...     yield 3
>>> o = odd()
>>> next(o)
step1
1
>>> next(o)
step2
2
>>> next(o)
step3
3

實驗記錄:

>>> def foo():
...     print('llllll')
...     yield 3
...     print('wwwwww')
  File "<stdin>", line 4
    print('wwwwww')
                  ^
IndentationError: unindent does not match any outer indentation level

yield后面跟返回的值如果不用括號括起來,容易報錯,注意養成習慣

作業代碼以及注釋:

#模擬管道功能,將cat的處理結果作為grep的輸入
#從文件中獲取想要的數據
def cat(file_name):         #傳參獲得目標文件名
    with open(file_name,'r') as f:
        res = iter(f.readlines())
    return res
#通過readlines方法獲取文件的行集合list,
#并且將這個集合生成為一個迭代器返回給函數


#傳參獲取目標關鍵字,文件內容迭代器,
def grep(key_str,iterator):
    for i in iterator:
    #迭代文件內容并且匹配關鍵字
        if key_str in i.strip():
            #匹配到后輸出關鍵字
            print (i.strip())

# 調用函數階段:
# grep('apple',cat('a.txt'))

協程函數

如果函數內yield的表達形式為var= yield,那么必須在往生成器函數中傳參前,next(g)或者e.send(None)
因為協程函數中,需要將函數初始化暫停至yield所在點,然后再進行生成器輪巡運算。

面向過程編程

在提協程函數的時候,還需要提一個面向過程編程思想
流水式的變成思想,在設計程序時,需要把整個流程設計出來。
這種思想的優缺點:

  • 優點:
    體系結構更加清晰
    簡化程序的復雜度
  • 缺點:
    可擴展性差,一條流程只能給一組功能使用。

作業以及代碼注解:

# 定義一個可以不斷傳入(send方法)網址來進行爬取數據的生成器函數
# 定義一個配合協程函數的裝飾器
def yield_next(func):
    def wrapper(*arg,**kwargs):
        res = func(*arg,**kwargs)
        next(res)
        #注意,這里仍然需要返回res給函數wrapper
        #如果缺少這步,下方調用return wrapper的時候無效
        return res
    return wrapper
#模塊加載
from urllib.request import urlopen

@yield_next
def get_url():
    while True:
        url = yield
        #外部傳值給yield 相當于yield 統一資源定位器,并且url能傳入新的值
        url_res = urlopen(url)      #爬取url指定的頁面內容
        webLine = url_res.read()    # 讀取html頁面
        print(webLine)              #輸出url頁面

g = get_url()
#由于之前使用了裝飾器,這里不用next()
g.send('http://www.baidu.com')

典型范例以及代碼解析:

實現linux中grep -rl 'python' dir_path效果的代碼

# 想了半天覺得這個代碼好蠢,不放了。

列表生成式

范例:

l = ['egg%s'%i for i in range(100) if i >50]
print(l)

g = os.walk(c:\\scott)
l1 = ['%s\\%s'%(i[0],j)for i in g for j in i[-1]]

返回結果放在最前,列表生成的for運算放在中間,后面可加判斷語句
如果直接調用這個列表生成式的結果給一個函數進行運算,可以不用添加[]

生成器表達式

就是把列表生成的[]換成()
l = ('egg%s'%i for i in range(100) if i >50)

作業和練習

# 今日作業
# 有兩個列表,
# 分別存放來老男孩報名學習linux和python課程的學生名字

linux = ['鋼彈', '小壁虎', '小虎比', 'alex', 'wupeiqi', 'yuanhao']
python = ['dragon', '鋼彈', 'zhejiangF4', '小虎比']
# 問題一:得出既報名linux又報名python的學生列表
l1 = [x for x in linux if  x in python]
print (l1)
# 問題二:得出只報名linux,而沒有報名python的學生列表
l2 = [x for x in linux if x not in python]
print(l2)
# 問題三:得出只報名python,而沒有報名linux的學生列表
l3 = [x for x in python if x not in linux]
print(l3)


shares = {

    'IBM': 36.6,

    'lenovo': 27.3,

    'huawei': 40.3,

    'oldboy': 3.2,
    'ocean':20.1
}

# 問題一:得出股票價格大于30的股票名字列表
list_1 = [i for i in shares if shares[i]>30]
print(list_1)
# 問題二:求出所有股票的總價格
list_sum = sum (shares[i] for i in shares)
print(list_sum)


l = [10, 2, 3, 4, 5, 6, 7]

# 得到一個新列表l1,
# 新列表中每個元素是l中對應每個元素值的平方
l1 = [(x*x) for x in l]
print(l1)
# 過濾出l1中大于40的值,然后求和
l1_sum = sum(y for y in l1 if y >40)
print(l1_sum)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容