• python理解描述符(descriptor)


    Descriptor基础

    python中的描述符可以用来定义触发自动执行的代码,它像是一个对象属性操作(访问、赋值、删除)的代理类一样。前面介绍过的property是描述符的一种。

    大致流程是这样的:

    1. 定义一个描述符类D,其内包含一个或多个__get__()__set__()__delete__()方法
    2. 将描述符类D的实例对象d赋值给另一个要代理的类中某个属性attr,即attr = D()
    3. 之后访问、赋值、删除attr属性,将会自动触发描述符类中的__get__()__set__()__delete__()方法

    简言之,就是创建一个描述符类,它的实例对象作为另一个类的属性

    要定义描述符类很简单,只要某个类中包含了下面一个或多个方法,就算是满足描述符协议,就是描述符类,就可以作为属性操作的代理器。

    class Descriptor():
        def __get__(self, instance, owner):...
        def __set__(self, instance, value):...
        def __delete__(self, instance):...
    

    需要注意的是,__get__的返回值需要是属性值或抛异常,另外两个方法要返回None。

    还需注意的是不要把__delete____del__搞混了,前者是实现描述符协议的一个方法,后者是对象销毁函数(也常称为析构函数)。

    先不管这几个方法中的参数,看一个示例先:

    class Descriptor():
        def __get__(self, instance, owner):
            print("self: %s
    instance: %s
    owner: %s" % (self, instance, owner))
    
    class S:
        # 描述符的示例对象作为S的属性
        attr = Descriptor()
    
    s1 = S()
    s1.attr  # 访问对象属性
    
    print("-" * 30)
    S.attr   # 访问类属性
    

    输出结果:

    self: <__main__.Descriptor object at 0x030C02D0>
    instance: <__main__.S object at 0x030C0AB0>
    owner: <class '__main__.S'>
    ------------------------------
    self: <__main__.Descriptor object at 0x030C02D0>
    instance: None
    owner: <class '__main__.S'>
    

    不难看出,在访问类S中的属性attr时,表示访问描述符类的实例对象,它会自动调用描述符类中的__get__方法。

    在这个方法中,3个参数self、instance、owner分别对应的内容从结果中已经显示出来了。它们之间有以下等价关系:

    s1.attr -> Descriptor.__get__(S.attr, s1, S)
    S.attr  -> Descriptor.__get__(S.attr, None, S)
    

    所以,这里解释下__get__(self, instance, owner)中的三个参数:

    • self:描述符对象自身,也就是被代理类S中的属性attr
    • instance:被代理类的实例对象。所以访问类属性(class.attr)时为None
    • owner:将描述符对象附加到哪个类上,其实是instance所属的类,也就是type(instance)

    再解释下这里相关的几个角色:

    • Descriptor:是描述符类,也是代理者
    • S:是另一个类,是托管类、客户类,也就是参数中的owner
    • attr = Descriptor():是描述符的实例对象,attr是托管类的属性,也就参数中的self
    • s1:是托管类实例对象,也就是参数中的instance

    按照descriptor的功能,大概可以用上面的方式去定义各个角色。当然,角色的定义没什么限制。

    descriptor的作用发挥在哪

    当定义了一个类后,可以访问、赋值、删除它的属性,这些操作也同样适用于它的实例对象。

    例如Foo类:

    class Foo():
        ...
    
    f = Foo()
    a = f.bar   # 访问属性
    f.bar = b   # 赋值属性
    del f.bar   # 删除属性
    

    decriptor发挥作用的时候就在于执行这3类操作的时候:

    • 当访问x.d的时候,将自动调用描述符类中的__get__
    • 当赋值x.d的时候,将自动调用描述符类中的__set__
    • 当删除x.d的时候,将自动调用描述符类中的__delete__

    考虑一下:如果x所属的类中已经定义了__getattr____setattr____delattr__会如何,是描述符类中的先生效,还是x自身所属类的这几个方法会生效。再继续考虑,如果x所属类没有定义,但它的父类定义了这几个方法,谁会生效。可自行测试或者参考我的下一篇文章。

    示例1:原始代码

    假设现在有一个Student类,需要记录stuid、name、score1、score2、score3信息。

    class Student():
        def __init__(self, stuid, name, score1, score2, score3):
            self.stuid = stuid
            self.name = name
            self.score1 = score1
            self.score2 = score2
            self.score3 = score3
    
        def returnMe(self):
            return "%s, %s, %i, %i, %i" % (
                self.stuid,
                self.name,
                self.score1,
                self.score2,
                self.score3)
    
    stu = Student("20101120", "malong", 67, 77, 88)
    print(stu.returnMe())
    

    但是现在有个需求,要求score1-score3的数值范围只能是0-100分。

    于是修改__init__()

    class Student():
        def __init__(self, stuid, name, score1, score2, score3):
            self.stuid = stuid
            self.name = name
    
            if 0 <= score1 <= 100:
                self.score1 = score1
            else:
                raise ValueError("score not in [0,100]")
    
            if 0 <= score2 <= 100:
                self.score2 = score2
            else:
                raise ValueError("score not in [0,100]")
    
            if 0 <= score3 <= 100:
                self.score3 = score3
            else:
                raise ValueError("score not in [0,100]")
    

    这个修改对于初始化Student对象时有效,但Python中属性的赋值太过自由,之后可以随意赋值:

    stu = Student("20101120", "malong", 67, 77, 88)
    
    stu.score1 = -23
    print(stu.returnMe())
    

    使用property

    使用Property或者自定义的getter、setter或运算符__getattr____setattr__重载都能解决上面的问题,保证无法赋值超出0到100范围内的数值。

    class Student():
        def __init__(self, stuid, name, score1, score2, score3):
            self.stuid = stuid
            self.name = name
            self._score1 = score1
            self._score2 = score2
            self._score3 = score3
    
        def get_score1(self):
            return self._score1
    
        def set_score1(self, score):
            if 0 <= score <= 100:
                self._score1 = score
            else:
                raise ValueError("score not in [0,100]")
    
        def get_score2(self):
            return self._score2
    
        def set_score2(self, score):
            if 0 <= score <= 100:
                self._score2 = score
            else:
                raise ValueError("score not in [0,100]")
    
        def get_score3(self):
            return self._score3
    
        def set_score3(self, score):
            if 0 <= score <= 100:
                self._score3 = score
            else:
                raise ValueError("score not in [0,100]")
    
        score1 = property(get_score1, set_score1)
        score2 = property(get_score2, set_score2)
        score3 = property(get_score3, set_score3)
    
        def returnMe(self):
            return "%s, %s, %i, %i, %i" % (
                self.stuid,
                self.name,
                self.score1,
                self.score2,
                self.score3)
    

    下面测试时将抛出异常。

    stu = Student("20101120", "malong", 67, 77, 88)
    print(stu.returnMe())
    stu.score1 = -23
    

    但很显然,上面的重复代码太多了。

    使用descriptor

    如果使用descriptor,将很容易解决上面的问题。只需将score1、score2、score3交给描述符类托管即可。

    from weakref import WeakKeyDictionary
    
    class Score():
        """ score should in [0,100] """
    
        def __init__(self):
            self.score = WeakKeyDictionary()
            #self.score = {}
    
        def __get__(self, instance, owner):
            return self.score[instance]
    
        def __set__(self, instance, value):
            if 0 <= value <= 100:
                self.score[instance] = value
            else:
                raise ValueError("score not in [0,100]")
    
    
    class Student():
        # 托管属性定义在类级别上
        score1 = Score()
        score2 = Score()
        score3 = Score()
    
        def __init__(self, stuid, name, score1, score2, score3):
            self.stuid = stuid
            self.name = name
            self.score1 = score1
            self.score2 = score2
            self.score3 = score3
    
        def returnMe(self):
            return "%s, %s, %i, %i, %i" % (
                self.stuid,
                self.name,
                self.score1,
                self.score2,
                self.score3)
    
    
    stu = Student("20101120", "malong", 67, 77, 88)
    print(stu.returnMe())
    stu.score1 = -23
    

    很明显地,它们的代码被完整地复用了。这里score1、score2、score3被描述符类Score托管了,这3个分值分别被放进了Score实例对象的dict中(是单独存放它们还是使用dict数据结构来保存,取决于你)。

    另外,上面使用了弱引用的字典,因为每个属性只在描述符对象中才会被用上,为了保证Student对象被销毁的时候能释放这些资源,所以采用弱引用,避免出现内存泄漏。

    参考资料

  • 相关阅读:
    操作系统六文件管理
    Educational Codeforces Round 38 (Rated for Div. 2) ABCD
    51nod 1100 斜率最大
    51nod 最小方差
    51nod 1065 最小正子段和
    P1280 尼克的任务
    牛客小白月赛2
    Codeforces Round #210 (Div. 1) B 二分+dp
    江西财经大学第一届程序设计竞赛
    51nod 1596 搬货物
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/10198020.html
Copyright © 2020-2023  润新知