用 pydantic 輕鬆進行設定管理(Settings management)
Posted on Jul 1, 2020 in Python 模組/套件推薦 , Python 程式設計 - 高階 by Amo Chen ‐ 3 min read
開發時,不免都會需要實作設定檔,藉由設定檔簡單地改變系統的行為。例如 Debug 模式的開關、資料庫相關設定等等,都通常會放在設定檔中。
而 Python 不僅提供內建模組 configparser 能夠簡單實作設定檔之外,也可以簡單利用 class 實作設定檔,例如以下 2 個檔案就是 class 形式的設定檔範例:
# settings.py
class Settings(object):
DB_HOST = 'localhost'
DB_PORT = 8888
settings = Settings()
# test.py
from settings import settings
print(settings.DB_HOST, settings.DB_PORT)
然而,現今開發專案也經常會使用 dotenv
(例如 python-dotenv )讓設定更加方便。
除了自行開發結合 dotenv
的功能之外,其實可以選擇利用 pydantic 輕鬆地整合 class 形式的設定檔與 dotenv
。
本文環境
- macOS 10.15
- Python 3.7
- pydantic 1.5.1
$ pip install pydantic==1.5.1
pydantic
pydantic 是透過型別註記(type annotations)提供資料型別驗證與設定管理的套件。
例如以下範例透過 pydantic 的 BaseModel 定義 Car 類別,同時也註記該類別各個屬性的型別:
from datetime import datetime
from pydantic import BaseModel
class Car(BaseModel):
brand_id: int
brand_name: str
wheels: int = 4
created_at: datetime = datetime.now()
當我們試圖建立該類別的實例(instance)時, pydantic 就會進行型別的驗證:
Car(brand_id='bad_id', brand_name='Bad name')
上述執行結果會出現類似以下的錯誤,顯示我們應該將 brand_id
設定為整數型別:
Traceback (most recent call last):
File "test.py", line 13, in <module>
Car(brand_id='bad_id', brand_name='Bad name')
File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Car
brand_id
value is not a valid integer (type=type_error.integer)
了解 pydantic 大致樣貌之後,接著可以試試 pydantic 提供的設定管理功能。
pydantic 設定管理
pydantic 的設定管理可以透過繼承 BaseSettings
類別進行實作,該類別與 BaseModel
類別最大差別在於 BaseSettings
提供環境變數(environment variables)與 dotenv
的整合。
當我們試圖建立繼承 BaseSettings
類別的實例(instance)時, BaseSettings
會試圖載入環境變數:
from pydantic import BaseSettings
class Settings(BaseSettings):
HOME: str
print(Settings())
上述範例執行結果如下,可以看到我們值都沒有設定給 HOME , pydantic 就自動為我們載入環境變數中的 HOME 到 Settings 中的 HOME :
HOME='/Users/<username>'
知道 BaseSettings
的用途之後,我們就能夠針對不同的環境設計不一樣的設定,例如以下範例,我們將通用的設定放在 Settings 類別中,藉由繼承 Settings 類別覆寫(override)因環境有所不同的設定:
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
API_BASE: str
DB_HOST: str = 'localhost'
DB_PORT: int = 8888
DB_NAME: str
TESTING: bool = False
class Production(Settings):
API_BASE = 'https://example.com/api'
DB_NAME = 'production'
class Testing(Settings):
API_BASE = 'https://testing.example.com/api'
DB_NAME = 'testing'
TESTING = True
def get_settings():
env = os.getenv('ENV', 'TESTING')
if env == 'PRODUCTION':
return Production()
return Testing()
settings = get_settings()
print('DB Host =', settings.DB_HOST)
print('DB Port =', settings.DB_PORT)
print('Override =', Production(TESTING=True).dict())
上述範例執行結果:
DB Host = localhost
DB Port = 8888
Override = {'API_BASE': 'https://example.com/api', 'DB_HOST': 'localhost', 'DB_PORT': 8888, 'DB_NAME': 'production', 'TESTING': True}
再來看看 pydantic 提供的型別驗證(Data validation)功能,當我們試圖將 DB_PORT 故意設定為字串時:
Production(DB_PORT='abc')
就會出現以下錯誤,該錯誤代表 DB_PORT 必須是整數才可以。這種型別驗證也能夠預防未來設定檔因為型別不一致所導致的問題:
Traceback (most recent call last):
File "pydantic_test.py", line 40, in <module>
Production(DB_PORT='abc')
File "pydantic/env_settings.py", line 28, in pydantic.env_settings.BaseSettings.__init__
File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Production
DB_PORT
value is not a valid integer (type=type_error.integer)
最後看看 pydantic 提供的 dotenv 整合功能。
BaseSettings
類別內可以透過定義 Config
類別,並指定 env_file
屬性,輕鬆整合 dotenv ,例如:
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
API_BASE: str
DB_HOST: str
DB_PORT: int
DB_NAME: str
TESTING: bool = False
class Config:
env_file = '.env'
class Production(Settings):
API_BASE = 'https://example.com/api'
DB_NAME = 'production'
class Testing(Settings):
API_BASE = 'https://testing.example.com/api'
DB_NAME = 'testing'
class Config:
env_file = '.testing.env'
def get_settings():
env = os.getenv('ENV', 'TESTING')
if env == 'PRODUCTION':
return Production()
return Testing()
settings = get_settings()
print('DB Host =', settings.DB_HOST)
print('DB Port =', settings.DB_PORT)
print('Override =', Production(TESTING=True).dict())
上述範例中的第 13, 14 行以及第 26, 27 行就是針對不同環境載入不同dotenv 設定檔的部分。
.env
與 .testing.env
2 個檔案的內容以及在檔案系統中的位置如下:
.
├── .env
├── .testing.env
└── settings.py
.env 內容:
DB_HOST=localhost
DB_PORT=8888
BLAH_BLAH=Banana
.testing.env 內容:
DB_HOST=127.0.0.1
DB_PORT=6666
整合 dotenv 之後的執行結果為:
DB Host = localhost
DB Port = 8888
Override = {'API_BASE': 'https://example.com/api', 'DB_HOST': 'localhost', 'DB_PORT': 8888, 'DB_NAME': 'production', 'TESTING': True}
必須注意的是 .env
中的 BLAH_BLAH=Banana
並不是前述 Settings
與 Production
2 類別內所定義的屬性之一,所以並不會被自動載入到 Production
內,如果想要載入 BLAH_BLAH
就必須為 Settings
或者 Production
類別內定義 BLAH_BLAH
屬性才行。
以上就是利用 pydantic 進行設定管理的簡單介紹。
事實上 pydantic 還有其他不一樣的用途,例如產生 JSON schema, 整合 Python 3.7 dataclass 等等,有興趣的話可以詳閱 官方文件 ,肯定可以帶來不少收穫。