何为描述符以及为什么用描述符
描述符(descriptor)是定义了_get_()、_set_()和_del_()中一个或多个方法的类。
为何叫“描述”符呢?个人理解是描述符是一个辅助类,辅助对另一个类的属性进行“描述”。
举个例子:定义一个类是Person,那么其属性age不能是负数,“不能是负数”就是对age属性的描述。
首先想到的做法是在实例初始化函数_init_()中对age进行描述,但这种方法有个缺陷,就是如果对属性进行更改的话并不调用_init_(),那么"描述失效"了!
class Person:
def __init__(self, age):
if age < 0:
raise ValueError("age should be a nonnegtive value")
self.age = age
# p = Person(-2) # 报错
p = Person(2)
p.age = -2 # 不报错
自然而然地,我们想到在类中定义一个方法,这个方法用来对属性进行附加描述。并且无论何时赋值或修改该属性时,该描述方法均被调用。Python引入@property很好地实现了这一点。@property是一个装饰器(decorator),其功能是修饰方法,使其能够像访问属性那样调用方法。这样,定义方法与属性名相同就实现目的了。
class Person:
def __init__(self, age):
self.age = age # 初始化函数内同样需要检查
self._age = None
@property
def age(self):
return self._age # 避免无穷递归,引入辅助变量_age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("age should be a nonnegtive value")
self._age = value
p = Person(2)
# p.age = -2 # 报错
p.age = 3
@property功能非常强大,可以满足大部分需求,但是如果需要描述的属性非常多,并且“描述内容”一致时,那么采用@property就显得臃肿,因为做了很多重复性的描述工作。例如,Person类的属性height,weight等同样需要描述其大于零。一个自然的想法就是,将这种"描述内容"独立出来,并组成一个NonNegtive类,用类来描述属性(属性也是对象,此时就相当于实例化NonNegtive类得到的实例作为属性),这个类就是描述符。
class NonNegtive:
def __init__(self):
self.data = dict() # 属性(需要描述的对象)为key,其值为value
def __get__(self, instance, owner):
return self.data[instance]
def __set__(self, instance, value): # 这里instance就是待赋值的属性
if value < 0:
raise ValueError("age should be a nonnegtive value")
else:
self.data[instance] = value
class Person:
age = NonNegtive() # 对属性的描述内容是class-level的,故应该作为类属性。相当于将属性包装了一下而已
height = NonNegtive()
weight = NonNegtive()
def __init__(self, age, height, weight):
self.age = age
self.height = height
self.weight = weight
#P = Person(-18, 170, 150) # 报错
p = Person(18, 170, 150)
# p.weight = -100 # 报错
补充
装饰器可以是高阶函数也可以是类。无论是哪一类装饰器,其初始化时传入的参数均为方法(函数)。查看Python源码可以看到property是一个类,并且在类中定义了_set_()、_get_()等方法,所以property本身就是一个描述符。
我们知道@property可以使一个方法可以像访问属性一样调用和赋值,其本质就是将函数的计算过程包装隐藏起来,下面这段代码摘自《精通Python设计模式》一书,感觉对描述符的运用非常的Pythonic,故拿来剖析一下:
class LazyProperty:
def __init__(self, method):
self.method = method
self.method_name = method.__name__
# print('function overriden: {}'.format(self.method))
# print("function's name: {}".format(self.method_name))
def __get__(self, obj, cls):
if not obj:
return None
value = self.method(obj)
# print('value {}'.format(value))
setattr(obj, self.method_name, value)
return value
class Test:
def __init__(self):
self.x = 'foo'
self.y = 'bar'
self._resource = None
@LazyProperty
def resource(self):
print('initializing self._resource which is: {}'.format(self._resource))
self._resource = tuple(range(5)) # 假设代价大的计算
return self._resource
t = Test()
print(t.x)
print(t.y)
# 做更多的事情。。。
print(t.resource)
print(t.resource)
输出结果:
foo
bar
initializing self._resource which is: None
(0, 1, 2, 3, 4)
(0, 1, 2, 3, 4)
可以看到,计算代价大的代码块仅仅只运行了一次,并且是延迟计算,这就要归功于描述符了。resource是Test类中的一个方法,@LazyProperty将其作为参数传入LazyProperty类中,将其包装起来视为一个属性。第一次访问resources属性时,其没有值,故调用resource方法初始化,同时运用setattr方法将初始化结果赋予resource属性,即以方法名作为名称的属性。(该过程等同于为resource方法缓存计算结果)。第二次调用时直接返回属性值。