可爱的 Python: Python 之优雅与瑕疵,第 2 部分 z

本文探讨了Python中属性和方法的多种实现方式,包括描述符、特性等,旨在帮助程序员更好地理解和使用这些特性。

在这篇 分两部分的系列文章 中,David 讨论了添加到最近几个 Python 版本中的一些不那么引人注目的特性和不恰当特性,目的是向业余 Python 编程人员揭示 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'




回页首


结束语

在本期文章中,我展示了大量方法,使 Python 实例属性的行为类似于(或变成)方法调用,但是对于如何减少复杂性,我确实提不出什么明确的建议来。我希望能够告诉您从所介绍的方法中仅选择一种,将其他 方法作为次要选择或不常用的方法。遗憾地是,每种方法都各有优缺点。每种方法都特别针对特定的编程环境,即使各种方法的语法和语义截然不同。

此外,虽然本文没有对此进行介绍,我曾考虑过一些更加晦涩的方法,编程人员可以使用元类、类工厂和修饰器来获得本文介绍的六种 “标准” 技术所获得的类似效果。这些方法将真正查明 Python 元编程语言中一些隐秘区域。

如果本文所描述的方法全部可行就太好了,但是这些方法之间的变化将通过某种简单的方式参数化,而不是使用完全不同的语法和结构。Python 3000 的主要目标就是简化这些方法;但是有关如何统一和简化属性和方法方面我还没有看到任何具体建议。我想到的一个方法是 Python 可以对类启用修饰器(结合目前在方法和函数中的使用),同时提供一些标准的修饰器模块,用于最常见的 “魔法属性” 行为。这仅是一种推测,我不知道它实际工作起来怎样,但是我能想象得到这种方法可以向 Python 编程人员隐藏 95% 的复杂性,他们确实不希望过多地关注 Python 内部和那些古怪的符号。



参考资料

学习

<script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值