Skip to main content
 首页 » 编程设计

python之避免在装饰器中进行双重包装

2025年05月04日16jpfss

我有一个类似于以下的 Python 3 脚本:

def transaction(write=False): 
    def _transaction_deco(fn): 
        @wraps(fn) 
        def _wrapper(*args, **kwargs): 
            env.timestamp = arrow.utcnow() 
            with TxnManager(write=write) as txn: 
                ret = fn(*args, **kwargs) 
            delattr(env, 'timestamp') 
            return ret 
        return _wrapper 
    return _transaction_deco 
 
@transaction(True) 
def fn1(x=False): 
    if x: 
        return fn2() 
    do_something() 
 
@transaction(True) 
def fn2(): 
    do_something_else() 

fn1 和fn2 可以独立调用,都封装在transaction 装饰器中。

此外,fn1 在某些情况下可以调用fn2。这是我遇到问题的地方,因为装饰器代码被调用了两次,事务被嵌套,时间戳设置了两次,删除了两次(实际上是第二次 delattr(env, 'timestamp')被调用时,将引发异常,因为 timestamp 不再存在)。

我希望用 transaction 装饰的任何东西都只在没有装饰的情况下才被装饰。

我试图根据 separate Q&A 的提示修改我的代码:

def transaction(write=False): 
    def _transaction_deco(fn): 
        # Avoid double wrap. 
        if getattr(fn, 'in_txn', False): 
            return fn(*args, **kwargs) 
 
        @wraps(fn) 
        def _wrapper(*args, **kwargs): 
            # Mark transaction begin timestamp. 
            env.timestamp = arrow.utcnow() 
            with TxnManager(write=write) as txn: 
                ret = fn(*args, **kwargs) 
            delattr(env, 'timestamp') 
            return ret 
        _wrapper.in_txn = True 
        return _wrapper 
 
    return _transaction_deco 

即如果为函数设置了 _in_txn 属性,则返回裸函数,因为我们已经在交易中。

但是,如果我使用调试器运行 fn1(True),我会发现对 in_txn 的检查永远不会为真,因为 _wrapper对于外部调用,变量设置为 f1,对于内部调用,变量设置为 f2,因此这两个函数被分别修饰。

我可以将时间戳变量设置为上下文管理器的属性,但我仍然会得到两个最好的上下文管理器,如果两个事务都是 R/W,就会发生坏事。

有人可以提出更好的解决方案吗?

编辑:根据收到的回复,我相信我省略了一个关键元素:fn1 和 fn2 是 API 方法,必须始终运行在交易中。使用装饰器的原因是为了防止 API 实现者必须对事务做出决定,或者必须在上下文管理器或装饰器中手动包装方法。然而,我会赞成非装饰器方法,只要它保持 fn1 和 fn2 的实现简单。

请您参考如下方法:

我不使用装饰器,而是创建另一个上下文管理器来显式使用。

@contextlib.contextmanager 
def MyTxnManager(write): 
    env.timestamp = arrow.utcnow() 
    with TxnManager(write=write) as txn: 
        yield txn 
    delattr(env, 'timestamp') 
 
with MyTxnManager(True) as txn: 
    fn1() 

在这里,我们没有将事务隐藏在 fn1 和 fn2 中,而是将时间戳管理包装在一个新的事务管理器中。这样,只有在我们明确要求它们这样做时,才会在事务中调用 fn1 和 fn2。