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

codists發表於2021-03-15

目錄 | 上一節 (5.2 封裝) | 下一節 (6.2 自定義迭代)

6.1 迭代協議

本節將探究迭代的底層過程。

迭代無處不在

許多物件都支援迭代:

a = 'hello'
for c in a: # Loop over characters in a
    ...

b = { 'name': 'Dave', 'password':'foo'}
for k in b: # Loop over keys in dictionary
    ...

c = [1,2,3,4]
for i in c: # Loop over items in a list/tuple
    ...

f = open('foo.txt')
for x in f: # Loop over lines in a file
    ...

迭代:協議

考慮以下 for 語句:

for x in obj:
    # statements

for 語句的背後發生了什麼?

_iter = obj.__iter__()        # Get iterator object
while True:
    try:
        x = _iter.__next__()  # Get next item
        # statements ...
    except StopIteration:     # No more items
        break

所有可應用於 for-loop 的物件都實現了上述底層迭代協議。

示例:手動迭代一個列表。

>>> x = [1,2,3]
>>> it = x.__iter__()
>>> it
<listiterator object at 0x590b0>
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in ? StopIteration
>>>

支援迭代

如果想要將迭代新增到自己的物件中,那麼瞭解迭代非常有用。例如:自定義容器。

class Portfolio:
    def __init__(self):
        self.holdings = []

    def __iter__(self):
        return self.holdings.__iter__()
    ...

port = Portfolio()
for s in port:
    ...

練習

練習 6.1:迭代演示

建立以下列表:

a = [1,9,4,25,16]

請手動迭代該列表:先呼叫 __iter__() 方法獲取一個迭代器,然後呼叫 __next__() 方法獲取下一個元素。

>>> i = a.__iter__()
>>> i
<listiterator object at 0x64c10>
>>> i.__next__()
1
>>> i.__next__()
9
>>> i.__next__()
4
>>> i.__next__()
25
>>> i.__next__()
16
>>> i.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

內建函式 next() 是呼叫迭代器的 __next__() 方法的快捷方式。嘗試在一個檔案物件上使用 next() 方法:

>>> f = open('Data/portfolio.csv')
>>> f.__iter__()    # Note: This returns the file itself
<_io.TextIOWrapper name='Data/portfolio.csv' mode='r' encoding='UTF-8'>
>>> next(f)
'name,shares,price\n'
>>> next(f)
'"AA",100,32.20\n'
>>> next(f)
'"IBM",50,91.10\n'
>>>

持續呼叫 next(f),直到檔案末尾。觀察會發生什麼。

練習 6.2:支援迭代

有時候,你可能想要使自己的類物件支援迭代——尤其是你的類物件封裝了已有的列表或者其它可迭代物件時。請在新的 portfolio.py 檔案中定義如下類:

# portfolio.py

class Portfolio:

    def __init__(self, holdings):
        self._holdings = holdings

    @property
    def total_cost(self):
        return sum([s.cost for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

Portfolio 類封裝了一個列表,同時擁有一些方法,如: total_cost property。請修改 report.py 檔案中的 read_portfolio() 函式,以便 read_portfolio() 函式能夠像下面這樣建立 Portfolio 類的例項:

# report.py
...

import fileparse
from stock import Stock
from portfolio import Portfolio

def read_portfolio(filename):
    '''
    Read a stock portfolio file into a list of dictionaries with keys
    name, shares, and price.
    '''
    with open(filename) as file:
        portdicts = fileparse.parse_csv(file,
                                        select=['name','shares','price'],
                                        types=[str,int,float])

    portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
    return Portfolio(portfolio)
...

接著執行 report.py 程式。你會發現程式執行失敗,原因很明顯,因為 Portfolio 的例項不是可迭代物件。

>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
... crashes ...

可以通過修改 Portfolio 類,使 Portfolio 類支援迭代來解決此問題:

class Portfolio:

    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    @property
    def total_cost(self):
        return sum([s.shares*s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

修改完成後, report.py 程式應該能夠再次正常執行。同時,請修改 pcost.py 程式,以便能夠像下面這樣使用新的 Portfolio 物件:

# pcost.py

import report

def portfolio_cost(filename):
    '''
    Computes the total cost (shares*price) of a portfolio file
    '''
    portfolio = report.read_portfolio(filename)
    return portfolio.total_cost
...

pcost.py 程式進行測試並確保其能正常工作:

>>> import pcost
>>> pcost.portfolio_cost('Data/portfolio.csv')
44671.15
>>>

練習 6.3:建立一個更合適的容器

通常,我們建立一個容器類時,不僅希望該類能夠迭代,同時也希望該類能夠具有一些其它用途。請修改 Portfolio 類,使其具有以下這些特殊方法:

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

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

    def __getitem__(self, index):
        return self._holdings[index]

    def __contains__(self, name):
        return any([s.name == name for s in self._holdings])

    @property
    def total_cost(self):
        return sum([s.shares*s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

現在,使用 Portfolio 類進行一些實驗:

>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> len(portfolio)
7
>>> portfolio[0]
Stock('AA', 100, 32.2)
>>> portfolio[1]
Stock('IBM', 50, 91.1)
>>> portfolio[0:3]
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44)]
>>> 'IBM' in portfolio
True
>>> 'AAPL' in portfolio
False
>>>

有關上述程式碼的一個重要發現——通常,如果一段程式碼和 Python 的其它程式碼"類似(speaks the common vocabulary of how other parts of Python normally work)",那麼該程式碼被認為是 “Pythonic” 的。同理,對於容器物件,其重要組成部分應該包括:支援迭代、可以進行索引、對所包含的元素進行判斷,以及其它操作等等。

目錄 | 上一節 (5.2 封裝) | 下一節 (6.2 自定義迭代)

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

相關文章