目錄 | 上一節 (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” 的。同理,對於容器物件,其重要組成部分應該包括:支援迭代、可以進行索引、對所包含的元素進行判斷,以及其它操作等等。