如何使用property掌控属性访问?

一、问题引入

假设现在有这样一个需求:实现当获取或设置一个实例对象的属性时,为属性的访问添加额外的处理(如:类型检查或验证)。

二、解决方案

1. 装饰器实现

针对上述问题,一个简单的方法是使用property类,如下列代码就使用了property类实现了:

  • 对实例属性的设置进行了类型检查;
  • 禁止了对实例属性进行删除的操作。
# property.py
class Person:
    def __init__(self, first_name):
        self.first_name = first_name  # 1

    # Getter function
    @property
    def first_name(self):
        return self._first_name

    # Setter function
    @first_name.setter  # 2
    def first_name(self, value):
        self._last_name = 'Eric'
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Deleter function(optional)
    @first_name.deleter  # 3
    def first_name(self):
        raise AttributeError("Can't delete an attribute")

在上述代码中,有三个和属性访问相关的方法,根据Python中property的语法规则,此三个方法名称必须相同:

  • 第一个方法是一个获取属性的函数,且其将first_name“变成了一个实例属性”;
  • 另外两个方法分别用于限制外界设置或删除first_name“属性”的行为

这里之所以将属性二字打引号,是因为虽然first_name实际是方法名,但是接下来我们可以看到,在使用时并不需要在first_name后加上(),所以在外界看来first_name像一个属性,如下列代码所示:

In[2]: from property import Person
In[3]: person = Person("Guido")
In[4]: person.first_name
Out[4]: 'Guido'
In[5]: person.first_name = 42
Traceback (most recent call last):
  ...
    raise TypeError('Expected a string')
TypeError: Expected a string
In[6]: del person.first_name
Traceback (most recent call last):
  ...
    raise AttributeError("Can't delete an attribute")
AttributeError: Can't delete an attribute

实际上,因为被外界获取的属性必定在实例对象中要有处可存,所以我们可以看到上述代码获取和设置属性的方法中用到了受保护属性_first_name

你可能对于上述代码还有这样的疑问:

  • # 1处:为什么在__init__()方法中是使用self.first_name而非self._first_name来保存创建实例对象时传入的参数呢?
  • # 2# 3处:为什么first_namesetterdeleter属性?

对于# 1处代码:因为使用property的很大程度上就是为了在设置属性时进行类型检查,所以我们必然也希望在实例化创建对象时也进行类型检查。因此,# 1处代码的含义实际上并非是赋值,而是自动触发了设置属性的方法,如下列代码所示:

# property.py
class Person:
    def __init__(self, first_name):
        self.first_name = first_name  # 5

    # Getter function
    @property
    def first_name(self):
        return self._first_name

    # Setter function
    @first_name.setter
    def first_name(self, value):
        print("Trying setting the value of _first_name...")  # 6
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Deleter function(optional)
    @first_name.deleter
    def first_name(self):
        raise AttributeError("Can't delete an attribute")


def main():
    person = Person("Guido")  # 4


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Trying setting the value of _first_name…

结合上述代码及其运行结果可知,虽然# 4处创建对象的语句会调用__init__()方法中# 5处的语句,但实际# 5处代码又调用了# 6处代码进行了_first_name属性的设置,并在此之前进行了类型检查。

对于# 2处代码:通过Python装饰器入门与简单应用了解过装饰器可以知道,下列代码:

@property
def first_name(self):
	return self._first_name

实际上相当于:

def first_name(self):
	return self._first_name
first_name = property(first_name)

即此时first_name是一个由property创建出来的实例对象,而property中定义了setterdeleter方法。

2. 类属性实现

实际上,上述功能还可以通过另外一种方式实现:为类创建一个类属性,该类属性是一个由property类创建的对象,且在创建该对象时传入的参数依次为:获取属性的方法、设置属性的方法、删除属性的方法,即:

# property_attr.py
class Person:
    def __init__(self, first_name):
        self.__first_name = None
        self.set_first_name(first_name)

    def get_first_name(self):
        return self.__first_name

    def set_first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self.__first_name = value

    def del_first_name(self):
        raise AttributeError("Can't delete attribute...")

    name = property(get_first_name, set_first_name, del_first_name)

使用上述代码的Person类进行对象创建时的属性设置、属性修改、删除属性的结果为:

In[2]: from property_attr import Person
In[3]: person = Person("Guido")
In[4]: person.name
Out[4]: 'Guido'
In[5]: person.name = 18
Traceback (most recent call last):
 ...
TypeError: Expected a string
In[6]: del person.name
Traceback (most recent call last):
  ...
AttributeError: Can't delete attribute...

三、注意事项

需要注意的是,property上述访问属性的功能不可被滥用,只有当你的确需要在访问属性做额外的处理时才建议使用,一些熟悉Java的程序员可能会写出如下列所示的代码:

# verbose_property.py
class Person:
    def __init__(self, first_name):
        self.first_name = first_name

    @property
    def first_name(self):
        return self.__first_name

    @first_name.setter
    def first_name(self, value):
        self.__first_name = value

上述代码实际上非但没能为代码添加诸如类型检查等功能,反而给人一种画蛇添足之感,也会降低代码的运行速度。

实际上,property还可以用于定义需计算得到的“属性”,这种属性不会被保存在实例对象中,而是会在需要时候计算出来:

# circle.py
import math


class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def perimeter(self):
        return 2 * math.pi * self.radius


def main():
    circle = Circle(4.0)
    print('circle.radius = ', circle.radius)
    print('circle.area = ', circle.area)
    print('circle.perimeter = ', circle.perimeter)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

circle.radius = 4.0
circle.area = 50.26548245743669
circle.perimeter = 25.132741228718345

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值