Python / pydantic
===
###### tags: `Python`
###### tags: `Python`, `PyPi`, `pydantic`, `BaseModel`, `BaseSettings`, `type`, `型別`, `typing`, `hint`, `annotation`, `env`
<br>
[TOC]
<br>
## intro
### 安裝
```
pip install pydantic
```
<br>
## BaseModel
> - 將真實物件,以一個抽象化物件表示
> - [[doc] Models](https://docs.pydantic.dev/latest/concepts/models/)
<br>
### example1: create instance
- ### 參數缺失
```python
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
p = Person()
```
- #### 執行結果
[](https://i.imgur.com/E46ZueH.png)
- #### 設定 name 後:
[](https://i.imgur.com/1LpReTo.png)
- #### 再設定 age 後,就沒有錯誤

- ### 參數類型錯誤
- 可接受字串類型的數值

- 錯誤型別

<br>
### example2: verify instance
- Question
```python
from pydantic import BaseModel
class Prompt(BaseModel):
id: int
type: str
lang: str
sentences: List[str]
order: Optional[int]
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"type": "對話提問",
"lang": "en",
"sentences": [
"句子1。",
"句子2。",
"句子3。",
],
"order": 1,
},
}
```
我要如何驗證和測試 Prompt 類別?
- ChatGPT4
為了驗證和測試你的 Prompt 類,你可以透過以下步驟進行:
1. **安裝依賴**
確保你已經安裝了 pydantic。 如果沒有安裝,可以透過pip安裝:
```bash
pip install pydantic
```
2. **導入需要的類別和方法**
你的程式碼中已經匯入了BaseModel。 為了使用清單(List)和可選類型(Optional),你還需要匯入這些類型:
```Python
from typing import List, Optional
```
3. **編寫測試程式碼**
建立一些範例數據,然後嘗試建立 Prompt 類別的實例。 這會自動進行驗證,如果資料不符合模型定義,將會拋出錯誤。
以下是一個基本的測試腳本:
```Python=
from pydantic import BaseModel, ValidationError
from typing import List, Optional
class Prompt(BaseModel):
id: int
type: str
lang: str
sentences: List[str]
order: Optional[int]
class Config:
orm_mode = True
schema_extra = {
"example": {
"id": 1,
"type": "對話提問",
"lang": "en",
"sentences": [
"句子1。",
"句子2。",
"句子3。",
],
"order": 1,
},
}
# 測試有效的數據
try:
prompt = Prompt(
id=1,
type="對話提問",
lang="en",
sentences=[
"句子1。",
"句子2。",
"句子3。",
],
order=1
)
print("有效的 Prompt 實例:", prompt)
except ValidationError as e:
print("驗證錯誤:", e)
# 測試無效的數據
try:
prompt_invalid = Prompt(id='非數字') # ID應該是整數
except ValidationError as e:
print("驗證錯誤:", e)
```
這段程式碼首先嘗試建立一個有效的Prompt實例。 如果資料結構正確,它會列印出實例。 然後,它嘗試創建一個無效的實例(在這裡,id不是整數),應該會拋出一個ValidationError。
4. **運行測試**
運行你的測試腳本,並觀察輸出。 有效的實例應該不會引發任何錯誤,而無效的資料應該會觸發驗證錯誤,並列印相關資訊。
5. **進一步的測試**
- 邊界條件測試:嘗試使用空字串、特別長的字串、不同的語言等資料來測試你的模型。
- 整合測試:如果你的`Prompt`類別是應用程式的一部分,考慮寫一些整合測試來檢查它與應用程式其他部分的交互作用。
透過這些步驟,你可以驗證和測試你的 `Prompt` 類別,確保它能按照預期運作,並且能夠正確處理各種資料。
<br>
### example3: instance + jsonable
- Question
```
from pydantic import BaseModel
class Item(BaseModel):
title: str
timestamp: datetime
description: str | None = None
```
我要如何生出 Item 實例進行測試
- ChatGPT3.5
要生成 Item 實例進行測試,你可以使用 `pydantic` 的 `parse_obj` 函數,它可以從字典或其他可迭代對象創建 Item 的實例。在這種情況下,你可以使用字典來初始化 Item:
```python=
from pydantic import BaseModel
from datetime import datetime
class Item(BaseModel):
title: str
timestamp: datetime
description: str | None = None
# 使用 parse_obj 生成 Item 實例
item_data = {
"title": "Example Item",
"timestamp": "2023-12-22T12:00:00",
"description": "This is an example item."
}
item_instance = Item.parse_obj(item_data)
# 現在 item_instance 是一個 Item 的實例,你可以訪問它的屬性
print(item_instance.title)
print(item_instance.timestamp)
print(item_instance.description)
```
在這個示例中,我們使用 parse_obj 函數根據 item_data 字典生成了 Item 的實例。你可以根據你的測試需求提供不同的 item_data。
<br>
### example4: instance + config
- Question
```python=
from pydantic import BaseModel
class Prompt(BaseModel):
id: int
type: str
lang: str
sentences: List[str]
order: Optional[int]
class Config:
orm_mode = True
schema_extra = {
"example": {
"id": 1,
"type": "對話提問",
"lang": "en",
"sentences": [
"請規劃台北兩日遊,並推薦每天的景點及說明其特色。",
"請幫我安排健康飲食計畫。",
"請建議我可以快速完成的食譜。",
],
"order": 1,
},
}
```
Config 類別作用為何?
- ChatGPT3.5
在 Pydantic 中,Config 類別用於配置 Pydantic 模型的行為。它允許你定制模型的行為方式,以滿足不同的需求。以下是一些常見的 Config 選項以及它們的作用:
1. `orm_mode`: 如果設置為 `True`,則表示該模型用於映射到 SQLAlchemy ORM 或其他 ORM 框架的數據模型,這將啟用一些特定於 ORM 的轉換。
2. `allow_population_by_field_name`: 如果設置為 `True`,則允許通過字段名稱(而不僅僅是模型字段名稱)來填充模型的屬性。這在某些情況下很有用,例如在解析請求數據時,可以使用 HTTP 表單字段名稱來填充模型。
3. `schema_extra`: 這是一個字典,可以包含額外的 JSON Schema 信息,用於文檔生成和驗證等目的。在你的示例中,它用於提供示例數據,以便在生成文檔時顯示模型的示例。
`Config` 類別允許你根據你的需求自定義模型的行為,並使 Pydantic 更加靈活和強大。你可以根據具體情況來使用這些選項。
- 查看另一篇筆記
- [Python / sqlalchemy - sqlalchemy + orm_mode](https://hackmd.io/HOyXTFU9RqKG8IIBNo4wRA#sqlalchemy--orm_mode)
<br>
## BaseModel / Config
### 用途
```python=
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"name": "Apple",
"description": "A fresh apple from an organic farm"
}
}
@app.post('/create')
async def create_item(item: Item):
return item
def main():
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=39999, reload=True)
if __name__ == '__main__':
main()
```
執行結果:
```bash
$ python main.py
# or
$ uvicorn main:app --host 0.0.0.0 --port 39999 --reload
```
- http://localhost:39999/docs
[](https://hackmd.io/_uploads/r1MJG-Q7R.png)
<br>
### 討論
- **Question**
BaseModel / Config / schema_extra 的具體用途是什麼? 沒有定義會怎樣?
- **ChatGPT4**
### BaseModel、Config、schema_extra
1. **BaseModel**:
`BaseModel` 是 Pydantic 的核心功能之一,它用於建立資料模型。 這些資料模型用於資料驗證、序列化(轉換為 JSON)、和反序列化(從 JSON 轉換回 Python 資料類型)。 `BaseModel` 提供了一個結構化的方式來定義資料的形狀,包括資料類型、預設值、選填或必填欄位等,並自動處理資料類型的轉換和驗證。
2. **Config**:
`Config` 是一個內嵌在 `BaseModel` 內部的類,用於配置模型的行為。 這些配置可以影響資料驗證的細節、JSON 序列化行為、模型欄位的順序等。 例如,你可以設定是否允許任意類型的欄位、是否使用列舉的值進行序列化、以及是否允許額外的欄位等。
3. **schema_extra**:
`schema_extra` 是在 `Config` 類別中可定義的一個方法,它允許你為模型產生的 JSON Schema 添加自訂的額外資料。 這對於產生文件或與外部系統整合時非常有用,因為你可以添加描述、範例、或其他任何有助於使用這個模型的資訊。
### 沒有定義 Config 或 schema_extra 會怎麼樣?
- 如果你**不定義 `Config`**,那麼 Pydantic 將使用預設的配置來處理你的模型。 這意味著模型將按照 Pydantic 的預設行為來驗證資料和序列化,例如不允許未聲明的欄位、不使用任意類型的欄位、枚舉使用其成員名稱進行序列化等。
- 如果你**不定義 `schema_extra`**,模型的 JSON Schema 僅包含由 Pydantic 自動產生的基於欄位定義的部分。 這意味著 Schema 將不包含任何自訂的描述、範例或其他說明性資訊。 這可能不會影響模型的功能,但可能會讓產生的 API 文件或用於整合的 Schema 變得不那麼清晰或易於理解。
### 總結
`BaseModel`、`Config` 和 `schema_extra` 提供了一個靈活且強大的方式來定義、配置和豐富你的資料模型。 它們使 Pydantic 不僅僅是一個資料驗證工具,更是一個強大的資料建模和 API 文件工具。 沒有它們中的某些部分不會影響基本功能,但利用它們可以顯著提高你的應用或服務的可維護性、可擴展性和易用性。
<br>
## Field
### 討論
- Question
`Field(..., description="The name of the item.")`
參數為 "...",表示什麼意思?
- ChatGPT4
在Pydantic 的Field函數中使用...(三個點,也稱為ellipsis)作為參數,具有特定的意義。在這個上下文中,...表示該欄位是必須的(mandatory)。這告訴Pydantic 這個欄位在創建模型的實例時必須被提供,否則將拋出一個驗證錯誤。
### 用途
使用`...`作為`Field`的第一個參數時,你在定義模型時正在指定:
- 這個字段不能被省略,它不是可選的。
- 當你嘗試創建模型的實例而沒有為這個字段提供值時,Pydantic 將驗證失敗並拋出錯誤。
### 範例
這裡是一個簡單的例子來說明如何使用`...`:
```Python
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(..., description="The name of the product.")
price: float = Field(..., description="The price of the product.", gt=0)
description: str = Field(None, description="A brief description of the product.")
product = Product(name="Coffee", price=2.99)
```
在這個例子中:
- name和price欄位使用了`...`作為`Field`的第一個參數,表示這兩個欄位是必要的。
- 如果嘗試建立一個 `Product` 實例而沒有提供 `name` 或 `price`,將會觸發驗證錯誤。
- `description` 欄位則是可選的,因為它的預設值是`None`(不是`...`)。
### 小貼士
- 使用`...`是Pydantic 特有的一種方式,它是基於Python 的Ellipsis 物件。在Python 中,Ellipsis 通常用於其他目的(如在切片操作中表示完整切片),但在Pydantic 中被用作表示必需欄位的一個方便的工具。
- 除了`...`,你也可以透過類型註解與預設值結合的方式來控製欄位的可選性和預設行為。例如,使用`Optional[str]`來宣告一個可選的字串欄位。
<br>
## BaseSettings
- ### 從環境變數讀取資料
```python
from pydantic import BaseSettings
class Settings(BaseSettings):
HOME: str
SHELL: str
s = Settings()
print('Settings():', s)
print('- HOME:', s.HOME)
print('- SHELL:', s.SHELL)
```
- [用 pydantic 輕鬆進行設定管理(settings management)](https://myapollo.com.tw/zh-tw/python-pydantic/) :+1: :+1: :100:
- ### 限制
- 當 type 為 bool
```python=
%env Force_Trim=1
from pydantic import BaseSettings
class Config(BaseSettings):
FORCE_TRIM: bool = None
if Config().FORCE_TRIM:
print('true')
else:
print('false')
```
- `0/1`, `true/false`, `t/f`, `yes/no`, `y/n` 可接受
- 但輸入 -1 會有 error

```
ValidationError: 1 validation error for Config
FORCE_TRIM
value could not be parsed to a boolean (type=type_error.bool)
```
<br>
## 實作原理
### 查看類別成員
```python
class A:
def __init__(self):
print('calling A.__init__()')
class B(A):
name: str = 'tj'
age: int
print('B.name:', B.name)
dir(B)
```
- #### 執行結果

備註:要有定義值,屬性才會出現
<br>
### 不同實例呈現出的屬性值
```python=
class A:
def __init__(self):
print('calling A.__init__()')
class B(A):
name: str = 'tj'
age: int
b1 = B()
b1.name = 'tj_tsai'
b1.age = 25
b2 = B()
b2.name = 'tsungjung'
b2.age = 18
print('b1.name:', b1.name)
print('b2.name:', b2.name)
print('B.name:', B.name)
print()
print('dir(b1):', dir(b1))
print('dir(b2):', dir(b2))
print('dir(B):', dir(B))
```

<br>
### 查看 hint
```python=
class A:
def __init__(self):
print('calling A.__init__()')
class B(A):
name: str
age: int
from typing import get_type_hints
get_type_hints(B)
```
- #### 執行結果

- #### 沒定義初始值、或沒給 hint 會有 error

- #### 有定義初始值,但沒給 hint

## BaseModel
<br>
<hr>
<br>
## 參考資料
- ### [[doc] pydantic](https://pydantic.readthedocs.io/)
- ### [pydantic 小筆記](https://editor.leonh.space/2022/pydantic/)
- 連接 Twitter API 的帳密、連接 MongoDB 的帳密等等,
這類參數習慣上會把它與程式碼分開,另外放在 .env 檔案內,
再透過像 [python-dotenv](https://github.com/theskumar/python-dotenv) 這樣的套件把 .env 讀入成為可調用的變數
:::warning
:bulb: **說個題外話**
關於 .env 檔案,有一條常見的鐵律是不要把 .env 提交到版控系統,個人是不太認同,只要我的 Git repository 是私有的,自架的,本機的,提交到 Git repository 又何妨?版控≠開源,當我的專案是閉源的,.env 的地位就與任何一份原碼一樣隱私,不具有特殊地位,我們要守護的也不僅是 .env,而是整個專案。
:::
- [Pedantic Configuration Management with Pydantic](https://rednafi.github.io/digressions/python/2020/06/03/python-configs.html)
- [用 pydantic 輕鬆進行設定管理(settings management)](https://myapollo.com.tw/zh-tw/python-pydantic/) :+1: :+1: :100:
- ### [[stackoverflow] How to get class variables and type hints?](https://stackoverflow.com/questions/52838204/)
- ### [Python Type Hints 从入门到实践](https://segmentfault.com/a/1190000040864758)