Config类的一种实现方式
最近在阅读源代码时看到项目中Config
类的实现方式,觉得这种实现方式结构感很强,非常清晰。所以总结一下分享在下面。在这里可以找到全部的代码。
先说明下需求,毕竟不是所有的项目都需要这样一个稍微复杂的实现:
- 可以从YAML配置文件中读取配置。
- 可以从环境变量中读取配置(优先级弱于YAML)。
- 如果某个配置项不在YAML或环境变量中,会取默认值。
- 可以生成带有默认值的YAML配置文件。
- 可以对输入的YAML配置文件做简单的核查。
先创建一个ValueDesc
类。后面会用这个类去初始化Config
类中的每一项配置。这里面的env_var
属性是用来记录配置项对应的环境变量,如果未来读取不到此项值的时候,可以从环境变量中获得。
class Config:
class ValueDesc:
def __init__(self, env_var, type=str, required=True, default=None):
self.env_var = env_var
self.type = type
self.required = required
self.default = default
再用ValueDesc
类去定义Config
类中的每一个具体配置项。
class Config:
# 部分代码省略
PARAMETERS = {
"web.request.timeout": ValueDesc("WEB_MAX_REQUEST_TIMEOUT", type=int, required=False, default=600),
"web.request.maxSize": ValueDesc("WEB_MAX_HEADER_SIZE", type=int, required=False, default=32768),
"web.headers.clientType": ValueDesc("WEB_HEADERS_CLIENT_TYPE", type=str, required=True, default="Customize-Client"),
"web.headers.maxSize": ValueDesc("WEB_HEADERS_MAX_SIZE", type=int, required=True, default=1024),
"database.username": ValueDesc("DB_USERNAME", type=str, required=True),
"database.password": ValueDesc("DB_PASSWORD", type=str, required=True),
"database.schema": ValueDesc("DB_SCHEMA", type=str, required=True),
"database.poolSize": ValueDesc("DB_POOL_SIZE", type=int, required=False, default=10)
}
下面的代码用来实现需求4。这里为了让代码更少,使用了mergedeep第三方库。其实完全可以自己实现这个merge
方法,具体可参考Deep merge dictionaries of dictionaries in Python。
import yaml
from mergedeep import merge
from functools import reduce
class Config:
# 部分代码省略
def generate_yaml_with_default_values(self):
res = []
for key, value in Config.PARAMETERS.items():
# [::-1] means reverse.
items = key.split(".")[::-1]
default = value.default
# convert ['a','b','c'] to {'c':{'b':'a'}}
x = reduce(lambda v, k: {k: default} if v is None else {k: v}, items, None)
res.append(x)
# 将所有的dictionary合并到一起。
# 例如:
# 将
# {'web':{'request':{'timeout': 100}}}
# {'web':{'request':{'maxSize': 200}}}
# 合并成
# {'web':{'request':{'timeout': 100, 'maxSize': 200}}}
x = reduce(merge, res)
return yaml.dump(x, default_flow_style=False)
执行#generate_yaml_with_default_values()
这个函数,输出如下:
database:
password: null
poolSize: 10
schema: null
username: null
web:
headers:
clientType: Customize-Client
maxSize: 1024
request:
maxSize: 32768
timeout: 600
下面的代码用来实现需求1,需求2和需求3。这里请注意_get_config_value()
函数对config.yaml
文件的处理,相当于每次被调用时,都会重新读取文件中的内容,是有很大优化空间的,但为了便于展示,这里就不做优化了。
import os
import yaml
from functools import reduce
class Config:
# 部分代码省略
def _get_config_value(self, key):
config_value = yaml.safe_load(open("config.yaml", "r").read())
# 使用 "web.request.timeout"的方式来访问config_value中对应的值。
value = reduce(lambda data, k: data[k] if data and k in data else None, key.split('.'), config_value)
value_desc = Config.PARAMETERS.get(key)
# 此处是对 YAML文件中的值,环境变量的值 和 默认值 按照优先级进行取舍。
if value is None:
if not value_desc:
return None
value = os.getenv(value_desc.env_var, value_desc.default)
return value
关于需求5,实现的方式就是在_get_config_value()
中的if
后面,通过value_desc
中的required
属性和type
属性,对从YAML中读到的值做核查。为了让代码更少便于理解,这里就略过了。
程序到这里还没有完,为了方便在程序其他地方访问Config
类中的配置项,最终还是要用变量来记录所有的配置项。
class Config:
# 部分代码省略
def __init__(self):
self.web_request_timeout = self._get_config_value("web.request.timeout")
self.web_request_maxSize = self._get_config_value("web.request.maxSize")
self.web_headers_clientType = self._get_config_value("web.headers.clientType")
self.web_headers_maxSize = self._get_config_value("web.headers.maxSize")
self.database_username = self._get_config_value("database.username")
self.database_password = self._get_config_value("database.password")
self.database_schema = self._get_config_value("database.schema")
self.database_poolSize = self._get_config_value("database.poolSize")
在这里可以找到全部的代码。