Using the same decorator (with arguments) with functions and methods

后端 未结 5 1512
隐瞒了意图╮
隐瞒了意图╮ 2020-12-05 00:54

I have been trying to create a decorator that can be used with both functions and methods in python. This on it\'s own is not that hard, but when creating a decorator that

相关标签:
5条回答
  • 2020-12-05 01:31

    Here is a general way I found to detect whether a decorated callable is a function or method:

    import functools
    
    class decorator(object):
    
      def __init__(self, func):
        self._func = func
        self._obj = None
        self._wrapped = None
    
      def __call__(self, *args, **kwargs):
        if not self._wrapped:
          if self._obj:
            self._wrapped = self._wrap_method(self._func)
            self._wrapped = functools.partial(self._wrapped, self._obj)
          else:
            self._wrapped = self._wrap_function(self._func)
        return self._wrapped(*args, **kwargs)
    
      def __get__(self, obj, type=None):
        self._obj = obj
        return self
    
      def _wrap_method(self, method):
        @functools.wraps(method)
        def inner(self, *args, **kwargs):
          print('Method called on {}:'.format(type(self).__name__))
          return method(self, *args, **kwargs)
        return inner
    
      def _wrap_function(self, function):
        @functools.wraps(function)
        def inner(*args, **kwargs):
          print('Function called:')
          return function(*args, **kwargs)
        return inner
    

    Example usage:

    class Foo(object):
      @decorator
      def foo(self, foo, bar):
        print(foo, bar)
    
    @decorator
    def foo(foo, bar):
      print(foo, bar)
    
    foo(12, bar=42)      # Function called: 12 42
    foo(12, 42)          # Function called: 12 42
    obj = Foo()
    obj.foo(12, bar=42)  # Method called on Foo: 12 42
    obj.foo(12, 42)      # Method called on Foo: 12 42
    
    0 讨论(0)
  • 2020-12-05 01:35

    The decorator is always applied to a function object -- have the decorator print the type of its argument and you'll be able to confirm that; and it should generally return a function object, too (which is already a decorator with the proper __get__!-) although there are exceptions to the latter.

    I.e, in the code:

    class X(object):
    
      @deco
      def f(self): pass
    

    deco(f) is called within the class body, and, while you're still there, f is a function, not an instance of a method type. (The method is manufactured and returned in f's __get__ when later f is accessed as an attribute of X or an instance thereof).

    Maybe you can better explain one toy use you'd want for your decorator, so we can be of more help...?

    Edit: this goes for decorators with arguments, too, i.e.

    class X(object):
    
      @deco(23)
      def f(self): pass
    

    then it's deco(23)(f) that's called in the class body, f is still a function object when passed as the argument to whatever callable deco(23) returns, and that callable should still return a function object (generally -- with exceptions;-).

    0 讨论(0)
  • 2020-12-05 01:44

    Since you're already defining a __get__ to use your decorator on the Bound Method, you could pass a flag telling it if it's being used on a method or function.

    class methods(object):
        def __init__(self, *_methods, called_on_method=False):
            self.methods = _methods
            self.called_on_method
    
        def __call__(self, func):
            if self.called_on_method:
                def inner(self, request, *args, **kwargs):
                    print request
                    return func(request, *args, **kwargs)
            else:
                def inner(request, *args, **kwargs):
                    print request
                    return func(request, *args, **kwargs)
            return inner
    
        def __get__(self, obj, type=None):
            if obj is None:
                return self
            new_func = self.func.__get__(obj, type)
            return self.__class__(new_func, called_on_method=True)
    
    0 讨论(0)
  • 2020-12-05 01:47

    A partial (specific) solution I have come up with relies on exception handling. I am attempting to create a decorator to only allow certain HttpRequest methods, but make it work with both functions that are views, and methods that are views.

    So, this class will do what I want:

    class methods(object):
        def __init__(self, *_methods):
            self.methods = _methods
    
        def __call__(self, func): 
            @wraps(func)
            def inner(*args, **kwargs):
                try:
                    if args[0].method in self.methods:
                        return func(*args, **kwargs)
                except AttributeError:
                    if args[1].method in self.methods:
                        return func(*args, **kwargs)
                return HttpResponseMethodNotAllowed(self.methods)
            return inner
    

    Here are the two use cases: decorating a function:

    @methods("GET")
    def view_func(request, *args, **kwargs):
        pass
    

    and decorating methods of a class:

    class ViewContainer(object):
        # ...
    
        @methods("GET", "PUT")
        def object(self, request, pk, *args, **kwargs):
            # stuff that needs a reference to self...
            pass
    

    Is there a better solution than to use exception handling?

    0 讨论(0)
  • 2020-12-05 01:57

    To expand on the __get__ approach. This can be generalized into a decorator decorator.

    class _MethodDecoratorAdaptor(object):
        def __init__(self, decorator, func):
            self.decorator = decorator
            self.func = func
        def __call__(self, *args, **kwargs):
            return self.decorator(self.func)(*args, **kwargs)
        def __get__(self, instance, owner):
            return self.decorator(self.func.__get__(instance, owner))
    
    def auto_adapt_to_methods(decorator):
        """Allows you to use the same decorator on methods and functions,
        hiding the self argument from the decorator."""
        def adapt(func):
            return _MethodDecoratorAdaptor(decorator, func)
        return adapt
    

    In this way you can just make your decorator automatically adapt to the conditions it is used in.

    def allowed(*allowed_methods):
        @auto_adapt_to_methods
        def wrapper(func):
            def wrapped(request):
                if request not in allowed_methods:
                    raise ValueError("Invalid method %s" % request)
                return func(request)
            return wrapped
        return wrapper
    

    Notice that the wrapper function is called on all function calls, so don't do anything expensive there.

    Usage of the decorator:

    class Foo(object):
        @allowed('GET', 'POST')
        def do(self, request):
            print "Request %s on %s" % (request, self)
    
    @allowed('GET')
    def do(request):
        print "Plain request %s" % request
    
    Foo().do('GET')  # Works
    Foo().do('POST') # Raises
    
    0 讨论(0)
提交回复
热议问题