views:

191

answers:

4

I am trying to write a decorator that gets a single arg, i.e

@Printer(1)
def f():
    print 3

So, naively, I tried:

class Printer:
    def __init__(self,num):
         self.__num=num
    def __call__(self,func):
         def wrapped(*args,**kargs):
              print self.__num
              return func(*args,**kargs**)
         return wrapped

This is ok, but it also works as a decorator receiving no args, i.e

@Printer
def a():
   print 3

How can I prevent that?

+1  A: 

Are you sure it works without arguments? If I leave them out I get this error message:

Traceback (most recent call last):
  File "/tmp/blah.py", line 28, in ?
    a()
TypeError: __call__() takes exactly 2 arguments (1 given)

You could try this alternative definition, though, if the class-based one doesn't work for you.

def Printer(num):
    def wrapper(func):
        def wrapped(*args, **kwargs):
            print num
            return func(*args, **kwargs)
        return wrapped

    return wrapper
John Kugelman
The decorating itself works. After it, `a` just isn't a function anymore, but an instance of `Printer`.
balpha
Right. I was just flummoxed by the "it also works as a decorator receiving no args" statement. I guess I have a different definition of "works"!
John Kugelman
+4  A: 

Well, it's already effectively prevented, in the sense that calling a() doesn't work.

But to stop it as the function is defined, I suppose you'd have to change __init__ to check the type of num:

def __init__(self,num):
    if callable(num):
        raise TypeError('Printer decorator takes an argument')
    self.__num=num

I don't know if this is really worth the bother, though. It already doesn't work as-is; you're really asking to enforce the types of arguments in a duck-typed language.

Eevee
This is not ideal if you wish to initialise the class with a function as the only argument.
Markus
Right, that's another reason this isn't a good idea, but I assumed "num" is probably not meant to be a function in this case. (Hm. Passing a function to a function to create a function to decorate a function?)
Eevee
There's nothing at all wrong with doing this. Python is a loosely-typed language, but that doesn't mean there's anything wrong with making manual type checks when it prevents errors, particularly when the errors manufest in unobvious ways, as it does here.
Glenn Maynard
+1  A: 

The decorator is whatever the expression after @ evaluates to. In the first case, that's an instance of Printer, so what happens is (pretty much) equivalent to

decorator = Printer(1) # an instance of Printer, the "1" is given to __init__

def f():
    print 3
f = decorator(f) # == dec.__call__(f) , so in the end f is "wrapped"

In the second case, that's the class Printer, so you have

decorator = Printer # the class

def a():
   print 3
a = decorator(a) # == Printer(a), so a is an instance of Printer

So, even though it works (because the constructor of Printer takes one extra argument, just like __call__), it's a totally different thing.

The python way of preventing this usually is: Don't do it. Make it clear (e.g. in the docstring) how the decorator works, and then trust that people do the right thing.

If you really want the check, Eevee's answer provides a way to catch this mistake (at runtime, of course---it's Python).

balpha
Punishing predictable typos with obscure, delayed stack traces is "Pythonic"? I hate that buzzword.
Glenn Maynard
@Glenn Maynard: That's nice of you to share, but why are you doing it here? I certainly didn't use your buzzword.
balpha
Also, this case isn't more obscure than any other case of accessing a function [`x = f`] vs. actually calling it [`x = f()`]. And as to the "delay", by which I assume you mean the fact that the error is only caught at runtime---well, that's the nature of dynamic languages.
balpha
+1  A: 

I can't think of an ideal answer, but if you force the Printer class to be instantiated with a keyword argument, it can never try to instantiate via the decorator itself, since that only deals with non-keyword arguments:

def __init__(self,**kwargs):
     self.__num=kwargs["num"]

...

@Printer(num=1)
def a():
    print 3
Markus