Python 程序配置文件管理的最佳工程实践
背景
最近在结合 Python-3.12.0a6 版本开发一个多线程架构的后台服务;服务启动时会读取配置文件,并且要求所有线程共享同一份配置。
服务本身直接通过 http 接口来动态调整配置项的值,还要做到服务退出之后持久化配置项到配置文件。
一开始以为这个用 Python 写也会要用几百行 ,最后发现完成核心功能就只需要不到 50 行,Python 牛逼!!!
需求一:支持简单的配置项
假设我们目前只支持 name 和 port 两个配置项,多支持几个不难,只是不方便演示。
"""实例配置管理
"""
from dataclasses import dataclass
class Config(object):
name:str= "mysql"
port:int = 3306
看起来是没问题了,下面可以试用一下,也顺带引导出第二个需求。
In [6]: a = Config()
In [7]: b = Config()
In [8]: id(a)
4407850896 :
In [9]: id(b)
4407852496 :
可以看到两个配置对象的 ID 值不一样。由于配置文件只有一个,我们希望配置对象也只有一个。
需求二:配置对象全局唯一
交代一个背景,解释器在做 import 的时候是单一线程在跑的。有了这个前提我们可以少写一些加锁的代码,能少写一行算一行吧。
"""实例配置管理
"""
from dataclasses import dataclass
class Config(object):
name:str= "mysql"
port:int = 3306
_instance = None
# 单例模式
def __new__(cls, *args, **kw):
if cls._instance is None:
cls._instance = object.__new__(cls, *args, **kw)
return cls._instance
用 Python 就是这么的简单,几行代码就搞定了。但是还是要测试一下顺带引导出下一个需求。
In [4]: a = Config()
In [5]: b = Config()
In [6]: id(a)
4414751568 :
In [7]: id(b)
4414751568 :
现在配置对象已经是单例了,但还有一个问题,它的每个配置项的值都是默认值,我们当然是希望它在创建对象的时候是使用配置文件中的值啦。下面看需求三怎么实现。
需求三:根据配置文件构造配置对象
假设我们的配置文件被 “持久化” 到了 /tmp/config.json ,现在就可以写读取配置文件并更新配置对象值的代码了。
"""实例配置管理
"""
import json
import logging
from pathlib import Path
from dataclasses import dataclass
class Config(object):
name:str= "mysql"
port:int = 3306
_instance = None
# 单例模式
def __new__(cls, *args, **kw):
if cls._instance is None:
cls._instance = object.__new__(cls, *args, **kw)
return cls._instance
# 读取配置文件
def __post_init__(self):
"""如果配置文件存在就用配置文件中的值,覆盖默认值。在这个过程中如果遇到异常就保持默认值
"""
if (config_file:=Path("/tmp/config.json")) and config_file.exists():
try:
with open(config_file) as f:
json_data = json.loads(f.read())
self.__dict__.update(json_data)
except Exception as err:
pass
else:
logging.warn("config file '{}' not exists. well using defautl values .".format(config_file))
假设我们的配置文件内容是这样的。
cat /tmp/config.json
{
"name": "trump",
"port": 8848
}
下面的测试一下
In [2]: a = Config()
In [3]: a
Out[3]: Config(name='trump', port=8848)
In [4]: b = Config()
In [5]: b
Out[5]: Config(name='trump', port=8848)
In [6]: a == b
Out[6]: True
可以看到 name 和 port 已经没有使用默认的 "mysql" 和 3306 了,而是使用了配置文件中的值。
到这里我们只剩下最后一个需求,就是在程序退出的时候,把配置对象的值更新回配置文件。这个就看需求四怎么写。
需求四:程序退出前自动持久化配置对象到配置文件
解释器在退出前有个钩子(atexit),我们可以在这里指定回调函数,这个时候保存配置文件再适合不过。
"""实例配置管理
"""
import json
import atexit
import logging
from pathlib import Path
from dataclasses import dataclass, asdict
class Config(object):
name:str= "mysql"
port:int = 3306
_instance = None
# 单例模式
def __new__(cls, *args, **kw):
if cls._instance is None:
cls._instance = object.__new__(cls, *args, **kw)
return cls._instance
# 读取配置文件
def __post_init__(self):
"""如果配置文件存在就用配置文件中的值,覆盖默认值;在这个过程中如果遇到异常就保持默认值。程序退出时持久到到配置到文件。
"""
if (config_file:=Path("/tmp/config.json")) and config_file.exists():
try:
with open(config_file) as f:
json_data = json.loads(f.read())
self.__dict__.update(json_data)
except Exception as err:
pass
else:
logging.warn("config file '{}' not exists. well using defautl values .".format(config_file))
# 程序退出时保存配置到配置文件 /tmp/config.json
def sync_to_disk():
"""
"""
json_str = json.dumps(asdict(self), indent=4)
with open(config_file, 'w') as f:
logging.warning("save configs to '{}' ".format(config_file))
f.write(json_str)
atexit.register(sync_to_disk)
验证一下
In [1]: from appconfig import Config
In [2]: a = Config()
In [3]: a.name
Out[3]: 'trump'
In [4]: a.name = "hello-world"
In [5]: exit()
WARNING:root:save configs to '/tmp/config.json'
看日志是已经把配置项更新回配置文件了,但是还是 cat 确认一下为好。
cat /tmp/config.json
{
"name": "hello-world",
"port": 8848
}
可以看到确实已经把配置项的值更新到文件了。
链接:https://cloud.tencent.com/developer/article/2269479
(版权归原作者所有,侵删)
微信扫码关注该文公众号作者