什么是描述符
如果一个对象定义了__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是描述符实例化时生成的唯一标识符。
小结
- 无论是lookup、storage、deletion属性操作,只要是通过
dot operate
形式,都会自动激活描述符调用逻辑,因为描述符的调用逻辑在__getattribute__
中实现,因此不要轻易改写__getattribute__
,否则描述符可能不能正常地运行。 - 数据型描述符的优先级高于实例字典,实例字典的优先级高于非数据型描述符。
- 建议将描述符实例的名称与托管实例的属性名称保持一致。
参考资料
- [technical-tutorial]https://docs.python.org/3/howto/descriptor.html#technical-tutorial
- 《流畅的python——第20章属性描述符》