如何使用装饰器检查参数

0 投票
3 回答
2368 浏览
提问于 2025-04-20 18:03

我有几个函数想要设置默认参数。因为这些默认值应该是新创建的对象,所以我不能直接在参数列表中设置它们。这样做会让代码变得很繁琐,因为我需要把默认值设为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 个回答

1

为什么不把你的装饰器改成接受一个字典形式的参数和你想要的默认值呢?我觉得这样做并不会比用多个不同参数的装饰器更麻烦。下面是一个对我来说很好用的示例。我对你的代码做了一些小改动,让它可以接受字典作为参数:

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
1

你可以在包装函数里加一个变量,用来存放参数的规格,比如叫做 __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

关于Python中的装饰器,这可能是你能得到的最好的建议:使用 wrapt.decorator

这样写装饰器会简单很多(你不需要在一个函数里面再写一个函数),而且可以避免大家在写简单装饰器时常犯的错误。如果你想了解这些错误是什么,可以看看Graham Dumpleton的 博客文章,标题是“你实现的Python装饰器是错的”(他的观点很有说服力)。

撰写回答