python中@property与Descriptor的使用方法

本文详细介绍了Python中使用@property和Descriptor进行属性约束和参数检查的方法,从私有属性+类方法到@property装饰器,再到Descriptor的使用,逐步揭示了更高级的代码组织方式。同时,文章还探讨了Descriptor中的常见代码陷阱,包括描述符的位置、实例数据管理和不可哈希对象的问题,最后提出了将属性作为字典Key的解决方案,以应对不可哈希对象的挑战。
摘要由CSDN通过智能技术生成

        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一些技巧的理解, 有什么不妥之处望批评指正, 欢迎与大家一起探讨!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值