【问题标题】:Lazy loading of class attributes类属性的延迟加载
【发布时间】:2013-07-03 09:25:38
【问题描述】:

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

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

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

【问题讨论】:

标签: python


【解决方案1】:

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

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

property 描述符是一个数据描述符(它实现了__get____set____delete__ 描述符挂钩),因此即使实例上存在bar 属性也会调用它,其中最终结果是 Python 会忽略该属性,因此需要在每次访问时测试单独的属性。

您可以编写自己的仅实现__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()

如果您更喜欢__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)

后续访问会在实例上找到bar属性,不会查询__getattr__

演示:

>>> 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'}

【讨论】:

  • @whatscanasta:不是property,因为 Python 赋予数据描述符优先于实例属性。但是有了__getattr__,你可以(查看更新)。
  • @schlamar: __getattr__ 不比使用非数据描述符更简单。 Both 在实例上设置属性以防止将来查找描述符或__getattr__ 方法。
  • @schlamar:与其投反对票,不如您自己将其发布为答案?我的回答没有错误或没有帮助。
  • @schlamar:但在描述符出现之前,__getattr__ 就已经存在为此目的了。该钩子显式地存在以允许您在自定义类上提供动态属性。我不会将其归类为 hack,也不会认为答案对使用没有帮助。
  • @schlamar:但是,如果您不打算将其用作答案,希望您不介意我将其添加到我的帐户中。 :-)
【解决方案2】:

是的,试试吧:

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

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

【讨论】:

  • 您能否说明一下非线程安全意味着什么?你的意思是给属性赋值不是线程安全的吗?
【解决方案3】:

目前的答案存在一些问题。具有属性的解决方案要求您指定一个额外的类属性,并且在每次查找时都要检查该属性。 __getattr__ 的解决方案存在一个问题,即在首次访问之前它会隐藏此属性。这不利于自省,使用__dir__ 的解决方法很不方便。

比两个提议的更好的解决方案是直接使用描述符。 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

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2017-12-28
  • 1970-01-01
  • 2019-08-28
  • 2014-07-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多