基于 Validator 类实现 ParamValidator,用于校验函数参数

简介: 基于 Validator 类实现 ParamValidator,用于校验函数参数

一、前置说明


1、本节目标

  • 了解 __getattr__ 的特性。
  • 了解 __call__ 的用法。
  • 了解如何在一个类中动态的使用另一个类中的方法。


2、相关回顾


二、操作步骤


1、项目目录


  • atme : @me 用于存放临时的代码片断或其它内容。
  • pyparamvalidate : 新建一个与项目名称同名的package,为了方便发布至 pypi
  • core : 用于存放核心代码。
  • tests : 用于存放测试代码。
  • utils : 用于存放一些工具类或方法。


2、代码实现

atme/demo/validator_v5/validator.py

import functools
import inspect
from typing import TypeVar
def _error_prompt(value, exception_msg=None, rule_des=None, field=None):
    default = f'"{value}" is invalid.'
    prompt = exception_msg or rule_des
    prompt = f'{default} due to: {prompt}' if prompt else default
    prompt = f'{field} error: {prompt}' if field else prompt
    return prompt
def raise_exception(func):
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        bound_args = inspect.signature(func).bind(self, *args, **kwargs).arguments
        exception_msg = kwargs.get('exception_msg', None) or bound_args.get('exception_msg', None)
        error_prompt = _error_prompt(self.value, exception_msg, self._rule_des, self._field)
        result = func(self, *args, **kwargs)
        if not result:
            raise ValueError(error_prompt)
        return self
    return wrapper
class RaiseExceptionMeta(type):
    def __new__(cls, name, bases, dct):
        for key, value in dct.items():
            if isinstance(value, staticmethod):
                dct[key] = staticmethod(raise_exception(value.__func__))
            if isinstance(value, classmethod):
                dct[key] = classmethod(raise_exception(value.__func__))
            if inspect.isfunction(value) and not key.startswith("__"):
                dct[key] = raise_exception(value)
        return super().__new__(cls, name, bases, dct)
'''
- TypeVar 是 Python 中用于声明类型变量的工具
- 声明一个类型变量,命名为 'Self', 意思为表示类的实例类型
- bound 参数指定泛型类型变量的上界,即限制 'Self' 必须是 'Validator' 类型或其子类型
'''
Self = TypeVar('Self', bound='Validator')
class Validator(metaclass=RaiseExceptionMeta):
    def __init__(self, value, field=None, rule_des=None):
        self.value = value
        self._field = field
        self._rule_des = rule_des
    def is_string(self, exception_msg=None) -> Self:
        return isinstance(self.value, str)
    def is_not_empty(self, exception_msg=None) -> Self:
        return bool(self.value)


atme/demo/validator_v5/param_validator.py

import inspect
from functools import wraps
from typing import Callable
from atme.demo.validator_v5.validator import Validator
class ParameterValidator:
    def __init__(self, param_name: str, param_rule_des=None):
        """
        :param param_name: 参数名
        :param param_rule_des: 该参数的规则描述
        """
        self.param_name = param_name
        self.param_rule_des = param_rule_des
        self._validators = []
    def __getattr__(self, name: str):
        """
        当调用一个不存在的属性或方法时,Python 会自动调用 __getattr__ 方法,因此可以利用这个特性,动态收集用户调用的校验方法。
        以使用 ParamValidator("param").is_string(exception_msg='param must be string').is_not_empty() 为例,代码执行过程如下:
        1. 当调用 ParamValidator("param").is_string(exception_msg='param must be string') 时,
        2. 由于 is_string 方法不存在,__getattr__ 方法被调用,返回 validator_method 函数(未被调用),is_string 方法此时实际上是 validator_method 函数的引用,
        3. 当执行 is_string(exception_msg='param must be string') 时,is_string 方法被调用, 实际上是执行了 validator_method(exception_msg='param must be string') ,
        4. validator_method 函数调用后,执行函数体中的逻辑:
             - 向 self._validators 中添加了一个元组 ('is_string', (),  {'exception_msg': 'param  must  be  string'})
             - 返回 self 对象
        5. self 对象继续调用 is_not_empty(), 形成链式调用效果,此时 validator_method 函数的引用就是 is_not_empty, 调用过程与 1-4 相同。
        """
        def validator_method(*args, **kwargs):
            self._validators.append((name, args, kwargs))
            return self
        return validator_method
    def __call__(self, func: Callable) -> Callable:
        """
        使用 __call__ 方法, 让 ParameterValidator 的实例变成可调用对象,使其可以像函数一样被调用。
        '''
        @ParameterValidator("param").is_string()
        def example_function(param):
            return param
        example_function(param="test")
        '''
        以这段代码为例,代码执行过程如下:
        1. 使用 @ParameterValidator("param").is_string() 装饰函数 example_function,相当于: @ParameterValidator("param").is_string()(example_function)
        2. 此时返回一个 wrapper 函数(未调用), example_function 函数实际上是 wrapper 函数的引用;
        3. 当执行 example_function(param="test") 时,相当于执行 wrapper(param="test"), wrapper 函数被调用,开始执行 wrapper 内部逻辑, 见代码中注释。
        """
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 获取函数的参数和参数值
            bound_args = inspect.signature(func).bind(*args, **kwargs).arguments
            if self.param_name in kwargs:
                # 如果用户以关键字参数传值,如 example_function(param="test") ,则从 kwargs 中取参数值;
                value = kwargs[self.param_name]
            else:
                # 如果用户以位置参数传值,如 example_function("test"),则从 bound_args 是取参数值;
                value = bound_args.get(self.param_name)
            # 实例化 Validator 对象
            validator = Validator(value, field=self.param_name, rule_des=self.param_rule_des)
            # 遍历所有校验器(注意:这里使用 vargs, vkwargs,避免覆盖原函数的 args, kwargs)
            for method_name, vargs, vkwargs in self._validators:
                # 通过 函数名 从 validator 实例中反射获取函数对象
                validate_method = getattr(validator, method_name)
                # 执行校验函数
                validate_method(*vargs, **vkwargs)
            # 执行原函数
            return func(*args, **kwargs)
        return wrapper


3、测试代码

atme/demo/validator_v5/test_param_validator.py

import pytest
from atme.demo.validator_v5.param_validator import ParameterValidator
def test_is_string_validator_passing_01():
    """
    校验一个参数
    """
    @ParameterValidator("param").is_string(exception_msg='param must be string')
    def example_function(param):
        print(param)
        return param
    assert example_function(param="test") == "test"
    with pytest.raises(ValueError) as exc_info:
        example_function(param=123)
    print(exc_info.value)
    assert "invalid" in str(exc_info.value)
def test_is_string_validator_passing_02():
    """
    校验多个参数
    """
    @ParameterValidator("param2").is_string().is_not_empty()
    @ParameterValidator("param1").is_string().is_not_empty()
    def example_function(param1, param2):
        print(param1, param2)
        return param1, param2
    assert example_function("test1", "test2") == ("test1", "test2")
    with pytest.raises(ValueError) as exc_info:
        example_function(123, 123)
    print(exc_info.value)
    assert "invalid" in str(exc_info.value)


4、日志输出

执行 test 的日志如下,验证通过:

============================= test session starts =============================
collecting ... collected 2 items
test_param_validator.py::test_is_string_validator_passing_01 PASSED      [ 50%]test
param error: "123" is invalid. due to: param must be string
test_param_validator.py::test_is_string_validator_passing_02 PASSED      [100%]test1 test2
param2 error: "123" is invalid.
============================== 2 passed in 0.01s ==============================


三、后置说明


1、要点小结

  • 当调用一个不存在的属性或方法时,Python 会自动调用 __getattr__ 方法,可以利用这个特性,动态收集用户调用的校验方法。
  • 使用 __call__ 方法, 让 ParameterValidator 的实例变成可调用对象,使其可以像函数一样被调用。
  • 可以结合使用 __getattr____call__ 方法,实现在一个类中动态调用另一个类中的方法。
  • 虽然从功能上实现了校验函数参数的功能,但由于 ParameterValidator 并没有显式的定义 is_string()is_not_empty() 方法,编辑器无法智能提示可校验方法,需要进一步优化。


2、下节准备

  • 优化 ParamValidator,让编辑器 Pycharm 智能提示校验方法

点击进入《Python装饰器从入门到进阶》总目录

目录
相关文章
|
6月前
|
Python
使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验方法
使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验方法
51 0
|
Java 数据库连接
hibernate validator】(三)声明和验证方法约束
hibernate validator】(三)声明和验证方法约束
|
Java 数据库连接 API
【hibernate validator】(二)声明和验证Bean约束(下)
【hibernate validator】(二)声明和验证Bean约束(下)
|
Java 数据库连接 容器
【hibernate validator】(二)声明和验证Bean约束(上)
【hibernate validator】(二)声明和验证Bean约束
|
5月前
|
Java Spring 容器
详解java参数校验之:顺序校验、自定义校验、分组校验(@Validated @GroupSequence)
详解java参数校验之:顺序校验、自定义校验、分组校验(@Validated @GroupSequence)
|
6月前
使用Hibernate-Validate进行参数校验
使用Hibernate-Validate进行参数校验
75 3
|
11月前
|
Java 数据库连接
hibernate-validator校验对象属性为List
hibernate-validator校验对象属性为List
195 1
|
前端开发
怎么使用async-validator快速校验表单
怎么使用async-validator快速校验表单
421 0
|
JSON Java 数据格式
hibernate-validator校验参数(统一异常处理)(下)
hibernate-validator校验参数(统一异常处理)
hibernate-validator校验参数(统一异常处理)(下)
|
Oracle Java 关系型数据库
hibernate-validator校验参数(统一异常处理)(上)
hibernate-validator校验参数(统一异常处理)
hibernate-validator校验参数(统一异常处理)(上)