最近在阅读源代码时看到项目中Config类的实现方式,觉得这种实现方式结构感很强,非常清晰。所以总结一下分享在下面。在这里可以找到全部的代码。

先说明下需求,毕竟不是所有的项目都需要这样一个稍微复杂的实现:

  1. 可以从YAML配置文件中读取配置。
  2. 可以从环境变量中读取配置(优先级弱于YAML)。
  3. 如果某个配置项不在YAML或环境变量中,会取默认值。
  4. 可以生成带有默认值的YAML配置文件。
  5. 可以对输入的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")

这里可以找到全部的代码。