部署到不同环境的应用会使用到各自的配置,如 Dev, QA, Stg, Prod 有自己的数据库等资源。Spring Boot 可采用 Profile 对应不同的环境,不同 Profile 选择自己的配置文件 application-${profile}.properties
。本人还是偏爱在同一个文件中分组配置,容易查错与编辑,类如在 application.properties 文件中以下面的方式
db.host=aaa
%dev.db.host=bbb
%prod.db.host=ccc
那么在 Python 的项目中应该如何针对不同环境进行配置呢?大概有以下几种
- 不同环境的 Config 类
- YAML 文件
- TOML 文件
- JSON 文件
- INI 文件
- dotenv(.env) 文件
第一种方式是本人推荐的,其他的方式只是在不同格式的配置文件中,按环境组织不同的配置值,其他方式的不同配置读入内存中基本是体现为字典变量。在 Python 配置中要支持像配置的 placeholder(像 ${host} 还需自己实现。
不同环境的 Config 类
在同一个文件中配置,方便用点号引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import os class Config: DB_HOST = "qa.example.com" DB_USER = "sa" class DevConfig(Config): DB_HOST = "dev.example.com" class QAConfig(Config): pass class ProdConfig(Config): DB_HOST = "prod.example.com" mapping = { 'dev': DevConfig, 'qa': QAConfig, 'prod': ProdConfig } APP_ENV = os.environ.get('APP_ENV', 'dev').lower() config = mapping[APP_ENV]() |
使用
1 2 3 |
from config import config print(config.DB_HOST) |
配置环境变量 APP_ENV
为 dev
, qa
, 或 prod
会输出不同的 DB_HOST 值
YAML 文件
需安装依赖 pyyaml
pip install pyyaml
config.yml 文件配置
1 2 3 4 5 6 7 8 9 10 |
default: db_host: qa.example.com dev: db_host: dev.example.com qa: prod: db_host: prod.example.com |
使用
1 2 3 4 5 6 7 8 |
import yaml from yaml import Loader with open("config.yml") as ymlfile: cfg = yaml.load(ymlfile, Loader) print(type(cfg)) print(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 中获取配置值
1 2 3 4 |
import os APP_ENV = os.environ.get('APP_ENV', 'dev').lower() print(cfg.get(APP_ENV, "default")["db_host"]) |
每次取值有些麻烦, 更高级一点的玩法是让 YAML 序列化为一个自定义对象,然后在自定义类中做文章,如新的 config.yml 配置中要告诉对应的类名
1 2 3 4 5 6 7 8 9 10 11 12 |
--- !Config default: db_host: qa.example.com dev: db_host: dev.example.com qa: prod: db_host: prod.example.com |
然后定义 Config 类,并使用相应的配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import yaml from yaml import Loader import os APP_ENV = os.environ.get('APP_ENV', 'dev').lower() class Config(yaml.YAMLObject): yaml_tag = u'!Config' def __int__(self, default, dev, qa, prod): self.default = default self.dev = dev self.qa = qa self.prod = prod def __getitem__(self, item): env_conf = getattr(self, APP_ENV) if hasattr(self, APP_ENV) else self.default env_conf = env_conf if env_conf else {} return env_conf[item] if item in env_conf else self.default[item] with open("config.yml") as ymlfile: cfg = yaml.load(ymlfile, Loader) print(type(cfg)) print(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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# This is a TOML document title = "TOML Example" [owner] name = "Tom Preston-Werner" dob = 1979-05-27T07:32:00-08:00 [database] enabled = true ports = [ 8000, 8001, 8002 ] data = [ ["delta", "phi"], [3.14] ] temp_targets = { cpu = 79.5, case = 72.0 } [servers] [servers.alpha] ip = "10.0.0.1" role = "frontend" [servers.beta] ip = "10.0.0.2" role = "backend" |
Python 的项目管理工具 Poetry 就是用 pyproject.toml 文件来管理依赖配置的。
使用 toml 一般安装
pip install toml
把前面的 config.yml 文件转换为 config.toml 文件,内容如下
1 2 3 4 5 6 7 8 9 10 |
[default] db_host="qa.example.com" [dev] db_host= "dev.example.com" [qa] [prod] db_host="prod.example.com" |
加载该 toml 文件
1 2 3 4 5 6 7 |
import toml with open('config.toml') as tomlfile: cfg = toml.load(tomlfile) print(type(cfg)) print(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 的话,代码可实现为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import toml import os APP_ENV = os.environ.get('APP_ENV', 'dev').lower() class Config: def __init__(self, default, dev, qa, prod): self.default = default self.dev = dev self.qa = qa self.prod = prod def __getitem__(self, item): env_conf = getattr(self, APP_ENV) if hasattr(self, APP_ENV) else self.default env_conf = env_conf if env_conf else {} return env_conf[item] if item in env_conf else self.default[item] with open('config.toml') as tomlfile: cfg = Config(**toml.load(tomlfile)) print(cfg['db_host']) |
变更 APP_ENV 环境变量的值为 qa, prod, 会输出以下相应的值
qa.example.com
prod.example.com
Config 类的 __init__
和 __getitem__
方法实现与前面的完全一样。
TOML 配置文件的表现力很丰富,更强大的功能还有待于日后去发掘。
JSON 文件
上面相应的配置文件变成 config.json 就是
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "default": { "db_host": "qa.example.com" }, "dev": { "db_host": "dev.example.com" }, "qa": {}, "prod": { "db_host": "prod.example.com" } } |
使用代码
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import json import os with open("config.json") as jsonfile: cfg = json.load(jsonfile) print(type(cfg)) print(cfg) APP_ENV = os.environ.get('APP_ENV', 'dev').lower() env_conf = cfg.get(APP_ENV) env_conf = env_conf if env_conf else cfg['default'] print(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 的例子差不多了
1 2 3 4 5 6 7 8 9 10 |
import json class Config: # 实现代码与前方 TOML 中的 Config 完全相同,故省略 with open("config.json") as jsonfile: cfg = Config(**json.load(jsonfile)) print(cfg["db_host"]) |
或者整体 JSON 对象可转换为一个 SimpleNamespace
1 2 3 4 5 |
from types import SimpleNamespace with open("config.json") as jsonfile: namespace = json.load(jsonfile, object_hook=lambda d: SimpleNamespace(**d)) # 再把 namespace 转换为 Config 对象 |
INI 文件
ini 文件以前广泛应用在 Windows 中作为配置文件的格式,Python 也内置了对它的支持,格式上有点像 TOML 但它不支持嵌套类型。这里只提下 INI 文件的简单读取
config.ini
1 2 3 4 5 6 7 8 9 10 |
[default] db_host=qa.example.com [dev] db_host=dev.example.com [qa] [prod] db_host=prod.example.com |
使用
1 2 3 4 5 6 7 |
import configparser cfg = configparser.ConfigParser() cfg.read("config.ini") print(type(cfg)) host = cfg['dev']['db_host'] print(host) |
输出为
<class 'configparser.ConfigParser'>
dev.example.com
由于只有一个层次的 Section 系列,不易于扩展,实际中应用较为狭窄,不作细究。
dotenv(.env) 文件
基本思路是把 .env
文件中的配置转换为环境变量,可由 os.environ().get(key) 获得,相当于 Linux 下的环境配置 env.sh
1 2 |
export DOMAIN=example.org export ADMIN_EMAIL=admin@${DOMAIN} |
然后
source env.sh
相应的 DOMAIN 和 ADMIN_EMAIL 就出现在了 env
列出的环境变量中
Python 的 dotenv 有两个实现库
第一个是 python-dotenv, 安装
pip install python-dotenv
我们在当前目录中创建一个 .env
文件,其中内容为
1 2 3 4 |
# Development settings DOMAIN=example.org ADMIN_EMAIL=admin@${DOMAIN} ROOT_URL=${DOMAIN}/app |
使用方式
1 2 3 4 5 6 7 |
from dotenv import load_dotenv import os load_dotenv() print(os.environ.get("DOMAIN")) print(os.environ.get("ADMIN_EMAIL")) |
输出
example.org
admin@example.org
load_dotenv()
可以指定不同的文件, 例如采用基于环境区分的文件命名
- .env -- 默认的配置
- .env_dev
- .env_qa
- .env_prod
加载文件
1 2 3 4 5 6 7 8 9 |
from dotenv import load_dotenv import os load_dotenv('.env') # 先加载默认的 .env 文件 APP_ENV = os.environ.get('APP_ENV', 'dev').lower() load_dotenv(f'.env_{APP_ENV}', override=True) # 再加载环境相关的, print(os.environ) |
先加载默认的 .env, 再加载环境相关的 .env_dev,这样 .env_dev 中的相同属性会覆盖 .env 中的配置。
还有一个 django 的实现 django-environ
, 但其中夹带了太多的私货, 如 environ.Env() 中有一些特定的配置项(db_url, cache_url 等),严格来说,它算不上通用 dotenv 实现。它加载 .env 文件时的行为与 python-dotenv 类似,如
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import environ import os env = environ.Env() env.read_env() # 加载 .env 文件 APP_ENV = os.environ.get('APP_ENV', 'dev').lower() env.read_env(f'.env_{APP_ENV}', overwrite=True) # 加载环境相关的,如 .env_qa print(os.environ) print(os.environ["ADMIN_EMAIL"]) print(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 Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。