使用装饰器处理函数参数

Handling a function argument with a decorator

核心,我想做的是采用一些看起来像这样的函数未修饰的验证函数:

def f(k: bool):
    def g(n):
        # check that n is valid
        return n
    return g

并使它们看起来像这样装饰验证函数:

@k
def f():
    def g(n):
        # check that n is valid
        return n
    return g

这里的想法是 k 描述了所有实现功能的相同功能。

具体来说,这些函数都返回 'validation' 函数以供 voluptuous validation framework 使用。所以所有 f() 类型的函数都返回一个稍后由 Schema() 执行的函数。 k 实际上是 allow_none,也就是说一个标志,用于确定 None 值是否正常。一个非常简单的示例可能是 示例使用代码:

x = "Some input value."
y = None
input_validator = Schema(f(allow_none=True))
x = input_validator(x)  # succeeds, returning x
y = input_validator(y)  # succeeds, returning None
input_validator_no_none = Schema(f(allow_none=False))
x = input_validator(x)  # succeeds, returning x
y = input_validator(y)  # raises an Invalid

在不更改示例使用代码的情况下 我试图通过将未修饰的验证函数更改为修饰的验证函数来获得相同的结果。举个具体的例子,改这个:

def valid_identifier(allow_none: bool=True):
    min_range = Range(min=1)
    validator = Any(All(int, min_range), All(Coerce(int), min_range))
    return Any(validator, None) if allow_none else validator

为此:

@allow_none(default=True)
def valid_identifier():
    min_range = Range(min=1)
    return Any(All(int, min_range), All(Coerce(int), min_range))

这两个返回的函数应该是等价的

我试图写的是这个,利用 decorator 库:

from decorator import decorator

@decorator
def allow_none(default: bool=True):
    def decorate_validator(wrapped_validator, allow_none: bool=default):
        @wraps(wrapped_validator)
        def validator_allowing_none(*args, **kwargs):
            if allow_none:
                return Any(None, wrapped_validator)
            else:
                return wrapped_validator(*args, **kwargs)
        return validator_allowing_none
    return decorate_validator

我有一个 unittest.TestCase 来测试它是否按预期工作:

@allow_none()
def test_wrapped_func():
    return Schema(str)

class TestAllowNone(unittest.TestCase):

    def test_allow_none__success(self):
        test_string = "blah"

        validation_function = test_wrapped_func(allow_none=False)
        self.assertEqual(test_string, validation_function(test_string))
        self.assertEqual(None, validation_function(None))

但是我的测试returns以下失败:

    def validate_callable(path, data):
        try:
>           return schema(data)
E           TypeError: test_wrapped_func() takes 0 positional arguments but 1 was given

我试过调试这个,但无法让调试器真正进入装饰。我怀疑由于命名问题,例如在 this (very lengthy) blog post series 中提出的问题,test_wrapped_func 没有正确设置它的参数列表,所以装饰器甚至从未执行过,但它也可能是其他原因完全。

我尝试了一些其他变体。通过从 @allow_none:

中删除函数括号
@allow_none
def test_wrapped_func():
    return Schema(str)

我得到一个不同的错误:

>       validation_function = test_wrapped_func(allow_none=False)
E       TypeError: test_wrapped_func() got an unexpected keyword argument 'allow_none'

删除 @decorator 失败:

>       validation_function = test_wrapped_func(allow_none=False)
E       TypeError: decorate_validator() missing 1 required positional argument: 'wrapped_validator'

这是有道理的,因为 @allow_none 需要一个参数,因此逻辑上需要括号。替换它们会给出原始错误。

装饰器很微妙,我显然在这里遗漏了一些东西。这类似于柯里化函数,但效果不佳。关于如何实施,我缺少什么?

我认为您将 allow_none=default 参数置于错误的嵌套级别。它应该在最里面的函数(包装器)上,而不是装饰器(中间层)。

尝试这样的事情:

def allow_none(default=True):    # this is the decorator factory
    def decorator(validator):    # this is the decorator
        @wraps(validator)
        def wrapper(*args, allow_none=default, **kwargs):    # this is the wrapper
            if allow_none:
                return Any(None, validator)
            else:
                return validator(*args, **kwargs)
        return wrapper
    return decorator

如果您不需要设置默认值,您可以摆脱最外层的嵌套,只需在包装函数中将默认值设置为常量(如果您的调用者总是传递一个常量,则忽略它)价值)。请注意,正如我在上面所写的那样,包装器的 allow_none 参数是一个仅限关键字的参数。如果你想将它作为位置参数传递,你可以将它移到 *args 之前,但这要求它是第一个位置参数,从 API 的角度来看这可能是不可取的。更复杂的解决方案可能是可能的,但这个答案太过分了。