属性的存取方法,是名称类似于 getHeight
和 setHeight
的方法,
用于获取或设置属性(这些属性可能是私有的)。如果访问给定属性时必须采取特定的措施,
那么像这样封装状态变量(属性)很重要。例如,请看下面的 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
,
它只能用于新式类。
随后将简单说明如何使用魔法方法来实现特性。
函数 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
执行的计算,但看起来就像普通属性一样。
注意:如果特性的行为怪异,务必确保使用的是新式类(通过直接或间接地继承 object
或直接设置 metaclass
)。
不然,特性的获取方法依然正常,但设置方法可能不正常(是否 如此取决于使用的Python版本)。这可能有点令人迷惑。
实际上,调用函数 property
时,还可不指定参数、指定一个参数、指定三个参数或指定四 个参数。
如果没有指定任何参数,创建的特性将既不可读也不可写。如果只指定一个参数(获 取方法),
创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法(这个方 法不接受任何参数)。
第四个参数也是可选的,指定一个文档字符串。这些参数分别名为 fget
、 fset
、fdel
和 doc
。
如果要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参 数来实现。
本节虽然很短(旨在说明函数 property
很简单),却非常重要。
这里要说明的是,对于新式类,应使用特性而不是存取方法。
令人很好奇的是,想知道特性是如何完成其魔法的,下面就来说一说。如果对此不感兴 趣,可跳过这些内容。
property
其实并不是函数,而是一个类。它的实例包含一些魔法方法,而所有的魔法都 是由这些方法完成的。
这些魔法方法为 get
、 set
和 delete
,它们一道定义了所谓 的描述符协议。
只要对象实现了这些方法中的任何一个,它就是一个描述符。描述符的独特 之处在于其访问方式。
例如,读取属性(具体来说,是在实例中访问类中定义的属性)时,如 果它关联的是一个实现了 get
的对象,
将不会返回这个对象,而是调用方法 get
并将其结果返回。实际上,
这是隐藏在特性、关联的方法、静态方法和类方法以及 super
后面的机制。
讨论旧的特性实现方式之前,先来说说另外两种实现方式类似于新式特性的功能。
静态方法和类方法是这样创建的:将它们分别包装在 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
的文档。
可以拦截对对象属性的所有访问企图,其用途之一是在旧式类中实现特性(在旧式类中,函数 property
的行为可能不符合预期)。
要在属性被访问时执行一段代码,必须使用一些魔法方法。
下面的四个魔法方法提供了需要的所有功能(在旧式类中,只需使用后面三个)。
getattribute (self, name)
:在属性被访问时自动调用(只适用于新式类)。getattr (self, name)
:在属性被访问而对象没有这样的属性时自动调用。setattr (self, name, value)
:试图给属性赋值时自动调用。delattr (self, name)
:试图删除属性时自动调用。
相比函数 property
,这些魔法方法使用起来要棘手些(从某种程度上说,效率也更低),
但它们很有用,因为可在这些方法中编写处理多个特性的代码。
然而,在可能的情况下,还是使 用函数 property
吧。
再来看前面的 Rectangle
示例,但这里使用的是魔法方法:
class Rectangle:
def __init__ (self):
self.width = 0
self.height = 0
def __setattr__(self, name, value):
if name == 'size':
self.width, self.height = value
else:
self. __dict__[name] = value
def __getattr__(self, name):
if name == 'size':
return self.width, self.height
else:
raise AttributeError()
这个版本需要处理额外的管理细节。对于这个代码示例,需要注意如下两点。
- 即便涉及的属性不是
size
,也将调用方法__setattr__
。因此这个方法必须考虑如下两种 情形:如果涉及的属性为size
,就执行与以前一样的操作;否则就使用魔法属性__dict__
。__dict__
属性是一个字典,其中包含所有的实例属性。之所以使用它而不是执行常规属性 赋值,是因为旨在避免再次调用__setattr__
,进而导致无限循环。 - 仅当没有找到指定的属性时,才会调用方法
__getattr__
。这意味着如果指定的名称不是size
, 这个方法将引发AttributeError
异常。这在要让类能够正确地支持hasattr
和getattr
等内置函数时很重要。 如果指定的名称为size
,就使用前一个实现中的表达式。
注意:前面说过,编写方法 __setattr__
时需要避开无限循环陷阱,编写 __getattribute__
时亦如此。
由于它拦截对所有属性的访问(在新式类中),因此将拦截对 __dict__
的访问! 在 __getattribute__
中 访问当 前实例的属性时,
唯一安全的方式 是使用超类 的方法 __getattribute__
(使用 super
)。