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