返回介绍

建议65:熟悉 Python 的迭代器协议

发布于 2024-01-30 22:19:09 字数 4309 浏览 0 评论 0 收藏 0

其实对于大部分Python程序员而言,迭代器的概念可能并不熟悉。但这很正常,与C++等语言不同,Python的迭代器集成在语言之中,与语言完美地无缝集成,不像C++中那样需要专门去理解这一个概念。比如,要遍历一个容器,Python代码如下:

>>> alist = range(2)
>>> for i in alist:
...   print i
0
1

而C++代码如下:

using namespace std;
vector<int> myIntVector;
// 
往容器 myIntVector 
中添加元素的操作,略
for(vector<int>::iterator = myIntVector.begin(); 
     myIntVectorIterator != myIntVector.end();
     myIntVectorIterator++){
   cout<<*myIntVectorIterator<<" ";
}

两相对比,可以看到C++的代码中,多了一个vector<int>::iterator类型,它是什么、有什么用、什么时候用、怎么用,都是C++程序员需要理解和掌握的内容,所以可以说,在“实现遍历容器”这一事情上,使用C++要付出更多的精力去学习更多的内容,这就是Python把迭代器内建在语言之中的好处。

但是,并非所有的时候都能够隐藏细节,特别是在写一本书向读者讲述其中的机理的时候。所以在这里,首先需要向大家介绍一下iter()函数。iter()可以输入两个实参,但为了简化起见,在这里忽略第二个可选参数,只介绍一个参数的形式。iter()函数返回一个迭代器对象,接受的参数是一个实现了__iter__()方法的容器或迭代器(精确来说,还支持仅有__getitem__()方法的容器)。对于容器而言,__iter__()方法返回一个迭代器对象,而对迭代器而言,它的__iter__()方法返回其自身,所以如果我们用一个迭代器对象it,当以它为参数调用iter(it)时,返回的是自身。

>>> it = iter(alist)
>>> it2 = iter(it)
>>> assert id(it) == id(it2)

到时此,就可以跟大家讲一下迭代器协议了。前文已经说过,所谓协议,是一种松散的约定,并没有相应的接口定义,所以把协议简单归纳如下:

1)实现__iter__()方法,返回一个迭代器。

2)实现next()方法,返回当前的元素,并指向下一个元素的位置,如果当前位置已无元素,则抛出StopIteration异常。

可以通过以下代码验证这个协议:

>>> alist = range(2)
>>> it = alist.__iter__()
>>> it.next()
0
>>> it.next()
1
>>> it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

与上例使用iter()内置函数不同,这次的代码是it=alist.__iter__(),可见list这一容器确实是实现了迭代器协议中容器的部分。后续连续3次的next()方法调用也印证了协议的第2条。

熟悉了迭代器协议,那么我们就可以使用它来遍历所有的容器,仍然以list对象为例。

>>> alist = range(2)
>>> it = iter(alist)
>>> while True:
...  try:
...      print it.next()
...  except StopIteration:
...      break
...
0
1

可以看到输出跟最初使用for循环是一样的,对,你的灵光一闪没有错,其实for语句就是对获取容器的迭代器、调用迭代器的next()方法以及对StopIteration进行处理等流程进行封装的语法糖(类似的语法糖还有in/not it语句)。

迭代器最大的好处是定义了统一的访问容器(或集合)的统一接口,所以程序员可以随时定义自己的迭代器,只要实现了迭代器协议就可以。除此之外,迭代器还有惰性求值的特性,它仅可以在迭代至当前元素时才计算(或读取)该元素的值,在此之前可以不存在,在此之后可以销毁,也就是说不需要在遍历之前事先准备好整个迭代过程中的所有元素,所以非常适合遍历无穷个元素的集合(如斐波那契数列)或巨大的事物(如文件)。

class Fib(object):
  def __init__(self):
    self._a = 0
    self._b = 1
  def __iter__(self):
    return self
  def next(self):
    self._a, self._b = self._b, self._a + self._b
    return self._a
for i, f in enumerate(Fib()):
  print f
   if i > 10:
     break

这段代码能够打印斐波那契数列的前10项。再来看一下传统的使用容器存储整个数列的方案。

>>> def fib(n):
  """
返回小于指定值的斐波那契数列"""
  result=[]
  a,b=0,1
  while b<n:
     result.append(b)
     a,b=b,a+b
  return result
>>> fib(10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

与直接使用容器的代码相比,它仅使用两个成员变量,显而易见更省内存,并在一些应用场景更省CPU计算资源,所以在编写代码中应当多多使用迭代器协议,避免劣化代码。对于这一观点,不必怀疑,从Python 2.3版本开始,itertools成为了标准库的一员已经充分印证这个观点。

itertools的目标是提供一系列计算快速、内存高效的函数,这些函数可以单独使用,也可以进行组合,这个模块受到了Haskell等函数式编程语言的启发,所以大量使用itertools模块中的函数的代码,看起来有点像函数式编程语言写推荐,比如sum(imap(operator.mul, vector1,vector2))能够用来运行两个向量的对应元素乘积之和。

itertools最为人所熟知的版本,应该算是zip、map、filter、slice的替代,izip(izip_longest)、imap(startmap)、ifilter(ifilterfalse)、islice,它们与原来的那几个内置函数有一样的功能,只是返回的是迭代器(在Python 3中,新的函数撤底替换掉了旧函数)。

除了对标准函数的替代,itertools还提供以下几个有用的函数:chain()用以同时连续地迭代多个序列;compress()、dropwhile()和takewhile()能用以遴选序列元素;tee()就像同名的UNIX应用程序,对序列作n次迭代;而groupby的效果类似SQL中相同拼写的关键字所带的效果。

 [k for k, g in groupby('AAAABBBCCDAABBB')] --> A B C D A B
 [list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D

除了这些针对有限元素的迭代帮助函数之外,还有count()、cycle()、repeat()等函数产生无穷序列,这3个函数就分别可以产生算术递增数列、无限重复实参序列的序列和重复产生同一个值的序列。

如果以上这些函数让你感到吃惊,那么接下来的4个组合数学的函数就会让你更加惊讶了,如表6-3所示。

表6-3 组合函数以及其意义

下面通过例子来熟悉一下,第一行是代码,第二行是结果。

product('ABCD', repeat=2)
AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD
permutations('ABCD', 2)
AB AC AD BA BC BD CA CB CD DA DB DC
combinations('ABCD', 2)
AB AC AD BC BD CD
combinations_with_replacement('ABCD', 2)
AA AB AC AD BB BC BD CC CD DD
其中 product() 
可以接受多个序列,如:
>>> for i in product('ABC', '123',  repeat=2): print ''.join(i)
... 
A1A1
A1A2
A1A3
A1B1
A1B2
A1B3
# 
略去其余输出

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文