岁月蹉跎是什么意思| 什么银行卡最好用| 怀孕一个月什么症状| 耳鸣吃什么药比较好| 哈伦裤配什么鞋子好看| 什么东西不能带上飞机| 什么叫增强ct| 腹泻吃什么药好| 经期吃芒果有什么影响| 自我为中心是什么意思| au750是什么意思| 特斯拉发明了什么| 吞服是什么意思| 人中浅的女人代表什么| 中午吃什么饭| 肺结核吃什么药| 真菌镜检阳性是什么意思| 老年人适合吃什么| 师奶是什么意思| 什么泡水喝治口臭| 福禄双全是什么意思| 什么叫cta检查| 物上代位性是什么意思| 5月19日什么星座| 什么是三焦| 白毫银针属于什么茶| 痛风什么引起的原因有哪些| 断桥是什么意思| 面霜什么时候用| 茶叶渣属于什么垃圾| 面试穿什么衣服比较合适| 人彘是什么| 冬天手脚冰凉是什么原因怎么调理| ca199偏高是什么原因| 十二指肠球部溃疡a1期是什么意思| 过敏性鼻炎有什么症状| 怨妇是什么意思| 闭日是什么意思| 淋巴结节吃什么药| 11点是什么时辰| 女性为什么不适合喝茉莉花茶| 打开心扉是什么意思| ct和磁共振有什么区别| 吃醪糟有什么好处| 什么散步| 1999年属兔的是什么命| 什么是全麦面包| 乙肝小三阳是什么意思| 逾越节是什么意思| 天天喝绿茶有什么好处和坏处| 亚历山大王什么档次| 政治面貌是什么意思| 马齿苋有什么作用| 中性皮肤的特征是什么| 什么是冷血动物| 霸天虎和威震天是什么关系| 食指上有痣代表什么| 阿赖耶识是什么意思| 乳腺靶向检查是什么| 肾上腺素高会导致什么| 壁虎为什么是五毒之一| 鼠加句念什么| 有什么办法让男人死精| 宝珀手表属于什么档次| 接吻是什么感觉| 光年是什么单位| 成都人民公园有什么好玩的| 酒精过敏有什么症状| 糖尿病吃什么水果| 北面是什么档次的牌子| 凝血五项是检查什么的| 花旗参和西洋参有什么区别| 男鼠配什么生肖最好| 现是什么生肖| 寂寞的反义词是什么| 瓜蒌根为什么叫天花粉| 尼姑是什么生肖| 促甲状腺素高是什么原因| 什么颜色属木| 梦见猫咬我是什么意思| 处女座属于什么星象| 脾脏是人体的什么器官| 小孩子为什么老是流鼻血| 言尽于此是什么意思| 湖北九头鸟是什么意思| 什么叫能量| 右肾结晶是什么意思| 姨妈疼吃什么止疼药| 淋巴癌是什么| 去拉萨需要准备什么| 回声结节什么意思| 蓝莓是什么味道| 脑白质变性是什么病| 经理是什么级别| 喉咙痛咳嗽吃什么药| 屁股眼痒是什么原因| 脑内小缺血灶是什么意思| esr是什么| 蛋白石是什么石头| 为什么耳鸣| 休是什么意思| 大拇指麻木是什么原因| 小儿发烧吃什么食物好| 血糖偏高能吃什么水果和食物最好| 急性胃炎吃什么药| 足底筋膜炎什么症状| 钟是什么意思| 午安是什么意思| 乙肝有抗体是什么意思| 吃汉堡为什么要配可乐| 吃烧烤后吃什么水果可以帮助排毒| 命悬一线的意思是什么| 杭州有什么| palace是什么牌子| 叶黄素有什么功效| 空腹吃西红柿有什么危害| 反目成仇是什么意思| 蚩尤姓什么| 作奸犯科是什么意思| 拉格啤酒是什么意思| 3月20号是什么星座| 无水焗是什么意思| 梦见情人是什么意思啊| 白色的猫是什么品种| ab阳性血型是什么血型| 白天咳嗽晚上不咳嗽是什么原因| 九月十四号是什么星座| 虚热是什么意思| 油价什么时候下调| 肚子有硬块是什么原因| 大便干吃什么药| 阴道口痒是什么原因| 尿隐血挂什么科| 窦性早搏是什么意思| ppi是什么意思啊| 什么食物含维生素b12最多| 过敏是什么样子的| 呦是什么意思| 力排众议是什么意思| 直肠炎有什么症状| 农历八月初三是什么星座| 凌晨2点是什么时辰| 生吃番茄有什么好处| 宝宝咬人是什么原因| 梦见建房子是什么预兆| 秦始皇的名字叫什么| 腰酸痛挂什么科| 一什么沙滩| 什么药| 立夏节吃什么| 大便溏稀吃什么药| 什么是乙肝| 牛仔蓝是什么颜色| 做梦梦见僵尸是什么预兆| 胃胀气是什么原因| 大豆磷脂是什么| 什么天喜地| 衣锦还乡是什么意思| 肽是什么| 风向是指风什么的方向| 咳嗽有黄痰是什么原因| 为什么超市大米不生虫| 君子什么意思| 过命之交是什么意思| 车前草能治什么病| 鸽子和什么炖气血双补| 尿失禁用什么药好| ce是什么意思| 东厂是什么意思| 正月是什么意思| 香精是什么| 中药先煎是什么意思| 吃什么长得高| 香菇配什么菜炒着好吃| 晚上9点到10点是什么时辰| 白醋和白米醋有什么区别| pisen是什么牌子| 造影检查对身体有什么伤害| 拔罐对身体有什么好处| 黄体不足吃什么| 什么是小三阳| 呆板是什么意思| 肚子疼挂什么科室| 韩五行属什么的| 保守是什么意思| 后背疼应该挂什么科| 膳食纤维有什么作用| 辐射对人体有什么伤害| 七点到九点是什么时辰| 茶减一笔是什么字| 真菌感染皮肤病用什么药最好| 什么颜色加什么颜色等于白色| 宝宝出急疹要注意什么| 双肺上叶肺大泡是什么意思| 减肥吃什么主食比较好| 钙片什么时候吃好| 食欲不振是什么原因| ox什么意思| 吃什么对肠道好| 什么是职务| 手术后为什么要平躺6小时| 劝君更尽一杯酒的下一句是什么| 口干舌燥口苦吃什么药| 生活质量是什么意思| 省政协常委是什么级别| 银手镯发黄是什么原因| 女人梦见龙是什么征兆| 925银是什么意思| 东南方五行属什么| 拔完火罐要注意什么| 鼻腔有臭味是什么原因| 化疗期间吃什么好| 心脏缺血吃什么药好| 腰间盘膨出吃什么药效果好| 什么是二次元| 强阳下降到什么程度开始排卵| 脖子短是什么原因| 法兰克穆勒什么档次| 哈达是什么意思| 抽脂有什么风险和后遗症| 女人一般什么时候绝经| 水由什么组成| 尿道感染应该吃什么药| 为什么北方人比南方人高| 南方元旦吃什么| 当你从我眼前慢慢走过是什么歌| 鹦鹉吃什么食物| 为什么会长腋毛| 孕晚期缺铁对胎儿有什么影响| 生物学是什么| 一鸣惊人指什么生肖| 为什么会流鼻涕| 肺结节是一种什么病| 2.1是什么星座| 财神叫什么名字| 水晶和玻璃有什么区别| 科级干部是什么级别| tr是什么材质| 河北有什么市| 5个月宝宝可以吃什么水果| 瞌睡多什么原因| 解压密码是什么| leep术是什么手术| 胎盘有什么用| 比中指是什么意思| h皮带是什么牌子| 晒伤擦什么药| 屁多是什么原因| 夜长梦多是什么意思| 七月十一日是什么日子| 什么是有氧运动什么是无氧运动| 为什么万恶淫为首| 抗病毒什么药效果好| 尿酸高是什么症状| 老虎头上为什么有王字| 八卦是什么意思| 250为什么是骂人的话| 牙龈疼吃什么消炎药| 频繁大便是什么原因| md是什么职位| pd什么意思| 从容面对是什么意思| 猪肝炒什么| 量贩装是什么意思| 百度

[python]基于动态实例的命令处理设计

前言

百度 身高打内线虽然矮了点,但在CBA连198cm的NBA边缘球员哈里斯都能游刃有余,还帮助四川拿到总冠军,对于在NBA驰骋12个赛季打了758场的巴斯,这不成为问题。

最近在做公司内部的一个聊天机器人服务,这个聊天机器人暂时不会用到现在热门的大模型技术,只是用于接收用户固定格式的命令,然后调用对应的方法。因为只是内部使用,所以性能也不需要太高。目前考虑的用户命令类型有以下几种:

  1. 单命令。比如用户发一个ping,调用ping主命令。
  2. 有一个子命令。比如用户发送ping version,调用ping主命令的version子命令。
  3. 单命令,带一系列位置参数。比如ping host1 host2 host3,调用ping主命令,主命令自行处理参数。
  4. 子命令有一系列位置参数。比如ping tcp host1 host2 host3,调用ping主命令的tcp子命令来处理参数。

暂不考虑子命令的子命令、flag等命令形式。

早期也没想着搞太复杂的功能,所以代码用正则表达式匹配,然后写了一堆if ... else,如今看来不是很美观,而且每次新增命令都要去配置下匹配逻辑,给别人修改时,别人经常忘了改匹配逻辑,比较繁琐。

这版的修改想法是命令类一旦声明就自动注册到某个地方,接收命令的时候自动分发到对应的命令类及其方法。想到的几个方案有监听者模式、责任链模式和本文所要提的动态实例方式(我也不知道这种方法怎么命名,瞎起了个名字)。

代码结构

│  .gitignore
│  main.py
│  README.md
│
└─commands
        cmda.py
        cmdb.py
        __init__.py

子命令的代码都存放在./commands目录下,./commands/__init__.py声明了命令的基类,导入commands目录下除了__init__.py之外的所有python文件,以及声明工厂函数。

除了__init__.pycommands目录下的所有python文件都是命令的实现。

基类

基类的声明位于commands/__init__.py文件中,要求子类必须实现main_cmd()方法,以及通过类属性判断是否需要导入命令类。自动注册子类的方法见__init_subclass__()

from pathlib import Path
from abc import ABCMeta, abstractmethod
from threading import Lock
from collections import UserDict
import importlib
from functools import wraps
import inspect
from typing import Callable

class ThreadSafeDict(UserDict):
    """线程安全的字典"""
    def __init__(self):
        super().__init__()
        self._lock = Lock()
    
    def __setitem__(self, key, item):
        with self._lock:
            super().__setitem__(key, item)

class Command(metaclass=ABCMeta):
    registry = ThreadSafeDict()

    def __init__(self):
        # self._sub_cmds = ThreadSafeDict()
        self._sub_cmd: str = ""
        self._cmd_args: list = []

    @abstractmethod
    def main_cmd(self):
        pass

    @sub_cmd(name="help")
    def get_help(self):
        """Get help info"""
        message = f"Usage: {self._main_name} [subcommand] [args]\n"
        for name, f in self._sub_cmds.items():
            doc = f.__doc__ or ""
            message += f"  {name}, {doc}\n"
        print(message)

    def parse_cmd(self):
        cmd_list = self.command.split(" ")
        cmd_list_length = len(cmd_list)
        if cmd_list_length == 1:
            self._sub_cmd = ""
            self._cmd_args = []
        elif cmd_list_length >= 2 and cmd_list[1] not in self._sub_cmds:
            self._sub_cmd = ""
            self._cmd_args = cmd_list[1:]
        elif cmd_list_length >= 2 and cmd_list[1] in self._sub_cmds:
            self._sub_cmd = cmd_list[1]
            self._cmd_args = cmd_list[2:]
        else:
            self._sub_cmd = ""
            self._cmd_args = []

    def dispatch_command(self) -> Callable:
        """
        根据主命令和子命令的名称分发到相应的命令处理方法

        Returns:
            Callable: 返回对应的命令处理方法, 如果找不到匹配的子命令则返回 None
        """
        if not self._sub_cmd and not self._cmd_args:
            return self.main_cmd
        elif not self._sub_cmd and self._cmd_args:
            return self.main_cmd
        elif self._sub_cmd and self._sub_cmd not in self._sub_cmds:
            return None
        else:
            return self._sub_cmds[self._sub_cmd]
        
    def run(self):
        self.parse_cmd()
        func = self.dispatch_command()
        if not func:
            self.get_help()
        else:
            func(self)

	def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls_main_name = getattr(cls, "_main_name", "")
        cls_enabled = getattr(cls, "_enabled", False)
        cls_description = getattr(cls, "_description", "")
        if cls_main_name and cls_enabled and cls_description:
            cls.registry[cls._main_name.lower()] = cls  # 自动注册子类
            if not hasattr(cls, "_sub_cmds"):
                cls._sub_cmds = ThreadSafeDict()
            for name, method in inspect.getmembers(cls, inspect.isfunction):
                if hasattr(method, "__sub_cmd__"):
                    cls._sub_cmds[method.__sub_cmd__] = method
        else:
            print(f"{cls.__name__} 未注册,请检查类属性 _main_name, _enabled, _description")

子类只有导入时才会自动注册,所以写了个遍历目录进行导入的函数。

def load_commands(dir_path: Path) -> None:
    """遍历目录下的所有python文件并导入"""
    commands_dir = Path(dir_path)
    for py_file in commands_dir.glob("*.py"):
        if py_file.stem in ("__init__"):
            continue
        module_name = f"commands.{py_file.stem}"
        try:
            importlib.import_module(module_name)
        except ImportError as e:
            print(f"Failed to import {module_name}: {e}")

load_commands(Path(__file__).parent)

子命令装饰器

命令类可以使用装饰器来注册子命令,其实只是给函数加个属性。

def sub_cmd(name: str):
    """
    装饰器函数, 用于包装目标函数并添加 __sub_cmd 属性

    Args:
        name (str): 子命令名称
    """
    def decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            return func(self, *args, **kwargs)
        wrapper.__sub_cmd__ = name
        return wrapper
    return decorator

实现命令类

随便写两个命令类。命令类必须声明_main_name_enabled_description这三个类属性,否则不会注册这个命令类。

cmda

代码文件为commands/cmda.py

from commands import Command, sub_cmd

class Cmda(Command):
    _main_name = "cmda"
    _enabled = True
    _description = "this is cmda"

    def __init__(self, command: str):
        self.command = command
        super().__init__()

    def main_cmd(self, *args: tuple, **kwargs):
        print("this is main cmd for cmda")
    
    @sub_cmd(name="info")
    def get_info(self):
        """Get info"""
        print(f"this is cmda's info")

cmdb

代码文件为commands/cmdb.py

from commands import Command, sub_cmd

class Cmdb(Command):
    _main_name = "cmdb"
    _enabled = True
    _description = "this is cmdb"

    def __init__(self, command: str):
        self.command = command
        super().__init__()

    def main_cmd(self, *args, **kwargs):
        print("this is cmdb main")


    @sub_cmd("info")
    def get_info(self):
        print("this is cmdb info")
        if self._cmd_args:
            print(f"args: {self._cmd_args}")

工厂函数

工厂函数的代码也是位于commands/__init__.py

def create_command(command: str) -> Command:
    """工厂函数"""
    if not command:
        raise ValueError("command can not be empty")
    command_list = command.split(" ")
    command_type = command_list[0]
    cls = Command.registry.get(command_type.lower())
    if not cls:
        raise ValueError(f"Unknown command: {command_type}")
    return cls(command)

使用示例

使用示例的代码位于main.py

from commands import create_command

if __name__ == '__main__':
    command = create_command("cmdb info aaa")
    command.run()
    command = create_command("cmda help")
    command.run()

执行输出

this is cmdb info
args: ['aaa']
Usage: cmda [subcommand] [args]
  help, Get help info
  info, Get info

完整代码

除了commands/__init__.py,其它代码文件的完整内容上面都有了,所以补充下__init__.py的内容

from pathlib import Path
from abc import ABCMeta, abstractmethod
from threading import Lock
from collections import UserDict
import importlib
from functools import wraps
import inspect
from typing import Callable

def sub_cmd(name: str):
    """
    装饰器函数, 用于包装目标函数并添加 __sub_cmd 属性

    Args:
        name (str): 子命令名称
    """
    def decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            return func(self, *args, **kwargs)
        wrapper.__sub_cmd__ = name
        return wrapper
    return decorator

class ThreadSafeDict(UserDict):
    """线程安全的字典"""
    def __init__(self):
        super().__init__()
        self._lock = Lock()
    
    def __setitem__(self, key, item):
        with self._lock:
            super().__setitem__(key, item)

class Command(metaclass=ABCMeta):
    registry = ThreadSafeDict()

    def __init__(self):
        # self._sub_cmds = ThreadSafeDict()
        self._sub_cmd: str = ""
        self._cmd_args: list = []

    @abstractmethod
    def main_cmd(self):
        pass

    @sub_cmd(name="help")
    def get_help(self):
        """Get help info"""
        message = f"Usage: {self._main_name} [subcommand] [args]\n"
        for name, f in self._sub_cmds.items():
            doc = f.__doc__ or ""
            message += f"  {name}, {doc}\n"
        print(message)

    def parse_cmd(self):
        cmd_list = self.command.split(" ")
        cmd_list_length = len(cmd_list)
        if cmd_list_length == 1:
            self._sub_cmd = ""
            self._cmd_args = []
        elif cmd_list_length >= 2 and cmd_list[1] not in self._sub_cmds:
            self._sub_cmd = ""
            self._cmd_args = cmd_list[1:]
        elif cmd_list_length >= 2 and cmd_list[1] in self._sub_cmds:
            self._sub_cmd = cmd_list[1]
            self._cmd_args = cmd_list[2:]
        else:
            self._sub_cmd = ""
            self._cmd_args = []

    def dispatch_command(self) -> Callable:
        """
        根据主命令和子命令的名称分发到相应的命令处理方法

        Returns:
            Callable: 返回对应的命令处理方法, 如果找不到匹配的子命令则返回 None
        """
        if not self._sub_cmd and not self._cmd_args:
            return self.main_cmd
        elif not self._sub_cmd and self._cmd_args:
            return self.main_cmd
        elif self._sub_cmd and self._sub_cmd not in self._sub_cmds:
            return None
        else:
            return self._sub_cmds[self._sub_cmd]
        
    def run(self):
        self.parse_cmd()
        func = self.dispatch_command()
        if not func:
            self.get_help()
        else:
            func(self)



    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls_main_name = getattr(cls, "_main_name", "")
        cls_enabled = getattr(cls, "_enabled", False)
        cls_description = getattr(cls, "_description", "")
        if cls_main_name and cls_enabled and cls_description:
            cls.registry[cls._main_name.lower()] = cls  # 自动注册子类
            if not hasattr(cls, "_sub_cmds"):
                cls._sub_cmds = ThreadSafeDict()
            for name, method in inspect.getmembers(cls, inspect.isfunction):
                if hasattr(method, "__sub_cmd__"):
                    cls._sub_cmds[method.__sub_cmd__] = method
        else:
            print(f"{cls.__name__} 未注册,请检查类属性 _main_name, _enabled, _description")

def create_command(command: str) -> Command:
    """工厂函数"""
    if not command:
        raise ValueError("command can not be empty")
    command_list = command.split(" ")
    command_type = command_list[0]
    cls = Command.registry.get(command_type.lower())
    if not cls:
        raise ValueError(f"Unknown command: {command_type}")
    return cls(command)

def load_commands(dir_path: Path) -> None:
    """遍历目录下的所有python文件并导入"""
    commands_dir = Path(dir_path)
    for py_file in commands_dir.glob("*.py"):
        if py_file.stem in ("__init__"):
            continue
        module_name = f"commands.{py_file.stem}"
        try:
            importlib.import_module(module_name)
        except ImportError as e:
            print(f"Failed to import {module_name}: {e}")

load_commands(Path(__file__).parent)

__all__ = [
    "create_command",
]
posted @ 2025-08-04 19:07  花酒锄作田  阅读(37)  评论(0)    收藏  举报
舌头上长泡是什么原因 m0是什么意思 为什么手会麻 小孩子流鼻血是什么原因引起的 pck是什么意思
叶绿素是什么 牙疼吃什么药止疼最快 猫可以吃什么水果 长寿面什么时候吃 闭经有什么症状
舒五行属性是什么 秋刀鱼是什么鱼 岛屿是什么 一级医院是什么医院 肛门有灼烧感什么原因
游龙斑是什么鱼 九三年属什么生肖 这些是什么 胃痛是什么原因 人设什么意思
簸箕是什么hcv8jop7ns0r.cn 脂膜炎是什么原因引起的hcv8jop3ns8r.cn 贝壳吃什么食物520myf.com 石棉是什么东西0297y7.com 角膜塑形镜什么牌子好hcv9jop0ns4r.cn
云南有什么hcv8jop2ns4r.cn 胆囊炎吃什么药好得快hcv9jop5ns0r.cn 羊癫疯有什么症状表现hkuteam.com 公知是什么意思hcv8jop7ns8r.cn 白细胞低吃什么药hcv8jop5ns9r.cn
翌是什么意思hcv8jop9ns7r.cn 109是什么意思mmeoe.com 挂失补办身份证需要什么hcv8jop7ns7r.cn 彩宝是什么hcv9jop4ns7r.cn 阳光是什么颜色mmeoe.com
喝酒对身体有什么危害sanhestory.com 女性下面水少是什么原因xscnpatent.com 小恙是什么意思jinxinzhichuang.com 孩子脾虚内热大便干吃什么药hcv7jop5ns4r.cn 每年什么时候最热hcv9jop2ns7r.cn
百度