使用Mixin类简单重构配置模块

前言

按照个人习惯,项目伊始我会按照如下结构组织项目配置,也就是配置文件放在conf/目录,单独写一个配置模块pkg/config.py去读取加载。有的小项目还好,没什么配置项。但有的项目要调用很多第三方的接口,配置文件写了一堆接口地址、认证方式等,配置模块也相应增加了几百行。看着这快上千行的配置模块,还是尽早改改比较好。

复制代码
conf/
  app.toml
pkg/
  config.py

有的项目会把配置打散,各个模块维护各自的配置,但对于使用单一配置模块的项目,除了配置模块,其它模块调用配置类单例的地方我都不想去碰,也懒得碰。这时候,使用Mixin类就比较合适。

在Python中,Mixin只是一种约定,语言层面没有显式支持,实际上就是python的多重继承。

旧代码的配置类

旧的配置模块pkg/config.py大概长这样,每个配置项都写成了动态属性。即便只是简单的取值,也可能会写很多。如果再加上校验,单个文件的内容就会很多了,鼠标滚轮翻快点估计就找不到哪对哪了。

复制代码
class Config:
    def __init__(self) -> None:
        self._config_file = Path(__file__).parent.parent.parent / "conf" / "config.toml"
        self._config = self._load_config()

    def _load_config(self) -> Dict[str, Any]:
        if not self._config_file.exists():
            raise FileNotFoundError(f"Configuration file {self._config_file} does not exist.")
        with open(self._config_file, "rb") as f:
            return tomllib.load(f)

    @property
    def service_host(self) -> str:
        return self._config.get("service").get("host", "127.0.0.1")

    @property
    def service_port(self) -> int:
        return self._config.get("service").get("port", 8000)

拆分

简单示例

如果配置的层级特别深,Mixin里写一长串.get().get()也挺碍眼的。可以写一个基类BaseMixin,在基类中定义一个递归读取配置的方法。

复制代码
class BaseMixin:
    _config: Dict[str, Any]
    
    def _get_conf(self, *keys: str, default: Any = None) -> Any:
        """递归获取配置"""
        data = self._config
        for k in keys:
            if isinstance(data, dict):
                data = data.get(k)
            else:
                return default
        return data if data is not None else default

class FeatureMixin(BaseMixin):
    @property
    def is_feature_enabled(self) -> bool:
        return self._get_conf("module", "submodule", "enabled", default=False)
        
from typing import Any, Dict

class ServiceMixin(BaseMixin):
    """处理 Service 相关的配置项"""
    @property
    def service_host(self) -> str:
        return self._get_conf("service", "host", default="127.0.0.1")

    @property
    def service_port(self) -> int:
        return self._get_conf("service", "port", default=8000)

class DatabaseMixin(BaseMixin):
    """处理 Database 相关的配置项"""
    @property
    def db_url(self) -> str:
        return self._get_conf("database", "url", default="sqlite:///./test.db")

组合成最终的Config类

复制代码
import tomllib
from pathlib import Path

class Config(ServiceMixin, DatabaseMixin):
    """
    最终的聚合类。继承了所有 Mixin,因此它拥有了所有定义好的 @property。
    """

    def __init__(self) -> None:
        self._config_file = Path(__file__).parent.parent.parent / "conf" / "config.toml"
        self._config = self._load_config()

    def _load_config(self) -> Dict[str, Any]:
        if not self._config_file.exists():
            raise FileNotFoundError(f"Configuration file {self._config_file} does not exist.")
        with open(self._config_file, "rb") as f:
            return tomllib.load(f)

# --- 调用端代码完全不需要修改 ---
config = Config()
print(config.service_host)  # 来源于 ServiceMixin
print(config.db_url)        # 来源于 DatabaseMixin

如上改造后,调用方依然使用config.db_url 这样的方式来使用,不用管配置模块如何改动。以后如果再想新增配置,比如Redis的连接配置,只需要新增一个RedisMixin类,并加到Config的继承列表里即可。

中间层聚合

当配置的Mixin类越来越多,Config类会有一溜排的Mixin类要继承,看着有点头重脚轻。这时可以按逻辑领域先进行聚合。

比如,数据库相关的先聚合成DBMixins(这种中间层聚合的Mixin类,推荐命名后缀为Mixins)

复制代码
# pkg/config/mixins/db.py

class PostgresMixin(BaseMixin):
    @property
    def pg_host(self) -> str:
        pass
        
    @property
    def pg_port(self) -> int:
        pass
        
class RedisMixin(BaseMixin):
    @property
    def redis_host(self) -> str:
        pass
        
    @property
    def redis_port(self) -> int:
        pass
        
class DBMixins(PostgresMixin, RedisMixin):
    pass

在Config类中组装

复制代码
# pkg/config/config.py
from pkg.config.mixins.db import DBMixins
class Config(DBMixins):
    pass

最终目录结构如下:

复制代码
conf/
  config.toml
pkg/
  config/
    __init__.py  # 在此创建 Config 类的单例
    config.py    # 其中只有 Config 类
    mixins/
      __init__.py  # 定义 BaseMixin 基类
      db.py
      third_parties.py

补充

对于新项目,可以试试把配置全部放到环境变量,各个模块实现各自的配置模块

相关推荐
YWamy9 小时前
如何降低RTC SDK在复杂网络环境下的运行成本
网络·音视频·实时音视频
实名上网宋凯宣9 小时前
Django
后端·python·django
北京盛世宏博9 小时前
信创国产化智慧档案馆管库系统:十二防一体化管控平台落地笔记
网络·方案·温湿度·智慧档案馆建设
w-w0w-w10 小时前
SQL注入
网络·数据库·sql
一条泥憨鱼10 小时前
【计算机网络】短连接、长连接与连接池:从 TCP 握手到池化复用的完整链路
网络·数据库·网络协议·tcp/ip·计算机网络
如若12310 小时前
【论文复现】CoFailMed:从零复现医学图像分割的共享失败审计,含完整代码流程与科研绘图
人工智能·python
北京盛世宏博10 小时前
数字孪生 3D 可视化实战:档案十防体系温湿度数据联动与告警定位实现
网络·档案八防·十防
Lynne30911 小时前
EtherCAT网络中,多个节点如何配置才能互不干扰?
网络·网络配置·ethercat·工业网络
砚底藏山河12 小时前
python量化自定义周期 K 线合成
java·开发语言·数据库·python·金融
视觉小萌新12 小时前
AI Class 3 · Chapter 1:变量、字符串、list/dict、if/for(python学习-小白版-有经验可跳过)
python·学习·list