在Python中使用itertools模块中的组合函数的教程
理解新概念
PythonV2.2中引入了迭代器的思想。唔,这并不十分正确;这种思想的“苗头”早已出现在较老的函数xrange()以及文件方法.xreadlines()中了。通过引入yield关键字,Python2.2在内部实现的许多方面推广了这一概念,并使编程定制迭代器变得更为简单(yield的出现使函数转换成生成器,而生成器反过来又返回迭代器)。
迭代器背后的动机有两方面。将数据作为序列处理通常是最简单的方法,而以线性顺序处理的序列通常并不需要都同时实际存在。
x*()前兆提供了这些原理的清晰示例。如果您想对某操作执行成千上万次,那么执行您的程序可能要花些时间,但该程序一般不需要占用大量内存。同样,对于许多类型的文件,可以一行一行地处理,且不需要将整个文件存储在内存中。最好对其它所有种类的序列也进行惰性处理;它们可能依赖于通过通道逐步到达的数据,或者依赖于一步一步执行的计算。
大多数时候,迭代器用在for循环内,就象真正的序列那样。迭代器提供了.next()方法,它可以被显式调用,但有百分之九十九的可能,您所看到的是以下行:
forxiniterator: do_something_with(x)
在对iterator.next()进行幕后调用而产生StopIteration异常时,该循环就被终止。顺便说一下,通过调用iter(seq),实际序列可以被转换成迭代器-这不会节省任何内存,但是在下面讨论的函数中它会很有用。
Python不断发展的分裂性格
Python对函数编程(FP)的观点有点相互矛盾。一方面,许多Python开发人员轻视传统的FP函数map()、filter()和reduce(),常常建议使用“列表理解”来代替它们。但完整的itertools模块恰恰是由与这些函数类型完全相同的函数组成的,只不过这些函数对“惰性序列”(迭代器)操作,而不是对完整的序列(列表,元组)操作。而且,Python2.3中没有任何“迭代器理解”的语法,这似乎与列表理解拥有一样的动机。
我猜想Python最终会产生某种形式的迭代器理解,但这取决于找到合适于它们的自然语法。同时,在itertools模块中,我们拥有大量有用的组合函数。大致地,这些函数中的每一个都接受一些参数(通常包含一些基础迭代器)并返回一个新迭代器。例如,函数ifilter()、imap()和izip()都分别直接等同于缺少词首i的内置函数。
缺少的等价函数
itertools中没有ireduce(),尽管按道理很自然地应该有这个函数;可能的Python实现是:
清单1.ireduce()的样本实现
defireduce(func,iterable,init=None): ifinitisNone: iterable=iter(iterable) curr=iterable.next() else: curr=init forxiniterable: curr=func(curr,x) yieldcurr
ireduce()的用例类似于reduce()的用例。例如,假设您想要添加某个大型文件所包含的一列数字,但是当满足一个条件时就停止。您可以使用以下代码来监控正在计算的合计数:
清单2.添加并合计一列数
fromoperatorimportadd fromitertoolsimport* nums=open('numbers') fortotintakewhile(condition,ireduce(add,imap(int,nums)): print"total=",tot
一个更实际的示例可能类似于将事件流应用于有状态对象上,例如应用到GUI窗口小部件上。但是即使是上述简单示例也显示了迭代器组合器的FP风格。
基本迭代器工厂
itertools中的所有函数都可以用纯Python轻松地实现为生成器。在Python2.3+中包含该模块的要点是为一些有用的函数提供标准行为和名称。尽管程序员可以编写他们自己的版本,但是每个人实际创建的变体都会有点不兼容。但是,另一方面是要以高效率的C代码实现迭代器组合器。使用itertools函数将比编写您自己的组合器稍微快一些。标准文档显示了每个itertools函数的等价纯Python实现,所以不需要在本文中重复这些内容了。
itertools中的函数再基本不过了-而且命名也完全不同-这样从该模块导入所有名称可能就有意义了。例如,函数enumerate()可能明显地出现在itertools中,但是它在Python2.3+中却是一个内置函数。值得注意的是,您可以用itertools函数很方便地表达enumerate():
fromitertoolsimport* enumerate=lambdaiterable:izip(count(),iterable)
让我们首先看一下几个itertools函数,它们没有将其它迭代器作为基础,而完全是“从头”创建迭代器。times()返回一个多次产生同一对象的迭代器;在本质上,这一能力比较有用,但它确实可以很好地替代使用过多的xrange()和索引变量,从而可以简单地重复一个操作。即,不必使用:
foriinxrange(1000): do_something()
您现在就可以使用更中性的:
for_intimes(1000): do_something()
如果times()只有一个参数,那么它只会重复产生None。函数repeat()类似于times(),但它无界地返回同一对象。不管是在包含独立break条件的循环中还是在象izip()和imap()这样的组合器中,这个迭代器都很有用。
函数count()有点类似于repeat()和xrange()的交叉。count()无界地返回连续整数(以可选的参数为开始)。但是,如果count()当前不支持溢出到现在正确的longs,那么您可能还是要使用xrange(n,sys.maxint);它并不是完全无界的,但是对于大多数用途,它实际上是一回事。类似于repeat(),count()在其它迭代器组合器内部特别有用。
组合函数
我们已经顺便提到了itertools中的几个实际组合函数。ifilter()、izip()和imap()的作用就象您会期望从它们相应的序列函数上获得的作用。ifilterfalse()很方便,所以您不需要去掉lambda和def中的谓词函数(而且这还节省了大量的函数调用开销)。但是在功能上,您可以将ifilterfalse()定义为(大致的情况,忽略了None谓词):
defifilterfalse(predicate,iterable): returnifilter(lambdapredicate:notpredicate,iterable)
函数dropwhile()和函数takewhile()根据谓词对迭代器进行划分。dropwhile()在直到满足某个谓词之前忽略所产生的元素,takewhile()在满足某个谓词时就终止。dropwhile()跳过迭代器的不定数目的初始元素,所以它可能直到某个延迟后才开始迭代。takewhile()马上开始迭代,但是如果被传入的谓词变为真,那么就终止迭代器。
函数islice()基本上就是列表分片的迭代器版本。您可以指定开始、停止和步长,就象使用常规的片。如果给定了开始,那么会删除大量元素,直到被传递的迭代器到达满足条件的元素为止。这是另一个我认为可以对Python进行改进的情形-迭代器最好只识别片,就象列表所做的(作为islice()行为的同义词)。
最后一个函数starmap()在imap()基础上略微有些变化。如果这个作为参数传递的函数获取多个参数,那么被传递的iterable会产生大小适合的元组。这基本上与包含多个被传入iterable的imap()相同,只不过它包含先前与izip()结合在一起的iterables集合。
深入探讨
itertools中包含的函数是一个很好的开始。不使用其它函数,只用这些函数就可以让Python程序员更轻松地利用和组合迭代器。一般说来,迭代器的广泛使用对Python的未来无疑是很重要的。但是除了过去所包含的内容以外,我还要对该模块的将来更新提几点建议。您可以立即很方便地使用这些函数-当然,如果它们是后来被包含进来的,那么名称或接口细节会有所不同。
一种可能会很通用的类别是一些将多个iterable作为参数,随后从每个iterable产生单独元素的函数。与此相对照的是,izip()产生元素元组,而imap()产生从基本元素计算而来的值。我头脑中很清晰的两个安排是chain()和weave()。第一个在效果上类似于序列并置(但是有点惰性)。即,在您可能使用的纯序列中,例如:
forxin('a','b','c')+(1,2,3): do_something(x)
对于一般的iterables,您可以使用:
forxinchain(iter1,iter2,iter3): do_something(x)
Python实现是:
清单3.chain()的样本实现
defchain(*iterables): foriterableiniterables: foriteminiterable: yielditem
使用iterables,您还可以通过使它们分散排列来组合几个序列。还没有任何对序列执行这样相同操作的内置语法,但是weave()本身也非常适用于完整的序列。下面是可能的实现(MagnusLieHetland提出了comp.lang.python的类似函数):
清单4.weave()的样本实现
defweave(*iterables): "Intersperseseveraliterables,untilallareexhausted" iterables=map(iter,iterables) whileiterables: fori,itinenumerate(iterables): try: yieldit.next() exceptStopIteration: deliterables[i]
让我来演示一下weave()的行为,因为从实现上看不是很明显:
>>>forxinweave('abc',xrange(4),[10,11,12,13,14]): ...printx, ... a010b111c21213314
即使一些迭代器到达终点,但其余迭代器会继续产生值,直到在某一时刻产生了所有可用的值为止。
我将另外只提出一个可行的itertools函数。提出这个函数主要是受到了构思问题的函数编程方法的启发。icompose()与上面提出的函数ireduce()存在某种对称。但是在ireduce()将值的(惰性)序列传递给某个函数并产生每个结果的地方,icompose()将函数序列应用于每个前趋函数的返回值。可以把ireduce()用于将事件序列传递给长期活动的对象。而icompose()可能将对象重复地传递给返回新对象的赋值函数。第一种方法是相当传统的考虑事件的OOP方法,而第二种的思路更接近于FP。
以下是可能的icompose()实现:
清单5.icompose()的样本实现
deficompose(functions,initval): currval=initval forfinfunctions: currval=f(currval) yieldcurrval
结束语
迭代器-被认为是惰性序列-是功能强大的概念,它开启了Python编程的新样式。但是在只把迭代器当作数据源与把它作为一种序列来考虑之间存在着微妙的差别。这两种想法本质上哪一种都不见得比另一种更正确,但是后者开创了操作编程事件的一种组合性的简略表达方法。itertools中的组合函数(尤其是它可能产生的一些类似于我建议的函数)接近于编程的声明样式。对我而言,这些声明样式一般都更不易出错且更强大。