惰性计算(Lazy evaluation),是指仅仅在真正需要执行的时候才计算表达式的值。充分利用其特性可以带来很多便利。
- 避免不必要的计算,带来性能的提升。
- 节省空间,使得无线循环的数据结构成为可能。
将描述符与惰性运算结合起来。我们直接看代码:
class LazyProperty:
def __init__(self, fun):
self.fun = fun
def __get__(self, instance, owner):
print("get被调用")
if instance is None:
return self
value = self.fun(instance)
# 三个参数,第一个预设置的对象,第二个是预设置对象key,第三个是名下的value
setattr(instance, self.fun.__name__, value) # 给instance设置个属性{"self.fun__name__": value},self.fun指向的是area那个函数,
# self.fun__name__的意思就是获取这个函数的名字,所以这句的意思就是给a指向的实例对象是设置一个属性{"area": value}
return value
# def __set__(self, instance, value):
# pass
class ReadonlyNumber:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
raise AttributeError(f"{self.value} is not modifiable")
class Circle:
pi = ReadonlyNumber(3.14)
def __init__(self, radius):
self.radius = radius # 4
@LazyProperty # 等价于 area = LazyProperty(area),即LazyProperty创建了一个实例对象 = area
def area(self):
print("计算面积")
return self.pi * self.radius ** 2
a = Circle(4)
print(a.__dict__)
print(a.area)
print(a.__dict__)
print(a.area) # 这里要继续调用的话就直接取值,不用在进行计算了。
运行结果:
{'radius': 4}
get被调用
计算面积
50.24
{'radius': 4, 'area': 50.24}
50.24
进程已结束,退出代码0
现在来将这个代码拆开一步一步的看:
- 执行
a = Circle(4)
,给 a 指向的实例对象添加一个radius
的属性 - 执行
print(a.area)
,解释器就会去a的实例对象中寻找area
属性,发现没有,于是去a的实例对象的父类中去寻找,发现area
被LazyProperty
修饰器所修饰,因此这时就相当于
area = LazyProperty(area)
,使得area
指向一个类,所以area
是一个描述符。 - 获取
area
,于是调用了LazyProperty
中的get
方法,执行value = self.fun(instance)
其中self.fun = fun
于是self.fun
就是area
,instance
就是实例对象a,所以上面的语句为value = area(a)
. - 于是返回找
area
函数area
函数中定义了一个self.pi * self.radius ** 2
,此时radius
已知,于是找pi
,pi 描述符
指向一个类pi = ReadonlyNumber(3.14)
,于是直接去把值用。 - 执行完
area
函数后,返回get
,执行setattr(instance, self.fun.__name__, value)
,self.fun__name__
的意思就是获取这个函数的名字,所以这句的意思就是给a指向的实例对象是设置一个属性{"area": value}
- 于是代码执行完毕,而且在a中创建了一个属性
area
用来存储value
- 再次调用
a.area
是就会发生描述符名和属性名冲突的情况,但此时area
指向的实例对象中只有__get__
,即area
是一个非数据描述符,所以在与属性名冲突,属性名的优先级更高,所以print(a.area)
返回的是属性值50.24
, 而不是再把函数运行一遍。
如果我么将在LazyProperty
中再加一个set
函数,那么根据描述符和属性名冲突时的优先顺序,运行的结果就会发生区别了。
class LazyProperty:
def __init__(self, fun):
self.fun = fun
def __get__(self, instance, owner):
print("get被调用")
if instance is None:
return self
value = self.fun(instance)
# 三个参数,第一个预设置的对象,第二个是预设置对象key,第三个是名下的value
setattr(instance, self.fun.__name__, value) # 给instance设置个属性{"self.fun__name__": value},self.fun指向的是area那个函数,
# self.fun__name__的意思就是获取这个函数的名字,所以这句的意思就是给a指向的实例对象是设置一个属性{"area": value}
return value
def __set__(self, instance, value):
pass
class ReadonlyNumber:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
raise AttributeError(f"{self.value} is not modifiable")
class Circle:
pi = ReadonlyNumber(3.14)
def __init__(self, radius):
self.radius = radius # 4
@LazyProperty # 等价于 area = LazyProperty(area),即LazyProperty创建了一个实例对象 = area
def area(self):
print("计算面积")
return self.pi * self.radius ** 2
a = Circle(4)
print(a.__dict__)
print(a.area)
print(a.__dict__)
print(a.area) # 这里要继续调用的话就直接取值,不用在进行计算了。
运行结果:
{'radius': 4} # 第一次
get被调用
计算面积
50.24
{'radius': 4} #第二次
get被调用
计算面积
50.24
进程已结束,退出代码0
从上述代码可以看出,调用了两次print(a.area)
函数就执行了两次,所以没有完成惰性运算,这就是将非数据描述符LazyProperty
变成数据描述符的区别了。
下面时流程图: