使用Pydantic管理应用配置

2023-04-13 16:30:35 浏览数 (2)

配置管理在现代应用开发和部署中至关重要,在十二要素应用(12 Factor App)中,配置管理也是第三个重要因素。

使用Pydantic库,我们可以方便灵活地在 Python 应用中管理配置。

使用 Pydantic

配置管理是Pydantic官方文档中列出的一个重要应用领域。

如果你创建了一个继承自 BaseSettings 的模型,模型初始化器将试图通过从环境中读取来确定任何没有作为关键字参数传递的字段的值。(如果匹配的环境变量没有被设置,默认值仍将被使用)。

简化了一下操作:

  1. 创建一个明确定义的、有类型提示的应用程序配置类。
  2. 自动从环境变量中读取对配置的修改。
  3. 在需要时手动覆盖初始化器中的特定设置(例如在单元测试中)。

接下来我们简单地介绍一下pydantic.BaseSettings的使用。

项目结构

我们将配置管理相关的代码放在app/config.py中。

代码语言:javascript复制
app/
├── __init__.py
├── config.py

定义配置类

config.py中定义 Settings 类,将应用的配置项定义为Settings的属性。

代码语言:javascript复制
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,用于测试应用配置。

代码语言:javascript复制
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类天然支持从环境变量获取配置。

代码语言:javascript复制
$ 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实例在某处业务逻辑中被修改(可能只是误操作),可能会对其他地方的业务逻辑产生影响。

代码语言:javascript复制
# 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函数的返回值,减少重复调用产生的开销(也避免了运行时环境变量发生改变的情况)。

代码语言:javascript复制
# 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内部类对配置类进行扩展,实现一些高阶功能。

代码语言:javascript复制
# 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内部类,并指定了一些属性,具体含义为:

  1. env_file = ".env", 支持从.env文件中获取配置项(需要python-dotenv依赖)。
  2. case_sensitive = True, 环境变量的名称大小写敏感(默认大小写不敏感)。
  3. env_prefix = "my_", 环境变量的前缀(默认无前缀)。
  4. fields属性可以对各个配置项进行额外的配置,在上述的示例中,我们定义了可以从redis_dsnredis_url两个环境变量中获取Settings.redis_dsn的配置。

为了测试custom_config.py,我们可以准备以下内容的.env文件。

代码语言:javascript复制
my_environment=test
MY_APP_KEY=MY_APP_KEY
redis_url=redis://localhost:6379/1

.env所在目录测试custom_config.py:

代码语言:javascript复制
$ 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可以方便的实现基于环境变量的应用配置管理,可以在业务代码中应用起来。

0 人点赞