什么是描述符(descriptor)描述符是一个特殊的Python对象,该对象定义了__get__、__set__、__delete__三个方法中的一个或多个;
描述符单独存在没有意义,一般描述符就是某个实例的属性,当访问该属性时,就会调用描述符的__get__方法,而修改会调用__set__方法,删除会调用__delete__方法;
描述符是对多个属性运用相同存取逻辑的一种方式。
描述符的种类
描述符分两种:覆盖型描述符和非覆盖型描述符,实现了__set__方法的就叫覆盖型描述符,没有实现就叫非覆盖型描述符;
这两种描述符的区别就是访问的优先级不同,访问属性的优先级如下:首先会先调用实例的 __getattribute__方法,然后依次按如下顺序去获取属性,获取到就返回:如果访问的属性是覆盖型描述符,则调用覆盖型描述符的__get__方法获取属性(由 __getattribute__调用)
从实例的__dict__字典获取属性
从实例所属类的__dict__字典获取属性
如果访问的属性是非覆盖型描述符,则调用非覆盖型描述符的__get__方法获取属性
从实例父类的__dict__字典获取属性
如果上述都未找到,则调用实例的 __getattr__方法
通过一个例子来验证下:
def cls_name(obj_or_cls):
cls = type(obj_or_cls)
if cls is type:
cls = obj_or_cls
return cls.__name__.split('.')[-1]
def display(obj):
cls = type(obj)
if cls is type:
return ''.format(obj.__name__)
elif cls in [type(None), int]:
return repr(obj)
else:
return '<{} object>'.format(cls_name(obj))
def print_args(name, *args):
pseudo_args = ', '.join(display(x) for x in args)
print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
class Overriding:
"""覆盖型描述符"""
def __init__(self, name):
self.name = name
def __get__(self, instance, owner): # self是描述符对象,instance是Managed的实例(也就是描述符对象所在类的实例),owner是Managed(也就是描述符对象所在的类)
print_args('get', self, instance, owner)
if instance is None:
return self
else:
return self.name
def __set__(self, instance, value):
print_args('set', self, instance, value)
self.name = value
class OverridingNoGet:
"""没有__get__方法的覆盖型描述符"""
def __init__(self, name):
self.name = name
def __set__(self, instance, value):
print_args('set', self, instance, value)
self.name = value
class NonOverriding:
"""非覆盖型描述符"""
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
if instance is None:
return self
else:
return self.name
class Managed:
over = Overriding("init_over_value")
over_no_get = OverridingNoGet("init_over_no_get_value")
non_over = NonOverriding("init_non_over_value")
def spam(self):
print('-> Managed.spam({})'.format(display(self)))
obj = Managed()
覆盖型描述符测试:
>>> obj.__dict__['over'] = "instance_over_value1" # 添加实例属性
>>> obj.__dict__
{'over': 'instance_over_value1'}
>>> obj.over # 由于over属性是一个覆盖型描述符,所以优先调用了描述符对象的__get__方法获取属性
-> Overriding.__get__(, , )
'init_over_value'
>>> obj.over = "instance_over_value2" # 修改over属性,可以看到调用了描述符的__set__方法
-> Overriding.__set__(, , )
>>> obj.over
-> Overriding.__get__(, , )
'instance_over_value2'
没有__get__方法的覆盖型描述符测试:
>>> obj.__dict__['over_no_get'] = "instance_over_no_get_value1" # 添加实例属性
>>> obj.__dict__
{'over_no_get': 'instance_over_no_get_value1'}
>>> obj.over_no_get # 访问over_no_get属性,由于over_no_get描述符对象没有__get__方法,所以从实例的__dict__获取属性
'instance_over_no_get_value1'
非覆盖型描述符测试:
>>> obj.__dict__['non_over'] = "instance_non_over_value1"
>>> obj.__dict__
{'non_over': 'instance_non_over_value1'}
>>> obj.non_over # 可以看到访问non_over属性没有调用描述符对象的__get__方法,而是优先从实例的__dict__获取属性
'instance_non_over_value1'
类的属性访问测试:
>>> Managed.__dict__
mappingproxy({'over': , '__dict__':
s>, 'spam': , 'non_over': , '
__doc__': None, '__weakref__': , '__module__': 'descriptor', 'over_no_get':
ptor.OverridingNoGet object at 0x0000000002644C18>})
>>> Managed.over
-> Overriding.__get__(, None, )
>>> Managed.over = 1 # 属性重新赋值
>>> Managed.over
1
property
property是Python自带的描述符。
给类的方法加上property装饰器可以使方法变为一个描述符,该描述符可以实现对属性值的校验等功能,如下代码所示:
class Item:
@property # 等价于price = property(price),也就是实例化了一个描述符对象price
def price(self):
return self._price
@price.setter # 使用描述符对象的setter方法对price进行装饰(price = price.setter(price)),这里就是将设置属性值的方法传入描述符内,修改price属性时就会通过__set__方法调用传入的方法
def price(self, value):
if value > 0: # 校验price
self._price = value
else:
raise ValueError("Valid value..")
item = Item()
item.price = 100
print(item.price)
item.price = -100 # 会抛出ValueError异常
property的实现原理:
# property2.py
class MyProperty(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
print("====>in __get__")
if obj is None:
return self
if self.fget is None:
raise AttributeError
return self.fget(obj)
def __set__(self, obj, value):
print("====>in __set__")
if self.fset is None:
raise AttributeError
self.fset(obj, value)
def __delete__(self, obj):
print("====>in __delete__")
if self.fdel is None:
raise AttributeError
self.fdel(obj)
def getter(self, fget):
pass
def setter(self, fset):
print("====>in setter")
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
print("====>in deleter")
return type(self)(self.fget, self.fset, fdel, self.__doc__)
class Item:
@MyProperty
def price(self):
return self._price
@price.setter
def price(self, value):
if value > 0:
self._price = value
else:
raise ValueError("Valid value..")
测试:
>>> from property2 import Item
====>in setter # 创建类的时候就开始装饰了
>>> item = Item()
>>> item.price = 100
====>in __set__
>>> item.price
====>in __get__
100
>>> item.price = -100
====>in __set__
Traceback (most recent call last):
File "", line 1, in
File "D:\mycode\localtest\property2.py", line 20, in __set__
self.fset(obj, value)
File "D:\mycode\localtest\property2.py", line 50, in price
raise ValueError("Valid value..")
ValueError: Valid value..
除了property,Python还自带了其他的描述符,如staticmethod和classmethod,有兴趣可以自行了解。
应用案例
1.利用描述符实现缓存和只读属性
class LazyProperty(object):
"""实现缓存(只计算一次,并将结果缓存)利用了 obj.__dict__访问优先级高于非覆盖型描述符的特性第一次调用 __get__ 以同名属性存于实例字典中,之后就不再调用 __get__"""
def __init__(self, fun):
self.fun = fun
def __get__(self, instance, owner):
if instance is None:
return self
value = self.fun(instance)
setattr(instance, self.fun.__name__, value)
return value
class ReadonlyNumber(object):
"""实现只读属性(实例属性初始化后无法被修改)利用了覆盖型描述符访问优先级高于 obj.__dict__ 的特性当试图对属性赋值时,总会先调用 __set__ 方法从而抛出异常"""
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
raise AttributeError(
"'%s' is not modifiable" % self.value
)
class Circle(object):
pi = ReadonlyNumber(3.14)
def __init__(self, radius):
self.radius = radius
@LazyProperty
def area(self):
print('====>Computing area')
return self.pi * self.radius ** 2
circle = Circle(5)
print(circle.area)
# 输出:
# ====>Computing area # 只计算一次
# 78.5
print(circle.area)
# 输出:
# 78.5
circle.pi = 3.1415
# 抛异常
2.简化代码
上面讲了property可以用来做属性校验,但使用property有个缺点就是如果一个类里有很多属性需要校验,而且校验逻辑都一样的话,使用property就会让代码不利于维护,如下所示:
# Item类里有两个属性price和weight需要进行属性校验
class Item:
def __init__(self, price, weight):
self.price = price
self.weight = weight
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value > 0:
self._price = value
else:
raise ValueError("Valid value..")
@property
def weight(self):
return self._weight
@weight.setter
def weight(self, value):
if value > 0:
self._weight = value
else:
raise ValueError("Valid value..")
item = Item(100, 10)
print(item.price)
print(item.weight)
item = Item(100, -10) # 抛异常
可以看到price和weight的校验逻辑是一样的,也就是说代码冗余了。我们可以通过描述符来简化代码,代码如下:
class CheckDescriptor:
"""用来进行属性校验的描述符"""
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('Valid value..')
class Item:
weight = CheckDescriptor('weight')
price = CheckDescriptor('price')
def __init__(self, price, weight):
self.price = price
self.weight = weight
item = Item(100, 10)
print(item.price)
print(item.weight)
item = Item(100, -10) # 抛异常