翻譯:《實用的Python編程》06_02_Customizing_iteration

目錄 | 上一節 (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 生產者/消費者)

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