魔法方法、特性和迭代器
9.4 其他魔法方法
>>> cl.counter 2
如你所见,CounterList的行为在大多数方面都类似于列表,但它有一个counter属性(其初 始值为0)。每当你访问列表元素时,这个属性的值都加1。执行加法运算cl[4] + cl[2]后,counter 的值递增两次,变成了2。
9.4 其他魔法方法
特殊(魔法)名称的用途很多,前面展示的只是冰山一角。魔法方法大多是为非常高级的用 途准备的,因此这里不详细介绍。然而,如果你感兴趣,可以模拟数字,让对象像函数一样被调 用,影响对象的比较方式,等等。要更详细地了解有哪些魔法方法,可参阅“Python Reference Manual”的Special method names一节。
9.5 特性
第7章提到了存取方法,它们是名称类似于getHeight和setHeight的方法,用于获取或设置属 性(这些属性可能是私有的,详情请参阅7.2.4节)。如果访问给定属性时必须采取特定的措施,
那么像这样封装状态变量(属性)很重要。例如,请看下面的Rectangle类:
class Rectangle:
def __init__(self):
self.width = 0 self.height = 0 def set_size(self, size):
self.width, self.height = size def get_size(self):
return self.width, self.height
下面的示例演示了如何使用这个类:
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.get_size() (10, 5)
>>> r.set_size((150, 100))
>>> r.width 150
get_size和set_size是假想属性size的存取方法,这个属性是一个由width和height组成的元 组。(可随便将这个属性替换为更有趣的属性,如矩形的面积或其对角线长度。)这些代码并非完 全错误,但存在缺陷。使用这个类时,程序员应无需关心它是如何实现的(封装)。如果有一天 你想修改实现,让size成为真正的属性,而width和height是动态计算出来的,就需要提供用于访
问width和height的存取方法,使用这个类的程序也必须重写。应让客户端代码(使用你所编写
代码的代码)能够以同样的方式对待所有的属性。
那么如何解决这个问题呢?给所有的属性都提供存取方法吗?这当然并非不可能,但如果有 大量简单的属性,这样做就不现实(而且有点傻),因为将需要编写大量这样的存取方法,除了 获取或设置属性外什么都不做。这将引入复制并粘贴(重复代码)的坏味,显然很糟糕(虽然在 有些语言中,这样的问题很常见)。所幸Python能够替你隐藏存取方法,让所有的属性看起来都 一样。通过存取方法定义的属性通常称为特性(property)。
在Python中,实际上有两种创建特定的机制,我将重点介绍较新的那种——函数property, 它只能用于新式类。随后,我将简单说明如何使用魔法方法来实现特性。
9.5.1 函数 property
函数property使用起来很简单。如果你编写了一个类,如前一节的Rectangle类,只需再添加
一行代码。
class Rectangle:
def __init__ (self):
self.width = 0 self.height = 0 def set_size(self, size):
self.width, self.height = size def get_size(self):
return self.width, self.height size = property(get_size, set_size)
在这个新版的Rectangle中,通过调用函数property并将存取方法作为参数(获取方法在前,
设置方法在后)创建了一个特性,然后将名称size关联到这个特性。这样,你就能以同样的方式 对待width、height和size,而无需关心它们是如何实现的。
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.size (10, 5)
>>> r.size = 150, 100
>>> r.width 150
如你所见,属性size依然受制于get_size和set_size执行的计算,但看起来就像普通属性 一样。
1 2 3 4 5
15 6 7 8 9 10 11 12 13 14
注意 如果特性的行为怪异,务必确保你使用的是新式类(通过直接或间接地继承object或直
接设置__metaclass__)。不然,特性的获取方法依然正常,但设置方法可能不正常(是否
如此取决于使用的Python版本)。这可能有点令人迷惑。
实际上,调用函数property时,还可不指定参数、指定一个参数、指定三个参数或指定四 个参数。如果没有指定任何参数,创建的特性将既不可读也不可写。如果只指定一个参数(获 取方法),创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法(这个方 法不接受任何参数)。第四个参数也是可选的,指定一个文档字符串。这些参数分别名为fget、 fset、fdel和doc。如果你要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参 数来实现。
本节虽然很短(旨在说明函数property很简单),却非常重要。这里要说明的是,对于新式 类,应使用特性而不是存取方法。
函数property的工作原理
你可能很好奇,想知道特性是如何完成其魔法的,下面就来说一说。如果你对此不感兴 趣,可跳过这些内容。
property其实并不是函数,而是一个类。它的实例包含一些魔法方法,而所有的魔法都
是由这些方法完成的。这些魔法方法为__get__、__set__和__delete__,它们一道定义了所谓 的描述符协议。只要对象实现了这些方法中的任何一个,它就是一个描述符。描述符的独特 之处在于其访问方式。例如,读取属性(具体来说,是在实例中访问类中定义的属性)时,如 果它关联的是一个实现了__get__的对象,将不会返回这个对象,而是调用方法__get__并将 其结果返回。实际上,这是隐藏在特性、关联的方法、静态方法和类方法(详细信息请参阅下 一小节)以及super后面的机制。
有关描述符的详细信息,请参阅Descriptor HowTo Guide(https://docs.python.org/3/howto/
descriptor.html)。
9.5.2 静态方法和类方法
讨论旧的特性实现方式之前,先来说说另外两种实现方式类似于新式特性的功能。静态方法 和类方法是这样创建的:将它们分别包装在staticmethod和classmethod类的对象中。静态方法的 定义中没有参数self,可直接通过类来调用。类方法的定义中包含类似于self的参数,通常被命 名为cls。对于类方法,也可通过对象直接调用,但参数cls将自动关联到类。下面是一个简单的 示例:
class MyClass:
def smeth():
print('This is a static method') smeth = staticmethod(smeth)
def cmeth(cls):
print('This is a class method of', cls) cmeth = classmethod(cmeth)
像这样手工包装和替换方法有点繁琐。在Python 2.4中,引入了一种名为装饰器的新语法,
可用于像这样包装方法。(实际上,装饰器可用于包装任何可调用的对象,并且可用于方法和函 数。)可指定一个或多个装饰器,为此可在方法(或函数)前面使用运算符@列出这些装饰器(指 定了多个装饰器时,应用的顺序与列出的顺序相反)。
class MyClass:
@staticmethod def smeth():
print('This is a static method') @classmethod
def cmeth(cls):
print('This is a class method of', cls)
定义这些方法后,就可像下面这样使用它们(无需实例化类):
>>> MyClass.smeth() This is a static method
>>> MyClass.cmeth()
This is a class method of <class '__main__.MyClass'>
在Python中,静态方法和类方法以前一直都不太重要,主要是因为从某种程度上说,总是可 以使用函数或关联的方法替代它们,而且早期的Python版本并不支持它们。因此,虽然较新的代 码没有大量使用它们,但它们确实有用武之地(如工厂函数),因此你或许应该考虑使用它们。
注意 实际上,装饰器语法也可用于特性,详情请参阅有关函数property的文档。