Apa itu memoisasi dan bagaimana saya bisa menggunakannya dengan Python?

378

Saya baru saja memulai Python dan saya tidak tahu apa itu memoisasi dan bagaimana menggunakannya. Juga, bolehkah saya memiliki contoh sederhana?

blur959
sumber
215
Ketika kalimat kedua dari artikel wikipedia yang relevan berisi frasa "parsing keturunan saling recursive [1] dalam algoritma parsing top-down umum [2] [3] yang mengakomodasi ambiguitas dan rekursi kiri dalam waktu dan ruang polinomial," saya pikir sangat tepat untuk bertanya kepada SO apa yang sedang terjadi.
Clueless
10
@Clueless: Frasa itu didahului oleh "Memoisasi juga telah digunakan dalam konteks lain (dan untuk tujuan selain kecepatan,), seperti dalam". Jadi itu hanya daftar contoh (dan tidak perlu dipahami); itu bukan bagian dari penjelasan memoisasi.
ShreevatsaR
1
@StefanGruenwald Tautan itu sudah mati. Bisakah Anda menemukan pembaruan?
JS.
2
Tautan baru ke file pdf, karena pycogsci.info tidak berfungsi
Stefan Gruenwald
4
@Clueless, Artikel ini sebenarnya mengatakan " parsing keturunan saling rekursif sederhana [1] dalam algoritma parsing top-down umum [2] [3] yang mengakomodasi ambiguitas dan rekursi kiri dalam waktu dan ruang polinomial". Anda melewatkan yang sederhana , yang jelas membuat contoh itu jauh lebih jelas :).
studgeek

Jawaban:

353

Memoisasi secara efektif mengacu pada mengingat ("memoisasi" → "memorandum" → untuk diingat) hasil pemanggilan metode berdasarkan input metode dan kemudian mengembalikan hasil yang diingat daripada menghitung hasilnya lagi. Anda dapat menganggapnya sebagai cache untuk hasil metode. Untuk perincian lebih lanjut, lihat halaman 387 untuk definisi dalam Pengantar Algoritma (3e), Cormen et al.

Contoh sederhana untuk menghitung faktorial menggunakan memoisasi dengan Python akan menjadi seperti ini:

factorial_memo = {}
def factorial(k):
    if k < 2: return 1
    if k not in factorial_memo:
        factorial_memo[k] = k * factorial(k-1)
    return factorial_memo[k]

Anda bisa menjadi lebih rumit dan merangkum proses memoisasi ke dalam kelas:

class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        #Warning: You may wish to do a deepcopy here if returning objects
        return self.memo[args]

Kemudian:

def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

factorial = Memoize(factorial)

Fitur yang dikenal sebagai " dekorator " telah ditambahkan dalam Python 2.4 yang memungkinkan Anda untuk sekarang cukup menulis yang berikut untuk mencapai hal yang sama:

@Memoize
def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

The Python dekorator Perpustakaan memiliki dekorator serupa yang disebut memoizedyang sedikit lebih kuat dari Memoizekelas yang ditampilkan di sini.

jason
sumber
2
Terima kasih atas saran ini. Kelas Memoize adalah solusi elegan yang dapat dengan mudah diterapkan pada kode yang ada tanpa perlu banyak refactoring.
Kapten Lepton
10
Solusi kelas Memoize adalah buggy, itu tidak akan bekerja sama dengan factorial_memo, karena bagian factorialdalam def factorialmasih menyebut yang lama tidak dapat diemo factorial.
adamsmith
9
Omong-omong, Anda juga bisa menulis if k not in factorial_memo:, yang lebih baik daripada membaca if not k in factorial_memo:.
ShreevatsaR
5
Harus benar-benar melakukan ini sebagai dekorator.
Emlyn O'Regan
3
@ durden2.0 Saya tahu ini adalah komentar lama, tetapi argstuple. def some_function(*args)membuat args tuple.
Adam Smith
232

Baru untuk Python 3.2 adalah functools.lru_cache. Secara default, hanya cache panggilan 128 yang terakhir digunakan, tapi Anda dapat mengatur maxsizeuntuk Noneuntuk menunjukkan bahwa cache tidak harus berakhir:

import functools

@functools.lru_cache(maxsize=None)
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

Fungsi ini dengan sendirinya sangat lambat, coba fib(36)dan Anda harus menunggu sekitar sepuluh detik.

Menambahkan lru_cacheanotasi memastikan bahwa jika fungsi baru-baru ini dipanggil untuk nilai tertentu, itu tidak akan menghitung ulang nilai itu, tetapi menggunakan hasil sebelumnya yang di-cache. Dalam hal ini, ini mengarah pada peningkatan kecepatan yang luar biasa, sementara kode tidak berantakan dengan detail caching.

Flimm
sumber
2
Mencoba fib (1000), mendapat RecursionError: kedalaman rekursi maksimum melebihi dalam perbandingan
X Æ A-12
5
@Andyk Batas rekursi Py3 default adalah 1000. Pertama kali fibdipanggil, harus berulang ke kasing dasar sebelum memoisasi dapat terjadi. Jadi, perilaku Anda sudah sesuai harapan.
Quelklef
1
Jika saya tidak salah, cache hanya sampai proses tidak terbunuh, kan? Atau apakah itu tembolok terlepas dari apakah prosesnya dimatikan? Seperti, misalnya saya memulai ulang sistem saya - apakah hasil yang di-cache masih di-cache?
Kristada673
1
@ Kristada673 Ya, itu disimpan dalam memori proses, bukan pada disk.
Flimm
2
Perhatikan bahwa ini mempercepat bahkan menjalankan fungsi pertama kali, karena ini adalah fungsi rekursif dan menyimpan hasil perantara sendiri. Mungkin lebih baik untuk menggambarkan fungsi non-rekursif yang secara inheren lambat untuk membuatnya lebih jelas untuk boneka seperti saya. : D
endolith
61

Jawaban lainnya mencakup apa yang cukup baik. Saya tidak mengulanginya. Hanya beberapa poin yang mungkin bermanfaat bagi Anda.

Biasanya, memoisasi adalah operasi yang dapat Anda terapkan pada fungsi apa pun yang menghitung sesuatu (mahal) dan mengembalikan nilai. Karena itu, ini sering diterapkan sebagai dekorator . Implementasinya sangat mudah dan akan seperti ini

memoised_function = memoise(actual_function)

atau diekspresikan sebagai dekorator

@memoise
def actual_function(arg1, arg2):
   #body
Noufal Ibrahim
sumber
18

Memoisasi adalah menjaga hasil perhitungan yang mahal dan mengembalikan hasil yang di-cache daripada terus-menerus menghitung ulang.

Ini sebuah contoh:

def doSomeExpensiveCalculation(self, input):
    if input not in self.cache:
        <do expensive calculation>
        self.cache[input] = result
    return self.cache[input]

Deskripsi yang lebih lengkap dapat ditemukan di entri wikipedia tentang memoisasi .

Bryan Oakley
sumber
Hmm, sekarang jika itu benar Python, itu akan bergoyang, tetapi tampaknya tidak menjadi ... oke, jadi "cache" bukan dict? Karena jika ya, seharusnya if input not in self.cache dan self.cache[input] ( has_keysudah usang sejak ... di awal seri 2.x, jika tidak 2.0. Tidak self.cache(index)pernah benar. IIRC)
Jürgen A. Erhard
15

Jangan lupa hasattrfungsi bawaan, untuk mereka yang ingin kerajinan tangan. Dengan begitu Anda dapat menyimpan cache mem di dalam definisi fungsi (sebagai lawan global).

def fact(n):
    if not hasattr(fact, 'mem'):
        fact.mem = {1: 1}
    if not n in fact.mem:
        fact.mem[n] = n * fact(n - 1)
    return fact.mem[n]
Karel Kubat
sumber
Ini sepertinya ide yang sangat mahal. Untuk setiap n, itu tidak hanya cache hasil untuk n, tetapi juga untuk 2 ... n-1.
codeforester
15

Saya menemukan ini sangat berguna

def memoize(function):
    from functools import wraps

    memo = {}

    @wraps(function)
    def wrapper(*args):
        if args in memo:
            return memo[args]
        else:
            rv = function(*args)
            memo[args] = rv
            return rv
    return wrapper


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

fibonacci(25)
mr.bjerre
sumber
Lihat docs.python.org/3/library/functools.html#functools.wraps untuk alasan penggunaannya functools.wraps.
anishpatel
1
Apakah saya perlu menghapus secara manual memosehingga memori dibebaskan?
nos
Seluruh ide adalah bahwa hasilnya disimpan di dalam memo dalam suatu sesi. Yaitu tidak ada yang dibersihkan
mr.bjerre
6

Memoisasi pada dasarnya menyimpan hasil operasi masa lalu yang dilakukan dengan algoritma rekursif untuk mengurangi kebutuhan untuk melintasi pohon rekursi jika perhitungan yang sama diperlukan pada tahap selanjutnya.

lihat http://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/

Contoh Memoisasi Fibonacci dengan Python:

fibcache = {}
def fib(num):
    if num in fibcache:
        return fibcache[num]
    else:
        fibcache[num] = num if num < 2 else fib(num-1) + fib(num-2)
        return fibcache[num]
Romaine Carter
sumber
2
Untuk kinerja lebih baik pra-seed fibcache Anda dengan beberapa nilai pertama yang diketahui, maka Anda dapat mengambil logika ekstra untuk menangani mereka dari 'jalur panas' kode.
jkflying
5

Memoisasi adalah konversi fungsi menjadi struktur data. Biasanya orang ingin konversi terjadi secara bertahap dan malas (berdasarkan permintaan elemen domain tertentu - atau "kunci"). Dalam bahasa fungsional malas, konversi malas ini dapat terjadi secara otomatis, dan dengan demikian memoisasi dapat diimplementasikan tanpa efek samping (eksplisit).

Conal
sumber
5

Baiklah saya harus jawab bagian pertama dulu: apa itu memoisasi?

Itu hanya metode untuk menukar memori untuk waktu. Pikirkan Tabel Multiplikasi .

Menggunakan objek yang dapat diubah sebagai nilai default dalam Python biasanya dianggap buruk. Tetapi jika menggunakannya dengan bijak, itu sebenarnya bisa berguna untuk mengimplementasikan memoization.

Berikut ini contoh yang diadaptasi dari http://docs.python.org/2/faq/design.html#why-are-default-values-share-between-objects

Menggunakan bisa berubah dictdalam definisi fungsi, hasil yang dihitung menengah dapat di-cache (misalnya ketika menghitung factorial(10)setelah menghitung factorial(9), kami dapat menggunakan kembali semua hasil menengah)

def factorial(n, _cache={1:1}):    
    try:            
        return _cache[n]           
    except IndexError:
        _cache[n] = factorial(n-1)*n
        return _cache[n]
yegle
sumber
4

Berikut ini adalah solusi yang akan bekerja dengan daftar atau jenis argumen tanpa merengek:

def memoize(fn):
    """returns a memoized version of any function that can be called
    with the same list of arguments.
    Usage: foo = memoize(foo)"""

    def handle_item(x):
        if isinstance(x, dict):
            return make_tuple(sorted(x.items()))
        elif hasattr(x, '__iter__'):
            return make_tuple(x)
        else:
            return x

    def make_tuple(L):
        return tuple(handle_item(x) for x in L)

    def foo(*args, **kwargs):
        items_cache = make_tuple(sorted(kwargs.items()))
        args_cache = make_tuple(args)
        if (args_cache, items_cache) not in foo.past_calls:
            foo.past_calls[(args_cache, items_cache)] = fn(*args,**kwargs)
        return foo.past_calls[(args_cache, items_cache)]
    foo.past_calls = {}
    foo.__name__ = 'memoized_' + fn.__name__
    return foo

Perhatikan bahwa pendekatan ini dapat diperluas secara alami ke objek apa pun dengan menerapkan fungsi hash Anda sendiri sebagai kasus khusus di handle_item. Misalnya, untuk membuat pendekatan ini berfungsi untuk fungsi yang mengambil set sebagai argumen input, Anda bisa menambahkan ke handle_item:

if is_instance(x, set):
    return make_tuple(sorted(list(x)))
RussellStewart
sumber
1
Usaha yang bagus. Tanpa rengekan, listargumen [1, 2, 3]salah dapat dianggap sama dengan setargumen berbeda dengan nilai {1, 2, 3}. Selain itu, set tidak teratur seperti kamus, jadi mereka juga perlu sorted(). Perhatikan juga bahwa argumen struktur data rekursif akan menyebabkan infinite loop.
martineau
Ya, set harus ditangani oleh handle_item casing khusus (x) dan penyortiran. Saya seharusnya tidak mengatakan bahwa implementasi ini menangani set, karena tidak - tetapi intinya adalah bahwa hal itu dapat dengan mudah diperluas untuk melakukannya dengan casing khusus handle_item, dan hal yang sama akan bekerja untuk kelas atau objek yang dapat diubah apa saja selama Anda bersedia menulis fungsi hash sendiri. Bagian yang sulit - berurusan dengan daftar multi-dimensi atau kamus - sudah dibahas di sini, jadi saya telah menemukan bahwa fungsi memoize ini jauh lebih mudah untuk digunakan sebagai basis daripada jenis "Saya hanya mengambil argumen hashable" yang sederhana.
RussellStewart
Masalah yang saya sebutkan adalah karena fakta bahwa huruf "s" listdan set"tupleized" menjadi hal yang sama dan karenanya tidak dapat dibedakan satu sama lain. Contoh kode untuk menambahkan dukungan untuk yang setsdijelaskan dalam pembaruan terbaru Anda tidak menghindarkan saya dari ketakutan. Ini dapat dengan mudah dilihat dengan melewati secara terpisah [1,2,3]dan {1,2,3}sebagai argumen untuk fungsi tes "memoize" dan melihat apakah itu dipanggil dua kali, sebagaimana mestinya, atau tidak.
martineau
ya, saya membaca masalah itu, tetapi saya tidak mengatasinya karena saya pikir itu jauh lebih kecil daripada yang lain yang Anda sebutkan. Kapan terakhir kali Anda menulis fungsi memo di mana argumen tetap bisa berupa daftar atau set, dan keduanya menghasilkan output yang berbeda? Jika Anda mengalami kasus yang jarang terjadi, Anda hanya perlu menulis ulang handle_item untuk menambahkan, katakan 0 jika elemennya adalah himpunan, atau 1 jika itu adalah daftar.
RussellStewart
Sebenarnya, ada masalah yang sama dengan lists dan dicts karena mungkin untuk listmemiliki hal yang persis sama di dalamnya yang dihasilkan dari memanggil make_tuple(sorted(x.items()))kamus. Solusi sederhana untuk kedua kasus adalah dengan memasukkan type()nilai dalam tuple yang dihasilkan. Saya bisa memikirkan cara yang lebih sederhana khusus untuk menangani sets, tetapi tidak menyamaratakan.
martineau
3

Solusi yang berfungsi dengan argumen posisi dan kata kunci secara terpisah dari urutan penetapan arg kata kunci (menggunakan inspect.getargspec ):

import inspect
import functools

def memoize(fn):
    cache = fn.cache = {}
    @functools.wraps(fn)
    def memoizer(*args, **kwargs):
        kwargs.update(dict(zip(inspect.getargspec(fn).args, args)))
        key = tuple(kwargs.get(k, None) for k in inspect.getargspec(fn).args)
        if key not in cache:
            cache[key] = fn(**kwargs)
        return cache[key]
    return memoizer

Pertanyaan serupa: Mengidentifikasi panggilan fungsi varargs setara untuk memoisasi dengan Python

ndpu
sumber
2
cache = {}
def fib(n):
    if n <= 1:
        return n
    else:
        if n not in cache:
            cache[n] = fib(n-1) + fib(n-2)
        return cache[n]
Vikrant Singh
sumber
4
Anda bisa menggunakan hanya if n not in cachesebagai gantinya. menggunakan cache.keysakan membangun daftar yang tidak perlu dalam python 2
n611x007
2

Hanya ingin menambah jawaban yang sudah disediakan, pustaka dekorator Python memiliki beberapa implementasi sederhana namun bermanfaat yang juga dapat memo "tipe yang tidak dapat dihancurkan", tidak seperti functools.lru_cache.

Sid
sumber
1
Dekorator ini tidak menghafal "tipe yang tidak dapat dihancurkan" ! Itu hanya jatuh kembali ke memanggil fungsi tanpa memoisasi, bertentangan dengan eksplisit lebih baik daripada dogma implisit .
ostrokach