描述符介绍
总所周知,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: """ 一个类中,只要出现了__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 = Descriptor()要写在类属性里面 """ # 我们将描述符的__set__属性去掉了 # 注意:一个描述符既有__get__又有__set__,那么称这个描述符为属性描述符,如果只出现了__get__,而没有__set__,那么称之为非属性描述符。 # 此时我们这里的描述符显然就是非属性描述符 c = Cls("satori", 16) print(c.name) # satori """ 此时我们惊奇的发现居然没有走__get__方法。 可我们记得之前访问__get__的时候,走的是描述符的__get__方法啊。 其实那是因为之前的描述符有__set__方法 """ # 因此我们得出了一个结论 # 优先级:非属性描述符 < 实例属性 < 属性描述符 """ 就是当一个实例对象去访问被代理某个属性时候。 如果是属性描述符,那么会走__get__方法 但如果是非属性描述符,会从实例对象的属性字典里面去获取 """
现在我们知道了,描述符和实例属性之间的关系。但如果是类属性呢?
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 = 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: """ 一个类中,只要出现了__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 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: """ 一个类中,只要出现了__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, 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: """ 一个类中,只要出现了__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.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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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,也相当于是访问age。虽然即便name不改变,也是可以实现的,但是毕竟属性字典里面是name而不是age,这总归是不好的。但是问题来了,我们要如何获取被代理的属性的名称呢?这个时候__set_name__的作用就来了。
class Descriptor: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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: """ 一个类中,只要出现了__get__或者__set__方法,就被称之为属性描述符 """ 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的时候会触发,这个很简单了就,可以自己去试一下。那么就到此结束啦。