【问题标题】:Python decorator with optional argument (which is function)带有可选参数的 Python 装饰器(它是函数)
【发布时间】:2015-01-25 20:29:05
【问题描述】:

注意:我知道带有可选参数的装饰器包含三个嵌套函数。但这里的可选参数是 function 本身。在将其标记为重复之前,请阅读完整的帖子。我已经尝试了带有可选参数的装饰器的所有技巧,但我找不到任何以 function 作为参数的技巧。

我有一个包装错误的装饰器:

def wrap_error(func):
    from functools import wraps

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except:
            import sys

            exc_msg = traceback.format_exception(*sys.exc_info())
            raise MyCustomError(exc_msg)

    return wrapper

如果某个函数引发任何异常,它会包装错误。这个包装器的用法如下:

@wrap_error
def foo():
    ...

现在我想用额外的回调函数来修改这个包装器,这将是可选的。我希望这个包装器被用作:

@wrap_error
def foo():
    ...

@wrap_error(callback)
def foo():
    ...

我知道如何编写带有可选参数的装饰器(如果传递的参数不是函数,基于isfunction(func) 检查包装器)。但我不知道如何处理这种情况。

注意:我可以使用@wrap_error() 代替@wrap_error。这个包装器在多个包中使用,并且不可能全部更新更改

这里是拦截器: 将包装器视为:

@wrap_error(callback)               --->       foo = wrap_error(callback)(foo)
def foo():
    ...

所以,当wrap_error(foo)被执行时,我们不知道之后是否会有任何回调函数执行(如果我们只使用@wrap_error而不是@wrap_error(callback))。

如果没有(callback),wrap_error 中的包装函数将返回func(*args. **kwargs),以便我可以引发异常。否则我们必须返回func,以便在下一步调用它,如果func() 引发异常,我们在except 块中调用callback()

【问题讨论】:

  • 接受参数的装饰器是不同的;他们有三层def,而不是两层。您可以尝试编写一些返回装饰器或装饰函数的东西,具体取决于它的调用方式,但这很难实现;您如何区分使用callback 调用和使用要包装的函数?两者都只是可调用的对象。
  • 您是否考虑过传递一个函数(被包装)或字符串(回调名称)?您必须有一些有效回调的映射,也许是某种注册方法,但这将允许您随意使用它。
  • @jonrsharpe 不过可以这样做; Django 在其template tag decorators 中做到了。
  • 啊,除了我看到你关于包装函数和回调之间混淆的观点。无视我。
  • 问题已更新。我知道带有可选参数的装饰器包含三个嵌套函数。但这里的可选参数是 function 本身。在将其标记为重复之前,请仔细阅读完整的帖子。我已经尝试了带有可选参数的装饰器的所有技巧,但我找不到任何以 function 作为参数的技巧。

标签: python python-2.7 decorator python-decorators


【解决方案1】:

为了在尝试回答之前总结问题,您需要一个在以下两种情况下都能正常工作的装饰器:

@decorator  # case 1
def some_func(...):
    ...

@decorator(some_callback)  # case 2
def some_func(...):
    ...

或者,展开@ 语法以澄清事情:

some_func = decorator(some_func)  # case 1

some_func = decorator(some_callback)(some_func)  # case 2

在我看来,这里的棘手问题是decorator 很难区分some_funcsome_callback(因此区分情况1 和2);两者(大概)都是可调用的对象。


一种可能的解决方案是提供命名参数:

# imports at top of file, not in function definitions
from functools import wraps
import sys

def decorator(func=None, callback=None):
    # Case 1
    if func is not None:
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)  # or whatever
        return wrapper
    # Case 2
    elif callback is not None: 
        def deco(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                return callback(f(*args, **kwargs))  # or whatever
            return wrapper
        return deco

这使得案例 2 看起来略有不同:

@decorator(callback=some_callback)
def some_func(...):
    ...

但除此之外,你想要什么。请注意,您说不能使用的选项

@decorator()
def some_func(...):
    ...

无法使用这个,因为装饰器希望提供funccallback(否则它将返回None,这是不可调用的,所以你会得到一个TypeError) .

【讨论】:

  • 我试过这个。但无法完成这项工作。您能否粘贴有关您的建议的完整代码?
  • @MoinuddinQuadri 我已更新,但除了 “无法完成这项工作” 之外的更多信息会有所帮助。
  • @jonrshape:我的意思是您编写的代码没有按预期工作。它没有包装异常。不过,感谢您的回复。
  • @MoinuddinQuadri 你能更具体一点吗?错误?意外行为?我的演示只是为了展示装饰器的工作原理,您必须根据您的特定需求对其进行调整。
  • @johnsharpe:更新了您对该问题的回答。
【解决方案2】:

由于很难区分 decorator(func)decorator(callback),所以制作两个装饰器:

from functools import wraps

class MyCustomError(Exception):
    def __init__(self):
        print('in MyCustomError')

# Common implementation
def wrap(func,cb=None):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except:
            if cb is not None:
                cb()
            raise MyCustomError()
    return wrapper

# No parameters version
def wrap_error(func):
    return wrap(func)

# callback parameter version
def wrap_error_cb(cb):
    def deco(func):
        return wrap(func,cb)
    return deco

@wrap_error
def foo(a,b):
    print('in foo',a,b)
    raise Exception('foo exception')

def callback():
    print('in callback')

@wrap_error_cb(callback)
def bar(a):
    print('in bar',a)
    raise Exception('bar exception')

检查 foo 和 bar 是否正确使用 functools.wraps:

>>> foo
<function foo at 0x0000000003F00400>
>>> bar
<function bar at 0x0000000003F00598>

检查包装的函数是否工作:

>>> foo(1,2)
in foo 1 2
in MyCustomError
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "C:\test.py", line 16, in wrapper
    raise MyCustomError()
MyCustomError
>>> bar(3)
in bar 3
in callback
in MyCustomError
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "C:\test.py", line 16, in wrapper
    raise MyCustomError()
MyCustomError

更新

这是一种使用您要求的语法的方法,但我认为上面的答案更清楚。

from functools import wraps

class MyCustomError(Exception):
    def __init__(self):
        print('in MyCustomError')

# Common implementation
def wrap(func,cb=None):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except:
            if cb is not None:
                cb()
            raise MyCustomError()
    return wrapper

def wrap_error(func_or_cb):
    # If the function is tagged as a wrap_error_callback
    # return a decorator that returns the wrapped function
    # with a callback.
    if hasattr(func_or_cb,'cb'):
        def deco(func):
            return wrap(func,func_or_cb)
        return deco
    # Otherwise, return a wrapped function without a callback.
    return wrap(func_or_cb)

# decorator to tag callbacks so wrap_error can distinguish them
# from *regular* functions.
def wrap_error_callback(func):
    func.cb = True
    return func

### Examples of use

@wrap_error
def foo(a,b):
    print('in foo',a,b)
    raise Exception('foo exception')

@wrap_error_callback
def callback():
    print('in callback')

@wrap_error(callback)
def bar(a):
    print('in bar',a)
    raise Exception('bar exception')

【讨论】:

  • 有没有办法将两者合二为一?
  • 不太实用。你有两种不同的行为,所以使用两个函数。为什么要复杂化? @jonrsharpe's 提供了一种使用命名参数的方法,但不是您指定的语法。
  • 我确实想出了一种使用您的语法的方法。它涉及使用另一个装饰器来标记回调函数,以便可以将其与包装函数区分开来。查看更新的答案。
猜你喜欢
  • 2014-07-21
  • 1970-01-01
  • 1970-01-01
  • 2015-10-19
  • 1970-01-01
  • 1970-01-01
  • 2020-02-14
  • 2012-03-14
  • 1970-01-01
相关资源
最近更新 更多