Apakah ada dekorator untuk menyimpan nilai pengembalian fungsi?

157

Pertimbangkan yang berikut ini:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Saya baru, tapi saya pikir caching bisa diperhitungkan menjadi dekorator. Hanya saya tidak menemukan yang seperti itu;)

PS perhitungan sebenarnya tidak tergantung pada nilai yang bisa diubah

Tobias
sumber
Mungkin ada dekorator di luar sana yang memiliki beberapa kemampuan seperti itu, tetapi Anda belum menentukan secara spesifik apa yang Anda inginkan. Jenis backend caching apa yang Anda gunakan? Dan bagaimana nilai akan dikunci? Saya berasumsi dari kode Anda bahwa apa yang sebenarnya Anda minta adalah properti read-only cached.
David Berger
Ada dekorator memo yang melakukan apa yang Anda sebut "caching"; mereka biasanya bekerja pada fungsi seperti itu (apakah dimaksudkan untuk menjadi metode atau tidak) yang hasilnya tergantung pada argumen mereka (bukan pada hal-hal yang bisa berubah seperti diri! -) dan menyimpan memo-dict terpisah.
Alex Martelli

Jawaban:

206

Mulai dari Python 3.2 ada dekorator bawaan:

@functools.lru_cache(maxsize=100, typed=False)

Penghias untuk membungkus suatu fungsi dengan memoable callable yang menyimpan hingga maksimum panggilan terbaru. Ini dapat menghemat waktu ketika fungsi yang mahal atau terikat I / O secara berkala dipanggil dengan argumen yang sama.

Contoh cache LRU untuk menghitung angka Fibonacci :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Jika Anda terjebak dengan Python 2.x, berikut adalah daftar pustaka memoisasi lain yang kompatibel:

Paolo Moretti
sumber
backport sekarang dapat ditemukan di sini: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord
@gerrit secara teori ini berfungsi untuk objek hash secara umum - meskipun beberapa objek hash hanya sama jika mereka adalah objek yang sama (seperti objek yang ditentukan pengguna tanpa fungsi __hash __ () eksplisit).
Jonathan
1
@ Jonathan Berhasil, tetapi salah. Jika saya memberikan argumen hashable, bisa berubah, dan mengubah nilai objek setelah panggilan pertama fungsi, panggilan kedua akan mengembalikan objek yang diubah, bukan yang asli. Itu hampir pasti bukan yang diinginkan pengguna. Agar bisa berfungsi untuk argumen yang bisa diubah, perlu lru_cachemembuat salinan dari hasil apa pun yang di-cache, dan tidak ada salinan yang dibuat dalam functools.lru_cacheimplementasinya. Melakukannya juga akan berisiko menciptakan masalah memori yang sulit ditemukan ketika digunakan untuk men-cache objek besar.
gerrit
@gerit Maukah Anda menindaklanjuti di sini: stackoverflow.com/questions/44583381/… ? Saya tidak sepenuhnya mengikuti teladan Anda.
Jonathan
28

Sepertinya Anda tidak meminta dekorator memoisasi tujuan umum (mis. Anda tidak tertarik pada kasus umum di mana Anda ingin menyimpan nilai pengembalian cache untuk nilai argumen yang berbeda). Yaitu, Anda ingin memiliki ini:

x = obj.name  # expensive
y = obj.name  # cheap

sementara dekorator memoisasi keperluan umum akan memberi Anda ini:

x = obj.name()  # expensive
y = obj.name()  # cheap

Saya sampaikan bahwa sintaks pemanggilan metode adalah gaya yang lebih baik, karena ini menyarankan kemungkinan perhitungan yang mahal sementara sintaksis properti menyarankan pencarian cepat.

[Pembaruan: Dekorator memoisasi berbasis kelas yang saya tautkan dan kutip di sini sebelumnya tidak berfungsi untuk metode. Saya telah menggantinya dengan fungsi dekorator.] Jika Anda ingin menggunakan dekorator memoisasi tujuan umum, berikut ini sederhana:

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

Contoh penggunaan:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

Dekorator memoisasi lain dengan batasan ukuran cache dapat ditemukan di sini .

Nathan Kitchen
sumber
Tidak ada dekorator yang disebutkan dalam semua jawaban yang cocok untuk metode! Mungkin karena mereka berbasis kelas. Hanya satu diri yang dilewati? Yang lain berfungsi dengan baik, tetapi tidak ada salahnya menyimpan nilai dalam fungsi.
Tobias
2
Saya pikir Anda mungkin mengalami masalah jika args tidak dapat di hash.
Tidak dikenal
1
@ Tidak Diketahui Ya, dekorator pertama yang saya kutip di sini terbatas pada tipe hashable. Yang di ActiveState (dengan batas ukuran cache) mengambil argumen menjadi string (hashable) yang tentu saja lebih mahal tetapi lebih umum.
Nathan Kitchen
@vanity Terima kasih telah menunjukkan keterbatasan dekorator berbasis kelas. Saya telah merevisi jawaban saya untuk menunjukkan fungsi dekorator, yang berfungsi untuk metode (saya benar-benar menguji yang ini).
Nathan Kitchen
1
@SiminJie Penghias hanya dipanggil sekali, dan fungsi terbungkus yang dikembalikan adalah sama dengan yang digunakan untuk semua panggilan berbeda fibonacci. Fungsi itu selalu menggunakan memokamus yang sama .
Nathan Kitchen
22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

Sampel menggunakan:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}
acmerfight
sumber
Aneh! Bagaimana cara kerjanya? Tampaknya tidak seperti dekorator lain yang pernah saya lihat.
PascalVKooten
1
Solusi ini mengembalikan TypeError jika seseorang menggunakan argumen kata kunci, misalnya foo (3, b = 5)
kadee
1
Masalah solusinya, adalah tidak memiliki batas memori. Adapun argumen yang disebutkan, Anda bisa menambahkannya ke __ call__ dan __ missing__ seperti ** nargs
Leonid Mednikov
16

functools.cached_propertyDekorator Python 3.8

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_propertydari Werkzeug disebutkan di: https://stackoverflow.com/a/5295190/895245 tetapi versi yang seharusnya diturunkan akan digabung menjadi 3,8, yang luar biasa.

Penghias ini dapat dilihat sebagai caching @property, atau sebagai pembersih @functools.lru_cacheketika Anda tidak memiliki argumen.

Dokumen mengatakan:

@functools.cached_property(func)

Mengubah metode kelas menjadi properti yang nilainya dihitung sekali dan kemudian di-cache sebagai atribut normal untuk kehidupan instance. Mirip dengan properti (), dengan penambahan caching. Berguna untuk properti contoh yang mahal yang tidak dapat diubah dengan efektif.

Contoh:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

Baru dalam versi 3.8.

Catatan Dekorator ini mensyaratkan bahwa atribut dict pada setiap instance harus pemetaan yang bisa berubah-ubah. Ini berarti tidak akan bekerja dengan beberapa tipe, seperti metaclasses (karena atribut dict pada instance type adalah read-only proxy untuk namespace kelas), dan mereka yang menentukan slot tanpa menyertakan dict sebagai salah satu slot yang didefinisikan (seperti kelas tersebut sama sekali tidak memberikan atribut dict ).

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
10

Werkzeug memiliki cached_propertydekorator ( dokumen , sumber )

Imran
sumber
Iya. Ini bermanfaat untuk dibedakan dari kasus memoisasi umum, karena memoisasi standar tidak berfungsi jika kelas tidak hashable.
Jameson Quinn
1
Sekarang di Python 3.8: docs.python.org/dev/library/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
9

Saya mengkodekan kelas dekorator sederhana ini untuk menyimpan respons fungsi cache. Saya merasa ini SANGAT berguna untuk proyek saya:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

Penggunaannya sangat mudah:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))
Pablo Besada
sumber
1
Tanda @cachedkurung pertama Anda tidak ada. Lain itu hanya akan mengembalikan cachedobjek di tempat myfuncdan ketika dipanggil seperti myfunc()itu innerakan selalu dikembalikan sebagai nilai kembali
Markus Meskanen
6

DISCLAIMER: Saya penulis kids.cache .

Anda harus memeriksa kids.cache, ia menyediakan @cachedekorator yang bekerja pada python 2 dan python 3. Tidak ada ketergantungan, ~ 100 baris kode. Sangat mudah digunakan, misalnya, dengan mengingat kode Anda, Anda dapat menggunakannya seperti ini:

pip install kids.cache

Kemudian

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

Atau Anda bisa meletakkan @cachedekorator setelah @property(hasil yang sama).

Menggunakan cache pada properti disebut evaluasi malas , kids.cachedapat melakukan lebih banyak lagi (ini berfungsi dengan argumen, properti, semua jenis metode, dan bahkan kelas ...). Untuk pengguna tingkat lanjut, kids.cachedukungan cachetoolsyang menyediakan toko cache mewah ke python 2 dan python 3 (LRU, LFU, TTL, cache RR).

CATATAN PENTING : penyimpanan cache default kids.cacheadalah dict standar, yang tidak disarankan untuk program yang berjalan lama dengan kueri yang berbeda karena akan mengarah pada penyimpanan cache yang terus berkembang. Untuk penggunaan ini, Anda dapat memasang plugin toko cache lain menggunakan misalnya ( @cache(use=cachetools.LRUCache(maxsize=2))untuk menghias fungsi / properti / kelas / metode ...)

vaab
sumber
Modul ini tampaknya menghasilkan waktu impor yang lambat pada python 2 ~ 0.9s (lihat: pastebin.com/raw/aA1ZBE9Z ). Saya menduga bahwa ini adalah karena baris ini github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (cf setuptools entry point). Saya membuat masalah untuk ini.
Att Righ
Ini adalah masalah untuk github.com/0k/kids.cache/issues/9 di atas .
Att Righ
Ini akan menyebabkan kebocoran memori.
Timothy Zhang
@vaab membuat instance cdari MyClass, dan memeriksanya dengan objgraph.show_backrefs([c], max_depth=10), ada rantai referensi dari objek kelas MyClasske c. Artinya, ctidak akan pernah dirilis sampai MyClassdirilis.
Timothy Zhang
@TimothyZhang Anda diundang dan dipersilakan untuk menambahkan kekhawatiran Anda di github.com/0k/kids.cache/issues/10 . Stackoverflow bukan tempat yang tepat untuk berdiskusi tentang hal itu. Dan klarifikasi lebih lanjut diperlukan. Terima kasih atas tanggapan Anda.
vaab
5

Ah, hanya perlu menemukan nama yang tepat untuk ini: " Evaluasi properti malas ".

Saya juga banyak melakukan ini; mungkin saya akan menggunakan resep itu dalam kode saya kapan-kapan.

Ken Arnold
sumber
4

Ada fastcache , yang merupakan "implementasi C dari Python 3 functools.lru_cache. Menyediakan speedup 10-30x lebih dari perpustakaan standar."

Sama seperti jawaban yang dipilih , hanya impor yang berbeda:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

Juga, ia dipasang di Anaconda , tidak seperti functools yang perlu diinstal .

Romi Kuntsman
sumber
1
functoolsadalah bagian dari pustaka standar, tautan yang Anda poskan adalah garpu git acak atau yang lainnya ...
cz
3

Ada contoh lain dari dekorator memoize di Python Wiki:

http://wiki.python.org/moin/PythonDecoratorLibrary#Memoize

Contoh itu agak cerdas, karena tidak akan men-cache hasil jika parameternya bisa berubah. (periksa kode itu, sangat sederhana dan menarik!)

Denilson Sa Maia
sumber
3

Jika Anda menggunakan Django Framework, ia memiliki properti seperti itu untuk men-cache tampilan atau respons penggunaan API @cache_page(time)dan mungkin ada opsi lain juga.

Contoh:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

Rincian lebih lanjut dapat ditemukan di sini .

Nikhil Kumar
sumber
2

Bersama dengan Contoh Memoize saya menemukan paket python berikut:

  • cachepy ; Memungkinkan mengatur ttl dan \ atau jumlah panggilan untuk fungsi yang di-cache; Juga, seseorang dapat menggunakan cache berbasis file terenkripsi ...
  • percache
gelembung
sumber
1

Saya menerapkan sesuatu seperti ini, menggunakan acar untuk ketekunan dan menggunakan sha1 untuk ID pendek yang hampir pasti-unik. Pada dasarnya cache hash kode fungsi dan hist argumen untuk mendapatkan sha1 kemudian mencari file dengan sha1 di namanya. Jika ada, itu akan membukanya dan mengembalikan hasilnya; jika tidak, ia memanggil fungsi dan menyimpan hasilnya (opsional hanya menyimpan jika butuh waktu untuk diproses).

Yang mengatakan, saya bersumpah saya menemukan modul yang ada yang melakukan ini dan menemukan diri saya di sini mencoba menemukan modul itu ... Yang paling dekat saya dapat temukan adalah ini, yang terlihat benar: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-caching-decorator.html

Satu-satunya masalah yang saya lihat adalah itu tidak akan bekerja dengan baik untuk input besar karena memiliki str (arg), yang tidak unik untuk array raksasa.

Akan lebih baik jika ada protokol unique_hash () yang memiliki kelas mengembalikan hash aman dari isinya. Saya pada dasarnya menerapkannya secara manual untuk jenis yang saya pedulikan.

Ben
sumber
1

Coba joblib http://pythonhosted.org/joblib/memory.html

from joblib import Memory
memory = Memory(cachedir=cachedir, verbose=0)
@memory.cache
    def f(x):
        print('Running f(%s)' % x)
        return x
Dror Hilman
sumber
1

Jika Anda menggunakan Django dan ingin men-cache view, lihat jawaban Nikhil Kumar .


Tetapi jika Anda ingin men-cache hasil fungsi APAPUN, Anda dapat menggunakan django-cache-utils .

Menggunakan kembali cache Django dan menyediakan cacheddekorator yang mudah digunakan :

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y
Greg Dubicki
sumber
1

@lru_cache tidak sempurna dengan nilai fungsi default

memdekorator saya :

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

dan kode untuk pengujian:

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

hasil - hanya 3 kali dengan tidur

tetapi dengan @lru_cacheitu akan menjadi 4 kali, karena ini:

print(count(1))
print(count(1, z=10))

akan dihitung dua kali (kerja buruk dengan default)

Sublimer
sumber