Python 基于多环境的配置方式

部署到不同环境的应用会使用到各自的配置,如 Dev, QA, Stg, Prod 有自己的数据库等资源。Spring Boot 可采用 Profile 对应不同的环境,不同 Profile 选择自己的配置文件 application-${profile}.properties。本人还是偏爱在同一个文件中分组配置,容易查错与编辑,类如在 application.properties 文件中以下面的方式

1db.host=aaa
2%dev.db.host=bbb
3%prod.db.host=ccc

那么在 Python 的项目中应该如何针对不同环境进行配置呢?大概有以下几种
  1. 不同环境的 Config 类
  2. YAML 文件
  3. TOML 文件
  4. JSON 文件
  5. INI 文件
  6. dotenv(.env) 文件

第一种方式是本人推荐的,其他的方式只是在不同格式的配置文件中,按环境组织不同的配置值,其他方式的不同配置读入内存中基本是体现为字典变量。在 Python 配置中要支持像配置的 placeholder(像 ${host} 还需自己实现。

不同环境的 Config 类

在同一个文件中配置,方便用点号引用
 1import os
 2
 3class Config:
 4    DB_HOST = "qa.example.com"
 5    DB_USER = "sa"
 6
 7class DevConfig(Config):
 8    DB_HOST = "dev.example.com"
 9
10class QAConfig(Config):
11    pass
12
13class ProdConfig(Config):
14    DB_HOST = "prod.example.com"
15
16mapping = {
17    'dev': DevConfig,
18    'qa': QAConfig,
19    'prod': ProdConfig
20}
21
22APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
23config = mapping[APP_ENV]()

使用
1from config import config
2
3print(config.DB_HOST)

配置环境变量 APP_ENVdev, qa, 或 prod 会输出不同的 DB_HOST 值

YAML 文件

需安装依赖 pyyaml
pip install pyyaml
config.yml 文件配置
 1default:
 2  db_host: qa.example.com
 3
 4dev:
 5  db_host: dev.example.com
 6
 7qa:
 8
 9prod:
10  db_host: prod.example.com 

使用
1import yaml
2from yaml import Loader
3
4with open("config.yml") as ymlfile:
5    cfg = yaml.load(ymlfile, Loader)
6
7print(type(cfg))
8print(cfg)

cfg 是一个字典,所以上面的输出为
<class 'dict'>
{'default': {'db_host': 'qa.example.com'}, 'dev': {'db_host': 'dev.example.com'}, 'qa': None, 'prod': {'db_host': 'prod.example.com'}}
如果结合环境变量 APP_ENV 从 cfg 中获取配置值
1import os
2
3APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
4print(cfg.get(APP_ENV, "default")["db_host"])

每次取值有些麻烦, 更高级一点的玩法是让 YAML 序列化为一个自定义对象,然后在自定义类中做文章,如新的 config.yml 配置中要告诉对应的类名
 1--- !Config
 2
 3default:
 4  db_host: qa.example.com
 5
 6dev:
 7  db_host: dev.example.com
 8
 9qa:
10
11prod:
12  db_host: prod.example.com

然后定义 Config 类,并使用相应的配置项
 1import yaml
 2from yaml import Loader
 3import os
 4
 5APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
 6
 7class Config(yaml.YAMLObject):
 8    yaml_tag = u'!Config'
 9
10    def __int__(self, default, dev, qa, prod):
11        self.default = default
12        self.dev = dev
13        self.qa = qa
14        self.prod = prod
15
16    def __getitem__(self, item):
17        env_conf = getattr(self, APP_ENV) if hasattr(self, APP_ENV) else self.default
18        env_conf = env_conf if env_conf else {}
19        return env_conf[item] if item in env_conf else self.default[item]
20
21
22with open("config.yml") as ymlfile:
23    cfg = yaml.load(ymlfile, Loader)
24
25print(type(cfg))
26
27print(cfg["db_host"])

上面的代码输出
<class '__main__.Config'>
dev.example.com
 再改变 APP_ENV 环境变量为 qa 和 prod 时对应的 cfg["db_host"] 的值分别为
qa.example.com
prod.example.com

TOML 文件

TOML(Tom's Obvious Minimal Language),初看它的格式像 ini 文件,其实它对 ini 格式强悍许多,支持丰富的数据类型,如布尔型,整数,浮点数,时间,日期,列表和字典等,下面是官方的一个配置样例
 1# This is a TOML document
 2
 3title = "TOML Example"
 4
 5[owner]
 6name = "Tom Preston-Werner"
 7dob = 1979-05-27T07:32:00-08:00
 8
 9[database]
10enabled = true
11ports = [ 8000, 8001, 8002 ]
12data = [ ["delta", "phi"], [3.14] ]
13temp_targets = { cpu = 79.5, case = 72.0 }
14
15[servers]
16
17[servers.alpha]
18ip = "10.0.0.1"
19role = "frontend"
20
21[servers.beta]
22ip = "10.0.0.2"
23role = "backend"

Python 的项目管理工具 Poetry 就是用 pyproject.toml 文件来管理依赖配置的。

使用 toml 一般安装
pip install toml
把前面的 config.yml 文件转换为 config.toml 文件,内容如下
 1[default]
 2db_host="qa.example.com"
 3
 4[dev]
 5db_host= "dev.example.com"
 6
 7[qa]
 8
 9[prod]
10db_host="prod.example.com"

加载该 toml 文件
1import toml
2
3with open('config.toml') as tomlfile:
4    cfg = toml.load(tomlfile)
5
6print(type(cfg))
7print(cfg)

输出为
<class 'dict'>
{'default': {'db_host': 'qa.example.com'}, 'dev': {'db_host': 'dev.example.com'}, 'qa': {}, 'prod': {'db_host': 'prod.example.com'}}
要取随 APP_ENV 环境而变的 db_host 的话,代码可实现为
 1import toml
 2import os
 3
 4APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
 5
 6class Config:
 7    def __init__(self, default, dev, qa, prod):
 8        self.default = default
 9        self.dev = dev
10        self.qa = qa
11        self.prod = prod
12
13    def __getitem__(self, item):
14        env_conf = getattr(self, APP_ENV) if hasattr(self, APP_ENV) else self.default
15        env_conf = env_conf if env_conf else {}
16        return env_conf[item] if item in env_conf else self.default[item]
17
18
19with open('config.toml') as tomlfile:
20    cfg = Config(**toml.load(tomlfile))
21
22print(cfg['db_host'])

变更 APP_ENV 环境变量的值为 qa, prod, 会输出以下相应的值
qa.example.com prod.example.com

Config 类的 __init____getitem__ 方法实现与前面的完全一样。

TOML 配置文件的表现力很丰富,更强大的功能还有待于日后去发掘。

JSON 文件

上面相应的配置文件变成 config.json 就是
 1{
 2  "default": {
 3    "db_host": "qa.example.com"
 4  },
 5  "dev": {
 6    "db_host": "dev.example.com"
 7  },
 8  "qa": {},
 9  "prod": {
10    "db_host": "prod.example.com"
11  }
12}

使用代码
 1import json
 2import os
 3
 4with open("config.json") as jsonfile:
 5    cfg = json.load(jsonfile)
 6
 7print(type(cfg))
 8print(cfg)
 9
10APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
11env_conf = cfg.get(APP_ENV)
12env_conf = env_conf if env_conf else cfg['default']
13print(env_conf['db_host'])

输出
<class 'dict'>
{'default': {'db_host': 'qa.example.com'}, 'dev': {'db_host': 'dev.example.com'}, 'qa': {}, 'prod': {'db_host': 'prod.example.com'}}
qa.example.com
切换 APP_ENV 环境变量测试不同环境下的 db_host 值

json.load() 方法也能由 JSON 格式数据反序列化为一个自定义的对象,直接用 object_hook 参数把有嵌套的 JSON 转换成一个自定义对象可就不那么容易了。但通过自定义的 __init__ 方法就和前面 YAML 的例子差不多了
 1import json
 2
 3class Config:
 4    # 实现代码与前方 TOML 中的 Config 完全相同,故省略
 5
 6with open("config.json") as jsonfile:
 7    cfg = Config(**json.load(jsonfile))
 8
 9
10print(cfg["db_host"])

或者整体 JSON 对象可转换为一个 SimpleNamespace

1from types import SimpleNamespace<br/><br/>
2with open("config.json") as jsonfile:
3    namespace = json.load(jsonfile, object_hook=lambda d: SimpleNamespace(**d))
4    # 再把 namespace 转换为 Config 对象

INI 文件

ini 文件以前广泛应用在 Windows 中作为配置文件的格式,Python 也内置了对它的支持,格式上有点像 TOML 但它不支持嵌套类型。这里只提下 INI 文件的简单读取

config.ini
 1[default]
 2db_host=qa.example.com
 3
 4[dev]
 5db_host=dev.example.com
 6
 7[qa]
 8
 9[prod]
10db_host=prod.example.com

使用
1import configparser
2
3cfg = configparser.ConfigParser()
4cfg.read("config.ini")
5print(type(cfg))
6host = cfg['dev']['db_host']
7print(host)

输出为
<class 'configparser.ConfigParser'>
dev.example.com
由于只有一个层次的 Section 系列,不易于扩展,实际中应用较为狭窄,不作细究。

dotenv(.env) 文件

基本思路是把 .env 文件中的配置转换为环境变量,可由 os.environ().get(key) 获得,相当于 Linux 下的环境配置 env.sh
1export DOMAIN=example.org
2export ADMIN_EMAIL=admin@${DOMAIN}

然后
source env.sh
相应的 DOMAIN 和 ADMIN_EMAIL 就出现在了 env 列出的环境变量中

Python 的 dotenv 有两个实现库

第一个是 python-dotenv, 安装
pip install python-dotenv
我们在当前目录中创建一个 .env 文件,其中内容为
1# Development settings
2DOMAIN=example.org
3ADMIN_EMAIL=admin@${DOMAIN}
4ROOT_URL=${DOMAIN}/app

使用方式
1from dotenv import load_dotenv
2import os
3
4load_dotenv()
5
6print(os.environ.get("DOMAIN"))
7print(os.environ.get("ADMIN_EMAIL"))

输出
example.org
admin@example.org
load_dotenv() 可以指定不同的文件, 例如采用基于环境区分的文件命名

  • .env      -- 默认的配置
  • .env_dev
  • .env_qa
  • .env_prod


加载文件
1from dotenv import load_dotenv
2import os
3
4load_dotenv('.env')           # 先加载默认的 .env 文件
5
6APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
7load_dotenv(f'.env_{APP_ENV}', override=True) # 再加载环境相关的,
8
9print(os.environ)

先加载默认的 .env, 再加载环境相关的 .env_dev,这样 .env_dev 中的相同属性会覆盖 .env 中的配置。

还有一个 django 的实现 django-environ, 但其中夹带了太多的私货, 如 environ.Env() 中有一些特定的配置项(db_url, cache_url 等),严格来说,它算不上通用 dotenv 实现。它加载 .env 文件时的行为与 python-dotenv 类似,如
 1import environ
 2import os
 3
 4env = environ.Env()
 5
 6env.read_env()  # 加载 .env 文件
 7APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
 8
 9env.read_env(f'.env_{APP_ENV}', overwrite=True)  # 加载环境相关的,如 .env_qa
10
11print(os.environ)
12print(os.environ["ADMIN_EMAIL"])
13print(env.str("ADMIN_EMAIL"))

django-environ 中配置的值可以有类型,如 str, bool, int 等。它也像 python-dotenv 一样把 .env 文件中配置加到 os.environ 中去,因此既可通过 os.environ 来获取 .env 文件中配置的值,也能用它自己专有的 environ.Env() 的方式取得值。

另外,比起 python-dotenv 弱的地方就是它不支持 placeholder 的解析,.env 配置中的 ${DOMAIN} 将会被原样输出。 永久链接 https://yanbin.blog/python-multi-envs-configurations/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。