【问题标题】:Best way to 'intelligently' reset memoized property values in Python when dependencies change当依赖关系发生变化时,在 Python 中“智能”重置记忆属性值的最佳方法
【发布时间】:2019-12-17 17:36:10
【问题描述】:

我正在编写一个具有各种属性的类,我只想在必要时进行计算(惰性评估)。但是,更重要的是,我想确保如果它们的计算所依赖的任何属性发生更改,则不会返回“陈旧”值。除了实现某种计算图(有没有办法做到这一点?)除了这涉及到很多setter方法以及相关计算的手动编码重置之外,我想不出任何好的方法价值观。

有没有更简单/更好或更不容易出错的方法来做到这一点? (我正在处理的实际应用程序比这个更大的计算图更复杂)

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,但我认为这是用于记忆函数结果而不是属性值。

【问题讨论】:

  • ...attributes that their calculation depended... - 它们都是实例/类属性吗?
  • 我只是在大声思考:Cylinder 是否需要是可变对象?也许您可以使其不可变并在某些属性更改时返回新的Cylinder
  • @wwii 是的,在我的应用程序中它们都是类属性。
  • @AndrejKesely 我实际使用的类有很多东西(其他属性和附加的东西)所以我不认为每次任何属性更改都从头开始重新生成它不会成为一个解决方案。
  • 有关更多背景信息,我正在使用的真实对象是一个随时间演变的仿真模型。每次迭代都会发生变化,但并非所有属性都需要重新计算(取决于用户在做什么),我希望获得最大的模拟速度。

标签: python properties lazy-evaluation memoization getattribute


【解决方案1】:

这是另一个有趣的解决方案,它使用了我发现的名为 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
>>> 

【讨论】:

    【解决方案2】:

    这是@Sraw's answer 的扩展版本,它将依赖图作为字典来确定需要重置哪些因变量。感谢@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
    

    【讨论】:

      【解决方案3】:

      这是一个描述符,可用于作为其他属性函数的属性。只有当它依赖的变量发生变化时,它才应该重新计算。

      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__ 方法中重新计算和设置似乎有点奇怪,但我暂时保留它。


      要使用描述符将其实例化为类属性;传递其名称、要使用的函数以及它所依赖的实例属性的名称。

      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 的方法似乎更清楚它是如何工作的,并且对于经验不足的程序员来说可能更容易(使用更简单的技术)。对其他人的想法感兴趣......不过,谢谢,这也是一个很好的解决方案!
      【解决方案4】:

      这里有一个解决方案:

      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__ 来委托所有集合操作。

      【讨论】:

      • 这很整洁。如果设置了任何自变量,它会在此处重置所有因变量,但当然我可以向__setattr__ 添加更多内容以使其更智能。谢谢,这似乎是迄今为止最简洁的解决方案。我喜欢依赖逻辑集中在一个地方的方式。