在 Python 中,有没有一种方法可以引用当前函数?

3

我希望有一个函数可以引用自己,比如递归。

所以我要做的像这样:

def fib(n):
    return n if n <= 1 else fib(n-1)+fib(n-2)

这通常是可以的,但是fib实际上并不是指向自己;它指向封闭块中fib的绑定。所以如果由于某种原因fib被重新分配,它会出问题:

>>> foo = fib
>>> fib = foo(10)
>>> x = foo(8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fib
TypeError: 'int' object is not callable

如果可能的话,我该如何防止这种情况发生(在fib内部)?据我所知,在函数定义完全执行之前,fib的名称不存在;是否有任何解决方法?

我没有一个真正的使用案例,它可能会真的发生;我只是出于好奇而问。

4个回答

7
我会为此编写一个装饰器。
from functools import wraps

def selfcaller(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(wrapper, *args, **kwargs)
    return wrapper

并像这样使用它

@selfcaller
def fib(self, n):
    return n if n <= 1 else self(n-1)+self(n-2)

这实际上是一种可读性高的定义固定点组合子(或Y组合子)的方法:

fix = lambda g: (lambda f: g(lambda arg: f(f)(arg))) (lambda f: g(lambda arg: f(f)(arg)))

用法:

fib = fix(lambda self: lambda n: n if n <= 1 else self(n-1)+self(n-2))

或者:

@fix
def fib(self):
    return lambda n: n if n <= 1 else self(n-1)+self(n-2)

这里的绑定发生在形式参数中,因此问题不会出现。

这并不完全相同;selfcaller 适用于实际的 fib 函数,但 Y 组合子适用于返回 fib 函数的函数。绑定发生在创建函数的函数的形式参数中。这只是一个本地变量,没有理由将其作为参数。此外,内部函数没有必要是 lambda 而不是 def - 在这种情况下,您已经拥有了本地名称。这意味着 Y 组合子没有添加任何内容;你所需要的装饰器只是调用创建函数的函数。这就是... 我的答案。 - abarnert
@abarnert 这样你就不需要显式删除了... 我在考虑 Y 组合子。我不确定你是否正确,但如果你确信,可以随意将我的添加删除 - 我不想误导。 - Elazar
1
@abarnert,由于λ演算认为多参数函数(x,y) -> z等价于高阶函数x -> (y -> z),因此selfcaller等同于Y组合子。 - Abe Karplus
此外,在我的答案中,实际上也没有必要进行显式删除,就像您在这里不能明确地隐藏或删除原始的 fib 一样。 - abarnert
1
从实现的角度来看,柯里化和取消柯里化在Python中的内在区别在于,它们不是一种无操作(no-op);每个操作都必须通过显式地创建另一个函数(使用partial、lambda或def)来完成。当然,在实际情况中,如果您已经创建了一个函数,有时可以折叠额外的一个函数和您已经拥有的一个函数,但是…我实际上并不确定如何用理论术语来描述这种情况。 - abarnert
显示剩余10条评论

3

你想做的事情是无法实现的。你说得对,fib在函数定义执行之前不存在(或者更糟糕的是,它存在但指向完全不同的东西...),这意味着从fib内部没有任何解决方法能够可能起作用。*

然而,如果你愿意放弃这个要求,有一些可行的解决方法。例如:

def _fibmaker():
    def fib(n):
        return n if n <= 1 else fib(n-1)+fib(n-2)
    return fib
fib = _fibmaker()
del _fibmaker

现在,fib指向来自调用_fibmaker的本地环境中闭包绑定。当然,即使你真的想这样做,它也可以被替换,但这不容易(fib.__closure__属性不可写;它是一个元组,因此无法替换其中任何一个单元格;每个单元格的cell_contents是一个只读属性,...),你不可能无意间这样做。
还有其他方法可以做到这一点(例如,在fib中使用一个特殊的占位符,并使用装饰器将占位符替换为装饰的函数),它们都同样不明显且丑陋,这似乎违反了TOOWTDI的原则。但在这种情况下,“it”是您可能不想进行的操作,因此并不重要。
以下是一种能够编写通用纯Python装饰器的方式,该装饰器使用self而不需要额外的self参数来传递函数:
def selfcaller(func):
    env = {}
    newfunc = types.FunctionType(func.__code__, globals=env)
    env['self'] = newfunc
    return newfunc

@selfcaller
def fib(n):
    return n if n <= 1 else self(n-1)+self(n-2)

当然,这种方法无法在具有任何从“globals”绑定的自由变量的函数中使用,但您可以通过一些内省来修复它。而且,在此过程中,我们还可以消除在函数定义内部使用“self”的需要。
def selfcaller(func):
    env = dict(func.__globals__)
    newfunc = types.FunctionType(func.__code__, globals=env)
    env[func.__code__.co_name] = newfunc
    return newfunc

这是特定于Python 3.x的;在2.x中,一些属性名称可能不同,但其他方面是相同的。

这仍然不是100%完全通用的。例如,如果您想要能够将其用于方法,以便它们可以调用自己,即使类或对象重新定义了它们的名称,您需要使用稍微不同的技巧。还有一些病态情况可能需要构建一个新的CodeType,其中包含func.__code__.co_code。但基本思想是相同的。


* 就Python而言,在名称绑定之前,它并不存在...但显然,在内部,解释器必须知道您正在定义的函数的名称。至少一些解释器提供了非可移植的方法来获取该信息。

例如,在CPython 3.x中,您可以非常容易地获取当前正在定义的函数的名称-只需使用sys._getframe().f_code.co_name即可。

当然,这对您没有任何直接好处,因为没有任何内容(或错误的内容)与该名称绑定。但请注意其中的f_code。那是当前帧的代码对象。当然,您无法直接调用代码对象,但是您可以间接地这样做,通过生成一个新的函数或使用bytecodehacks

例如:

def fib2(n):
    f = sys._getframe()
    fib2 = types.FunctionType(f.f_code, globals=globals())
    return n if n<=1 else fib2(n-1)+fib2(n-2)

再次说明,这种方法不能处理所有的病态情况...但我能想到的唯一解决方法是实际上保留对帧的循环引用,或至少保留其全局变量(例如,通过传递 globals = f.f_globals),但这似乎是一个非常糟糕的主意。

请参见Frame Hacks以了解更多聪明的技巧。


最后,如果你愿意完全离开Python,你可以创建一个导入hook,它会预处理或编译你的代码,从Python自定义扩展中转换为纯Python和/或字节码。

如果你认为“但这听起来更像宏而不是预处理程序的黑客,如果只有Python有宏”......那么你可能更喜欢使用一个给Python提供宏的预处理程序黑客,比如MacroPy,然后将你的扩展编写为宏。


(如果“存在但指的是完全不同的事物”,则没有任何影响 - 很快将分配此名称。) - Elazar
我明白了。我以为可能有一些反射的东西我不熟悉...你的答案似乎是直截了当的方式。 - Elazar
1
@Elazar:嗯,从fib内部来看,没有什么可以反映的。但是,是的,从外部——例如,在装饰器中——您可以获得各种信息,例如函数中未绑定名称的列表。 (inspect模块文档在顶部显示了所有易于反射的内容的漂亮图表。)如果您愿意,您可以使用它来制作一个不需要添加显式self参数的装饰器,就像Abe Karplus的那样。 - abarnert
2
@Elazar:如果你想编写仅在CPython中运行而不是纯Python中运行的代码,那么可以使用sys._getframe(1)。请参阅Frame Hacks以了解一些可行的示例。 - abarnert
@Elazar:不使用显式的self的想法是使用types.FunctionTypes来构建一个新函数,该函数具有原始代码,但具有特殊的全局环境。给我一秒钟,我会详细说明。 - abarnert
显示剩余6条评论

2

就像abamert说的"..没有从内部解决这个问题的方法。"

这是我的方法:

def fib(n):
    def fib(n):
        return n if n <= 1 else fib(n-1)+fib(n-2)
    return fib(n)

如果你不满意我的编辑,你可以回滚它;我本以为它需要你的批准。 - Elazar
我认为这个答案需要宏(在@abarnert的最后一部分提到)。 - Elazar
@Elazar 我对Python和abamert提到的MacroPy一无所知。事实上,我正在阅读这篇文章(http://www.gigamonkeys.com/book/macros-standard-control-constructs.html),因此我*假设*同时阅读Python宏会很棒。 - Bleeding Fingers
如果您有兴趣使用宏来完成此操作,并且MacroPy示例中没有现成的宏,而且您自己也无法弄清楚如何实现,请联系共同作者Haoyi Li,我敢打赌明天示例中就会有这个宏。 - abarnert
1
接受挑战!我认为它不够通用,不能放在示例中,但我会在某个地方发布代码片段供人们查看。 - Li Haoyi
显示剩余2条评论

1

有人向我询问基于宏的解决方案,所以这里提供一个:

# macropy/my_macro.py
from macropy.core.macros import *

macros = Macros()

@macros.decorator()
def recursive(tree, **kw):
    tree.decorator_list = []

    wrapper = FunctionDef(
        name=tree.name,
        args=tree.args,
        body=[],
        decorator_list=tree.decorator_list
    )

    return_call = Return(
        Call(
            func = Name(id=tree.name),
            args = tree.args.args,
            keywords = [],
            starargs = tree.args.vararg,
            kwargs = tree.args.kwarg
        )
    )

    return_call = parse_stmt(unparse_ast(return_call))[0]

    wrapper.body = [tree, return_call]

    return wrapper

这可以如下使用:

>>> import macropy.core.console
0=[]=====> MacroPy Enabled <=====[]=0
>>> from macropy.my_macro import macros, recursive
>>> @recursive
... def fib(n):
...     return n if n <= 1 else fib(n-1)+fib(n-2)
...
>>> foo = fib
>>> fib = foo(10)
>>> x = foo(8)
>>> x
21

它基本上就是完全按照hus787的包装方式进行操作:
  • 创建一个新语句,其中使用原始函数的参数列表作为 ...return fib(...)
  • 创建一个与旧函数同名、同参数、同 decorator_list 的新 def
  • 将旧函数连同 return 语句一起放入新函数def的主体中
  • 剥离原始函数的装饰器(我假设您想要装饰包装器)
parse_stmt(unparse_ast(return_call))[0] 的垃圾代码只是为了快速让东西工作(实际上,您无法仅从函数的参数列表中复制 argument AST 并在 Call AST 中使用它们),但这只是细节。
为了证明它确实在执行此操作,您可以添加一个 print unparse_ast 语句来查看转换后的函数的样子:
@macros.decorator()
def recursive(tree, **kw):
    ...
    print unparse_ast(wrapper)
    return wrapper

当按照以上方式运行时,会打印出以下内容。
def fib(n):

    def fib(n):
        return (n if (n <= 1) else (fib((n - 1)) + fib((n - 2))))
    return fib(n)

看起来正是你所需要的!它应该适用于任何带有多个参数、kwargs、默认值等的函数,但我懒得测试。使用AST有点繁琐,而MacroPy仍然是超级实验性的,但我认为它非常棒。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接