Я щойно запустив Python і не маю поняття, що таке запам'ятовування і як ним користуватися. Також, чи можу я мати спрощений приклад?
Я щойно запустив Python і не маю поняття, що таке запам'ятовування і як ним користуватися. Також, чи можу я мати спрощений приклад?
Відповіді:
Пам'ять ефективно стосується запам'ятовування ("запам'ятовування" → "меморандуму" → запам'ятовується) результатів викликів методів на основі методів введення, а потім повернення запам'ятовуваного результату, а не обчислення результату заново. Ви можете думати про це як кеш результатів методу. Детальнішу інформацію див. На сторінці 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
клас показаного тут.
factorial_memo
, тому що factorial
всередині def factorial
все ще викликає старе знімати factorial
.
if k not in factorial_memo:
, що читає краще, ніж if not k in factorial_memo:
.
args
це кортеж. def some_function(*args)
робить арги кортежем.
Новим для 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
анотації гарантує, що якщо функція була викликана нещодавно для певного значення, вона не буде перераховувати це значення, а використовувати кешований попередній результат. У цьому випадку це призводить до величезного підвищення швидкості, в той час як код не захаращений деталями кешування.
fib
викликається, для повторного запам'ятовування потрібно буде повернутися до базового випадку. Отже, ваша поведінка майже очікувана.
Інші відповіді стосуються того, що це досить добре. Я цього не повторюю. Лише деякі моменти, які можуть вам стати в нагоді.
Зазвичай запам'ятовування - це операція, яку можна застосувати до будь-якої функції, яка щось обчислює (дорого) і повертає значення. Через це його часто реалізують як декоратор . Реалізація проста, і це було б щось подібне
memoised_function = memoise(actual_function)
або виражається як декоратор
@memoise
def actual_function(arg1, arg2):
#body
Пам'ять - це збереження результатів дорогих обчислень та повернення кешованого результату, а не постійний перерахунок.
Ось приклад:
def doSomeExpensiveCalculation(self, input):
if input not in self.cache:
<do expensive calculation>
self.cache[input] = result
return self.cache[input]
Більш повний опис можна знайти у статті вікіпедії про запам'ятовування .
if input not in self.cache
і self.cache[input]
( has_key
застаріло з ... рано в серії 2.x, якщо не 2.0. self.cache(index)
Ніколи не було правильним. IIRC)
Не будемо забувати вбудовану 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]
Я вважаю це надзвичайно корисним
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)
functools.wraps
.
memo
щоб звільнити пам'ять?
Пам'ять - це, в основному, збереження результатів минулих операцій, виконаних за допомогою рекурсивних алгоритмів, щоб зменшити необхідність проходження дерева рекурсії, якщо той же розрахунок необхідний на більш пізньому етапі.
див. 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]
Пам'ять - це перетворення функцій в структури даних. Зазвичай хочеться, щоб перетворення відбувалося поступово і ліниво (на вимогу заданого елемента домену - або "ключа"). У лінивих функціональних мовах ця ледача конверсія може відбуватися автоматично, і таким чином запам'ятовування може бути здійснено без (явних) побічних ефектів.
Ну, я повинен відповісти на першу частину: що таке запам'ятовування?
Це просто метод торгувати пам'яттю на час. Придумайте таблицю множення .
Використання об'єкта, що змінюється, як значення за замовчуванням у 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]
Ось рішення, яке працюватиме з аргументами типу 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)))
list
аргумент [1, 2, 3]
помилково можна вважати таким же, як і інший set
аргумент зі значенням {1, 2, 3}
. Крім того, множини є не упорядкованими, як словники, тому вони також повинні бути sorted()
. Також зауважте, що аргумент рекурсивної структури даних може викликати нескінченний цикл.
list
s і set
s "переплетені" в одне і те ж і стають невідрізними один від одного. Приклад коду для додавання підтримки для sets
описаного в вашому останньому оновлення не боїться цього. Це легко зрозуміти, пройшовши окремо [1,2,3]
і {1,2,3}
як аргумент тестової функції "запам'ятовування" d і побачивши, чи викликається вона двічі, як належить, чи ні.
list
s та dict
s, оскільки можливо, що в a list
є точно те саме, що було результатом виклику make_tuple(sorted(x.items()))
словника. Простим рішенням для обох випадків було б включення type()
значення у створений кортеж. Я можу придумати ще простіший спосіб конкретно поводитись із set
s, але це не узагальнює.
Рішення, яке працює з позиційними та ключовими аргументами незалежно від того, в якому порядку передано аргументи ключових слів (використовуючи 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
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]
if n not in cache
замість цього. використання cache.keys
Просто хотіли додати до вже наданих відповідей, бібліотека декораторів Python має кілька простих, але корисних реалізацій, які також можуть запам'ятати "незмінні типи", на відміну від functools.lru_cache
.