本系列 第一期文章 讨论了序列和比较。本期文章将以这些主题为基础展开讨论。
在大多数面向对象语言中,方法和属性几乎相同(但并非完全相同)。两者都可以附加到类和/或实例。除了实现细节外,存在一个关键区别:当附加到对象时,您可以调用方法 发起动作和计算;而属性 仅具有一些可被检索(或者修改)的值。
对于某些语言(例如 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 类将 this 和 that 定义为 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 方法中的 obj。obj 是具有描述符的实例。因此您可能会定义一个非单例模式的描述符,例如:
清单 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]