摘自《Python Cookbook》 4.6
任务
序列中的子序列可能是序列,子序列的子项仍有可能是序列,以此类推,则序列嵌套可以达到任意的深度。需要循环遍历一个序列,将其所有的子序列展开成一个单一的,只具有基本子序列的序列。(一个基本子项或者原子,可以是任何非序列的对象-或者说叶子,假如你认为序列是一棵树)
解决方案
我们需要能够判断哪些我们正在处理的子项是需要被展开的,那些是原子。为了获得通用性,我们使用了一个断定来作为参数,由它来判断子项是否可以展开。(断定[predicate]是一个函数,每当我们处理一个元素时就将其运用于该元素并返回一个布尔值;在这里,如果元素是一个需要展开的子序列就返沪True,否则返回False。)我们假定每一个列表或者原组都是需要被展开的,而其他类型不是。那么简单的解决方法就是提供一个递归的生成器
1 def list_or_update(x): 2 return isinstance(x, (list, tuple)) 3 4 def flatten(sequence, to_expand=list_or_update): 5 for item in sequence: 6 if to_expand(item): 7 for subitem in flatten(item, to_expand): 8 yield subitem 9 else: 10 yield item
讨论
展开一个嵌套的序列,或者等价地,按照顺序“遍历”一棵树的所有叶子,是在各种运用中很常见的任务。如果有一个嵌套的结构,元素都被组织成序列或者子序列,而且,基于某些理由,你并不关心结构本省,需要的只是一个接一个的处理所有的元素。举个例子:
1 l = [['a', 'b'], 'c', ['d', ['e', ['f'], 'g']], 'h'] 2 3 for i in flatten(l): 4 print i,
这个任务唯一的问题是,怎样在尽量通用的尺度下,判断什么是需要展开的,什么是需要被当作原子的,这其实没看上其那么简单。所以,我绕开直接的判断,把这个工作交给一个可调用的判定参数,调用者可以将其传递给 flatten,如果调用者满足于flatten简单的默认行为方法,即指展开原组和列表。
在flatten所在的模块中,我们还需要提供另一个调用者可能需要用到的判定——它将展开任何非字符串(无论是普通字符串还是Unicode)的可迭代对象。字符串是可迭代,但是绝大多数运用程序还是想把他们当成原子,而不是子序列。
至于判断对象是否可迭代,我们只需要对该对象调用内建的iter函数;若该对象不可迭代,此函数将抛出TypeError异常。为了判断对象是否是类字符串,我们则简单第检查它是否是 basestring 的实例,当obj是basestring的任何子类的实例时, isinstance(obj, basestring)的返回值将是True——这意味着任何类字符串类型。因此,这样的一个判定并不难写:
1 def nonstring_iterable(obj): 2 try: 3 iter(obj) 4 except TypeError: 5 return False 6 else: 7 return not isinstance(obj, basestring)
当具体的需求展开任何可迭代非字符串对象时,调用者可以调用flatten(seq, nonstring_iterable)。无疑,不把nonstring_iterable 断定作为flattern的默认选项是一个更好的选择:在简单的需求中,如我们前面展示的示例代码片段,使用nonstring_iterable会比使用list_or_tuple慢3倍以上。
我们也可以写一个非递归版本的flattern。这种写法可以超越Python的递归层次的极限,一般不超过及千层。实现无递归遍历的要点是,采用一个明确的后进先出(LIFO)栈。在这个例子中,我们可以用迭代器的列表实现:
1 def flatten(sequence, to_expand=list_or_tuple): 2 iterators = [iter(sequence)] 3 while iterators: 4 #循环当前的最深嵌套(最后)的迭代器 5 for item in iterators[-1]: 6 if to_expand(item): 7 #找到子序列,循环子序列的迭代器 8 iterators.append(iter(item)) 9 break 10 else: 11 yield item 12 else: 13 #最深嵌套的迭代器耗尽,回过头来循环它的父迭代器 14 iterators.pop()
其中 if 语句块的 if 子句会展开一个我们需要展开的元素——即我们需要循环遍历的子序列;所以我们该在子句中,我们将那个子序列的迭代器压入栈的末尾,在通过break打断for的执行,回到外层的while,外层while会针对我们刚刚压入的新的迭代器执行一个新的for语句。else子句则用于处理那些不需要展开的元素,它直接产生元素本身。
如果for循环未被打断,for语句块所属的else子句将得以执行——换句话说,当for循环完全执行完毕,说明它已经遍历完当前的最新的迭代器。所以,在else子句中,我们移除了已经耗尽的嵌套最深(最近)的迭代器,之后外层的while循环继续执行,如果栈已经空了,则中止循环,如果栈中还有迭代器,则执行一个新的for循环来处理之+正好是上次执行中断的地方,本质上,迭代器的任务就是记忆迭代的状态。
flatten的非递归实现产生的结果和前面的简单一些的递归版本的结果完全一致。如果你认为非递归实现会比递归方式快,那么你可能会失望;我采用一系列的测试用例进行观察测量,发现非递归版本比递归版本慢约10%。