背景
想要模仿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