Python中的@property与Descriptor的方法可以为属性添加约束或进行参数检查, 方便简洁, 而网络上的介绍相对较为杂乱, 本文对它们做一个清晰的梳理.
一. 典型例子:
我们通常使用类定义一些实体, 比如学生成绩(0~100分):
class student(object):
def __init__(self, score):
self.score = score
有了类定义, 老师可以方便的录入每位学生成绩, 但有一个很大的缺陷: 程序允许录入负数, 或超过100的分数. 为了解决这个问题, 有一个简单的解决方案:
class student(object):
def __init__(self, score):
if not isinstance(score, int):
raise ValueError('score must be an integer')
if score < 0 or score > 100:
raise ValueError('score must between 0~100')
self.score = score
这样一来, 如果成绩录入为非整数或者不在0~100分之间的成绩时, 程序提示ValueError. 可是如果使用者要进行成绩修改, 函数__init__中的限制将不起任何作用, 成绩还是可能被修改为不正确的值.
Jack = Student()
Jack.score = 9999
这类问题称为参数检查问题, 解决这类问题, 有几种方法, 本文将由浅入深一一介绍.
二. 私有属性+类方法
程序可以通过属性访问限制与类方法相结合的方式解决问题:
class student(object):
def get_score(self):
return self.__score
def set_score(self, score):
if not isinstance(score, int):
raise ValueError('score must be an integer')
if score < 0 or score > 100:
raise ValueError('score must between 0~100')
self.__score = score
实例变量名如果以__开头,就变成了私有变量, 外部不能直接访问(系统讲变量名进行了修改变为了_student__score), 只能通过类方法的方式访问或修改属性. 如此无论是初始化还是成绩修改都必须经过参数检查.
三. @property
通过以上方案虽然解决了问题, 但调用类方法丢失了操作的便捷性, 并且如果有其他属性也要做检查时, 要重新定义这些方法, 代码将臃肿不堪. Python提供了一种装饰器的方法, 可以对类动态的添加功能. @property便可以把函数调用伪装成属性的访问, 这样既进行了参数检查, 又可以方便的调用或修改属性值.
class student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, score):
if not isinstance(score, int):
raise ValueError('score must be an integer')
if score < 0 or score > 100:
raise ValueError('score must between 0~100')
self._score = score
jack = student()
jack.score = 99
print jack.score
四. Descriptor
@property的方法虽然简单有效, 但它们不能被重复使用, 如果除了score需要进行参数检查, 学生年龄也许要检查, 则需要定义多个@property的方法, 代码重用效率较低. 类内代码无法做到简介漂亮. descriptor是property的升级版, 允许为重复的参数检查编写单独的类来处理. descriptor的工作流程如下:
class ParameterCheck(object):
def __init__(self, score):
self.default = score
def __get__(self, instance, owner):
return self.default
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('value must be an integer')
if value < 0 or value > 100:
raise ValueError('value must between 0~100')
self.default = value
class student(object):
score = ParameterCheck(0)
age = ParameterCheck(0)
Jack = student()
Jack.score = 80
Jack.age = 20
print Jack.score
print Jack.age
ParameterCheck就是上文所谓的descripor, 其中__get__/__set__方法是描述符方法, 当解释器遇到print Jack.score时, 它就会自动把score当做一个带有__get__方法的描述符, 调用student.score.__get__方法并返回default值, 而赋值时, __set__方法的调用过程类似, 另外还存在__det__方法, 调用del Jack.age时, 自动删除属性.
五. Descripor中的代码坑
由于描述符是以单独类的形式出现的, 所以可以有效的解决了代码@property臃肿的问题. 但不要认为descriptor如此简单, 因为Python语言本身设计的结构, 这里有几个代码坑需要注意:
1. 描述符必须在class level的位置
class ParameterCheck(object):
def __init__(self, score):
self.default = score
def __get__(self, instance, owner):
return self.default
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('value must be an integer')
if value < 0 or value > 100:
raise ValueError('value must between 0~100')
self.default = value
class student(object):
def __init__(self):
self.score = ParameterCheck(0)
Jack = student()
Jack.score = 800
print "Jack.score = %s" % Jack.score
以上代码片将描述符放到__init__的函数中, 由打印结果看出, 程序并没有进行描述符中的参数检查. 所以描述符必须放到类的层次上才能正常工作, 不然python无法自动调用描述符中的方法.
2. 确保实例的数据只属于实例本身
其实, Descriptor那一节为了简单起见, 描述符的内容写的过于随便, 当然在那一节中程序并没有问题, 但如果实例化两个学生的成绩
Jack = student()
Tom = student()
Jack.score = 88
print "'" * 10, "result", "'" * 10
print "Jack.score =", Jack.score
Tom.score = 59
print "Jack.score =", Jack.score
'''''''''' result ''''''''''
Jack.score = 88
Jack.score = 59
从运行结果可以看出, 修改实例Tom的分数会造成Jack分数的变动, 这也是Python设计让人觉得不爽的一处, 解决方法之一是使用一个字典单独保存专属于实例的数据:
from weakref import WeakKeyDictionary
class ParameterCheckDict(object):
def __init__(self, score):
self.default = score
self.InstanceDict = WeakKeyDictionary()
def __get__(self, instance, owner):
return self.InstanceDict.get(instance, self.default)
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('value must be an integer')
if value < 0 or value > 100:
raise ValueError('value must between 0~100')
self.InstanceDict[instance] = value
# print self.InstanceDict.items()
class student(object):
score = ParameterCheckDict(0)
Jack = student()
Tom = student()
Jack.score = 88
print "'" * 10, "result", "'" * 10
print "Jack.score =", Jack.score
Tom.score = 59
print "Jack.score =", Jack.score
'''''''''' result ''''''''''
Jack.score = 88
Jack.score = 88
PS: 使用WeakKeyDictionary的目的是为了防止内存泄露.
3. 描述符的所有者为unhashable对象
由于字典自身的特性, 以下代码段将报错:
class student(list):
score = ParameterCheckDict(0)
Jack = student()
print "'" * 10, "result", "'" * 10
print Jack.score
'''''''''' result ''''''''''
TypeError: unhashable type: 'student'
这是因为student是list的子类, Jack实例是不可哈希的, 因此不可作为Jack.score.InstanceDict的key. 解决这个问题最好的方法是给描述符加标签:
class ParameterCheckLabel(object):
def __init__(self, label):
self.label = label
def __get__(self, instance, owner):
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('value must be an integer')
if value < 0 or value > 100:
raise ValueError('value must between 0~100')
instance.__dict__[self.label] = value
# print self.InstanceDict.items()
class student(list):
score = ParameterCheckLabel('score')
Jack = student()
Tom = student()
Jack.score = 30
print "'" * 10, "result", "'" * 10
Tom.score = 40
print Jack.score
以上代码片确实解决了字典存储的问题, 同样是字典存储, 之前的字典存储的Key是instance(例如Jack), 这里的字典存储的Key是属性值(例如score), 请求Jack.score时, 系统自动调用Jack.__dict__['score'], 但这段代码是特别脆弱的:
class student(list):
score = ParameterCheckLabel("value")
Jack = student()
Jack.score = 30
print "'" * 10, "result", "'" * 10
Jack.value = 40
print Jack.score
'''''''''' result ''''''''''
40
虽然如此, 当遇到不可哈希的对象时, 这种方法还是很普遍. 由于此类问题可能造成不必要的麻烦, 可以采用元类的方法将属性的标签自动定义为属性变量:
class ParameterCheckLabel(object):
def __init__(self):
self.label = None
def __get__(self, instance, owner):
return instance.__dict__.get(self.label, None)
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('value must be an integer')
if value < 0 or value > 100:
raise ValueError('value must between 0~100')
instance.__dict__[self.label] = value
class DescriptorOwner(type):
def __new__(cls, name, bases, attrs):
for n, v in attrs.items():
if isinstance(v, ParameterCheckLabel):
v.label = n
return super(DescriptorOwner, cls).__new__(cls, name, bases, attrs)
class student(list):
__metaclass__ = DescriptorOwner
score = ParameterCheckLabel()
Jack = student()
Jack.score = 30
print "'" * 10, "result", "'" * 10
print Jack.score
'''''''''' result ''''''''''
30
但这样大大增加了代码的复杂度, 要不要采用元类的方法视情况而定.
六. 属性作为字典Key__求批评指正
对于unhashable的对象, 既然不能作为字典的key值, 那么我们换一种思路, 把实例的属性对象直接作为字典的key值, 这样不论对象是不是可哈希的, 都可以用描述符了, 也不存在上文所提到的为属性打标签的问题. 代码如下:
class ParameterCheckLabel(object):
def __get__(self, instance, owner):
return instance.__dict__.get(self)
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('value must be an integer')
if value < 0 or value > 100:
raise ValueError('value must between 0~100')
instance.__dict__[self] = value
class student(list):
score = ParameterCheckLabel()
age = ParameterCheckLabel()
Jack = student()
Jack.score = 30
print "'" * 10, "result", "'" * 10
Jack.age = 90
print "Jack.age =", Jack.age
print "Jack.score =", Jack.score
'''''''''' result ''''''''''
Jack.age = 90
Jack.score = 30
以上是最近本人对Python一些技巧的理解, 有什么不妥之处望批评指正, 欢迎与大家一起探讨!