spencer
spencer

Reputation: 189

Arguments to a decorator without using classes

def tracer(func, enabled=True):
    def wrap(*args, **kwargs):
        if enabled:
            print('Calling {}'.format(func))
        return func(*args, **kwargs)
    return wrap


@tracer(enabled=False)
def rotate_list(l):
    return l[1:] + [l[0]]

I'm having a bit of confusion as to why this doesn't work, specifically this part: @tracer(enabled=False)

What I understand is happening here is: The function object rotate_list is passed as an argument whenever tracer performs a call.

I think the reason this doesn't work is because tracer(and any wrapper for that matter) only accepts callable objects, enabled=False isn't a callable so it doesn't work.

The error message however is not very indicative of that, so I'm wondering why the error message was: TypeError: tracer() missing 1 required positional argument: 'func'

I guess the arguments inside the parentheses were evaluated first, such that no callable object was passed to tracer?

I guess this can be solved by using class decorators, doing something like

class Trace:
    def __init__(self, enabled=False):
        print('Inside __init__')
        self.enabled = enabled

then tracer = Trace(enabled=True) would work, but I'd like to see how this can be solved without using classes.

============

EDIT (Solution): Don't mind this, just typing it out to ensure I understood the solution. Since placing an argument inside a decorator makes it act like a normal function. The solution would be to make that decorator return another callable object (which is the actual decorator).

Like: @dec def foo: pass would become foo = dec(foo)

@dec(ARG) def foo: pass would become foo = dec(ARG)(foo)

The solution would be to make dec return another callable which is the actual decorator. For example that function would be wrap

foo = dec(ARG)(foo) would become foo = wrap(foo) where ARG was already passed to dec. Thanks guys! I love functional programming.

Upvotes: 0

Views: 226

Answers (2)

cmdLP
cmdLP

Reputation: 1856

To pass other parameters than the function to a decorator-function you nest multiple defs:

def my_decorator(flagDoThat)
    def internal(func):
        def wrapper(*argv, **kwv):
            retval = func(*argv, **kwv)
            if flagDoThat:
                print("Hello", retval)
            return retval

        wrapper.__name__ = func.__name__ #update name
        return wrapper
    return internal

@my_decorator(True)
def my_func(): return "world"

#equals to

tmp = my_decorator(True)

@tmp
def my_func(): return "world"

Edit

This decorator helps to build other decorators. While this contains multiple nested functions, it allows you to define decorators with only two layers and arguments, like you did:

def decorator(keepFunctionName=True):
    def internal(func):
        def newFunc(*argv, **kwv):
            def decoWrapper(theFuncUsedInFunc):
                fRet = func(theFuncUsedInFunc, *argv, **kwv)
                if keepFunctionName:
                    fRet.__name__ = theFuncUsedInFunc.__name__
                return fRet
            return decoWrapper
        return newFunc
    return internal

And can be used like this:

@decorator()
def my_decorator(func, flagDoThat):
    def wrapper(*argv, **kwv):
        retval = func(*argv, **kwv)
        if flagDoThat:
            print("Hello", retval)
        return retval

    return wrapper

And this decorator does exactly that what the decorator above does.

Edit II

You can do the same thing with classes, by attaching the decorator to the init-function.

But here is an other way you can make decorators with parameters by storing them in classes:

class my_decorator:
    __slots__ = ["flagDoThat"] # optional line
    def __init__(self, flagDoThat):
        self.flagDoThat = flagDoThat

    def __call__(self, func):
        def wrapper(*argv, **kwv):
            retval = func(*argv, **kwv)
            if self.flagDoThat:
                print("Hello", retval)
            return retval

        return wrapper

Upvotes: 1

del-boy
del-boy

Reputation: 3654

It can be done only with functions. tracer should look like this:

def tracer(enabled=False):
    def wrapper(func):
        def wrap(*args, **kwargs):
            if enabled:
                print('Calling {}'.format(func))
            return func(*args, **kwargs)
        return wrap
    return wrapper

What is done here is that you created function (tracer) that returns decorator. That decorator accepts a function.

If you translate it to same format as python does for any decorator, you will see why this is required.

Every time python sees

@dec
def foo(): pass

it translates it to:

def foo(): pass
foo = dec(foo)

So, when you have decorator that requires arguments, it is translated like this:

foo = dec(ARGS)(foo)

So, what you need to do is make sure that decorator returns something that accepts function as its parameter.

PS: It is nice to use functools.wraps for decorator to preserve function name, docstrings, etc.

Upvotes: 1

Related Questions