如何使用装饰器来检查参数

时间:2014-09-09 20:28:16

标签: python python-2.7 python-decorators

我有几个函数要提供默认参数。由于这些默认值应该是新初始化的对象,因此我不能在参数列表中将它们设置为默认值。它引入了许多笨重的代码来将它们默认为None并检入每个函数

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

为了减少重复的代码,我做了一个装饰器来初始化参数。

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

我写的装饰者是这样的:

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

如果我只需要装饰一次就行得很好,但是它会打破多个装饰。具体来说,假设我想初始化foo和bar来分隔变量:

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

因为在第二个装饰器中,func_arg_defaults获取第一个装饰器而不是FunctionWithDefaults的参数。结果,我无法判断参数是否已经在位置上传递过。

有没有什么干净的方法来解决这个问题?

不起作用的事情:

  • functools.wraps(func)使函数的__name__成为装饰函数,但它不会更改参数(除非有不同的方法)。
  • inspect.getcallargs不会在第二个装饰器中引发异常
  • 使用func.func_code.co_varnames不在第二个装饰器中提供函数的参数

我可以使用一个装饰器,但这看起来不太干净,感觉应该有更清洁的解决方案。

谢谢!

3 个答案:

答案 0 :(得分:2)

这可能是您在python中获得有关装饰器的最佳建议:使用wrapt.decorator

这将使编写装饰器的任务变得更加简单(您不需要在函数内部的函数内编写函数),并且可以避免在编写甚至简单的装饰器时所有人都犯的错误。要了解它们是什么,您可能需要阅读Graham Dumpleton的blog posts标题为&#34;您如何实现您的Python装饰器是错误的&#34; (他非常有说服力)

答案 1 :(得分:1)

您可以向包含argspec的包装函数添加变量(例如__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

答案 2 :(得分:0)

为什么不更改装饰器以接受参数字典和您想要的默认值?我不明白为什么它会比具有不同参数的多个装饰更不干净。这是一个适合我的样本。我对您的代码进行了小的更改,以反映字典为参数:     导入检查

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