You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python中如何动态装饰递归函数且不修改当前作用域

动态装饰Python递归函数内部调用的问题与解决方法

问题背景

需要为递归遍历二叉树的函数traverse_tree动态添加递归深度统计功能,核心要求:

  • 动态实现装饰,不修改当前作用域中的原函数
  • 内部递归调用也需被装饰,正确返回递归深度
  • 保留未装饰的原函数,按需使用带深度统计的版本

使用@with_recursion_depth语法装饰时,递归调用能正确被装饰,输出每个节点值对应的深度;但动态调用with_recursion_depth(traverse_tree)(root)时,仅外层调用被装饰,内部递归调用仍指向原函数,导致所有深度都显示为0。直接赋值traverse_tree = with_recursion_depth(traverse_tree)虽能解决,但会覆盖原函数,不符合需求。

现有代码示例

基础定义与装饰器

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

装饰器语法(正常工作)

@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)
# 输出:
# (1, 0)
# (2, 1)
# (4, 2)
# (5, 2)
# (3, 1)
# (6, 2)
# (7, 2)

动态调用(问题场景)

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)
# 输出:
# (1, 0)
# (2, 0)
# (4, 0)
# (5, 0)
# (3, 0)
# (6, 0)
# (7, 0)

解决方案

方案1:添加可选参数传递装饰后的函数(最简单)

修改原函数,增加一个可选参数self_func,让递归调用使用该参数指向的函数,而非硬编码的原函数名。这样装饰后的wrapper可以将自身传递给原函数,确保内部递归调用也使用装饰后的版本。

def traverse_tree(node, self_func=None):
    # 若未传入self_func,默认指向原函数
    self_func = self_func or traverse_tree
    yield node.value
    if node.left:
        yield from self_func(node.left, self_func)
    if node.right:
        yield from self_func(node.right, self_func)

# 动态装饰并调用,原函数traverse_tree仍保留
decorated_traverse = with_recursion_depth(traverse_tree)
for item in decorated_traverse(root):
    print(item)
# 输出与@语法装饰的结果一致

方案2:修复AST修改方案

原AST修改代码的问题在于每次递归调用都会重新装饰函数,导致状态混乱。修复后的方案会在函数内部创建一个指向装饰后函数的局部变量,替换所有递归调用的目标。

import ast
import inspect
import types

def modify_recursive_calls(func, decorator):
    # 解析原函数源码为AST
    source = inspect.getsource(func)
    tree = ast.parse(source)
    
    func_def = tree.body[0]
    # 在函数开头添加局部变量,指向装饰后的原函数
    decorated_assign = ast.Assign(
        targets=[ast.Name(id="_decorated_self", ctx=ast.Store())],
        value=ast.Call(
            func=ast.Name(id=decorator.__name__, ctx=ast.Load()),
            args=[ast.Name(id=func.__name__, ctx=ast.Load())],
            keywords=[]
        )
    )
    func_def.body.insert(0, decorated_assign)
    
    # 替换所有递归调用为_decorated_self
    def replace_recursive_calls(node):
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == func.__name__:
            node.func = ast.copy_location(ast.Name(id="_decorated_self", ctx=ast.Load()), node.func)
        # 递归处理所有子节点
        for field, value in ast.iter_fields(node):
            if isinstance(value, list):
                for item in value:
                    if isinstance(item, ast.AST):
                        replace_recursive_calls(item)
            elif isinstance(value, ast.AST):
                replace_recursive_calls(item)
    
    replace_recursive_calls(func_def)
    ast.fix_missing_locations(tree)
    
    # 编译AST并生成新函数
    compiled = compile(tree, filename="<ast_modified>", mode="exec")
    new_func = types.FunctionType(
        compiled.co_consts[1], 
        func.__globals__, 
        func.__name__, 
        func.__defaults__, 
        func.__closure__
    )
    return new_func

# 使用方式
def traverse_tree(node):
    yield node.value
    if node.left:
        yield from traverse_tree(node.left)
    if node.right:
        yield from traverse_tree(node.right)

# 生成修改后的装饰函数,原函数不受影响
decorated_traverse = modify_recursive_calls(traverse_tree, with_recursion_depth)
for item in decorated_traverse(root):
    print(item)
# 输出与@语法装饰的结果一致

总结

  • 方案1实现最简单,仅需修改原函数添加一个可选参数,无需复杂的AST或字节码操作,推荐优先使用。
  • 方案2无需修改原函数,通过AST修改生成新的装饰后函数,适合无法修改原函数源码的场景。

内容的提问来源于stack exchange,提问作者Michiel Karrenbelt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 23:36:02