使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验方法

简介: 使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验方法

一、前置说明


1、本节目标

  • 了解元类 metaclass 的使用。
  • 了解 __new__ 方法的使用。
  • 了解如何使用元类隐式装饰类中的所有方法。


2、相关回顾


二、操作步骤


1、项目目录


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


2、代码实现

atme/demo/validator_v3/validator.py

import functools
import inspect
def _error_prompt(value, exception_msg=None, rule_des=None, field=None):
    """
    优先使用校验方法中的错误提示, 如果方法中没有错误提示,则使用"字段规则描述"代替错误提示
    拼接出:name error: "123" is invalid. due to: name must be string.
    """
    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():
            # 如果是静态方法,则将它替换为一个新的静态方法,新的静态方法调用 raise_exception 函数,将原静态方法作为参数传递给raise_exception
            if isinstance(value, staticmethod):
                dct[key] = staticmethod(raise_exception(value.__func__))
            # 如果是类方法,则将它替换为一个新的类方法,新的类方法调用 raise_exception 函数,将原类方法作为参数传递给raise_exception
            if isinstance(value, classmethod):
                dct[key] = classmethod(raise_exception(value.__func__))
            # 如果是普通的成员方法,则将它替换为一个新的函数,新函数调用 raise_exception 函数,将原函数作为参数传递给 raise_exception
            # 排除掉以双下划线 __ 开头的方法, 如 __init__,__new__等
            if inspect.isfunction(value) and not key.startswith("__"):
                dct[key] = raise_exception(value)
        return super().__new__(cls, name, bases, dct)
class Validator(metaclass=RaiseExceptionMeta):
    def __init__(self, value, field=None, rule_des=None):
        """
        :param value: 待校验的值
        :param field: 校验字段
            - 用于提示具体哪个字段错误
            - 如 'name error: name must be string'
            - error 前面的 `name` 即为 field
        :param rule_des: 校验规则描述
        """
        self.value = value
        self._field = field
        self._rule_des = rule_des
    def is_string(self, exception_msg=None):
        return isinstance(self.value, str)
    def is_not_empty(self, exception_msg=None):
        return bool(self.value)


3、测试代码

atme/demo/validator_v3/test_validator.py

import pytest
from atme.demo.validator_v3.validator import Validator
def test_validator_01():
    """
    在 Validator 实例化时,不给 field、rule_des 传值; 在校验方法中,不给 exception_msg 传值
    """
    validator = Validator('Jane')
    assert validator.is_string().is_not_empty()
    with pytest.raises(ValueError) as exc_info:
        validator = Validator(123)
        validator.is_string().is_not_empty()
    assert 'invalid' in str(exc_info.value)
    print(exc_info.value)  # 输出: "123" is invalid.
def test_validator_02():
    """
    在 Validator 实例化时,给 field、rule_des 传值
    """
    validator = Validator('Jane', field='name', rule_des='name must be string from rule des.')
    assert validator.is_string().is_not_empty()
    with pytest.raises(ValueError) as exc_info:
        validator = Validator(123, field='name', rule_des='name must be string from rule des.')
        validator.is_string().is_not_empty()
    assert 'name must be string from rule des.' in str(exc_info.value)
    print(exc_info.value)  # 输出: name error: "123" is invalid. due to: name must be string from rule des.
def test_validator_03():
    """
    在 Validator 实例化时,给 field、rule_des 传值; 在校验方法中,给 exception_msg 传值
    """
    validator = Validator('Jane', field='name', rule_des='name must be string from rule des.')
    assert validator.is_string().is_not_empty()
    with pytest.raises(ValueError) as exc_info:
        validator = Validator(123, field='name', rule_des='name must be string from rule des.')
        validator.is_string('name must be string from method exception msg.').is_not_empty()
    assert 'name must be string from method exception msg.' in str(exc_info.value)
    print(exc_info.value)  # 输出: "123" is invalid due to "name error: name must be string from method exception msg."
def test_validator_04():
    """
    field_name 为空
    """
    validator = Validator('Jane', rule_des='name must be string from rule des.')
    assert validator.is_string().is_not_empty()
    with pytest.raises(ValueError) as exc_info:
        validator = Validator(123, rule_des='name must be string from rule des.')
        validator.is_string('name must be string from method exception msg.').is_not_empty()
    assert 'name must be string from method exception msg.' in str(exc_info.value)
    print(exc_info.value)  # 输出: "123" is invalid due to "name must be string from method exception msg."


4、日志输出

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

============================= test session starts =============================
collecting ... collected 4 items
test_validator.py::test_validator_01 PASSED                              [ 25%]"123" is invalid.
test_validator.py::test_validator_02 PASSED                              [ 50%]name error: "123" is invalid. due to: name must be string from rule des.
test_validator.py::test_validator_03 PASSED                              [ 75%]name error: "123" is invalid. due to: name must be string from method exception msg.
test_validator.py::test_validator_04 PASSED                              [100%]"123" is invalid. due to: name must be string from method exception msg.
============================== 4 passed in 0.01s ==============================


三、后置说明


1、要点小结

  • 元类 metaclass 可以控制类的创建过程,可以动态的修改类的属性、方法和其他行为;
  • __new__ 用于创建实例,__init__ 用于初始化实例。
  • RaiseExceptionMeta 类中,它会遍历类的字典(包括属性和方法),并对其中的静态方法、类方法和普通成员方法进行修改。
  • 经过优化后,虽然从功能上实现了链式调用的效果,但由于校验方法没有 return self , 编辑器如 pycharm 不能智能识别可链式调用的方法(如下图),对用户很不友好,需要继续优化。


2、下节准备

  • 使用 TypeVar 创建 Self 类变量,方便用户在 pycharm 编辑器中进行链式调用

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

目录
相关文章
|
5天前
|
C++
c++将一个类的回调函数注入到另一个类中的方法
c++将一个类的回调函数注入到另一个类中的方法
|
8月前
2.【类的组合(在一个类中定义一个类)】
2.【类的组合(在一个类中定义一个类)】
17 0
|
9月前
|
Java API
Java反射(通过反射获取构造函数、方法、属性)
1.通过反射获取构造函数,2.通过反射获取方法,3.通过反射调用成员属性
85 0
从工厂方法到注解的小例子
这里的前几个过程就不符代码了,请读者自行补充;只附上第四版,通过注解替换工厂方法实现自动化的小例子
|
缓存 Dart 安全
Dart中的类——初始化列表、命名构造器、factory构造器、常量构造器、构造器私有化、get和set方法、枚举
Dart中的类——初始化列表、命名构造器、factory构造器、常量构造器、构造器私有化、get和set方法、枚举
注解与反射6得到Class类的几种方式
注解与反射6得到Class类的几种方式
|
安全 Java
注解和反射12.动态创建对象执行方法
注解和反射12.动态创建对象执行方法
|
Python
Python面向对象、类的抽象、类的定义、类名遵循大驼峰的命名规范创建对象、类外部添加和获取对象属性、类内部操作属性魔法方法__init__()__str__()__del__()__repr__()
面向对象和面向过程,是两种编程思想. 编程思想是指对待同一个问题,解决问题的套路方式.面向过程: 注重的过程,实现的细节.亲力亲为.面向对象: 关注的是结果, 偷懒.类和对象,是面向对象中非常重要的两个概念object 是所有的类基类,即最初始的类class 类名(object): 类中的代码PEP8代码规范:类定义的前后,需要两个空行 创建的对象地址值都不一样如dog和dog1的地址就不一样,dog的地址为2378043254528dog1的地址为2378044849840 8.类内部操作属性 sel
177 1
Python面向对象、类的抽象、类的定义、类名遵循大驼峰的命名规范创建对象、类外部添加和获取对象属性、类内部操作属性魔法方法__init__()__str__()__del__()__repr__()