Python 類型入門¶
Python 支援可選的「類型提示」(也稱為「類型註釋」)。
這些「類型提示」或註釋是一種特殊的語法,允許宣告變數的類型。
透過宣告變數的類型,編輯器和工具可以提供更好的支援。
這只是一個關於 Python 類型提示的快速教程/複習。它僅涵蓋使用FastAPI所需的最低限度… 實際上非常少。
FastAPI 完全基於這些類型提示,它們賦予它許多優點和好處。
但即使您從未使用過 FastAPI,您也會從學習一些相關知識中受益。
備註
如果您是 Python 專家,並且已經了解所有關於類型提示的知識,請跳到下一章。
動機¶
讓我們從一個簡單的例子開始
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
呼叫此程式會輸出
John Doe
該函式執行以下操作
- 接受
first_name
和last_name
。 - 使用
title()
將每個名稱的首字母轉換為大寫。 - 串接 它們,中間用空格隔開。
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
編輯它¶
這是一個非常簡單的程式。
但現在想像一下,您是從頭開始編寫它。
在某個時候,您會開始定義函式,您已經準備好了參數…
但是接下來您必須呼叫「那個將首字母轉換為大寫的方法」。
是 upper
嗎?是 uppercase
嗎?first_uppercase
?capitalize
?
然後,您嘗試使用程式設計師的老朋友,編輯器自動完成。
您輸入函式的第一個參數 first_name
,然後輸入一個點 (.
),然後按 Ctrl+Space
觸發自動完成。
但是,遺憾的是,您沒有得到任何有用的資訊
新增類型¶
讓我們修改上一版本中的一行。
我們將把函式的參數,從
first_name, last_name
改為
first_name: str, last_name: str
就這樣。
這些就是「類型提示」
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
這與宣告預設值不同,例如
first_name="john", last_name="doe"
這是不同的。
我們使用的是冒號 (:
),而不是等號 (=
)。
而且新增類型提示通常不會改變沒有它們時會發生的情況。
但現在,想像您再次在建立該函式的過程中,但使用了類型提示。
在同一個地方,您嘗試使用 Ctrl+Space
觸發自動完成,您會看到
這樣,您可以滾動查看選項,直到找到「感覺對」的選項
更多動機¶
檢查這個函式,它已經有類型提示
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
由於編輯器知道變數的類型,您不僅可以獲得自動完成,還可以獲得錯誤檢查。
現在您知道您必須修復它,使用 str(age)
將 age
轉換為字串。
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
宣告類型¶
您剛剛看到了宣告類型提示的主要位置:作為函數參數。
這也是您在 FastAPI 中主要使用它們的地方。
簡單類型¶
您可以宣告所有標準 Python 類型,而不僅僅是 str
。
例如,您可以使用:
int
float
bool
bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_d, item_e
具有類型參數的泛型類型¶
有一些資料結構可以包含其他值,例如 dict
、list
、set
和 tuple
。而且內部值也可以有自己的類型。
這些具有內部類型的類型稱為「泛型」類型。而且可以宣告它們,甚至可以連同它們的內部類型一起宣告。
要宣告這些類型和內部類型,您可以使用標準 Python 模組 typing
。它的存在專門用於支援這些類型提示。
較新版本的 Python¶
使用 typing
的語法與所有版本相容,從 Python 3.6 到最新版本,包括 Python 3.9、Python 3.10 等。
隨著 Python 的發展,較新版本對這些類型註釋提供了更好的支援,在許多情況下,您甚至不需要導入和使用 typing
模組來宣告類型註釋。
如果您可以為您的專案選擇較新版本的 Python,您將能夠利用這種額外的簡潔性。
所有文件中都提供了與每個 Python 版本相容的範例(如果存在差異)。
例如,「Python 3.6+」表示它與 Python 3.6 或更高版本(包括 3.7、3.8、3.9、3.10 等)相容。而「Python 3.9+」表示它與 Python 3.9 或更高版本(包括 3.10 等)相容。
如果您可以使用最新版本的 Python,請使用最新版本的範例,這些範例將具有最佳且最簡單的語法,例如「Python 3.10+」。
List(列表)¶
例如,讓我們定義一個變數為 str
的 list
(列表)。
使用相同的冒號 (:
) 語法宣告變數。
將 list
設為類型。
由於列表是一種包含一些內部類型的類型,因此您將它們放在方括號中。
def process_items(items: list[str]):
for item in items:
print(item)
從 typing
導入 List
(使用大寫字母 L
)。
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
使用相同的冒號 (:
) 語法宣告變數。
將您從 typing
導入的 List
設為類型。
由於列表是一種包含一些內部類型的類型,因此您將它們放在方括號中。
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
資訊
方括號中的那些內部類型稱為「類型參數」。
在這種情況下,str
是傳遞給 List
(或 Python 3.9 及更高版本中的 list
)的類型參數。
這表示:「變數 items
是一個 list
(列表),並且此列表中的每個項目都是一個 str
(字串)」。
提示
如果您使用 Python 3.9 或更高版本,則不必從 typing
導入 List
,您可以改用相同的常規 list
類型。
這樣做,您的編輯器即使在處理列表中的項目時也能提供支援。
沒有類型,這幾乎是不可能實現的。
請注意,變數 item
是列表 items
中的元素之一。
而且,編輯器仍然知道它是 str
(字串),並為其提供支援。
Tuple(元組)和 Set(集合)¶
宣告 tuple
和 set
的方式也相同
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
return items_t, items_s
from typing import Set, Tuple
def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
return items_t, items_s
這表示
- 變數
items_t
是一個包含 3 個項目的tuple
,分別是一個int
、另一個int
和一個str
。 - 變數
items_s
是一個set
,它的每個項目都是bytes
類型。
字典 (Dict)¶
要定義一個 dict
,您需要傳遞兩個以逗號分隔的類型參數。
第一個類型參數用於 dict
的鍵。
第二個類型參數用於 dict
的值。
def process_items(prices: dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
from typing import Dict
def process_items(prices: Dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
這表示
- 變數
prices
是一個dict
- 這個
dict
的鍵是str
類型(例如,每個項目的名稱)。 - 這個
dict
的值是float
類型(例如,每個項目的價格)。
- 這個
聯集 (Union)¶
您可以宣告一個變數可以是**多種類型**,例如 int
或 str
。
在 Python 3.6 及以上版本(包含 Python 3.10)中,您可以使用 typing
模組中的 Union
類型,並在方括號內放入可接受的類型。
在 Python 3.10 中,還有一種**新語法**,您可以使用 豎線 (|
) 分隔可能的類型。
def process_item(item: int | str):
print(item)
from typing import Union
def process_item(item: Union[int, str]):
print(item)
在這兩種情況下,都表示 item
可以是 int
或 str
。
可能為 None (Possibly None)¶
您可以宣告一個值可以具有某種類型,例如 str
,但也可能為 None
。
在 Python 3.6 及以上版本(包含 Python 3.10)中,您可以透過從 typing
模組導入和使用 Optional
來宣告。
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
使用 Optional[str]
而不是單純的 str
,可以讓編輯器幫助您偵測錯誤,避免您假設值始終是 str
類型,而實際上它也可能是 None
。
Optional[Something]
實際上是 Union[Something, None]
的簡寫,它們是等效的。
這也表示在 Python 3.10 中,您可以使用 Something | None
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Union
def say_hi(name: Union[str, None] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
使用 Union 或 Optional¶
如果您使用的是 Python 3.10 以下的版本,以下是我個人**主觀**的建議
- 🚨 避免使用
Optional[SomeType]
- 而是 ✨ 使用
Union[SomeType, None]
✨。
兩者是等效的,底層實現也相同,但我建議使用 Union
而不是 Optional
,因為「optional」一詞似乎暗示該值是可選的,而實際上它的意思是「它可以是 None
」,即使它不是可選的,仍然是必需的。
我認為 Union[SomeType, None]
更能明確表達其含義。
這只是關於詞語和名稱的問題。但這些詞語會影響您和您的團隊成員對程式碼的理解。
舉例來說,讓我們來看這個函式
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
參數 name
定義為 Optional[str]
,但它**並非可選**,您不能在不使用該參數的情況下呼叫函式
say_hi() # Oh, no, this throws an error! 😱
name
參數**仍然是必需的**(不是*可選的*),因為它沒有預設值。儘管如此,name
仍然接受 None
作為值
say_hi(name=None) # This works, None is valid 🎉
好消息是,一旦您使用 Python 3.10,您就不必擔心這個問題,因為您可以簡單地使用 |
來定義類型的聯集
def say_hi(name: str | None):
print(f"Hey {name}!")
這樣您就不必擔心 Optional
和 Union
這樣的名稱了。 😎
泛型¶
這些使用方括號中帶有類型參數的類型稱為**泛型**,例如:
您可以使用與泛型相同的內建類型(使用方括號並在其中包含類型)
list(列表)
tuple(元組)
set(集合)
dict(字典)
與 Python 3.8 相同,來自 typing
模組
聯集 (Union)
Optional
(與 Python 3.8 相同)- ...以及其他。
在 Python 3.10 中,作為使用泛型 Union
和 Optional
的替代方案,您可以使用 豎線 (|
) 來宣告類型的聯集,這樣更好更簡潔。
您可以使用與泛型相同的內建類型(使用方括號並在其中包含類型)
list(列表)
tuple(元組)
set(集合)
dict(字典)
與 Python 3.8 相同,來自 typing
模組
聯集 (Union)
Optional(可選)
- ...以及其他。
列表 (List)
Tuple(元組)
Set(集合)
字典 (Dict)
聯集 (Union)
Optional(可選)
- ...以及其他。
類別作為類型¶
您也可以將類別宣告為變數的類型。
假設您有一個名為 Person
的類別,帶有名稱
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
然後您可以宣告一個類型為 Person
的變數
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
然後,您再次獲得所有編輯器支援
請注意,這表示「one_person
是 Person
類別的**實例**」。
這並不表示「one_person
是名為 Person
的**類別**」。
Pydantic 模型¶
Pydantic 是一個用於執行資料驗證的 Python 函式庫。
您可以將資料的「形狀」宣告為具有屬性的類別。
每個屬性都有一個類型。
然後,您使用一些值建立該類別的實例,它將驗證這些值,將它們轉換為適當的類型(如果有的話),並為您提供一個包含所有資料的物件。
您將獲得該結果物件的所有編輯器支援。
來自官方 Pydantic 文件的範例
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import List, Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: List[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
資訊
要進一步了解 Pydantic,請查看其文件。
FastAPI 完全基於 Pydantic。
您將在教學 - 使用者指南中看到更多實際應用。
提示
當您使用沒有預設值的 Optional
或 Union[Something, None]
時,Pydantic 會有特殊的行為,您可以在 Pydantic 文件中閱讀更多關於 必要 Optional 欄位 的資訊。
帶有元資料註釋的類型提示¶
Python 還具有一項功能,允許使用 Annotated
在這些類型提示中放置**額外的 元資料**。
在 Python 3.9 中,Annotated
是標準函式庫的一部分,因此您可以從 typing
匯入它。
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
在 Python 3.9 以下的版本中,您從 typing_extensions
匯入 Annotated
。
它將與 FastAPI 一起安裝。
from typing_extensions import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Python 本身不會對這個 Annotated
做任何事情。對於編輯器和其他工具,類型仍然是 str
。
但是您可以使用 Annotated
中的空間向 FastAPI 提供有關您希望應用程式如何運作的額外元資料。
需要記住的重要一點是,您傳遞給 Annotated
的**第一個*類型參數***是**實際類型**。其餘的只是提供給其他工具的元資料。
目前,您只需要知道 Annotated
存在,而且它是標準 Python。 😎
稍後您將看到它有多麼**強大**。
提示
由於 FastAPI 使用的是標準 Python 語法,這表示您在編輯器中將擁有最佳的開發體驗,可以繼續使用您習慣的工具來分析和重構程式碼等等。 ✨
同時,您的程式碼將與許多其他的 Python 工具和函式庫高度相容。 🚀
FastAPI 中的型別提示¶
FastAPI 利用這些型別提示來完成幾件事情。
在 FastAPI 中,您可以使用型別提示來宣告參數,並獲得以下好處:
- 編輯器支援.
- 型別檢查.
...而且 FastAPI 使用相同的宣告來:
- 定義需求:從請求路徑參數、查詢參數、標頭、主體、依賴項等等。
- 資料轉換:將請求資料轉換為所需的型別。
- 資料驗證:驗證來自每個請求的資料。
- 當資料無效時,自動產生錯誤並回傳給用戶端。
- 使用 OpenAPI 產生 API 文件
- 這些文件接著會被自動化的互動式文件使用者介面所使用。
這聽起來可能有點抽象。別擔心,您將在教學 - 使用者指南中看到所有這些實際應用。
重點是,藉由在單一位置使用標準 Python 型別(而不是新增更多類別、裝飾器等等),FastAPI 將會為您完成許多工作。
資訊
如果您已經完成了所有教學並回來了解更多關於型別的資訊,一個很好的資源是mypy
的「速查表」。