Python 函數與常用模組 - 生成器
Posted 我不会
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 函數與常用模組 - 生成器相关的知识,希望对你有一定的参考价值。
生成器
什麼是列表生成式?
這個是基本的列表
>>> a = [1, 2, 3]
>>> a
[1, 2, 3]
也可以用另一種方式來表示
>>> [ i*2 for i in range(10)]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
但這二個列表有什麼差別嗎?
第一種列表數據已經是寫死了,不能改變,第二種列表在產生的時候,是動態去產生的,而第二種寫法,也可以用下面代碼來實現。
>>> a = []
>>> for i in range(10):
... a.append(i*2)
...
>>> a
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
嗯!效果跟第二種寫法的結果是一樣的,所以第二種寫法,其實就是 列表生成式
,主要的目的就是把代碼變的更簡潔一點。
生成器
通過列表生成式,我們可以直接創建一個列表,但是受到記憶體的限制,列表的容量是有限,假設創建一個100萬個元素的列表,不僅占用很大的存儲空間,實際上我們卻只會存取這個列表的前面幾個元素,那後面絕大多數的元素占用的空間,不就白白浪費了?!
所以,如果列表元素可以按照某種算法推算出來的,那我們是不是可以在循環的過程中,不斷地推算出後面的元素呢?!這樣就不用建立完整的列表了,進而省下大量的存儲空間,在Python中,這樣一邊循環一邊計算的機制,就稱為 生成器(generator)
在Python2.x版中, range(10)
是已經把所有元素都準備好,放在記憶體裡了,而在Python3是還沒準備好,所以就可以節省一些記憶體的空間,至於為什麼呢?!細節不在此說明 只需要先知道就好了。
# Python2.x
>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Python3
>>> range(10)
range(0, 10)
能不能在調用到所需要的元素後,才生成相對應的元素?!實際上是可以的,我們接下來就以『有規律』的來做一個簡單的生成器
這是 列表生成式
↓↓↓
>>> [ i * 2 for i in range(10) ]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
只要把 中括號
換成 小括號
就可以變成 生成器
>>> ( i*2 for i in range(10) )
<generator object <genexpr> at 0x103cd8910>
上面這個代碼就是一個 生成器
,每循環一次就乘上2
接下來我們來驗証一下,為什麼 生成器
會比 列表生成式
還要省空間
首先我們先來觀察,使用 列表生成式
來觀察列表生成的結果,把下面的代碼,逐一貼在Python3解釋器中觀察。
[ i*2 for i in range(100) ]
[ i*2 for i in range(1000) ]
[ i*2 for i in range(10000) ]
[ i*2 for i in range(100000) ]
[ i*2 for i in range(1000000) ]
[ i*2 for i in range(10000000) ]
=> 我的筆電執行到這就變慢了[ i*2 for i in range(100000000) ]
有發現執行到 range(10000000)
時,就變慢了,這是因為列表生成時,會一次直接打印所有列表中的元素,而當你的機器的記憶體空間不夠時,就會發生這樣子的問題
再來觀察把 列表生成式
賦值給 a
>>> a = [ i*2 for i in range(10000000) ]
>>> a
...(略), 153018, 153020, 153022, 153024, 153026, 153028, 153030, 153032, 153034, 153036, 153038, 153040, 153042, 153044, 153046, 153048, 153050, 153052, 153054, 153056, 153058, 153060, 153062, 153064, 153066, 153068, 153070, 153072, 153074, 153076, 153078, 153080, 153082, 153084, 153086, 153088, 153090, 153092, 153094, 153096, 153098, 153100, 153102, 153104, 153106, 153108, 153110, 153112, 153114, 153116, 153118, 153120, 153122, 153124, 153126, 153128, 153130, 153132, 153134, 153136, 153138, 153140, 153142, 153144, 153146, 153148, 153150, 15^CTraceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyboardInterrupt
>>>
>>> # 實際去訪問a列表中的第10000個元素
>>> a[10000]
20000
>>>
再來觀察 生成器
賦值給 b
>>> b = ( i*2 for i in range(10000000) )
>>> b
<generator object <genexpr> at 0x1042c80a0>
>>> # ↑實際上會發現根本沒有生成任何元素,只是返回了一個記憶體位址而已
>>>
>>>
>>> # ↓直接去循環這個生成式,這樣才會去調用它,才會生成對應的元素
>>> for i in b:
... print(i)
...(略)
99240
99242
99244
99246
99248
^C99260
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt
>>>
>>> # ↓同樣去用列表的方式去訪問它
>>> c[10000]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ‘generator‘ object is not subscriptable
>>>
由上面實驗觀察可以得知,生成器
是要調用時,才會產生相應的數據,所以不支援列表的切片的形式取得。
那除了for循環可以取得數據外,那還有其他方式嗎?!目前只有一個方法叫 next
- Python2.x 的用法:
next()
- Python3的用法:
__next__()
那如果要個別訪問時,那要怎麼訪問呢?
>>> # 請看上面最後停止的數字是 99260
>>> b.__next__()
99262
>>> b.__next__()
99264
>>> b.__next__()
99266
生成器是不能往前調用的
,那…生成器是怎麼省記憶體呢!? 生成器 只記得當前的位置
,
所以我們創建了一個generator後,基本上不會調用 __next__()
,而是透過for循環來調用它,並且不需要關心StopIteration的錯誤。
如果推算的算法比較複雜,用類似列表生成式的for循環無法實現的時候,還可以用函數來實現,例如,著名的斐波拉契數列(Fibonacci),除了第一個和第二個數之外,任意一個數都可以由前面二個數相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ....
斐波拉契數列(Fibonacci)用列表生成式寫不出來,但是用函數把它打印出來卻很容易
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
# a = b => a = 1, b = 2 ; a = b, a = 2
# b = a + b => b = 2 + 2 = 4
n = n + 1
return ‘done‘
print(fib(10))
---------------執行結果---------------
1
1
2
3
5
8
13
21
34
55
done
Process finished with exit code 0
注意,賦值語句
a, b = b , a + b
其實就相當於是
t = (b, a + b) # t是一個tuple
a = t[0]
b = t[1]
請看範例
>>> a, b = 1, 2
>>> t = ( b, a + b )
>>> t
(2, 3)
>>> print(t[0])
2
>>> print(t[1])
3
>>>
但不必顯式地寫出臨時變量 t
就可以賦值。
仔細觀察後,不難看出,fib函數實際上是定義了斐波拉契數的推算規則,可以從第一個元素開始,推算出後面任意的元素,這種邏輯其實非常類似 generator
。
也就是說,上面的函數和generator只差一步了,要把fib函數變成generator,只需要把 print(b)
修改成 yield b
就可以了。
!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
return ‘done‘
# fib(10)
print(fib(10))
---------------執行結果---------------
<generator object fib at 0x10a0c5f10>
Process finished with exit code 0
耶! 這樣就變成 生成器
了。
這就是定義generator的另一種方法,如果一個函數定義中包含 yield
這個關鍵字,那麼這個函數就不是一個普通的函數,而是一個 generator
那我們來按照之前說的,使用 __next__()
這個方法來調用這個生成器
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
# return ‘done‘
f = fib(100)
print(f.__next__())
print(f.__next__())
print(f.__next__())
---------------執行結果---------------
1
1
2
Process finished with exit code 0
yield
這個可以讓我們在暫時的離開目前的函數,可以跳出來做別的事情
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
# return ‘done‘
f = fib(100)
print(f.__next__())
print("====exit function====")
print(f.__next__())
print(f.__next__())
---------------執行結果---------------
1
====exit function====
1
2
Process finished with exit code 0
這裡最難理解的就是generator和函數執行的流程不一樣,函數是順序執行的,遇到 return語句
或者最後一行函數語句就返回。而變成generator的函數,在每次調用 __next__()
的時候執行,遇到 yield語句
返回,再執行時從上次返回的yield語句處繼續執行。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
# return ‘done‘
f = fib(10)
print(f.__next__())
print("====exit function====")
print(f.__next__())
print(f.__next__())
print("====start loop====")
for i in f:
print(i)
---------------執行結果---------------
1
====exit function====
1
2
====start loop====
3
5
8
13
21
34
55
Process finished with exit code 0
在上面的fib的例子,我們在循環過程中不斷調用yield,就會不斷中斷,當然要給循環設置一個條件來退出循環,不然就會產生一個無限數列出來。
同樣的,把函數改成generator後,我們基本上從來不會用 __next__()
來獲取下一個返回值,而是直接使用for循環來迭代
這次把return語句的注解拿掉,來觀察用for循環時,會打印出來嗎?
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
return ‘done‘
f = fib(10)
print(f.__next__())
print("====exit function====")
print(f.__next__())
print(f.__next__())
print("====start loop====")
for i in f:
print(i)
---------------執行結果---------------
1
====exit function====
1
2
====start loop====
3
5
8
13
21
34
55
Process finished with exit code 0
居然沒有打印出來耶,為什麼呢?!沒關係,那我們就不要使用for循環了,直接用print的方式直接打印好了。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
return ‘done‘
f = fib(10)
print(f.__next__())
print("====exit function====")
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())
---------------執行結果---------------
Traceback (most recent call last):
File "/Pythone/project/fibonacci_basic.py", line 27, in <module>
print(f.__next__())
StopIteration: done
1
====exit function====
1
2
3
5
8
13
21
34
55
Process finished with exit code 1
只要用 __next__()
這個方法取值,如果取到最後一行,沒有值可以取的話,就會拋出 StopIteration: done
這樣的異常。
如果想要拿到返回值,必須捕抓StopIteration錯誤,返回值包含在StopIteration的value中
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
def fib(max):
n, a, b = 0, 0, 1
while n < max:
# print(b)
yield b
a, b = b, a + b
n = n + 1
return ‘出現異常,return想打印什麼就打印什麼‘
f = fib(10)
while True:
try:
x = next(f)
print(‘f:‘, x)
except StopIteration as e:
print("Generator return value:", e.value)
break
print("====start loop====")
for i in f:
print(i)
---------------執行結果---------------
f: 1
f: 1
f: 2
f: 3
f: 5
f: 8
f: 13
f: 21
f: 34
f: 55
Generator return value: 出現異常,return想打印什麼就打印什麼
====start loop====
Process finished with exit code 0
上面這個代碼,已經不能叫做一個函數了,而是一個生成器,因為代碼中,已經有包含了yield語句了,而return在這裡面的作用則是異常的時候,所打印的訊息。
以上是关于Python 函數與常用模組 - 生成器的主要内容,如果未能解决你的问题,请参考以下文章