• 详解python中的描述符


    描述符介绍

    总所周知,python声明变量的时候,不需要指定类型。虽然现在有了注解,但这只是一个规范,在语法层面是无效的。比如:

    这里我们定义了一个hello函数,我们要求name参数传入str类型的变量,然而最终我们传入的变量却是int类型,pycharm也很智能的提示我们需要传入str。但我就传入int,它能拿我怎么样吗?显然不能,这个程序是可以正常执行的。因此这个注解并没有在语法层面上限制你。

    于是便出现了描述符,我们来看看描述符是干什么的。

    class Descriptor:
        """
        一个类中,只要出现了__get__或者__set__方法,就被称之为描述符
        """
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        def __set__(self, instance, value):
            print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    """
    此时的name属性就被描述符代理了
    """
    
    c = Cls("satori", 16)
    # 输出内容
    """
    __set__ <__main__.Cls object at 0x0000022E1CE3EE80> satori
    """
    # 可以看到,当程序执行self.name = name的时候,并没有把值设置到self的属性字典里面
    # 而是执行了描述符的__set__方法,参数instance是调用的实例对象,也就是我们这里的c
    # 至于value显然就是我们给self.name赋的值
    
    # 对于self.age,由于它没有被代理,所以正常的设置到属性字典里面去了。所以也是可以正常打印的
    print(c.age)  # 16
    
    # 如果是获取c.name呢?
    name = c.name  
    # 输出内容
    """
    __get__ <__main__.Cls object at 0x0000022E94FBEEB8> <class '__main__.Cls'>
    """
    # 可以看到,由于实例的name属性被代理了,那么获取的时候,会触发描述符的__get__方法。
    # 现在我们可以得到如下结论,如果实例的属性被具有__get__和__set__方法的描述符代理了
    # 那么给被代理的属性赋值的时候,会执行描述符的__set__方法。获取值则会执行描述符的__get__方法。
    

    属性字典

    我们给实例添加属性的时候,本质上都是添加到了实例的属性字典__dict__里了。

    class Descriptor:
        """
        一个类中,只要出现了__get__或者__set__方法,就被称之为描述符
        """
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        def __set__(self, instance, value):
            print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    c = Cls("satori", 16)
    print(c.__dict__)
    """
    __set__ <__main__.Cls object at 0x00000204FF77EEB8> satori
    {'age': 16}
    """
    # 可以看到,由于实例的name属性被代理了
    # 如果没有被代理,按照python的逻辑,会自动设置到实例的属性字典里面
    # 但是现在被代理了,因此走的是描述符的__set__方法,所以没有设置到字典里面去。
    c.__dict__["name"] = "satori"
    # 我们可以通过这种方式,来向实例对象设置值
    # 其实,不光实例对象,类也是,属性都在自己对应的属性字典里面
    # self.name = "xxx",就等价于self.__dict__["name"] = "xxx"
    # self.__dict__里面的属性,都可以通过self.的方式来获取
    print(c.__dict__)  # {'age': 16, 'name': 'satori'}
    # 由于实例对象的name属性被代理了,那么我们通过属性字典的方式就绕过去了
    
    # 下面我们来获取值
    name = c.name
    """
    __get__ <__main__.Cls object at 0x000002B7F51CE940> <class '__main__.Cls'>
    """
    # 可以看到还是跟之前一样,被代理了,是无法通过self.的方式来获取,那怎么办呢?还是使用字典的方式
    print(c.__dict__["name"])  # satori
    

    因此对于类和实例对象来说,都有各自的属性字典,设置属性本质上都设置到属性字典里面去。

    class A:
    
        def add(self, a, b):
            return a + b
    
    
    a = A()
    
    print(A.__dict__["add"](a, 10, 20))  # 30
    # 所以A.__dict__["add"]就等价于A.add
    
    # 既然如此的话,那么a.__dict__["add"]可以吗?
    # 显然不可以,因为属性字典就是去获取自己的属性
    # 可是a里面没有这个属性,但是a.add话,自己没有,会去到类里面找
    # 因此a.__dict__这种形式,表示就在a的属性字典里面去找add,然后里面没有add
    print(a.add(10 ,20))  # 30
    
    try:
        a.__dict__["add"]
    except KeyError as e:
        print(f"没有{e}这个属性")  # 没有'add'这个属性
    
    
    # 我们可以手动添加
    a.__dict__["add"] = lambda a, b, c: a + b + c
    print(a.add(10, 20, 30))  # 60
    # 如果实例对象里面已经有了,就不会再到类里面找了。
    
    
    # 我们再来看看函数
    def foo():
        name = "satori"
        age = 16
    
    print(foo.__dict__)  # {}
    # 我们看到函数也有属性字典,只不过属性字典是空的
    

    描述符的优先级

    描述符也是有优先级的,我们说当一个类里面出现了__get__或者__set__任意一种就被称为描述符。但是如果只出现一种呢?

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        # def __set__(self, instance, value):
        #     print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    """
    注意:name = Descriptor()要写在类属性里面
    """
    
    # 我们将描述符的__set__属性去掉了
    # 注意:一个描述符既有__get__又有__set__,那么称这个描述符为数据描述符,如果只出现了__get__,而没有__set__,那么称之为非数据描述符。
    # 此时我们这里的描述符显然就是非数据描述符
    c = Cls("satori", 16)
    print(c.name)  # satori
    
    """
    此时我们惊奇的发现居然没有走__get__方法。
    可我们记得之前访问__get__的时候,走的是描述符的__get__方法啊。
    其实那是因为之前的描述符有__set__方法
    """
    # 因此我们得出了一个结论
    # 优先级:非数据描述符 < 实例属性 < 数据描述符
    """
    就是当一个实例对象去访问被代理某个属性时候。
    如果是数据描述符,那么会走__get__方法
    但如果是非数据描述符,会从实例对象的属性字典里面去获取
    """
    

    现在我们知道了,描述符和实例属性之间的关系。但如果是类属性呢?

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        def __set__(self, instance, value):
            print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    name = Cls.name
    """
    __get__ None <class '__main__.Cls'>
    """
    
    Cls.name = "mashiro"
    print(Cls.name)  # mashiro
    """
    我们注意到,类去访问的话,由于name被代理了,访问依旧会触发__get__方法
    但是,我们设置的时候并没有触发__set__方法,访问的时候,也没有触发__get__方法
    只是在没有重新设置该属性的时候,才会触发描述符的__get__方法。
    但是在设置属性、设置完之后获取属性的时候,是不会触发的
    """
    # 因此我们得出了一个结论
    # 优先级:非数据描述符<实例属性<数据描述符<类属性<未设置
    # 这里的未设置是指:属性被代理,肯定会触发__get__,比如这里类里面的name,被代理了,但是一开始我们类没有设置,所以触发__get__。但是类重新设置name的时候,优先级是比描述符高的。
    print(Cls.__dict__["name"])  # mashiro
    # 显然已经被设置到类的属性字典里面去了
    

    被代理的属性

    很多人可能好奇name = Descriptor()这里的name,到底是实例的name,还是类的name。首先既然是name = Descriptor(),那么这肯定是一个类属性。但我们无论是使用类还是使用实例对象,貌似都可以触发描述符的属性方法啊。那么描述符的角度来说,这个name到底是针对谁的。其实,答案可以说是两者都是吧,我们可以看代码。

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        def __set__(self, instance, value):
            print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    Cls.name
    print(Cls.__dict__.get("name"))
    """
    __get__ None <class '__main__.Cls'>
    <__main__.Descriptor object at 0x000001BD63AE66A0>
    """
    # 可以看到,直接访问的话会触发__get__,但是通过属性字典获取的话这就是一个Descriptor对象,这是毫无疑问的。
    
    c = Cls("satori", 16)
    """
    __set__ <__main__.Cls object at 0x000002A25167EF60> satori
    """
    # 用大白话解释就是,实例去访问自身的name属性,但是发现类里面有一个和自己同名、而且被描述符代理的属性,所以实例自身的这个属性也相当于被描述符代理了。
    
    Cls.name = "类里面的name不再等于Descriptor()了"
    c1 = Cls("mashiro", 16)
    print(c1.name)  # mashiro
    """
    于是惊奇的事情发生了,此时设置属性、访问属性没有再触发描述符的方法。
    这是因为类属性的优先级比两种描述符的优先级都要高,从而把name给修改了。
    那么此时再去设置实例属性的话,此时类里面已经没有和自己同名并且被描述符代理的name了,所以直接设置到属性字典里面
    """
    

    进一步验证:

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        def __set__(self, instance, value):
            print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, age):
            self.age = age
    
    # 此时实例已经没有name属性了
    c = Cls(16)
    print(c.age)  # 16
    name = c.name
    """
    __get__ <__main__.Cls object at 0x0000021A41C7EE10> <class '__main__.Cls'>
    """
    # 此时依旧触发描述符的__get__方法,这是肯定的。因为实例属性里面根本没有name这个属性
    # 于是去到类里面去找,但是被代理了,类还没有设置值。没有设置值,那么走描述符的__get__方法。
    
    c.__dict__["name"] = "satori"  # 我现在通过属性字典的方式,向实例里面设置一个name属性
    name = c.name
    """
    __get__ <__main__.Cls object at 0x00000142AD99EF28> <class '__main__.Cls'>
    """
    # 此时获取属性又触发了描述符的方法,这是为什么?
    # 说明:即使__init__函数里面没有name,但是我们后续手动设置,并且获取的时候依旧会触发
    # 实例获取属性是否会触发代理的条件就是,类中有没有和自己属性名相同、并且被代理的属性
    
    Cls.name = "修改了"
    print(c.name)  # satori
    # 此时获取成功,因为类把name这个属性修改了
    # 所以实例能获取成功,至于原因,已经解释过了。
    # 另外如果类不重新设置name这个属性,那么即便类去获取依旧会触发__get__方法
    # 因为name等于的本来就是一个描述符,当然会触发描述符方法,同理实例也是
    # 如果类把name改了,实例和类就都不会触发了
    

    但如果是非数据描述符就另当别论了

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        # def __set__(self, instance, value):
        #     print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    c = Cls("satori", 16)
    print(c.name)  # satori
    """
    因为是非数据描述符,实例的优先级要高,因此即便当实例的获取属性的时候
    发现类里面有和自己同名并且被代理的属性,还是会获取自身的属性,而不会走描述符的__get__方法。
    """
    
    name = Cls.name
    """
    __get__ None <class '__main__.Cls'>
    """
    # 但是我们发现使用类去获取,依旧触发__get__方法
    # 这是因为类的name就是一个描述符,当然会触发__get__方法
    # 类的name和实例的name不是同一个name
    # 因此name = Descriptor()本质上是一个类属性,但如果实例中也有一个同名的属性,那么也会被描述符代理
    # 至于怎么执行,我们刚才解释的很清楚了,是由优先级决定的
    
    
    # 但是对于当前来说,类是否重新设置name,对于实例已经没有关系了,因为是非数据描述符
    # 但如果是数据描述符,那么就类如果不重新设置name的属性,实例想通过.的方式获取是行不通的
    # 因为发现类里面有和自己同名并且被描述符代理的属性,如果类不把name=Descriptor()改成name="其他的",那么实例对象想获取就需要采用属性字典的方式了
    
    

    类和实例获取被代理属性的区别

    首先name = Descriptor(),类和实例都可以访问,在类未给name设置其它值的时候,并且都会触发。那么类和实例访问,两者有什么区别呢?另外我们刚才讲了很多,但其实我们一般都是用实例去访问的,很少有描述符代理之后用类去访问的。

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__", instance, owner)
    
        def __set__(self, instance, value):
            print("__set__", instance, value)
    
    
    class Cls:
    
        name = Descriptor()
    
    
    Cls.name
    Cls().name
    """
    __get__ None <class '__main__.Cls'>
    __get__ <__main__.Cls object at 0x00000212FC6EAC88> <class '__main__.Cls'>
    """
    
    # 我们发现__get__里面的instance就是实例,owner就是类
    # 如果实例获取,那么instance就是实例,如果类去获取instance就是None
    
    # 那么对于__set__来说,instance依旧是实例,value就是我们给实例被代理的属性设置的值
    

    _set_name_

    相信到这里,描述符的原理已经清楚了,但是这个__set_name__是什么呢?

    我们之前说,如果是数据描述符,只能使用属性字典的方式,那是在描述符不做的逻辑处理的情况下,现在我们来看看如果让描述符支持实例对象通过.的方式访问自身被代理的属性。

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("获取值")
            # instance就是下面Cls的实例,我们来帮它获取并返回
            # 注意这里也要通过属性字典的方式,如果通过instance.name的方式会怎么样
            return instance.__dict__["name"]
            # 首先instance.name就等价于c.name(c是Cls的实例),那么会触发__get__
            # 然后又instance.name,由触发__get__,因此自身会无限递归,直到栈溢出
    
        def __set__(self, instance, value):
            print("设置值")
            # 这里也是通过属性字典的方式进行设置值
            instance.__dict__["name"] = value
    
    
    class Cls:
    
        name = Descriptor()
    
        def __init__(self, name):
            self.name = name
    
    c = Cls("satori")
    """
    设置值
    """
    print(c.name)
    """
    获取值
    satori
    """
    # 因此,如果我们不加那两个print,那么表现出来的结果和不使用描述符是一样的
    

    但是这里又有一个问题,那就是在描述符中instance.__dict__["name"],这里我们把key写死了,如果我们想对age进行代理呢?如果这里的key还写name的话,表示还是给name设置属性

    class Descriptor:
    
        def __get__(self, instance, owner):
            return instance.__dict__["name"]
    
        def __set__(self, instance, value):
            instance.__dict__["name"] = value
    
    
    class Cls:
    
        age = Descriptor()
    
        def __init__(self, age):
            self.age = age
    
    c = Cls(16)
    c.age = 16
    print(c.age)  # 16
    
    print(c.__dict__)  # {'name': 16}
    

    我们发现对于访问来说,貌似是没啥影响的。因为设置age,相当于是设置name,访问age,也相当于是访问name。虽然即便name不改变,也是可以实现的,但是毕竟属性字典里面是name而不是age,这总归是不好的。但是问题来了,我们要如何获取被代理的属性的名称呢?这个时候__set_name__的作用就来了。

    class Descriptor:
    
        def __get__(self, instance, owner):
            print("__get__")
            return instance.__dict__["name"]
    
        def __set__(self, instance, value):
            print("__set__")
            instance.__dict__["name"] = value
    
        def __set_name__(self, owner, name):
            print("__set_name__")
            print(owner, name)
    
    
    class Cls:
    
        age = Descriptor()
    
        def __init__(self, age):
            self.age = age
    
    c = Cls(16)
    print(c.age)
    """
    __set_name__
    <class '__main__.Cls'> age
    __set__
    __get__
    16
    """
    # 当我执行c = Cls(16)的时候,执行__init__,self.age = age
    # 说明会触发__set__方法, 但是我们看到在执行__set__之前,先执行了__set_name__
    # __set_name__里面的owner还是类本身,name就是实例的属性名
    # 再通过self.name = name,把name设置到self里面去,注意这里的self,是描述符的self
    

    下面我们就可以实现了

    class Descriptor:
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.name]
    
        def __set__(self, instance, value):
            instance.__dict__[self.name] = value
    
        def __set_name__(self, owner, name):
            self.name = name
    
    class Cls:
    
        age = Descriptor()
    
        def __init__(self, age):
            self.age = age
    
    c = Cls(16)
    print(c.age)  # 16
    print(c.__dict__)  # {'age': 16}
    """
    此时的实例属性就被正确的设置进去了。
    """
    

    就我个人而言,还是更喜欢使用__init__的方式,比如:

    class Descriptor:
    
        def __init__(self, key):
            self.key = key
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.key]
    
        def __set__(self, instance, value):
            instance.__dict__[self.key] = value
    
    
    class Cls:
    
        # 可以同时让多个属性被代理
        name = Descriptor("name")
        age = Descriptor("age")
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    c = Cls("satori", 16)
    print(c.__dict__)  # {'name': 'satori', 'age': 16}
    """
    我们看到,可以通过手动指定属性名的方式
    """
    

    描述符的作用

    说了这么多,描述符的作用有哪些呢?我们之所以使用描述符,是为了某些场景实现起来比较方便,但是就目前来说,貌似和我们不使用描述符没啥区别啊。下面我们来看看描述符有哪些作用。

    类型检测

    python不是在语法层面上没有类型检测吗?那么我们就来手动实现一个。

    class Descriptor:
    
        def __init__(self, key, excepted_type):
            # self.key:属性名
            # self.excepted_key:期望的属性
            self.key = key
            self.excepted_type = excepted_type
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.key]
    
        def __set__(self, instance, value):
            if isinstance(value, self.excepted_type):
                instance.__dict__[self.key] = value
            else:
                raise TypeError(f"{self.key}期待一个{self.excepted_type}类型,但是你传了{type(value)}")
    
    
    class Cls:
    
        name = Descriptor("name", str)
        age = Descriptor("age", int)
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    try:
        c = Cls("satori", "16")
    except TypeError as e:
        print(e)  # age期待一个<class 'int'>类型,但是你传了<class 'str'>
    
    """
    当我们设置self.age的时候,会触发__set__方法
    value是我们传入的"16",这是一个字符串,但是我们在描述符中指定的self.excepted_type是int
    因此类型不对,所以报错。至于name,因为传入的类型是对的,所以不会报错。
    """
    

    表单验证

    有时候在html的input标签里面输入内容的时候,会有表单验证,那么我们也可以在python的层面上实现。

    class Descriptor:
    
        def __init__(self, key):
            self.key = key
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.key]
    
        def __set__(self, instance, value):
            if self.key == "phone":
                # 如果是手机号,那么必须是int类型,且11位、开头是1
                if isinstance(value, int) and len(str(value)) == 11 and str(value)[0] == 1:
                    instance.__dict__[self.key] = value
                else:
                    raise TypeError("不合法的手机号")
    
            elif self.key == "username":
                # 如果是用户名,必须要大于6位
                if isinstance(value, str) and len(value) > 6:
                    instance.__dict__["username"] = value
                else:
                    raise TypeError("不合法的用户名")
    
            elif self.key == "password":
                # 如果是密码,则长度大于8为,且必须同时包含大写、小写、数字、指定特殊字符当中的三种。
                import re
                flag1 = bool(re.search(r"[A-Z]", value))
                flag2 = bool(re.search(r"[a-z]", value))
                flag3 = bool(re.search(r"[0-9]", value))
                flag4 = bool(re.search(r"[._~!@#$%^&*]", value))
                if sum([flag1, flag2, flag3, flag4]) >= 3:
                    instance.__dict__["password"] = value
                else:
                    raise TypeError("不合法的密码")
    
    
    class PhoneField:
    
        phone = Descriptor("phone")
    
        def __init__(self, phone):
            self.phone = phone
    
    
    class UsernameField:
    
        username = Descriptor("username")
    
        def __init__(self, username):
            self.username = username
    
    
    class PasswordField:
        password = Descriptor("password")
    
        def __init__(self, password):
            self.password = password
    
    
    try:
        class Form:
            phone = PhoneField(135)
    except TypeError as e:
        print(e)  # TypeError: 不合法的手机号
    """
    注意到,我们还没实例化,就报错了。
    因为类在创建的时候,就会检测里面的属性,而Descriptor()这是一个调用,因此就执行了
    """
    
    try:
        class Form:
            username = UsernameField("ABCBD")
    
    except TypeError as e:
        print(e)  # 不合法的用户名
    
    
    try:
        class Form:
            password = PasswordField("satori123!!!")
    
    except TypeError as e:
        print(e)  
    """
    合法的,所以未报错
    """
    

    描述符实现property、staticmethod、classmethod

    我们在python中,通过给一个方法,加上property、staticmethod、classmethod之类的装饰器,那么可以改变这个方法的行为,那么我们便使用描述符来模拟一下。

    实现property

    首先python中property作用就是让一个方法可以以属性的形式访问,也就是不用加括号。

    class Property:
    
        def __init__(self, func):
            self.func = func
    
        def __get__(self, instance, owner):
    
            # 注意:此时的self.func是显然是Satori对象里面的一个函数
            # 函数都是属于类的,但是实例可以调用,并且自动传入self
            # 但是我们直接调用的话,不行。因为这相当于Satori.print_info()
            # 所以还需要把实例对象传进去,显然就是这里的instance,注意不是这里的self
            # 这个self是描述符的self,而instance才相当于是Satori这个类的self
            return self.func(instance)
    
    
    class Satori:
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        @Property
        def print_info(self):
            return f"name is {self.name}, age is {self.age}"
        """
        我们来解释一下,首先类也是可以作为装饰器的
        装饰器装饰完之后,等价于print_info = Property(print_info),等于是把print_info这个函数作为参数,传递给Property了
        那么之后再访问这个print_info,那么显然由于被我们的描述符Property代理了,所以走__get__方法
        """
    
    
    s = Satori("satori", 16)
    print(s.print_info)
    
    """
    可以看到,在不使用调用的情况下,也能执行函数,说明我们自己实现的Property和python内置的property是一样的。
    但是注意的是:我们这里的不使用调用,指的是我们自己定义的Satori这个类的实例对象在执行函数的时候可以不使用调用。
    这是因为在描述符中,已经帮我们调用了。
    
    可以看到,不管做什么变换,本质上都是一样的。
    该怎么传就怎么传,不存在所谓的会自动帮你传。我们在使用property的时候,之所以不用传调用,肯定是property在背后做了一些trick
    但是我们在实现自己的Property的时候,已经看到了,这是我们自己实现的,因此不再有人帮我们了。
    这就意味着,每一步都需要我们自己来操作,不管怎么做,即便我们Satori实例调用函数,不传调用
    那在描述符里面,也要进行调用。总之必须要有代码显式地进行调用,该怎么传就怎么传。
    我们在使用python内置的类进行装饰的时候,经常可以少传参数、不传调用,但之所以能实现,肯定是那些方法背后帮你做了很多事情。
    如果我们自己使用描述符实现那些方法的话,那么在描述符当中肯定还是要实现相应的逻辑,把少传的参数、或者调用补上去。
    正如这里的Property,即便实例对象调用print_info不用传调用,但是在描述符当中还是要传调用的。
    
    通过后面我们再手动实现staticmethod、classmethod就能更清晰地认识到
    """
    

    但是这里还有一个缺陷,我们来看一下

    class Satori:
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        @property
        def print_info(self):
            return f"name is {self.name}, age is {self.age}"
    
    
    print(Satori.print_info)  # <property object at 0x00000191BB8A5408>
    # 我们注意:如果是类去调用被property装饰的方法,那么返回的就是一个property对象
    # 但是我们的Property,则不是,还记得当类去访问的时候__get__里面的instance是什么吗?没错是None
    # 所以我们还要进行一层检测
    
    class Property:
    
        def __init__(self, func):
            self.func = func
    
        def __get__(self, instance, owner):
            if instance:
                return self.func(instance)
            # 如果instance为None,就把描述符实例返回回去
            return self
    
    
    class Satori:
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        @Property
        def print_info(self):
            return f"name is {self.name}, age is {self.age}"
    
    
    print(Satori.print_info)  # <__main__.Property object at 0x000002AC59EBEFC8>
    

    使用自定制的Property实现缓存

    class Property:
    
        def __init__(self, func):
            self.func = func
    
        def __get__(self, instance, owner):
            if instance:
                # 如果有这个属性,我们直接返回
                result = instance.__dict__.get("result", None)
                if result:
                    return f"走的是缓存:{result}"
                # 没有重新计算,然后设置进去
                result =  self.func(instance)
                instance.__dict__["result"] = result
                return result
            return self
    
    
    class Satori:
    
        def __init__(self, a, b):
            self.a = a
            self.b = b
    
        @Property
        def calc_mul(self):
            return self.a * self.b
    
    
    s = Satori(1234234314324213, 2312423123243254353)
    print(s.calc_mul)  # 2854071967943593129558534065549189
    print(s.calc_mul)  # 走的是缓存:2854071967943593129558534065549189
    

    实现staticmethod

    staticmethod就是让一个方法可以没有self这个参数,也就是变成静态方法。

    class StaticMethod:
    
        def __init__(self, func):
            self.func = func
    
        def __get__(self, instance, owner):
            # 此时的self.func是Satori.add
            # 因此我们直接返回,此时实例调用相当于是类调用,因为是Satori.add
            # 注意类调用的话,不会自动传递第一个参数。而我们的方法也不需要第一个参数
            # 所以直接返回即可
            return self.func
    
    
    class Satori:
    
        @StaticMethod  # add = StaticMethod(add)
        def add(a, b):
            return f"a + b = {a + b}"
    
    
    s = Satori()
    print(s.add(10, 20))  # a + b = 30
    

    实现classmethod

    classmethod就是让一个方法可以,也就是变成类方法。就是可以直接使用类进行调用的。

    class ClassMethod:
    
        def __init__(self, func):
            self.func = func
    
        def __get__(self, instance, owner):
            # 此时的self.func是Satori.add
            # 因此我们直接返回,此时实例调用相当于是类调用,因为是Satori.add
    
            # 当类调用add的时候,执行的显然是这里tmp
            # 里面使用*args和**kwargs将参数原封不动地接收进来
            def tmp(*args, **kwargs):
                # 注意类调用的话,不会自动传递第一个参数。
                # 但是又需要一个cls,因此我们手动传递,而这个cls显然就是owner
                return self.func(owner, *args, **kwargs)
            # 别忘了将tmp返回
            return tmp
    
    
    class Satori:
    
        c = 30
        @ClassMethod  # add = ClassMethod(add)
        def add(cls, a, b):
            return f"a={a}, b={b}, {a + b == cls.c}"
    
    
    print(Satori.add(10, 20))  # a=10, b=20, True
    """
    可以看到原本类调用方法,第一个参数是不会自动传的。
    类不会和实例一样,自动把自身作为第一个参数传进去。
    但是现在自动传了,说明我们在背后做了一些手脚,在描述符当中传递了。
    还是那句话,不能多传,也不能少传,该传几个就传几个。
    之所以可以少传,必然要在其它地方做一些手脚。
    """
    
    
    class A:
    
        c = 30
    
        def add(cls, a, b):
            return f"a={a}, b={b}, c={cls.c}"
    
    
    # 如果是这种情况,没有描述符,那么要是想少传递,就不可能了
    print(A.add(A, 10, 20))  # 10, b=20, c=30
    
    # 至于add里面的第一个参数我们起名叫cls,其实叫什么无所谓,但是一般我们都叫self
    # 关键看我们传的是什么,如果传的A,那么即便第一个参数叫self、不叫cls,那么这个self也是A,而不是A的实例对象
    # 同理,这里叫cls,但是我们传递A(),那么即使叫cls,这个cls也是A的实例对象,而不是A这个类
    print(A.add(A(), 10, 20))  # a=10, b=20, c=30
    # 当然这里依旧能访问成功,因为如果A的实例对象里面没有c这个属性,那么会自动去类里面找。
    
    
    # 我们再来举个栗子
    class Info:
    
        def __init__(self):
            self.info = {"name": "mashiro", "age": 16, "gender": "f"}
    
        def get(self, key):
            return self.info.get(key)
    
    
    info = Info()
    print(Info.get(info, "name"))  # mashiro
    # 以上显然是没有问题的
    
    # 但是
    class C:
        info = {"name": "古明地觉"}
    
    print(Info.get(C, "name"))  # 古明地觉
    print(Info.get(C(), "name"))  # 古明地觉
    """
    我们传入了C和C(),那么Info.add的self就是C、C()
    那么会从C里面获取info属性
    """
    

    おしまい

    以上就是描述符的用法,哦对了,还有一个_delete_

    class ClassMethod:
    
        def __get__(self, instance, owner):
            pass
        
        def __set__(self, instance, value):
            pass
        
        def __delete__(self, instance):
            pass
    

    至于__delete__,只接收一个instance,就是当执行del的时候会触发,这个很简单了就,可以自己去试一下。那么就到此结束啦。

  • 相关阅读:
    设置eclipse编码
    前端基础知识
    微信小程序
    jQuery下拉框
    Vue-cli的安装
    vue的数据交互形式
    node安装和小测试
    shui
    JQ-滚动条下拉无限的加载数据
    HTML-video全屏
  • 原文地址:https://www.cnblogs.com/traditional/p/11714356.html
Copyright © 2020-2023  润新知