在不调用的情况下修改Python中的函数

2 投票
3 回答
575 浏览
提问于 2025-04-18 01:06

假设我在Python里有一个任意的函数f,它可以接收一些参数。

def f(x): return 2*x

现在假设我想要一个函数,这个函数可以接收另一个函数,并返回这个函数的一个变体,变体的特点是如果把它画出来的话,会沿着y轴翻转。

最简单的方法是这样做:

def reverse_fn(f): return lambda x, funct=f: funct(-x) 

不过,像这样一个接一个地堆叠修改函数的方式,过一段时间就会导致最大递归深度超出限制,因为最终得到的结果只是一个函数,它调用了另一个函数,而这个函数又调用更多的函数,一直往下调用。

那么,在Python中,制作可以反复使用的修改函数的最佳方法是什么呢?这样做不会占用过多的调用栈或嵌套函数?

3 个回答

0

我觉得如果你用的语言不支持尾调用优化,那就很难做到这一点,除非使用一种叫做“蹦床”的方法。还有一种选择是提取你想要的函数的抽象语法树(AST),然后生成一个“全新的”函数,这个新函数根本不调用原来的函数,但实现这个过程并不简单,需要对Python的一些内部机制有很好的理解。

而“蹦床”方法相对简单实现,但有个缺点,就是你的函数不能再是普通的Python函数了。每次需要递归调用时,它们会以一种像这样格式的元组返回调用:(some_fn, args, kwargs)(而正常的返回值会被包裹在一个单元素的元组中)。然后蹦床会帮你执行这个调用,这样就不会让调用栈变得越来越大。

def rec(fn, *args, **kwargs):
    return (fn, args, kwargs)

def value(val):
    return (val,)

def tailrec(fn, *args, **kwargs):
    while True:
        ret = fn(*args, **kwargs)
        if ret is None:
            return None
        elif len(ret) == 1:
            return ret[0]
        else:
            fn, args, kwargs = ret  # no kwargs supported if using tuples

def greet_a_lot(n):
    if n > 0:
        print "hello: " + str(n)
        return rec(greet_a_lot, n - 1)
    else:
        return value("done")

print tailrec(greet_a_lot, 10000)

输出:

hello: 100000
hello: 99999
...
hello: 3
hello: 2
hello: 1
done
1

默认的递归深度限制是1000,可以通过 sys.setrecursionlimit() 来增加这个限制。不过,即使是1000层递归也已经非常深了,如果你的代码只是做一些简单的修改,这样的深度会让程序运行得很慢。

如果你想从简单的基本功能逐步构建复杂的函数,可以把这些复合函数写成Python代码文本,然后通过 eval() 来生成可以调用的函数。这样做的好处是,构建一个由1000个基本功能组成的函数,在执行时不会产生1000次函数调用和返回的开销。

需要注意的是,使用 eval() 时要小心;不要对不可信的来源使用 eval()

每创建一个函数,使用 eval() 的成本都比较高,如果不了解你具体想做什么,很难给出建议。你也可以简单地写一个程序,生成一个包含你想要的复合函数的大 .py 文件。

2

一种方法是编辑函数的字节码。这是一种非常高级的技术,而且也很脆弱。所以,不要在生产环境中使用这个方法!

不过,有一个模块正好可以实现你想要的编辑功能。它叫做 bytecodehacks,第一次发布是在2000年4月1日(没错,这个是愚人节的玩笑,但它完全可以用)。稍晚一些的版本(2005年的)在我的Python 2.7.6上运行得很好;你可以从 CVS下载,然后像往常一样运行 setup.py。(不要使用2000年4月的版本;它在新版本的Python上不工作)。

bytecodehacks基本上实现了一些工具,可以让你编辑一段代码的字节码(比如一个函数、模块,甚至只是函数中的一个代码块)。你可以用它来实现宏功能。例如,在修改函数时,inline 工具可能是最有用的。

下面是如何使用 bytecodehacks 实现 reverse_fn 的方法:

from bytecodehacks.inline import inline

def reverse_fn(f):
    def g(x):
        # Note that we use a global name here, not `f`.
        return _f(-x)
    return inline(g, _f=f)

就这样!inline 处理了将函数 f "内联" 到 g 的主体中的繁琐工作。实际上,如果 f(x)return 2*x,那么从 reverse_fn(f) 返回的函数就相当于 return 2*(-x)(其中不会有任何函数调用)。

现在,bytecodehacks 的一个限制是变量重命名(在 inline.pyextend_and_rename 中)有点笨拙。所以,如果你连续调用 reverse_fn 1000 次,性能会大幅下降,因为局部变量的名称会变得非常庞大。我不太确定怎么解决这个问题,但如果你能解决的话,会显著提高重复内联函数的性能。

撰写回答