返回文章列表

Python 迭代器模式與協程應用

本文探討 Python 中的迭代器模式和協程的應用,包含生成器的使用、迭代器介面、序列物件的迭代、協程的定義和使用方法,以及如何使用 `yield from` 簡化協程呼叫。文章從簡化迭代邏輯開始,逐步深入講解迭代器和協程的概念,並搭配程式碼範例說明,幫助讀者理解如何在 Python 中有效地使用迭代器和協程。

Python 程式設計

Python 的迭代器模式提供簡潔的迭代邏輯抽象化機制,讓客戶端程式碼無需關注底層實作細節。生成器函式能產生迭代器,有效節省記憶體並提升程式碼可讀性。迭代器模式透過 __iter____next__ 魔法方法定義迭代行為,序列物件則可透過 __getitem____len__ 方法實作迭代。協程作為特殊的生成器,能透過 yield 陳述式暫停和還原執行,並使用 send() 方法接收值,實作更靈活的控制流程。yield from 語法簡化了協程呼叫,能有效地收集子生成器傳回的值,提升程式碼的簡潔性和可維護性。

Python 中的迭代器模式

在深入瞭解 Python 中的迭代之前,我們先來探討如何利用生成器(generators)來簡化迭代邏輯。生成器不僅可以用於節省記憶體,還可以用來建立抽象化的迭代邏輯。

簡化迭代邏輯

利用生成器,我們可以將迭代邏輯抽象化,使得客戶端程式碼無需瞭解底層的實作細節。這種抽象化使得程式碼更加簡潔、易讀。

def _iterate_array2d(array):
    for i, row in enumerate(array):
        for j, cell in enumerate(row):
            yield (i, j), cell

def find_value(array, desired_value):
    try:
        coord = next(
            coord
            for (coord, cell) in _iterate_array2d(array)
            if cell == desired_value
        )
    except StopIteration as e:
        raise ValueError(f"{desired_value} not found") from e
    logger.info("value %r found at [%i, %i]", desired_value, *coord)
    return coord

內容解密:

  • _iterate_array2d 是一個生成器函式,用於迭代二維陣列中的每個元素,並傳回元素的坐標和值。
  • find_value 函式利用 _iterate_array2d 生成器來查詢陣列中特定值的坐標。

Python 中的迭代器模式

Python 中的迭代器模式(iterator pattern)是一種設計模式,允許物件以序列方式遍歷其元素。迭代器模式使得客戶端程式碼可以以統一的方式遍歷不同的資料結構。

迭代器介面

在 Python 中,迭代器介面由兩個魔法方法組成:__iter____next____iter__ 方法傳回一個迭代器物件,而 __next__ 方法傳回下一個元素。

概念魔法方法說明
可迭代物件(Iterable)__iter__支援迭代,可以在 for 迴圈中使用
迭代器(Iterator)__next__定義產生值的邏輯,當迭代結束時丟擲 StopIteration 例外
class SequenceIterator:
    def __init__(self, start=0, step=1):
        self.current = start
        self.step = step

    def __next__(self):
        value = self.current
        self.current += self.step
        return value

# 使用範例
si = SequenceIterator(1, 2)
print(next(si))  # 輸出:1
print(next(si))  # 輸出:3
print(next(si))  # 輸出:5

內容解密:

  • SequenceIterator 類別實作了 __next__ 方法,但沒有實作 __iter__ 方法,因此它是一個迭代器,但不是可迭代物件。
  • 使用 next() 函式可以取得下一個元素,但無法在 for 迴圈中使用。

序列物件作為可迭代物件

在 Python 中,如果一個物件實作了 __getitem____len__ 方法,那麼它也可以被視為可迭代物件。

class MappedRange:
    def __init__(self, transformation, start, end):
        self._transformation = transformation
        self._wrapped = range(start, end)

    def __getitem__(self, index):
        value = self._wrapped.__getitem__(index)
        result = self._transformation(value)
        logger.info("Index %d: %s", index, result)
        return result

    def __len__(self):
        return len(self._wrapped)

內容解密:

  • MappedRange 類別實作了 __getitem____len__ 方法,因此它可以被視為可迭代物件。
  • __getitem__ 方法中,對輸入的值進行轉換,並記錄日誌。

Python 中的迭代器與協程

在 Python 中,迭代器(Iterators)與協程(Coroutines)是兩個重要的概念。迭代器允許我們遍歷容器中的元素,而協程則允許我們暫停和還原函式的執行。

迭代器

迭代器是一個物件,它實作了 __iter__()__next__() 方法。當我們使用 for 迴圈遍歷一個容器時,Python 會自動呼叫迭代器的 __next__() 方法來取得下一個元素。

自定義迭代器

我們可以透過實作 __iter__()__next__() 方法來建立自己的迭代器。例如:

class MappedRange:
    def __init__(self, func, start, end):
        self.func = func
        self.start = start
        self.end = end

    def __getitem__(self, index):
        print(f"Index {index}: {self.func(self.start + index)}")
        return self.func(self.start + index)

    def __len__(self):
        return self.end - self.start

mr = MappedRange(abs, -10, 5)
print(list(mr))

內容解密:

  • MappedRange 類別實作了 __getitem__() 方法,使其可以被 for 迴圈遍歷。
  • __getitem__() 方法根據索引傳回對應的值,並列印出索引和值。
  • list(mr) 會呼叫 __getitem__() 方法來取得元素,直到超出範圍。

協程

協程是一種特殊的函式,可以暫停和還原其執行。Python 中的協程是根據生成器(Generators)實作的。

生成器

生成器是一種特殊的迭代器,它實作了 __iter__()__next__() 方法。我們可以使用 yield 陳述式來建立生成器。

協程的方法

PEP-342 為生成器增加了三個方法:close()throw()send()。這些方法使生成器可以用作協程。

close()

close() 方法用於關閉生成器,並觸發 GeneratorExit 例外。如果生成器沒有處理這個例外,則會終止迭代。

def stream_db_records(db_handler):
    try:
        while True:
            yield db_handler.read_n_records(10)
    except GeneratorExit:
        db_handler.close()

streamer = stream_db_records(DBHandler("testdb"))
next(streamer)
next(streamer)
streamer.close()

內容解密:

  • stream_db_records 是一個生成器,它從資料函式庫中讀取記錄。
  • 當呼叫 close() 方法時,生成器會觸發 GeneratorExit 例外,並關閉資料函式庫連線。
throw(ex_type[, ex_value[, ex_traceback]])

throw() 方法用於向生成器丟擲一個例外。如果生成器處理了這個例外,則會繼續執行。

圖示解密:
  • 可迭代物件(Iterable)實作了 __iter__() 方法,傳回一個迭代器(Iterator)。
  • 迭代器實作了 __next__() 方法,傳回下一個元素。
  • 協程(Coroutine)根據生成器實作,並支援 close()throw()send() 方法。

協程(Coroutine)與生成器(Generator)的深入解析

在 Python 中,協程和生成器是兩個緊密相關但又不同的概念。它們都允許函式在執行的過程中暫停並在稍後還原,但它們的使用方式和目的各有不同。本篇文章將探討協程和生成器的基本原理、使用方法以及它們在實際開發中的應用。

生成器(Generator)

生成器是一種特殊的函式,它可以在執行的過程中暫停並傳回一個值。當再次呼叫生成器時,它會從上次暫停的地方繼續執行。生成器使用 yield 關鍵字來傳回一個值並暫停執行。

基本使用

以下是一個簡單的生成器示例:

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()
print(next(gen))  # 輸出:1
print(next(gen))  # 輸出:2
print(next(gen))  # 輸出:3

實際應用

生成器可以用於處理大量資料,例如讀取資料函式庫中的記錄:

def stream_data(db_handler):
    while True:
        try:
            yield db_handler.read_n_records(10)
        except CustomException as e:
            logger.info("controlled error %r, continuing", e)
        except Exception as e:
            logger.info("unhandled error %r, stopping", e)
            db_handler.close()
            break

這個生成器會不斷讀取資料函式庫中的記錄,直到發生異常或被關閉。

協程(Coroutine)

協程是一種特殊的生成器,它可以使用 send() 方法接收值並根據接收到的值進行不同的操作。協程使用 yield 關鍵字來接收值並傳回一個值。

基本使用

以下是一個簡單的協程示例:

def my_coroutine():
    receive = yield
    print(receive)

coro = my_coroutine()
next(coro)  # 必須先呼叫 next() 方法
coro.send("Hello, World!")  # 輸出:Hello, World!

實際應用

協程可以用於動態調整生成器的行為,例如調整讀取資料函式庫記錄的數量:

def stream_db_records(db_handler):
    retrieved_data = None
    page_size = 10
    try:
        while True:
            page_size = (yield retrieved_data) or page_size
            retrieved_data = db_handler.read_n_records(page_size)
    except GeneratorExit:
        db_handler.close()

這個協程可以根據接收到的值動態調整讀取資料函式庫記錄的數量。

最佳實踐

  • 使用生成器處理大量資料,例如讀取資料函式庫中的記錄。
  • 使用協程動態調整生成器的行為,例如調整讀取資料函式庫記錄的數量。
  • 在使用協程時,必須先呼叫 next() 方法以啟動協程。

程式碼範例解密:

def stream_db_records(db_handler):
    retrieved_data = None
    page_size = 10
    try:
        while True:
            page_size = (yield retrieved_data) or page_size
            retrieved_data = db_handler.read_n_records(page_size)
    except GeneratorExit:
        db_handler.close()

這個程式碼範例展示瞭如何使用協程動態調整讀取資料函式庫記錄的數量。其中,yield 關鍵字用於接收值並傳回一個值,send() 方法用於傳送值給協程。這個範例還展示瞭如何使用 try-except 區塊來處理異常。

此範例中,page_size = (yield retrieved_data) or page_size 這行程式碼的作用是接收從 send() 方法傳送的值,並將其指定給 page_size。如果沒有收到值,則 page_size 保持原有的值。

這個範例還展示瞭如何使用 GeneratorExit 異常來關閉資料函式庫連線。

總的來說,這個範例展示瞭如何使用協程來動態調整生成器的行為,並處理異常。

更進階的協程應用

在前面的章節中,我們已經對協程有了更深入的瞭解,並且能夠建立簡單的協程來處理一些小任務。可以說,這些協程實際上只是更進階的生成器(generators),但如果我們想要開始支援更複雜的場景,通常需要設計一個能夠同時處理多個協程的架構,這就需要更多的功能。

同時處理多個協程的新問題

當處理多個協程時,我們會遇到新的問題。隨著應用程式的控制流程變得更加複雜,我們希望能夠在堆積疊中上下傳遞值(以及例外),能夠捕捉到在任何層級呼叫的子協程中的值,最終,排程多個協程以實作共同的目標。

擴充套件生成器以支援更複雜的協程

為了簡化這些問題,生成器必須再次被擴充套件。這就是 PEP-380 所做的,透過改變生成器的語義,使它們能夠傳回值,並引入了新的 yield from 結構。

在協程中傳回值

在本章的開頭介紹了迭代是一種機制,它會在可迭代物件上多次呼叫 next(),直到引發 StopIteration 例外。

到目前為止,我們一直在探索生成器的迭代性質——一次產生一個值,一般來說,我們只關心在 for 迴圈的每一步中產生的每個值。這是一種非常合乎邏輯的思考生成器的方式,但協程有不同的想法;儘管它們在技術上是生成器,但它們並不是以迭代的思想來構思的,而是以掛起程式碼執行直到稍後還原的思想。

這是一個有趣的挑戰;當我們設計一個協程時,我們通常更關心的是掛起狀態,而不是迭代(迭代一個協程將是一個奇怪的情況)。挑戰在於很容易將兩者混淆。這是由於技術實作的細節;在 Python 中對協程的支援是建立在生成器的基礎上的。

簡單生成器傳回值的例子

def generator():
    yield 1
    yield 2
    return 3

value = generator()
print(next(value))  # 輸出:1
print(next(value))  # 輸出:2

try:
    next(value)
except StopIteration as e:
    print(f">>>>>> returned value: {e.value}")  # 輸出:>>>>>> returned value: 3

內容解密:

  1. generator函式定義:這是一個生成器函式,使用yield陳述式產生值,並在最後使用return傳回一個最終值。
  2. yield的作用:每次呼叫next()時,生成器從上次yield的位置繼續執行,直到遇到下一個yield
  3. return陳述式的作用:在生成器中,return陳述式用於傳回一個最終值,並引發StopIteration例外。
  4. StopIteration例外:當生成器結束時,會引發此例外,並且傳回值儲存在例外的value屬性中。

使用 yield from 簡化協程呼叫

在 PEP-380 之後,協程可以傳回值,但仍需要適當的語法支援,以便更方便地取得傳回值。yield from 語法的一個主要功能就是能夠收集子生成器傳回的值。

yield from 的基本用法

def sub_generator():
    yield 1
    yield 2
    return 3

def main_generator():
    value = yield from sub_generator()
    print(f"Returned value: {value}")

for v in main_generator():
    print(v)

內容解密:

  1. sub_generator函式:這是一個子生成器,產生兩個值並傳回第三個值。
  2. main_generator函式:使用yield from呼叫sub_generator(),並取得其傳回值。
  3. yield from的作用:將sub_generator產生的值直接傳遞給外部迭代,同時捕捉其傳回值並賦給value變數。
  4. 輸出結果:首先輸出1和2,然後輸出sub_generator傳回的值3。