Python 的 yield from 語法簡化了生成器的實作,並強化了與子生成器的互動。它不僅能串接多個生成器,還能有效地捕捉子生成器的傳回值,並實作雙向資料傳遞和例外處理。隨著 Python 的發展,asyncawait 語法出現,提供了更清晰的非同步程式設計模型。然而,理解 yield from 的運作機制仍然有助於掌握 Python 非同步程式設計的底層邏輯。此外,文章也介紹了非同步迭代器和產生器的使用,以及如何利用 anext 函式推進非同步迭代器。最後,文章也強調了單元測試的重要性,並示範瞭如何使用 unittestpytest 框架編寫單元測試,確保程式碼的品質和可維護性。

yield from 語法解析與應用

在 Python 中,yield from 語法是一種強大的工具,用於簡化生成器(generator)的實作並提升其功能。本文將探討 yield from 的基本概念、捕捉子生成器傳回值、以及如何與子生成器進行資料傳遞。

基本概念與簡化生成器實作

yield from 語法主要用於簡化巢狀生成器的實作。一個典型的例子是實作類別似於標準函式庫中的 itertools.chain() 函式。該函式允許傳入多個可迭代物件,並將它們合併成一個單一的迭代流。

原始實作

def chain(*iterables):
    for it in iterables:
        for value in it:
            yield value

使用 yield from 簡化

def chain(*iterables):
    for it in iterables:
        yield from it

兩種實作方式具有相同的行為,能夠正確地將多個可迭代物件合併成一個迭代流。

內容解密:

  1. yield from it 直接將 it 中的值產生出來,無需巢狀迴圈。
  2. 這種語法簡化了程式碼,使其更易讀且更具 Python 風格。

捕捉子生成器的傳回值

yield from 不僅能簡化生成器的實作,還能捕捉子生成器的傳回值。以下是一個範例,展示瞭如何使用 yield from 捕捉子生成器的傳回值。

範例程式碼

def sequence(name, start, end):
    logger.info("%s started at %i", name, start)
    yield from range(start, end)
    logger.info("%s finished at %i", name, end)
    return end

def main():
    step1 = yield from sequence("first", 0, 5)
    step2 = yield from sequence("second", step1, 10)
    return step1 + step2

內容解密:

  1. sequence 生成器函式使用 yield from range(start, end) 產生一系列數值,並在結束時傳回 end 值。
  2. main 函式中,yield from sequence(...) 不僅產生 sequence 中的值,還捕捉其傳回值並指定給 step1step2
  3. 最終,main 傳回 step1step2 的總和。

與子生成器進行資料傳遞

除了捕捉傳回值,yield from 還允許向子生成器傳送資料或丟擲異常。以下範例展示瞭如何修改 sequence 生成器以接收資料和處理異常。

修改後的 sequence 生成器

def sequence(name, start, end):
    value = start
    logger.info("%s started at %i", name, value)
    while value < end:
        try:
            received = yield value
            logger.info("%s received %r", name, received)
            value += 1
        except CustomException as e:
            logger.info("%s is handling %s", name, e)
            received = yield "OK"
    return end

內容解密:

  1. sequence 生成器現在能夠接收透過 send() 傳送的值,並在日誌中記錄。
  2. 當透過 throw() 丟擲 CustomException 時,生成器能夠捕捉並處理該異常。
  3. 在處理異常後,生成器產生一個 "OK" 值表示處理成功。

非同步程式設計的演進與 Python 中的實作

在 Python 中,非同步程式設計的概念隨著時間不斷演進。早期,開發者利用生成器(generators)和 yield from 語法來實作非同步操作。這些技術讓開發者能夠建立並管理多個協程(coroutines),並在適當的時候切換它們,以達到非同步處理的效果。

早期實作:使用生成器和 yield from

在 Python 3.5 之前,開發者使用生成器來實作協程,並利用 yield from 語法來傳遞值或異常給子協程。這種方法雖然有效,但存在一些問題。由於生成器和協程在技術上是相同的,這導致開發者可能會將這兩種不同的物件混淆,進而在執行時才發現錯誤。

def main():
    step1 = yield from first_coroutine()
    step2 = yield from second_coroutine(step1)
    return step2

#### 內容解密:
此段程式碼展示瞭如何使用 `yield from` 將控制流交給子協程並接收它們的傳回值首先,`main` 協程呼叫 `first_coroutine` 並等待其完成然後將結果傳遞給 `second_coroutine`。這裡的關鍵在於 `yield from` 語法允許 `main` 協程暫停執行直到子協程完成

### 新語法:`async def` 和 `await`

為瞭解決上述問題Python 3.5 引入了新的語法:`async def` 用於定義協程,`await` 用於取代 `yield from`。這種新語法提高了程式碼的可讀性和安全性因為它明確區分了協程和生成器

```python
async def main():
    step1 = await first_coroutine()
    step2 = await second_coroutine(step1)
    return step2

#### 內容解密:
這段程式碼展示瞭如何使用 `async def` 定義協程並利用 `await` 語法等待子協程的完成`yield from` 類別似,`await` 會暫停當前協程的執行直到被等待的協程完成這種語法更加直觀也更不容易出錯

### 事件迴圈與非同步程式設計

Python 中的非同步程式設計依賴於事件迴圈event loop來管理協程的執行事件迴圈負責排程協程並在適當的時候切換它們開發者可以利用 `asyncio` 函式庫來建立和管理事件迴圈

```python
import asyncio

async def main():
    # 非同步操作
    await asyncio.sleep(1)
    print("非同步操作完成")

asyncio.run(main())

#### 內容解密:
此範例展示瞭如何使用 `asyncio` 函式庫來執行非同步操作。`asyncio.run(main())` 用於啟動事件迴圈並執行 `main` 協程`main` 協程中,`await asyncio.sleep(1)` 暫停執行 1模擬非同步操作

## 深入理解Python中的非同步程式設計

Python中的`async def``await`語法為開發者提供了一種與事件迴圈event loop互動的API預設情況下這個事件迴圈通常是標準函式庫中的`asyncio`,但任何符合API規範的事件迴圈系統都可以使用這意味著開發者可以使用像`uvloop``trio`這樣的函式庫並且程式碼仍然能夠正常運作

### 神奇的非同步方法

在之前的章節中我們已經瞭解到利用Python的神奇方法magic methods可以使我們建立的抽象概念自然地融入語言的語法中從而實作更好更簡潔的程式碼但是當我們需要在這些方法中呼叫協程coroutine情況就變得不同了如果一個函式中需要使用`await`,那麼這個函式本身就必須被定義為一個協程使用`async def`),否則就會出現語法錯誤

為瞭解決這個問題Python引入了新的語法和新的神奇方法這些新的神奇方法與原有的語法是類別似的

#### 非同步上下文管理器

非同步上下文管理器的概念很簡單如果我們需要使用一個上下文管理器但是需要對它呼叫一個協程那麼我們就不能使用普通的`__enter__``__exit__`方法因為它們是被定義為普通的函式因此我們需要使用新的`__aenter__``__aexit__`協程方法並且我們需要使用`async with`語法來呼叫它

```python
@contextlib.asynccontextmanager
async def db_management():
    try:
        await stop_database()
        yield
    finally:
        await start_database()

async def run_db_backup():
    async with db_management(), metrics_logger():
        print("Performing DB backup...")

非同步迭代

與普通的迭代器物件類別似,我們也可以建立非同步迭代器。非同步迭代器支援使用Python內建的async for迴圈進行迭代。如果我們想要建立一個迭代器來抽象化從外部來源讀取資料的方式,但是提取資料的部分是一個協程,那麼我們就需要使用__anext__協程。

class AsyncIterator:
    def __init__(self, data):
        self.data = data

    async def __aiter__(self):
        return self

    async def __anext__(self):
        if not self.data:
            raise StopAsyncIteration
        item = self.data.pop(0)
        # 假設這裡有一個協程來處理item
        await process_item(item)
        return item

async def main():
    data = [1, 2, 3]
    async for item in AsyncIterator(data):
        print(item)

程式碼解析

在上述範例中,我們定義了一個非同步迭代器AsyncIterator,它實作了__aiter____anext__方法。在__anext__方法中,我們模擬了一個協程來處理每個專案。當資料列表為空時,我們引發了StopAsyncIteration異常來停止迭代。

main函式中,我們使用async for迴圈來迭代AsyncIterator例項,並列印出每個專案。

非同步迭代器與產生器在Python中的應用

在Python的非同步程式設計中,迭代器和產生器是兩個非常重要的概念。它們讓我們能夠以非同步的方式處理資料流,使得程式設計更加靈活和高效。

非同步迭代器

非同步迭代器是一種特殊的物件,它允許我們在非同步程式中使用async for迴圈來迭代資料。下面是一個簡單的非同步迭代器範例:

import asyncio
import random

async def coroutine():
    await asyncio.sleep(0.1)
    return random.randint(1, 10000)

class RecordStreamer:
    def __init__(self, max_rows=100) -> None:
        self._current_row = 0
        self._max_rows = max_rows

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self._current_row < self._max_rows:
            row = (self._current_row, await coroutine())
            self._current_row += 1
            return row
        raise StopAsyncIteration

內容解密:

  1. __aiter__ 方法傳回迭代器物件本身,這是實作非同步迭代器的必要步驟。
  2. __anext__ 方法是核心,它負責傳回下一個值。如果沒有更多的值,則丟出 StopAsyncIteration 例外。
  3. __anext__ 中,我們使用 await coroutine() 來非同步地取得資料。

使用非同步迭代器:

async for row in RecordStreamer(10):
    print(row)

推進非同步迭代器

由於非同步迭代器不支援 next() 函式,我們需要使用不同的方式來推進它。

NOT_SET = object()

async def anext(async_generator_expression, default=NOT_SET):
    try:
        return await async_generator_expression.__anext__()
    except StopAsyncIteration:
        if default is NOT_SET:
            raise
        return default

內容解密:

  1. anext 函式模擬了 next() 的行為,但適用於非同步迭代器。
  2. 它嘗試取得下一個值,如果沒有更多的值,則根據是否有預設值來決定是否丟出例外。

使用範例:

$ python -m asyncio
>>> streamer = RecordStreamer(10)
>>> await anext(streamer)
(0, 5017)
>>> await anext(streamer)
(1, 5257)

非同步產生器

Python 3.6 之後引入了非同步產生器,讓我們能夠以更簡潔的方式實作非同步迭代。

async def record_streamer(max_rows):
    current_row = 0
    while current_row < max_rows:
        row = (current_row, await coroutine())
        current_row += 1
        yield row

內容解密:

  1. 使用 async def 定義非同步產生器。
  2. 在迴圈內,使用 yield 來產生值。
  3. 非同步產生器比非同步迭代器更簡潔,易於理解和維護。

單元測試與重構:軟體開發的根本

單元測試是軟體開發中不可或缺的一環,它們對於確保程式碼的品質和可維護性至關重要。在本章中,我們將探討單元測試的重要性、運作原理,以及如何利用單元測試來提升程式碼的品質。

設計原則與單元測試

在探討單元測試的實踐之前,我們需要先了解其背後的設計原則。單元測試不僅是驗證程式碼正確性的工具,更是軟體工程設計原則的體現。

單元測試的定義

單元測試是負責驗證程式碼其他部分的程式碼。它們通常被視為軟體開發的核心組成部分,而非次要元素。一個單元測試會匯入包含業務邏輯的程式碼,並執行其邏輯,以斷言多種場景,確保特定的條件得到滿足。

單元測試的特性

單元測試必須具備以下特性:

  • 隔離性:單元測試應完全獨立於任何外部代理,只關注業務邏輯。這意味著它們不應連線到資料函式庫、不應執行HTTP請求等。隔離性還意味著測試之間是獨立的,它們可以按任意順序執行,而不依賴於任何先前的狀態。

實踐單元測試

在瞭解了單元測試的概念和特性之後,接下來我們將探討如何在實踐中應用單元測試。

使用unittest模組

Python的標準函式庫中提供了unittest模組,用於編寫和執行單元測試。下面是一個簡單的例子:

import unittest

def add(a, b):
    return a + b

class TestAddFunction(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(1, 2), 3)
        self.assertEqual(add(-1, 1), 0)
        self.assertEqual(add(-1, -1), -2)

if __name__ == '__main__':
    unittest.main()

內容解密:

在這個例子中,我們定義了一個簡單的add函式,並建立了一個TestAddFunction類別來測試它。test_add方法包含了多個斷言,用於驗證add函式在不同輸入下的行為是否正確。

使用pytest框架

除了unittest模組外,pytest是一個流行的外部套件,用於編寫和執行單元測試。它提供了更多的功能和彈性,使得編寫測試更加方便。

安裝pytest

pip install pytest

編寫pytest測試

def add(a, b):
    return a + b

def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0
    assert add(-1, -1) == -2

內容解密:

在這個例子中,我們使用pytest框架編寫了一個測試函式test_addpytest自動發現並執行以test_開頭的函式,簡化了測試的編寫和執行過程。