- PVSM.RU - https://www.pvsm.ru -
Вот так можно мемоизировать питоновскую функцию:
def memo_square(a, cache={}):
if a not in cache:
cache[a] = a*a
return cache[a]
Приём незаслуженно малоизвестный, так что под катом мы разберём, как он работает и для чего нужен.
Сперва о том, как и почему это работает. memo_square
(как и любая другая функция) — это объект класса function, у которого в числе прочих аттрибутов есть заполняемый при создании объекта кортеж memo_square.__defaults__
. Сначала он содержит пустой словарь, как и указано в заголовке функции:
>>> memo_square.__defaults__
({},)
__defaults__
— обычный кортеж и менять его элементы нельзя. Можно, правда, подменить весь набор дефолтных значений разом, но только на другой кортеж:
>>> def test(a=1, b=2):
... print(a, b)
...
>>> test.__defaults__
(1, 2)
>>> test()
1 2
>>> test.__defaults__ = ('Привет, ', 'Хабр')
>>> test()
Привет, Хабр
>>> test.__defaults__[1] = 'Пикабу'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> test.__defaults__ = {0: 'Привет, ', 1: 'Пикабу'}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __defaults__ must be set to a tuple object
Сорян, на Пикабу эта статья не попадёт. Ну да ладно, важно не это. Важно то, что за исключением совсем уж хитрого кода func.__defaults__
создаётся один раз за время работы программы вместе со всеми своими элементами. Кортеж и его элементы не будут пересоздаваться с каждым вызовом функции, они будут использоваться до тех пор, пока функция существует. Но вот меняться, если сами элементы мутабельны, им никто не запрещает. Неумение работать с такими элементами — один из самых распространённых способов выстрелить себе в ногу в питоне [1]. Но вообще-то сохранять значения между вызовами функции бывает довольно полезно. После нескольких вызовов memo_square.__defaults__
будет выглядеть вот так:
>>> memo_square(2)
4
>>> memo_square.__defaults__
({2: 4},)
>>> memo_square(5)
25
>>> memo_square.__defaults__
({2: 4, 5: 25},)
>>> memo_square(2)
4
>>> memo_square.__defaults__
({2: 4, 5: 25},)
Если функция уже вызывалась для того же значения, то вычисление значения и, соответственно, пополнение кэша не происходит. Для квадрата выгода небольшая (строго говоря, для квадрата выгода отрицательная, потому что поиск в словаре дороже перемножения двух чисел), но для реальных дорогостоящих функций мемоизация/кэширование может быть полезно. Конечно, обеспечить её в питоне можно более чем одним способом. Вот какие у нас есть альтернативы:
def square(a):
return a**a
cache = {}
for x in values:
if x not in cache:
cache[x] = x**x
print cache[x]
Смысл тот же, но гораздо более громоздко. К тому же переменная cache видна вне функции, хотя ни для чего, кроме её мемоизации, не используется. Кэш при мемоизации дефолтным аргументом доступен снаружи только через func.__defaults__
, к которым довольно сложно обратиться по ошибке.
Главное, в чём проигрывает такой способ мемоизации — он не очень идиоматичен. Лично я, наткнувшись на это решение в первый раз, пару минут размышлял о том, что тут вообще происходит и зачем. С другой стороны, за эти пару минут я стал чуть лучше понимать, как устроены питоновские функции и их аргументы. Так что даже если вы не будете пользоваться дефолтными аргументами (для мемоизации или, например, ускорения разрешения имён [3]), знание этого приёма всё равно полезно для любого питонщика.
Автор: Алексей Морозов
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/295676
Ссылки в тексте:
[1] один из самых распространённых способов выстрелить себе в ногу в питоне: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values
[2] @functools.lru_cache: https://docs.python.org/3/library/functools.html#functools.lru_cache
[3] ускорения разрешения имён: http://effbot.org/zone/default-values.htm
[4] Источник: https://habr.com/post/426341/?utm_source=habrahabr&utm_medium=rss&utm_campaign=426341
Нажмите здесь для печати.