背景
想要模仿restful_framework的寫法,把組件的固定屬性寫在class里,不同的基礎(chǔ)組件的搭配組合之后可以構(gòu)成不同的Craft,構(gòu)建成一個Craft的時候把所有組件的同名列表屬性混到一起。
與restful_framework用例的區(qū)別: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):
# 快速定義組件,編寫獲取當(dāng)前param的方法,在Craft中直接可以調(diào)用獲取參數(shù)
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"]
為實現(xiàn)以上參數(shù)混入的功能,主要需要實現(xiàn)以下三個類:
- 組件基類BaseComponent
- 構(gòu)建Craft子類的MetaClass
- Craft基礎(chǔ)工具類
示例
需要構(gòu)建用于渲染Template的Notice類, Notice類輸出的參數(shù)為context字典
字典的鍵為Notice的每個組件的context_keys的合集,而值的獲取方式則是在不同的組件內(nèi)來定義的
from django.template import Template, Context # 最后用于渲染template的django的工具
class BasicNoticeComponent:
"""
擴展指南:
對于accepted_init_keys中的keys, **可以**編寫verify_{key}作為初始化驗證的方法
對于context中的keys, **必須**編寫get_{key}作為獲取對應(yīng)值的方法
"""
accepted_init_keys = [] # 初始化輸入的keys
context_keys = [] # 生成的用于渲染Template使用的Context的關(guān)鍵字
class NoticeMetaClass(type):
"""創(chuàng)建Notice子類的方法
由NoticeMetaClass創(chuàng)建的類:
1) Notice本身 —— 基礎(chǔ)工具類, 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 # 與繼承順序有關(guān), 由于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及其子類的創(chuàng)建")
# 用于創(chuàng)建該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里的定義是否重復(fù)
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}}"""
# 調(diào)用Notice,傳入組件所需的所有數(shù)據(jù)
>>> notice = MyNotice(code="A", notice_type=0)
# 渲染Template
>>> assert notice.content == "NameA Type0"