1. 程式人生 > >對Python生成器的理解

對Python生成器的理解

下午看了一點生成器的內容,這部分算是python的基礎吧。所以我就不放在我的進階系列了。正好吃飽飯沒事做,就來寫寫我對生成器的一點淺薄理解吧。 ——無聊的前言

一.為什麼要有生成器

秉著先問為什麼,再問怎麼做的原則,我們來看看為什麼python會新增生成器這個功能。

python在資料科學領域可以說是很火。我想有一部分的功勞就是它的生成器了吧。

我們知道我們可以用列表儲存資料,可是當我們的資料特別大的時候建立一個列表的儲存資料就會很佔記憶體的。這時生成器就派上用場了。它可以說是一個不怎麼佔計算機資源的一種方法。

二.簡單的生成器

我們可以用列表推導(生成式)來初始化一個列表:

list5 = [x for x in range(5)]
print(list5)   #output:[0, 1, 2, 3, 4]

我們用類似的方式來生成一個生成器,只不過我們這次將上面的[ ]換成( ):

gen = (x for x in range(5))
print(gen) 
#output: <generator object <genexpr> at 0x0000000000AA20F8>

看到上面print(gen) 並不是直接輸出結果,而是告訴我們這是一個生成器。那麼我們要怎麼呼叫這個gen呢。
有兩種方式:
第一種:

for item in gen:
    print(item)
#output:
0
1
2
3
4

第二種:

print(next(gen))#output:0
print(next(gen))#output:1
print(next(gen))#output:2
print(next(gen))#output:3
print(next(gen))#output:4
print(next(gen))#output:Traceback (most recent call last):StopIteration

好了。現在可以考慮下背後的原理是什麼了。
從第一個用for的呼叫方式我們可以知道生成器是可迭代的。更準確的說法是他就是個迭代器。
我們可以驗證一下:

from collections import Iterable, Iterator
print(isinstance(gen, Iterable))#output:True
print(isinstance(gen, Iterator))#output:True

str,list,tuple,dict,set這些都是可迭代的,就是可用for來訪問裡面的每一個元素。但他們並不是迭代器。

那什麼是迭代器?
我們可以理解為我們平時做一件事的步驟。

比如我們泡茶:
首先,得去煮水。
然後,拿出茶具,和茶葉
接著,水開了,就開始泡茶
最後,就是品茶了。

假如我們定義了一個泡茶的函式(迭代器),然後將泡茶步驟的方法封裝進這個函式。每一次呼叫這個函式就返回一個步驟,並儲存好當前執行到哪個狀態。如果中途有事,比如我們執行到步驟二的時候突然去接了個電話,回來呼叫這個函式就會得到步驟三(水開了,就開始泡茶),也就是狀態儲存好了。我們可以執行這個泡茶函式直到呼叫完所有步驟為止。
定義一個方法,這個方法是一步步執行的,並能儲存狀態,這就是迭代器。

回到上面上面第二種訪問方法中,到第六個print(next(gen))時,系統告訴我們Traceback (most recent call last): StopIteration。也就是gen迭代到最後了,無法繼續迭代了。

而生成器本身就是一個迭代器
我們在內部封裝好了演算法,並規定好在某個條件下就返回一個結果給呼叫者。(x for x in range(5))就是這樣子實現的,並不是實現了(0,1,2,3,4)然後在一個個迭代出來,而是逐個生成。這就是為什麼next(gen)可以作用了。

三.應用

1.前面說到生成器生成大量資料的時候可幫助系統節省記憶體。真的是這樣嗎?通過下面的程式碼感受下:

程式碼裡面會運用到裝飾器的原理,看不懂沒關係後文會解釋有什麼作用。當然想了解的可以看我另一篇博文:Python進階(四):淺析裝飾器(decorator)@

import time

def get_time(func):
    def wraper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print("Spend:", end_time - start_time)
        return result
    return wraper

@get_time
def _list(n):
    l1 = [list(range(n)) for i in range(n)]
    del ge


@get_time
def _generator(n):
    ge = (tuple(range(n)) for i in range(n))
    del t1

_list(1000)
_generator(1000)

哈哈,有人實在看不懂的話可以看下面的程式碼,與上面等效的

題外話:其實上面那個get_time函式是我自己平常經常要用到的一個功能。我把它寫到一個我的工具包裡了(造輪子),就是如果我想看看一段函式的執行時間(測試效率),就掉用這個包裡的get_time裝飾器,裝飾一下就好了。所以建議大家如果發現有個功能經常要用到的話可以嘗試下自己造輪子。這裡我就直接貼上程式碼了

import time

def _list(n):
    l1 = [list(range(n)) for i in range(n)]
    del l1

def _generator(n):
    ge = (tuple(range(n)) for i in range(n))
    del ge

start_time = time.time()
_list(1000)
end_time = time.time()
print("Spend:",end_time - start_time)

start_time = time.time()
_generator(1000)
end_time = time.time()
print("Spend:",end_time - start_time)

好了,執行程式碼我們可以看到:

Spend: 0.04300236701965332
Spend: 0.0

分析下就可以知道,列表是將0-999都生成後放進一個列表裡面了,所以用得時間比較多。
而生成器只是封裝了演算法,每次呼叫在去呼叫演算法,這樣做就可以做到節省記憶體了。

2.yield 關鍵詞

好吧,前面只告訴我們用( )來建立一個生成器。如果我們想定義一個自己的生成器函式怎麼辦?用return好像不行。沒關係,python有yield的關鍵詞。其作用和return的功能差不多,就是返回一個值給呼叫者,只不過有yield的函式返回值後函式依然保持呼叫yield時的狀態,當下次呼叫的時候,在原先的基礎上繼續執行程式碼,直到遇到下一個yield或者滿足結束條件結束函式為止。

一個簡單的例子:

def test():
    yield 1
    yield 2
    yield 3
t = test()

print(next(t))#output:1
print(next(t))#output:1
print(next(t))#output:1
print(next(t))#output:Traceback (most recent call last):StopIteration

好像並沒啥卵用啊!騷年,存在即合理,python有生成器不是沒有道理的。數學中有很多演算法是無限窮舉的(比如自然數),我們不可能一一窮舉出來,所以生成器就可以幫助我們。
舉個例子:楊輝三角。
這裡寫圖片描述
這就是一個無限窮舉的,我們可以將他的演算法封裝成生成器,需要的時候去生成就好,這樣就不會佔用大量的電腦記憶體資源。
下面給出程式碼:

def triangle():
    _list, new_list = [], []
    while True:
        length = len(_list)
        if length == 0:
            new_list.append(1)
        else:
            for times in range(length + 1):
                if times == 0:
                    new_list.append(1)
                elif times == length:
                    new_list.append(1)
                else:
                    temp = _list[times - 1] + _list[times]
                    new_list.append(temp)
        yield new_list #返回值,然後掛起函式,等待下一次呼叫
        _list = new_list.copy()#呼叫後會繼續執行下去
        new_list.clear()

n = 0
for result in triangle():
    n += 1
    print(result)
    if n == 10:
        break

結果:

[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]

好了,覺得寫得好可以關注微信公眾號:
這裡寫圖片描述