Dekorator właściwości zapamiętywania / odroczonego wyszukiwania w Pythonie

109

Ostatnio przejrzałem istniejącą bazę kodu zawierającą wiele klas, w których atrybuty instancji odzwierciedlają wartości przechowywane w bazie danych. Przefaktoryzowałem wiele z tych atrybutów, aby ich wyszukiwanie w bazie danych zostało odroczone, tj. nie być inicjalizowane w konstruktorze, ale tylko przy pierwszym czytaniu. Te atrybuty nie zmieniają się przez cały okres istnienia instancji, ale są prawdziwym wąskim gardłem w obliczaniu tego pierwszego razu i są dostępne tylko w specjalnych przypadkach. W związku z tym mogą być również buforowane po ich pobraniu z bazy danych (dlatego pasuje to do definicji memoizacji, w której dane wejściowe to 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 już dekorator, który może to zrobić w Pythonie, o którym po prostu nie wiem? A może istnieje dość prosty sposób na zdefiniowanie dekoratora, który to robi?

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

Uwaga

To pytanie zostało zadane, zanim Python zawierał wiele gotowych dekoratorów do tego. Zaktualizowałem go tylko pod kątem poprawnej terminologii.

bezczelnie
źródło
Używam Pythona 2.7 i nie widzę nic o gotowych dekoratorach do tego. Czy możesz podać link do gotowych dekoratorów wymienionych w pytaniu?
Bamcclur
@Bamcclur przepraszam, były inne komentarze szczegółowo je opisujące, nie jestem pewien, dlaczego zostały usunięte. Jedynym mogę znaleźć w tej chwili jest to jeden Python 3: functools.lru_cache().
detly
Nie jestem pewien, czy są wbudowane (przynajmniej Python 2.7), ale istnieje buforowana
właściwość
@guyarad Do tej pory nie widziałem tego komentarza. To fantastyczna biblioteka! Opublikuj to jako odpowiedź, abym mógł ją zagłosować.
detly

Odpowiedzi:

12

Do wszelkiego rodzaju świetnych narzędzi używam boltons .

W ramach tej biblioteki masz cachedwłaściwość :

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)
Gujarada
źródło
124

Oto przykład implementacji 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]
Mike Boers
źródło
1
Czy ktoś może polecić odpowiednią nazwę dla funkcji wewnętrznej? Jestem kiepski w nazywaniu rzeczy rano ...
Mike Boers,
2
Zwykle nazywam funkcję wewnętrzną taką samą jak funkcja zewnętrzna z poprzedzającym podkreśleniem. Tak więc „_lazyprop” - podąża za filozofią „tylko do użytku wewnętrznego” 8.
spędziłhil
1
To działa świetnie :) Nie wiem, dlaczego nigdy nie przyszło mi do głowy, aby użyć dekoratora do takiej funkcji zagnieżdżonej.
detly
4
biorąc pod uwagę protokół nie-deskryptora danych, ten jest znacznie wolniejszy i mniej elegancki niż poniższa odpowiedź, używając__get__
Ronny,
1
Wskazówka: umieść @wraps(fn)poniżej, @propertyaby nie wrapsfunctools
zgubić
111

Napisałem to dla siebie ... Aby użyć go do prawdziwych, jednorazowo obliczonych leniwych właściwości. Podoba mi się, ponieważ unika przyklejania dodatkowych atrybutów do obiektów, 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: lazy_propertyKlasa nie jest deskryptorem danych , co oznacza, że ​​jest przeznaczona tylko do odczytu. Dodanie __set__metody uniemożliwiłoby jej prawidłowe działanie.

Cyklon
źródło
9
Trochę to zajęło zrozumienie, ale jest to absolutnie oszałamiająca odpowiedź. Podoba mi się, jak sama funkcja jest zastępowana wartością, którą oblicza.
Paul Etherton,
2
Dla potomnych: inne wersje tego zostały zaproponowane w innych odpowiedziach od czasu (ref. 1 i 2 ). Wydaje się, że jest to popularne w frameworkach internetowych Pythona (pochodne istnieją w Pyramid i Werkzeug).
André Caron,
1
Dziękuję za uwagę, że Werkzeug ma werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira
3
Okazało się, że ta metoda jest 7,6 razy szybsza niż wybrana odpowiedź. (2.45 µs / 322 ns) Zobacz notatnik ipython
Dave Butler,
1
Uwaga: to nie przeszkadza w przypisaniu do fgetdrogi @property. Aby zapewnić niezmienność / idempotencję, musisz dodać __set__()metodę, która wywołuje AttributeError('can\'t set attribute')(lub jakikolwiek wyjątek / wiadomość Ci odpowiada, ale to właśnie propertywywołuje). To niestety pochodzi z wpływu wydajności ułamka mikrosekundy, ponieważ __get__()zostanie wywołana przy każdym dostępie niż ciągnięcie wartości fget od dict w drugim i kolejnym dostępem. Moim zdaniem warto zachować niezmienność / idempotencję, która jest kluczowa dla moich przypadków użycia, ale YMMV.
scanny
4

Oto wywoływalnym że pobiera opcjonalny argument limitu czasu, w __call__można również skopiować nad __name__, __doc__, __module__od nazw func za:

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]

dawny:

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
gnr
źródło
3

propertyjest klasą. Deskryptor być dokładne. Po prostu wyprowadzaj z niego i wdrażaj pożądane zachowanie.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')
Ignacio Vazquez-Abrams
źródło
3

To, czego naprawdę chcesz, to dekorator reify(powiązany ze źródłem!) Z Pyramid:

Użyj jako dekoratora metody klas. Działa prawie dokładnie tak samo, jak @propertydekorator w Pythonie , ale po pierwszym wywołaniu umieszcza wynik metody, którą ozdabia, w dyktacie instancji, skutecznie zastępując dekorowaną funkcję zmienną instancji. W języku Pythona jest to deskryptor niebędący danymi. Poniżej znajduje się przykład i jego zastosowanie:

>>> 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
Antti Haapala
źródło
1
Niezły, robi dokładnie to, czego potrzebowałem ... chociaż Pyramid może być dużą zależnością dla jednego dekoratora:)
ostrożnie
@detly Implementacja dekoratora jest prosta i możesz ją zaimplementować samodzielnie, bez potrzeby stosowania pyramidzależności.
Peter Wood
Stąd link mówi „źródło powiązane”: D
Antti Haapala,
@AnttiHaapala Zauważyłem, ale pomyślałem, że podkreślę, że jest łatwy do wdrożenia dla tych, którzy nie korzystają z linku.
Peter Wood
1

Jak dotąd dochodzi do pomieszania terminów i / lub pomieszania pojęć, zarówno w pytaniach, jak iw odpowiedziach.

Leniwa ocena oznacza po prostu, że coś jest oceniane w czasie wykonywania w ostatnim możliwym momencie, gdy potrzebna jest wartość. Właśnie to robi standardowy @propertydekorator. (*) Dekorowana 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)

(*) Właściwie prawdziwie leniwa ocena (porównaj np. Haskell) jest bardzo trudna do osiągnięcia w Pythonie (i skutkuje kodem, który jest daleki od idiomatyczności).

Zapamiętywanie jest poprawnym określeniem tego, czego szuka pytający. Czyste funkcje, które nie są uzależnione od efektów ubocznych dla oceny wartości powrotu może być bezpiecznie memoized i tam jest rzeczywiście dekorator w functools @functools.lru_cache więc bez konieczności pisania własnych dekoratorów, chyba że trzeba specjalizuje zachowanie.

Jason Herbburn
źródło
Użyłem terminu „leniwy”, ponieważ w pierwotnej implementacji element członkowski był obliczany / pobierany z bazy danych w momencie inicjalizacji obiektu i chcę odroczyć te obliczenia do momentu, gdy właściwość zostanie faktycznie użyta w szablonie. Wydawało mi się, że pasuje to do definicji lenistwa. Zgadzam się, że skoro moje pytanie już zakłada rozwiązanie z użyciem słowa @property„leniwy” nie ma w tym momencie większego sensu. (Pomyślałem również o memoisacji jako mapie danych wejściowych do danych wyjściowych w pamięci podręcznej, a ponieważ te właściwości mają tylko jedno wejście, nic, mapa wydawała się bardziej złożona niż to konieczne.)
detly
Zwróć uwagę, że wszystkie dekoratory, które ludzie sugerowali jako rozwiązania „po wyjęciu z pudełka”, również nie istniały, kiedy o to zapytałem.
detly
Zgadzam się z Jasonem, to jest kwestia buforowania / zapamiętywania, a nie leniwej oceny.
poindexter
@poindexter - Buforowanie nie do końca to obejmuje; nie rozróżnia wyszukiwania wartości w czasie inicjalizacji obiektu i buforowania jej od sprawdzania wartości w górę i buforowania jej podczas uzyskiwania dostępu do właściwości (co jest tutaj kluczową cechą). Jak mam to nazwać? Dekorator „Pamięć podręczna po pierwszym użyciu”?
detly
@detly Memoize. Powinieneś nazwać to Memoize. en.wikipedia.org/wiki/Memoization
poindexter
0

Możesz to zrobić przyjemnie 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żyć tej klasy właściwości jak zwykłej właściwości klasy (obsługuje również przypisywanie elementów, 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ść obliczona tylko za pierwszym razem, a następnie wykorzystaliśmy zapisaną wartość

Wynik:

I am calculating value
My calculated value
My calculated value
2
2
rezakamalifard
źródło