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

4 回答 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)

这里的绑定发生在形参中,所以不会出现问题。

于 2013-05-29T19:16:06.447 回答
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。但在这种情况下,“它”是你可能不想做的事情,所以这并不重要。


self这是您可以为使用而不是自己的名称的函数编写通用的纯 python 装饰器的一种方法,而无需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% 完全通用的。例如,如果您希望能够在方法上使用它,这样即使类或对象重新定义了它们的名称,它们仍然可以调用自己,那么您需要稍微不同的技巧。并且有一些病理案例可能需要构建一个新CodeTypefunc.__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,您可以创建一个导入钩子,将您的代码从自定义扩展的 Python 预处理或编译defrec成纯 Python 和/或字节码。

如果你在想“但是如果只有 Python 有宏,这听起来作为一个宏比作为一个预处理器黑客要好得多”......那么你可能更喜欢使用提供 Python 宏的预处理器黑客,比如MacroPy,然后将扩展编写为宏。

于 2013-05-29T19:08:45.937 回答
2

就像abamert所说的“..从内部无法解决问题..”。

这是我的方法:

def fib(n):
    def fib(n):
        return n if n <= 1 else fib(n-1)+fib(n-2)
    return fib(n)
于 2013-05-29T21:00:52.317 回答
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(...),它使用原始函数的参数列表作为...
  • 创建一个新def的,与旧的具有相同的名称、相同的参数、相同的 decorator_list
  • 将旧函数和return语句一起放在新函数定义的主体中
  • 剥离其装饰器的原始功能(我假设您想要装饰包装器)

垃圾是让东西工作的parse_stmt(unparse_ast(return_call))[0]快速技巧(实际上,您不能只argument从函数的参数列表中复制 AST 并在CallAST 中使用它们),但这只是细节。

为了表明它确实在这样做,您可以添加一个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)

看起来正是你想要的!它应该适用于具有多个 args、kwargs、默认值等的任何函数,但我懒得测试。使用 AST 有点冗长,MacroPy仍然是超级实验性的,但我认为它非常简洁。

于 2013-05-31T20:10:25.040 回答