|
有时候,有意义比较的 “局部序列” 是有用的,甚至在出现不同类型对象的情况下也是如此(例如,“依次” 处理所有浮点值,即使它们与其他地方处理的字符串没有可比性)。
排序失败
当然,上面执行 “list diff” 的代码几乎可以任意扩展。例如,list1 和 list2 可以是下面这样的小列表的集合。请试着猜一下哪些部分是可以排序的:
清单 4. 可排序和不可排序列表的大杂烩 ['x','y','z', 1], ['x','y','z', 1j], ['x','y','z', 1j, 1], # Adding an element makes IT unsortable [0j, 1j, 2j], # An obvious "natural" order [0j, 1, 2], [0, 1, 2], # Notice that 0==0j --> True [chr(120), chr(240)], [chr(120), chr(240), 'x'], [chr(120), chr(240), u'x'], # Notice u'x'=='x' --> True [u'a', 'b', chr(240)], [chr(240), u'a', 'b'] # Same items, different inITial order
我编写了一个小程序来尝试排序各列表:
清单 5. 对各列表进行排序的结果 % python compare.py (0) ['x', 'y', 'z', 1] --> [1, 'x', 'y', 'z'] (1) ['x', 'y', 'z', 1j] --> [1j, 'x', 'y', 'z'] (2) ['x', 'y', 'z', 1j, 1] --> exceptions.TypeError (3) [0j, 1j, 2j] --> exceptions.TypeError (4) [0j, 1, 2] --> exceptions.TypeError (5) [0, 1, 2] --> [0, 1, 2] (6) ['x', '\xf0'] --> ['x', '\xf0'] (7) ['x', '\xf0', 'x'] --> ['x', 'x', '\xf0'] (8) ['x', '\xf0', u'x'] --> exceptions.UnicodeDecodeError (9) [u'a', 'b', '\xf0'] --> [u'a', 'b', '\xf0'] (10) ['\xf0', u'a', 'b'] --> exceptions.UnicodeDecodeError
通过前面的解释,或多或少能够猜出一部分结果。但是,看一下 (9) 和 (10),这两个列表以不同次序包含完全相同的对象:由此可见,排序是否失败不但取决于列表中 对象的类型和值,还取决于 list.sort() 方法的特定实现!
修订比较
自 1.5.2 以来,Python 发展出了一种非常有用的数据类型:集(set),它最初是一个标准模块,后来成了一种内置类型(模块还包含一些额外的特性)。对于上面描述的许多问题,只需使用集来取代列表即可轻松地判断对象是在一个集合中、在另一个集合中还是同时存在于两个集合中,而不需要编写自己的 “list diff” 代码。例如:
清单 6. 集与集操作 >>> set1 = set([1j, u'2', 3, 4.0]) >>> set2 = set([4, 3, 2, 1]) >>> set1 | set2 set([3, 1, 2, 1j, 4.0, u'2']) >>> set1 & set2 set([3, 4])
在编写上面这个示例时,我发现了一些相当奇怪的现象。集操作似乎采用相等性(equality)而不是同一性(identITy)。或许这有某种意义,但奇怪的是,这两个集的合集包含浮点数 4.0,而其交集包含整数 4。更奇怪的是,尽管求合集和交集的操作在理论上是对称的,但是实际结果却与次序相关:
清单 7. 集中得到的奇怪类型 >>> set2 & set1 set([3, 4.0]) >>> set([3, 4.0, 4, 4+0j]) set([3, 4.0])
当然,初看起来,集是一种很棒的数据类型。但是,定制的比较总是应该考虑的解决方案。在 Python 2.4 之前,完全有可能实现定制的 cmp() 函数并将它传递给 list.sort()。这样就可以实现原本不可比较的对象之间的比较;cmp 自变量的问题是,每次比较时都要调用这个函数:Python 的调用开销非常高,而且更糟糕的是,要经过多次计算才能得出所计算的值。
对于 cmp 效率低下的问题,有效的解决方案是使用 Schwartzian 排序:修饰(decorate)各对象,进行排序,然后去掉修饰。遗憾的是,这需要使用一些定制的代码,不是简单地调用 list.sort() 就能够实现的。Python 2.4 提供了一种出色的组合解决方案,使用 key 自变量。这个自变量接受一个函数,这个函数返回一个经装饰的对象并 “在幕后” 进行 Schwartzian 排序。请记住,即便是复数与复数之间也不能进行比较,而 Unicode 对象只在与某些 字符串比较时会出问题。我们可以使用以下代码:
清单 8. 一个稳定、通用的排序键 def stablesort(o): # Use as: mylist.sort(key=stablesort) if type(o) is complex: return (type(o), o.real, o.imag) else: return (type(o), o)
请记住,元素的次序可能与您期望的不完全一致:即使在未修饰排序成功的地方,次序与未修饰排序也不一样。尤其是,具有不同数字类型的元素不再混在一起,而是被分隔为排序结果的不同部分。但是,它至少是固定的,而且对于几乎任何列表都是有效的(仍然可以用定制的对象扩展这种排序)。
以生成器作为准序列
经过数个版本的发展,Python 的 “惰性” 大大增强。有几个版本中已出现了在函数体中用 yield 语句定义的生成器。但是,在这个过程中,还出现了 itertools 模块,它可以组合和创建各种类型的迭代器。还出现了 ITer() 内置函数,它可以将许多类似于序列的对象转换为迭代器。在 Python 2.4 中,出现了生成器表达式(generator expression);在 2.5 中,出现了改进的生成器,这使编写协同例程更为轻松。另外,越来越多的 Python 对象成为迭代器或类迭代器,例如,过去读取文件需要使用 .xreadlines() 方法或 xreadlines 模块,而现在 open() 的默认行为就能是读取文件。
同样,过去要实现 dict 的惰性循环遍历需要使用 .iterkeys() 方法;现在,它是默认的 for key in dct 行为。xrange() 这样的函数与生成器类似的方面有些 “特殊”,它们既不是真正的 迭代器(没有 .next() 方法),也不是实际的列表(比如 range() 返回的列表)。但是,enumerate() 返回一个真正的生成器,通常会实现以往希望 xrange() 实现的功能。ITertools.count() 是另一个惰性调用,它的作用与 xrange() 几乎 完全相同,但它是一个功能完备的迭代器。
Python 的发展大方向是以惰性方式构造类似于序列的对象;总体来说,这个方向是正确的。惰性的伪序列既可以节省内存空间,还可以提高操作的速度(尤其是在处理非常大的与序列类似的 “东西” 时)。
问题在于,Python 在判断 “硬” 序列和迭代器之间的差异和相似性方面仍然不完善。这个问题最棘手的部分是,它实际上违背了 Python 的 “duck typing” 思想:只要给定的对象具有正确的行为,就能够将它用于特定的用途,而不存在任何继承或类型限制。迭代器或类迭代器有时表现得像序列,但有时候不是;反过来说,序列常常表现得像迭代器,但并非总是如此。表现不像迭代器那些序列已涉及 Python 的神秘领地,其作用尚不明朗。 上一页 [1] [2] [3] 下一页 |