简体   繁体   English

带有可选参数(功能)的Python装饰器

[英]Python decorator with optional argument (which is function)

Note: I know that decorators with optional argument contain three nested function. 注意:我知道带有可选参数的装饰器包含三个嵌套函数。 But optional argument here is function itself. 但是这里的可选参数是函数本身。 Please go through the complete post before you mark this as duplicate. 在将其标记为重复之前,请阅读完整的帖子。 I already tried all the tricks for decorators with optional argument, but I could not found any that takes function as argument. 我已经尝试了带有可选参数的装饰器的所有技巧,但是找不到任何以函数为参数的装饰器。

I am having a decorator for wrapping error: 我有一个包装错误的装饰器:

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

If some function raises any exception, it wraps the error. 如果某个函数引发任何异常,则包装错误。 This wrapper is used like: 该包装器的用法如下:

@wrap_error
def foo():
    ...

Now I want to modify this wrapper with additional callback function which will be optional. 现在,我想使用其他可选的回调函数来修改此包装器。 And I want this wrapper to be used as: 我希望将此包装器用作:

@wrap_error
def foo():
    ...

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

I know how to write decorators with optional arguments (in case passed argument is not function, based on isfunction(func) check within wrapper). 我知道如何用可选参数编​​写装饰器(如果传递的参数不起作用,则基于包装器中的isfunction(func)检查)。 But I am not sure how to handle this case. 但是我不确定该如何处理。

Note: I can not use @wrap_error() instead of @wrap_error . 注意:我不能使用@wrap_error()代替@wrap_error This wrapper is used in multiple number of packages, and it is not possible to update the change in all 此包装器用于多个软件包,并且不可能全部更新更改

Here is the blocker : Consider the wrapper as: 这是阻止程序 :将包装器视为:

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

So, by the time wrap_error(foo) is executed, we do not know whether there will be any callback function for execution after that or not (in case we use just @wrap_error instead of @wrap_error(callback) ). 因此,在执行wrap_error(foo) ,我们不知道在那之后是否将有任何回调函数要执行(以防我们仅使用@wrap_error而不是@wrap_error(callback) )。

If there is no (callback) , wrapping function within wrap_error will return func(*args. **kwargs) so that I can raise exception. 如果没有(callback) ,则wrap_error中的包装函数将返回func(*args. **kwargs)以便引发异常。 Else we have to return func so that it is called at next step, and if func() raises the exception, we call callback() in except block. 否则,我们必须返回func以便在下一步中调用它,并且如果func()引发异常,我们将在except块中调用callback()

To summarise the problem before attempting to answer it, you want a decorator that works correctly in both of the following contexts: 为了在尝试回答问题之前总结问题,您需要一个装饰器,它可以在以下两个上下文中正常工作:

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

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

or, to unroll the @ syntax to clarify things: 或者,展开@语法以澄清问题:

some_func = decorator(some_func)  # case 1

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

The tricky issue here, as I see it, is that it's very hard for decorator to tell the difference between some_func and some_callback (and therefore between cases 1 and 2); 正如我所看到的,这里的棘手问题是decorator很难分辨some_funcsome_callback之间的区别(因此,情况1和2之间也是如此); both are (presumably) just callable objects. 两者都是(大概)只是可调用对象。


One potential solution is to provide named arguments: 一种可能的解决方案是提供命名参数:

# 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

This makes case 2 look slightly different: 这使情况2看起来略有不同:

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

But otherwise does what you want. 但是,否则您想要什么。 Note that the option you say you can't use 请注意,您说无法使用的选项

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

won't work with this, as the decorator expects either func or callback to be supplied (it will return None otherwise, which isn't callable, so you'll get a TypeError ). 不会与此配合使用,因为装饰器希望提供funccallback (否则将返回None ,这是不可调用的,因此您将得到TypeError )。

Since it is hard to tell decorator(func) from decorator(callback) , make two decorators: 由于很难从decorator(callback)告诉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')

Check that foo and bar are correctly using functools.wraps : 使用functools.wraps检查foo和bar是否正确:

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

Check that the wrapped functions work: 检查包装的功能是否正常工作:

>>> 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

Updated 更新

Here's a way to do it with the syntax you requested, but I think the above answer is clearer. 这是使用您请求的语法的一种方法,但是我认为上面的答案更清楚。

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')

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM