目录 | 上一节 (5.2 封装) | 下一节 (6.2 自定义迭代)

6.1 迭代协议

本节将探究迭代的底层过程。

迭代无处不在

许多对象都支持迭代:

  1. a = 'hello'
  2. for c in a: # Loop over characters in a
  3. ...
  4. b = { 'name': 'Dave', 'password':'foo'}
  5. for k in b: # Loop over keys in dictionary
  6. ...
  7. c = [1,2,3,4]
  8. for i in c: # Loop over items in a list/tuple
  9. ...
  10. f = open('foo.txt')
  11. for x in f: # Loop over lines in a file
  12. ...

迭代:协议

考虑以下 for 语句:

  1. for x in obj:
  2. # statements

for 语句的背后发生了什么?

  1. _iter = obj.__iter__() # Get iterator object
  2. while True:
  3. try:
  4. x = _iter.__next__() # Get next item
  5. # statements ...
  6. except StopIteration: # No more items
  7. break

所有可应用于 for-loop 的对象都实现了上述底层迭代协议。

示例:手动迭代一个列表。

  1. >>> x = [1,2,3]
  2. >>> it = x.__iter__()
  3. >>> it
  4. <listiterator object at 0x590b0>
  5. >>> it.__next__()
  6. 1
  7. >>> it.__next__()
  8. 2
  9. >>> it.__next__()
  10. 3
  11. >>> it.__next__()
  12. Traceback (most recent call last):
  13. File "<stdin>", line 1, in ? StopIteration
  14. >>>

支持迭代

如果想要将迭代添加到自己的对象中,那么了解迭代非常有用。例如:自定义容器。

  1. class Portfolio:
  2. def __init__(self):
  3. self.holdings = []
  4. def __iter__(self):
  5. return self.holdings.__iter__()
  6. ...
  7. port = Portfolio()
  8. for s in port:
  9. ...

练习

练习 6.1:迭代演示

创建以下列表:

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

请手动迭代该列表:先调用 __iter__() 方法获取一个迭代器,然后调用 __next__() 方法获取下一个元素。

  1. >>> i = a.__iter__()
  2. >>> i
  3. <listiterator object at 0x64c10>
  4. >>> i.__next__()
  5. 1
  6. >>> i.__next__()
  7. 9
  8. >>> i.__next__()
  9. 4
  10. >>> i.__next__()
  11. 25
  12. >>> i.__next__()
  13. 16
  14. >>> i.__next__()
  15. Traceback (most recent call last):
  16. File "<stdin>", line 1, in <module>
  17. StopIteration
  18. >>>

内置函数 next() 是调用迭代器的 __next__() 方法的快捷方式。尝试在一个文件对象上使用 next() 方法:

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

持续调用 next(f),直到文件末尾。观察会发生什么。

练习 6.2:支持迭代

有时候,你可能想要使自己的类对象支持迭代——尤其是你的类对象封装了已有的列表或者其它可迭代对象时。请在新的 portfolio.py 文件中定义如下类:

  1. # portfolio.py
  2. class Portfolio:
  3. def __init__(self, holdings):
  4. self._holdings = holdings
  5. @property
  6. def total_cost(self):
  7. return sum([s.cost for s in self._holdings])
  8. def tabulate_shares(self):
  9. from collections import Counter
  10. total_shares = Counter()
  11. for s in self._holdings:
  12. total_shares[s.name] += s.shares
  13. return total_shares

Portfolio 类封装了一个列表,同时拥有一些方法,如: total_cost property。请修改 report.py 文件中的 read_portfolio() 函数,以便 read_portfolio() 函数能够像下面这样创建 Portfolio 类的实例:

  1. # report.py
  2. ...
  3. import fileparse
  4. from stock import Stock
  5. from portfolio import Portfolio
  6. def read_portfolio(filename):
  7. '''
  8. Read a stock portfolio file into a list of dictionaries with keys
  9. name, shares, and price.
  10. '''
  11. with open(filename) as file:
  12. portdicts = fileparse.parse_csv(file,
  13. select=['name','shares','price'],
  14. types=[str,int,float])
  15. portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
  16. return Portfolio(portfolio)
  17. ...

接着运行 report.py 程序。你会发现程序运行失败,原因很明显,因为 Portfolio 的实例不是可迭代对象。

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

可以通过修改 Portfolio 类,使 Portfolio 类支持迭代来解决此问题:

  1. class Portfolio:
  2. def __init__(self, holdings):
  3. self._holdings = holdings
  4. def __iter__(self):
  5. return self._holdings.__iter__()
  6. @property
  7. def total_cost(self):
  8. return sum([s.shares*s.price for s in self._holdings])
  9. def tabulate_shares(self):
  10. from collections import Counter
  11. total_shares = Counter()
  12. for s in self._holdings:
  13. total_shares[s.name] += s.shares
  14. return total_shares

修改完成后, report.py 程序应该能够再次正常运行。同时,请修改 pcost.py 程序,以便能够像下面这样使用新的 Portfolio 对象:

  1. # pcost.py
  2. import report
  3. def portfolio_cost(filename):
  4. '''
  5. Computes the total cost (shares*price) of a portfolio file
  6. '''
  7. portfolio = report.read_portfolio(filename)
  8. return portfolio.total_cost
  9. ...

pcost.py 程序进行测试并确保其能正常工作:

  1. >>> import pcost
  2. >>> pcost.portfolio_cost('Data/portfolio.csv')
  3. 44671.15
  4. >>>

练习 6.3:创建一个更合适的容器

通常,我们创建一个容器类时,不仅希望该类能够迭代,同时也希望该类能够具有一些其它用途。请修改 Portfolio 类,使其具有以下这些特殊方法:

  1. class Portfolio:
  2. def __init__(self, holdings):
  3. self._holdings = holdings
  4. def __iter__(self):
  5. return self._holdings.__iter__()
  6. def __len__(self):
  7. return len(self._holdings)
  8. def __getitem__(self, index):
  9. return self._holdings[index]
  10. def __contains__(self, name):
  11. return any([s.name == name for s in self._holdings])
  12. @property
  13. def total_cost(self):
  14. return sum([s.shares*s.price for s in self._holdings])
  15. def tabulate_shares(self):
  16. from collections import Counter
  17. total_shares = Counter()
  18. for s in self._holdings:
  19. total_shares[s.name] += s.shares
  20. return total_shares

现在,使用 Portfolio 类进行一些实验:

  1. >>> import report
  2. >>> portfolio = report.read_portfolio('Data/portfolio.csv')
  3. >>> len(portfolio)
  4. 7
  5. >>> portfolio[0]
  6. Stock('AA', 100, 32.2)
  7. >>> portfolio[1]
  8. Stock('IBM', 50, 91.1)
  9. >>> portfolio[0:3]
  10. [Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44)]
  11. >>> 'IBM' in portfolio
  12. True
  13. >>> 'AAPL' in portfolio
  14. False
  15. >>>

有关上述代码的一个重要发现——通常,如果一段代码和 Python 的其它代码”类似(speaks the common vocabulary of how other parts of Python normally work)”,那么该代码被认为是 “Pythonic” 的。同理,对于容器对象,其重要组成部分应该包括:支持迭代、可以进行索引、对所包含的元素进行判断,以及其它操作等等。

目录 | 上一节 (5.2 封装) | 下一节 (6.2 自定义迭代)