【问题标题】:Caching in python using *args and lambda functions使用 *args 和 lambda 函数在 python 中缓存
【发布时间】:2023-03-13 02:57:01
【问题描述】:

我最近尝试使用 Google 搜索 foo.bar challenge。在我的时间到了之后,我决定尝试找到一个我无法解决的问题的解决方案,并找到了一个解决方案here (如果你有兴趣,包括问题陈述)。我之前一直在为我想要缓存的每个函数制作一个字典,但看起来在这个解决方案中任何函数/输入都可以使用相同的语法进行缓存。

首先,我对代码的工作方式感到困惑,*args 变量没有作为参数输入(并且什么也没有打印)。这是一个经过修改的最小示例来说明我的困惑:

mem = {}

def memoize(key, func, *args):
    """
    Helper to memoize the output of a function
    """

    print(args)

    if key not in mem:
        # store the output of the function in memory
        mem[key] = func(*args)

    return mem[key]

def example(n):
    return memoize(
        n,
        lambda: longrun(n),
    )

def example2(n):
     return memoize(
        n,
        longrun(n),
     )

def longrun(n):
    for i in range(10000):
        for j in range(100000):
            2**10
    return n

在这里,我使用相同的 memoize 功能,但带有打印功能。 example 函数返回 memoize(n, a lambda function,)longrun 函数只是一个具有大量无用计算的恒等函数,因此很容易查看缓存是否正常工作(example(2) 第一次大约需要 5 秒并且几乎是在之后)。

这是我的困惑:

  • 为什么memoize的第三个参数是空的?当在 memoize 中打印 args 时,它会打印 ()。然而不知何故 mem[key] 将 func(*args) 存储为 func(key)?
  • 为什么这种行为只在使用 lambda 函数时才有效(example 会缓存,而 example2 不会)?我认为 lambda: longrun(n) 只是将返回 longrun(n) 的函数作为输入提供的一种简短方式。

作为奖励,有人知道如何使用装饰器来记忆函数吗?

我也想不出一个更具描述性的标题,欢迎编辑。谢谢。

【问题讨论】:

标签: python caching lambda


【解决方案1】:

*args 表示可变数量的位置参数。例如print可以用作print(1)print(1, 2)print(1, 2, 3)等。同样,**kwargs 代表可变数量的关键字参数。

请注意,名称 argskwargs 只是一个约定 - 是 *** 符号使它们成为可变参数。

无论如何,memoize 使用它基本上接受 any 输入到 func。如果 func 的结果没有被缓存,则使用参数调用它。在函数调用中,*args 基本上是函数定义中*args 的反面。例如,以下是等价的:

# provide *args explicitly
print(1, 2, 3)
# unpack iterable to *args
arguments = 1, 2, 3
print(*arguments)

如果args 为空,则调用print(*args) 与调用print() 相同——不传递任何参数。


函数和 lambda 函数在 python 中是相同的。这只是创建函数对象的不同表示法。

问题是在example2 中,您没有传递函数。你调用一个函数,然后传递它的结果。相反,您必须分别传递函数及其参数。

def example2(n):
    return memoize(
        n,
        longrun,  # no () means no call, just the function object
        # all following parameters are put into *args
        n
    )

现在,一些实现细节:为什么args 是空的,为什么有一个单独的键?

  • 空的 args 来自您对 lambda 的定义。为了清楚起见,让我们把它写成一个函数:

    def example3(n):
        def nonlambda():
            return longrun(n)
        return memoize(n, nonlambda)
    

    注意nonlambda 如何无参数。参数n 从包含范围绑定为闭包bound from the containing scope。因此,您不必将它传递给 memoize - 它已经绑定在 nonlambda 内。因此,args 在 memoize 中是空的,即使 longrun 确实接收了一个参数,因为两者不直接交互。

  • 现在,为什么是mem[key] = f(*args),而不是mem[key] = f(key)?这实际上是一个稍微错误的问题。正确的问题是“为什么不是mem[f, args] = f(*args)?”。

    记忆之所以有效,是因为相同函数的相同输入会导致相同的输出。也就是说,f, args 标识您的输出。理想情况下,您的 key 将是 f, args,因为这是唯一相关的信息。

    问题是您需要一种在mem 中查找fargs 的方法。如果您曾经尝试将list 放在dict 中,您就会知道有些类型在映射(或任何其他合适的查找结构)中不起作用。所以如果你定义key = f, args,你就不能记忆采用可变/不可散列类型的函数。 Python的functools.lru_cache其实有这个限制。

    定义一个显式的key 是解决这个问题的一种方法。它的优点是调用者可以选择适当的键,例如无需任何修改即可使用n。这提供了最佳优化潜力。但是,它很容易中断 - 仅使用 n 会错过调用的实际函数。记忆具有相同输入的第二个函数会破坏您的缓存。

    有多种替代方法,每种方法各有利弊。常见的是类型的显式转换:listtuplesetfrozenset,等等。这很慢,但最精确。另一种方法是只调用strrepr,就像key = repr((f, args, sorted(kwargs.items()))) 一样,但它依赖于每个值都具有正确的repr

【讨论】:

  • 感谢您的回答。我理解 *args 的含义,我的困惑来自这样一个事实,即当调用示例时,它的 *args 为空。字典应该存储 mem[key] = f(key) 但它看起来像它存储 mem[key] = f() (它甚至不应该与 longrun 一起使用)。我本来希望看到 def example(n): return memoize(n, lambda: longrun(n), n) (就像你演示的 example2 一样,它也可以在没有最后一个参数的情况下工作。这是我的问题,如何如果没有最后一个参数,这仍然有效吗?)
  • 感谢您指出example2中的问题,我错过了!
  • @HBeel 我添加了对f()key 的解释。简而言之,example 不会传递longrun(需要参数)而是传递lambda 函数(不需要参数)。
  • 感谢@MisterMiyagi 提供的其他信息。如果我理解正确example 将一个函数及其参数组合成一个函数(不接受任何参数);像 f_n() = f(n) 这样的东西?你的第二点也很有意义,谢谢。对于这个特定的问题,为什么这段代码的作者甚至都使用*args,如果它总是传递为空?也许是他们经常使用的一般代码,有时使用额外的参数?
猜你喜欢
  • 2015-02-21
  • 1970-01-01
  • 2018-04-23
  • 1970-01-01
  • 2012-04-30
  • 1970-01-01
  • 2022-11-14
  • 1970-01-01
  • 2015-05-06
相关资源
最近更新 更多