Python描述符【一】:定义、功能与调用

什么是描述符

如果一个对象定义了__get__, __set__, __del__三种特殊方法中的任意一种或者几种,该对象就是描述符对象。三种方法的签名如下所示:

__get__(self, obj, owner=None) -> value  # owner是托管类, instance是托管实例,描述符一般作为托管类的类属性来使用
__set__(self, obj, value) -> None
__del__(self, obj) -> None

包含__get__以及,__set__或者__del__特殊方法的对象称为数据型描述符(data descriptor);仅包含__get__特殊方法的描述符,称为非数据型描述符(non data descriptor)描述符通过实例化并作为托管类的类属性来管理托管实例的属性descriptor let objects customize attribute lookup, storage, deletion.)。
 

默认模式

obj.a为例,其中a是非描述符对象,obj.a会先在obj的__dict__属性中检查是否包含a属性,若包含则返回对应的值。否则查找对象的类及超类(不包括元类)的__dict__属性,检查是否包含a属性,若包含则返回。如果还未查找到指定的属性,并且__getattr__方法在类中被定义,则自动调用__getattr__方法,否则抛出AttributeError。

class Default(object):
    """ 探索attribute lookup的默认行为:instance.__dict__ >> type(instance).__dict__ >> getattr """
    a = 'i am class attribute'

    def __init__(self, var):
        self.a = var

    def __getattr__(self, name):
        print('{!r} __getattr__ is activated'.format(self))
        return 999

if __name__ == '__main__':
    obj = Default('test')
    print(obj.__dict__)  # {"a": "test"}
    print('*'*20)
    
    for k, v in Default.__dict__.items():
        print(f'{k} : {v}')
    print('*' * 20)
    
    print(obj.a) # return test
    del obj.__dict__['a']
    print('*' * 20)
    
    print(obj.a) # obj.__dict__中未检索到属性a,则从类的字典中检索并返回值
    print(getattr(obj, 'bb'))  # getattr会自动调用__getattr__方法

下图为执行结果:
在这里插入图片描述
注意__getattr__位于优先级的末端,对于属性lookup操作,会自动优先调用__getattribute__, 再调用__getattr__
另外默认情况下,实例属性的storage、deletion,是直接更新实例对象的属性字典__dict__,但是如果实例对应的类定义了__setattr__与__delattr__方法,这两个方法将会优先调用,而不是直接操作实例的字典(参考Data Model中的Class Instance)。
 

描述符调用方式

描述符调用有两种形式,第一种使用描述符对象直接调用,第二种是描述符中的特殊方法被自动调用,其中自动调用是更常见的形式Descriptors get invoked by the dot operator during attribute lookup. If a descriptor is accessed indirectly with vars(some_class)[descriptor_name], the descriptor instance is returned without invoking it.)(这句话的大致意思就是描述符通过点操作被自动激活,但是通过vars函数等间接方式会直接返回,不会被激活)。

当描述符被自动调用时,根据调用对象不同,会执行不同的逻辑,调用对象可分为“托管实例、托管类、super实例”。

托管实例调用描述符

当调用对象是托管实例时,会按照以下优先级顺序执行数据型描述符 >> 托管实例变量 >> 非数据型描述符 >> 托管类变量 >> __getattr__方法。由于点操作会自动调用__getattribute__方法,因此先看内置的__getattribute__实现逻辑,下列代码直接引用参考文献:

def object_getattribute(obj, name):  # obj是托管实例, name是属性名称
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()  #表示None
    objtype = type(obj) # 得到托管类
    cls_var = getattr(objtype, name, null)  # 得到托管类变量
    descr_get = getattr(type(cls_var), '__get__', null)  # 从托管类中直接过去__get__属性,直接返回__get__函数
    if descr_get is not null: 
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor  # 如果是数据型描述符,并且是实例调用,则调用描述符的__get__方法,第一个参数是描述符实例, 第二个参数表示托管类实例, 第三个参数表示托管类
    if hasattr(obj, '__dict__') and name in vars(obj):  # 在托管实例的字典中存在属性名
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:							# 如果仅定义了__get__方法,并且托管实例的可写字典中不存在属性名
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

上述的__getattribute__并未包含__getattr__的逻辑, 下面的代码将这两部分整合到一起:

def getattr_hook(obj, name):
    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
    try:
        return obj.__getattribute__(name)  # 先调用__getattribute__
    except AttributeError:  # 如果__getattribute__抛出异常
        if not hasattr(type(obj), '__getattr__'):  # 如果没有定义__getattr__直接抛出异常
            raise
    return type(obj).__getattr__(obj, name)             # __getattr__ # 如果定义了__getattr__,则调用

托管类调用描述符

由于描述符一般作为托管类的类属性,因此通过类调用时,会直接返回描述符实例对象(如果__get__方法被调用,__get__方法的第二个参数是None,表示托管实例为空)。

super实例调用描述符

super(A, obj).m()会转换成B.__dict__["m"].__get__(obj, A), 其中B = obj.__class__.__mro__[index+1], index=obj.__class__.__mro__.index[A]。在__get__方法调用中,最重要的是要确定第二个参数——实例对象的值。下列代码模拟实现内置的super类:

class Super(object):
	"""ref: https://www.python.org/download/releases/2.2.3/descrintro/#cooperation"""
    def __init__(self, type, obj=None):
        self.__type__ = type
        self.__obj__ = obj  # **important**

    def __get__(self, obj, type=None):
        if self.__obj__ is None and obj is not None:
            return Super(self.__type__, obj)
        else:
            return self

    def __getattr__(self, attr):
        if isinstance(self.__obj__, self.__type__):
            starttype = self.__obj__.__class__
        else:
            starttype = self.__obj__
        mro = iter(starttype.__mro__)
        for cls in mro:
            if cls is self.__type__:
                break
        # Note: mro is an iterator, so the second loop picks up where the first one left off!
        for cls in mro:
            if attr in cls.__dict__:
                x = cls.__dict__[attr]
                if hasattr(x, "__get__"):
                    x = x.__get__(self.__obj__)
                return x
        raise AttributeError


class A(object):
    def m(self):
        return "A"

class B(A):
    def m(self):
        return "B" + Super(B, self).m()

class C(A):
    def m(self):
        return "C" + Super(C, self).m()

class D(C, B):
    def m(self):
        return "D" + Super(D, self).m()

print(D().m())  # "DCBA"

 

实例

数据型描述符

对于属性lookup,数据型描述符具有最高优先级,数据型描述符的特殊方法会遮盖默认的属性访问行为。

class DataDesc(object):
	""" 实现数据型描述符类 """
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner=None): 
    	""" 实现get特殊方法,注意__get__协议包含三个参数,而非两个 """
        print('{!r} __get__ is activated!'.format(self))
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]  # 可以用其它逻辑代替

    def __set__(self, instance, value):
    	""" 实现set特殊方法 """
        print('{!r} __set__ is activated!'.format(self))
        if value < 0:
            raise ValueError('must greater 0!')
        else:
            instance.__dict__[self.name] = value  # 可以用其它逻辑代替
 
class Obj(object):
    """ 包含描述符实例的类 """
    quantity = DataDesc('quantity')  # 设置描述符实例与Obj实例属性quantity同名

    def __init__(self, description, quantity, price):
        self.description = description
        self.quantity = quantity
        self.price = price

    def cost(self):
        print(f'the total costs {self.quantity * self.price}')
    
if __name__ == '__main__':
    apple = Obj('apple', 10, 5)
    print('*' * 30)
    
    print(apple.quantity)
    print('*'*30)
    
    print(apple.__dict__)
    print('*' * 30)
    
    apple.quantity = 20
    print('*' * 30)
    
    print(apple.__dict__)
    print('*' * 30)
    print(apple.price)

数据型描述符中自定义的__get____set__方法自动调用。

在这里插入图片描述

非数据型描述符

非数据型描述符的优先级低于实例字典,即实例属性会遮盖实例方法。

class NonDataDesc(object):
    """ 实现非数据型描述符 """
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner=None):
        print('{!r} __get__ is activated!'.format(self))
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

class Obj(object):
    """ 包含描述符实例的类 """
    quantity = NonDataDesc('quantity')
    price = NonDataDesc('price')

    def __init__(self, description, quantity, price):
        self.description = description
        self.quantity = quantity
        self.price = price

    def cost(self):
        print(f'the total costs {self.quantity * self.price}')

if __name__ == '__main__':
    apple = Obj('apple', 10, 5)
    print('*' * 30)
    
    print(apple.quantity)
    print('*'*30)
    print(apple.price)
    print('*' * 30)
    apple.cost()
    print('*' * 30)
    
    print(apple.__dict__)
    print('*' * 30)
    
    apple.cost = 999
    print(apple.__dict__)
    print('*' * 30)
    print(apple.cost)
    # apple.cost() 报错,因为cost实例属性遮盖了cost实例方法

在这里插入图片描述

仅包含__set__的覆盖型描述符

从另一个角度,可以将描述符分为两类,当描述符包含__set__方法时,称为覆盖型描述符,否则称为非覆盖型描述符。

class Desc(object):
    """ 仅包含__set__方法的描述符 """
    def __init__(self, name):
        self.name = name

    def __set__(self, instance, value):
        print('{!r} __set__ is activated!'.format(self))
        if value < 0:
            raise ValueError('must greater 0!')
        else:
            instance.__dict__[self.name] = value

class Obj(object):
    quantity = Desc('quantity')

    def __init__(self, description, quantity, price):
        self.description = description
        self.quantity = quantity
        self.price = price

    def cost(self):
        print(f'the total costs {self.quantity * self.price}')

在这里插入图片描述
可以发现仅写操作别描述符的__set__方法接管,读操作会按照默认方式进行。
 

描述符实例名称与托管实例属性名称不一致

一般建议将描述符实例的名称设置为与托管实例属性同名,托管类是描述符实例的宿主类,托管实例就是托管类的实例化对象。下面代码表示描述符实例的名称可以不一致:

class Desc(object):
    """ 每一描述符实例有唯一标识符用于标识 """
    __counter = 1
    def __init__(self):  # 不需要传递名称参数了
        self.name = f"Desc_{Desc.__counter}"
        Desc.__counter += 1

    def __get__(self, instance, owner=None):
        print('{!r} __get__ is activated!'.format(self))
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
       print('{!r} __set__ is activated!'.format(self))
       if value < 0:
           raise ValueError('must greater 0!')
       else:
           instance.__dict__[self.name] = value

class Obj(object):
    quantity = Desc('quantity')
    price = Desc("price")

    def __init__(self, description, quantity, price):
        self.description = description
        self.quantity = quantity
        self.price = price

    def cost(self):
        print(f'the total costs {self.quantity * self.price}')

obj = Obj("apple", 10, 5)
print(obj.weight)  # 10
print(obj.__dict__)  # {'description': 'apple', 'Desc_1': 10, 'Desc_2': 5}

注意托管实例的名称,Desc1与Desc2是描述符实例化时生成的唯一标识符
 

小结

  1. 无论是lookup、storage、deletion属性操作,只要是通过dot operate形式,都会自动激活描述符调用逻辑,因为描述符的调用逻辑在__getattribute__中实现,因此不要轻易改写__getattribute__,否则描述符可能不能正常地运行。
  2. 数据型描述符的优先级高于实例字典,实例字典的优先级高于非数据型描述符。
  3. 建议将描述符实例的名称与托管实例的属性名称保持一致。
     

参考资料

  1. [technical-tutorial]https://docs.python.org/3/howto/descriptor.html#technical-tutorial
  2. 《流畅的python——第20章属性描述符》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值