1

I have been experimenting with different kinds of decorators in Python and I am finding it difficult to understand Parameterized decorators. A decorator accepts callables and returns callable (callable being a function in my case) In the following code:-

def check_non_negative(index):
    def validator(f):
        def wrap(*args):
            if args[index]<0:
                raise ValueError(
                    'Argument {} must be non negative.'.format(index))
            return f(*args)
        return wrap
    return validator

@check_non_negative(1)
def create_list(value,size):
    return [value]*size


create_list('a',3)

Here I see that check_non_negative is not a decorator according to the definition but behaves like one (During run Validator is the actual decorator). check_non_negative takes an integer and not a callable, yet it behaves like decorator. Can somebody explain why?

Phani Teja
  • 163
  • 9

2 Answers2

4

check_non_negative is not technically a decorator by your definition, one might call it a 'decorator factory'. It returns validator, which is a decorator.

Basically whenever you have:

@<expression>
def ...

then <expression> must evaluate to a decorator, i.e. a callable which accepts a single argument that is also a callable.

Alex Hall
  • 34,833
  • 5
  • 57
  • 89
1

When you pass parameters to a decorator function (i.e. @check_non_negative(1)), the function is invoked with those parameters, and then it returns a decorator that takes and returns a function (like if you'd used @validator with no parameters).

It's easier to understand with type annotations IMO:

import functools
from typing import cast, Callable, List, TypeVar


_Elem = TypeVar('_Elem')
_Func = TypeVar('_Func', bound=Callable)


def check_non_negative(index: int) -> Callable[[_Func], _Func]:
    def validator(f: _Func) -> _Func:
        @functools.wraps(f)
        def wrap(*args, **kwargs):
            if args[index] < 0:
                raise ValueError(
                    'Argument {} must be non negative.'.format(index))
            return f(*args, **kwargs)
        return cast(_Func, wrap)
    return validator


@check_non_negative(1)
def create_list(value: _Elem, size: int) -> List[_Elem]:
    return [value]*size

So check_non_negative takes an int argument, and returns a function (validator) that takes a specific type of function (_Func, which here refers to the type of the decorated function, e.g. create_list) and returns the same type of function.

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • As Alex said above I agree with the fact that check_non_integer is a decorator factory that is returning a validator for me. – Phani Teja Apr 15 '20 at 16:34