Sam King
Sam King

Reputation: 2188

How to use a decorator to check arguments

I have several functions that I want to give default arguments. Since these defaults should be newly-initialized objects, I can't just set them as defaults in the argument list. It introduces a lot of clunky code to default them to None and check in every single function

def FunctionWithDefaults(foo=None):
  if foo is None:
    foo = MakeNewObject()

To reduce repeated code, I made a decorator to initialize parameters.

@InitializeDefaults(MakeNewObject, 'foo')
def FunctionWithDefaults(foo=None):
  # If foo wasn't passed in, it will now be equal to MakeNewObject()

The decorator I wrote is as such:

def InitializeDefaults(initializer, *parameters_to_initialize):
  """Returns a decorator that will initialize parameters that are uninitialized.

  Args:
    initializer: a function that will be called with no arguments to generate
        the values for the parameters that need to be initialized.
    *parameters_to_initialize: Each arg is the name of a parameter that
        represents a user key.
  """
  def Decorator(func):
    def _InitializeDefaults(*args, **kwargs):
      # This gets a list of the names of the variables in func.  So, if the
      # function being decorated takes two arguments, foo and bar,
      # func_arg_names will be ['foo', 'bar'].  This way, we can tell whether or
      # not a parameter was passed in a positional argument.
      func_arg_names = inspect.getargspec(func).args
      num_args = len(args)
      for parameter in parameters_to_initialize:
        # Don't set the default if parameter was passed as a positional arg.
        if num_args <= func_arg_names.index(parameter):
          # Don't set the default if parameter was passed as a keyword arg.
          if parameter not in kwargs or kwargs[parameter] is None:
            kwargs[parameter] = initializer()
      return func(*args, **kwargs)
    return _InitializeDefaults
  return Decorator

That works great if I only need to decorate it once, but it breaks with multiple decorations. Specifically, suppose I want to initialize foo and bar to separate variables:

@InitializeDefaults(MakeNewObject, 'foo')
@InitializeDefaults(MakeOtherObject, 'bar')
def FunctionWithDefaults(foo=None, bar=None):
  # foo should be MakeNewObject() and bar should be MakeOtherObject()

That breaks because in the second decorator, func_arg_defaults gets the arguments of the first decorator rather than FunctionWithDefaults. As a result, I can't tell whether or not a parameter has already been passed in positionally.

Is there any clean way to solve this problem?

Things that don't work:

I could just use one decorator, but that seems less clean, and it feels like there should be a cleaner solution.

Thanks!

Upvotes: 0

Views: 2410

Answers (3)

user3885927
user3885927

Reputation: 3503

Why not change your decorator to accept a dictionary of arguments and the default value that you want? I don't see why it would be less cleaner than multiple decorations with different arguments. Here is a sample that works fine for me. I made minor changes to your code to reflect dictionary as an argument: import inspect

class Person:
    def __init__(self):
        self.name = 'Personality'


class Human(Person):
    def __init__(self):
        self.name = 'Human'


class Animal(Person):
    def __init__(self):
        self.name = 'Animal'

#def InitializeDefaults(**initializerDict):
def InitializeDefaults(**initializerDict):
    """Returns a decorator that will initialize parameters that are uninitialized.

    Args:
      initializer: a function that will be called with no arguments to generate
          the values for the parameters that need to be initialized.
      *parameters_to_initialize: Each arg is the name of a parameter that
          represents a user key.
    """

    def Decorator(func):
        def _InitializeDefaults(*args, **kwargs):
            # This gets a list of the names of the variables in func.  So, if the
            # function being decorated takes two arguments, foo and bar,
            # func_arg_names will be ['foo', 'bar'].  This way, we can tell whether or
            # not a parameter was passed in a positional argument.
            func_arg_names = inspect.getargspec(func).args
            num_args = len(args)
            for parameter in initializerDict:
                # Don't set the default if parameter was passed as a positional arg.
                if num_args <= func_arg_names.index(parameter):
                    # Don't set the default if parameter was passed as a keyword arg.
                    if parameter not in kwargs or kwargs[parameter] is None:
                        kwargs[parameter] = initializerDict[parameter]()
            return func(*args, **kwargs)
        return _InitializeDefaults
    return Decorator


#@InitializeDefaults(Human, 'foo')

@InitializeDefaults(foo = Human, bar = Animal)
def FunctionWithDefaults(foo=None, bar=None):
    print foo.name
    print bar.name


#test by calling with no arguments
FunctionWithDefaults()

#program output
Human
Animal

Upvotes: 1

shx2
shx2

Reputation: 64308

This might be the best advice you'd get about decorators in python: use wrapt.decorator.

It would make the task of writing decorators much simpler (you don't need to write a function inside a function inside a function), and would avoid all the errors which everybody makes when writing even simple decorators. To learn what those are, you might want to read Graham Dumpleton's blog posts titled "How you implemented your Python decorator is wrong" (he is very convincing)

Upvotes: 2

Jesse the Game
Jesse the Game

Reputation: 2630

You could add a variable to the wrapper function containing the argspec (e.g. __argspec):

def Decorator(func):

    # Retrieve the original argspec
    func_arg_names = getattr(func, '__argspec', inspect.getargspec(func).args)

    def _InitializeDefaults(*args, **kwargs):
        # yada yada

    # Expose the argspec for wrapping decorators
    _InitializeDefaults.__argspec = func_arg_names

    return _InitializeDefaults

Upvotes: 1

Related Questions