识别等效的可变参数函数调用以实现备忘录

2 投票
5 回答
725 浏览
提问于 2025-04-17 14:17

我正在使用一种变体的这个装饰器来进行记忆化处理:

# note that this decorator ignores **kwargs
def memoize(obj):
    cache = obj.cache = {}

    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        if args not in cache:
            cache[args] = obj(*args, **kwargs)
        return cache[args]
    return memoizer

我在想,有没有合理的方法可以同时根据argskwargs进行记忆化,特别是在一些情况下,两个函数调用虽然参数的位置和关键字赋值不同,但实际上传入的参数是完全一样的?

5 个回答

1

一般来说,我们不能简单地认为两个函数调用的参数意思是一样的。比如看看下面的调用:

func(foo=1)
func(1)
func(bar=1)

这两个调用是否相同,取决于位置参数是叫 foo 还是 bar:如果这个参数叫 foo,那么第一个调用就和第二个调用是匹配的,反之亦然。不过,位置参数也可能有完全不同的名字。

换句话说,你需要仔细考虑要调用的函数,而这有时候可能并不容易(例如,如果这个函数是用 C 语言写的,或者它本身只是一个处理 *args 和 **kwargs 的包装器)。

如果你想深入了解这个反射的过程,可以参考 ndpu 的回答,作为一个好的起点。

1

在编程中,我们常常会遇到一些问题,尤其是在使用特定的工具或库时。比如,有时候我们需要处理一些数据,但这些数据的格式可能和我们想要的不一样。这就需要我们对数据进行一些转换或处理,以便能够顺利使用。

另外,编程语言通常会有一些特定的语法和规则,这些规则帮助我们更好地组织代码,让计算机能够理解我们的指令。对于初学者来说,掌握这些语法是非常重要的,因为它们是编写有效代码的基础。

在解决问题时,很多时候我们需要查阅一些资料,比如文档、教程或者社区的讨论。这些资源可以帮助我们更快地找到解决方案,避免走弯路。

总之,编程是一个不断学习和实践的过程,遇到问题时不要气馁,积极寻找答案,慢慢就能掌握更多的知识和技能。

import inspect
def memoize(obj):
    cache = obj.cache = {}
    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        kwargs.update(dict(zip(inspect.getargspec(obj).args, args)))
        key = tuple(kwargs.get(k, None) for k in inspect.getargspec(obj).args)
        if key not in cache:
            cache[key] = obj(**kwargs)
        return cache[key]
    return memoizer
2

如果你总是以位置参数或关键字参数的方式使用参数,那么Thorsten的解决方案就很好用。但如果你想让不同的调用方式,只要给参数赋相同的值,就被认为是相等的,那你就需要做一些更复杂的事情:

import inspect


def make_key_maker(func):
    args_spec = inspect.getargspec(func)

    def key_maker(*args, **kwargs):
        left_args = args_spec.args[len(args):]
        num_defaults = len(args_spec.defaults or ())
        defaults_names = args_spec.args[-num_defaults:]

        if not set(left_args).symmetric_difference(kwargs).issubset(defaults_names):
            # We got an error in the function call. Let's simply trigger it
            func(*args, **kwargs)

        start = 0
        key = []
        for arg, arg_name in zip(args, args_spec.args):
            key.append(arg)
            if arg_name in defaults_names:
                start += 1

        for left_arg in left_args:
            try:
                key.append(kwargs[left_arg])
            except KeyError:
                key.append(args_spec.defaults[start])

            # Increase index if we used a default, or if the argument was provided
            if left_arg in defaults_names:
                start += 1
        return tuple(key)

    return key_maker

上面的函数试图将关键字参数(和默认值)映射到位置参数,并使用得到的元组作为键。我测试了一下,发现它在大多数情况下都能正常工作。不过,当目标函数也使用**kwargs参数时,它就会出问题。

>>> def my_function(a,b,c,d,e=True,f="something"): pass
... 
>>> key_maker = make_key_maker(my_function)
>>> 
>>> key_maker(1,2,3,4)
(1, 2, 3, 4, True, 'something')
>>> key_maker(1,2,3,4, e=True)               # same as before
(1, 2, 3, 4, True, 'something')
>>> key_maker(1,2,3,4, True)                 # same as before
(1, 2, 3, 4, True, 'something')
>>> key_maker(1,2,3,4, True, f="something")  # same as before
(1, 2, 3, 4, True, 'something')
>>> key_maker(1,2,3,4, True, "something")    # same as before
(1, 2, 3, 4, True, 'something')
>>> key_maker(1,2,3,d=4)                     # same as before
(1, 2, 3, 4, True, 'something')
>>> key_maker(1,2,3,d=4, f="something")      # same as before
(1, 2, 3, 4, True, 'something')

撰写回答