Значение по умолчанию не то же самое, что переданное значение?

Ради интереса решил попробовать написать какое-то подобие аналога @cache из functools. Декоратор проверяет, выполнялась ли уже такая функция с такими аргументами. Если да - возвращает уже когда-то посчитанный результат.

def decor(func):
    cashe = dict()
    def wrapper(*args, **kwargs):
        key = f"{func}"
        for item in args:
            key += str(hash(str(item)))
        for key_item in kwargs:
            key += str(hash(str(key_item)))

        if key not in cashe:
            print('Создал!')
            result = func(*args, **kwargs)
            cashe[key] = result
        else:
            print('Взял из кеша!')
            result = cashe[key]
        return result
    return wrapper

В целом, думаю, можно посидеть и сделать лаконичнее/быстрее/короче, но суть сейчас не в этом.

Когда я использую

@decor
def check(a, b, c=0):
    return a + b + c


check(1, 5) # Создал 
check(1, 5) # Взял из кеша

Всё отрабатывает так, как я ожидаю, но когда

check(1, 5, 0) # Создал
check(1, 5) # Создал

Почему так происходит? c ведь неизменяема и в обоих случаях равна 0, но почему функции считаются разными?


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

Автор решения: insolor

Потому что ваш декоратор смотрит только на фактические параметры. В него приходят конкретно два набора параметров: (1, 5, 0) и (1, 5) - это два разных наборов параметров, соответственно и ключи в кэше разные. Параметры по умолчанию в набор фактических параметров добавляются только при вызове самой функции, а не при вызове "обертки", добавленной декоратором.

Нужно использовать интроспекцию, и вытаскивать информацию о параметрах функции по умолчанию, их добавлять в фактические аргументы.

Пример с передачей именованного параметра:

import inspect


def get_default_args(func):
    # Код функции взят из ответа: https://stackoverflow.com/a/12627202
    signature = inspect.signature(func)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def decor(func):
    cache = dict()
    def wrapper(*args, **kwargs):
        key = ""
        kwargs_with_defaults = get_default_args(func)
        kwargs_with_defaults.update(kwargs)

        for item in args:
            key += str(hash(str(item)))

        # У именованных параметром берем и имена, и значения, отсортированные по возрастанию имени
        for key_item in sorted(kwargs_with_defaults.items(), key=lambda x: x[0]):  
            key += str(hash(str(key_item)))

        if key not in cache:
            print('Создал!')
            result = func(*args, **kwargs)
            cache[key] = result
        else:
            print('Взял из кеша!')
            result = cache[key]

        return result
    return wrapper


@decor
def check(a, b, c=0):
    return a + b + c


check(1, 5, c=0)
check(1, 5)

Вывод:

Создал!
Взял из кеша!

С позиционными параметрами тоже можно реализовать, возможно чуть позже допишу (но это не точно).

→ Ссылка