避免在装饰器中进行双层包装

时间:2018-03-07 20:34:47

标签: python decorator

我有一个类似于以下内容的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()

fn1fn2可以独立调用,并且都包含在transaction装饰器中。

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

我希望用timestamp装饰的任何东西只能装饰,如果不是这样的话。

我尝试根据separate Q&A

中的提示修改我的代码
transaction

即。如果为函数设置了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用于内部调用,因此函数都是单独修饰的。

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

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

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

3 个答案:

答案 0 :(得分:4)

而不是装饰器,我只是让另一个上下文管理器明确使用。

@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()

这里,我们不是将事务隐藏在fn1fn2中,而是将时间戳管理包装在新的事务管理器中。这样,只有在我们明确要求它们执行此操作时,才会在事务中调用fn1fn2

答案 1 :(得分:2)

没有必要在装饰器中处理这个问题。分离{{1}}的包装和展开版本要简单得多,例如:

{{1}}

装饰器语法只是语法糖,因此结果将完全相同。

答案 2 :(得分:0)

您始终只需检查环境是否已有时间戳,在这种情况下不创建时间戳。它使用已存在的属性作为标志,而不是创建一个新标志:

def transaction(write=False):
    def _transaction_deco(fn):
        @wraps(fn)
        def _wrapper(*args, **kwargs):
            if hasattr(env, 'timestamp'):
                ret = fn(*args, **kwargs)
            else:
                env.timestamp = arrow.utcnow()
                with TxnManager(write=write) as txn:
                    ret = fn(*args, **kwargs)
                delattr(env, 'timestamp')
            return ret
        return _wrapper
    return _transaction_deco

这将允许您嵌套任意数量的事务调用,并且只使用最外层调用的时间戳和上下文管理器。