[TOC] 1. 迭代器 什麼是生成器呢,其實生成器的本質就是迭代器;在python中有3中方式來獲取生成器(這裡主要介紹前面2種) 通過生成器函數獲取 通過各種推導式來實現生成器 生成器函數 我們來看一個普通的函數: 那麼生成器函數跟普通函數有什麼不同呢,我們只要把其中的 換成 關鍵字參數就是生成 ...
目錄
1. 迭代器
什麼是生成器呢,其實生成器的本質就是迭代器;在python中有3中方式來獲取生成器(這裡主要介紹前面2種)
- 通過生成器函數獲取
- 通過各種推導式來實現生成器
生成器函數
我們來看一個普通的函數:
In[2]: def func1():
...: print('aaaa')
...: return 1111
...:
In[3]: fun = func1()
aaaa
In[4]: print(fun)
1111
那麼生成器函數跟普通函數有什麼不同呢,我們只要把其中的return
換成yield
關鍵字參數就是生成器函數了:
In[5]: def func1():
...: print('aaaa')
...: yield 1111
...:
In[6]: fun = func1() # 此時並沒有任何列印信息,可以說明函數並沒有執行
In[7]: print(fun) # 從輸出可以看出這是一個生成器對象
<generator object func1 at 0x0000016F900D6DB0>
從上面的結果來看,我們發現函數func1根本就沒有執行,而最後列印的是一個記憶體地址,這個就是生成器很明顯的一個特性:惰性計算,那麼我們要怎麼執行它呢?我們可以回顧一下迭代器的取值方法:使用迭代器的__next__
的方法可以取到迭代器的一個值,那生成器的本質就是迭代器,那我們也可以試下可以這樣取值
In[8]: fun.__next__() # 從輸出可以看出,yield也和return一樣可以有返回值
aaaa # 這裡我們就可以看到函數中的aaaa也列印了,表示函數在此處才執行
Out[8]: 1111
我們再來看個例子,觀察下生成器是怎麼工作的:
In[9]: def func1():
...: print('aaaa')
...: yield '我是第一個yield'
...: print('bbbb')
...: yield '我是第二個yield'
...: print('cccc')
...:
In[10]: gen = func1() # 這裡得到的是一個生成器,此處並不會運行函數
...: print(gen)
<generator object func1 at 0x0000016F900F8BA0>
In[11]: print(gen.__next__()) # 首次執行生成器的__netx__()函數時,開始執行函數,
aaaa # 直到遇到yield時返回,並且yield也可以有返回值
我是第一個yield
In[12]: print(gen.__next__()) # 再次運行__netx__()函數時,會繼續執行函數(從上次yield的位置繼續執行)
bbbb
我是第二個yield
In[13]: print(gen.__next__()) # 再次執行__next__()方法繼續執行,此處再往下執行時沒有了yield關鍵字,
cccc # 會拋出StopIteration異常(但時會執行後面的代碼)
Traceback (most recent call last):
File "D:\Environment\python-virtualenv\jupyter\lib\site-packages\IPython\core\interactiveshell.py", line 3265, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-13-9340d28f24b7>", line 1, in <module>
print(gen.__next__())
StopIteration
從上面我們呢可以總結出:
- yield也可以像return一樣也是返回值
- yield執行完之後會返回到調用者,執行後續的代碼,直到再次調用__next__方法,此時生成器函數再從上次停止的位置繼續執行
- 當執行
__next__
方法後沒有yield關鍵字時,會拋出StopIteration
異常,但是會執行yield後面的代碼
send方法
接下來我們來看send⽅法, send和__next__()⼀樣都可以讓⽣成器執⾏到下⼀個yield
In[14]: def eat():
...: print("aaaa")
...: a = yield 1111
...: print("a=",a)
...: b = yield "bbbb"
...: print("b=",b)
...: c = yield "cccc"
...: print("c=",c)
...: yield "GAME OVER"
...:
In[15]: gen = eat() # 獲取⽣成器
In[16]: ret1 = gen.__next__()
...: print(ret1)
aaaa
1111
In[17]: ret2 = gen.send("我send了一個參數給a")
...: print(ret2)
a= 我send了一個參數給a # 可以看出send的數據是被上一個yield前的a給接收了
bbbb
In[18]: ret3 = gen.send("我send了一個參數給b")
...: print(ret3) # 這裡send的數據也是被b接收了
b= 我send了一個參數給b
cccc
In[19]: ret4 = gen.send("我send了一個參數給c")
...: print(ret4)
c= 我send了一個參數給c
GAME OVER
**send和__next__()**:
- send和next()都是讓⽣成器向下走⼀次
- send可以給上⼀個yield的位置傳遞值, 不能給最後⼀個yield發送值. 在第⼀次執⾏⽣成器代碼的時候不能使⽤send()
2. 推導式
列表推導式
關於列表推導式,其實之前的文章中已經使用過,這裡再正式介紹下;假設我們要列印1到20之間的奇數,照之前正常的寫法我們要這麼寫:
# 假設有一個需求,要寫一個迴圈遍歷1到20之間所有的奇數
lst = []
for i in range(1, 21):
if i % 2 == 1:
lst.append(i)
print(lst)
# 結果:
# [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
列表推導式的語法為:
- 第一種只使用for迴圈遍歷
[expr for item in itratorable]
# 相當於以下代碼
ret = []
for item in iterable:
ret.append(expr)
- 第二種for迴圈遍歷再加if條件判斷
[expr for item in iterable if cond]
# 相當於以下結構代碼
ret = []
for item in iterable:
if cond:
ret.append(expr)
第三種for迴圈加if雙分支結構,註意此時的if/else語句要寫在for語句前面
[expr1 if cond else expr2 for item in iterable ]
# 相當於以下代碼
ret = []
for item in iterable:
if cond:
ret.append(expr1)
else:
ret.append(expr2)
對於上面的例子使用列表推導式可以這樣寫:
# 使用推導式:
lst = [i for i in range(1, 21) if i % 2 == 1]
print(lst)
# 結果:
# [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
使用列表推導式我們可以發現代碼時精簡了許多,而且代碼的可讀性更高了,其實還有一個優勢是推導式速度更快:
In [1]: %%timeit
...: lst1 = []
...: for i in range(10000):
...: lst1.append(i)
...:
788 µs ± 14.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [2]: %%timeit
...: lst1 = [i for i in range(10000)]
...:
307 µs ± 1.84 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [3]:
從上面的結果分析,使用列表推導式生成列表的方式要比普通for迴圈的效率要高很多
字典推導式
字典跟列表推導式的語法非常相似,使用{}
括起來,然後在裡面想列表推導式一樣寫自己的表達式即可:
dic = {expr for k, v in iterable if cond} # 這裡的expr表達式可以寫成:k: v的形式
# 相當於以下代碼
dic = dict()
for k, v in iterable:
if cond:
expr(dic)
例如,把字典中的鍵值對都調換以下可以用如下方法:
dic = {"張無忌":"趙敏", "楊過":"小龍女", "郭靖":"黃蓉"}
# dic = {'k1':'v1', 'k2': 'v2', 'k3': 'v3'}
dic = {v: k for k, v in dic.items()}
print(dic)
生成器表達式
對於生成器表達式來說,只需要把列表推導式的中括弧換成小括弧就可以了:
In[20]: def inc(x):
...: print('inc {0}'.format(x))
...: return x+1
...:
In[21]: g = (inc(x) for x in range(10)) # 這裡的g就是一個生成器對象
In[22]: print(g)
<generator object <genexpr> at 0x0000016F90161DB0>
In[23]: print(g.__next__())
inc 0
1
In[24]: print(g.__next__()) # 也可以使用__next__方法取出一個值
inc 1
2
In[25]: print(g.__next__())
inc 2
3
In[26]: next(g) # 使用netx()和__next__()方法是一樣的
inc 3
Out[26]: 4
In[27]: next(g)
inc 4
Out[27]: 5
當然,生成器表達式也可以跟其他推導式一樣套用if語句,其語法都是一樣的,這裡就不做介紹了。
⽣成器表達式和列表推導式的區別:
列表推導式比較耗記憶體. ⼀次性載入. ⽣成器表達式⼏乎不占⽤記憶體. 使⽤的時候才分
配和使⽤記憶體得到的值不⼀樣. 列表推導式得到的是⼀個列表. ⽣成器表達式獲取的是⼀個⽣成器.
⽣成器的惰性機制: ⽣成器只有在訪問的時候才取值. 說⽩了. 你找他要他才給你值. 不找他
要. 他是不會執⾏的.
def func():
print(111)
yield 222
g = func() # ⽣成器g
g1 = (i for i in g) # ⽣成器g1. 但是g1的數據來源於g
g2 = (i for i in g1) # ⽣成器g2. 來源g1
print(list(g)) # 獲取g中的數據. 這時func()才會被執⾏. 列印111.獲取到222. g完畢.
print(list(g1)) # 獲取g1中的數據. g1的數據來源是g. 但是g已經取完了. g1 也就沒有數據了
print(list(g2)) # 和g1同理
# 註:list中有for的調用,可以迭代遍歷生成器元素
#結果:
# 1111
# [222]
# []
# []
訪問生成器的另一種方法
使用yield from iterator
語句
In[28]: def test():
...: l1 = [1, 2, 3, 4]
...: l2 = ['a', 'b', 'c', 'd']
...: yield from l1 #
...: yield from l2
...:
In[29]: g = test()
In[30]: for i in g:
...: print(i)
...:
1
2
3
4
a
b
c
d