2
\$\begingroup\$

I have assignment to generate argument based decorator I wrote like this.

def my_decorator(*deco_args): 
    def wrap(f):
        def wrapped_f(*func_args):
            if len(deco_args) == 1 and len(func_args) ==1:
                return f(func_args[0], deco_args[0])
            elif len(deco_args) == 2 and len(func_args) == 2:
                return f(*(deco_args+func_args))
        return wrapped_f
    return wrap

@my_decorator(2)
def pow(x, y): 
    return x ** y

print pow(4) #=> 16

@my_decorator("/home", "user")
def path(*args):
   return "/".join(args)

print path("videos", "cats.png") #=> "/home/user/videos/cats.png"

Is there any better way to do this ?

\$\endgroup\$
  • \$\begingroup\$ What exactly was your assignment? Why does func_args sometimes precede deco_args and sometimes func_args follow deco_args? Perhaps you should calculate 2 ** 4 instead of 4 ** 2? \$\endgroup\$ – 200_success Apr 29 '16 at 16:56
3
\$\begingroup\$

I don't think my_decorator is a very descriptive name, but maybe that's just part of the assignment. Also, f isn't a very descriptive name for the function you are decorating.

I did my own test:

>>> @my_decorator(2)
... def here(*args):
...     print args
... 
>>> here(4, 5)
>>> 

Do you know why it doesn't print anything? It's because you have if and elif in wrapped_f, but one argument to my_decorator and two arguments to here doesn't match either of your conditions, so wrapped_f doesn't know what to do. To be safe, it does not call the function and just returns None. That always happens when you don't tell a function what to return: it returns None. My question is, why do you have those conditions? I would change that function to:

def wrapped_f(*func_args):
    return f(*(deco_args+func_args))

Your code really has only four lines of reviewable code, so I will just suggest a new (not necessarily better) way:

from functools import partial

class MyDecorator:
    def __init__(self, *args):
        self.args = args
    def __call__(self, func):
        return partial(func, *self.args)

functools.partial takes a function and any other arguments and returns a callable object that, when called, calls the function with the original arguments and whatever other arguments are given. Our decorator is called with 2 for my above example, so it sets self.args to (2,). When our MyDecorator object is called (with here as an argument), the __call__ method is executed and we return a partial object with that function and the arguments we got earlier. Therefore, when the result is called with more arguments, it calls the function with the arguments originally given to the decorator as well as the arguments being given currently. You can see that partial is very similar to your decorator. We just needed to make it decorator-compatible.


Edit: As mentioned by @JoeWallis in the comments, you could still use partial() in nested functions similar to yours:

def my_decorator(*deco_args):
    def wrap(f):
        return partial(f, *deco_args)
    return wrap
\$\endgroup\$
  • \$\begingroup\$ I used that condition because, if you see, in pow we are passing 2 arguments, but when call, it passed only one argument. Second argument I passed from decorator argument. Thanks for answer. \$\endgroup\$ – Lafada Apr 29 '16 at 16:40
  • \$\begingroup\$ @Lafada: When you pass 2 as an argument, deco_args will be (2,). When you pass 4 later, func_args will be (4,). Consequently, deco_args + func_args will be (2, 4) and f(*(deco_args + func_args)) will be pow(2, 4). \$\endgroup\$ – zondo Apr 29 '16 at 16:43
  • 1
    \$\begingroup\$ f seems like a very reasonable name for a completely generic function. \$\endgroup\$ – 200_success Apr 29 '16 at 16:57
  • \$\begingroup\$ @200_success: Reasonable, but not optimal IMO. I prefer to use names that are unambiguous. f could be used for anything that starts with "f". I prefer func, function, wrapped, wrapped_func, etc. \$\endgroup\$ – zondo Apr 29 '16 at 17:01
  • \$\begingroup\$ @zondo Fair enough, I find this easier to read (Don't want to post an answer), you don't. I'll clean up my other comments \$\endgroup\$ – Peilonrayz Apr 29 '16 at 17:14
1
\$\begingroup\$

Please pardon my delay, I just thought I had something very similar which may help tremendously.

I think I have a much similar implementation except with predicates instead of argument counts and more arbitrariness with the calls of functions.

In terms of my thing, I think yours can be written as follows:

def my_decorator(n_args):
    #see the similar thing to understand partial_til_cond's implementation
    #it's a wrapper that raises a TypeError if the second lambda is True,
    #stores previous arguments if the first lambda is True (both for the entire arg list),
    #and calls the wrapped function otherwise.
    return partial_til_cond(lambda x: len(x) < 2 * n_args, 
                            lambda x: len(x) not in [n_args, 2 * n_args])

pow = my_decorator(2)(lambda x,y: x ** y)(2)
path = my_decorator(4)(lambda *files: "/".join(files))("home", "user")

pow and path were written on their own lines to hide the underlying lambdas.

Also, your code returns None in the cases where there aren't 2 or 4 total arguments. Mine will raise a TypeError. Yours seems to only handle two or four arguments while this would take care of any even number.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.