メモ化とは何ですか?Pythonでそれをどのように使用できますか?


378

Pythonを始めたばかりですが、メモ化とは何か、どのように使用するのかわかりません。また、簡単な例がありますか?


215
関連するウィキペディアの記事の2番目の文に、「多項式の時間と空間での曖昧性と左再帰に対応する一般的なトップダウン解析アルゴリズム[2] [3]での相互再帰降下解析[1]」というフレーズが含まれている場合、何が起こっているのかをSOに尋ねることは完全に適切です。
2010年

10
@無知:このフレーズの前には、「メモ化が他のコンテキスト(および速度向上以外の目的)でも使用されています」などが続きます。したがって、これは単なる例のリストです(理解する必要はありません)。メモ化の説明の一部ではありません。
ShreevatsaR 2014

1
@StefanGruenwaldそのリンクは死んでいる。アップデートを見つけてもらえますか?
JS。

2
pycogsci.infoがダウンしているため、pdfファイルへの新しいリンク:people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf
Stefan Gruenwald 14

4
@無知、記事は実際に「多項式の時間と空間でのあいまいさと左再帰に対応する一般的なトップダウン解析アルゴリズム[2] [3]での単純な相互再帰降下解析[1]」を述べています。あなたは単純なを見逃しました、それは明らかにその例をはるかに明確にします:)
studgeek 2017

回答:


353

メモ化とは、メソッドの入力に基づいてメソッド呼び出しの結果を記憶(「メモ」→「メモ」→記憶)することを意味し、結果を再計算するのではなく、記憶された結果を返します。メソッドの結果のキャッシュと考えることができます。詳細については、アルゴリズムの概要(3e)、Cormenらの定義の387ページを参照してください。

Pythonでメモ化を使用して階乗を計算する簡単な例は、次のようになります。

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]

より複雑にして、メモ化プロセスをクラスにカプセル化できます。

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]

次に:

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

factorial = Memoize(factorial)

デコレータ」と呼ばれる機能がPython 2.4に追加されました。これにより、次のように記述するだけで同じことを実行できます。

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

Pythonのデコレータライブラリと呼ばれる同様のデコレータ持ってmemoized少しより堅牢よりもMemoizeここに示されているクラスを。


2
この提案をありがとう。Memoizeクラスは、多くのリファクタリングを必要とせずに既存のコードに簡単に適用できるエレガントなソリューションです。
レプトン船長、2013

10
Memoizeクラスのソリューションはバグが多くfactorial_memofactorial内部はdef factorialまだ古いunmemoizeを呼び出しているため、と同じようには機能しませんfactorial
adamsmith 2013

9
ちなみにif k not in factorial_memo:、を読むこともできますif not k in factorial_memo:
ShreevatsaR 2014

5
これは本当にデコレータとして行うべきです。
Emlyn O'Regan 2014年

3
@ durden2.0私はこれが古いコメントであることを知っていますargsが、タプルです。def some_function(*args)argsをタプルにします。
アダム・スミス

232

Python 3.2の新機能はfunctools.lru_cacheです。デフォルトでは、最近使用された128の呼び出しのみがキャッシュmaxsizeNoneれますが、キャッシュを期限切れにしないことを示すようにを設定できます。

import functools

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

この関数自体は非常に遅いので、fib(36)10秒ほど待つ必要があります。

lru_cacheアノテーションを追加すると、特定の値に対して関数が最近呼び出された場合、その値は再計算されず、キャッシュされた以前の結果が使用されます。この場合、コードがキャッシングの詳細で乱雑になることはありませんが、速度が大幅に向上します。


2
fib(1000)を試して、RecursionErrorを取得:比較で最大再帰深度を超えました
-X

5
@AndykデフォルトのPy3再帰制限は1000です。最初にfib呼び出されたときは、メモ化が行われる前に、基本ケースまで再帰する必要があります。だから、あなたの行動はほぼ期待どおりです。
Quelklef、

1
私が間違っていなければ、プロセスが強制終了されなくなるまでキャッシュされますよね?または、プロセスが強制終了されたかどうかに関係なく、キャッシュしますか?たとえば、システムを再起動するとします-キャッシュされた結果は引き続きキャッシュされますか?
Kristada673 2018年

1
@ Kristada673はい、ディスクではなくプロセスのメモリに保存されます。
Flimm、

2
これは再帰関数であり、独自の中間結果をキャッシュしているため、関数の最初の実行でさえ速度が上がることに注意してください。私のようなダミーにわかりやすくするために本質的に遅い非再帰関数を説明するのに良いかもしれません。:D
endolith

61

他の答えはそれがかなりうまくいくものをカバーしています。繰り返しません。あなたに役立つかもしれないほんのいくつかのポイント。

通常、メモ化は、何か(高価)を計算して値を返す関数に適用できる操作です。このため、多くの場合、デコレータとして実装されます。実装は簡単で、このようなものになります

memoised_function = memoise(actual_function)

またはデコレータとして表現

@memoise
def actual_function(arg1, arg2):
   #body

18

メモ化とは、継続的に再計算するのではなく、高価な計算の結果を保持し、キャッシュされた結果を返すことです。

次に例を示します。

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

より完全な説明は、メモ化に関するウィキペディアのエントリにあります。


うーん、今それが正しいPythonだったとしたら、それは揺るぎませんが、そうではないようです...大丈夫なので、「キャッシュ」は口述ではありませんか?それがあれば、それはする必要があるため if input not in self.cacheself.cache[input]has_key以降...早期2.xシリーズでは廃止され、そうでない場合は2.0。 self.cache(index)正しいことはなかったIIRC。)
ユルゲン・A.エアハルト

15

hasattr手作りしたい人のために、組み込み関数を忘れないようにしましょう。そうすれば、(グローバルではなく)関数定義内にmemキャッシュを保持できます。

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]

これは非常に高価なアイデアのようです。nごとに、nの結果だけでなく、2 ... n-1の結果もキャッシュします。
codeforester

15

これは非常に便利だと思いました

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)

使用する理由については、docs.python.org / 3 / library / functools.html#functools.wrapsをご覧くださいfunctools.wraps
anishpatel 2017

1
memoメモリを解放するために手動でクリアする必要がありますか?
番号

全体のアイデアは、結果はセッション内のメモ内に保存されるということです。
つまり、現状の

6

メモ化とは、基本的に、後の段階で同じ計算が必要な場合に再帰ツリーをたどる必要性を減らすために、再帰アルゴリズムで行われた過去の操作の結果を保存することです。

http://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/を参照してください

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]

2
より多くのパフォーマンスを得るには、最初のいくつかの既知の値を使用してfibcacheを事前シードしてから、それらを処理するための追加のロジックをコードの「ホットパス」から取り出すことができます。
jkflying 2014年

5

メモ化とは、関数をデータ構造に変換することです。通常、変換は段階的かつ遅延的に(特定のドメイン要素または「キー」の要求に応じて)行われることを望みます。遅延関数型言語では、この遅延変換は自動的に行われる可能性があるため、メモ化は(明示的な)副作用なしに実装できます。


5

さて、私は最初の部分に最初に答える必要があります:メモ化とは何ですか?

それは、時間とメモリを交換する方法にすぎません。考えて乗算表

Pythonでデフォルト値として可変オブジェクトを使用することは、通常は悪いと考えられています。しかし、賢く使用する場合、実際にを実装すると便利ですmemoization

これは、http://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objectsから変更された例です

dict関数定義で可変を使用すると、中間の計算結果をキャッシュできます(たとえば、calculateのfactorial(10)後に計算する場合factorial(9)、すべての中間結果を再利用できます)。

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

4

以下は、うなりを出さずにリスト型またはdict型の引数を処理するソリューションです。

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

このアプローチは、handle_itemの特殊なケースとして独自のハッシュ関数を実装することで、任意のオブジェクトに自然に拡張できることに注意してください。たとえば、セットを入力引数として取る関数でこのアプローチを機能させるには、handle_itemに次のように追加します。

if is_instance(x, set):
    return make_tuple(sorted(list(x)))

1
いいね。ウィーニングなしでは、のlist引数は、の値を持つ[1, 2, 3]別のset引数と誤って同じと見なされる可能性があります{1, 2, 3}。また、セットは辞書のように順序付けされていないため、も必要sorted()です。また、再帰的なデータ構造引数は無限ループを引き起こすことにも注意してください。
martineau 2014年

ええ、セットは特別なケーシングのhandle_item(x)とソートによって処理されるべきです。この実装はセットを処理するので、そうではないので言ったはずはありませんが、重要なのは、特別な大文字と小文字を区別するhandle_itemを使用して簡単に拡張できることです。ハッシュ関数を自分で作成してもかまいません。トリッキーな部分-多次元のリストまたは辞書の扱い-は既にここで扱われているので、このメモ化関数は、単純な「ハッシュ可能な引数のみをとる」タイプよりもベースとして作業する方がはるかに簡単であることがわかりました。
RussellStewart 2014年

私が述べた問題は、listsとsetsが同じものに「タプル化」され、したがって互いに区別できなくなるという事実によるものです。sets最新のアップデートで説明されているサポートを追加するためのサンプルコードは、私が恐れていることを避けていません。これは、個別に「メモ化」されたテスト関数に引数として渡して[1,2,3]{1,2,3}2回呼び出されるかどうかを確認することで簡単に確認できます。
martineau 14年

ええ、私はその問題を読みましたが、あなたが言及した他の問題よりもはるかに小さいと思うので、私はそれを取り上げませんでした。固定引数がリストまたはセットのいずれかである可能性があるメモ化された関数を最後に作成したのはいつですか?このようなまれなケースが発生した場合は、handle_itemを再度追加して先頭に追加します。要素がセットの場合は0、リストの場合は1とします。
RussellStewart 2014年

実際には、listsとdictsにも同様の問題があります。これは、aが辞書を呼び出した結果としてまったく同じものを持つ可能性があるlistためmake_tuple(sorted(x.items()))です。どちらの場合も簡単な解決策は、type()生成されたタプルにof値を含めることです。具体的にはsets を処理するさらに簡単な方法を考えることができますが、それは一般化されていません。
martineau 2014年

3

キーワード引数が渡された順序とは関係なく、位置引数とキーワード引数の両方で機能するソリューション(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

同様の質問:Pythonでのメモ化のための同等の可変引数関数呼び出しの識別


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]

4
if n not in cache代わりに単純に使用できます。を使用cache.keysすると、Python 2で不要なリストが作成されます
n611x007

2

すでに提供されている回答に追加したいだけで、Pythonデコレータライブラリには、とは異なり、「ハッシュ化できない型」をメモすることもできるシンプルでありながら便利な実装がいくつかありfunctools.lru_cacheます。


1
このデコレータは、「ハッシュできないタイプ」を記憶していません。それは単にメモ化なしで関数を呼び出すことにフォールバックします。明示的なものに反対することは暗黙的なドグマよりも優れています。
ostrokach
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.