property类返回 property 属性,其语法格式:
#fget 是获取属性值的函数。 fset 是用于设置属性值的函数。 fdel 是用于删除属性值的函数。并且 doc 为属性对象创建文档字符串。
class property(fget=None, fset=None, fdel=None, doc=None)
一个典型的用法是定义一个托管属性 x:
class C:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
如果 c 是 C 的实例,c.x
将调用getter,c.x = value
将调用setter, del c.x
将调用deleter。
这使得用 property() 作为 decorator装饰器 来创建只读的特征属性可以很容易地实现:
class Parrot:
def __init__(self):
self._voltage = 100000
@property
def voltage(self):
"""获取当前电压。"""
return self._voltage
以上 @property
装饰器会将 voltage()
方法转化为一个具有相同名称的只读属性的 "getter",并将 voltage 的文档字符串设置为 "获取当前电压。",即给出了doc。
如果给出doc,doc 将成为该 property 属性的文档字符串,否则该 property 将拷贝 fget 的文档字符串(如果存在)。
特征属性对象具有 getter
, setter
以及 deleter
方法,它们可用作装饰器来创建该特征属性的副本,并将相应的访问函数设为所装饰的函数。
这最好是用一个例子来解释:
class C:
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
上述代码与第一个例子完全等价。 注意一定要给附加函数与原始的特征属性相同的名称 (在本例中为 x
)
返回的特征属性对象同样具有与构造器参数相对应的属性 fget
, fset
和 fdel
。现版本的特征属性对象的文档字符串现在是可写的。
内置装饰器:property
property是内建
装饰器,它通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。
通常我们给实例绑定属性是这样的:
class Student(object):
def __init__(self, name, age=None):
self.name = name
self.age = age
# 实例化
XiaoMing = Student("小明")
# 添加属性
XiaoMing.age=25
# 查询属性
XiaoMing.age
# 删除属性
del XiaoMing.age
但是稍有经验的开发人员,一下就可以看出,这样直接把属性暴露出去,为了 能对属性的值做合法性限制。我们可以这样写:
class Student(object):
def __init__(self, name):
self.name = name
def set_age(self, age):
if not isinstance(age, int):
raise ValueError('输入不合法:年龄必须为数值!')
if not 0 < age < 100:
raise ValueError('输入不合法:年龄范围必须0-100')
self._age=age
def get_age(self):
return self._age
def del_age(self):
self._age = None
XiaoMing = Student("小明")
# 添加属性
XiaoMing.set_age(25)
# 查询属性
XiaoMing.get_age()
# 删除属性
XiaoMing.del_age()
上面的代码设计虽然可以变量的定义,但是可以发现不管是获取还是赋值(都是通过函数)都和我们平时见到的不一样。
按照我们思维习惯应该是这样的:
# 赋值
XiaoMing.age = 25
# 获取
XiaoMing.age
那么这样的方式我们如何实现呢。请看下面的代码。
class Student(object):
def __init__(self, name):
self.name = name
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise ValueError('输入不合法:年龄必须为数值!')
if not 0 < value < 100:
raise ValueError('输入不合法:年龄范围必须0-100')
self._age=value
@age.deleter
def age(self):
del self._age
XiaoMing = Student("小明")
# 设置属性
XiaoMing.age = 25
# 查询属性
XiaoMing.age
# 删除属性
del XiaoMing.age
用@property
装饰过的函数,会将被装饰函数定义成一个属性,属性的值就是该函数return的内容。同时,会将这个函数变成另外一个装饰器。就像后面我们使用的@age.setter
和@age.deleter
。
@age.setter
使得我们可以使用XiaoMing.age = 25
这样的方式直接赋值。@age.deleter
使得我们可以使用del XiaoMing.age
这样的方式来删除属性。
property 的底层实现机制是「描述符」,这里也介绍一下吧。
如下,我写了一个类,里面使用了 property 将 math 变成了类实例的属性
class Student:
def __init__(self, name):
self.name = name
@property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("有效值必须为[0,100]")
为什么说 property 底层是基于描述符协议的呢?通过 PyCharm 点击进入 property 的源码,很可惜,只是一份类似文档一样的伪源码,并没有其具体的实现逻辑。
不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。
这里我自己通过模仿其函数结构,结合「描述符协议」来自己实现类 property
特性。
代码如下:
class TestProperty(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
print("in __get__")
if obj is None:
return self
if self.fget is None:
raise AttributeError
return self.fget(obj)
def __set__(self, obj, value):
print("in __set__")
if self.fset is None:
raise AttributeError
self.fset(obj, value)
def __delete__(self, obj):
print("in __delete__")
if self.fdel is None:
raise AttributeError
self.fdel(obj)
def getter(self, fget):
print("in getter")
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
print("in setter")
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
print("in deleter")
return type(self)(self.fget, self.fset, fdel, self.__doc__)
然后 Student 类,我们也相应改成如下
class Student:
def __init__(self, name):
self.name = name
# 其实只有这里改变
@TestProperty
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("有效值必须为[0,100]")
为了便于理解,这里做两点说明:
- 使用类装饰器
TestProperty
装饰后,math
不再是一个方法,而是TestProperty
类的一个实例。所以第二个math函数可以使用math.setter
来装饰,本质是调用TestProperty.setter
来产生一个新的TestProperty
实例赋值给第二个math
。 - 第一个
math
和第二个math
是两个不同TestProperty
实例。但他们都属于同一个描述符类(TestProperty),当对 math 赋值时,就会进入TestProperty.__set__
,当对math 进行取值里,就会进入TestProperty.__get__
。仔细一看,其实最终访问的还是Student实例的_math
属性。
说了这么多,还是运行一下,更加直观一点。
# 运行后,会直接打印这一行,这是在实例化 TestProperty 并赋值给第二个math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90