Декоратор Python с необязательным аргументом (который является функцией)

Примечание. Я знаю, что декораторы с необязательным аргументом содержат три вложенные функции. Но необязательным аргументом здесь является сама функция. Пожалуйста, просмотрите весь пост, прежде чем отмечать его как дубликат. Я уже перепробовал все приемы декораторов с необязательным аргументом, но не нашел ни одного, который принимает 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() вызывает исключение, мы вызываем callback() в блоке except.


person Anonymous    schedule 27.11.2014    source источник
comment
Декораторы, принимающие аргументы, бывают разными; у них три слоя def, а не два. Вы можете попытаться написать что-то, что возвращает либо декоратор, либо декорированную функцию, в зависимости от того, как она вызывается, но это будет очень сложно реализовать; как вы можете определить разницу между вызовом с callback и с функцией, которую нужно обернуть? Оба являются просто вызываемыми объектами.   -  person jonrsharpe    schedule 27.11.2014
comment
Думали ли вы о передаче либо функции (для переноса), либо строки (имя обратного вызова)? Вам нужно будет иметь какое-то сопоставление действительных обратных вызовов, возможно, какой-то метод регистрации, но это позволит вам использовать его по своему усмотрению.   -  person jonrsharpe    schedule 27.11.2014
comment
@jonrsharpe это возможно; Django делает это в своих декораторах тегов шаблонов. .   -  person Daniel Roseman    schedule 27.11.2014
comment
Ах, кроме того, что я вижу вашу точку зрения о путанице между обернутой функцией и обратным вызовом. Игнорируй меня.   -  person Daniel Roseman    schedule 27.11.2014
comment
Вопрос обновлен. Я знаю, что декораторы с необязательным аргументом содержат три вложенные функции. Но необязательным аргументом здесь является сама функция. Пожалуйста, просмотрите всю публикацию до конца, прежде чем отмечать ее как дубликат. Я уже перепробовал все приемы декораторов с необязательным аргументом, но не нашел ни одного, который принимает функцию в качестве аргумента.   -  person Anonymous    schedule 27.11.2014
comment
@jonrsharpe: как вы можете определить разницу между вызовом с обратным вызовом и функцией, которую нужно обернуть? Это проблема, с которой я сталкиваюсь. Пытаюсь найти обходной путь для этого.   -  person Anonymous    schedule 27.11.2014
comment
Похоже, вы не можете: stackoverflow.com/questions/17119154/ - поэтому самый простой способ обойти это, вероятно, состоит в том, чтобы сделать два декоратора: wrap_error и wrap_error_callback, или использовать аргументы ключевого слова или несколько аргументов.   -  person Pieter Witvoet    schedule 27.11.2014
comment
Вы можете, но это включает в себя пометку обратного вызова, чтобы декоратор мог заметить разницу. В своем ответе я использовал декоратор, чтобы пометить обратный вызов, поэтому вам все равно придется написать два декоратора. Я думаю, что просто иметь два декоратора с разными именами чище.   -  person Mark Tolonen    schedule 28.11.2014


Ответы (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')
person Mark Tolonen    schedule 27.11.2014
comment
Есть ли способ объединить оба в один? - person Anonymous; 27.11.2014
comment
Это не очень практично. У вас есть два разных поведения, поэтому используйте две функции. Зачем усложнять? @jonrsharpe предоставляет способ с именованными параметрами, но не с указанным вами синтаксисом. - person Mark Tolonen; 28.11.2014
comment
Я придумал способ сделать это с вашим синтаксисом. Это включает в себя использование другого декоратора для пометки функции обратного вызова, чтобы ее можно было отличить от обернутой функции. Смотрите обновленный ответ. - person Mark Tolonen; 28.11.2014

Подводя итог проблеме, прежде чем пытаться ответить на нее, вам нужен декоратор, который правильно работает в обоих следующих контекстах:

@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_func и some_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(...):
    ...

не будет работать с этим, так как декоратор ожидает, что будет предоставлено либо func, либо callback (в противном случае он вернет None, что не может быть вызвано, поэтому вы получите TypeError).

person jonrsharpe    schedule 27.11.2014
comment
Я попробовал это. но не смог сделать эту работу. Не могли бы вы вставить полный код вашего предложения? - person Anonymous; 27.11.2014
comment
@MoinuddinQuadri Я обновил, но было бы полезно больше информации, чем не в состоянии сделать эту работу. - person jonrsharpe; 27.11.2014
comment
@jonrshape: я имел в виду, что написанный вами код не работает должным образом. Это не обертка исключений. Тем не менее, спасибо за ответ. - person Anonymous; 27.11.2014
comment
@MoinuddinQuadri, не могли бы вы быть более конкретным? Ошибки? Неожиданное поведение? Моя демонстрация предназначена только для того, чтобы показать, как будет работать декоратор, вам придется адаптировать его к вашим конкретным потребностям. - person jonrsharpe; 27.11.2014
comment
@johnsharpe: обновлен ваш ответ с проблемой. - person Anonymous; 27.11.2014
comment
@MoinuddinQuadri, что не является подходящим использованием функции редактирования. Если я попробую простой def callback(): print "Calling back", предложенный вами код отлично работает для меня - я вижу Calling back, а затем трассировку из файла raise. И пожалуйста, прекратите добавлять import во вложенные функции — все операции импорта должны быть в верхней части скрипта. - person jonrsharpe; 27.11.2014
comment
@johnsharpei: я использую @decorator(callback=callback) с функцией foo, которая вызывает ошибку. Фрагмент кода, который я отправил, не завершает эту ошибку, когда я вызываю foo() И я согласен, все импорты должны быть вверху. Это был просто тестовый код :) - person Anonymous; 27.11.2014
comment
@MoinuddinQuadri, потому что вы не просите об этом в ветке if callback is not None. Если вы хотите использовать MyCustomError в обеих ветвях, вам придется добавить его в обе версии wrapper. - person jonrsharpe; 27.11.2014