【问题标题】:How to use a decorator to check arguments如何使用装饰器检查参数
【发布时间】:2014-11-03 08:58:26
【问题描述】:

我有几个要提供默认参数的函数。由于这些默认值应该是新初始化的对象,我不能只将它们设置为参数列表中的默认值。它引入了很多笨拙的代码来将它们默认为 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 不会在第二个装饰器中提供函数的参数

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

谢谢!

【问题讨论】:

    标签: python python-2.7 python-decorators


    【解决方案1】:

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

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

    【讨论】:

      【解决方案2】:

      您可以在包含 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
      

      【讨论】:

        【解决方案3】:

        为什么不改变你的装饰器来接受一个参数字典和你想要的默认值呢?我不明白为什么它比具有不同参数的多个装饰更干净。这是一个对我来说很好的示例。我对您的代码进行了细微更改,以将字典反映为参数: 进口检查

        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
        

        【讨论】:

          猜你喜欢
          • 2013-02-24
          • 2014-10-15
          • 2021-04-14
          • 2022-08-15
          • 2018-01-12
          • 2019-12-05
          • 2015-06-25
          • 2014-07-19
          • 2019-07-29
          相关资源
          最近更新 更多