【问题标题】:Intermediate results from recursion递归的中间结果
【发布时间】:2021-05-25 00:50:39
【问题描述】:

我有一个问题,我需要生成一些自然递归计算的东西,但如果需要,我还需要能够询问递归中的中间步骤。

我知道我可以通过传递和改变列表或类似结构来做到这一点。但是,这对我来说看起来很丑,我相信一定有一个更整洁的方法,例如使用发电机。理想情况下,我希望能够做的事情是:

intermediate_results = [f(x) for x in range(T)]
final_result = intermediate_results[T-1]

以一种有效的方式。虽然我的解决方案不是性能关键,但我无法证明第一行中大量的冗余工作是合理的。在我看来,生成器将是完美的选择,除了 f 在我的情况下从根本上更适合递归(至少在我看来,这与生成器完全相反,但也许我是只是没有跳出框框思考)。

有没有一种简洁的 Pythonic 方式来做这样的事情,我只是不知道,还是我只需要通过传递一个 intermediate_results 列表来投降和污染我的函数 f 然后我对其进行变异作为副作用?

【问题讨论】:

  • f(x)可以使用memoization吗?如果是这样,可以查询内存表以获得中间结果。
  • 您可以创建一个附加函数,例如logger,它将记录传递给它的任何值的结果。这个logger 可能很难看,但是您的递归函数将只包含对这个logger 的调用,并将值作为参数传递给记录,这样看起来会更干净。
  • 我知道这个问题已经回答了,但无论如何让我想知道。在计算最终结果之后,您真的需要所有中间结果吗?如果不是这样,那么memoization可能不是最好的解决方案,最好玩一下程序的逻辑
  • @heinwol 我的问题是我有许多函数以某种顺序重复应用于初始值。例如,f(x) = g(x) + a、g(x) = h(x) - b 和 h(x) = f(x-1) * (1+c),有一些初始边界像 f(0) = 0 这样的条件。这些函数的细节没有预先定义,但是用例会调用,比如 f(10)。但是,如果 f(10) 被证明是违反直觉的,则有时有必要查看 g(10)、h(10)、f(9)、g(9) 的结果......所以是的,这个只有在计算结果后才需要进行中间检查,而且只是有时。
  • @heinwol 只是为了进一步了解 - 这种事情用电子表格非常简单。我只需为每个函数设置一个列,中间结果就会自然而然地出来。使用电子表格还有其他一些我想避免的缺点,但是能够自然地看到像这样的递归计算的中间结果真的很好。我希望我可以用数据框做类似的事情,但即使使用上面高度简化的示例,这似乎也不是一件容易的事;至少在没有先计算一切的情况下并非如此。

标签: python recursion functional-programming generator


【解决方案1】:

我有一个使用装饰器的通用解决方案。我们创建了一个Memoize 类,它存储函数之前执行的结果(包括递归调用)。如果已经看到给定的参数,则使用缓存的版本快速查找结果。

lru_cache 相比,自定义类的优势在于您可以看到结果。

from functools import wraps

class Memoize:
    def __init__(self):
        self.store = {}

    def save(self, fun):
        @wraps(fun)
        def wrapper(*args):
            if args not in self.store:
                self.store[args] = fun(*args)
            return self.store[args]
        return wrapper

m = Memoize()

@m.save
def fibo(n):
    if n <= 0: return 0
    elif n == 1: return 1
    else: return fibo(n-1) + fibo(n-2)

然后在运行不同的东西后,你可以看到缓存包含的内容。当您运行未来的函数调用时,m.store 将用作查找,因此无需重做计算。

>>> f(8)
21
>>> m.store
{(1,): 1,
 (0,): 0,
 (2,): 1,
 (3,): 2,
 (4,): 3,
 (5,): 5,
 (6,): 8,
 (7,): 13,
 (8,): 21}

你可以修改save函数,使用函数名和args作为key,这样多个函数结果可以存储在同一个Memoize类中。

【讨论】:

  • 这实际上是完美的,但不幸的是,我无法让装饰器按所写的那样工作。任何想法我做错了什么? (并不是 fibo 在递归中使用 f,因为我也测试了自己的函数)。
  • 哎呀,命名的好地方。你只是在上面复制和粘贴,这不起作用吗?或者它仅不适用于您自己的功能。如果是后者,您的功能是否足够简单,可以共享?
  • 我实际上无法让它与你写的东西一起工作,即使有更正。我收到类型错误:“NoneType”对象不可调用。
  • 对不起。我从终端复制了错误的东西。缺少退货声明。立即尝试。
  • 啊哈!谢谢你。尽管我很喜欢其他受访者的轻量级 lru_cache 方法,但我认为这将使我无需手动重新计算我想看到的所有内容,所以我会接受你的回答。
【解决方案2】:

您可以使用对 f 进行多次“冗余”调用的现有解决方案,但使用函数缓存将结果保存到之前对 f 的调用中。 也就是说,当 f(x1) 被调用时,它的输入参数和对应的返回值都被保存下来,下次调用时,只是从缓存中拉取结果

有关此问题的标准库解决方案,请参阅 functools.lru_cache

即:

from functools import lru_cache

@lru_cache
intermediate_results = [f(x) for x in range(T)]
final_result = intermediate_results[T-1]

但请注意,f 必须是 pure function(无副作用,一对一映射)才能正常工作

【讨论】:

  • 这对我来说似乎是最简洁的解决方案,尤其是因为我知道我的函数将是纯粹的(我想避免我想到的其他解决方案的部分原因)。在我标记为已回答之前,我会花一些时间让其他答案流入。
【解决方案3】:

在考虑了您的 cmets 之后,我现在将尝试从另一个角度来看待这个问题。

那么,让我们考虑一个具体的例子:

def f(x):
    a = 2
    return g(x) + a if x != 0 else 0
def g(x):
    b = 1
    return h(x) - b
def h(x):
    c = 1/2
    return f(x-1)*(1+c)

首先,应该提到的是(在我们的特定情况下)该算法具有以下形式:f(x) = p(f(x - 1)) for some p。随之而来的是f(x) = p^x(f(0)) = p^x(0)。这意味着我们只需将p 应用到0 x 次即可获得所需的结果,这可以在迭代过程中完成,因此 可以 无需递归即可编写。尽管我相信您的真实情况要困难得多。而且,在这里停下来会太无聊和没有信息)

一般来说,我们可以将所有可能的解决方案分为两组:需要重构(即重写函数 f、g、h)的和不需要重构的。后一个我几乎没有提供什么(我认为没有人可以)。但是,请考虑以下事项:

def fk(x, k):
    a = 2
    return k(gk(x, k) + a if x != 0 else 0)
def gk(x, k):
    b = 1
    return k(hk(x, k) - b)
def hk(x, k):
    c = 1/2
    return k(fk(x-1, k)*(1+c))

def printret(x):
    print(x)
    return x

f(4, printret) # see what happens

灵感来自continuation-passing style,但完全不是这样。

有什么意义?这介于传递列表以写下所有计算和记忆的想法之间。这个 k 带有额外的行为,例如打印或写入列表(您可以创建一个写入某个列表的函数,为什么不呢?)。但是如果你仔细看你会发现它几乎没有改变这些函数的内部代码(只有函数的输入和输出受到影响),所以可以生成一个与类似printret 的函数相关联的装饰器,它基本上做同样的事情对于 f、g、h。

  • 优点:无需修改代码,比传递列表更灵活,无需额外工作(如记忆)。
  • 缺点:不纯(打印或修改某事),没有我们想要的那么灵活。

III

现在让我们看看修改函数体有什么帮助。不要害怕下面写的东西,慢慢来,玩一下那个东西。

class Logger:
    def __init__(self, lst, cur_val):
        self.lst = lst
        self.cur_val = cur_val
    
    def bind(self, f):
        res = f(self.cur_val)
        return Logger([self.cur_val] + res.lst + self.lst, res.cur_val)
    
    def __repr__(self):
        return "Logger( " + repr({'value' : self.cur_val,'lst' : self.lst}) + " )" 

def unit(x):
    return Logger([], x)

# you can also play with lala
def lala(x):
    if x <= 0:
        return unit(1)
    else:
        return lala(x - 1).bind(lambda y: unit(2*y))


def f(x):
    a = 2
    if x == 0:
        return unit(0)
    else:
        return g(x).bind(lambda y: unit(y + a))
def g(x):
    b = 1
    return h(x).bind(lambda y: unit(y - b))
def h(x):
    c = 1/2
    return f(x-1).bind(lambda y: unit(y*(1+c)))

f(4) # see for yourself

Logger 称为monad。我自己对这个概念不是很熟悉,但我想我做的一切都是正确的)f、g、h 是接受一个数字并返回一个 Logger 实例的函数。 Loggerbind 接受一个函数(如 f)并返回 Logger 新值(由 f 计算)和更新的“日志”。关键点 - 在我看来 - 是能够按照计算结果值的顺序对收集的函数做任何我们想做的事情。

后记

我根本不是某种函数式编程的“大师”,我相信我在这里遗漏了很多东西。但我所理解的是,函数式编程是关于反转程序的流程。这就是为什么,例如,我完全同意你关于生成器反对函数式编程的观点。当我们在函数func 中使用生成器gen 时,我们会一一生成值 funcfunc 在例如一个循环。函数式方法是使gen 成为一个以func 为参数的函数,并使func 对“产生”值执行计算。这就像genfunc 交换了他们的位置。所以流量是反转的!还有很多其他方法可以反转流动。 Monad 就是其中之一。

【讨论】:

  • 非常感谢您的深入回复 - 我有一些使用 Haskell 的经验,所以这对我来说很有意义,这也是我特意避免只是将列表传递给在一般情况下不应该真正依赖它们的函数。我会玩这个,看看我是否可以将它应用到我的问题上。再次感谢!
【解决方案4】:

itertools islice 获取生成器、起始值和终止值。它将为您提供起始值和终止值之间的元素作为生成器。如果 islice 不清楚,你可以在这里查看文档https://docs.python.org/3/library/itertools.html

intermediate_result = map(f, range(T))
final_result = next(itertools.islice(intermediate_result, start=T-1, stop=T))

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-18
    • 2018-08-17
    • 2018-02-18
    • 1970-01-01
    • 2014-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多