Python @Property 装饰器


2021年8月23日, Learn eTutorial
2164

在本教程中,您将通过简单易懂的示例了解 @property 装饰器,以及何时以及如何在 Python 中使用 @property 装饰器。
通过我们之前的教程,我们知道装饰器的主要目的是修改函数或类属性的行为,而类的使用者无需进行任何更改。我们在上一个教程中处理的装饰器是用户自定义的装饰器。为了使装饰更加简单,Python 提供了三个基本的内置装饰器,如 @classmethod、@staticmethod 和 @property 方法。在本教程中,我们将重点关注 Python 的 @property 装饰器。

什么是 @property 装饰器?

@property 装饰器基本上是一个内置装饰器,其主要目的之一是将类方法用作属性,尽管它是一个带括号 () 的方法。.

让我们通过创建一个名为 License 的类来理解这一点。我们的 License 类包含 nameagedata 作为其属性。


class License:
    
    def __init__(self,name,age):
        self.name=name
        self.age=age
        self.data = '{} is {} years old'.format(self.name,self.age) 

 

现在我们已经创建了类,下一步是为 License 类创建一个实例。这里 L 是具有两个值的实例,借助点运算符(.),我们可以访问类中属性的值,如下所示。

现在我们已经创建了类,下一步是为 License 类创建一个实例。这里 L 是具有两个值的实例,借助点运算符(.),我们可以访问类中属性的值,如下所示。


L = License('Tom',25)
print(L.name)
print(L.age)
print(L.data)
 

输出


Tom
25
Tom is 25 years old

当您观察代码片段时,您会发现 self.nameself.age 是主属性,而 self.data 是派生属性。

何时使用 @property

假设在某个阶段,您希望更改类中任何属性的值,并期望您所做的更改会反映在程序中使用该属性(已更改的属性)的任何地方。但实际上,这种情况不会发生。

在我们的例子中,假设您希望在实例化后更改 age,为此我们将 L.age 赋值为 15,如下所示。


L = License('Tom',25)
print(L.name)

L.age = 15  #assigned new age
print(L.age)
print(L.data)
 

实际上,预期的输出应该是这样的:L.age15L.dataTom is 15 years old。但我们收到的输出是


Tom
15
Tom is 25 years old

在这里,您可以看到 age 属性更改为 15。但是 self.data 属性中的 age 保持不变,没有更新。
这个问题的原因是,在类中,如果您更改一个属性的值,从该类派生的其他属性不会自动更新。

因此,如果对类所做的更改不会在其派生类中自动更新,那么破坏代码的可能性很高。

那么我们如何解决这个问题呢?

属性到方法的转换

解决此问题的一种方法是将类中的 self.data 属性转换为方法,即 getdata(),如下例所示。


class License:
    
    def __init__(self,name,age):
        self.name=name
        self.age=age
         
    def getdata(self):
        return '{} is {} years old'.format(self.name,self.age )
 

现在,通过删除属性并添加新方法,该类已被修改。因此,实际上,我们的类现在没有名为 self.data 的属性,而是有一个方法 getdata()

由于 getdata() 是一个方法,我们在调用此方法时必须小心。因为如果我们不带括号调用该方法,如下所示,我们将会遇到属性错误。
 


L = License('Tom',25)   # Instance 
print(L.name)

L.age = 15
print(L.age)
print(L.getdata)   # missing parantheses,getdata is a method not an attribute

 

您将收到的输出将是


Tom
15
<bound method License.getdata of <__main__.License object at 0x038CB580>>

所以整个程序应该是这样的


class License:
    
    def __init__(self,name,age):
        self.name=name
        self.age=age
         
    def getdata(self):
        return '{} is {} years old'.format(self.name,self.age )
L = License('Tom',25)
print(L.name)

L.age = 15
print(L.age)
print(L.getdata())  #calling method

 

输出


Tom
15
Tom is 15 years old

简而言之,所有实现上述类的程序都必须在遇到 self.data 的地方将其代码修改为 getdata()。如果客户端代码有成百上千行,那肯定会很麻烦。

使用 @property 装饰器解决问题

因此,Python 提供的完美解决方案是将该方法转换为属性。这可以通过在方法定义之前添加一个 @property 装饰器来实现,它允许将方法用作属性。结果是,getdata() 方法可以作为属性访问,而不是带 () 的方法。以下示例说明了 @property 的使用。

 


class License:
    
    def __init__(self,name,age):
        self.name=name
        self.age=age

    @property 
    def getdata(self):
        return '{} is {} years old'.format(self.name,self.age )

    

L = License('Tom',25)
print(L.name)

L.age = 15
print(L.age)
print(L.getdata)  #calling method as attribute

 

这段代码片段会给你相同的输出,但在这里你可以看到我们在调用用 @property 装饰的方法时移除了括号 (),即 getdata()。因为通过使用 @property,getdata() 方法被作为属性访问,从而使我们的代码与客户端代码松散耦合。上述代码中的以下序列通常被称为 getter 方法,用于检索属性中的值。


@property 
    def getdata(self):
        return '{} is {} years old'.format(self.name,self.age )
 

Setter 方法

与 getter 方法一样,在 Python 中,我们有 setter 方法,用于为类中的属性设置值,从而确保数据封装的原则。
在定义 setter 方法时,您需要遵循一些规则,如下所列

  • setter 方法的语法是 @{方法名}.setter
  • setter 和 getter 方法的名称应该相同
  • 用户设置的值总是作为参数在 setter 方法中接受。

想象一下,我们的客户在他年满 18 岁时想要更改他的年龄,为了从客户端实现这一需求,我们需要在我们的类中引入 setter 方法属性,从而在客户不知情的情况下帮助他们。我们的程序也会相应地改变,如下所示


class License:
    
    def __init__(self,name,age):
        self.name=name
        self.__age=age

    @property 
    def getdata(self):
        return self.name,self.__age

    @getdata.setter
    def getdata(self,age):
        if age < 18:
            print('Sorry {}, You are ineligible for License.'.format(self.name))
            self.__age=age
        else:
            print('Congrats {},You are eligible for License'.format(self.name))
            self.__age=age
               

L1 = License('Tom',25)
print('Before setting',L1.getdata)

L1.getdata=15 #age set to 15
print('After setting values changed to',L1.getdata)

print('-'*30)

L2 = License('Chris',17)
print('Before setting',L2.getdata)

L2.getdata=18 #age set to 18
print('After setting values changed to',L2.getdata)

 

输出


Before setting ('Tom', 25)
Sorry Tom, You are ineligible for License.
After setting values changed to ('Tom', 15)
------------------------------
Before setting ('Chris', 17)
Congrats Chris,You are eligible for License
After setting values changed to ('Chris', 18)

在这个程序中,我们添加了 setter 方法来验证年龄。通过检查 age 是否低于 18 岁。如果年龄低于 18 岁,类将打印消息说该人没有资格获得驾照;如果年龄超过 18 岁,则该人有资格获得驾照。

语句 self._age = age 将客户端提供的任何更新年龄设置到私有属性中,从而提供了数据隐藏功能。在我们的输出中,您可以看到 Tom 的年龄最初是 25 岁,后来被客户端设置为 15 岁。这也反映在我们的私有属性中。

所以现在,每当对基属性进行更改时,我们的类将自动更新派生属性,反之亦然。

Deleter 方法

与 @property 相关的另一个方法是 deleter 方法,它用于从内存中删除属性。

让我们在我们的 getdata 中实现 deleter 方法。与 setter 方法类似,实现 deleter 的特殊语法是 @{方法名}.setter,应在 deleter 方法之前提及。deleter 方法的名称应与应用 @decorator 的方法名称相同。

在这里,我们计划从内存中删除不需要的数据。在我们的例子中,我们正在删除不符合驾照资格的人员的数据。现在,在我们之前的示例中,我们添加了 deleter 方法,如下例所示。


class License:
    
    def __init__(self,name,age):
        self.name=name
        self.__age=age

    @property 
    def getdata(self):
        return self.name,self.__age

    @getdata.setter
    def getdata(self,age):
        if age < 18:
            print('Sorry {}, You are ineligible for License.'.format(self.name))
            self.__age=age
        else:
            print('Congrats {},You are eligible for License'.format(self.name))
            self.__age=age
    
    @getdata.deleter
    def getdata(self):
        del self.name
        self.__age=None
             

L1 = License('Tom',25)
L2 = License('Chris',35)

print('Deleting....')
print(L1.getdata)
del L1.getdata

print(L2.name)
print(L1.age)
 

在这里,我们通过使用 del 关键字删除 name 属性来定义我们的 deleter 方法,另一种删除属性的方法是将属性 age 赋值为 None

我们使用 del 关键字删除了我们的一个实例 L1,为了确认删除,我们尝试打印该实例中的 age。以下输出将清除所有疑问。


Deleting....
('Tom', 25)
Chris
Traceback (most recent call last):
  File "C:\Users\TP-E540\Desktop\PY PRO\atpro_ex.py", line 188, in 
    print(L1.age)
AttributeError: 'License' object has no attribute 'age'

Property 函数

在 Python 中,我们有 property() 函数,可以用来代替 @property 装饰器。property() 函数是一个内置函数,它在 getter、setter 和 deleter 前面返回一个 property 属性。

语法
property() 的语法是


property(fget=None, fset=None, fdel=None, doc=None)
 

其中,

  • fget(): 是用于获取属性值的函数
  • fset(): 是用于设置属性值的函数
  • fdel(): 是用于删除属性值的函数
  • doc: 是用于属性文档的字符串

这四个函数参数是可选的。因此我们也可以简单地将 property() 创建为一个属性对象。

下面给出了一个简单的示例说明


class License:
    def __init__(self, name,age):
        self._name = name
        self._age = age

    def get_name(self):
        print('Getting name and age')
        return self._name ,self._age

    def set_name(self, val):
        print('Setting name to ', val)
        self._name = val

    def del_name(self):
        print('Deleting name and age')
        self._name = None
        self._age = None

    # Set property to use get_name, set_name
    # and del_name methods
    name = property(get_name, set_name, del_name, 'Data property')

L = License('Tom',25)
print(L.name)
L.name = 'Jerry'
del L.name

 

输出


Getting name and age
('Tom', 25)
Setting name to  Jerry
Deleting name and age