检查Python中函数的参数

时间:2015-04-11 22:02:56

标签: python function

例如,我有一个产生程序噪音的函数

def procedural_noise(width, height, seed):
   ...

此功能的所有参数均应为正。我想,如果on参数是非正的,我需要检查它并抛出异常。它是一种好的(pythonic方式)方法吗?

让我们假设,我是对的。哪个是检查参数的最佳方法?


我可以为每个参数编写检查器:

def procedural_noise(width, height, seed):
    if width <= 0:
        raise ValueError("Width should be positive")
    if height <= 0:
        raise ValueError("Height should be positive")
    if seed <= 0:
        raise ValueError("Seed should be positive")
    ...
对于程序员来说,当他得到例外时应该清楚,他应该纠正什么,但在我看来它并不好看。

以下代码更容易,但距离理想太远:

def procedural_noise(width, height, seed):
    if width <= 0 or height <= 0 or seed <= 0:
        raise ValueError("All of the parameters should be positive")
    ...

最后一个问题:哪个是使用unittest框架编写测试的最佳方法,它检查参数类型及其值?

我可以在测试类中编写以下函数:

def test_positive(self):
    self.assertRaises(ValueError, main.procedural_noise, -10, -10, 187)

这是一个正确的解决方案吗?


UPD:我赞成所有答案,因为他们每个人都有一个有用的信息,但我无法选择他们的最佳答案(我想明天选择最多的投票问题是公平的。 )

6 个答案:

答案 0 :(得分:6)

这也可能是函数注释的一个很好的用例(在Python 3中)。例如:

import inspect
from functools import wraps    

def positive(name, value):
    if value < 0:
        raise ValueError(name + " must be positive")

def check_params(f):
    signature = inspect.signature(f)
    @wraps(f)
    def wrapper(*args, **kwargs):
        bound_arguments = signature.bind(*args, **kwargs)
        for name, value in bound_arguments.arguments.items():
            annotation = signature.parameters[name].annotation
            if annotation is inspect.Signature.empty:
                continue
            annotation(name, value)
        return f(*args, **kwargs)
    return wrapper

@check_params
def procedural_noise(width: positive, height: positive, seed: positive):
    pass # ...

check_params装饰器(受github.com/ceronman/typeannotations启发)中有一些inspect-fu,但它提供了非常好用且灵活的方法来检查函数参数 - 没有任何if s, for或类型检查函数中的其他噪声代码。

答案 1 :(得分:3)

我会这样做:

def procedural_noise(width, height, seed):
    check_positive(width, "width")
    check_positive(height, "height")
    check_positive(seed, "seed")

def check_positive(value, name):
    if value < 0:
        raise ValueError(name + " must be positive")

另一个想法 - 一点点“黑客”:

def procedural_noise(width, height, seed):
    check_positive(width=width)
    check_positive(height=height)
    check_positive(seed=seed)

def check_positive(**kwargs):
    for name, value in kwargs.items():
        if value < 0:
            raise ValueError(name + " must be positive")

也可以这样称呼:

def procedural_noise(width, height, seed):
    check_positive(width=width, height=height, seed=seed)

这与其他答案几乎相同,但这样原始函数procedural_noise保持相当清晰,除了非常基本的信息之外的任何参数处理。它更像语义:)

答案 2 :(得分:2)

关于第一个问题,使用inspect模块:

import inspect

def procedural_noise(width, height, seed):
    frame = inspect.currentframe()
    args, _, _, values = inspect.getargvalues(frame)

    for name in args:
        if values[name] < 0:
             raise ValueError(name + " should be positive")

procedural_noise(3, -66, 2)

输出:

Traceback (most recent call last):
  File "C:\Users\Sam\Desktop\hack.py", line 10, in <module>
    procedural_noise(3, -6, 2)
  File "C:\Users\Sam\Desktop\hack.py", line 8, in procedural_noise
    raise ValueError(name + " should be positive")
ValueError: height should be positive

否则,您也可以这样使用dictionary packing

def procedural_noise(**params):
    for name in params.keys():
        if params[name] < 0:
             raise ValueError(name + " should be positive")

procedural_noise(width=3, height=6, seed=-2)

输出:

Traceback (most recent call last):
  File "...\hack.py", line 6, in <module>
    procedural_noise(width=3, height=6, seed=-2)
  File "...\hack.py", line 4, in procedural_noise
    raise ValueError(name + " should be positive")
ValueError: seed should be positive

答案 3 :(得分:1)

尝试类似

的内容
def procedural_noise(width, height, seed):
    for key,val in locals().items():
        if val < 0:
             raise ValueError(key + " should be positive")

答案 4 :(得分:1)

如果你发现自己做了很多,也许装饰工具会让你的代码更具可读性:

def assert_positive(f):
    def wrapper(*args, **kwargs):
        for i, v in enumerate(args):
            if v < 0:
                raise ValueError('The parameter at position %d should be >= 0' % i)
        for k, v in kwargs.items():
            if v < 0:
                raise ValueError('The parameter %s should be >= 0' % k)
        return f(*args, **kwargs)
    return wrapper

然后你可以像这样声明你的函数:

@assert_positive
def procedural_noise(width, height, seed=0):
    ...

它会引发这样的例外:

>>> procedural_noise(0,-1,0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "argument_checking.py", line 5, in wrapper
    raise ValueError('The parameter at position %d should be >= 0' % i)
ValueError: The parameter at position 1 should be >= 0
>>> procedural_noise(0,0,seed=-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "argument_checking.py", line 8, in wrapper
    raise ValueError('The parameter %s should be >= 0' % k)
ValueError: The parameter seed should be >= 0

pythonic方式通常不会过多检查你的论点,但有反例。内置函数的例子完全不同:

  • range(-1) - 看起来很有趣,但很好,返回[]
  • time.sleep(-1) - 与IOError: [Errno 22] Invalid argument崩溃,我认为这只是Python的一种说法,即系统调用返回错误。也许我很幸运,系统调用会进行参数检查......
  • chr(-1) - ValueError: chr() arg not in range(256)

答案 5 :(得分:0)

您可以使用all内置 - all获取可迭代,如果迭代中的所有项都是真实的,则返回True。因此,对于您的示例,由于所有正数都是真实的,您可以写:

def procedural_noise(width, height, seed):
  if all((width, height, seed)):
    # Do stuff
  else:
    raise ValueError("You biffed it.")

请注意双重 - 这是因为我将元组(width, height, seed)传递给all - 我不能只写all(width, height, seed),因为all需要一个可迭代的参数,就像一个元组。


关于测试问题,假设定义了procedural_noise的模块的名称命名为foo,那么在测试文件中,您可以将assertRaiseswith一起使用{1}}关键字,如下所示:

import foo
import unittest

class Test(unittest.TestCase):
  def test_positive(self):
    with self.assertRaises(ValueError):
      foo.procedural_noise(-10, -10, 187)

unittest docs了解详情。