>>> from env_helper import info; info()
页面更新时间: 2024-01-19 23:30:40
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-17-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

9.7. 属性

属性的存取方法,它们是名称类似于 getHeightsetHeight 的方法, 用于获取或设置属性(这些属性可能是私有的)。如果访问给定属性时必须采取特定的措施, 那么像这样封装状态变量(属性)很重要。例如,请看下面的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 成为真正的属性,而 widthheight 是动态计算出来的, 就需要提供用于访问 widthheight 的存取方法,使用这个类的程序也必须重写。 应让客户端代码(使用你所编写 代码的代码)能够以同样的方式对待所有的属性。

那么如何解决这个问题呢?给所有的属性都提供存取方法吗?这当然并非不可能,但如果有 大量简单的属性,这样做就不现实(而且有点傻),因为将需要编写大量这样的存取方法,除了 获取或设置属性外什么都不做。这将引入复制并粘贴(重复代码)的坏味,显然很糟糕(虽然在 有些语言中,这样的问题很常见)。所幸Python能够替你隐藏存取方法,让所有的属性看起来都 一样。通过存取方法定义的属性通常称为特性(property)。

在 Python 中,实际上有两种创建特定的机制,我将重点介绍较新的那种——函数 property , 它只能用于新式类。 随后,我将简单说明如何使用魔法方法来实现特性。

9.7.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执行的计算,但看起来就像普通属性一样。

注意 如果特性的行为怪异,务必确保你使用的是新式类(通过直接或间接地继承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.7.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的文档。

可以拦截对对象属性的所有访问企图,其用途之一是在旧式类中实现特性(在旧式类中,函 数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 )。