Python - 类属性的延迟加载

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17486104/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-19 08:19:49  来源:igfitidea点击:

Python - Lazy loading of class attributes

python

提问by whats canasta

Class foo has a bar. Bar is not loaded until it is accessed. Further accesses to bar should incur no overhead.

foo 班有一个酒吧。Bar 在被访问之前不会被加载。进一步访问 bar 应该不会产生开销。

class Foo(object):

    def get_bar(self):
        print "initializing"
        self.bar = "12345"
        self.get_bar = self._get_bar
        return self.bar

    def _get_bar(self):
        print "accessing"
        return self.bar

Is it possible to do something like this using properties or, better yet, attributes, instead of using a getter method?

是否可以使用属性或更好的属性来做这样的事情,而不是使用 getter 方法?

The goal is to lazy load without overhead on all subsequent accesses...

目标是延迟加载而不会对所有后续访问产生开销......

采纳答案by schlamar

There are some problems with the current answers. The solution with a property requires that you specify an additional class attribute and has the overhead of checking this attribute on each look up. The solution with __getattr__has the issue that it hides this attribute until first access. This is bad for introspection and a workaround with __dir__is inconvenient.

目前的答案存在一些问题。带有属性的解决方案要求您指定一个额外的类属性,并且在每次查找时都会产生检查此属性的开销。解决方案__getattr__有一个问题,它在第一次访问之前隐藏此属性。这不利于内省,并且__dir__不方便的解决方法。

A better solution than the two proposed ones is utilizing descriptors directly. The werkzeug library has already a solution as werkzeug.utils.cached_property. It has a simple implementation so you can directly use it without having Werkzeug as dependency:

比两个提议的更好的解决方案是直接使用描述符。werkzeug 库已经有一个解决方案,如werkzeug.utils.cached_property. 它有一个简单的实现,因此您可以直接使用它,而无需将 Werkzeug 作为依赖项:

_missing = object()

class cached_property(object):
    """A decorator that converts a function into a lazy property.  The
    function wrapped is called the first time to retrieve the result
    and then that calculated result is used the next time you access
    the value::

        class Foo(object):

            @cached_property
            def foo(self):
                # calculate something important here
                return 42

    The class has to have a `__dict__` in order for this property to
    work.
    """

    # implementation detail: this property is implemented as non-data
    # descriptor.  non-data descriptors are only invoked if there is
    # no entry with the same name in the instance's __dict__.
    # this allows us to completely get rid of the access function call
    # overhead.  If one choses to invoke __get__ by hand the property
    # will still work as expected because the lookup logic is replicated
    # in __get__ for manual invocation.

    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, _missing)
        if value is _missing:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

回答by Martijn Pieters

Sure, just have your property set an instance attribute that is returned on subsequent access:

当然,只需让您的属性设置一个在后续访问时返回的实例属性:

class Foo(object):
    _cached_bar = None 

    @property
    def bar(self):
        if not self._cached_bar:
            self._cached_bar = self._get_expensive_bar_expression()
        return self._cached_bar

The propertydescriptor is a data descriptor (it implements __get__, __set__and __delete__descriptor hooks), so it'll be invoked even if a barattribute exists on the instance, with the end result that Python ignores that attribute, hence the need to test for a separate attribute on each access.

property描述符是一种数据描述符(它实现__get____set__并且__delete__描述符钩),所以它会被调用即使bar存在于实例属性,与最终结果的Python忽略属性,因此需要测试用于在单独的属性每次访问。

You can write your own descriptor that only implements __get__, at which point Python uses an attribute on the instance over the descriptor if it exists:

您可以编写自己的仅实现 的描述符,__get__此时 Python 在描述符上使用实例上的属性(如果存在):

class CachedProperty(object):
    def __init__(self, func, name=None):
        self.func = func
        self.name = name if name is not None else func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, instance, class_):
        if instance is None:
            return self
        res = self.func(instance)
        setattr(instance, self.name, res)
        return res

class Foo(object):
    @CachedProperty
    def bar(self):
        return self._get_expensive_bar_expression()

If you prefer a __getattr__approach (which has something to say for it), that'd be:

如果你更喜欢一种__getattr__方法(它有话要说),那就是:

class Foo(object):
    def __getattr__(self, name):
        if name == 'bar':
            bar = self.bar = self._get_expensive_bar_expression()
            return bar
        return super(Foo, self).__getattr__(name)

Subsequent access will find the barattribute on the instance and __getattr__won't be consulted.

后续访问将bar在实例上找到该属性并且__getattr__不会被查阅。

Demo:

演示:

>>> class FooExpensive(object):
...     def _get_expensive_bar_expression(self):
...         print 'Doing something expensive'
...         return 'Spam ham & eggs'
... 
>>> class FooProperty(FooExpensive):
...     _cached_bar = None 
...     @property
...     def bar(self):
...         if not self._cached_bar:
...             self._cached_bar = self._get_expensive_bar_expression()
...         return self._cached_bar
... 
>>> f = FooProperty()
>>> f.bar
Doing something expensive
'Spam ham & eggs'
>>> f.bar
'Spam ham & eggs'
>>> vars(f)
{'_cached_bar': 'Spam ham & eggs'}
>>> class FooDescriptor(FooExpensive):
...     bar = CachedProperty(FooExpensive._get_expensive_bar_expression, 'bar')
... 
>>> f = FooDescriptor()
>>> f.bar
Doing something expensive
'Spam ham & eggs'
>>> f.bar
'Spam ham & eggs'
>>> vars(f)
{'bar': 'Spam ham & eggs'}

>>> class FooGetAttr(FooExpensive):
...     def __getattr__(self, name):
...         if name == 'bar':
...             bar = self.bar = self._get_expensive_bar_expression()
...             return bar
...         return super(Foo, self).__getatt__(name)
... 
>>> f = FooGetAttr()
>>> f.bar
Doing something expensive
'Spam ham & eggs'
>>> f.bar
'Spam ham & eggs'
>>> vars(f)
{'bar': 'Spam ham & eggs'}

回答by Stefano Sanfilippo

Sure it is, try:

当然可以,试试:

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

    @property
    def bar(self):
        if self._bar is None:
            self._bar = HeavyObject()
        return self._bar

Note that this is not thread-safe. cPython has GIL, so it's a relative issue, but if you plan to use this in a true multithread Python stack (say, Jython), you might want to implement some form of lock safety.

请注意,这不是线程安全的。cPython 有 GIL,所以这是一个相对的问题,但是如果您打算在真正的多线程 Python 堆栈(例如 Jython)中使用它,您可能想要实现某种形式的锁安全。