为了避免前面提到的所有问题,在 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 中创建着色器的代价非常高,因为需要对 GLSLOpenGL 着色语言)编写的代

码进行编译。合理的做法是只创建一次,然后将其定义放在需要用到它的类附近。另一方

面,如果没有对 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 高级语法特性一样,这一特性也应该谨慎使用,并在代码中详细说明。

对于没有经验的开发者而言,这种类行为的改变可能令人既困惑又意外,因为描述符影响

的是类行为最基本的内容(例如属性访问)。因此,如果描述符在项目代码库中发挥重要作

用的话,那么确保团队所有成员都熟悉并理解这一概念是很重要的。