Python memoising / deferred lookup property decorator

Ostatnio przejrzałem istniejącą bazę kodu zawierającą wiele klas, w których atrybuty instancji odzwierciedlają wartości przechowywane w bazie danych. Zrefakturowałem wiele z tych atrybutów, aby ich wyszukiwanie bazy danych zostało odroczone, tj. nie zostanie zainicjowana w konstruktorze, ale dopiero po pierwszym wczytaniu. Atrybuty te nie zmieniają się w czasie trwania wystąpienia, ale są prawdziwym wąskim gardłem do obliczenia tego po raz pierwszy i naprawdę dostępne tylko w szczególnych przypadkach. Dzięki temu mogą być również buforowane po ich pobraniu z bazy danych (dlatego pasuje to do definicji memoisation, gdzie dane wejściowe są po prostu "brak danych wejściowych").

W kółko wpisuję następujący fragment kodu dla różnych atrybutów w różnych klasach:

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

Czy istnieje istniejący dekorator, który robi to już w Pythonie, o którym po prostu nie wiem? Czy istnieje dość prosty sposób na zdefiniowanie dekoratora, który to robi?

Pracuję pod Pythonem 2.5, ale odpowiedzi 2.6 nadal mogą być interesujące, jeśli są znacznie różne.

Uwaga

To pytanie zostało zadane zanim Python zawierał wiele gotowych dekoratorów do tego. Zaktualizowałem go tylko w celu poprawienia terminologii.

Author: detly, 2010-06-10

8 answers

Do wszelkiego rodzaju świetnych narzędzi używam boltonów .

Jako część tej biblioteki masz cachedproperty :

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)
 8
Author: guyarad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-29 00:07:23

Oto przykładowa implementacja leniwego dekoratora Nieruchomości:

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)

Sesja Interaktywna:

>>> 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]
 119
Author: Mike Boers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-06 12:43:03

Napisałam to dla siebie... Do użycia dla true jednorazowo obliczonych właściwości leniwych. Podoba mi się, ponieważ unika przyklejania dodatkowych atrybutów na obiektach, a po aktywacji nie marnuje czasu na sprawdzanie obecności atrybutów itp.:

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

Uwaga: klasa lazy_property jest nie-deskryptorem danych, co oznacza, że jest tylko do odczytu. Dodanie metody __set__ uniemożliwiłoby jej poprawne działanie.

 104
Author: Cyclone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-06 12:36:59

Oto wywołanie, które pobiera opcjonalny argument timeout, w __call__ można również skopiować __name__, __doc__, __module__ z przestrzeni nazw 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]

Ex:

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
 4
Author: gnr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-04 22:51:28

property to klasa. A deskryptor aby być dokładnym. Po prostu wyprowadzić z niego i wdrożyć pożądane zachowanie.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')
 3
Author: Ignacio Vazquez-Abrams,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-10 07:45:57

To, czego naprawdę chcesz, to reify (źródło: linked!) dekorator z piramidy:

Użyj jako dekorator metody klasowej. Działa prawie dokładnie tak, jak dekorator Pythona @property, ale po pierwszym wywołaniu umieszcza wynik metody, którą dekoruje w dict instancji, skutecznie zastępując dekorowaną funkcję zmienną instancji. Jest to, w języku Python, deskryptor Nie-danych. Poniżej znajduje się przykład i jego użycie:

>>> 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
 2
Author: Antti Haapala,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-12 18:34:06

Do tej pory istnieje pomieszanie pojęć i/lub pomieszanie pojęć zarówno w pytaniach, jak i w odpowiedziach.

Leniwa ocena oznacza tylko, że coś jest oceniane w czasie wykonywania w ostatnim możliwym momencie, gdy potrzebna jest wartość. dekorator standardowy @property właśnie to robi. (*) funkcja jest obliczana tylko i za każdym razem, gdy potrzebujesz wartości tej właściwości. (zobacz artykuł na Wikipedii o leniwej ocenie)

(*)faktycznie prawdziwa leniwa ocena (porównaj np. haskell) jest bardzo trudne do osiągnięcia w Pythonie (i skutkuje kodem, który jest daleki od idiomatycznego).

Memoizacja jest właściwym określeniem tego, czego asker wydaje się szukać. Czyste funkcje, które nie zależą od efektów ubocznych, mogą być bezpiecznie zapamiętane i w functools istnieje dekorator @functools.lru_cache więc nie ma potrzeby pisania własnych dekoratorów, chyba że potrzebujesz specjalistycznego zachowania.

 0
Author: Jason Herbburn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-26 19:13:36

Możesz to zrobić ładnie i łatwo, budując klasę z natywnej właściwości Pythona:

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

Możemy używać tej właściwości jak zwykłej właściwości klasy (jest to również wsparcie przypisanie pozycji, jak widać)

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)

Wartość obliczana tylko za pierwszym razem, a następnie użyliśmy naszej zapisanej wartości

Wyjście:

I am calculating value
My calculated value
My calculated value
2
2
 0
Author: itmard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-30 08:59:29