在不调用的情况下修改Python中的函数
假设我在Python里有一个任意的函数f,它可以接收一些参数。
def f(x): return 2*x
现在假设我想要一个函数,这个函数可以接收另一个函数,并返回这个函数的一个变体,变体的特点是如果把它画出来的话,会沿着y轴翻转。
最简单的方法是这样做:
def reverse_fn(f): return lambda x, funct=f: funct(-x)
不过,像这样一个接一个地堆叠修改函数的方式,过一段时间就会导致最大递归深度超出限制,因为最终得到的结果只是一个函数,它调用了另一个函数,而这个函数又调用更多的函数,一直往下调用。
那么,在Python中,制作可以反复使用的修改函数的最佳方法是什么呢?这样做不会占用过多的调用栈或嵌套函数?
3 个回答
我觉得如果你用的语言不支持尾调用优化,那就很难做到这一点,除非使用一种叫做“蹦床”的方法。还有一种选择是提取你想要的函数的抽象语法树(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
默认的递归深度限制是1000,可以通过 sys.setrecursionlimit()
来增加这个限制。不过,即使是1000层递归也已经非常深了,如果你的代码只是做一些简单的修改,这样的深度会让程序运行得很慢。
如果你想从简单的基本功能逐步构建复杂的函数,可以把这些复合函数写成Python代码文本,然后通过 eval()
来生成可以调用的函数。这样做的好处是,构建一个由1000个基本功能组成的函数,在执行时不会产生1000次函数调用和返回的开销。
需要注意的是,使用 eval()
时要小心;不要对不可信的来源使用 eval()
。
每创建一个函数,使用 eval()
的成本都比较高,如果不了解你具体想做什么,很难给出建议。你也可以简单地写一个程序,生成一个包含你想要的复合函数的大 .py 文件。
一种方法是编辑函数的字节码。这是一种非常高级的技术,而且也很脆弱。所以,不要在生产环境中使用这个方法!
不过,有一个模块正好可以实现你想要的编辑功能。它叫做 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.py
的 extend_and_rename
中)有点笨拙。所以,如果你连续调用 reverse_fn
1000 次,性能会大幅下降,因为局部变量的名称会变得非常庞大。我不太确定怎么解决这个问题,但如果你能解决的话,会显著提高重复内联函数的性能。