Giới thiệu kiểu dữ liệu Python¶
Python hỗ trợ tùy chọn "type hints" (còn được gọi là "type annotations").
Những "type hints" hay chú thích là một cú pháp đặc biệt cho phép khai báo kiểu dữ liệu của một biến.
Bằng việc khai báo kiểu dữ liệu cho các biến của bạn, các trình soạn thảo và các công cụ có thể hỗ trợ bạn tốt hơn.
Đây chỉ là một hướng dẫn nhanh về gợi ý kiểu dữ liệu trong Python. Nó chỉ bao gồm những điều cần thiết tối thiểu để sử dụng chúng với FastAPI... đó thực sự là rất ít.
FastAPI hoàn toàn được dựa trên những gợi ý kiểu dữ liệu, chúng mang đến nhiều ưu điểm và lợi ích.
Nhưng thậm chí nếu bạn không bao giờ sử dụng FastAPI, bạn sẽ được lợi từ việc học một ít về chúng.
Note
Nếu bạn là một chuyên gia về Python, và bạn đã biết mọi thứ về gợi ý kiểu dữ liệu, bỏ qua và đi tới chương tiếp theo.
Động lực¶
Hãy bắt đầu với một ví dụ đơn giản:
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"))
Kết quả khi gọi chương trình này:
John Doe
Hàm thực hiện như sau:
- Lấy một
first_name
vàlast_name
. - Chuyển đổi kí tự đầu tiên của mỗi biến sang kiểu chữ hoa với
title()
. - Nối chúng lại với nhau bằng một kí tự trắng ở giữa.
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"))
Sửa đổi¶
Nó là một chương trình rất đơn giản.
Nhưng bây giờ hình dung rằng bạn đang viết nó từ đầu.
Tại một vài thời điểm, bạn sẽ bắt đầu định nghĩa hàm, bạn có các tham số...
Nhưng sau đó bạn phải gọi "phương thức chuyển đổi kí tự đầu tiên sang kiểu chữ hoa".
Có phải là upper
? Có phải là uppercase
? first_uppercase
? capitalize
?
Sau đó, bạn thử hỏi người bạn cũ của mình, autocompletion của trình soạn thảo.
Bạn gõ tham số đầu tiên của hàm, first_name
, sau đó một dấu chấm (.
) và sau đó ấn Ctrl+Space
để kích hoạt bộ hoàn thành.
Nhưng đáng buồn, bạn không nhận được điều gì hữu ích cả:
Thêm kiểu dữ liệu¶
Hãy sửa một dòng từ phiên bản trước.
Chúng ta sẽ thay đổi chính xác đoạn này, tham số của hàm, từ:
first_name, last_name
sang:
first_name: str, last_name: str
Chính là nó.
Những thứ đó là "type hints":
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"))
Đó không giống như khai báo những giá trị mặc định giống như:
first_name="john", last_name="doe"
Nó là một thứ khác.
Chúng ta sử dụng dấu hai chấm (:
), không phải dấu bằng (=
).
Và việc thêm gợi ý kiểu dữ liệu không làm thay đổi những gì xảy ra so với khi chưa thêm chúng.
But now, imagine you are again in the middle of creating that function, but with type hints.
Tại cùng một điểm, bạn thử kích hoạt autocomplete với Ctrl+Space
và bạn thấy:
Với cái đó, bạn có thể cuộn, nhìn thấy các lựa chọn, cho đến khi bạn tìm thấy một "tiếng chuông":
Động lực nhiều hơn¶
Kiểm tra hàm này, nó đã có gợi ý kiểu dữ liệu:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
Bởi vì trình soạn thảo biết kiểu dữ liệu của các biến, bạn không chỉ có được completion, bạn cũng được kiểm tra lỗi:
Bây giờ bạn biết rằng bạn phải sửa nó, chuyển age
sang một xâu với str(age)
:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
Khai báo các kiểu dữ liệu¶
Bạn mới chỉ nhìn thấy những nơi chủ yếu để đặt khai báo kiểu dữ liệu. Như là các tham số của hàm.
Đây cũng là nơi chủ yếu để bạn sử dụng chúng với FastAPI.
Kiểu dữ liệu đơn giản¶
Bạn có thể khai báo tất cả các kiểu dữ liệu chuẩn của Python, không chỉ là str
.
Bạn có thể sử dụng, ví dụ:
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
Các kiểu dữ liệu tổng quát với tham số kiểu dữ liệu¶
Có một vài cấu trúc dữ liệu có thể chứa các giá trị khác nhau như dict
, list
, set
và tuple
. Và những giá trị nội tại cũng có thể có kiểu dữ liệu của chúng.
Những kiểu dữ liệu nội bộ này được gọi là những kiểu dữ liệu "tổng quát". Và có khả năng khai báo chúng, thậm chí với các kiểu dữ liệu nội bộ của chúng.
Để khai báo những kiểu dữ liệu và những kiểu dữ liệu nội bộ đó, bạn có thể sử dụng mô đun chuẩn của Python là typing
. Nó có hỗ trợ những gợi ý kiểu dữ liệu này.
Những phiên bản mới hơn của Python¶
Cú pháp sử dụng typing
tương thích với tất cả các phiên bản, từ Python 3.6 tới những phiên bản cuối cùng, bao gồm Python 3.9, Python 3.10,...
As Python advances, những phiên bản mới mang tới sự hỗ trợ được cải tiến cho những chú thích kiểu dữ liệu và trong nhiều trường hợp bạn thậm chí sẽ không cần import và sử dụng mô đun typing
để khai báo chú thích kiểu dữ liệu.
Nếu bạn có thể chọn một phiên bản Python gần đây hơn cho dự án của bạn, ban sẽ có được những ưu điểm của những cải tiến đơn giản đó.
Trong tất cả các tài liệu tồn tại những ví dụ tương thích với mỗi phiên bản Python (khi có một sự khác nhau).
Cho ví dụ "Python 3.6+" có nghĩa là nó tương thích với Python 3.7 hoặc lớn hơn (bao gồm 3.7, 3.8, 3.9, 3.10,...). và "Python 3.9+" nghĩa là nó tương thích với Python 3.9 trở lên (bao gồm 3.10,...).
Nếu bạn có thể sử dụng phiên bản cuối cùng của Python, sử dụng những ví dụ cho phiên bản cuối, những cái đó sẽ có cú pháp đơn giản và tốt nhât, ví dụ, "Python 3.10+".
List¶
Ví dụ, hãy định nghĩa một biến là list
các str
.
Khai báo biến với cùng dấu hai chấm (:
).
Tương tự kiểu dữ liệu list
.
Như danh sách là một kiểu dữ liệu chứa một vài kiểu dữ liệu có sẵn, bạn đặt chúng trong các dấu ngoặc vuông:
def process_items(items: list[str]):
for item in items:
print(item)
Từ typing
, import List
(với chữ cái L
viết hoa):
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Khai báo biến với cùng dấu hai chấm (:
).
Tương tự như kiểu dữ liệu, List
bạn import từ typing
.
Như danh sách là một kiểu dữ liệu chứa các kiểu dữ liệu có sẵn, bạn đặt chúng bên trong dấu ngoặc vuông:
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Info
Các kiểu dữ liệu có sẵn bên trong dấu ngoặc vuông được gọi là "tham số kiểu dữ liệu".
Trong trường hợp này, str
là tham số kiểu dữ liệu được truyền tới List
(hoặc list
trong Python 3.9 trở lên).
Có nghĩa là: "biến items
là một list
, và mỗi phần tử trong danh sách này là một str
".
Tip
Nếu bạn sử dụng Python 3.9 hoặc lớn hơn, bạn không phải import List
từ typing
, bạn có thể sử dụng list
để thay thế.
Bằng cách này, trình soạn thảo của bạn có thể hỗ trợ trong khi xử lí các phần tử trong danh sách:
Đa phần đều không thể đạt được nếu không có các kiểu dữ liệu.
Chú ý rằng, biến item
là một trong các phần tử trong danh sách items
.
Và do vậy, trình soạn thảo biết nó là một str
, và cung cấp sự hỗ trợ cho nó.
Tuple and Set¶
Bạn sẽ làm điều tương tự để khai báo các tuple
và các 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
Điều này có nghĩa là:
- Biến
items_t
là mộttuple
với 3 phần tử, mộtint
, mộtint
nữa, và mộtstr
. - Biến
items_s
là mộtset
, và mỗi phần tử của nó có kiểubytes
.
Dict¶
Để định nghĩa một dict
, bạn truyền 2 tham số kiểu dữ liệu, phân cách bởi dấu phẩy.
Tham số kiểu dữ liệu đầu tiên dành cho khóa của dict
.
Tham số kiểu dữ liệu thứ hai dành cho giá trị của 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)
Điều này có nghĩa là:
- Biến
prices
là mộtdict
:- Khóa của
dict
này là kiểustr
(đó là tên của mỗi vật phẩm). - Giá trị của
dict
này là kiểufloat
(đó là giá của mỗi vật phẩm).
- Khóa của
Union¶
Bạn có thể khai báo rằng một biến có thể là **một vài kiểu dữ liệu" bất kì, ví dụ, một int
hoặc một str
.
Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể sử dụng kiểu Union
từ typing
và đặt trong dấu ngoặc vuông những giá trị được chấp nhận.
In Python 3.10 there's also a new syntax where you can put the possible types separated by a vertical bar (|
).
Trong Python 3.10 cũng có một cú pháp mới mà bạn có thể đặt những kiểu giá trị khả thi phân cách bởi một dấu sổ dọc (|
).
def process_item(item: int | str):
print(item)
from typing import Union
def process_item(item: Union[int, str]):
print(item)
Trong cả hai trường hợp có nghĩa là item
có thể là một int
hoặc str
.
Khả năng None
¶
Bạn có thể khai báo một giá trị có thể có một kiểu dữ liệu, giống như str
, nhưng nó cũng có thể là None
.
Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể khai báo nó bằng các import và sử dụng Optional
từ mô đun typing
.
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Sử dụng Optional[str]
thay cho str
sẽ cho phép trình soạn thảo giúp bạn phát hiện các lỗi mà bạn có thể gặp như một giá trị luôn là một str
, trong khi thực tế nó rất có thể là None
.
Optional[Something]
là một cách viết ngắn gọn của Union[Something, None]
, chúng là tương đương nhau.
Điều này cũng có nghĩa là trong Python 3.10, bạn có thể sử dụng 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")
Sử dụng Union
hay Optional
¶
If you are using a Python version below 3.10, here's a tip from my very subjective point of view:
Nếu bạn đang sử dụng phiên bản Python dưới 3.10, đây là một mẹo từ ý kiến rất "chủ quan" của tôi:
- 🚨 Tránh sử dụng
Optional[SomeType]
- Thay vào đó ✨ sử dụng
Union[SomeType, None]
✨.
Cả hai là tương đương và bên dưới chúng giống nhau, nhưng tôi sẽ đễ xuất Union
thay cho Optional
vì từ "tùy chọn" có vẻ ngầm định giá trị là tùy chọn, và nó thực sự có nghĩa rằng "nó có thể là None
", do đó nó không phải là tùy chọn và nó vẫn được yêu cầu.
Tôi nghĩ Union[SomeType, None]
là rõ ràng hơn về ý nghĩa của nó.
Nó chỉ là về các từ và tên. Nhưng những từ đó có thể ảnh hưởng cách bạn và những đồng đội của bạn suy nghĩ về code.
Cho một ví dụ, hãy để ý hàm này:
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
Tham số name
được định nghĩa là Optional[str]
, nhưng nó không phải là tùy chọn, bạn không thể gọi hàm mà không có tham số:
say_hi() # Oh, no, this throws an error! 😱
Tham số name
vẫn được yêu cầu (không phải là tùy chọn) vì nó không có giá trị mặc định. Trong khi đó, name
chấp nhận None
như là giá trị:
say_hi(name=None) # This works, None is valid 🎉
Tin tốt là, khi bạn sử dụng Python 3.10, bạn sẽ không phải lo lắng về điều đó, bạn sẽ có thể sử dụng |
để định nghĩa hợp của các kiểu dữ liệu một cách đơn giản:
def say_hi(name: str | None):
print(f"Hey {name}!")
Và sau đó, bạn sẽ không phải lo rằng những cái tên như Optional
và Union
. 😎
Những kiểu dữ liệu tổng quát¶
Những kiểu dữ liệu này lấy tham số kiểu dữ liệu trong dấu ngoặc vuông được gọi là Kiểu dữ liệu tổng quát, cho ví dụ:
Bạn có thể sử dụng các kiểu dữ liệu có sẵn như là kiểu dữ liệu tổng quát (với ngoặc vuông và kiểu dữ liệu bên trong):
list
tuple
set
dict
Và tương tự với Python 3.6, từ mô đun typing
:
Union
Optional
(tương tự như Python 3.6)- ...và các kiểu dữ liệu khác.
Trong Python 3.10, thay vì sử dụng Union
và Optional
, bạn có thể sử dụng sổ dọc ('|') để khai báo hợp của các kiểu dữ liệu, điều đó tốt hơn và đơn giản hơn nhiều.
Bạn có thể sử dụng các kiểu dữ liệu có sẵn tương tự như (với ngoặc vuông và kiểu dữ liệu bên trong):
list
tuple
set
dict
Và tương tự với Python 3.6, từ mô đun typing
:
Union
Optional
- ...and others.
List
Tuple
Set
Dict
Union
Optional
- ...và các kiểu khác.
Lớp như kiểu dữ liệu¶
Bạn cũng có thể khai báo một lớp như là kiểu dữ liệu của một biến.
Hãy nói rằng bạn muốn có một lớp Person
với một tên:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Sau đó bạn có thể khai báo một biến có kiểu là Person
:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Và lại một lần nữa, bạn có được tất cả sự hỗ trợ từ trình soạn thảo:
Lưu ý rằng, điều này có nghĩa rằng "one_person
" là một thực thể của lớp Person
.
Nó không có nghĩa "one_person
" là một lớp gọi là Person
.
Pydantic models¶
Pydantic là một thư viện Python để validate dữ liệu hiệu năng cao.
Bạn có thể khai báo "hình dạng" của dữa liệu như là các lớp với các thuộc tính.
Và mỗi thuộc tính có một kiểu dữ liệu.
Sau đó bạn tạo một thực thể của lớp đó với một vài giá trị và nó sẽ validate các giá trị, chuyển đổi chúng sang kiểu dữ liệu phù hợp (nếu đó là trường hợp) và cho bạn một object với toàn bộ dữ liệu.
Và bạn nhận được tất cả sự hỗ trợ của trình soạn thảo với object kết quả đó.
Một ví dụ từ tài liệu chính thức của 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
Info
Để học nhiều hơn về Pydantic, tham khảo tài liệu của nó.
FastAPI được dựa hoàn toàn trên Pydantic.
Bạn sẽ thấy nhiều ví dụ thực tế hơn trong Hướng dẫn sử dụng.
Tip
Pydantic có một hành vi đặc biệt khi bạn sử dụng Optional
hoặc Union[Something, None]
mà không có giá trị mặc dịnh, bạn có thể đọc nhiều hơn về nó trong tài liệu của Pydantic về Required Optional fields.
Type Hints với Metadata Annotations¶
Python cũng có một tính năng cho phép đặt metadata bổ sung trong những gợi ý kiểu dữ liệu này bằng cách sử dụng Annotated
.
Trong Python 3.9, Annotated
là một phần của thư viện chuẩn, do đó bạn có thể import nó từ typing
.
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Ở phiên bản dưới Python 3.9, bạn import Annotated
từ typing_extensions
.
Nó đã được cài đặt sẵng cùng với FastAPI.
from typing_extensions import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Python bản thân nó không làm bất kì điều gì với Annotated
. Với các trình soạn thảo và các công cụ khác, kiểu dữ liệu vẫn là str
.
Nhưng bạn có thể sử dụng Annotated
để cung cấp cho FastAPI metadata bổ sung về cách mà bạn muốn ứng dụng của bạn xử lí.
Điều quan trọng cần nhớ là tham số kiểu dữ liệu* đầu tiên bạn truyền tới Annotated
là **kiểu giá trị thực sự*. Phần còn lại chỉ là metadata cho các công cụ khác.
Bây giờ, bạn chỉ cần biết rằng Annotated
tồn tại, và nó là tiêu chuẩn của Python. 😎
Sau đó, bạn sẽ thấy sự mạnh mẽ mà nó có thể làm.
Tip
Thực tế, cái này là tiêu chuẩn của Python, nghĩa là bạn vẫn sẽ có được trải nghiệm phát triển tốt nhất có thể với trình soạn thảo của bạn, với các công cụ bạn sử dụng để phân tích và tái cấu trúc code của bạn, etc. ✨
Và code của bạn sẽ tương thích với nhiều công cụ và thư viện khác của Python. 🚀
Các gợi ý kiểu dữ liệu trong FastAPI¶
FastAPI lấy các ưu điểm của các gợi ý kiểu dữ liệu để thực hiện một số thứ.
Với FastAPI, bạn khai báo các tham số với gợi ý kiểu và bạn có được:
- Sự hỗ trợ từ các trình soạn thảo.
- Kiểm tra kiểu dữ liệu (type checking).
...và FastAPI sử dụng các khia báo để:
- Định nghĩa các yêu cầu: từ tham số đường dẫn của request, tham số query, headers, bodies, các phụ thuộc (dependencies),...
- **Chuyển dổi dữ liệu*: từ request sang kiểu dữ liệu được yêu cầu.
- Kiểm tra tính đúng đắn của dữ liệu: tới từ mỗi request:
- Sinh lỗi tự động để trả về máy khác khi dữ liệu không hợp lệ.
- Tài liệu hóa API sử dụng OpenAPI:
- cái mà sau được được sử dụng bởi tài liệu tương tác người dùng.
Điều này có thể nghe trừu tượng. Đừng lo lắng. Bạn sẽ thấy tất cả chúng trong Hướng dẫn sử dụng.
Điều quan trọng là bằng việc sử dụng các kiểu dữ liệu chuẩn của Python (thay vì thêm các lớp, decorators,...), FastAPI sẽ thực hiện nhiều công việc cho bạn.
Info
Nếu bạn đã đi qua toàn bộ các hướng dẫn và quay trở lại để tìm hiểu nhiều hơn về các kiểu dữ liệu, một tài nguyên tốt như "cheat sheet" từ mypy
.