【问题标题】:Storing calculated values in an object将计算值存储在对象中
【发布时间】:2017-05-04 20:33:38
【问题描述】:

最近一直在写一堆这样的代码:

class A:
  def __init__(self, x):
    self.x = x
    self._y = None

  def y(self):
    if self._y is None:
      self._y = big_scary_function(self.x)

    return self._y

  def z(self, i):
    return nice_easy_function(self.y(), i)

在给定的类中,我可能有许多类似y 的东西,我可能还有其他使用存储的预计算值的东西。这是做事的最佳方式还是您会推荐不同的方式?

请注意,我在这里没有预先计算,因为您可能会使用A 的实例而不使用y

我已经用 Python 编写了示例代码,但如果相关,我会对其他语言的特定答案感兴趣。相反,我想听听 Pythonistas 的意见,他们是否觉得这段代码是 Pythonic。

【问题讨论】:

  • 只要您可以懒惰地评估变量,我认为这没有什么问题。如果您正在渲染图形或类似的东西,您可能需要提前计算。
  • 还有一种使用__getattr__ 的替代方法。这里简单展示一下:rhodesmill.org/brandon/slides/2013-08-pyconca/#id65
  • __getattr__ 现在最好避免使用描述符...
  • @brunodesthuilliers,我看不到如何使用描述符而不是 __getattr__ 在使用 __slots__ 的类中定义“惰性计算”属性。

标签: python oop memoization


【解决方案1】:

第一件事:这是 Python 中非常常见的模式(在 Django IIRC 中甚至有一个 cached_property 描述符类)。

话虽如此,这里至少存在两个潜在问题。

第一个对于所有“缓存属性”实现来说都是通用的,事实上人们通常不期望属性访问会触发一些繁重的计算。这是否真的是一个问题取决于上下文(以及读者近乎宗教的观点......)

第二个问题 - 更具体到您的示例 - 是传统的缓存失效/状态一致性问题:这里有 y 作为 x 的函数 - 或者至少这是人们所期望的 - 但重新绑定 @987654324 @ 不会相应地更新 y。在这种情况下,可以通过将 x 也设为属性并在 setter 上使 _y 无效来轻松解决此问题,但随后会发生更多意想不到的繁重计算。

在这种情况下(取决于上下文和计算成本),我可能会保留记忆(无效),但提供更明确的 getter 以明确我们可能正在进行一些计算。

编辑:我误读了您的代码,并在y 上设想了一个属性装饰器——这表明这种模式是多么普遍;)。但是,当“自称为 pythonista”的人发布支持计算属性的答案时,我的评论仍然特别有意义。

编辑:如果您想要一个或多或少通用的“缓存失效的缓存属性”,这里有一个可能的实现(可能需要更多测试等):

class cached_property(object):
    """
    Descriptor that converts a method with a single self argument 
    into a property cached on the instance.

    It also has a hook to allow for another property setter to
    invalidated the cache, cf the `Square` class below for
    an example.
    """
    def __init__(self, func):
        self.func = func
        self.__doc__ = getattr(func, '__doc__')
        self.name = self.encode_name(func.__name__)

    def __get__(self, instance, type=None):
        if instance is None:
            return self
        if self.name not in instance.__dict__:
            instance.__dict__[self.name] = self.func(instance)
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        raise AttributeError("attribute is read-only")

    @classmethod
    def encode_name(cls, name):
        return "_p_cached_{}".format(name)

    @classmethod
    def clear_cached(cls, instance, *names):
        for name in names:
            cached = cls.encode_name(name)
            if cached in instance.__dict__:
                del instance.__dict__[cached]

    @classmethod
    def invalidate(cls, *names):
        def _invalidate(setter):
            def _setter(instance, value):
                cls.clear_cached(instance, *names)
                return setter(instance, value)
            _setter.__name__ = setter.__name__
            _setter.__doc__ =  getattr(setter, '__doc__')
            return _setter
        return _invalidate



class Square(object):
    def __init__(self, size):
        self._size = size

    @cached_property
    def area(self):
        return self.size * self.size

    @property
    def size(self):
        return self._size

    @size.setter
    @cached_property.invalidate("area")
    def size(self, size):
        self._size = size

并不是说我认为增加的认知开销实际上值得付出代价 - 通常情况下,简单的内联实现会使代码更易于理解和维护(并且不需要更多的 LOC) - 但它仍然可能有用如果一个包需要大量缓存属性和缓存失效。

【讨论】:

  • 谢谢。在我的用例中,x 一旦实例初始化就永远不会改变,所以我不必担心失效。鉴于此,我还应该做些什么吗?
  • 如果 x 在初始化后不应该更改,您可能希望将其设置为只读属性,并且如果您选择坚持使用 y 的显式 getter,则将其命名为像 get_y() 这样可能会避免一些混淆(只是将其命名为 y() 有点让它看起来像一个属性,所以有人可能会忘记实际调用该方法,并且对获得一个未绑定的方法作为值感到有点惊讶)。
【解决方案2】:

作为一个自称为 Pythonista 的人,我更喜欢在这种情况下使用 property 装饰器:

class A:
    def __init__(self, x):
        self.x = x

    @property
    def y(self):
        if not hasattr(self, '_y'):
            self._y = big_scary_function(self.x)
        return self._y

    def z(self, i):
        return nice_easy_function(self.y, i)

这里self._y 也是惰性求值的。 property 允许您在同一基础上引用 self.xself.y。也就是说,在使用类的实例时,您将 xy 都视为属性,即使 y 被编写为方法。

我还使用了not hasattr(self, '_y') 而不是self._y is None,这样我就可以跳过__init__ 中的self.y = None 声明。你当然可以在这里使用你的方法,并且仍然使用 property 装饰器。

【讨论】:

  • 谢谢。我认为这也有助于我的 IDE 推断 _y 的错误类型。
  • 常见的成语是的,但它也可能有一些缺点 - 参见我的回答。 Nb不想在这里开始一场圣战,正如我提到的,我也使用这种模式,但你必须考虑上下文并权衡利弊。
【解决方案3】:

我的EAFPpythonista 方法由以下 sn-p 描述。

我的类从 WithAttributes 继承 _reset_attributes 并使用它来使可怕的值无效。

class WithAttributes:

    def _reset_attributes(self, attributes):
        assert isinstance(attributes,list)
        for attribute in attributes:
            try:
                delattr(self, '_' + attribute)
            except:
                pass

class Square(WithAttributes):

    def __init__(self, size):
        self._size = size

    @property
    def area(self):
        try:
            return self._area
        except AttributeError:
            self._area = self.size * self.size
            return self._area

    @property
    def size(self):
        return self._size

    @size.setter
    def size(self, size):
        self._size = size
        self._reset_attributes('area')

【讨论】:

  • 这里不需要基类和继承,一个普通的函数也可以工作。还有一个刚刚通过的空缺子句,天哪!如果重点是提供一个通用的“无效缓存属性”,那么还有更好的方法。
  • @brunodesthuilliers 你一直在评论,但没有举个例子。如果您只向我们展示它,我们都会很高兴向您学习。这显然是一个小sn-p,并没有显示所有内容。我留下了我认为会有所帮助的东西。
  • 抱歉,我是用手机发帖的,所以输入一个完整的代码 sn-p 将是一个 PITA。
  • StackOverflow 是帮助需要帮助的人的好工具。我看不出您如何出现在这里并批评其他人的回答有帮助。如果您无法通过电话提供帮助,请不要提供帮助,让我们独自一人。我注意到你的大部分活动都是批评,而不是帮助代码示例。
  • cf 我编辑的答案。请随意批评,我不会生气的。
猜你喜欢
  • 2015-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-28
  • 1970-01-01
  • 2021-04-23
  • 2021-07-08
  • 1970-01-01
相关资源
最近更新 更多