Изменяемый объект в локальной области видимости как кэш для декоратора

Есть правило - не использовать изменяемые объекты в сигнатуре функции, так как они создаются единожды при создании объекта - функция. Иногда это можно использовать осознанно, если необходим один и тот же объект для каждого вызова функции (не берём в расчёт, что может быть выполнен вызов с указанием объекта и кэш не будет использоваться). Например, для "кэширования" результатов работы другой функции.

Пример с декоратором, который выполняет работу с кэшем для "долгоиграющих функция".

import time

args = (1,2,1,2) # создаю набор аргуметов для функции "f"

def caching(fn,cache = dict()):
    """декоратор для создания кэша результатов функции
    cash = dict() специально вынесена в сигнатуру, так как объект
    на который указывает имя cash будет создан единожды при создании объекта - функция
    """
    def wrapper(i):
        result = cache.get(i) # ищу в кэше
        if not result:
            result = cache.setdefault(i,fn(i)) # в кэше нет, вычисляю и помещаю в кэш
        else:
            print('(результат возвращён из кэша) ',end='')
        return result
    return wrapper


@caching
def f(arg):
    time.sleep(3) # имитирую долгое вычисление
    return arg

if __name__ =='__main__':
    for i in args:
        print(f'result: {f(i)}')

Такого же эффекта можно добиться, переместив создание пустого словаря и имени для него уже в тело декоратора

import time

args = (1,2,1,2) # создаю набор аргуметов для функции "f"

def caching(fn,):
    """декоратор для создания кэша результатов функции
    """
    cache = dict()
    print(id(cache))
    def wrapper(i):
        result = cache.get(i) # ищу в кэше
        if not result:
            result = cache.setdefault(i,fn(i)) # в кэше нет, вычисляю и помещаю в кэш
        else:
            print('(результат возвращён из кэша) ',end='')
        return result
    return wrapper


@caching
def f(arg):
    time.sleep(3) # имитирую долгое вычисление
    return arg

if __name__ =='__main__':
    for i in args:
        print(f'result: {f(i)}')

Тут уже с натяжкой, но понятно, что при создании функции содаётся локальное пространство имён и добавляя туда изменяемый объект и он фигурирует в этом пространстве имён. Но, непонятно другое, почему вызов print(id(cash)) так же выполняется только один раз при создании объекта функция?


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

print(id(cache)) выполняется столько раз, сколько вызывается функция-декоратор caching. Она вызывается один раз в строке @caching.

Если вы будете кэшировать несколько разных функций, она вызовется несколько раз.

Важно понимать что происходит при декорировании. Код

@caching
def f(arg):
    time.sleep(3)
    return arg

переводится компилятором в

def f(arg):
    time.sleep(3)
    return arg
f = caching(f)

Последнее присваивание приводит к тому, что теперь под именем f прячется wrapper из декоратора и именно его теперь будут вызывать под этим именем. Проверить не сложно:

print(f)

напечатает что-то такое:

<function caching.<locals>.wrapper at 0x7f3fb7415760>
→ Ссылка