神奇的描述符(三):覆盖描述符与非覆盖描述符
描述符可以细分为覆盖型描述符与非覆盖型描述符。
可以按如下规则区分它们:
- 实现 __set__ 方法的类,称之为“覆盖型描述符”
- 没有实现 __set__ 方法的类,但实现了__get__方法的类,称之为“非覆盖型描述符”
- 同时实现 __set__ 和 __get__ 方法的类,通常称之为“强制描述符”。
先分别定义三种类型的描述符,及它们托管的类,为后续的测试及验证做准备。
class OverAll: """ 描述符是指含有__get__ 、 __set__ 、__delete__方法的类, 对于实现了 __set__方法的类,称之为覆盖描述符, 如果同时也实现了 __get__ 方法,则称之为强制描述符 """ def __get__(self, instance, owner): print("强制描述符 __get__ 被运行") return self, instance, owner def __set__(self, instance, value): print("强制描述符 __set__ 被运行") return self, instance, value def __delete__(self, instance): print("强制描述符 __delete__ 被运行") class OnlySet: """ 对于覆盖型描述符,因为实现了__set__方法, 会覆盖掉实例属性的赋值操作 但不会影响类属性的赋值操作 """ def __set__(self, instance, value): print("覆盖型描述符 __set__ 被运行") return self, instance, value class OnlyGet: """ 如果描述符没有实现 __set__ 方法(即只有 __get__方法), 则称之为非覆盖型描述符 """ def __get__(self, instance, owner): print("非覆盖型描述符 __get__ 被运行") return self, instance, owner class Spam: """ 定义一个类,类属性是上面三种描述符 """ over_all = OverAll() only_set = OnlySet() only_get = OnlyGet() # 实例化Spam spam = Spam()
覆盖型描述符
通常情况下,覆盖型描述符会同时实现__get__与__set__方法,但也可以只实现__set__方法。如果在描述符中定义了__set__方法,那么描述符会接管对这个实例属性的赋值。
在下面这个例子中,对only_set进行赋值,会触发描述符的__set__方法,打印出“覆盖型描述符 __set__ 被运行”
spam.only_set = 2
如果直接操作实例属性的__dict__对其进行赋值,描述符不会生效,即使定义了__set__方法。
spam.__dict__['only_set'] = '描述符不会执行'
如果覆盖型描述符没有定义__get__方法,那么描述符不会接管实例属性的读取,所以从实例读取only_set时,实际上读取的是类属性only_set,所以会返回描述符对象。
print(spam.only_set) # <__main__.OnlySet object at 0x000001A3C49575C0>
需要额外注意的是,描述符的__set__方法,只能接管实例属性的赋值,无法接管类属性的赋值,所以对类属性的赋值是无能为力的。
下面的例子可以看到,即使完整定义了__get__、__set__、__delete__方法的强制描述符, 都无法接管类属性的赋值。
# 对类属性描述符进行赋值,会直接把描述符覆盖掉,并且不会触发描述符的__set__方法 Spam.over_all = 1 # 因为作为类属性的描述符已经被覆盖,所以会打印出 1 print(Spam.over_all)
如果需要使用描述符接管类属性的赋值操作,需要通过在元类中定义描述符来实现。
非覆盖型描述符
当通过实例访问非覆盖型描述符时,描述符的__get__方法会被执行,打印出“非覆盖型描述符 __get__ 被运行”的字符串。
# 获取实例的非覆盖型描述符时,会发现描述符的 __get__ 方法被执行 print(spam.only_get) # 非覆盖型描述符 __get__ 被运行 # (<__main__.OnlyGet object at 0x000002C120637668>, <__main__.Spam object at 0x000002C1206376A0>, <class '__main__.Spam'>)
与__set__方法不同的是,__get__方法不仅会接管实例属性的访问,还会接管类属性的访问。
下面的例子中,通过类直接访问only_get,描述符的__get__方法被执行,打印出“非覆盖型描述符 __get__ 被运行”的字符串。
print(Spam.only_get) # 非覆盖型描述符 __get__ 被运行 # (<__main__.OnlyGet object at 0x00000283A7C27630>, None, <class '__main__.Spam'>)
如果对实例属性赋值,因为非覆盖型描述符进行*没有*定义__set__方法,无法接管对实例属性的赋值,会在实例中创建一个同名的属性,导致描述符失效。
# 对实例的非覆盖描述符进行赋值,因为没有 __set__ 方法 # 所以描述符本身没有接管实例属性的赋值操作 spam.only_get = 3 # 这个时候,会实例属性进行取值,就会发现描述符的 __get__ 方法也不会执行 # 所以打印结果是 3 print(spam.only_get)
出现这种情况的原因是通过实例去获取属性时,优先返回的是实例自身的属性,只有在实例自身没有这个属性的情况下,才会到类中查找对应的属性。而描述符本身也是属于类属性,所以受到这个规则的影响。
spam.only_get = 3 ,对 only_get 进行赋值时,实际上已经在实例中创建出一个同名的属性 only_get, 后续再对 only_get 进行读取,获取的是实例的属性only_get,而不是作为类属性的描述符 only_get。所以 描述符的 __get__ 方法不会触发。
《Python Cookbook》8.10 让属性具有惰性求值的能力,即是用非覆盖型描述符,让实例属性的读取实现惰性求值,以提高执行性能。核心的逻辑就是在描述符执行__get__方法时,同时对同名实例属性进行赋值,覆盖掉描述符自身。那么在下次读取时,直接读取的便是实例自身的属性,不会再调用描述符的__get__进行运算,以达到提高性能的目的。
如果需要恢复描述符的正常功能,需要将对应的实例属性删除。下面的代码中,删除了实例属性only_get之后,描述符恢复正常工作。
# 将实例属性删除,再获取一次 spam.only_get ,就会发现描述符的__get__正常执行了 del spam.only_get print(spam.only_get) # (<__main__.OnlyGet object at 0x0000017EFBAC7668>, <__main__.Spam object at 0x0000017EFBAC76A0>, <class '__main__.Spam'>)