9.16 args和*kwargs的强制参数签名

问题

你有一个函数或方法,它使用args和*kwargs作为参数,这样使得它比较通用,但有时候你想检查传递进来的参数是不是某个你想要的类型。

解决方案

对任何涉及到操作函数调用签名的问题,你都应该使用 inspect 模块中的签名特性。我们最主要关注两个类:SignatureParameter 。下面是一个创建函数前面的交互例子:

  1. >>> from inspect import Signature, Parameter
  2. >>> # Make a signature for a func(x, y=42, *, z=None)
  3. >>> parms = [ Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
  4. ... Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
  5. ... Parameter('z', Parameter.KEYWORD_ONLY, default=None) ]
  6. >>> sig = Signature(parms)
  7. >>> print(sig)
  8. (x, y=42, *, z=None)
  9. >>>

一旦你有了一个签名对象,你就可以使用它的 bind() 方法很容易的将它绑定到 args*kwargs 上去。下面是一个简单的演示:

  1. >>> def func(*args, **kwargs):
  2. ... bound_values = sig.bind(*args, **kwargs)
  3. ... for name, value in bound_values.arguments.items():
  4. ... print(name,value)
  5. ...
  6. >>> # Try various examples
  7. >>> func(1, 2, z=3)
  8. x 1
  9. y 2
  10. z 3
  11. >>> func(1)
  12. x 1
  13. >>> func(1, z=3)
  14. x 1
  15. z 3
  16. >>> func(y=2, x=1)
  17. x 1
  18. y 2
  19. >>> func(1, 2, 3, 4)
  20. Traceback (most recent call last):
  21. ...
  22. File "/usr/local/lib/python3.3/inspect.py", line 1972, in _bind
  23. raise TypeError('too many positional arguments')
  24. TypeError: too many positional arguments
  25. >>> func(y=2)
  26. Traceback (most recent call last):
  27. ...
  28. File "/usr/local/lib/python3.3/inspect.py", line 1961, in _bind
  29. raise TypeError(msg) from None
  30. TypeError: 'x' parameter lacking default value
  31. >>> func(1, y=2, x=3)
  32. Traceback (most recent call last):
  33. ...
  34. File "/usr/local/lib/python3.3/inspect.py", line 1985, in _bind
  35. '{arg!r}'.format(arg=param.name))
  36. TypeError: multiple values for argument 'x'
  37. >>>

可以看出来,通过将签名和传递的参数绑定起来,可以强制函数调用遵循特定的规则,比如必填、默认、重复等等。

下面是一个强制函数签名更具体的例子。在代码中,我们在基类中先定义了一个非常通用的 init() 方法,然后我们强制所有的子类必须提供一个特定的参数签名。

  1. from inspect import Signature, Parameter
  2.  
  3. def make_sig(*names):
  4. parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
  5. for name in names]
  6. return Signature(parms)
  7.  
  8. class Structure:
  9. __signature__ = make_sig()
  10. def __init__(self, *args, **kwargs):
  11. bound_values = self.__signature__.bind(*args, **kwargs)
  12. for name, value in bound_values.arguments.items():
  13. setattr(self, name, value)
  14.  
  15. # Example use
  16. class Stock(Structure):
  17. __signature__ = make_sig('name', 'shares', 'price')
  18.  
  19. class Point(Structure):
  20. __signature__ = make_sig('x', 'y')

下面是使用这个 Stock 类的示例:

  1. >>> import inspect
  2. >>> print(inspect.signature(Stock))
  3. (name, shares, price)
  4. >>> s1 = Stock('ACME', 100, 490.1)
  5. >>> s2 = Stock('ACME', 100)
  6. Traceback (most recent call last):
  7. ...
  8. TypeError: 'price' parameter lacking default value
  9. >>> s3 = Stock('ACME', 100, 490.1, shares=50)
  10. Traceback (most recent call last):
  11. ...
  12. TypeError: multiple values for argument 'shares'
  13. >>>

讨论

在我们需要构建通用函数库、编写装饰器或实现代理的时候,对于 args*kwargs 的使用是很普遍的。但是,这样的函数有一个缺点就是当你想要实现自己的参数检验时,代码就会笨拙混乱。在8.11小节里面有这样一个例子。这时候我们可以通过一个签名对象来简化它。

在最后的一个方案实例中,我们还可以通过使用自定义元类来创建签名对象。下面演示怎样来实现:

  1. from inspect import Signature, Parameter
  2.  
  3. def make_sig(*names):
  4. parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
  5. for name in names]
  6. return Signature(parms)
  7.  
  8. class StructureMeta(type):
  9. def __new__(cls, clsname, bases, clsdict):
  10. clsdict['__signature__'] = make_sig(*clsdict.get('_fields',[]))
  11. return super().__new__(cls, clsname, bases, clsdict)
  12.  
  13. class Structure(metaclass=StructureMeta):
  14. _fields = []
  15. def __init__(self, *args, **kwargs):
  16. bound_values = self.__signature__.bind(*args, **kwargs)
  17. for name, value in bound_values.arguments.items():
  18. setattr(self, name, value)
  19.  
  20. # Example
  21. class Stock(Structure):
  22. _fields = ['name', 'shares', 'price']
  23.  
  24. class Point(Structure):
  25. _fields = ['x', 'y']

当我们自定义签名的时候,将签名存储在特定的属性 signature 中通常是很有用的。这样的话,在使用 inspect 模块执行内省的代码就能发现签名并将它作为调用约定。

  1. >>> import inspect
  2. >>> print(inspect.signature(Stock))
  3. (name, shares, price)
  4. >>> print(inspect.signature(Point))
  5. (x, y)
  6. >>>

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p16_enforce_argument_signature_on_args_kwargs.html