Що таке запам'ятовування і як я можу використовувати його в Python?


378

Я щойно запустив Python і не маю поняття, що таке запам'ятовування і як ним користуватися. Також, чи можу я мати спрощений приклад?


215
Коли друге речення відповідної статті вікіпедії містить фразу "взаємно-рекурсивний синтаксичний розбір [1] в загальному алгоритмі розбору зверху вниз [2] [3], який вміщує неоднозначність і залишає рекурсію в поліноміальному часі та просторі", я думаю цілком доречно запитати ТА, що відбувається.
Clueless

10
@Clueless: цій фразі передує "Пам'ять також використовується в інших контекстах (і для інших цілей, ніж збільшення швидкості), наприклад в". Тож це лише список прикладів (і їх не потрібно розуміти); це не є частиною пояснення запам'ятовування.
ShreevatsaR

1
@StefanGruenwald Це посилання мертве. Ви можете знайти оновлення?
JS.

2
Нове посилання на pdf-файл, оскільки pycogsci.info не працює: people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf
Стефан

4
@ Clueless, У статті фактично йдеться про " простий взаємно-рекурсивний синтаксичний аналіз [1] у загальному алгоритмі розбору зверху вниз [2] [3], який містить у собі неоднозначність та залишені рекурсії в поліноміальному часі та просторі". Ви пропустили просте , що, очевидно, робить цей приклад набагато зрозумілішим :).
studgeek

Відповіді:


353

Пам'ять ефективно стосується запам'ятовування ("запам'ятовування" → "меморандуму" → запам'ятовується) результатів викликів методів на основі методів введення, а потім повернення запам'ятовуваного результату, а не обчислення результату заново. Ви можете думати про це як кеш результатів методу. Детальнішу інформацію див. На сторінці 387 для визначення у Вступі до алгоритмів (3e), Cormen et al.

Простий приклад для обчислення факторіалів з використанням мемоанізації в 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 - це елегантне рішення, яке можна легко застосувати до існуючого коду, не вимагаючи особливого рефакторингу.
Капітан Лептон

10
Рішення класу Memoize є помилковим, воно не працюватиме так само, як the factorial_memo, тому що factorialвсередині def factorialвсе ще викликає старе знімати factorial.
adamsmith

9
До речі, ви також можете писати if k not in factorial_memo:, що читає краще, ніж if not k in factorial_memo:.
ShreevatsaR

5
Справді слід це робити як декоратор.
Emlyn O'Regan

3
@ durden2.0 Я знаю, що це старий коментар, але argsце кортеж. def some_function(*args)робить арги кортежем.
Адам Сміт

232

Новим для Python 3.2 є functools.lru_cache. За замовчуванням, він кешируєт тільки 128 недавно використані дзвінків, але ви можете встановити , maxsizeщоб Noneвказати , що кеш ніколи не повинен закінчуватися:

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)і вам доведеться почекати близько десяти секунд.

Додавання lru_cacheанотації гарантує, що якщо функція була викликана нещодавно для певного значення, вона не буде перераховувати це значення, а використовувати кешований попередній результат. У цьому випадку це призводить до величезного підвищення швидкості, в той час як код не захаращений деталями кешування.


2
Спробував фіб (1000), отримав RecursionError: максимальна глибина рекурсії перевищена порівняно
X Æ A-12,

5
@Andyk За замовчуванням ліміт рекурсії Py3 - 1000. Перший раз, коли fibвикликається, для повторного запам'ятовування потрібно буде повернутися до базового випадку. Отже, ваша поведінка майже очікувана.
Quelklef

1
Якщо я не помиляюся, він кешується лише до тих пір, поки процес не буде вбито, правда? Або це кеш-пам'ять незалежно від того, загинув процес? Скажімо, я перезавантажую систему - чи будуть кешовані результати все-таки кешовані?
Крістада673

1
@ Kristada673 Так, він зберігається в пам'яті процесу, а не на диску.
Flimm

2
Зауважте, що це прискорює навіть перший запуск функції, оскільки це рекурсивна функція і кешує свої власні проміжні результати. Можливо, добре проілюструвати нерекурсивну функцію, яка по суті є повільною, щоб зробити її зрозумілішими манекенами, як я. : D
ендоліти

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.cache і self.cache[input] ( has_keyзастаріло з ... рано в серії 2.x, якщо не 2.0. self.cache(index)Ніколи не було правильним. IIRC)
Юрген А. Ерхард

15

Не будемо забувати вбудовану hasattrфункцію для тих, хто хоче займатися рукою. Таким чином ви можете зберігати кеш пам'яті всередині визначення функції (на відміну від глобального).

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

1
Чи потрібно вручну очистити, memoщоб звільнити пам'ять?
нос

Вся ідея полягає в тому, що результати зберігаються всередині нагадування протягом сеансу. Тобто нічого не очищається так, як це
mr.bjerre

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
Для більшої продуктивності заздалегідь заздалегідь встановіть свій фібкеш з першими кількома відомими значеннями, тоді ви можете скористатися додатковою логікою для обробки з «гарячого шляху» коду.
jkflying

5

Пам'ять - це перетворення функцій в структури даних. Зазвичай хочеться, щоб перетворення відбувалося поступово і ліниво (на вимогу заданого елемента домену - або "ключа"). У лінивих функціональних мовах ця ледача конверсія може відбуватися автоматично, і таким чином запам'ятовування може бути здійснено без (явних) побічних ефектів.


5

Ну, я повинен відповісти на першу частину: що таке запам'ятовування?

Це просто метод торгувати пам'яттю на час. Придумайте таблицю множення .

Використання об'єкта, що змінюється, як значення за замовчуванням у Python, як правило, вважається поганим. Але якщо використовувати його з розумом, це може бути корисно реалізувати memoization.

Ось приклад, адаптований з http://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objects

Використовуючи змінний dictу визначенні функції, проміжні обчислені результати можна кешувати (наприклад, при обчисленні 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

Ось рішення, яке працюватиме з аргументами типу list або 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(). Також зауважте, що аргумент рекурсивної структури даних може викликати нескінченний цикл.
мартіно

Так, набори повинні оброблятися спеціальним обробкою корпусу_item (x) та сортуванням. Я не мав би сказати, що ця реалізація обробляє набори, тому що це не так - але справа в тому, що це можна легко розширити, щоб зробити це спеціальним корпусом handle_item, і те ж саме буде працювати для будь-якого класу чи ітерабельного об'єкта до тих пір, поки ви готові самостійно написати хеш-функцію. Тут вже розглядається складна частина - багатомірні списки чи словники - тому я виявив, що з цією функцією запам'ятовування працювати набагато простіше, ніж з простими типами "я беру лише місивні аргументи".
RussellStewart

Проблема, про яку я згадав, пов'язана з тим, що lists і sets "переплетені" в одне і те ж і стають невідрізними один від одного. Приклад коду для додавання підтримки для setsописаного в вашому останньому оновлення не боїться цього. Це легко зрозуміти, пройшовши окремо [1,2,3]і {1,2,3}як аргумент тестової функції "запам'ятовування" d і побачивши, чи викликається вона двічі, як належить, чи ні.
мартіно

Так, я читав цю проблему, але не вирішував її, тому що вважаю, що вона набагато менша, ніж та, яку ви згадали. Коли ви востаннє писали запам’ятовувану функцію, де фіксований аргумент міг бути або списком, або набором, а два привели до різних результатів? Якщо ви зіткнулися з таким рідкісним випадком, ви знову просто перепишете handle_item, щоб додати препред, сказати 0, якщо елемент - це набір, або 1, якщо це список.
RussellStewart

Насправді, подібні проблеми є і з lists та dicts, оскільки можливо, що в a listє точно те саме, що було результатом виклику make_tuple(sorted(x.items()))словника. Простим рішенням для обох випадків було б включення type()значення у створений кортеж. Я можу придумати ще простіший спосіб конкретно поводитись із sets, але це не узагальнює.
мартіно

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

Аналогічне запитання: Ідентифікація еквівалентних функцій varargs вимагає запам'ятовування в 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
створило

2

Просто хотіли додати до вже наданих відповідей, бібліотека декораторів Python має кілька простих, але корисних реалізацій, які також можуть запам'ятати "незмінні типи", на відміну від functools.lru_cache.


1
Цей декоратор не запам’ятовує «непосильні типи» ! Це просто повертається до виклику функції без запам'ятовування, протидіяти явним краще, ніж неявна догма.
ostrokach
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.