2020-12-07 python MetaClass初嘗試【class屬性混入】

背景

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

參考

Python MetaClasses

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容