为了避免前面提到的所有问题,在 Python 在这个领域取得进展之前,我们需要考虑以
下几点。
• 应该避免多重继承:可以采用第 14 章介绍的一些设计模式来代替它。
• super 的使用必须一致:在类的层次结构中,要么全部用 super,要么全不用。
混用 super 和传统调用是一种混乱的做法。人们往往会避免使用 super,这样代
码会更清晰。
• 如果代码的使用范围包括 Python 2,在 Python 3 中也应该显式地继承自 object:
在 Python 2 中,没有指定任何祖先的类被认为是旧式类。在 Python 2 中应避免混
用旧式类和新式类。
• 调用父类时必须查看类的层次结构:为了避免出现任何问题,每次调用父类时,必
须快速查看有关的 MRO(使用__mro__)。
高级属性访问模式
许多 C++和 Java 程序员第一次学习 Python 时,他们会对 Python 没有 private 关键
字感到惊讶。与之最接近的概念是名称修饰(name mangling)。每当在一个属性前面加上
__前缀,解释器就会立刻将其重命名:
class MyClass:
__secret_value = 1
利用原始名称访问__secret_value 属性,将会引发 AttributeError 异常:
>>> instance_of = MyClass()
>>> instance_of.__secret_value
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__secret_value'
>>> dir(MyClass)
['_MyClass__secret_value', '__class__', '__delattr__', '__dict__', '__
dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__
setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> instance_of._MyClass__secret_value
1
Python 提供这一特性是为了避免继承中的名称冲突,因为属性被重命名为以类名为前
缀的名称。这并不是真正的锁定(real lock),因为可以通过其组合名称来访问该属性。这
一特性可用于保护某些属性的访问,但在实践中,永远不应使用__。如果一个属性不是公
有的,约定使用_前缀。这不会调用任何名称修饰的算法,而只是说明这个属性是该类的私
有元素,这是流行的写法。
Python 中还有其他可用的机制来构建类的公有部分和私有代码。应该使用描述符和
property 这些 OOP 设计的关键特性来设计一个清晰的 API。
描述符
描述符(descriptor)允许你自定义在引用一个对象的属性时应该完成的事情。
描述符是 Python 中复杂属性访问的基础。它在内部被用于实现 property、方法、类
方法、静态方法和 super 类型。它是一个类,定义了另一个类的属性的访问方式。换句话
说,一个类可以将属性管理委托给另一个类。
描述符类基于 3 个特殊方法,这 3 个方法组成了描述符协议(descriptor protocol):
• __set__(self, obj, type=None):在设置属性时将调用这一方法。在下面的
示例中,我们将其称为 setter。
• __get__(self, obj, value):在读取属性时将调用这一方法(被称为 getter)。
• __delete__(self, obj):对属性调用 del 时将调用这一方法。
实现了__get__()和__set__()的描述符被称为数据描述符(data descriptor)。如果只
实现了__get__(),那么就被称为非数据描述符(non-data descriptor)。
在每次属性查找中,这个协议的方法实际上由对象的特殊方法__getattribute__()
调用(不要与 __getattr__()弄混,后者用于其他目的)。每次通过点号(形式为
instance.attribute)或者 getattr(instance, 'attribute')函数调用来执行
这样的查找时,都会隐式地调用__getattribute__(),它按下列顺序查找该属性:
1.验证该属性是否为实例的类对象的数据描述符。
2.如果不是,就查看该属性是否能在实例对象的__dict__中找到。
3.最后,查看该属性是否为实例的类对象的非数据描述符。
换句话说,数据描述符优先于__dict__查找,而__dict__查找优先于非数据描述符。
为了表达得更清楚,下面是 Python 官方文档中的示例,给出了描述符在真实代码中的
工作方式:
class RevealAccess(object):
"""一个数据描述符,正常设定值并返回值,同时打印出记录访问的信息。
"""
def __init__(self, initval=None, name='var'):
self.val = initval
= name
def __get__(self, obj, objtype):
print('Retrieving', )
return self.val
def __set__(self, obj, val):
print('Updating', )
self.val = val
class MyClass(object):
x = RevealAccess(10, 'var "x"')
y = 5
下面是在交互式会话中的使用示例:
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
前一个例子清楚地表明,如果一个类的某个属性有数据描述符,那么每次查找这个属
性时,都会调用描述符的__get__()方法并返回它的值,每次对这个属性赋值时都会调用
__set__()。虽然前一个例子没有给出描述符__del__方法的例子,但现在也应该清楚了:
每次通过 del instance.attribute 语句或 delattr(instance, 'attribute')
调用删除一个实例属性时都会调用它。
由于上述原因,数据描述符和非数据描述符的区别很重要。Python 已经使用描述符协
议将类函数绑定为实例方法。它还支持了 classmethod 和 staticmethod 装饰器背后
的机制。事实上,这是因为函数对象也是非数据描述符,如下所示:
>>> def function(): pass
>>> hasattr(function, 'get')
True
>>> hasattr(function, '__set__')
False
对于 lambda 表达式创建的函数也是如此:
>>> hasattr(lambda: None, '__get__')
True
>>> hasattr(lambda: None, '__set__')
False
因此,如果没有__dict__优先于非数据描述符,我们将不可能在运行时在已经构建好
的实例上动态覆写特定的方法。幸运的是,多亏了 Python 描述符的工作方式。由于这一工
作方法,使得开发人员可以使用一种叫作猴子补丁(monkey-patching)的流行技术来改变
实例的工作方式,而不需要子类化。
现实例子—延迟求值属性
描述符的一个示例用法就是将类属性的初始化延迟到被实例访问时。如果这些属性的初始
化依赖全局应用上下文的话,那么这一点可能有用。另一个使用场景是初始化的代价很大,但
在导入类的时候不知道是否会用到这个属性。这样的描述符可以按照如下所示来实现:
class InitOnAccess:
def __init__(self, klass, *args, **kwargs):
self.klass = klass
self.args = args
self.kwargs = kwargs
self._initialized = None
def __get__(self, instance, owner):
if self._initialized is None:
print('initialized!')
self._initialized = self.klass(*self.args,
**self.kwargs)
else:
print('cached!')
return self._initialized
下面是示例用法:
>>> class MyClass:
... lazily_initialized = InitOnAccess(list, "argument")
...
m = MyClass()
>>> m.lazily_initialized
initialized!
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
>>> m.lazily_initialized
cached!
['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
PyPI上OpenGL的官方Python库PyOpenGL用到了相似的技术来实现lazy_property,
它既是装饰器又是数据描述符,如下所示:
class lazy_property(object):
def __init__(self, function):
self.fget = function
def __get__(self, obj, cls):
value = self.fget(obj)
setattr(obj, self.fget.__name__, value)
return value
这样的实现与使用 property 装饰器(稍后介绍)类似,但它所包装的函数仅执行一
次,然后类属性就被替换为它的返回值。当开发人员需要同时满足以下两点要求时,这种
技术通常很有用。
• 对象实例需要被保存为实例之间共享的类属性,以节约资源。
• 在全局导入时对象不能被初始化,因为其创建过程依赖某个全局应用状态/上下文。
对于使用 OpenGL 编写的应用来说,往往需要同时满足这两点要求。举个例子,在
OpenGL 中创建着色器的代价非常高,因为需要对 GLSL(OpenGL 着色语言)编写的代
码进行编译。合理的做法是只创建一次,然后将其定义放在需要用到它的类附近。另一方
面,如果没有对 OpenGL 上下文进行初始化,是无法执行着色器编译的,因此很难在全局
导入时在全局模块命名空间中可靠地定义并编译着色器。
下面的例子展示了 PyOpenGL 的 lazy_property 装饰器(这里是 lazy_class_
attribute)的修改版在某个虚构的基于 OpenGL 应用中的可能用法。为了在不同的类实例
之间共享属性,需要将加粗部分的代码修改为原始的 lazy_property 装饰器,如下所示:
import as gl
from import shaders
class lazy_class_attribute(object):
def __init__(self, function):
self.fget = function
def __get__(self, obj, cls):
value = self.fget(obj or cls)
# 注意:无论是类级别还是实例级别的访问
# 都要保存在类对象中,而不是保存在实例中
setattr(cls, self.fget.__name__, value)
return value
class ObjectUsingShaderProgram(object):
# trivial pass-through vertex shader implementation
VERTEX_CODE = """
#version 330 core
layout(location = 0) in vec4 vertexPosition;
void main(){
gl_Position = vertexPosition;
}
"""
# trivial fragment shader that results in everything
# drawn with white color
FRAGMENT_CODE = """
#version 330 core
out lowp vec4 out_color;
void main(){
out_color = vec4(1, 1, 1, 1);
}
"""
@lazy_class_attribute
def shader_program(self):
print("compiling!")
return shaders.compileProgram(
shaders.compileShader(
self.VERTEX_CODE, gl.GL_VERTEX_SHADER
),
shaders.compileShader(
self.FRAGMENT_CODE, gl.GL_FRAGMENT_SHADER
)
)
和所有 Python 高级语法特性一样,这一特性也应该谨慎使用,并在代码中详细说明。
对于没有经验的开发者而言,这种类行为的改变可能令人既困惑又意外,因为描述符影响
的是类行为最基本的内容(例如属性访问)。因此,如果描述符在项目代码库中发挥重要作
用的话,那么确保团队所有成员都熟悉并理解这一概念是很重要的。