一、前置说明
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
智能提示校验方法