ChinaUnix.net 首页 | 博客 | Linux | 论坛 | 人才 | 培训 | 知识库 | 资料 | 读书 | 手册 | 精华 | 下载 | 沙龙 | 搜索
Linux首页 | Linux论坛 | 论坛精华 | 开源新闻 | 技术文章 | 专题专栏 | 新手指南 | 迁移方案 | 产品方案 | 开源项目 | 开源图书 | 软件下载 | 人才招聘 | Linux博客
  搜索

  产品与方案
·中科红旗全面打造现代化邮政体系
·红旗助力“网上审批服务” 推动电子政务
·红旗正版化开创呼和浩特网吧建设新起点
·红旗Linux助信息产业部邮件服务器“快跑”
·中标普华Linux 为电子政务信息化保驾护航
·中标普华Linux助力基金产业
·中标普华Office率先支持UOF标准
·中标普华邮件系统助力西藏政府信息化建设
·红旗Linux助力国库集中支付系统改革
·红旗助中信卫星 掀起GIS通信应用风暴
·红旗软件助力烟草总局 全面建设“数字烟草”
·红旗助力“信访阳光工程”打造畅通信访渠道
·红帽联合FIS发布下一代实时核心银行平台
·红旗助力金盾 打造全无忧出入境信息系统
·红旗Linux全力打造中国邮政总局名址信息库
·爱尔兰证交所从Unix迁移到红帽企业Linux
·一流的意大利银行选择使用红帽企业Linux
·PLUS Finanzservice选择使用红帽企业Linux
·红帽助力TransACT Communications 公司
·法国零售业巨头Lapeyre采用Redhat Linux
·旅游预订网站选择使用红帽企业Linux
·马哈拉施特拉邦政府的红帽解决之道
·美国联邦政府案例
·红帽为慕尼黑展览会提供现代化集群系统
·Yuba郡用开源软件和红帽产品提高了效率
·红帽企业Linux助印度理工建立高性能计算中心
·采用红帽Linux 将系统维护时间缩短了65%
·从UNIX迁移到Linux使Peñoles公司获益非浅
·Hikal公司用红帽企业Linux开展任务关键的ERP项目
·KDE3.5.4新版本发布
·芝加哥商业交易所从Unix向Linux迁移
·南方基金管理有限公司成功案例 Red Hat Linux
·广东北电通讯设备有限公司成功案例
·挪威国家石油公司从UNIX迁移到红帽Linux,成本减半
·中央电视台CCTV动画部案例 Red Hat Linux

  图书

鸟哥的Linux私房菜基础学..


Linux程序设计.第3版


Linux设备驱动开发详解


  下载
·Endian Firewall
·linux kernel(Linux 内核)
·CentOS
·Fedora Core 6
·Scientific Linux
·Slackware 11.0
·Gentoo Linux
·ubuntu-6.10-i386服务器版本
·ubuntu-6.10-amd64服务器版
·ubuntu-6.10-i386桌面版
·ubuntu-6.10-amd64桌面版
·Engarde Linux
您的位置: Linux时代 > 技术文档 > 程序开发 >

Python 之优雅与瑕疵

日期:2007-05-23 作者:David Mertz 来自:IBM DW中国


自从 Python 1.5.2(一个长期以来一直稳定且可靠的版本)迈入 “黄金时代” 以来,Python 增加了许多语法特性以及内置函数和类型。这些改进单独地看都是合理的调整,但是作为一个整体,它们使 Python 变得更加复杂,不再是有经验的程序员 “花上一个下午” 就能够掌握的语言了;另外,一些修改在带来好处的同时也有缺陷。

在本文中,我要讨论在最近几个 Python 版本中增加的不那么引人注目的特性,我将分析哪些改进具有真正的价值,哪些特性只是不必要地增加了复杂性。我希望向所有并非一直使用 Python 的程序员指出真正具有价值的东西。这包括使用其他语言的程序员以及只将编程当做副业的科学家。当遇到一些难题时,我会提供解决方案。

不可比较的麻烦

在 Python 2.0 和 Python 2.1 之间,发生了一些奇怪的变化。以前可以比较的对象在进行比较时却引发了异常。具体地说,复数无法与其他数字进行比较了,包括其他复数以及整数、浮点数和长整数。实际上,在此之前,比较 Unicode 字符串和文本字符串时就可能会遇到这个问题,但那只发生在一些极端情况下。

我认为,这些修改很怪异,没有必要。在 1.5.2 的黄金时代,无论比较什么对象,不等操作符总会返回一个结果。当然,结果不一定是有意义的 —— 比如字符串和浮点数的比较就没有意义。但是,至少我们总会得到一个一致的结果。

出现这些修改之后,一些 Python 支持者认为不允许对不同类型的对象进行不等比较是件好事,只有定义了定制的比较函数之后,才能进行这种比较。我觉得,在处理定制类和多重继承时,编写定制的比较函数实际上很需要技巧。另外,不能在浮点数、整数和长整数(比如 decimal)之间进行比较是非常不方便的。但是,或许可以定义一个合理的规则。

但是,无论定义什么样的规则,它都与 Python 过去的做法有非常大的差异。现在的情况是比较行为无规律可循,即使知道比较的对象的类型,也无法确定它们是否是可比较的(而且不等性既非可传递也非封闭式):


清单 1. 比较是否成功取决于类型和值
>>> map(type, (u1, s1, s2))
[<type 'unicode'>, <type 'str'>, <type 'str'>]

>>> u1 < s1
True

>>> s1 < s2
True

>>> u1 < s2
UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0 in position 0:
    ordinal not in range(128)

>>> map(type, (n, j, u1))
[<type 'int'>, <type 'complex'>, <type 'unicode'>]

>>> n < u1
True

>>> j < u1
True

>>> n < j
TypeError: no ordering relation is defined for complex numbers

更糟糕的是,复数现在不能与大多数 数字值进行比较,但是可以通过大多数非数字值判断出绝对的不等性。例如,考虑到理论纯洁性,我知道 1+1j2-3j 的比较是没有意义的,但是为什么有下面这样的结果:


清单 2. 令人吃惊的比较结果
>>> 2-3j < 'spam'
True

>>> 4+0j < decimal.Decimal('3.14')
True

>>> 4+0j < 5+0j
TypeError: no ordering relation is defined for complex numbers
      

从理论上来讲,这全无 “纯” 可言。

一个真正的瑕疵:对异构集合进行排序

自变量有时候会造成编程错误,试图对不可比较的类型进行比较。但是 Python 可以顺利地执行许多这种类型的比较;并且依照 “duck typing” 哲学来完成这样的任务(duck typing 是指 “如果看起来像鸭子,听起来像鸭子,就可以把它当作鸭子”,也就是说,不管对象 什么,只在乎它 什么。)Python 集合常常将不同类型的对象组织在一起,希望能够 与其中的各对象相似的事情。一种常见的用例是对一组不同类型的值进行编码,以便通过某种协议进行传输。

对于这其中的大多数值,不等比较是不必要的。但是,在一种常见的情况下,不等性是非常有用的;那就是对集合进行排序 时,通常是对列表或与列表类似的定制集合进行排序。有时候,需要以一种有意义的升序来处理集合(例如,按照数据值从小到大的次序)。在其他时候,需要为多个集合创建一种固定的次序,尤其是为了对两个集合执行某种类似于 “list diff” 的处理时。也就是说,如果一个对象在这两个集合中都存在,那么就执行一种操作;如果它只在一个集合中存在,就执行另一种操作。不断地检查 if x in otherlist 会导致效率成 big-O 式几何级数递减;在两个固定排序的列表之间进行平行匹配的效率要高得多。例如:


清单 3. 根据两个列表的成员关系执行不同的操作
list1.sort()
list2.sort()
list2_xtra = []
list2_ndx = 0
for it1 in list1:
    it2 = list2[list2_ndx]
    while it1 < it2:
        list2_ndx += 1
        it2 = list2[list2_ndx]
        if it1 == it2:
            item_in_both(it1)
        elif it1 > it2:
            item_in_list1(it1)
        else:
            list2_xtra.appen(it2)
 for it2 in list2_xtra:
     item_in_list2(it2)

有时候,有意义比较的 “局部序列” 是有用的,甚至在出现不同类型对象的情况下也是如此(例如,“依次” 处理所有浮点值,即使它们与其他地方处理的字符串没有可比性)。

排序失败

当然,上面执行 “list diff” 的代码几乎可以任意扩展。例如,list1list2 可以是下面这样的小列表的集合。请试着猜一下哪些部分是可以排序的:


清单 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 的神秘领地,其作用尚不明朗。

分歧

所有类序列或类迭代器的主要相似之处是,它们都允许进行循环遍历,无论是使用 for 循环、列表理解(list comprehension)还是生成器理解(generator comprehension)。除此之外,就出现了分歧。其中最重要的差异是,序列既可编制索引,也可直接切片(slice),而迭代器不能。实际上,为序列编制索引可能是最常用的序列操作 —— 究竟为什么在迭代器上无法使用索引呢?例如:


清单 9. 与序列和迭代器相似的东西
>>> r = range(10)

>>> i = iter(r)

>>> x = xrange(10)

>>> g = itertools.takewhile(lambda n: n<10, itertools.count())

#...etc...

对于所有这一切,都可以使用 for n in thing。实际上,如果用 list(thing) 显示它们,会得到完全相同的结果。但是,如果希望获得其中的一个特定条目(或一些条目的切片),就需要考虑 thing 的具体类型。例如:


清单 10. 索引操作成功和失败的示例
>>> r[4]
4

>>> i[4]
TypeError: unindexable object

对于每种序列/迭代器类型,只要费一番功夫,总能够获得其中的特定条目。一种方法是进行循环,直至到达所需的对象。另一种古怪的解决方案如下:


清单 11. 获得索引的怪方法
>>> thing, temp = itertools.tee(thing)

>>> zip(temp, '.'*5)[-1][0]
4

itertools.tee() 的预调用保留了原始迭代器。对于切片,可以按照特殊方式使用 itertools.islice() 函数。


清单 12. 获得一个切片的特殊方法
>>> r[4:9:2]
[4, 6, 8]

>>> list(itertools.islice(r,4,9,2))  # works for iterators
[4, 6, 8]

类包装器

为了方便起见,可以将这些技术组合成一个类包装器:


清单 13. 使迭代器可编制索引

所以,通过某些措施,可以让一个对象同时表现得像序列和迭代器。但是,费这么大力气实际上 应该是不必要的;无论涉及的是序列还是迭代器,编制索引和切片都应该是 “可行的”。

注意,Indexable 类包装器仍然不够灵活。主要问题是,每次都要创建迭代器的一个新副本。更好的方法应该是在对序列切片时缓存序列的头部,然后在以后访问已经检查的元素时使用所缓存的头部。当然,在使用迭代器时,要在占用的内存和速度之间有所取舍。但是,如果 Python 本身能够 “在幕后” 完成这些,就再好不过了 —— “高级用户” 可以对行为进行调优,而一般程序员应该不需要考虑这些。

在本系列的下一期中,我将讨论如何使用属性语法访问方法。

原文链接:http://www.ibm.com/developerworks/cn/linux/l-python-elegance-1.html

本文被浏览



 相关新闻

Python anygui 项目预览2006-12-25 14:18:17
欧洲Python开发者年会2005-06-30 10:01:58
在 Vim 中编写 Python 程序2005-01-31 15:11:52
Python 与 Gnumeric 共舞2005-01-19 11:46:00
python几种开发工具介绍2005-01-13 11:09:53


 相关评论
关于我们 | 联系方式 | 广告合作 | 诚聘英才 | 网站地图 | 免费注册

Copyright © 2001-2006 ChinaUnix.net All Rights Reserved

感谢所有关心和支持过ChinaUnix的朋友们

京ICP证041476号