2020-12-07 python MetaClass初尝试【class属性混入】

背景

想要模仿restful_framework的写法,把组件的固定属性写在class里,不同的基础组件的搭配组合之后可以构成不同的Craft,构建成一个Craft的时候把所有组件的同名列表属性混到一起。
与restful_framework用例的区别:restful_framework里面有一个viewsets使用mixins的场景,使用的时候类似以下,不过它涉及到的是不同名的class的方法(将不同的方法混入到ViewSet里),而这里我想要探索的方法涉及到的是同名的class的属性

# rest_framework/viewsets.py
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    """
    A viewset that provides default `list()` and `retrieve()` actions.
    """
    pass


class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass

需要达成的效果(使用案例)

快速编写自定义组件,将组件

class ComponentA(BaseComponent):
    accepted_init_keys = ["a1", "a2", "a3"]
    output_keys = ["result_ax", "result_ay"]
    def get_result_ax(self):
        # 快速定义组件,编写获取当前param的方法,在Craft中直接可以调用获取参数
        return self.a1 + self.a2 + self.a3
    def get_result_ay(self):
        return self.a2 + self.a3
class ComponentB(BaseComponent):
    accepted_init_keys = ["b"]
    output_keys = ["result_b"]
    def get_result_b(self):
        return self.b + "result"
class ABCraft(Craft):
    components = [ComponentA, ComponentB]
>>> craft = ABCraft(a1="a1_input", a2="a2_input", a3="a3_input", b="b_input")
>>> assert craft.accepted_init_keys == ["a1", "a2", "a3", "b"]
>>> assert craft.output_keys == ["result_ax", "result_ay", "result_b"]

为实现以上参数混入的功能,主要需要实现以下三个类:

  • 组件基类BaseComponent
  • 构建Craft子类的MetaClass
  • Craft基础工具类

示例

需要构建用于渲染Template的Notice类, Notice类输出的参数为context字典
字典的键为Notice的每个组件的context_keys的合集,而值的获取方式则是在不同的组件内来定义的

from django.template import Template, Context # 最后用于渲染template的django的工具

class BasicNoticeComponent:
    """
    扩展指南:
        对于accepted_init_keys中的keys, **可以**编写verify_{key}作为初始化验证的方法
        对于context中的keys, **必须**编写get_{key}作为获取对应值的方法
    """
    accepted_init_keys = [] # 初始化输入的keys
    context_keys = [] # 生成的用于渲染Template使用的Context的关键字

class NoticeMetaClass(type):
    """创建Notice子类的方法
    由NoticeMetaClass创建的类:
    1) Notice本身 —— 基础工具类, Notice必须继承这一类
    2) CustomedNotice
        Params:
            components(List[BasicComponent]): 列表中的元素必须是BasicNoticeComponent的子类
                - components会作为CustomedNotice的继承类
                - 所有components类的非私有列表属性将会被合并
    Example for 2):
        class ComponentA(BasicComponent):
            list1 = ["a1", "a2"]
            _private_list = ["aa", "ab"]
        class ComponentB(BasicComponent):
            list1 = ["b1", "b2"]
            _private_list = ["aa", "ab"]
        class CustomedNotice(Notice):
            components = [ComponentA, ComponentB]
        
        >>> notice = CustomedNotice()
        >>> notice.list1
        ["b1", "b2", "a1", "a2]
        >>> notice._private_list # 与继承顺序有关, 由于CustomedNotice的第一个components为ComponentA, 所以继承的是它的属性
        ["aa", "ab"]

    """
    def __new__(cls, name, bases, attrs):
        # 如果是Notice本身, 或者没有继承Notice
        if name == "Notice":
            # Notice类的本身
            return super().__new__(cls, name, bases, attrs)
        else:
            if Notice not in bases:
                raise Exception("NoticeMetaClass只允许用于Notice及其子类的创建")
        
        # 用于创建该Notice子类的所有components class
        components = tuple(attrs['components'])
        if any([not issubclass(component, BasicNoticeComponent) for component in components]):
            raise Exception("components: must be list of subclasses of BasicNoticeComponent")
        # 将components全部加入Notice子类的继承类
        bases += components
        # 将所有继承类的同名 且 为列表的属性进行混合连接
        for component in components:
            if issubclass(component, BasicNoticeComponent):
                # 所有的非私有变量
                params = [param for param in dir(component) if not param.startswith("_")]
                # TODO: 检查components里的定义是否重复
                for param in params:
                    component_value = getattr(component, param) # 变量值/function/property
                    # 所有列表类的属性进行合并
                    if isinstance(component_value, list):
                        if param in attrs: # 已有则extend, 对于第一个之后的component的同名属性
                            attrs[param].extend(component_value)
                        else: # 未有则赋值
                            attrs[param] = component_value

class Notice(metaclass=NoticeMetaClass):
    """
    Notice: 不要直接使用该类
    生成Notice的子类时会将同时继承的NoticeComponent里的所有属性中的列表属性进行合并
    """
    components = []
    content_template = ""
    def __init__(self, *args, **kwargs):
        self.check_init_kwargs(kwargs)
        for key, value in kwargs.items():
            setattr(self, key, value)
    def check_init_kwargs(self, kwargs):
        pass
    @property
    def context(self):
        context = {}
        for component in self.components:
            for key in component.context_keys:
                context_value_getter_func_name = f"get_{key}"
                context_value_getter_func = getattr(self, context_value_getter_func_name, None)
                if context_value_getter_func:
                    value = context_value_getter_func()
                    context[key] = value
                else:
                    # 代码错误
                    raise NotImplementedError(f"Please implement {context_value_getter_func_name} for {self.__class__.__name__}")
        return context
    @property
    def content(self):
        content = Template(self.content_template).render(Context(self.context))
        return content

测试用例

# 编写组件
class EntityNoticeComponent(BasicNoticeComponent):
    accepted_init_keys = ["code"]
    context_keys = ["name"]
    def get_fund_name(self):
        return {"A": "NameA", "B", "NameB"}.get(self.code)
class NoticeTypeNoticeComponent(BasicNoticeComponent):
    accepted_init_keys = ["notice_type"]
    context_keys = ["notice_type_name"]
    def get_notice_type_name(self):
        return {0: "Type 0", "1", "Type 1"}.get(int(self.notice_type))
# 组合组件成为Notice
class MyNotice(Notice):
    components = [EntityNoticeComponent, NoticeTypeNoticeComponent]
    template = """{{name}} {{notice_type_name}}"""
# 调用Notice,传入组件所需的所有数据
>>> notice = MyNotice(code="A", notice_type=0)
# 渲染Template
>>> assert notice.content == "NameA Type0"

参考

Python MetaClasses

你可能感兴趣的:(2020-12-07 python MetaClass初尝试【class属性混入】)