在对象中存储计算值

Storing calculated values in an object

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

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)

在给定的 class 中,我可能有很多东西像这样工作 y,我可能还有其他东西使用存储的预先计算的值。这是做事的最佳方式还是您会推荐一些不同的方式?

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

我已经在 Python 中编写了示例代码,但如果相关的话,我会对特定于其他语言的答案感兴趣。相反,我想听听 Pythonistas 关于他们是否认为此代码是 Pythonic 的意见。

作为一个自称的 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。也就是说,当使用 class 的实例时,您将 xy 都视为属性,即使 y 被写为方法。

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

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

据说这里至少有两个潜在问题。

第一个是所有 'cached properties' 实现所共有的,事实是人们通常不希望属性访问触发一些繁重的计算。这是否真的是一个问题取决于上下文(以及 reader...的近乎宗教的观点)

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

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

编辑:我误读了您的代码并在 y 上想象了一个 属性 装饰器 - 这显示了这种模式的普遍性;)。但是,当 "self proclaimed pythonista" 发布支持计算属性的答案时,我的评论尤其有意义。

编辑:如果你想要一个或多或少通用的 "cached property with cache invalidation",这里有一个可能的实现(可能需要更多测试等):

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) - 但它仍然可能如果包需要大量缓存属性和缓存失效,则很有用。

我的 EAFP pythonista 方法由以下片段描述。

我的 类 从 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')