描述符
一、什么是描述符
Python为开发者提供了一个非常强大的功能——描述符。那什么是描述符呢?通过查看Python的官方文档,我们知道把实现了__get__()、__set__()和__delete__()中的其中任意一种方法的类称之为描述符,描述符的本质是新式类,并且被代理的类(即应用描述符的类)也是新式类。描述符的作用是用来代理一个类的属性,需要注意的是描述符不能定义在类的构造函数中,只能定义为类的属性,它只属于类的,不属于实例,我们通过查看实例和类的字典即可知晓。
描述符是可以实现大部分Python类特性中最底层的数据结构的实现手段,我们常使用的@classmethod、@staticmethd、@property、甚至是__slots__等属性都是通过描述符来实现的。它是很多高级库和框架的重要工具之一,是使用到装饰器或者元类的大型框架中的一个非常重要组件。在一般的开发中我们可能用不到描述符,但是我们如果想要开发一个大型的框架或者大型的系统,那使用描述符会起到如虎添翼的作用。它的加盟将会使得系统更加完美。下面将简单的介绍描述符的使用。
class Descriptors: """ 数据描述符 """ def __init__(self, key, expected_type): """ key: 用户传进的值 expected_type:用户传进的值的类型 """ self.key = key self.expected_type = expected_type """ 描述符的三个内置属性的参数如下: --------------------------------------------------- self: 是描述符的对象,不是使用描述符类的对象 instance: 这才是使用描述符类的对象 owner: 是instance的类 value: 是instance的值 --------------------------------------------------- """ def __get__(self, instance, owner): print("执行Descriptors的get") return instance.__dict__[self.key] #将参数存入实例的字典 def __set__(self, instance, value): print("执行Descriptors的set") #如果用户输入的值和值的类型不一致,则抛出TypeError异常 if not isinstance(value, self.expected_type): raise TypeError("参数%s必须为%s"%(self.key, self.expected_type)) instance.__dict__[self.key] = value #为实例字典的key设值 def __delete__(self, instance): print("执行Descriptors的delete") instance.__dict__.pop(self.key) #删除实例字典的key class Light: #使用描述符 name = Descriptors("name",str) price = Descriptors("price",float) def __init__(self, name, price): self.name = name self.price = price #设置两个参数,触发两次set的执行 light = Light("电灯泡", 66.66) print(light.__dict__) light.name = "火箭筒" print(light.__dict__) """ 执行Descriptors的set 执行Descriptors的set {'name': '电灯泡', 'price': 66.66} 执行Descriptors的set {'name': '火箭筒', 'price': 66.66} """
二、描述符的种类及优先级
描述符分为数据描述符和非数据描述符。把至少实现了内置属性__set__()和__get__()方法的描述符称为数据描述符;把实现了除__set__()以外的方法的描述符称为非数据描述符。之所以要区分描述符的种类,主要是因为它在代理类属性时有着严格的优先级限制。例如当使用数据描述符时,因为数据描述符大于实例属性,所以当我们实例化一个类并使用该实例属性时,该实例属性已被数据描述符代理,此时我们对该实例属性的操作是对描述符的操作。描述符的优先级的高低如下:
类属性 > 数据描述符 > 实例属性 > 非数据描述符 > 找不到的属性触发__getattr__()
1. 类属性 > 数据描述符
在以下实验中,使用 Light.name = "电灯泡" 语句没有触发set的执行说明类属性的优先级大于数据描述符的优先,此时相当于类属性覆盖了数据描述符,从而说明对类属性的一切操作都与描述符无关。
class Descriptors: """ 数据描述符 """ def __get__(self, instance, owner): print("执行Descriptors的get") def __set__(self, instance, value): print("执行Descriptors的set") def __delete__(self, instance): print("执行Descriptors的delete") class Light: #使用描述符 name = Descriptors() #测试 Light.name #执行描述符的get内置属性 print(Light.__dict__) #此时的name显示的是描述符的对象 Light.name = "电灯泡" #没有执行描述符的set内置属性 print(Light.name) #输出:电灯泡 del Light.name #没有执行描述符的delete内置属性 print(Light.name) #报错,因为Light类中的name被删了
2. 数据描述符 > 实例属性
在以下实验中,数据描述符的优先级大于实例属性的优先级,此时实例属性name被数据描述符所覆盖,而price没有描述符代理,所以它任然是实例属性。
class Descriptors: """ 数据描述符 """ def __get__(self, instance, owner): print("执行Descriptors的get") def __set__(self, instance, value): print("执行Descriptors的set") def __delete__(self, instance): print("执行Descriptors的delete") class Light: #使用描述符 name = Descriptors() def __init__(self, name, price): self.name = name self.price = price #使用类的实例对象来测试 light = Light("电灯泡",60) #执行描述符的set内置属性 light.name #执行描述符的get内置属性 print(light.__dict__) #查看实例的字典,不存在name print(Light.__dict__) #查看类的字典,存在name(为描述符的对象) del light.name #执行描述符的delete内置属性
3. 实例属性 > 数据描述符
在以下实验中,如果我们的实例属性中使用了非数据描述符,就不能对其进行复制操作。可见非数据描述符应该应用于不需要设置值的属性或者函数中。上述的设计没有多大的意义,只是增加对描述符的理解。
class Descriptors: """ 非数据描述符 """ def __get__(self, instance, owner): print("执行Descriptors的set") def __delete__(self, instance): print("执行Descriptors的delete") class Light: #使用描述符 name = Descriptors() def __init__(self, name, price): self.name = name self.price = price #测试 light = Light("电灯泡",60) #报错,描述符中没有__set__()方法
经以下实验证明在该类中并没有set方法,所以该类是一个非数据描述符,Python中一切皆对象,函数也是一个对象,既然是对象那也可以是类实例化所得到的结果。函数在类中本身也是一种属性(函数属性),描述符在应用的时候也是被实例化为一个属性。
class Descriptors: """ 非数据描述符 """ def func(self): print("世界的变化真快!近日00后都已经开始在街头打小三了") d = Descriptors() d.func() print(hasattr(Descriptors.func,"__set__")) #False print(hasattr(Descriptors.func,"__get__")) #True print(hasattr(Descriptors.func,"__delete__")) #False d.func = "函数也是属性,也可以赋值,没毛病" print(d.func) del d.func d.func
三、描述符模拟系统的内置属性
此利用描述符的原理,我们完全可以自定义模拟@classmethod、@staticmethd、@property、等属性。实现这种类似系统的属性,我们还需要装饰器作为修饰,结合装饰器做成一个描述符。下面将简单的介绍使用描述符模拟系统自带的装饰器。
1. 模拟 @classmethod
class Imitate_classmethod: """ 使用描述符模拟@classmethod """ def __init__(self, func): self.func = func def __get__(self, instance, owner): #对传进函数进行加工,最后返回该函数 def machining_func(*args, **kwargs): print("函数加工处理后,返回实例的类") return self.func(owner, *args, **kwargs) return machining_func class Test: book_name = "从你的世界路过" #使用装饰器的结果等价于将函数变为属性: # book = Imitate_classmethod( book ) @Imitate_classmethod def book_1(cls): print("这本书的名字是:%s"%cls.book_name) @Imitate_classmethod def book_2(cls, price): print("这本书的名字是:%s;价格是 %s"%(cls.book_name, price)) #测试 Test.book_1() Test.book_2(28.5) """ 运行输出: --------------------------------------------- 函数加工处理后,返回实例的类 这本书的名字是:从你的世界路过 函数加工处理后,返回实例的类 这本书的名字是:从你的世界路过;价格是 28.5 --------------------------------------------- """
2. 模拟 @staticmethod
staticmethod方法与classmethod方法的区别在于classmethod方法在使用需要传进一个类的引用作为参数。而staticmethod则不用。
class Imitate_staticmethod: """ 使用描述符模拟@staticmethod """ def __init__(self, func): self.func = func def __get__(self, instance, owner): #对传进函数进行加工,最后返回该函数 def machining_func(*args, **kwargs): print("函数加工处理后,返回实例的类") return self.func(*args, **kwargs) return machining_func class Test: @Imitate_staticmethod def static_func(*args): print("您输入的是:",*args) #测试 Test.static_func("柠檬","香蕉") test = Test() test.static_func(110, 112) """ 运行输出: ------------------------------------------------------------------- 函数加工处理后,返回实例的类 您输入的是: 柠檬 香蕉 函数加工处理后,返回实例的类 您输入的是: 110 112 ------------------------------------------------------------------- """
3. 模拟 @property
在以下实验中,我们将描述符的回调结果存入对象字典中的好处是我们以后再执行函数时就不会每一次都触发描述的运行,从而提高程序的效率。这样,我们有再执行同样的函数时,解释器会先检索对象的字典,如果字典存在上次执行结果的值,那就不用触发描述符的运行了。在这个实验中必须强调的一点是描述符的优先级,我们想让程序的描述符不能覆盖实例属性就必须使用非数据描述符。所以因需求不同,描述符的优先级也不同。
class Imitate_property: """ 使用描述符模拟property """ def __init__(self, func): self.func = func def __get__(self, instance, owner): if instance is None: return self #回调传入的函数,将运行结果保存在变量res中 res = self.func(instance) #为函数名func.__name__设置一个值res后存入对象的字典中 setattr(instance, self.func.__name__, res) return res class Test: def __init__(self, value): self.value = value @Imitate_property def function(self): return self.value**2 test = Test(2) print(test.function) #输出:4 print(test.__dict__) #输出:{'value': 2, 'function': 4} print(test.function) #输出:4 print(test.function) #输出:4 print(test.__dict__) #输出:{'value': 2, 'function': 4}