'intelligently' 在依赖项更改时重置 Python 中的记忆 属性 值的最佳方法
Best way to 'intelligently' reset memoized property values in Python when dependencies change
我正在编写一个 class,其中包含我只想在必要时计算的各种属性(惰性求值)。但是,更重要的是,我想确保如果它们的计算所依赖的任何属性发生更改,则不会返回 'stale' 值。除了实现某种计算图(有没有办法做到这一点?)之外,我想不出任何好的方法来做到这一点,这涉及到很多 setter
方法和手工编码的重置相关计算值。
是否有 easier/better 或更不容易出错的方法来做到这一点? (我正在处理的实际应用程序比这个更复杂,计算图更大)
from math import pi
class Cylinder:
def __init__(self, radius, length, density):
self._radius = radius
self._length = length
self._density = density
self._volume = None
self._mass = None
@property
def volume(self):
if self._volume is None:
self._volume = self.length*pi*self.radius**2
print("Volume calculated")
return self._volume
@property
def mass(self):
if self._mass is None:
self._mass = self.volume*self.density
print("Mass calculated")
return self._mass
@property
def length(self):
return self._length
@length.setter
def length(self, value):
self._length = value
self._volume = None
self._mass = None
print("Volume and mass reset")
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
self._volume = None
self._mass = None
print("Volume and mass reset")
@property
def density(self):
return self._density
@density.setter
def density(self, value):
self._density = value
self._mass = None
print("Mass reset")
(打印语句暂时仅供解释)
这行得通。在解释器中:
>>> c = Cylinder(0.25, 1.0, 450)
>>> c.radius
0.25
>>> c.length
1.0
>>> c.density
450
>>> c.volume
Volume calculated
0.19634954084936207
>>> c.mass
Mass calculated
88.35729338221293
>>> c.length = c.length*2 # This should change things!
Volume and mass reset
>>> c.mass
Volume calculated
Mass calculated
176.71458676442586
>>> c.volume
0.39269908169872414
>>>
我能找到的最接近的答案是 this one,但我认为这是针对记忆函数结果而不是属性值。
这是一个解决方案:
from math import pi
class Cylinder:
_independent = {"length", "radius", "density"}
_dependent = {"volume", "mass"}
def __init__(self, radius, length, density):
self._radius = radius
self._length = length
self._density = density
self._volume = None
self._mass = None
def __setattr__(self, name, value):
if name in self._independent:
name = f"_{name}"
for var in self._dependent:
super().__setattr__(f"_{var}", None)
if name in self._dependent:
print("Cannot set dependent variable!")
return
super().__setattr__(name, value)
@property
def volume(self):
if self._volume is None:
self._volume = self.length*pi*self.radius**2
print("Volume calculated")
return self._volume
@property
def mass(self):
if self._mass is None:
self._mass = self.volume*self.density
print("Mass calculated")
return self._mass
@property
def length(self):
return self._length
@property
def radius(self):
return self._radius
@property
def density(self):
return self._density
想法是使用__setattr__
委托所有集合操作。
这是一个描述符,可用于作为其他属性函数的属性。它应该只在它依赖的变量发生变化时才重新计算。
from weakref import WeakKeyDictionary
class DependantAttribute:
"""Describes an attribute that is a fuction of other attributes.
Only recalculates if one of the values it relies on changes.
'interns' the value and the values used to calculate it.
This attribute must be set in the class's __init__
name - the name of this instance attribute
func - the function used to calculate the value
attributes - instance attribute names that this attribute relies on
must match function parameter names
mapping - not implemented: {attribute_name: function_parameter_name}
"""
def __init__(self, name, func, attributes):
self.name = name
self.func = func
self.attributes = attributes
#self.mapping = None
self.data = WeakKeyDictionary()
def __get__(self, instance, owner):
values = self.data.get(instance)
if any(getattr(instance,attr) != values[attr]
for attr in self.attributes):
value = self.recalculate(instance)
setattr(instance,self.name, value)
return self.data.get(instance)['value']
def __set__(self, instance, value):
# store the new value and current attribute values
values = {attr:getattr(instance,attr) for attr in self.attributes}
# validate?! : value == self.recalculate(**values)
values['value'] = value
self.data[instance] = values
def recalculate(self, instance):
# calculating a new value relies on
# attribute_name == function_parameter_name
kwargs = {attr:getattr(instance,attr) for attr in self.attributes}
return self.func(**kwargs)
这依赖于实例属性名称与函数的参数名称相同。虽然我没有在这里实现它,但可能有一个字典将实例属性名称映射到函数参数名称以解决任何不匹配问题。
虽然在 __get__
方法中重新计算和设置似乎有点 奇怪 ,但我暂时保留它。
要使用描述符将其实例化为class属性;传递其名称、要使用的函数以及它所依赖的实例属性的名称。
from math import pi
# define the functions outside the class
def volfnc(length, radius):
return length * pi * pow(radius,2)
def massfnc(volume, density):
return volume * density
class Cylinder:
volume = DependantAttribute('volume',volfnc, ('length','radius'))
mass = DependantAttribute('mass',massfnc, ('volume','density'))
def __init__(self, radius, length, density):
self.radius = radius
self.length = length
self.density = density
# the dependent attributes must be set in __init__
self.volume = volfnc(length,radius)
self.mass = massfnc(self.volume,density)
c = Cylinder(1,1,1)
d = Cylinder(1,2,1)
>>> c.volume, c.mass
(3.141592653589793, 3.141592653589793)
>>> d.volume, d.mass
(6.283185307179586, 12.566370614359172)
>>> c.radius = 2
>>> d.density = 3
>>> c.volume, c.mass
(12.566370614359172, 12.566370614359172)
>>> d.volume, d.mass
(6.283185307179586, 18.84955592153876)
这里是 的扩展版本,它将依赖图实现为字典,以确定哪些因变量需要重置。感谢@Sraw 为我指明了这个方向。
from itertools import chain
from math import pi
class Cylinder:
_dependencies = {
"length": ["volume"],
"radius": ["volume"],
"volume": ["mass"],
"density": ["mass"]
}
_dependent_vars = set(chain(*list(_dependencies.values())))
def __init__(self, radius, length, density):
self._radius = radius
self._length = length
self._density = density
self._volume = None
self._mass = None
def _reset_dependent_vars(self, name):
for var in self._dependencies[name]:
super().__setattr__(f"_{var}", None)
if var in self._dependencies:
self._reset_dependent_vars(var)
def __setattr__(self, name, value):
if name in self._dependent_vars:
raise AttributeError("Cannot set this value.")
if name in self._dependencies:
self._reset_dependent_vars(name)
name = f"_{name}"
super().__setattr__(name, value)
@property
def volume(self):
if self._volume is None:
self._volume = self.length*pi*self.radius**2
print("Volume calculated")
return self._volume
@property
def mass(self):
if self._mass is None:
self._mass = self.volume*self.density
print("Mass calculated")
return self._mass
@property
def length(self):
return self._length
@property
def radius(self):
return self._radius
@property
def density(self):
return self._density
这是另一个有趣的解决方案,使用我发现的名为 pythonflow 的包。它确实使构建计算图变得容易,但我不清楚它是否进行惰性求值。据我所知,它不存储或缓存值,您只能临时更改常量。如果我对这个包有更多了解,我会更新这个答案...
>>> import pythonflow as pf
>>> import math
>>> with pf.Graph() as graph:
... pi = pf.constant(math.pi)
... length = pf.constant(1.0)
... radius = pf.constant(0.25)
... density = pf.constant(450)
... volume = length*pi*radius**2
... mass = volume*density
...
>>> graph(volume)
0.19634954084936207
>>> graph(mass)
88.35729338221293
>>> graph(volume, {length: graph(length)*2})
0.39269908169872414
>>> graph(mass, {length: graph(length)*2})
176.71458676442586
>>>
我正在编写一个 class,其中包含我只想在必要时计算的各种属性(惰性求值)。但是,更重要的是,我想确保如果它们的计算所依赖的任何属性发生更改,则不会返回 'stale' 值。除了实现某种计算图(有没有办法做到这一点?)之外,我想不出任何好的方法来做到这一点,这涉及到很多 setter
方法和手工编码的重置相关计算值。
是否有 easier/better 或更不容易出错的方法来做到这一点? (我正在处理的实际应用程序比这个更复杂,计算图更大)
from math import pi
class Cylinder:
def __init__(self, radius, length, density):
self._radius = radius
self._length = length
self._density = density
self._volume = None
self._mass = None
@property
def volume(self):
if self._volume is None:
self._volume = self.length*pi*self.radius**2
print("Volume calculated")
return self._volume
@property
def mass(self):
if self._mass is None:
self._mass = self.volume*self.density
print("Mass calculated")
return self._mass
@property
def length(self):
return self._length
@length.setter
def length(self, value):
self._length = value
self._volume = None
self._mass = None
print("Volume and mass reset")
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
self._volume = None
self._mass = None
print("Volume and mass reset")
@property
def density(self):
return self._density
@density.setter
def density(self, value):
self._density = value
self._mass = None
print("Mass reset")
(打印语句暂时仅供解释)
这行得通。在解释器中:
>>> c = Cylinder(0.25, 1.0, 450)
>>> c.radius
0.25
>>> c.length
1.0
>>> c.density
450
>>> c.volume
Volume calculated
0.19634954084936207
>>> c.mass
Mass calculated
88.35729338221293
>>> c.length = c.length*2 # This should change things!
Volume and mass reset
>>> c.mass
Volume calculated
Mass calculated
176.71458676442586
>>> c.volume
0.39269908169872414
>>>
我能找到的最接近的答案是 this one,但我认为这是针对记忆函数结果而不是属性值。
这是一个解决方案:
from math import pi
class Cylinder:
_independent = {"length", "radius", "density"}
_dependent = {"volume", "mass"}
def __init__(self, radius, length, density):
self._radius = radius
self._length = length
self._density = density
self._volume = None
self._mass = None
def __setattr__(self, name, value):
if name in self._independent:
name = f"_{name}"
for var in self._dependent:
super().__setattr__(f"_{var}", None)
if name in self._dependent:
print("Cannot set dependent variable!")
return
super().__setattr__(name, value)
@property
def volume(self):
if self._volume is None:
self._volume = self.length*pi*self.radius**2
print("Volume calculated")
return self._volume
@property
def mass(self):
if self._mass is None:
self._mass = self.volume*self.density
print("Mass calculated")
return self._mass
@property
def length(self):
return self._length
@property
def radius(self):
return self._radius
@property
def density(self):
return self._density
想法是使用__setattr__
委托所有集合操作。
这是一个描述符,可用于作为其他属性函数的属性。它应该只在它依赖的变量发生变化时才重新计算。
from weakref import WeakKeyDictionary
class DependantAttribute:
"""Describes an attribute that is a fuction of other attributes.
Only recalculates if one of the values it relies on changes.
'interns' the value and the values used to calculate it.
This attribute must be set in the class's __init__
name - the name of this instance attribute
func - the function used to calculate the value
attributes - instance attribute names that this attribute relies on
must match function parameter names
mapping - not implemented: {attribute_name: function_parameter_name}
"""
def __init__(self, name, func, attributes):
self.name = name
self.func = func
self.attributes = attributes
#self.mapping = None
self.data = WeakKeyDictionary()
def __get__(self, instance, owner):
values = self.data.get(instance)
if any(getattr(instance,attr) != values[attr]
for attr in self.attributes):
value = self.recalculate(instance)
setattr(instance,self.name, value)
return self.data.get(instance)['value']
def __set__(self, instance, value):
# store the new value and current attribute values
values = {attr:getattr(instance,attr) for attr in self.attributes}
# validate?! : value == self.recalculate(**values)
values['value'] = value
self.data[instance] = values
def recalculate(self, instance):
# calculating a new value relies on
# attribute_name == function_parameter_name
kwargs = {attr:getattr(instance,attr) for attr in self.attributes}
return self.func(**kwargs)
这依赖于实例属性名称与函数的参数名称相同。虽然我没有在这里实现它,但可能有一个字典将实例属性名称映射到函数参数名称以解决任何不匹配问题。
虽然在 __get__
方法中重新计算和设置似乎有点 奇怪 ,但我暂时保留它。
要使用描述符将其实例化为class属性;传递其名称、要使用的函数以及它所依赖的实例属性的名称。
from math import pi
# define the functions outside the class
def volfnc(length, radius):
return length * pi * pow(radius,2)
def massfnc(volume, density):
return volume * density
class Cylinder:
volume = DependantAttribute('volume',volfnc, ('length','radius'))
mass = DependantAttribute('mass',massfnc, ('volume','density'))
def __init__(self, radius, length, density):
self.radius = radius
self.length = length
self.density = density
# the dependent attributes must be set in __init__
self.volume = volfnc(length,radius)
self.mass = massfnc(self.volume,density)
c = Cylinder(1,1,1)
d = Cylinder(1,2,1)
>>> c.volume, c.mass
(3.141592653589793, 3.141592653589793)
>>> d.volume, d.mass
(6.283185307179586, 12.566370614359172)
>>> c.radius = 2
>>> d.density = 3
>>> c.volume, c.mass
(12.566370614359172, 12.566370614359172)
>>> d.volume, d.mass
(6.283185307179586, 18.84955592153876)
这里是
from itertools import chain
from math import pi
class Cylinder:
_dependencies = {
"length": ["volume"],
"radius": ["volume"],
"volume": ["mass"],
"density": ["mass"]
}
_dependent_vars = set(chain(*list(_dependencies.values())))
def __init__(self, radius, length, density):
self._radius = radius
self._length = length
self._density = density
self._volume = None
self._mass = None
def _reset_dependent_vars(self, name):
for var in self._dependencies[name]:
super().__setattr__(f"_{var}", None)
if var in self._dependencies:
self._reset_dependent_vars(var)
def __setattr__(self, name, value):
if name in self._dependent_vars:
raise AttributeError("Cannot set this value.")
if name in self._dependencies:
self._reset_dependent_vars(name)
name = f"_{name}"
super().__setattr__(name, value)
@property
def volume(self):
if self._volume is None:
self._volume = self.length*pi*self.radius**2
print("Volume calculated")
return self._volume
@property
def mass(self):
if self._mass is None:
self._mass = self.volume*self.density
print("Mass calculated")
return self._mass
@property
def length(self):
return self._length
@property
def radius(self):
return self._radius
@property
def density(self):
return self._density
这是另一个有趣的解决方案,使用我发现的名为 pythonflow 的包。它确实使构建计算图变得容易,但我不清楚它是否进行惰性求值。据我所知,它不存储或缓存值,您只能临时更改常量。如果我对这个包有更多了解,我会更新这个答案...
>>> import pythonflow as pf
>>> import math
>>> with pf.Graph() as graph:
... pi = pf.constant(math.pi)
... length = pf.constant(1.0)
... radius = pf.constant(0.25)
... density = pf.constant(450)
... volume = length*pi*radius**2
... mass = volume*density
...
>>> graph(volume)
0.19634954084936207
>>> graph(mass)
88.35729338221293
>>> graph(volume, {length: graph(length)*2})
0.39269908169872414
>>> graph(mass, {length: graph(length)*2})
176.71458676442586
>>>