Python - ленивая загрузка атрибутов класса

Класс foo имеет панель. Бар не загружается до тех пор, пока он не будет доступен. Дальнейший доступ к бару не должен наноситься накладные расходы.

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?

Целью является ленивая загрузка без накладных расходов при всех последующих достуках...

Ответ 1

Есть некоторые проблемы с текущими ответами. Решение с свойством требует, чтобы вы указали дополнительный атрибут класса и имеете накладные расходы на проверку этого атрибута при каждом просмотре. Решение с __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 __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

Ответ 2

Конечно, просто укажите свой атрибут экземпляра, который возвращается при последующем доступе:

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

Ответ 3

Конечно, попробуйте:

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), вы можете реализовать некоторую форму безопасности блокировки.