配置管理在现代应用开发和部署中至关重要,在十二要素应用(12 Factor App)中,配置管理也是第三个重要因素。
使用Pydantic
库,我们可以方便灵活地在 Python 应用中管理配置。
使用 Pydantic
配置管理是Pydantic
官方文档中列出的一个重要应用领域。
如果你创建了一个继承自 BaseSettings 的模型,模型初始化器将试图通过从环境中读取来确定任何没有作为关键字参数传递的字段的值。(如果匹配的环境变量没有被设置,默认值仍将被使用)。
简化了一下操作:
- 创建一个明确定义的、有类型提示的应用程序配置类。
- 自动从环境变量中读取对配置的修改。
- 在需要时手动覆盖初始化器中的特定设置(例如在单元测试中)。
接下来我们简单地介绍一下pydantic.BaseSettings
的使用。
项目结构
我们将配置管理相关的代码放在app/config.py
中。
app/
├── __init__.py
├── config.py
定义配置类
在config.py
中定义 Settings
类,将应用的配置项定义为Settings
的属性。
from pydantic import BaseSettings, RedisDsn
class Settings(BaseSettings):
environment: str = "development"
app_key: str = "app_key"
app_secret: str = "app_secret"
redis_dsn: RedisDsn = "redis://localhost:6379"
def get_settings():
settings = Settings()
print(f"Loaded settings for environment: {settings.environment}")
return settings
测试配置类
临时编写一个app/export_config.py
,用于测试应用配置。
from app.config import get_settings
if __name__ == "__main__":
settings = get_settings()
print(settings.dict())
代码语言:javascript复制$ python -m app.export_config
Loaded settings for environment: development
{'environment': 'development', 'app_key': 'app_key', 'app_secret': 'app_secret', 'redis_dsn': RedisDsn('redis://localhost:6379/0', scheme='redis', host='localhost', host_type='int_domain', port='6379', path='/0')}
使用环境变量覆盖默认配置
很多情况下我们需要通过环境变量进行应用的配置,这也是 12 Factor App 中的推荐做法。
BaseSettings
类天然支持从环境变量获取配置。
$ export ENVIRONMENT=production
$ export APP_KEY=my_app_key
$ export APP_SECRET=my_app_secret
$ export REDIS_DSN=redis://localhost:6379/0
$ python -m app.export_config
Loaded settings for environment: production
{'environment': 'production', 'app_key': 'my_app_key', 'app_secret': 'my_app_secret', 'redis_dsn': RedisDsn('redis://localhost:6379/0', scheme='redis', host='localhost', host_type='int_domain', port='6379', path='/0')}
也可以只通过环境变量指定部分配置项。
代码语言:javascript复制ENVIRONMENT=test python -m app.export_config [11:39:50]
Loaded settings for environment: test
{'environment': 'test', 'app_key': 'app_key', 'app_secret': 'app_secret', 'redis_dsn': RedisDsn('redis://localhost:6379/0', scheme='redis', host='localhost', host_type='int_domain', port='6379', path='/0')}
使用全局变量还是函数
上面的示例中我们定义了get_settings
函数用于初始化并获取Settings
实例。
有些同学可能觉得在app/config.py
中定义一个全局变量settings
,在业务代码中从config.py
导入settings
获取配置项即可。
使用全局变量的一个问题在于,如果settings
实例在某处业务逻辑中被修改(可能只是误操作),可能会对其他地方的业务逻辑产生影响。
# global_settings.py
from pydantic import BaseSettings
class Settings(BaseSettings):
environment: str = "development"
settings = Settings()
def action_a():
print(f'{settings.environment=}')
settings.environment = "production"
def action_b():
print(f'{settings.environment=}')
if __name__ == "__main__":
action_a()
action_b()
代码语言:javascript复制$ python -m global_settings
settings.environment='development'
settings.environment='production'
在global_settings.py
的示例中,一旦action_a
函数被执行过,settings.environment
的值就会被永久修改为production
。可以预见到在真实业务中,这是一个非常危险的操作。
使用get_settings
函数获取配置信息就能避免受到业务代码修改全局的情况发生。
缓存get_settings
结果
使用get_settings
函数获取应用配置项的一个直接后果是业务代码中会有多次get_settings
函数的调用,产生一些额外的开销。
也存在应用运行过程中环境变量发生变化导致get_settings
返回值变化的可能性,不过在目前业务应用部署普遍容器化的背景下,可以忽略这个问题。
使用functools.lru_cache
装饰器,我们可以简单地缓存get_settings
函数的返回值,减少重复调用产生的开销(也避免了运行时环境变量发生改变的情况)。
# cache_config.py
from functools import lru_cache
from pydantic import BaseSettings
class Settings(BaseSettings):
environment: str = "development"
@lru_cache
def get_settings():
settings = Settings()
print(f"Loaded settings for environment: {settings.environment}")
return settings
def action_a():
print(f'{get_settings().environment=}')
def action_b():
print(f'{get_settings().environment=}')
if __name__ == "__main__":
action_a()
action_b()
代码语言:javascript复制$ python -m cache_config
Loaded settings for environment: development
get_settings().environment='development'
get_settings().environment='development'
可以看到使用functools.lru_cache
装饰器后,get_settings()
函数只被调用了一次,后续使用应用配置的地方会直接使用缓存的结果。
定义Config
内部类
我们可以通过定义Config
内部类对配置类进行扩展,实现一些高阶功能。
# custom_config.py
from pydantic import BaseSettings, RedisDsn
class Settings(BaseSettings):
environment: str = "development"
app_key: str = "app_key"
app_secret: str = "app_secret"
redis_dsn: RedisDsn = "redis://localhost:6379"
class Config:
env_file = ".env"
case_sensitive = True
env_prefix = "my_"
fields = {
'redis_dsn':{
'env':['redis_dsn','redis_url']
}
}
def get_settings():
settings = Settings()
print(f"Loaded settings for environment: {settings.environment}")
return settings
if __name__ == "__main__":
print(get_settings().dict())
在custom_config.py
中,我们在Settings
类中定义了一个Config
内部类,并指定了一些属性,具体含义为:
env_file = ".env"
, 支持从.env
文件中获取配置项(需要python-dotenv
依赖)。case_sensitive = True
, 环境变量的名称大小写敏感(默认大小写不敏感)。env_prefix = "my_"
, 环境变量的前缀(默认无前缀)。fields
属性可以对各个配置项进行额外的配置,在上述的示例中,我们定义了可以从redis_dsn
和redis_url
两个环境变量中获取Settings.redis_dsn
的配置。
为了测试custom_config.py
,我们可以准备以下内容的.env
文件。
my_environment=test
MY_APP_KEY=MY_APP_KEY
redis_url=redis://localhost:6379/1
在.env
所在目录测试custom_config.py
:
$ python -m custom_config
Loaded settings for environment: test
{'environment': 'test', 'app_key': 'app_key', 'app_secret': 'app_secret', 'redis_dsn': RedisDsn('redis://localhost:6379/1', scheme='redis', host='localhost', host_type='int_domain', port='6379', path='/1')}
.env
文件对初始化Settings
实例的影响包括:
- 规定了
my_
作为环境变量的前缀,Settings.enviornment
的值从.env
文件中的my_enviornment
中获取。 - 目前对环境变量的大小写敏感,虽然在
.env
文件中指定了MY_APP_KEY
的值,但是生效的app_key
的值仍然是默认值。 - 根据
Config.fields
的配置,Settings.redis_dsn
的值从.env
文件中的redis_url
中获取。
总结
使用pydantic
可以方便的实现基于环境变量的应用配置管理,可以在业务代码中应用起来。