不过这是一种很糟糕的解决方法,因为它使得所有构造函数都接受任何类型的参数。 这会导致代码变得脆弱,因为任何参数都可以传入并通过。另一种解决方法是在 MyClass 中显式地使用特定类的__init__()调用,但这又会导致第一种错误。

3.3 高级属性访问模式 73

 3.2.4 最佳实践

为了避免前面提到的所有问题,在 Python 在这个领域取得进展之前,我们需要考虑以 下几点。

• 应该避免多重继承:可以采用第 14 章介绍的一些设计模式来代替它。

• super的使用必须一致:在类的层次结构中,要么全部用super,要么全不用。 混用 super 和传统调用是一种混乱的做法。人们往往会避免使用 super,这样代

码会更清晰。

• 如果代码的使用范围包括 Python 2,在 Python 3 中也应该显式地继承自 object:

在Python 2中,没有指定任何祖先的类被认为是旧式类。在Python 2中应避免混

用旧式类和新式类。

• 调用父类时必须查看类的层次结构:为了避免出现任何问题,每次调用父类时,必

须快速查看有关的 MRO(使用__mro__)。 3.3 高级属性访问模式

许多 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

Python 提供这一特性是为了避免继承中的名称冲突,因为属性被重命名为以类名为前 缀的名称。这并不是真正的锁定(real lock),因为可以通过其组合名称来访问该属性。这 一特性可用于保护某些属性的访问,但在实践中,永远不应使用__。如果一个属性不是公 有的,约定使用_前缀。这不会调用任何名称修饰的算法,而只是说明这个属性是该类的私 有元素,这是流行的写法。

Python 中还有其他可用的机制来构建类的公有部分和私有代码。应该使用描述符和 property 这些 OOP 设计的关键特性来设计一个清晰的 API。

3.3.1 描述符

描述符(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): """一个数据描述符,正常设定值并返回值,同时打印出记录访问的信息。 """

3.3 高级属性访问模式 75

def __init__(self, initval=None, name='var'): self.val = initval

self.name = name

def __get__(self, obj, objtype): print('Retrieving', self.name) return self.val

def __set__(self, obj, val): print('Updating', self.name) 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') 调用删除一个实例属性时都会调用它。