https://habr.com/post/426341/Вот так можно мемоизировать питоновскую функцию:
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__
создаётся один раз за время работы программы вместе со всеми своими элементами. Кортеж и его элементы не будут пересоздаваться с каждым вызовом функции, они будут использоваться до тех пор, пока функция существует. Но вот меняться, если сами элементы мутабельны, им никто не запрещает. Неумение работать с такими элементами —
один из самых распространённых способов выстрелить себе в ногу в питоне. Но вообще-то сохранять значения между вызовами функции бывает довольно полезно. После нескольких вызовов
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},)
Если функция уже вызывалась для того же значения, то вычисление значения и, соответственно, пополнение кэша не происходит. Для квадрата выгода небольшая (строго говоря, для квадрата выгода отрицательная, потому что поиск в словаре дороже перемножения двух чисел), но для реальных дорогостоящих функций мемоизация/кэширование может быть полезно. Конечно, обеспечить её в питоне можно более чем одним способом. Вот какие у нас есть альтернативы:
Главное, в чём проигрывает такой способ мемоизации — он не очень идиоматичен. Лично я, наткнувшись на это решение в первый раз, пару минут размышлял о том, что тут вообще происходит и зачем. С другой стороны, за эти пару минут я стал чуть лучше понимать, как устроены питоновские функции и их аргументы. Так что даже если вы не будете пользоваться дефолтными аргументами (для мемоизации или, например,
ускорения разрешения имён), знание этого приёма всё равно полезно для любого питонщика.