可爱的 Python: Python 之优雅与瑕疵

可爱的 Python: Python 之优雅与瑕疵


David Mertz, Ph. D. (mertz@gnosis.cx), 开发人员, Gnosis Software, Inc.

2007 年 4 月 29 日

自 从 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. 使迭代器可编制索引
>>> class Indexable(object):
... def __init__(self, it):
... self.it = it
... def __getitem__(self, x):
... self.it, temp = itertools.tee(self.it)
... if type(x) is slice:
... return list(itertools.islice(self.it, x.start, x.stop, x.step))
... else:
... return zip(temp, range(x+1))[-1][0]
... def __iter__(self):
... self.it, temp = itertools.tee(self.it)
... return temp
...

>>> integers = Indexable(itertools.count())

>>> integers[4]
4
>>> integers[4:9:2]
[4, 6, 8]

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

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

在大多数面向对象语言中,方法和属性几乎相同(但并非完全相同)。两者都可以附加到类和/或实例。除了实现细节外,存在一个关键区别:当附加到对象时,您可以调用 方法 发起动作和计算;而属性 仅具有一些可被检索(或者修改)的值。

对 于某些语言(例如 Java™ 语言),这可能是惟一的区别。属性和方法之间泾渭分明。Java 语言通常主要关注封装和数据隐藏;因此鼓励使用 “setters” 和 “getters” 方法访问其他私有的属性数据。对于 Java 式的思考方式,如果您希望向数据访问和修改中添加计算功能和副作用,则需要提前使用显式的方法调用。当然,Java 方法生成的结果比较冗长,并且某些时候必须遵守一些人为规定的规则:编写 foo.getBar()(而不是 foo.bar)和 foo.setBar(value)(而不是 foo.bar=value)。

作为这方面的一种独特技术,有必要提到 Ruby。实际上,Ruby 在数据隐藏方面要求比 Java 更严格:所有 属性始终 是 “私有的”;您决不能 直接访问实例数据。同时,Ruby 使用了某些语法约定,使方法调用类似于其他语言中的属性访问。第一个约定是在方法调用中使用 Ruby 的圆括号(可选);第二个约定就是使用半专有的方法命名,其中使用了在其他语言中作为运算符的符号。因此在 Ruby 中,foo.bar 仅仅是调用 foo.bar() 的一种更简短方法;而 “设置” foo.bar=value 则是 foo.bar=(value) 的一种简略形式。实际上,所有内容 都涉及到方法调用。

Python 要比 Java 或 Ruby 更加灵活,这个优点既值得称道,同时也为人们所诟病。如果您在 Python 中访问 foo.bar,或设置 foo.bar=value, 您可能使用了一个简单的数据值,或者调用了某些半隐藏的代码。此外,在后者中,至少有六种不同方法可以访问代码块,各种方法之间稍有不同,这些细微差别极 易混淆。过多的方法损害了 Python 的正则性,使非专家人员(甚至专家)难于理解。我知道为什么这些方法都自成体系:因为新的功能是分步添加到 Python 的面向对象基础中的。但是我并不觉得这种混乱有什么值得高兴的。

一种老式方法

在过去(Python 2.1 以前),Python 具有一个神奇的方法,称为 .__getattr__(),类可以定义该方法以返回经过计算的值,而不仅仅是简单的数据访问。同样神奇的 .__setattr__().__delattr__() 方法可以在设置或删除 “属性” 时使代码运行。这种旧式机制的问题是,您从来没有真正了解代码是否确实将被调用,因为这取决于属性是否具有与 obj.__dict__ 中访问过的属性相同的名称。您可以尝试创建控制 obj.__dict__ 最终状态的 .__setattr__().__delattr__() 方法,但即使这样也不能防止其他代码对 obj.__dict__ 的直接操作。不管在处理对象时是否实际运行了方法,修改继承树和将对象传递给外部函数经常会使这一点变得不那么明显。例如:


清单 1. 是否将运行方法?

>>> class Foo(object):
... def __getattr__(self, name):
... return "Value of %s" % name
>>> foo = Foo()
>>> foo.just_this = "Some value"
>>> foo.just_this
'Some value'
>>> foo.something_else
'Value of something_else'

foo.just_this 的访问跳过了方法代码,而对 foo.something_else 的访问则运行了代码;除了这个 shell 会话较短以外,没什么特别明显的不同。事实上,是否运行了 hasattr(),答案很让人容易误解:


清单 2. hasattr() 使用的多义性

>>> hasattr(foo,'never_mentioned')
True
>>> foo2.__dict__.has_key('never_mentioned') # this works
False
>>> foo2.__dict__.has_key('just_this')
True


slot 方法

使用 Python 2.2,我们获得了一种创建 “限制” 类的新机制。新式类 _slots_ 属性的具体用途并不十分明了。大部分情况下,Python 文档建议只有对具有大量实例的类进行性能优化时使用 .__slots__ —— 但这绝不是 一种声明属性的方法。但是,后者正是 slot 的作用:它们将创建一个不具备 .__dict__ 属性的类,其中的属性都经过显式命名(然而,在类主体内仍按常规声明方法)。这有一点特别,但是这种方法可以确保在访问属性时调用方法代码:


清单 3. 确保方法执行使用 .__slots__

>>> class Foo2(object):
... __slots__ = ('just_this')
... def __getattr__(self, name):
... return "Value of %s" % name
>>> foo2 = Foo2()
>>> foo2.just_this = "I'm slotted"
>>> foo2.just_this
"I'm slotted"
>>> foo2.something_else = "I'm not slotted"
AttributeError: 'Foo' object has no attribute 'something_else'
>>> foo2.something_else
'Value of something_else'

声明 .__slots__ 可确保只能直接访问您指定的那些属性;所有属性都将经过 .__getattr__() 调用。如果您还创建了一个 .__setattr__() 方法,您可以指定执行一些其他工作,而不是引发一个 AttributeError(但要确保在指定中使用经过 “slot” 处理的值)。例如:


清单 4. 结合使用 .__setattr__ 和 .__slots__

>>> class Foo3(object):
... __slots__ = ('x')
... def __setattr__(self, name, val):
... if name in Foo.__slots__:
... object.__setattr__(self, name, val)
... def __getattr__(self, name):
... return "Value of %s" % name
...
>>> foo3 = Foo3()
>>> foo3.x
'Value of x'
>>> foo3.x = 'x'
>>> foo3.x
'x'
>>> foo3.y
'Value of y'
>>> foo3.y = 'y' # Doesn't do anything, but doesn't raise exception
>>> foo3.y
'Value of y'

.__getattribute__() 方法

在 Python 2.2 及之后版本中,您可以选择使用 .__getattribute__() 方法,代替具有类似名称且易被混淆的老式 .__getattr__() 方法。如果使用的是新式的类(一般情况下总是如此),您就可以这样做。.__getattribute__() 方法比它的同类方法更为强大,因为不管属性是不是在 obj.__dict__obj.__slots__ 中定义的,它将拦截所有 属性访问。使用 .__getattribute__() 方法的一个缺点是,所有访问都需通过该方法。如果您使用这种方法,并希望返回(或操作)属性的 “real” 值,则需要进行少量特殊的编程:通常可通过对超类(一般为 object)调用 .__getattribute__() 实现。例如:


清单 5. 返回一个 “real” .__getattribute__ value


>>> class Foo4(object):
... def __getattribute__(self, name):
... try:
... return object.__getattribute__(self, name)
... except:
... return "Value of %s" % name
...
>>> foo4 = Foo4()
>>> foo4.x = 'x'
>>> foo4.x
'x'
>>> foo4.y
'Value of y'

在 Python 的所有版本中,.__setattr__().__delattr__() 还拦截了所有对属性的写入和删除访问,而不仅仅是 obj.__dict__ 缺少的那些访问。


描述符

通过枚举的方式,我们逐一介绍了如何使属性的行为类似于方法。通过使用这些方法,您可以检查被访问、赋值或删除的特定属性名。事实上,如果愿意的 话,可以 通过正则表达式或其他计算检查这些属性名。理论上讲,您可以制定任何类型的运行时决策,确定如何处理某些给定的伪属性。例如,假设您并不想对属性名和字符 串模式进行比较,而只是想查明具有该属性名的属性是否一直保存在持久性数据库中。

然而,很多时候,您仅希望以某种特殊的方式使用少数属性,而其他属性则按照普通属性操作。这些普通属性不会触发任何特殊代码,也不会因为遍历方法代码而浪费时间。在这些情况下,您可以对属性使用描述符。或者,定义与描述符密切关联的特性(property)。实际上,特性和描述符基本是同一类东西,但是定义语法却截然不同。并且由于定义类型存在差别,正如您所料,特性和描述符各有优缺点。

让我们首先查看描述符。其原理就是将某种特殊类型的类的实例指派给另一个类的属性。这个特殊的 “描述符” 类是一种新式类,包含的方法有 .__get__().__set__()__delete__()(或者至少包含其中的几种)。如果描述符类至少实现了前两个方法,则被称为 “数据描述符”;如果只实现了第一个方法,则被称为 “非数据描述符”。

非数据描述符最常用于返回一个可调用对象。某种意义上讲,非数据描述符通常是某种方法的一个好听的名字 —— 但是可以在运行时确定描述符访问所返回的特定方法。它将首先处理类似元类和修饰器等最棘手的内容,我在之前的文章中讨论过这些内容(参考 参考资料 中的链接)。当然,普通的方法也可以根据运行时条件确定要运行哪些代码,因此,关于在运行时确定 “方法” 处理的概念不存在什么特别新的内容。

无论如何,数据描述符更为常见,因此我将向您展示一个例子。这种描述符可以 返回可调用的内容 —— 毕竟 Python 函数或方法可以返回任何内容。但此处的示例仅处理简单的值(和副作用)。我们希望利用一些属性将动作记录到 STDERR:


清单 6. 数据描述符示例


>>> class ErrWriter(object):
... def __get__(self, obj, type=None):
... print >> sys.stderr, "get", self, obj, type
... return self.data
... def __set__(self, obj, value):
... print >> sys.stderr, "set", self, obj, value
... self.data = value
... def __delete__(self, obj):
... print >> sys.stderr, "delete", self, obj
... del self.data
>>> class Foo(object):
... this = ErrWriter()
... that = ErrWriter()
... other = 4
>>> foo = Foo()
>>> foo.this = 5
set <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> 5
>>> print foo.this
get <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> <class '__main__.Foo'>
5
>>> print foo.other
4
>>> foo.other = 6
>>> print foo.other
6

Foo 类将 thisthat 定义为 ErrWriter 类的描述符。属性 other 只是一个普通的类属性。在第一次访问 foo.other 时,我们将读取类属性;对其赋值后,将读取实例属性。类属性仍然存在,只是被隐藏了,例如:


清单 7. 类属性与实例属性的对比

>>> foo.other
6
>>> foo.__class__.other
4

相比之下,即使可以通过实例进行访问,描述符仍然属于类级别对象。这通常对描述符起到不好的影响,使它类似于一个单例模式(singleton)。例如:


清单 8. 单例模式描述符

>>> foo2 = Foo()
>>> foo2.this
get <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> <class '__main__.Foo'>
5

要模拟普通的 “单实例” 行为,需要利用传递到 ErrWriter 方法中的 objobj 是具有描述符的实例。因此您可能会定义一个非单例模式的描述符,例如:


清单 9. 定义一个非单例模式的描述符

class ErrWriter(object):
def __init__(self):
self.inst = {}
def __get__(self, obj, type=None):
return self.inst[obj]
def __set__(self, obj, value):
self.inst[obj] = value
def __delete__(self, obj):
del self.inst[obj]


特性

特性的工作原理与描述符类似,但通常是在特定类的内部定义,而不是被创建为各种类都可使用的 “实用描述符”。与 “常规” 描述符一样,特性的工作原理就是定义 “getters”、“setters” 和 “deleters”。之后,使用特殊函数 property() 将这些方法转换为一种描述符。对于希望进一步了解这些内容的读者:property() 并不是一个真正的函数,而是一种类型 —— 因此不必过多考虑它。

奇怪地是,特性将我在上文中描述的 Ruby 编程语言的工作原理重新演绎了一遍。特性其实就是在语法上与属性类似的一种东西,通过定义所有 getters、setters 等就可定义特性。如果需要的话,您可以在 Python 中强行执行 “Ruby 式的规则”,并且永远不访问 “真正的” 属性。更可能的一种情况是,您将希望进行 “混合搭配(mix-and-match)”。下面举例说明了特性的工作原理。


清单 10. 特性的工作原理

class FooP(object):
def getX(self): return self.__x
def setX(self, value): self.__x = value
def delX(self): del self.__x
x = property(getX, setX, delX, "I'm the 'x' property.")

getter、setter 和 deleter 的名字没什么特别的约束。通常,您希望使用类似上文的可感知的名字。实际上没什么具体作用,但对属性名使用两个下划线比较合理。这些属性将使用普通的 Python 名字(针对 “半隐藏” 属性做了修改)附加到实例。而且,方法仍然保持可用性:


清单 11. 使用方法

>>> foop = FooP()
>>> foop.x = 'FooP x'
>>> foop.getX()
'FooP x'
>>> foop._FooP__x
'FooP x'
>>> foop.x
'FooP x'
 
阅读更多
换一批

没有更多推荐了,返回首页