我有一个场景,我需要在Python中动态修饰函数内的递归调用.关键要求是动态地实现这一点,而不修改当前作用域中的函数.让我解释一下目前的情况以及我目前所做的努力.

假设我有一个函数traverse_tree,它递归遍历一个二元树并产生它的值.现在,我想修饰这个函数中的递归调用,以包含额外的信息,比如递归深度.当我直接将decorator 与函数一起使用时,它会像预期的那样工作.然而,我希望动态地实现相同的功能,而不修改当前作用域中的函数.


import functools


class Node:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right


def generate_tree():
    root = Node(1)
    root.left = Node(2)
    root.right = Node(3)
    root.left.left = Node(4)
    root.left.right = Node(5)
    root.right.left = Node(6)
    root.right.right = Node(7)
    return root


def with_recursion_depth(func):
    """Yield recursion depth alongside original values of an iterator."""
    
    class Depth(int): pass
    depth = Depth(-1)

    def depth_in_value(value, depth) -> bool:
        return isinstance(value, tuple) and len(value) == 2 and value[-1] is depth

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal depth
        depth = Depth(depth + 1)
        for value in func(*args, **kwargs):
            if depth_in_value(value, depth):
                yield value
            else:
                yield value, depth
        depth = Depth(depth - 1)

    return wrapper

# 1. using @-syntax
@with_recursion_depth
def traverse_tree(node):
    """Recursively yield values of the binary tree."""
    yield node.value
    if node.left:
        yield from traverse_tree(node.left)
    if node.right:
        yield from traverse_tree(node.right)


root = generate_tree()
for item in traverse_tree(root):
    print(item)

# Output:
# (1, 0)
# (2, 1)
# (4, 2)
# (5, 2)
# (3, 1)
# (6, 2)
# (7, 2)


# 2. Dynamically:  
def traverse_tree(node):
    """Recursively yield values of the binary tree."""
    yield node.value
    if node.left:
        yield from traverse_tree(node.left)
    if node.right:
        yield from traverse_tree(node.right)


root = generate_tree()
for item in with_recursion_depth(traverse_tree)(root):
    print(item)

# Output:
# (1, 0)
# (2, 0)
# (4, 0)
# (5, 0)
# (3, 0)
# (6, 0)
# (7, 0)

问题似乎在于函数中的递归调用是如何修饰的.当动态地使用修饰器时,它只修饰外部函数调用,而不是函数内的递归调用.我可以通过重新赋值(traverse_tree = with_recursion_depth(traverse_tree))来实现这一点,但是现在函数在当前作用域中已经被修改了.我希望动态地实现这一点,这样我就可以使用非修饰函数,或者可选地包装它以获得递归深度的信息.

我更喜欢保持简单,如果有替代解决方案的话,我希望避免使用字节码操作等技术."不过,如果这是必要的道路,我愿意go 探索,我在那个方向上做了try ,但还没有成功."

import ast


def modify_recursive_calls(func, decorator):

    def decorate_recursive_calls(node):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == func.__name__:
            func_name = ast.copy_location(ast.Name(id=node.func.id, ctx=ast.Load()), node.func)
            decorated_func = ast.Call(
                func=ast.Name(id=decorator.__name__, ctx=ast.Load()),
                args=[func_name],
                keywords=[],
            )
            node.func = decorated_func
        for field, value in ast.iter_fields(node):
            if isinstance(value, list):
                for item in value:
                    if isinstance(item, ast.AST):
                        decorate_recursive_calls(item)
            elif isinstance(value, ast.AST):
                decorate_recursive_calls(value)

    tree = ast.parse(inspect.getsource(func))
    decorate_recursive_calls(tree)
    ast.fix_missing_locations(tree)
    modified_code = compile(tree, filename="<ast>", mode="exec")
    modified_function = types.FunctionType(modified_code.co_consts[1], func.__globals__)
    return modified_function

推荐答案

这是一种可以使用上下文变量的情况:"contextvars"是语言中一个新的添加,它的设计是为了让在任务中运行的异步代码可以传递嵌套调用的"带外"信息,而不会被调用相同函数的其他任务覆盖或混淆.支票:https://docs.python.org/3/library/contextvars.html

它们有点像threading.locals,但有一个丑陋的接口—你最里面的调用可以从contextvar而不是全局作用域检索要调用的函数,所以,如果最外面的调用将这个contextvar设置为修饰函数,只有在那个"下降"中调用的函数会受到影响.

cotnextvars足够强大.然而,它们的接口是horrible个,因为必须首先创建一个上下文副本,并使用该上下文副本调用一个函数,而这个输入的函数可以改变上下文变量.但是,调用之外的代码将始终看到未改变的值,并且这在线程、XSLT任务等中是一致的和健壮的.

使用一个更简单的"mymul"递归函数和一个"logger"decorator ,代码可以是这样的:

import contextvars

recursive_func = contextvars.ContextVar("recursive_func")


def logger(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} called with {args}  and {kwargs}")
        return func(*args, **kwargs)
    return wrapper


def mymul(a, b):
    if b == 0:
        return 0
    return a + recursive_func.get()(a, b - 1)

recursive_func.set(mymul)

# non logging call:
mymul(3, 4)

# logging call - there must be an "entry point" which
# can change the contextvar from within the new context.
def logging_call_maker(*args, **kwargs):
    recursive_func.set(logger(mymul))
    return mymul(*args, **kwargs)


contextvars.copy_context().run(logging_call_maker, 3, 4)


# Another non logging call:
mymul(3, 4)

这里的关键点是:递归函数从ContextVar中检索要调用的函数,而不是在全局作用域中使用它的名称.

如果你喜欢这种方法,但是ContextVar方法似乎太过样板,那么这个方法可以更容易地使用:用途:在这里留下一条 comments :我有一个项目,我在几年前就开始了,用一些更友好的代码包装这个行为,但是我自己缺乏用例导致我停止摆弄它.

Python相关问答推荐

替换字符串中的多个重叠子字符串

从收件箱中的列中删除html格式

Pandas 有条件轮班操作

try 将一行连接到Tensorflow中的矩阵

如何使用表达式将字符串解压缩到Polars DataFrame中的多个列中?

如何让这个星型模式在Python中只使用一个for循环?

递归访问嵌套字典中的元素值

在含噪声的3D点网格中识别4连通点模式

形状弃用警告与组合多边形和多边形如何解决

如何在UserSerializer中添加显式字段?

根据列值添加时区

Python Tkinter为特定样式调整所有ttkbootstrap或ttk Button填充的大小,适用于所有主题

在输入行运行时停止代码

在Python中使用yaml渲染(多行字符串)

搜索按钮不工作,Python tkinter

Python类型提示:对于一个可以迭代的变量,我应该使用什么?

在第一次调用时使用不同行为的re. sub的最佳方式

使用np.fft.fft2和cv2.dft重现相位谱.为什么结果并不相似呢?

如何在Python中解析特定的文本,这些文本包含了同一行中的所有内容,

大Pandas 中的群体交叉融合