前言
按照個人習慣,項目伊始我會按照如下結構組織項目配置,也就是配置文件放在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
補充
對於新項目,可以試試把配置全部放到環境變量,各個模塊實現各自的配置模塊。好處就是找起來方便,而且從環境變量中讀取配置也不用操心文件讀取的阻塞問題。目前很多運行在k8s上的服務喜歡用這種讀取環境變量的配置方式。缺點可能就是維護起來不太方便,畢竟配置被打散了,交接給別人的話,別人可能得到處找配置。