在Python中缓存类属性

发布于 2024-09-29 14:55:01 字数 280 浏览 5 评论 0原文

我正在用 python 编写一个类,并且有一个属性需要相对较长的时间来计算,因此我只想执行一次。此外,并非该类的每个实例都需要它,因此我不想在 __init__ 中默认执行此操作

我是 Python 新手,但不是编程新手。我可以很容易地想出一种方法来做到这一点,但我一遍又一遍地发现,“Pythonic”的做事方式通常比我利用其他语言的经验想出的方法要简单得多。

在 Python 中是否有“正确”的方法来做到这一点?

I'm writing a class in python and I have an attribute that will take a relatively long time to compute, so I only want to do it once. Also, it will not be needed by every instance of the class, so I don't want to do it by default in __init__.

I'm new to Python, but not to programming. I can come up with a way to do this pretty easily, but I've found over and over again that the 'Pythonic' way of doing something is often much simpler than what I come up with using my experience in other languages.

Is there a 'right' way to do this in Python?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

像极了他 2024-10-06 14:55:01

3.8≤Python
@property@functools.lru_cache 已合并到 @cached_property

import functools
class MyClass:
    @functools.cached_property
    def foo(self):
        print("long calculation here")
        return 21 * 2

3.2≤Python< 3.8

您应该同时使用 @property< /a> 和 @functools.lru_cache 装饰器:

import functools
class MyClass:
    @property
    @functools.lru_cache()
    def foo(self):
        print("long calculation here")
        return 21 * 2

这个答案有更详细的示例,还提到了以前的 Python 版本的向后移植。

Python 3.2

Python wiki 有一个 缓存属性装饰器(MIT 许可),可以像这样使用:

import random
# the class containing the property must be a new-style class
class MyClass(object):
   # create property whose value is cached for ten minutes
   @cached_property(ttl=600)
   def randint(self):
       # will only be evaluated every 10 min. at maximum.
       return random.randint(0, 100)

或者其他答案中提到的任何适合您需求的实现。

或者上面提到的向后移植。

3.8 ≤ Python
@property and @functools.lru_cache have been combined into @cached_property.

import functools
class MyClass:
    @functools.cached_property
    def foo(self):
        print("long calculation here")
        return 21 * 2

3.2 ≤ Python < 3.8

You should use both @property and @functools.lru_cache decorators:

import functools
class MyClass:
    @property
    @functools.lru_cache()
    def foo(self):
        print("long calculation here")
        return 21 * 2

This answer has more detailed examples and also mentions a backport for previous Python versions.

Python < 3.2

The Python wiki has a cached property decorator (MIT licensed) that can be used like this:

import random
# the class containing the property must be a new-style class
class MyClass(object):
   # create property whose value is cached for ten minutes
   @cached_property(ttl=600)
   def randint(self):
       # will only be evaluated every 10 min. at maximum.
       return random.randint(0, 100)

Or any implementation mentioned in the others answers that fits your needs.

Or the above mentioned backport.

烟花易冷人易散 2024-10-06 14:55:01

我曾经按照 gnibbler 的建议这样做,但最终我厌倦了这些小家务步骤。

所以我构建了自己的描述符:

class cached_property(object):
    """
    Descriptor (non-data) for building an attribute on-demand on first use.
    """
    def __init__(self, factory):
        """
        <factory> is called such: factory(instance) to build the attribute.
        """
        self._attr_name = factory.__name__
        self._factory = factory

    def __get__(self, instance, owner):
        # Build the attribute.
        attr = self._factory(instance)

        # Cache the value; hide ourselves.
        setattr(instance, self._attr_name, attr)

        return attr

以下是使用它的方法:

class Spam(object):

    @cached_property
    def eggs(self):
        print 'long calculation here'
        return 6*2

s = Spam()
s.eggs      # Calculates the value.
s.eggs      # Uses cached value.

I used to do this how gnibbler suggested, but I eventually got tired of the little housekeeping steps.

So I built my own descriptor:

class cached_property(object):
    """
    Descriptor (non-data) for building an attribute on-demand on first use.
    """
    def __init__(self, factory):
        """
        <factory> is called such: factory(instance) to build the attribute.
        """
        self._attr_name = factory.__name__
        self._factory = factory

    def __get__(self, instance, owner):
        # Build the attribute.
        attr = self._factory(instance)

        # Cache the value; hide ourselves.
        setattr(instance, self._attr_name, attr)

        return attr

Here's how you'd use it:

class Spam(object):

    @cached_property
    def eggs(self):
        print 'long calculation here'
        return 6*2

s = Spam()
s.eggs      # Calculates the value.
s.eggs      # Uses cached value.
自由如风 2024-10-06 14:55:01

通常的方法是使属性成为 property 并首先存储值计算的时间

import time

class Foo(object):
    def __init__(self):
        self._bar = None

    @property
    def bar(self):
        if self._bar is None:
            print "starting long calculation"
            time.sleep(5)
            self._bar = 2*2
            print "finished long caclulation"
        return self._bar

foo=Foo()
print "Accessing foo.bar"
print foo.bar
print "Accessing foo.bar"
print foo.bar

The usual way would be to make the attribute a property and store the value the first time it is calculated

import time

class Foo(object):
    def __init__(self):
        self._bar = None

    @property
    def bar(self):
        if self._bar is None:
            print "starting long calculation"
            time.sleep(5)
            self._bar = 2*2
            print "finished long caclulation"
        return self._bar

foo=Foo()
print "Accessing foo.bar"
print foo.bar
print "Accessing foo.bar"
print foo.bar
一场春暖 2024-10-06 14:55:01

Python 3.8 包含 functools.cached_property 装饰器。

将类的方法转换为计算值的属性
一次,然后在生命周期内作为正常属性缓存
实例。与property()类似,但增加了缓存。有用
对于实例的昂贵的计算属性,否则
实际上是不可变的。

此示例直接来自文档:

from functools import cached_property

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

限制是具有要缓存的属性的对象必须具有作为可变映射的 __dict__ 属性,排除具有 __slots__ 的类除非 __dict__ 是在 __slots__ 中定义的。

Python 3.8 includes the functools.cached_property decorator.

Transform a method of a class into a property whose value is computed
once and then cached as a normal attribute for the life of the
instance. Similar to property(), with the addition of caching. Useful
for expensive computed properties of instances that are otherwise
effectively immutable.

This example is straight from the docs:

from functools import cached_property

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

The limitation being that the object with the property to be cached must have a __dict__ attribute that is a mutable mapping, ruling out classes with __slots__ unless __dict__ is defined in __slots__.

·深蓝 2024-10-06 14:55:01

如前所述,functools.cached_property 适用于缓存的实例属性。对于缓存的class属性:

3.9 <= python < 3.13

from functools import cache

class MyClass:
    @classmethod
    @property
    @cache  # or lru_cache() for python < 3.9
    def foo(cls):
        print('expensive calculation')
        return 42
>>> MyClass.foo
expensive calculation
42
>>> MyClass.foo
42

如果你想要一个可重用的装饰器:

def cached_class_attr(f):
    return classmethod(property(cache(f)))

class MyClass:
    @cached_class_attr
    def foo(cls):
        ...

python >= 3.13

在 3.13 中,链接 classmethodproperty 是不允许的,所以你会必须使用元类或自定义装饰器,这是只读缓存属性的示例:

class MyMeta(type):
    @property
    @cache
    def foo(self):
        ...

class MyClass(metaclass=MyMeta):
    ...

MyClass.foo  # read-only access

或者自定义装饰器:

class classproperty:
    def __init__(self, func) -> None:
        functools.update_wrapper(self, func)
    def __get__(self, instance, owner):
        return self.__wrapped__(owner)

class MyClass:
    @classproperty
    @cache
    def foo(cls):
        ...

As mentioned, functools.cached_property will work for cached instance attributes. For cached class attributes:

3.9 <= python < 3.13

from functools import cache

class MyClass:
    @classmethod
    @property
    @cache  # or lru_cache() for python < 3.9
    def foo(cls):
        print('expensive calculation')
        return 42
>>> MyClass.foo
expensive calculation
42
>>> MyClass.foo
42

And if you want a reusable decorator:

def cached_class_attr(f):
    return classmethod(property(cache(f)))

class MyClass:
    @cached_class_attr
    def foo(cls):
        ...

python >= 3.13

In 3.13 chaining classmethod and property is disallowed so you will have to use metaclasses or a custom decorator, here's an example of a read-only cached attribute:

class MyMeta(type):
    @property
    @cache
    def foo(self):
        ...

class MyClass(metaclass=MyMeta):
    ...

MyClass.foo  # read-only access

Or alternatively a custom decorator:

class classproperty:
    def __init__(self, func) -> None:
        functools.update_wrapper(self, func)
    def __get__(self, instance, owner):
        return self.__wrapped__(owner)

class MyClass:
    @classproperty
    @cache
    def foo(cls):
        ...
墨洒年华 2024-10-06 14:55:01

dickens 包(不是我的)提供了 cachedpropertyclasspropertycachedclassproperty 装饰器。

缓存类属性

from descriptors import cachedclassproperty

class MyClass:
    @cachedclassproperty
    def approx_pi(cls):
        return 22 / 7

The dickens package (not mine) offers cachedproperty, classproperty and cachedclassproperty decorators.

To cache a class property:

from descriptors import cachedclassproperty

class MyClass:
    @cachedclassproperty
    def approx_pi(cls):
        return 22 / 7
简单 2024-10-06 14:55:01
class MemoizeTest:

      _cache = {}
      def __init__(self, a):
          if a in MemoizeTest._cache:
              self.a = MemoizeTest._cache[a]
          else:
              self.a = a**5000
              MemoizeTest._cache.update({a:self.a})
class MemoizeTest:

      _cache = {}
      def __init__(self, a):
          if a in MemoizeTest._cache:
              self.a = MemoizeTest._cache[a]
          else:
              self.a = a**5000
              MemoizeTest._cache.update({a:self.a})
墨离汐 2024-10-06 14:55:01

您可以尝试研究记忆。它的工作方式是,如果您向函数传递相同的参数,它将返回缓存的结果。您可以在此处找到有关在 Python 中实现它的更多信息。

另外,根据您的代码的设置方式(您说并非所有实例都需要它),您可以尝试使用某种享元模式或延迟加载。

You could try looking into memoization. The way it works is that if you pass in a function the same arguments, it will return the cached result. You can find more information on implementing it in python here.

Also, depending on how your code is set up (you say that it is not needed by all instances) you could try to use some sort of flyweight pattern, or lazy-loading.

·深蓝 2024-10-06 14:55:01

大多数(如果不是全部)当前答案都是关于缓存实例属性。要缓存属性,您可以简单地使用字典。这确保了每个类计算一次属性,而不是每个实例计算一次。

mapping = {}

class A:
    def __init__(self):
        if self.__class__.__name__ not in mapping:
            print('Expansive calculation')
            mapping[self.__class__.__name__] = self.__class__.__name__
        self.cached = mapping[self.__class__.__name__]

为了说明,

foo = A()
bar = A()
print(foo.cached, bar.cached)

给出

Expansive calculation
A A

Most if not all current answers are about caching instance attributes. To cache class attributes, you can simply use a dictionary. This ensures the attributes are calculated once per class, instead of once per instance.

mapping = {}

class A:
    def __init__(self):
        if self.__class__.__name__ not in mapping:
            print('Expansive calculation')
            mapping[self.__class__.__name__] = self.__class__.__name__
        self.cached = mapping[self.__class__.__name__]

To illustrate,

foo = A()
bar = A()
print(foo.cached, bar.cached)

gives

Expansive calculation
A A
九厘米的零° 2024-10-06 14:55:01

最简单的方法可能是只编写一个包装属性的方法(而不是使用属性)(getter 方法)。第一次调用时,该方法计算、保存并返回值;之后它只返回保存的值。

The most simple way of doing this would probably be to just write a method (instead of using an attribute) that wraps around the attribute (getter method). On the first call, this methods calculates, saves and returns the value; later it just returns the saved value.

生寂 2024-10-06 14:55:01

对于 Python 2,而不是 Python 3,这就是我所做的。这大约是您可以获得的最高效率:

class X:
    @property
    def foo(self):
        r = 33
        self.foo = r
        return r

说明:基本上,我只是用计算值重载属性方法。因此,在您第一次访问该属性(对于该实例)后,foo 不再是一个属性,而是成为一个实例属性。这种方法的优点是缓存命中尽可能便宜,因为 self.__dict__ 被用作缓存,并且如果不使用该属性,则没有实例开销。

此方法不适用于 Python 3。

With Python 2, but not Python 3, here's what I do. This is about as efficient as you can get:

class X:
    @property
    def foo(self):
        r = 33
        self.foo = r
        return r

Explanation: Basically, I'm just overloading a property method with the computed value. So after the first time you access the property (for that instance), foo ceases to be a property and becomes an instance attribute. The advantage of this approach is that a cache hit is as cheap as possible because self.__dict__ is being used as the cache, and there is no instance overhead if the property is not used.

This approach doesn't work with Python 3.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文