目录
- 一、前置说明
- 1、总体目录
- 2、相关回顾
- 3、本节目标
- 二、操作步骤
- 1、项目目录
- 2、代码实现
- 3、测试代码
- 4、日志输出
- 三、后置说明
- 1、要点小结
- 2、下节准备
一、前置说明
1、总体目录
- 《 pyparamvalidate 参数校验器,从编码到发布全过程》
2、相关回顾
- 使用 TypeVar 创建 Self 类型变量,方便用户在 Pycharm 编辑器中链式调用校验方法
3、本节目标
- 了解
__getattr__
的特性。 - 了解
__call__
的用法。 - 了解如何在一个类中动态的使用另一个类中的方法。
二、操作步骤
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:
"""
将返回类型注解定义为 Self, 支持编辑器如 pycharm 智能提示链式调用方法,如:Validator(input).is_string().is_not_empty()
- 从 Python 3.5 版本开始支持类型注解
- 在 Python 3.5 中引入了 PEP 484(Python Enhancement Proposal 484),其中包括了类型注解的概念,并引入了 typing 模块,用于支持类型提示和静态类型检查;
- 类型注解允许开发者在函数参数、返回值和变量上添加类型信息,但是在运行时,Python 解释器不会检查这些注解是否正确;
- 它们主要用于提供给静态类型检查器或代码编辑器进行,以提供更好的代码提示和错误检测;
- Python 运行时并不强制执行这些注解,Python 依然是一门动态类型的语言。
- 本方法中:
- 返回值类型为 bool 类型,用于与装饰器函数 raise_exception 配合使用,校验 self.value 是否通过;
- 为了支持编辑器如 pycharm 智能识别链式调用方法,将返回类型注解定义为 Self, 如:Validator(input).is_string().is_not_empty();
- Self, 即 'Validator', 由 Self = TypeVar('Self', bound='Validator') 定义;
- 如果返回类型不为 Self, 编辑器如 pycharm 在 Validator(input).is_string() 之后,不会智能提示 is_not_empty()
"""
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 方法被调用, 使用关键字参数传递 exception_msg='param must be string',
4. 实际上是执行了 validator_method(exception_msg='param must be string') , 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:
# 通过 函数名 反射获取校验函数对象
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
智能提示校验方法
点击返回主目录