找回密码
 立即注册
首页 业界区 业界 [python]基于动态实例的命令处理设计

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

赶塑坠 2025-8-3 17:58:31
前言

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

  • 单命令。比如用户发一个ping,调用ping主命令。
  • 有一个子命令。比如用户发送ping version,调用ping主命令的version子命令。
  • 单命令,带一系列位置参数。比如ping host1 host2 host3,调用ping主命令,主命令自行处理参数。
  • 子命令有一系列位置参数。比如ping tcp host1 host2 host3,调用ping主命令的tcp子命令来处理参数。
暂不考虑子命令的子命令、flag等命令形式。
早期也没想着搞太复杂的功能,所以代码用正则表达式匹配,然后写了一堆if ... else,如今看来不是很美观,而且每次新增命令都要去配置下匹配逻辑,给别人修改时,别人经常忘了改匹配逻辑,比较繁琐。
这版的修改想法是命令类一旦声明就自动注册到某个地方,接收命令的时候自动分发到对应的命令类及其方法。想到的几个方案有监听者模式、责任链模式和本文所要提的动态实例方式(我也不知道这种方法怎么命名,瞎起了个名字)。
代码结构
  1. │  .gitignore
  2. │  main.py
  3. │  README.md
  4. └─commands
  5.         cmda.py
  6.         cmdb.py
  7.         __init__.py
复制代码
子命令的代码都存放在./commands目录下,./commands/__init__.py声明了命令的基类,导入commands目录下除了__init__.py之外的所有python文件,以及声明工厂函数。
除了__init__.py,commands目录下的所有python文件都是命令的实现。
基类

基类的声明位于commands/__init__.py文件中,要求子类必须实现main_cmd()方法,以及通过类属性判断是否需要导入命令类。自动注册子类的方法见__init_subclass__()
  1. from pathlib import Path
  2. from abc import ABCMeta, abstractmethod
  3. from threading import Lock
  4. from collections import UserDict
  5. import importlib
  6. from functools import wraps
  7. import inspect
  8. from typing import Callable
  9. class ThreadSafeDict(UserDict):
  10.     """线程安全的字典"""
  11.     def __init__(self):
  12.         super().__init__()
  13.         self._lock = Lock()
  14.    
  15.     def __setitem__(self, key, item):
  16.         with self._lock:
  17.             super().__setitem__(key, item)
  18. class Command(metaclass=ABCMeta):
  19.     registry = ThreadSafeDict()
  20.     def __init__(self):
  21.         # self._sub_cmds = ThreadSafeDict()
  22.         self._sub_cmd: str = ""
  23.         self._cmd_args: list = []
  24.     @abstractmethod
  25.     def main_cmd(self):
  26.         pass
  27.     @sub_cmd(name="help")
  28.     def get_help(self):
  29.         """Get help info"""
  30.         message = f"Usage: {self._main_name} [subcommand] [args]\n"
  31.         for name, f in self._sub_cmds.items():
  32.             doc = f.__doc__ or ""
  33.             message += f"  {name}, {doc}\n"
  34.         print(message)
  35.     def parse_cmd(self):
  36.         cmd_list = self.command.split(" ")
  37.         cmd_list_length = len(cmd_list)
  38.         if cmd_list_length == 1:
  39.             self._sub_cmd = ""
  40.             self._cmd_args = []
  41.         elif cmd_list_length >= 2 and cmd_list[1] not in self._sub_cmds:
  42.             self._sub_cmd = ""
  43.             self._cmd_args = cmd_list[1:]
  44.         elif cmd_list_length >= 2 and cmd_list[1] in self._sub_cmds:
  45.             self._sub_cmd = cmd_list[1]
  46.             self._cmd_args = cmd_list[2:]
  47.         else:
  48.             self._sub_cmd = ""
  49.             self._cmd_args = []
  50.     def dispatch_command(self) -> Callable:
  51.         """
  52.         根据主命令和子命令的名称分发到相应的命令处理方法
  53.         Returns:
  54.             Callable: 返回对应的命令处理方法, 如果找不到匹配的子命令则返回 None
  55.         """
  56.         if not self._sub_cmd and not self._cmd_args:
  57.             return self.main_cmd
  58.         elif not self._sub_cmd and self._cmd_args:
  59.             return self.main_cmd
  60.         elif self._sub_cmd and self._sub_cmd not in self._sub_cmds:
  61.             return None
  62.         else:
  63.             return self._sub_cmds[self._sub_cmd]
  64.         
  65.     def run(self):
  66.         self.parse_cmd()
  67.         func = self.dispatch_command()
  68.         if not func:
  69.             self.get_help()
  70.         else:
  71.             func(self)
  72.         def __init_subclass__(cls, **kwargs):
  73.         super().__init_subclass__(**kwargs)
  74.         cls_main_name = getattr(cls, "_main_name", "")
  75.         cls_enabled = getattr(cls, "_enabled", False)
  76.         cls_description = getattr(cls, "_description", "")
  77.         if cls_main_name and cls_enabled and cls_description:
  78.             cls.registry[cls._main_name.lower()] = cls  # 自动注册子类
  79.             if not hasattr(cls, "_sub_cmds"):
  80.                 cls._sub_cmds = ThreadSafeDict()
  81.             for name, method in inspect.getmembers(cls, inspect.isfunction):
  82.                 if hasattr(method, "__sub_cmd__"):
  83.                     cls._sub_cmds[method.__sub_cmd__] = method
  84.         else:
  85.             print(f"{cls.__name__} 未注册,请检查类属性 _main_name, _enabled, _description")
复制代码
子类只有导入时才会自动注册,所以写了个遍历目录进行导入的函数。
  1. def load_commands(dir_path: Path) -> None:
  2.     """遍历目录下的所有python文件并导入"""
  3.     commands_dir = Path(dir_path)
  4.     for py_file in commands_dir.glob("*.py"):
  5.         if py_file.stem in ("__init__"):
  6.             continue
  7.         module_name = f"commands.{py_file.stem}"
  8.         try:
  9.             importlib.import_module(module_name)
  10.         except ImportError as e:
  11.             print(f"Failed to import {module_name}: {e}")
  12. load_commands(Path(__file__).parent)
复制代码
子命令装饰器

命令类可以使用装饰器来注册子命令,其实只是给函数加个属性。
  1. def sub_cmd(name: str):
  2.     """
  3.     装饰器函数, 用于包装目标函数并添加 __sub_cmd 属性
  4.     Args:
  5.         name (str): 子命令名称
  6.     """
  7.     def decorator(func):
  8.         @wraps(func)
  9.         def wrapper(self, *args, **kwargs):
  10.             return func(self, *args, **kwargs)
  11.         wrapper.__sub_cmd__ = name
  12.         return wrapper
  13.     return decorator
复制代码
实现命令类

随便写两个命令类。命令类必须声明_main_name、_enabled和_description这三个类属性,否则不会注册这个命令类。
cmda

代码文件为commands/cmda.py
  1. from commands import Command, sub_cmd
  2. class Cmda(Command):
  3.     _main_name = "cmda"
  4.     _enabled = True
  5.     _description = "this is cmda"
  6.     def __init__(self, command: str):
  7.         self.command = command
  8.         super().__init__()
  9.     def main_cmd(self, *args: tuple, **kwargs):
  10.         print("this is main cmd for cmda")
  11.    
  12.     @sub_cmd(name="info")
  13.     def get_info(self):
  14.         """Get info"""
  15.         print(f"this is cmda's info")
复制代码
cmdb

代码文件为commands/cmdb.py
  1. from commands import Command, sub_cmd
  2. class Cmdb(Command):
  3.     _main_name = "cmdb"
  4.     _enabled = True
  5.     _description = "this is cmdb"
  6.     def __init__(self, command: str):
  7.         self.command = command
  8.         super().__init__()
  9.     def main_cmd(self, *args, **kwargs):
  10.         print("this is cmdb main")
  11.     @sub_cmd("info")
  12.     def get_info(self):
  13.         print("this is cmdb info")
  14.         if self._cmd_args:
  15.             print(f"args: {self._cmd_args}")
复制代码
工厂函数

工厂函数的代码也是位于commands/__init__.py
  1. def create_command(command: str) -> Command:
  2.     """工厂函数"""
  3.     if not command:
  4.         raise ValueError("command can not be empty")
  5.     command_list = command.split(" ")
  6.     command_type = command_list[0]
  7.     cls = Command.registry.get(command_type.lower())
  8.     if not cls:
  9.         raise ValueError(f"Unknown command: {command_type}")
  10.     return cls(command)
复制代码
使用示例

使用示例的代码位于main.py
  1. from commands import create_command
  2. if __name__ == '__main__':
  3.     command = create_command("cmdb info aaa")
  4.     command.run()
  5.     command = create_command("cmda help")
  6.     command.run()
复制代码
执行输出
  1. this is cmdb info
  2. args: ['aaa']
  3. Usage: cmda [subcommand] [args]
  4.   help, Get help info
  5.   info, Get info
复制代码
完整代码

除了commands/__init__.py,其它代码文件的完整内容上面都有了,所以补充下__init__.py的内容
  1. from pathlib import Pathfrom abc import ABCMeta, abstractmethodfrom threading import Lockfrom collections import UserDictimport importlibfrom functools import wrapsimport inspectfrom typing import Callabledef sub_cmd(name: str):
  2.     """
  3.     装饰器函数, 用于包装目标函数并添加 __sub_cmd 属性
  4.     Args:
  5.         name (str): 子命令名称
  6.     """
  7.     def decorator(func):
  8.         @wraps(func)
  9.         def wrapper(self, *args, **kwargs):
  10.             return func(self, *args, **kwargs)
  11.         wrapper.__sub_cmd__ = name
  12.         return wrapper
  13.     return decoratorclass 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:
  14.     """工厂函数"""
  15.     if not command:
  16.         raise ValueError("command can not be empty")
  17.     command_list = command.split(" ")
  18.     command_type = command_list[0]
  19.     cls = Command.registry.get(command_type.lower())
  20.     if not cls:
  21.         raise ValueError(f"Unknown command: {command_type}")
  22.     return cls(command)def load_commands(dir_path: Path) -> None:
  23.     """遍历目录下的所有python文件并导入"""
  24.     commands_dir = Path(dir_path)
  25.     for py_file in commands_dir.glob("*.py"):
  26.         if py_file.stem in ("__init__"):
  27.             continue
  28.         module_name = f"commands.{py_file.stem}"
  29.         try:
  30.             importlib.import_module(module_name)
  31.         except ImportError as e:
  32.             print(f"Failed to import {module_name}: {e}")
  33. load_commands(Path(__file__).parent)__all__ = [    "create_command",]
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册