• (转)面向对象(深入)|python描述器详解


    原文:https://zhuanlan.zhihu.com/p/32764345

    https://www.cnblogs.com/aademeng/articles/7262645.html------Python描述器(descriptor)

    ########################################################################################################################################################

    https://blog.csdn.net/andybegin/article/details/80770108--------Python 描述符 (descriptor) 详解

    对象属性的访问顺序: 
    1. 实例属性 
    2. 类属性 
    3. 父类属性 
    4. __getattr__()方法

    每次属性查找,这个协议的方法实际上是由对象的特殊方法getattribute()调用。每次通过点号(ins.attribute)或者getattr(ins, ‘attribute’)函数调用都会隐式的调用getattribute(),它的属性查找的顺序如下: 
    1. 验证属性是否为实例的类对象的数据描述符 
    2. 查看该属性是否能在实例对象的dict中找到 
    3. 查看该属性是否为实例的类对象的非数据描述符

    换句话说:数据描述符优先于dict,而dict查找优先于非数据描述符。

    ########################################################################################################################################################

    本文分为如下部分

    • 引言——用@property批量使用的例子来引出描述器的功能
    • 描述器的基本理论及简单实例
    • 描述器的调用机制
    • 描述器的细节
    • 实例方法、静态方法和类方法的描述器原理
    • property装饰器的原理
    • 描述器的应用
    • 参考资料

    引言

    前面python面向对象的文章中我们讲到过,我们可以用@property装饰器将方法包装成属性,这样的属性,相比于其他属性有一个优点就是可以在对属性赋值时,进行变量检查,举例代码如下

    class A:
        def __init__(self, name, score):
            self.name = name # 普通属性
            self._score = score
            
        @property
        def score(self):
            return self._score
        
        @score.setter
        def score(self, value):
            print('setting score here')
            if isinstance(value, int):
                self._score = value
            else:
                print('please input an int')
            
    a = A('Bob',90)
    a.name # 'Bob'
    a.score # 90
    a.name = 1
    a.name # 1 ,名字本身不应该允许赋值为数字,但是这里无法控制其赋值
    a.score = 83
    a.score # 83,当赋值为数值型的时候,可以顺利运行
    a.score = 'bob' # please input an int
    a.score # 83,赋值为字符串时,score没有被改变
    

    当我们有很多这样的属性时,如果每一个都去使用@property,代码就会过于冗余。如下

    class A:
        def __init__(self, name, score, age):
            self.name = name # 普通属性
            self._score = score
            self._age = age
            
        @property
        def score(self):
            return self._score
        
        @score.setter
        def score(self, value):
            print('setting score here')
            if isinstance(value, int):
                self._score = value
            else:
                print('please input an int')
                
        @property
        def age(self):
            return self._age
        
        @age.setter
        def age(self, value):
            print('setting age here')
            if isinstance(value, int):
                self._age = value
            else:
                print('please input an int')
                
    a = A('Bob', 90, 20)
    

    因为每一次检验的方法都是一样的,所以最好有方法可以批量实现这件事,只写一次if isinstance。描述器就可以用来实现这件事。

    为了能够更清楚地理解描述器如何实现,我们先跳开这个话题,先讲一讲描述器的基本理论。

    描述器基本理论及简单实例

    描述器功能强大,应用广泛,它可以控制我们访问属性、方法的行为,是@property、super、静态方法、类方法、甚至属性、实例背后的实现机制,是一种比较底层的设计,因此理解起来也会有一些困难。

    定义:从描述器的创建来说,一个类中定义了__get____set____delete__中的一个或几个,这个类的实例就可以叫做一个描述器。

    为了能更真切地体会描述器是什么,我们先看一个最简单的例子,这个例子不实现什么功能,只是使用了描述器

    # 创建一个描述器的类,它的实例就是一个描述器
    # 这个类要有__get__  __set__ 这样的方法
    # 这种类是当做工具使用的,不单独使用
    class M:
        def __init__(self, x=1):
            self.x = x
            
        def __get__(self, instance, owner):
            return self.x
        
        def __set__(self, instance, value):
            self.x = value
            
    # 调用描述器的类
    class AA:
        m = M() # m就是一个描述器
        
    aa = AA()
    aa.m # 1
    aa.m = 2
    aa.m # 2
    

    我们分析一下上面这个例子

    • 创建aa实例和普通类没什么区别,我们从aa.m开始看
    • aa.m是aa实例调用了m这个类属性,然而这个类属性不是普通的值,而是一个描述器,所以我们从访问这个类属性变成了访问这个描述器
    • 如果调用时得到的是一个描述器,python内部就会自动触发一套使用机制
    • 访问的话自动触发描述器的__get__方法
    • 修改设置的话就自动触发描述器的__set__方法
    • 这里就是aa.m触发了__get__方法,得到的是self.x的值,在前面__init__中定义的为1
    • aa.m = 2则触发了__set__方法,赋的值2传到value参数之中,改变了self.x的值,所以下一次aa.m调用的值也改变了

    进一步思考:当访问一个属性时,我们可以不直接给一个值,而是接一个描述器,让访问和修改设置时自动调用__get__方法和__set__方法。再在__get__方法和__set__方法中进行某种处理,就可以实现更改操作属性行为的目的。这就是描述器做的事情。

    相信有的读者已经想到了,开头引言部分的例子,就是用描述器这样实现的。在讲具体如何实现之前,我们要先了解更多关于描述器的调用机制

    描述器的调用机制

    aa.m命令其实是查找m属性的过程,程序会先到哪里找,没有的话再到哪里找,这是有一个顺序的,说明访问顺序时需要用到__dict__方法。

    先看下面的代码了解一下__dict__方法

    class C:
        x = 1
        def __init__(self, y):
            self.y = y
            
        def fun(self):
            print(self.y)
            
    c = C(2)
    # 实例有哪些属性
    print(c.__dict__) # {'y': 2}
    # 类有什么属性
    print(C.__dict__) # 里面有 x fun
    print(type(c).__dict__) # 和上一条一样
    
    print(vars(c)) # __dict__ 也可以用 vars 函数替代,功能完全相同
    
    # 调用
    c.fun() # 2
    c.__dict__['y'] # 2
    # type(c).__dict__['fun']() # 报错,说明函数不是这么调用的
    

    __dict__方法返回的是一个字典,类和实例都可以调用,键就是类或实例所拥有的属性、方法,可以用这个字典访问属性,但是方法就不能这样直接访问,原因我们之后再说。

    下面我们来说一下,当我们调用aa.m时的访问顺序

    • 程序会先查找 aa.__dict__['m'] 是否存在
    • 不存在再到type(aa).__dict__['m']中查找
    • 然后找type(aa)的父类
    • 期间找到的是普通值就输出,如果找到的是一个描述器,则调用__get__方法

    下面我们来看一下__get__方法的调用机制

    class M:
        def __init__(self):
            self.x = 1
            
        def __get__(self, instance, owner):
            return self.x
        
        def __set__(self, instance, value):
            self.x = value
            
    # 调用描述器的类
    class AA:
        m = M() # m就是一个描述器
        n = 2
        def __init__(self, score):
            self.score = score
        
            
    aa = AA(3)
    print(aa.__dict__) # {'score': 3}
    print(aa.score) # 3, 在 aa.__dict__ 中寻找,找到了score直接返回
    print(aa.__dict__['score']) # 3, 上面的调用机制实际上是这样的
    
    print(type(aa).__dict__) # 里面有n和m
    print(aa.n) # 2, 在aa.__dict__中找不到n,于是到type(aa).__dict__中找到了n,并返回其值
    print(type(aa).__dict__['n']) # 2, 其实是上面一条的调用机制
    
    print(aa.m) # 1, 在aa.__dict__中找不到n,于是到type(aa).__dict__中找到了m
    # m是一个描述器对象,于是调用__get__方法,将self.x的值返回,即1
    print(type(aa).__dict__['m'].__get__(aa,AA)) # 1, 上面一条的调用方式是这样的
    # __get__的定义中,除了self,还有instance和owner,其实分别表示的就是描述器所在的实例和类,这里的细节我们后文会讲
    
    print('-'*20)
    print(AA.m) # 1, 也是一样调用了描述器
    print(AA.__dict__['m'].__get__(None, AA)) # 类相当于调用这个
    

    此外还有特例,与描述器的种类有关

    • 同时定义了__get____set__方法的描述器称为资料描述器
    • 只定义了__get__的描述器称为非资料描述器
    • 二者的区别是:当属性名和描述器名相同时,在访问这个同名属性时,如果是资料描述器就会先访问描述器,如果是非资料描述器就会先访问属性 举例如下
    # 既有__get__又有__set__,是一个资料描述器
    class M:
        def __init__(self):
            self.x = 1
            
        def __get__(self, instance, owner):
            print('get m here') # 打印一些信息,看这个方法何时被调用
            return self.x
        
        def __set__(self, instance, value):
            print('set m here') # 打印一些信息,看这个方法何时被调用
            self.x = value + 1 # 这里设置一个+1来更清楚了解调用机制
    
    # 只有__get__是一个非资料描述器
    class N:
        def __init__(self):
            self.x = 1
            
        def __get__(self, instance, owner):
            print('get n here') # 打印一些信息,看这个方法何时被调用
            return self.x
            
    # 调用描述器的类
    class AA:
        m = M() # m就是一个描述器
        n = N()
        def __init__(self, m, n):
            self.m = m # 属性m和描述器m名字相同,调用时发生一些冲突
            self.n = n # 非资料描述器的情况,与m对比
        
    aa = AA(2,5)
    print(aa.__dict__) # 只有n没有m, 因为资料描述器同名时,不会访问到属性,会直接访问描述器,所以属性里就查不到m这个属性了
    print(AA.__dict__) # m和n都有
    print(aa.n) # 5, 非资料描述器同名时调用的是属性,为传入的5
    print(AA.n) # 1, 如果是类来访问,就调用的是描述器,返回self.x的值
    
    print(aa.m) # 3, 其实在aa=AA(2,5)创建实例时,进行了属性赋值,其中相当于进行了aa.m=2
    # 但是aa调用m时却不是常规地调用属性m,而是资料描述器m
    # 所以定义实例aa时,其实触发了m的__set__方法,将2传给value,self.x变成3
    # aa.m调用时也访问的是描述器,返回self.x即3的结果
    # 其实看打印信息也能看出什么时候调用了__get__和__set__
    
    aa.m = 6 # 另外对属性赋值也是调用了m的__set__方法
    print(aa.m) # 7,调用__get__方法
    
    print('-'*20)
    # 在代码中显式调用__get__方法
    print(AA.__dict__['n'].__get__(None, AA)) # 1
    print(AA.__dict__['n'].__get__(aa, AA)) # 1
    

    注:要想制作一个只读的资料描述器,需要同时定义 __set__ 和 __get__,并在 __set__ 中引发一个 AttributeError 异常。定义一个引发异常的 __set__ 方法就足够让一个描述器成为资料描述器。

    描述器的细节

    本节分为如下两个部分

    • 调用描述器的原理
    • __get____set__方法中的参数解释

    1.首先是调用描述器的原理 当调用一个属性,而属性指向一个描述器时,为什么就会去调用这个描述器呢,其实这是由object.__getattribute__()方法控制的,其中object是新式类定义时默认继承的类,即py2这么写的class(object)中的object。新定义的一个类继承了object类,也就继承了__getattribute__方法。当访问一个属性比如b.x时,会自动调用这个方法 __getattribute__()的定义如下

    def __getattribute__(self, key):
        "Emulate type_getattro() in Objects/typeobject.c"
        v = object.__getattribute__(self, key)
        if hasattr(v, '__get__'):
            return v.__get__(None, self)
        return v
    

    上面的定义显示,如果b.x是一个描述器对象,即能找到__get__方法,则会调用这个get方法,否则就使用普通的属性。 如果在一个类中重写__getattribute__,将会改变描述器的行为,甚至将描述器这一功能关闭。

    2.__get____set__方法中的参数解释 官网中标明了这三个方法需要传入哪些参数,还有这些方法的返回结果是什么,如下所示

    descr.__get__(self, obj, type=None) --> value
    descr.__set__(self, obj, value) --> None
    descr.__delete__(self, obj) --> None 
    

    我们要了解的就是self obj type value分别是什么 看下面一个例子

    class M:
        def __init__(self, name):
            self.name = name
            
        def __get__(self, obj, type):
            print('get第一个参数self: ', self.name)
            print('get第二个参数obj: ', obj.age)
            print('get第三个参数type: ', type.name)
            
        def __set__(self, obj, value):
            obj.__dict__[self.name] = value
            
    class A:
        name = 'Bob'
        m = M('age')
        def __init__(self, age):
            self.age = age
    
    a = A(20) # age是20
    a.m
    # get第一个参数self:  age
    # get第二个参数obj:  20
    # get第三个参数type:  Bob
    a.m = 30
    a.age # 30
    

    总结如下

    • self是描述器类M中的实例
    • obj是调用描述器的类a中的实例
    • type是调用描述器的类A
    • value是对这个属性赋值时传入的值,即上面的30

    上面的代码逻辑如下

    • a.m访问描述器,调用__get__方法
    • 三次打印分别调用了m.name a.age A.name
    • a.m = 30调用了__set__方法,令a(即obj)的属性中的'age'(即M('age')这里传入的self.name)为30

    实例方法、静态方法和类方法的描述器原理

    本节说明访问些方法其实都访问的是描述器,并说明它们调用顺序是怎样的,以及类方法和静态方法描述器的python定义。

    class B:
        @classmethod
        def print_classname(cls):
            print('Bob')
            
        @staticmethod
        def print_staticname():
            print('my name is bob')
            
        def print_name(self):
            print('this name')
            
    b = B()
    b.print_classname() # 调用类方法
    b.print_staticname() # 调用静态方法
    b.print_name() # 调用实例方法
    print(B.__dict__) # 里面有实例方法、静态方法和类方法
    
    # 但其实字典里的还不是可以直接调用的函数
    print(B.__dict__['print_classname'])
    print(b.print_classname) # 和上不一样
    print(B.__dict__['print_staticname'])
    print(b.print_staticname) # 和上不一样
    print(B.__dict__['print_name'])
    print(b.print_name) # 和上不一样
    
    # <classmethod object at 0x0000024A92DA67B8>
    # <bound method B.print_classname of <class '__main__.B'>>
    # <staticmethod object at 0x0000024A92DA6860>
    # <function B.print_staticname at 0x0000024A92D889D8>
    # <function B.print_name at 0x0000024A92D88158>
    # <bound method B.print_name of <__main__.B object at 0x0000024A92DA6828>>
    

    上面结果表明,实例直接调用时,类方法和实例方法都是bound method,而静态方法是function。因为静态方法本身就是定义在类里面的函数,所以不属于方法范畴。

    除此之外,由于实例直接调用后得到的结果可以直接接一个括号,当成函数来调用。而使用字典调用时,得到的结果和实例调用都不一样,所以它们是不可以直接接括号当成函数使用的。

    其实从显示的结果我们可以看出,静态方法和类方法用字典调用得到的其实分别是staticmethod和classmethod两个类的对象,这两个类其实是定义描述器的类,所以用字典访问的两个方法得到的都是描述器对象。它们需要用一个__get__方法才可以在后面接括号当成函数调用。

    而普通实例方法用字典调用得到的是一个function即函数,理论上是可以用括号直接调用的,但是调用时报错说少了self参数,其实它也是描述器对象,用通过__get__方法将self传入来调用

    三种方法本质上调用__get__方法的情况展示如下

    B.__dict__['print_classname'].__get__(None, B)()
    B.__dict__['print_staticname'].__get__(None, B)()
    B.__dict__['print_name'].__get__(b, B)()
    
    print(B.__dict__['print_classname'].__get__(None, B))
    print(B.__dict__['print_staticname'].__get__(None, B))
    
    print(B.__dict__['print_name'])
    print(B.__dict__['print_name'].__get__(None, B)) # 这是不传入实例即self的情况,和直接从字典调用结果相同,在python2中是一个unbound method
    print(B.__dict__['print_name'].__get__(b, B))
    
    # B.print_name() # 报错,说少输入一个self参数
    # B.print_name(B()) # this name  输入实例即不会报错
    

    所以说我们平常调用的方法都是本质上在调用描述器对象,访问描述器时自动调用__get__方法。

    上面调用时注意到,前两个__get__的第一个参数都是None,而实例方法是一个b,这是因为实例方法需要具体的实例来调用而不能用类直接调用。在python2中,用类直接调用实例方法得到的是一个unbound method,用实例调用才是一个bound method,(在python3删除了unbound method的概念,改为function),而类方法本身就可以被类调用,所以参数是None时就是一个bound method了。所以说__get__的第一个参数使用b可以理解成方法的bound过程。

    既然三种方法都是调用了描述器对象,那么这些对象都是各自类的实例,它们的类是如何定义的呢?python中这些类的定义是用底层的C语言实现的,为了理解其工作原理,这里展示一个用python语言实现classmethod装饰器的方法,(来源),即构建能产生类方法对应描述器对象的类。

    class myclassmethod(object):
        def __init__(self, method):
            self.method = method
        def __get__(self, instance, cls):
            return lambda *args, **kw: self.method(cls, *args, **kw)
        
    class Myclass:
        x = 3
        @myclassmethod
        def method(cls, a):
            print(cls.x+a)
            
    m = Myclass()
    Myclass.method(a=2)
    

    下面我们分析一下上述代码

    • 我们看到使用@myclassmethod装饰器达到的效果和使用@classmethod装饰器没有什么区别
    • 首先定义了myclassmethod类,里面使用了__get__方法,所以它的实例会是一个描述器对象
    • myclassmethod当做装饰器作用于method函数,根据装饰器的知识,相当于这样设置method=myclassmethod(method)
    • 调用Myclass.method()时调用了改变后了的method方法,即myclassmethod(method)(a)
    • myclassmethod(method)这是myclassmethod类的一个实例,即一个描述器,此处访问于是调用__get__方法,返回一个匿名函数
    • __get__中其实是将owner(cls)部分传入method方法,因为methon在Myclass类中调用,这个owner也就是Myclass类。这一步其实是提前传入了method的第一个参数cls,后面的参数a由myclassmethod(method)(a)第二个括号调用
    • 仔细分析上面的定义与调用过程,我们会发现,我们常常说的类方法第一个参数要是cls,其实是不对的,第一个参数是任意都可以,它只是占第一个位置,用于接收类实例引用类属性,随便换成任意变量都可以,用cls只是约定俗成的。比如下面的代码正常运行
    class Myclass:
        x = 3
        @classmethod
        def method(b, a):
            print(b.x+a)
            
    m = Myclass()
    Myclass.method(a=2) # 5
    

    下面看一下staticmethod类的等价python定义(来源

    class mystaticmethod:
        def __init__(self, callable):
            self.f = callable
        def __get__(self, obj, type=None):
            return self.f
        
    class Myclass:
        x = 3
        @mystaticmethod
        def method(a, b):
            print(a + b)
            
    m = Myclass()
    m.method(a=2, b=3)
    

    注:从源码角度来理解静态方法和类方法

    • 静态方法相当于不自动传入实例对象作为方法的第一个参数,类方法相当于将默认传入的第一个参数由实例改为类
    • 使用@classmethod后无论类调用还是实例调用,都会自动转入类作为第一个参数,不用手动传入就可以调用类属性,而没有@classmethod的需要手动传入类
    • 既不用@classmethod也不用@staticmethod则类调用时不会自动传入参数,实例调用时自动传入实例作为第一个参数
    • 所以说加@classmethod是为了更方便调用类属性,加@staticmethod是为了防止自动传入的实例的干扰
    • 除此之外要说明一点:当属性和方法重名时,调用会自动访问属性,是因为这些方法调用的描述器都是非资料描述器。而当我们使用@property装饰器后,自动调用的就是新定义的get set方法,是因为@property装饰器是资料描述器

    property装饰器的原理

    到这里我们可以讲一讲开头提出的问题了,即@property装饰器是如何使用描述器实现的,调用机制是怎样的,如何通过描述器达到精简多次使用@property装饰器的问题。

    首先要明确,property有两种调用形式,一种是用装饰器,一种是用类似函数的形式,下面会用引言中的例子分别说明两种形式的调用机制。

    下面贴出property的等价python定义(来源于官网的中文翻译

    class Property(object):
        "Emulate PyProperty_Type() in Objects/descrobject.c"
    
        def __init__(self, fget=None, fset=None, fdel=None, doc=None):
            self.fget = fget
            self.fset = fset
            self.fdel = fdel
            if doc is None and fget is not None:
                doc = fget.__doc__
            self.__doc__ = doc
    
        def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError("unreadable attribute")
            return self.fget(obj)
    
        def __set__(self, obj, value):
            if self.fset is None:
                raise AttributeError("can't set attribute")
            self.fset(obj, value)
    
        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError("can't delete attribute")
            self.fdel(obj)
    
        def getter(self, fget):
            return type(self)(fget, self.fset, self.fdel, self.__doc__)
    
        def setter(self, fset):
            return type(self)(self.fget, fset, self.fdel, self.__doc__)
    
        def deleter(self, fdel):
            return type(self)(self.fget, self.fset, fdel, self.__doc__)
    

    从上面的定义中我们可以看出,定义时分为两个部分,一个是__get__等方法的定义,另一部分是getter等方法的定义,同时注意到这个类要传入fget等三个函数作为属性。getter等方法的定义是为了让它可以完美地使用装饰器形式,我们先不看这一部分,先看看不是使用第一种即不使用装饰器的形式的调用机制。

    # 类似函数的形式
    class A:
        def __init__(self, name, score):
            self.name = name # 普通属性
            self.score = score
            
        def getscore(self):
            return self._score
        
        def setscore(self, value):
            print('setting score here')
            if isinstance(value, int):
                self._score = value
            else:
                print('please input an int')
                
        score = property(getscore, setscore)
            
    a = A('Bob',90)
    a.name # 'Bob'
    a.score # 90
    a.score = 'bob' # please input an int
    

    分析上述调用score的过程

    • 初始化时即开始访问score,发现有两个选项,一个是属性,另一个是property(getscore, setscore)对象,因为后者中定义了__get____set__方法,因此是一个资料描述器,具有比属性更高的优先级,所以这里就访问了描述器
    • 因为初始化时是对属性进行设置,所以自动调用了描述器的__set__方法
    • __set__中对fset属性进行检查,这里即传入的setscore,不是None,所以调用了fsetsetscore方法,这就实现了设置属性时使用自定义函数进行检查的目的
    • __get__也是一样,查询score时,调用__get__方法,触发了getscore方法

    下面是另一种使用property的方法

    # 装饰器形式,即引言中的形式
    class A:
        def __init__(self, name, score):
            self.name = name # 普通属性
            self.score = score
            
        @property
        def score(self):
            print('getting score here')
            return self._score
        
        @score.setter
        def score(self, value):
            print('setting score here')
            if isinstance(value, int):
                self._score = value
            else:
                print('please input an int')
            
    a = A('Bob',90)
    # a.name # 'Bob'
    # a.score # 90
    # a.score = 'bob' # please input an int
    

    下面进行分析

    • 在第一种使用方法中,是将函数作为传入property中,所以可以想到是否可以用装饰器来封装
    • get部分很简单,访问score时,加上装饰器变成访问property(score)这个描述器,这个score也作为fget参数传入__get__中指定调用时的操作
    • 而set部分就不行了,于是有了setter等方法的定义
    • 使用了propertysetter装饰器的两个方法的命名都还是score,一般同名的方法后面的会覆盖前面的,所以调用时调用的是后面的setter装饰器处理过的score,是以如果两个装饰器定义的位置调换,将无法进行属性赋值操作。
    • 而调用setter装饰器的score时,面临一个问题,装饰器score.setter是什么呢?是scoresetter方法,而score是什么呢,不是下面定义的这个score,因为那个score只相当于参数传入。自动向其他位置寻找有没有现成的score,发现了一个,是property修饰过的score,这是个描述器,根据property的定义,里面确实有一个setter方法,返回的是property类传入fset后的结果,还是一个描述器,这个描述器传入了fgetfset,这就是最新的score了,以后实例只要调用或修改score,使用的都是这个描述器
    • 如果还有del则装饰器中的score找到的是setter处理过的score,最新的score就会是三个函数都传入的score
    • 对最新的score的调用及赋值删除都跟前面一样了

    property的原理就讲到这里,从它的定义我们可以知道它其实就是将我们设置的检查等函数传入get set等方法中,让我们可以自由对属性进行操作。它是一个框架,让我们可以方便传入其他操作,当很多对象都要进行相同操作的话,重复就是难免的。如果想要避免重复,只有自己写一个类似property的框架,这个框架不是传入我们希望的操作了,而是就把这些操作放在框架里面,这个框架因为只能实现一种操作而不具有普适性,但是却能大大减少当前问题代码重复问题

    下面使用描述器定义了Checkint类之后,会发现A类简洁了非常多

    class Checkint:
        
        def __init__(self, name):
            self.name = name
            
        def __get__(self, instance, owner):
            if instance is None:
                return self
            else:
                return instance.__dict__[self.name]
            
        def __set__(self, instance, value):
            if isinstance(value, int):
                instance.__dict__[self.name] = value
            else:
                print('please input an integer')
    
    # 类似函数的形式
    class A:
        score = Checkint('score')
        age = Checkint('age')
        
        def __init__(self, name, score, age):
            self.name = name # 普通属性
            self.score = score
            self.age = age
            
    a = A('Bob', 90, 30)
    a.name # 'Bob'
    a.score # 90
    # a.score = 'bob' # please input an int
    # a.age='a' # please input an integer
    

    描述器的应用

    因为我本人也刚刚学描述器不久,对它的应用还不是非常了解,下面只列举我现在能想到的它有什么用,以后如果想到其他的再补充

    • 首先是上文提到的,它是实例方法、静态方法、类方法、property的实现原理
    • 当访问属性、赋值属性、删除属性,出现冗余操作,或者苦思无法找到答案时,可以求助于描述器
    • 具体使用1:缓存。比如调用一个类的方法要计算比较长的时间,这个结果还会被其他方法反复使用,我们不想每次使用和这个相关的函数都要把这个方法重新运行一遍,于是可以设计出第一次计算后将结果缓存下来,以后调用都使用存下来的结果。只要使用描述器在__get__方法中,在判断语句下,obj.__dict__[self.name] = value。这样每次再调用这个方法都会从这个字典中取得值,而不是重新运行这个方法。(例子来源最后的那个例子)
  • 相关阅读:
    简单粗暴,微生物生态研究中常用数据库简介--转载
    sliva数据库简介--转载
    DriverDBv2---人类肿瘤driver基因数据库
    lncrnablog
    胞外囊泡与外泌体数据库--转载
    Oncomine: 一个肿瘤相关基因研究的数据库--转载
    circRNA研究手册
    常用Gene ID转换工具--转载
    miRNA几大常用的数据库
    zk使用通知移除节点
  • 原文地址:https://www.cnblogs.com/liujiacai/p/10064774.html
Copyright © 2020-2023  润新知