【问题标题】:Modify a function in python without calling it在python中修改一个函数而不调用它
【发布时间】:2014-04-03 02:17:29
【问题描述】:

假设我在 Python 中有一个任意函数 f,它接受参数。

def f(x): return 2*x

现在假设我想要一个函数,它接受一个函数并返回相同的函数,但沿 y 轴翻转(如果它是图形的话)。

显而易见的方法是

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

但是,像这样堆叠函数修改函数会在一段时间后破坏最大递归深度,因为结果只是一个调用另一个函数的函数,该函数一直调用更多函数。

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

【问题讨论】:

  • 似乎funct不会调用另一个调用更多函数的函数。
  • 我使用了一个 for 循环来应用 f = reverse_fn(f) 1000 次。 f(1) 现在立即抛出 RuntimeError: maximum recursion depth exceeded
  • 最好的方法通常是重新编写你的代码以不堆叠这么多装饰器。普通的 Python 代码不会对单个函数应用多个。与尾递归一样,它是 Python 并不真正支持的一种编程风格。

标签: python function lambda


【解决方案1】:

一种方法是编辑函数的字节码。这是一种非常先进的技术,也非常脆弱。所以,不要将它用于生产代码!

也就是说,有一个模块可以精确地实现您想要的那种编辑。它被称为bytecodehacks,于 2000 年 4 月 1 日首次发布(是的,这是一个愚人节玩笑,但功能齐全)。稍晚的版本(从 2005 年开始)在我安装的 Python 2.7.6 上运行良好;抓住它from CVS 并像往常一样运行setup.py。 (不要使用 April2000 版本;它不适用于较新的 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 次,您将获得巨大的减速,因为局部变量名称将开始爆炸式增长。我不知道如何解决这个问题,但如果你这样做,它将大大提高重复内联函数的性能。

【讨论】:

    【解决方案2】:

    可以使用sys.setrecursionlimit() 增加默认的递归限制 1000,但即使是 1000 也是非常深的递归,并且如果您的包装器往往是您在示例中显示的这种微不足道的更改,则会导致严重的性能损失。

    如果您尝试从简单的原语程序化地构建复杂的函数,您可以做的是将复合函数组合为 Python 源文本并将它们传递给 eval() 以获取可调用函数。这种方法具有显着优势,即由 1000 个原语构建的函数不会产生 1000 次函数调用的成本,并在执行时返回。

    注意eval()要谨慎使用;不要eval() 不受信任的来源。

    eval() 每个创建的函数都会相当昂贵,而且如果不知道更多关于你想要做什么的信息,很难给出建议。您也可以简单地编写一个程序,生成一个包含所需复合函数的大 .py 文件。

    【讨论】:

    • 我之前也想过这个问题,但我认为来自 javascript 的反 eval() 宣传已经破坏了我。显然这比 1000 深度递归要好,但是 eval() 会显着损害运行速度吗?
    • @NameHere:字节码操作有点难实现,但会产生比eval更好看的代码;或者,如果您赶时间,eval 应该可以:)
    • eval() 会非常昂贵,但这是创建每个函数的一次性成本。
    • 使用 eval 时,您必须解析定义中使用的变量的名称,除非您想稍后将所有内容作为 kwargs 传递。
    【解决方案3】:

    我认为,如果不使用蹦床,任何不支持尾调用优化的语言都无法实现这一点。另一种选择是提取所讨论函数的 AST 并生成一个“全新”函数,该函数根本不调用原始函数,但实现这一点并非易事,需要对 Python 的一些更内部的部分有很好的理解。

    另一方面,蹦床很容易实现,但缺点是您的函数不再是简单的 Python 函数——每次需要进行递归调用时,它们都会将该调用返回为,例如,表单中的元组(some_fn, args, kwargs)(虽然正常的返回值将被包装在一个 1 元组中),然后蹦床会为您进行调用,这样堆栈就不会增长。

    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
    

    【讨论】:

    • 函数本身不再有任何 AST。您必须编辑字节码。 (这实际上可能更容易,而不是更难)。
    猜你喜欢
    • 2011-11-18
    • 1970-01-01
    • 2012-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-18
    • 1970-01-01
    • 2021-01-30
    相关资源
    最近更新 更多