Ленивая инициализация члена класса Python

Я хотел бы знать, что такое способ Python инициализации члена класса, но только при доступе к нему, если доступ. Я попробовал код ниже, и он работает, но есть ли что-то проще?

class MyClass(object):

    _MY_DATA = None

    @staticmethod
    def _retrieve_my_data():
        my_data = ...  # costly database call
        return my_data

    @classmethod
    def get_my_data(cls):
        if cls._MY_DATA is None:
            cls._MY_DATA = MyClass._retrieve_my_data()
        return cls._MY_DATA

4 ответов


вы могли бы использовать @property on метакласс вместо:

class MyMetaClass(type):
    @property
    def my_data(cls):
        if getattr(cls, '_MY_DATA', None) is None:
            my_data = ...  # costly database call
            cls._MY_DATA = my_data
        return cls._MY_DATA


class MyClass(metaclass=MyMetaClass):
    # ...

это делает my_data атрибут класса, поэтому вызов дорогостоящей базы данных откладывается до тех пор, пока вы не попытаетесь получить доступ MyClass.my_data. Результат вызова базы данных кэшируется путем его хранения в MyClass._MY_DATA вызов только после для класса.

для Python 2, Используйте class MyClass(object): и добавить __metaclass__ = MyMetaClass атрибут в определении класса тело для прикрепления метакласса.

демо:

>>> class MyMetaClass(type):
...     @property
...     def my_data(cls):
...         if getattr(cls, '_MY_DATA', None) is None:
...             print("costly database call executing")
...             my_data = 'bar'
...             cls._MY_DATA = my_data
...         return cls._MY_DATA
... 
>>> class MyClass(metaclass=MyMetaClass):
...     pass
... 
>>> MyClass.my_data
costly database call executing
'bar'
>>> MyClass.my_data
'bar'

это работает, потому что дескриптор данных, таких как property просматривается родительский тип объекта; для классов это type и type может быть расширен с помощью метаклассов.


другой подход к очистке кода-написать функцию-оболочку, которая выполняет желаемую логику:

def memoize(f):
    def wrapped(*args, **kwargs):
        if hasattr(wrapped, '_cached_val'):
            return wrapped._cached_val
        result = f(*args, **kwargs)
        wrapped._cached_val = result
        return result
    return wrapped

вы можете использовать его следующим образом:

@memoize
def expensive_function():
    print "Computing expensive function..."
    import time
    time.sleep(1)
    return 400

print expensive_function()
print expensive_function()
print expensive_function()

выходы:

Computing expensive function...
400
400
400

теперь ваш classmethod будет выглядеть следующим образом, например:

class MyClass(object):
        @classmethod
        @memoize
        def retrieve_data(cls):
            print "Computing data"
            import time
            time.sleep(1) #costly DB call
            my_data = 40
            return my_data

print MyClass.retrieve_data()
print MyClass.retrieve_data()
print MyClass.retrieve_data()

выход:

Computing data
40
40
40

обратите внимание, что это будет кэшировать только одно значение для любого набора аргументов функции, поэтому, если вы хотите вычислить разные значения в зависимости от ввода значения, вы должны будете сделать memoize немного сложнее.


этот ответ для типичного только атрибут/метод экземпляра, Не для атрибута класса/classmethod или staticmethod.

как насчет использования как property и lru_cache оформителей? Последние memoizes.

from functools import lru_cache

class MyClass:

    @property
    @lru_cache()
    def my_lazy_attr(self):
        print('Initializing and caching attribute, once per class instance.')
        return 7**7**8

обратите внимание, что для этого требуется Python ≥3.2.

кредит: ответ от Maxime Р.


рассмотрим pip-installable Dickens пакета, который доступен для Python 3.5+. У него есть descriptors пакет, который предоставляет соответствующие cachedproperty и cachedclassproperty декораторы, использование которых показано в примере ниже. Кажется, это работает, как ожидалось.

from descriptors import cachedproperty, classproperty, cachedclassproperty

class MyClass:
    FOO = 'A'

    def __init__(self):
        self.bar = 'B'

    @cachedproperty
    def my_cached_instance_attr(self):
        print('Initializing and caching attribute, once per class instance.')
        return self.bar * 2

    @cachedclassproperty
    def my_cached_class_attr(cls):
        print('Initializing and caching attribute, once per class.')
        return cls.FOO * 3

    @classproperty
    def my_class_property(cls):
        print('Calculating attribute without caching.')
        return cls.FOO + 'C'