详解python中的描述符

描述符介绍

总所周知,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__是什么呢?

详解python中的描述符

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

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的时候会触发,这个很简单了就,可以自己去试一下。那么就到此结束啦。

详解python中的描述符

上一篇:【学亮IT手记】jQuery callback方法实例


下一篇:详解C++中继承的基本内容