翻譯:《實用的Python程式設計》06_02_Customizing_iteration

codists發表於2021-03-16

目錄 | 上一節 (6.1 迭代協議) | 下一節 (6.3 生產者/消費者)

6.2 自定義迭代

本節探究如何使用生成器函式自定義迭代。

問題

假設你想要自定義迭代模式。

例如:倒數:

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

有一個j簡單方法可以做到這一點。

生成器

生成器(generator)是定義了迭代的函式:

def countdown(n):
    while n > 0:
        yield n
        n -= 1

示例:

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

任何使用了 yield 語句的函式稱為生成器。

生成器函式的行為不同於普通於普通函式。呼叫生成器函式會建立一個生成器物件(generator object),而不是立即執行函式:

def countdown(n):
    # Added a print statement
    print('Counting down from', n)
    while n > 0:
        yield n
        n -= 1
>>> x = countdown(10)
# There is NO PRINT STATEMENT
>>> x
# x is a generator object
<generator object at 0x58490>
>>>

生成器函式只在 __next__() 方法被呼叫時才執行:

>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.__next__()
Counting down from 10
10
>>>

yield 生成一個值,但是掛起(suspend)函式執行。生成器函式會在下次呼叫 __next__() 方法時恢復(resume),

>>> x.__next__()
9
>>> x.__next__()
8

當生成器返回最後一個值後,再次迭代將會觸發一個錯誤(譯註:StopIteration)。

>>> x.__next__()
1
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in ? StopIteration
>>>

觀察:生成器函式實現的協議與 for 語句在列表、元組、字典、檔案上使用的底層協議相同。

練習

練習 6.4:一個簡單的生成器

如果想要自定義迭代,那麼你應該始終考慮生成器函式。生成器函式非常容易編寫——建立一個函式,執行所需的迭代邏輯,並使用 yield 傳送一個值。

例如,建立一個在檔案各行中查詢匹配子串的生成器:

>>> def filematch(filename, substr):
        with open(filename, 'r') as f:
            for line in f:
                if substr in line:
                    yield line

>>> for line in open('Data/portfolio.csv'):
        print(line, end='')

name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
>>> for line in filematch('Data/portfolio.csv', 'IBM'):
        print(line, end='')

"IBM",50,91.10
"IBM",100,70.44
>>>

這是一種有趣的思想——你可以在函式中隱藏自定義的處理過程,並將該函式應用於 for 迴圈。下一個例子探究一種更不尋常的情況。

練習 6.5:監視流資料來源

生成器可應用於監視實時資料來源(如:日誌檔案,股票市場訊息)。本部分,我們將對“使用生成器監視實時資料來源”這一思想進行探索。首先,請嚴格遵循以下說明。

Data/stocksim.py 用來模仿股市資料,將實時資料不斷地寫入到 Data/stocklog.csv 檔案。請開啟一個獨立的命令列視窗,進入到 Data/ 目錄,然後執行 stocksim.py 程式:

bash % python3 stocksim.py

如果你使用的是 Windows 系統,那麼請找到 stocksim.py 檔案,然後雙擊該檔案執行。然後,讓我們先把這個程式放到一邊(讓它一直在那執行),開啟另外一個命令列視窗,檢視正在被模擬程式(譯註:stocksim.py)寫入資料的 Data/stocklog.csv 檔案(譯註:如果使用的是 Linux 系統,那麼可以進入到 Data 目錄下,然後使用 tail -f stocklog.csv 命令檢視)。你應該可以看到每隔幾秒新的文字行被新增到 Data/stocklog.csv 檔案中。同樣,讓程式在後臺執行——該程式會執行幾個小時(對此不用擔心)。

stocksim.py 程式執行後,讓我們編寫一個程式來開啟 Data/stocklog.csv 檔案、移動到檔案末尾、並檢視新的輸出。請在 Work 目錄下建立 follow.py 檔案,並把以下程式碼放入其中:

# follow.py
import os
import time

f = open('Data/stocklog.csv')
f.seek(0, os.SEEK_END)   # Move file pointer 0 bytes from end of file

while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   # Sleep briefly and retry
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

執行 follow.py 程式,你將會看到實時的股票報價(stock ticker)。 follow.py 裡的程式碼類似於 Unix 系統檢視日誌檔案的 tail -f 命令。

注意事項:在本示例中,readline() 方法的使用與通常從檔案中讀取行的方式稍微有點不同(通常使用 for 迴圈)。在這種情況下,我們使用 readline() 來重複探測檔案的末尾,以檢視是否新增了新的資料(readline() 方法返回新的資料或者空字串)。

練習 6.6:使用生成器生成資料

檢視練習 6.5 中程式碼你會發現,程式碼的第一部分產生了幾行資料,而 while 迴圈末尾的語句消費資料。生成器的一個主要特性是你可以將生成資料的程式碼移動到可重用的函式中。

請修改練習 6.5 的程式碼,以便通過生成器函式 follow(filename) 執行檔案讀取。請實現更改以便下面的程式碼能夠工作:

>>> for line in follow('Data/stocklog.csv'):
          print(line, end='')

... Should see lines of output produced here ...

請修改股票報價程式碼,使程式碼看起來像下面這樣:

if __name__ == '__main__':
    for line in follow('Data/stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

練習 6.7:檢視股票投資組合

請修改 follow.py 程式,以便程式能夠檢視股票資料流,並列印股票投資組合中的那些股票的資訊。示例:

if __name__ == '__main__':
    import report

    portfolio = report.read_portfolio('Data/portfolio.csv')

    for line in follow('Data/stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if name in portfolio:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

注意事項:要想這段程式碼能夠執行, Portfolio 類必須支援 in 運算子。請參閱 練習 6.3 ,確保 Portfolio 類實現了 __contains__() 運算子。

討論

在這裡,你將一個有趣的迭代模式(在檔案末尾讀取行)移動到函式中。follow()函式現在是可以在任何程式中使用的完全通用的實用程式。例如,你可以使用 follow() 函式檢視伺服器日誌、除錯日誌、其它類似的資料來源。

目錄 | 上一節 (6.1 迭代協議) | 下一節 (6.3 生產者/消費者)

注:完整翻譯見 https://github.com/codists/practical-python-zh

相關文章