views:

241

answers:

2

I'm excited to see the latest version of the decorator python module (3.0). It looks a lot cleaner (e.g. the syntax is more sugary than ever) than previous iterations.

However, it seems to have lousy support (e.g. "sour" syntax, to horribly stretch the metaphor) for decorators that take arguments themselves. Does anyone have a good example for how you'd cleanly do this using decorator 3.0?

 def substitute_args(fun, arg_sub_dict):
      def wrapper(arg):
         new_arg = arg_sub_dict.get(arg, arg)
         return fun(new_arg)

      # some magic happens here to make sure that type signature, 
      # __name__, __doc__, etc. of wrapper matches fun

 return wrapper
+5  A: 

In this case, you need to make your function return the decorator. (Anything can be solved by another level of indirection...)

from decorator import decorator
def substitute_args(arg_sub_dict):
  @decorator
  def wrapper(fun, arg):
    new_arg = arg_sub_dict.get(arg, arg)
    return fun(new_arg)
  return wrapper

This means substitute_args isn't a decorator itself, it's a decorator factory. Here's the equivalent without the decorator module.

def substitute_args(arg_sub_dict):
  def my_decorator(fun):
    def wrapper(arg):
      new_arg = arg_sub_dict.get(arg, arg)
      return fun(new_arg)
    # magic to update __name__, etc.
    return wrapper
  return my_decorator

Three levels deep isn't very convenient, but remember two of them are when the function is defined:

@substitute_args({}) # this function is called and return value is the decorator
def f(x):
  return x
# that (anonymous) decorator is applied to f

Which is equivalent to:

def f(x):
  return x
f = substitude_args({})(f) # notice the double call
Roger Pate
this was hugely helpful. thanks!
YGA
A: 

here is another way i have just discovered: check whether the first (and only) argument to your decorator is callable; if so, you are done and can return your behavior-modifying wrapper method (itself decorated with functools.wraps to preserve name and documentation string).

in the other case, one or more named or positional arguments should be present; you can collect those arguments and return a callable that accepts a callable as first argument and returns a wrapper method—and since that description fits the description of the decorator method, return that very decorator method! i’ve used functools.partial here to get a version of my decorator, is_global_method (which i’m working on right now—its implementation is of course nonsense as shown below, this is only to demonstrate the decoration works).

this solution appears to work but sure needs more testing. if you quint our eyes, you can see that the trick is only three or four lines as a pattern to remember. now i wonder whether i can wrap that kind of functionality into another decorator? ah, the metaness of it!

from functools import wraps
from functools import partial

_               = print
is_instance_of  = isinstance
is_callable     = lambda x: hasattr( x, '__call__' )

def is_global_method( x, *, name = None ):
  if is_callable( x ):
    @wraps( x )
    def wrapper( *P, **Q ):
      return { 'name': name, 'result': x( *P, **Q ), }
    return wrapper
  # assert is_instance_of( x, str ) # could do some sanity checks here
  return partial( is_global_method, name = x )

@is_global_method
def f( x ):
  """This is method f."""
  return x ** 2

@is_global_method( 'foobar' )
def g( x ):
  """This is method g."""
  return x ** 2

_( f.__name__ )
_( f.__doc__ )
_( f( 42 ) )
_( g.__name__ )
_( g.__doc__ )
_( g( 42 ) )
flow