神奇的描述符(三):覆盖描述符与非覆盖描述符

描述符可以细分为覆盖型描述符与非覆盖型描述符。

可以按如下规则区分它们:

  • 实现 __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'>)

 

posted @ 2017-08-17 14:35  BlackMatrix  阅读(479)  评论(0编辑  收藏  举报