1. 程式人生 > >Python 函數與常用模組 - 生成器

Python 函數與常用模組 - 生成器

board lin 停止 叠代 pri mod expr ner next()

生成器

什麼是列表生成式?

這個是基本的列表

>>> 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 函數與常用模組 - 生成器