『無為則無心』Python基礎 — 63、Python中的生成器

繁華似錦Fighting發表於2022-03-03

1、為什麼要有生成器

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

我們知道我們可以用列表儲存資料,可是當我們的資料特別大的時候,列表中的資料都是放在記憶體中,受到記憶體限制,列表容量肯定是有限的,而且還會降低計算機的效能。

如果僅僅需要訪問前面幾個元素,那後面絕大多數元素佔用的空間都白白浪費了。但如果列表中元素是按照某種演算法推算出來,那我們就可以在迴圈的過程中不斷推算出後續的元素,這樣就不必建立完整的列表資料,從而節省大量的空間。

換句話說,我又想要得到龐大的資料,又想讓它佔用空間少,這時生成器就派上用場了,它可以說是一個不怎麼佔計算機資源的一種方法。

2、建立生成器

(1)簡單建立生成器

將一個列表推導式(也叫列表生成式) [] 改為 ()即可建立一個生成器。

# 1.用推導式定義一個列表
# 關於推導式請看以前的文章有講解。
my_list = [x * x for x in range(10)]

# 列印列表
print(my_list)
# 檢視my_list型別,是一個列表
print(type(my_list))
"""
輸出結果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
<class 'list'>
"""



# 2.建立一個生成器
my_gen = (x * x for x in range(10))

# 列印生成器,是一個生成器物件
print(my_gen)
# 檢視my_gen物件型別,是生成器型別
print(type(my_gen))
"""
輸出結果:
<generator object <genexpr> at 0x0000000002575148>
<class 'generator'>
"""

(2)生成器的使用

# 建立生成器
my_gen = (x * x for x in range(10))

# 1。方式一:遍歷生成器,使用next方法
print(my_gen.__next__())  # 0
print(my_gen.__next__())  # 1
print(my_gen.__next__())  # 4
print(my_gen.__next__())  # 9
# 或者
print(next(my_gen))  # 16
print(next(my_gen))  # 沒有資料了則會丟擲異常StopIteration



# 2.方式二:遍歷生成器的內容
for i in my_gen:
    print(1)


# 3.方式三:遍歷生成器的內容
while True:
    try:
        # 呼叫next函式,獲取下一個字元
        result = next(my_gen)
        print(result)
    except StopIteration:
        # 釋放對it的引用,即廢棄迭代器物件
        del my_gen
        # 不推出迴圈會成為私迴圈
        break

提示:

  • 在上邊練習中,可以看到和迭代器的用法差不多,在這裡說明一下生成器本身就是一個迭代器。如果有對迭代器不清楚的可以檢視前面說明迭代器的文章。
  • 上面方式一不斷呼叫next()方法回去元素,實在是太變態了,正確的方法是使用for迴圈。
  • generator儲存的是演算法,每次呼叫next(),就計算出下一個元素的值,直到計算到最後一個元素,沒有更多的元素時,丟擲StopIteration的錯誤。

總結:

  • []推匯出來的是迭代器(Iterables)。
  • ()推匯出來的是生成器(Generators)。

3、yield關鍵詞

(1)yield關鍵詞說明

如果我們想定義一個自己的生成器函式怎麼辦?

Python有yield關鍵詞。其作用和return的功能差不多,就是返回一個值給呼叫者,只不過有yield的函式返回值後,函式依然保持呼叫yield時的狀態,當下次呼叫的時候,在原先的基礎上繼續執行程式碼,直到遇到下一個yield或者滿足結束條件結束函式為止。

啥意思?啥意思?啥意思?

  • 你先把yield關鍵字直觀的看做return關鍵字,它首先是return的功能,就是在函式或方法中返回某個值,返回之後程式就不再往下執行了。
  • yield相當於返回一個值給呼叫者,停止執行函式中的語句,並且記住這個返回的位置。下次迭代時(或者執行next方法的時候),程式碼從yield記錄位置的下一條語句開始執行。
  • 帶有yield的函式不再是一個普通函式,而是一個生成器generator
  • 呼叫一個生成器函式,返回的是一個迭代器物件。

示例:

# 定義一個生成器函式
def testYield():
    yield 1
    yield 2
    yield 3

# 獲得一個生成器物件
ty = testYield()

"""
呼叫過程:
next(ty)相當於ty.__next__()
掉呼叫一次next(ty)時
就會執行testYield()內的方法。
當執行的第一行, yield 1時,
返回當前yield的值1給呼叫者,停止向下執行,並記錄函式中當前的執行位置。
也就是每次遇到 yield 時函式會暫停並儲存當前所有的執行資訊,返回 yield 的值。
程式執行結束

當下次再呼叫next(ty)的時候,
還是會執行testYield()內的方法,
只不過是從yield 1下面一句開始執行。
以此類推。
"""
print(next(ty))  # 1
print(next(ty))  # 2
print(next(ty))  # 3
print(next(ty))  # StopIteration

注意:每次呼叫 testYield()函式都會生成一個新的 generator 例項,各例項互不影響。

(2)send()方法說明

send()方法和next()方法一樣,都能讓生成器繼續往下走一步(下次遇到yield停),但send()能傳一個值,這個值作為yield表示式整體的結果。

def testYield():
    yield 1
    y = yield 2
    if (y == 'hello'):
        yield 9
    yield 3


ty = testYield()

print(ty.__next__())  # 1
print(next(ty))  # 2
"""
第三次執行,send方法會把"hello"傳遞進去
就是y = "hello"
換句話說,就是send可以強行修改上一個yield表示式值
程式會從第二個yield的下一行開始執行
執行到下一個yield停止,並記錄位置,返回結果。
"""
print(ty.send("hello"))  # 9
print(next(ty))  # 3
print(next(ty))  # StopIteration

注意:第一次執行要麼next(ty)要麼ty.send(None),不能使用ty.send('xxxxx'),否則會報錯的。

4、使用yield實現斐波那契數列

"""
數學中有個著名的斐波那契數列(Fibonacci),
數列中第⼀個數0,第⼆個數1,其後的每⼀個數都可由前兩個數相加得到:
如下:
0,    1,    1,   2,    3,    5,   8,    13,    21,   34,    ...

現在我們想要通過for...in...迴圈來遍歷迭代斐波那契數列中的前n個數。
那麼這個斐波那契數列我們就可以⽤生成來實現,
每次迭代都通過數學計算來⽣成下⼀個數。
"""
from collections.abc import Iterable, Iterator


class FibGenerator(object):
    """
        fib數列生成器
    """

    # 初始化方法
    def __init__(self):
        # 斐波拉契數列中的前兩個數
        self.num1 = 0
        self.num2 = 1

        # 用來記錄迭代次數(計數器)
        self.i = 0

    def gen(self, count):
        # 用來儲存迭代的總次數
        self.count = count

        # 判斷是否迭代結束,如果沒有到達迭代次數,則返回資料
        # self.count 需要迭代的次數
        # self.i已迭代次數
        while self.i < self.count:
            yield self.num2
            # 計算num1, num2的值,方便下次迭代返回
            # 這裡運用的是序列的封包與解包,不會的可以看我以前的文章(元組)
            self.num1, self.num2 = self.num2, self.num1 + self.num2

            # 執行一次next方法,計數器+1
            self.i = self.i + 1


# 建立一個物件
fibGen = FibGenerator()

# 呼叫生成器函式得到一個生成器
fg = fibGen.gen(15)

# fibIter物件是一個迭代器
print(isinstance(fg, Iterable))  # True
print(isinstance(fg, Iterator))  # True

# next方法方式獲取資料
# print(next(fg))  # 1
# print(next(fg))  # 1
# print(next(fg))  # 2
# print(next(fg))  # 3
# print(next(fg))  # 5
# print(next(fg))  # 8


# 遍歷生成器,可執行
for li in fg:
    print(li)

5、總結

  • 生成器generator就是迭代器iterator的一種,以更優雅的方式實現的iterator,而且完全可以像使用iterator一樣使用generator
  • 當然除了定義,定義一個iterator,你需要分別實現__iter__()方法方法和__next__()方法。但generator只需要一個yield關鍵字就可以。
  • Python生成器主要目的就是為了讓你的程式碼更省資源,更高效!

參考:

相關文章