描述符介绍

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

详解python中的描述符的更多相关文章

  1. 举例详解Python中的split()函数的使用方法

    这篇文章主要介绍了举例详解Python中的split()函数的使用方法,split()函数的使用是Python学习当中的基础知识,通常用于将字符串切片并转换为列表,需要的朋友可以参考下   函数:sp ...

  2. 详解Python中re.sub--转载

    [背景] Python中的正则表达式方面的功能,很强大. 其中就包括re.sub,实现正则的替换. 功能很强大,所以导致用法稍微有点复杂. 所以当遇到稍微复杂的用法时候,就容易犯错. 所以此处,总结一 ...

  3. 详解Python中内置的NotImplemented类型的用法

    它是什么? ? 1 2 >>> type(NotImplemented) <type 'NotImplementedType'> NotImplemented 是Pyth ...

  4. python2.7高级编程 笔记二(Python中的描述符)

    Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装饰器(decorator).对于大部分特性来说,这些" ...

  5. 详解Python中的循环语句的用法

    一.简介 Python的条件和循环语句,决定了程序的控制流程,体现结构的多样性.须重要理解,if.while.for以及与它们相搭配的 else. elif.break.continue和pass语句 ...

  6. 详解python中@的用法

    python中@的用法 @是一个装饰器,针对函数,起调用传参的作用. 有修饰和被修饰的区别,‘@function'作为一个装饰器,用来修饰紧跟着的函数(可以是另一个装饰器,也可以是函数定义). 代码1 ...

  7. Python Deque 模块使用详解,python中yield的用法详解

    Deque模块是Python标准库collections中的一项. 它提供了两端都可以操作的序列, 这意味着, 你可以在序列前后都执行添加或删除. https://blog.csdn.net/qq_3 ...

  8. 聊聊Python中的描述符

    描述符是实现描述符协议方法的Python对象,当将其作为其他对象的属性进行访问时,该描述符使您能够创建具有特殊行为的对象. 通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法所 ...

  9. 详解 Python 中的下划线命名规则

    在 python 中,下划线命名规则往往令初学者相当 疑惑:单下划线.双下划线.双下划线还分前后……那它们的作用与使用场景 到底有何区别呢?今天 就来聊聊这个话题. 1.单下划线(_) 通常情况下,单 ...

随机推荐

  1. Linux安装配置JDK1.8

    JDK1.8 链接:http://pan.baidu.com/s/1nvGBzdR 密码:ziqb 1  在/usr/local   文件夹下新建一个文件夹software ,将JDK放到此文件夹中 ...

  2. Visual Studio Code的设置及插件同步

    Visual Studio Code的设置及插件同步 使用Visual Studio Code开发有一段时间了,用起来是极其的顺手,但是唯独一点不爽的就是,Visual Studio Code不像Vi ...

  3. Data Exfiltration with DNS in MSSQL SQLi attacks

    DNS解析过程 DNS解析过程 DNS 查询的过程如下图1所示. 图1 文字举例说明: 假定浏览器想知道域名xprp8i.dnslog.cn的IP地址. 1.浏览器先向本地DNS服务器进行递归查询. ...

  4. 【C/C++开发】【VS开发】win32位与x64位下各类型长度对比

    64 位的优点:64 位的应用程序可以直接访问 4EB 的内存和文件大小最大达到4 EB(2 的 63 次幂):可以访问大型数据库.本文介绍的是64位下C语言开发程序注意事项. 1. 32 位和 64 ...

  5. [bzoj3162]独钓寒江雪_树hash_树形dp

    独钓寒江雪 题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=3162 题解: 首先,如果没有那个本质相同的限制这就是个傻逼题. 直接树形dp ...

  6. [转帖]字符编码笔记:ASCII,Unicode 和 UTF-8

    字符编码笔记:ASCII,Unicode 和 UTF-8 http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html 转帖 ...

  7. Wordpress 所有 hook 钩子

    muplugins_loaded 在必须使用的插件加载之后. registered_taxonomy 对于类别,post_tag 等 Registered_post_type 用于帖子,页面等 plu ...

  8. Linux系列之putty远程登录

    在工作中,我们通常都是通过远程操作Linux服务器的,因此必须熟悉一些远程登录的软件,在此使用的是putty,在Windows上安装putty软件,通过该软件访问Linux主机. 1.远程登录步骤 1 ...

  9. $listeners 在vue中的使用 --初学

    事件回传之 $listeners 组件由下向上回传事件 <!doctype html><html lang="en"> <head> <m ...

  10. P1541 乌龟棋(动态规划)

    (点击此处查看原题) 题意 此处有n个位置,记为1~n,每个位置上都对应一个权值,乌龟从编号为1的位置出发,利用m张爬行卡片到达位置n,爬行卡牌有四种,分别可以让乌龟移动1,2,3,4步,并保证将m张 ...