Декоратор властивостей запам'ятовування / відкладеного пошуку Python


109

Нещодавно я пройшов існуючу базу коду, що містить безліч класів, де атрибути екземплярів відображають значення, що зберігаються в базі даних. Я відновив багато цих атрибутів, щоб відкласти їх пошукові бази даних, тобто. не ініціалізується в конструкторі, а лише після першого читання. Ці атрибути не змінюються впродовж життя примірника, але вони є справжнім вузьким місцем для обчислення цього першого разу і лише дійсно доступним для особливих випадків. Отже, вони також можуть бути кешовані після їх отримання з бази даних (отже, це відповідає визначенню запам'ятовування, де вхід просто "не вводиться").

Я можу вводити наступний фрагмент коду знову і знову для різних атрибутів для різних класів:

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

Чи існує існуючий декоратор, який би це зробив уже в Python, про який я просто не знаю? Або є досить простий спосіб визначити декоратора, який це робить?

Я працюю під Python 2.5, але 2,6 відповіді все ж можуть бути цікавими, якщо вони суттєво відрізняються.

Примітка

Це питання було задано ще до того, як Python включив для цього чимало готових декораторів. Я оновив його лише для виправлення термінології.


Я використовую Python 2.7, і нічого не бачу про готові декоратори для цього. Чи можете ви надати посилання на готові декоратори, про які йдеться у питанні?
Бамклур

@Bamcclur вибачте, раніше були коментарі, де їх детально не було, не знаю, чому їх видалили. Тільки один я можу знайти прямо зараз це Python 3 один: functools.lru_cache().
деттер

Не впевнений, що є вбудовані модулі (принаймні Python 2.7), але є кешована власність бібліотеки Болтонів
guyarad

@guyarad Я до цього часу не бачив цього коментаря. Це фантастична бібліотека! Опублікуйте це як відповідь, щоб я міг його схвалити.
деттер

Відповіді:


12

Для всіх видів чудових утиліт я використовую болтони .

Як частина цієї бібліотеки ви маєте кешоване властивість :

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)

124

Ось приклад реалізації лінивого декоратора властивостей:

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

Інтерактивна сесія:

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]

1
Чи може хтось порекомендувати відповідне ім’я для внутрішньої функції? Я так погано називаю речі вранці ...
Майк Бурс

2
Зазвичай внутрішню функцію я називаю так само, як і зовнішню функцію з попереднім підкресленням. Тож "_lazyprop" - дотримується філософії "тільки для внутрішнього використання" з pep 8.
вересень

1
Це чудово працює :) Я не знаю, чому мені ніколи не приходило в голову використовувати декоратор для вкладеної функції, як це.
detly

4
з урахуванням протоколу дескриптора даних, цей спосіб набагато повільніше і менш елегантний, ніж відповідь нижче, використовуючи__get__
Ронні

1
Порада: Покладіть @wraps(fn)нижче, @propertyщоб не втратити рядки в документі тощо ( wrapsпоходить від functools)
letmaik

111

Я написав це для себе ... Щоб використовуватись для справжніх одноразових обчислених лінивих властивостей. Мені це подобається, тому що він уникає наклеювання додаткових атрибутів на об'єкти, а після активації не витрачає час на перевірку наявності атрибутів тощо:

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

    def __init__(self, fget):
        self.fget = fget

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

Примітка. lazy_propertyКлас - це дескриптор даних , що означає, що він доступний лише для читання. Додавання __set__методу не дозволить йому правильно працювати.


9
Це знадобилося трохи часу, щоб зрозуміти, але це абсолютно приголомшлива відповідь. Мені подобається, як сама функція замінюється значенням, яке вона обчислює.
Пол Етертон

2
Для нащадків: інші варіанти цього варіанту пропонуються в інших відповідях з тих пір (посилання 1 та 2 ). Здається, це популярність у веб-рамках Python (похідні існують у Pyramid та Werkzeug).
Андре Карон

1
Дякуємо за те, що
помітили,

3
Я знайшов цей метод у 7,6 разів швидшим, ніж обрана відповідь. (2,45 мкс / 322 нс) Дивіться ноутбук ipython
Дейв Батлер,

1
NB: це не заважає призначення на fgetшляху @propertyробить. Щоб забезпечити незмінність / ідентифікацію, потрібно додати __set__()метод, який піднімає AttributeError('can\'t set attribute')(або як би виняток / повідомлення підходило вам, але саме це propertyпідвищує). На жаль , це приходить з впливом на продуктивність в частки мікросекунди , тому що __get__()буде викликатися при кожному доступі , а не потонув значення FGET з Dict на другий і наступний доступ. На мій погляд, варто того, щоб підтримувати незмінність / ідентифікацію, що є ключовим для моїх випадків використання, але YMMV.
скан.

4

Ось відкликані , який приймає необов'язковий аргумент тайм - ауту, в __call__Ви можете скопіювати над __name__, __doc__, __module__з простору імен Func в:

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

колишній:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar


3

Те, що ви насправді хочете, - це reify(джерело, пов’язане з джерелом!) Декоратор від Pyramid:

Використовувати як декоратор методу класу. Він працює майже так само, як і @propertyдекоратор Python , але він вводить результат методу, який він декорує, в дік екземпляра після першого виклику, ефективно замінюючи функцію, яку він декорує, змінною екземпляра. Це, кажучи Python, дескриптор даних. Нижче наведено приклад та його використання:

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2

1
Ніцца один, робить саме те, що мені потрібно ... хоча піраміда може бути велика залежність для одного декоратора:)
detly

@detly Реалізація декоратора проста, і ви можете її реалізувати самостійно, не потрібно в pyramidзалежності.
Пітер Вуд

Звідси посилання говорить "джерело пов'язане": D
Антті Хаапала

@AnttiHaapala Я помітив, але подумав, що підкреслив, що це просто реалізувати для тих, хто не переходить за посиланням.
Пітер Вуд

1

Наразі існує змішання термінів та / або плутанини понять як у питанні, так і у відповідях.

Ледача оцінка просто означає, що щось оцінюється під час виконання в останній можливий момент, коли потрібне значення. Стандарт@property декоратор робить саме це. (*) Оформлена функція оцінюється лише і кожного разу, коли вам потрібно значення цього властивості. (див. статтю Вікіпедії про ледачу оцінку)

(*) Насправді справжньої лінивої оцінки (порівняйте, наприклад, haskell) дуже важко досягти в python (і приводить до коду, який далеко не ідіоматичний).

Пам'ять - це правильний термін того, що, як видається, шукає. Чисті функції, які не залежать від побічних ефектів для оцінки повернених значень, можна безпечно запам’ятати, і насправді у фунікулерах є декоратор, @functools.lru_cacheтому немає необхідності писати власні декоратори, якщо вам не потрібна спеціалізована поведінка.


Я використовував термін "ледачий", тому що в оригінальній реалізації член був обчислений / отриманий з БД під час ініціалізації об'єкта, і я хочу відкласти це обчислення, поки властивість фактично не використовувалася в шаблоні. Це мені здавалося, що відповідає визначенню лінь. Я погоджуюсь, що оскільки моє запитання вже передбачає рішення з використанням @property, "ледачий" не має великого сенсу на той момент. (Я також думав про запам'ятовування як карту входів до кешованих виходів, і оскільки ці властивості мають лише один вхід, нічого, карта здавалася більш складною, ніж потрібно.)
дет

Зауважте, що всіх декораторів, які люди запропонували як "нестандартні" рішення, не було, коли я і запитав це.
detly

Я погоджуюся з Джейсоном, це питання щодо кешування / запам'ятовування, а не лінивої оцінки.
poindexter

@poindexter - кешування не дуже охоплює це; це не відрізняє пошук значення під час об'єкта під час його кешування та кешування його від пошуку значення та кешування його під час доступу до властивості (що є ключовою особливістю тут). Як мені це назвати? «Кеш-декор після першого використання» декоратор?
detly

@detly Пам'ятайте. Ви повинні назвати це Пам'ятати. en.wikipedia.org/wiki/Memoization
poindexter

0

Ви можете зробити це красиво і легко, побудувавши клас з рідної власності Python:

class cached_property(property):
    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 __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

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

Ми можемо використовувати цей клас властивостей, як властивість звичайного класу (це також підтримка призначення елементів, як ви бачите)

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

Значення розраховується лише перший раз, після чого ми використали збережене значення

Вихід:

I am calculating value
My calculated value
My calculated value
2
2
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.