Python memoising / deferred lookup property decorator

109

Baru-baru ini saya telah melalui basis kode yang ada yang berisi banyak kelas di mana atribut contoh mencerminkan nilai yang disimpan dalam database. Saya telah merefaktor banyak atribut ini agar pencarian database mereka ditangguhkan, yaitu. tidak akan dijalankan di konstruktor tetapi hanya setelah dibaca pertama kali. Atribut ini tidak berubah selama masa pakai instance, tetapi merupakan hambatan nyata untuk menghitungnya pertama kali dan hanya benar-benar diakses untuk kasus khusus. Oleh karena itu, mereka juga dapat di-cache setelah diambil dari database (oleh karena itu hal ini sesuai dengan definisi memoisation di mana input hanyalah "tanpa input").

Saya mendapati diri saya mengetik cuplikan kode berikut berulang kali untuk berbagai atribut di berbagai kelas:

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

Apakah sudah ada dekorator untuk melakukan ini dengan Python yang tidak saya sadari? Atau, adakah cara yang cukup sederhana untuk mendefinisikan dekorator yang melakukan ini?

Saya bekerja dengan Python 2.5, tetapi jawaban 2.6 mungkin masih menarik jika berbeda secara signifikan.

Catatan

Pertanyaan ini ditanyakan sebelum Python memasukkan banyak dekorator siap pakai untuk ini. Saya telah memperbaruinya hanya untuk memperbaiki terminologi.

detly
sumber
Saya menggunakan Python 2.7, dan saya tidak melihat apa pun tentang dekorator siap pakai untuk ini. Dapatkah Anda memberikan tautan ke dekorator siap pakai yang disebutkan dalam pertanyaan?
Bamcclur
@Bamcclur maaf, dulu ada komentar lain yang merincinya, tidak yakin kenapa dihapus. -Satunya yang saya dapat menemukan sekarang adalah Python 3 satu: functools.lru_cache().
detly
Tidak yakin ada built-in (setidaknya Python 2.7), tetapi ada properti cache
guyarad
@guyarad Saya tidak melihat komentar ini sampai sekarang. Itu adalah perpustakaan yang luar biasa! Posting itu sebagai jawaban agar saya bisa memberi suara positif.
detly

Jawaban:

12

Untuk semua jenis utilitas hebat, saya menggunakan bolton .

Sebagai bagian dari perpustakaan itu, Anda memiliki properti cache :

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)
guyarad
sumber
124

Berikut ini contoh implementasi dekorator properti malas:

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)

Sesi interaktif:

>>> 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
sumber
1
Dapatkah seseorang merekomendasikan nama yang sesuai untuk fungsi bagian dalam? Aku sangat buruk dalam memberi nama di pagi hari ...
Mike Boers
2
Saya biasanya menamai fungsi bagian dalam sama dengan fungsi luar dengan garis bawah sebelumnya. Jadi "_lazyprop" - mengikuti filosofi "khusus penggunaan internal" dari semangat 8.
dihabiskan
1
Ini berfungsi dengan baik :) Saya tidak tahu mengapa tidak pernah terpikir oleh saya untuk menggunakan dekorator pada fungsi bersarang seperti itu juga.
detly
4
mengingat protokol deskriptor non-data, yang ini jauh lebih lambat dan kurang elegan daripada jawaban di bawah ini menggunakan__get__
Ronny
1
Tip: Letakkan di @wraps(fn)bawah ini @propertyuntuk tidak kehilangan string dokumen Anda dll ( wrapsberasal dari functools)
letmaik
111

Saya menulis ini untuk diri saya sendiri ... Untuk digunakan untuk properti malas yang dihitung satu kali . Saya menyukainya karena ini menghindari penempelan atribut tambahan pada objek, dan setelah diaktifkan tidak membuang waktu untuk memeriksa keberadaan atribut, dll .:

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

Catatan: lazy_propertyKelas ini adalah deskriptor non-data , yang artinya ini hanya-baca. Menambahkan __set__metode akan mencegahnya bekerja dengan benar.

Topan
sumber
9
Ini membutuhkan sedikit waktu untuk memahami tetapi merupakan jawaban yang benar-benar menakjubkan. Saya suka bagaimana fungsinya itu sendiri diganti dengan nilai yang dihitungnya.
Paul Etherton
2
Untuk anak cucu: versi lain dari ini telah diusulkan dalam jawaban lain sejak (ref 1 dan 2 ). Tampaknya ini adalah salah satu yang populer di kerangka kerja web Python (turunannya ada di Pyramid dan Werkzeug).
André Caron
1
Terima kasih telah memperhatikan bahwa Werkzeug memiliki werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira
3
Saya menemukan metode ini menjadi 7,6 kali lebih cepat dari jawaban yang dipilih. (2,45 µs / 322 ns) Lihat notebook ipython
Dave Butler
1
NB: ini tidak mencegah tugas untuk fgetjalan @propertytidak. Untuk memastikan kekekalan / idempotensi, Anda perlu menambahkan __set__()metode yang memunculkan AttributeError('can\'t set attribute')(atau pengecualian / pesan apa pun yang cocok untuk Anda, tetapi inilah yang propertymuncul). Sayangnya ini datang dengan dampak kinerja sepersekian mikrodetik karena __get__()akan dipanggil pada setiap akses daripada menarik nilai fget dari dict pada akses kedua dan selanjutnya. Layak menurut pendapat saya untuk mempertahankan kekekalan / idempotensi, yang merupakan kunci untuk kasus penggunaan saya, tetapi YMMV.
scanny
4

Berikut adalah callable yang mengambil timeout argumen opsional, dalam __call__Anda juga bisa menyalin atas __name__, __doc__, __module__dari namespace func ini:

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
gnr
sumber
3

propertyadalah sebuah kelas. Sebuah deskriptor tepatnya. Cukup dapatkan darinya dan terapkan perilaku yang diinginkan.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')
Ignacio Vazquez-Abrams
sumber
3

Apa yang Anda benar-benar inginkan adalah reify(sumber terkait!) Dekorator dari Piramida:

Gunakan sebagai dekorator metode kelas. Ini beroperasi hampir persis seperti @propertydekorator Python , tetapi menempatkan hasil dari metode yang didekorasi ke dalam instance dict setelah panggilan pertama, secara efektif menggantikan fungsi yang didekorasi dengan variabel instance. Ini, dalam bahasa Python, deskriptor non-data. Berikut contoh dan penggunaannya:

>>> 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
sumber
1
Bagus, melakukan apa yang saya butuhkan ... meskipun Pyramid mungkin menjadi ketergantungan besar untuk satu dekorator:)
detly
@detly Implementasi dekorator sederhana, dan Anda dapat mengimplementasikannya sendiri, tanpa pyramidketergantungan.
Peter Wood
Karenanya tautan tersebut mengatakan "sumber terkait": D
Antti Haapala
@AnttiHaapala Saya perhatikan, tetapi saya pikir saya akan menyoroti bahwa itu mudah diterapkan bagi mereka yang tidak mengikuti tautan.
Peter Wood
1

Ada campuran istilah dan / atau kebingungan konsep baik dalam pertanyaan maupun jawaban sejauh ini.

Evaluasi malas hanya berarti bahwa sesuatu dievaluasi pada waktu proses pada saat-saat terakhir yang memungkinkan ketika suatu nilai diperlukan. Dekorator standar @propertymelakukan hal itu. (*) Fungsi yang didekorasi hanya dievaluasi dan setiap kali Anda membutuhkan nilai properti itu. (lihat artikel wikipedia tentang evaluasi malas)

(*) Sebenarnya evaluasi malas yang sebenarnya (bandingkan mis. Haskell) sangat sulit dicapai dengan python (dan menghasilkan kode yang jauh dari idiomatik).

Memoisasi adalah istilah yang tepat untuk apa yang tampaknya dicari oleh penanya. Fungsi murni yang tidak bergantung pada efek samping untuk evaluasi nilai pengembalian dapat disimpan dengan aman dan sebenarnya terdapat dekorator di functools @functools.lru_cache sehingga tidak perlu menulis dekorator sendiri kecuali jika Anda memerlukan perilaku khusus.

Jason Herbburn
sumber
Saya menggunakan istilah "malas" karena dalam implementasi asli, anggota dihitung / diambil dari DB pada saat inisialisasi objek, dan saya ingin menunda komputasi itu hingga properti itu benar-benar digunakan dalam template. Bagi saya hal ini cocok dengan definisi kemalasan. Saya setuju bahwa karena pertanyaan saya sudah mengasumsikan solusi menggunakan @property, "malas" tidak masuk akal pada saat itu. (Saya juga menganggap memoisation sebagai peta input ke output yang di-cache, dan karena properti ini hanya memiliki satu input, tidak ada apa-apa, peta tampak lebih kompleks daripada yang diperlukan.)
detly
Perhatikan bahwa semua dekorator yang disarankan orang sebagai solusi "di luar kotak" tidak ada ketika saya menanyakan hal ini juga.
detly
Saya setuju dengan Jason, ini adalah pertanyaan tentang caching / memoization bukan evaluasi malas.
poindexter
@ poindexter - Caching tidak cukup menutupi; itu tidak membedakan mencari nilai pada objek init time dan menyimpannya dari mencari nilai dan menyimpannya ketika properti diakses (yang merupakan fitur utama di sini). Aku harus menyebutnya apa? Dekorator "Cache-after-first-use"?
detly
@Memang Anda harus menyebutnya Memoize. en.wikipedia.org/wiki/Memoization
poindexter
0

Anda dapat melakukan ini dengan baik dan mudah dengan membangun kelas dari properti asli 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

Kita dapat menggunakan kelas properti ini seperti properti kelas biasa (Ini juga mendukung penugasan item seperti yang Anda lihat)

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)

Nilai hanya dihitung pertama kali dan setelah itu kami menggunakan nilai simpanan kami

Keluaran:

I am calculating value
My calculated value
My calculated value
2
2
rezakamalifard
sumber