假设现在有这样一个需求:实现当获取或设置一个实例对象的属性时,为属性的访问添加额外的处理(如:类型检查或验证)。
针对上述问题,一个简单的方法是使用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_name
有setter
和deleter
属性?对于# 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
中定义了setter
和deleter
方法。
实际上,上述功能还可以通过另外一种方式实现:为类创建一个类属性,该类属性是一个由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