你真的了解 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 属性就被描述符代理了
    """
    
    name = Descriptor()

    def __init__(self, name, age):
        self.name = name
        self.age = age

        
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:

    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__ 方法啊。
其实那是因为代理 self.name 的是数据描述符,但这里由于描述符是非数据描述符,也就是没有 __set__ 方法
"""
# 因此我们得出了一个结论
# 优先级:非数据描述符 < 实例属性 < 数据描述符
"""
就是当一个实例对象去访问被代理某个属性时候(通过 . 的方式),如果实例内部没有这个属性的话,显然会触发 __get__
但如果实例有这个属性,那么会分为两种情况:
    如果是数据描述符,那么会无条件走 __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,因为我们已经写死了,所以我们需要获取到相应的属性名称。但是问题来了,我们要如何获取被代理的属性的名称呢?这个时候 __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):
        # 这里的 name 就是字符串 "age"
        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 时候会触发,这个很简单了就,可以自己去试一下,那么就到此结束啦。

posted @ 2019-10-21 16:35  古明地盆  阅读(1438)  评论(0编辑  收藏  举报