مقدمهای بر انواع نوع در پایتون¶
پایتون از "نوعنما"های اختیاری (که بهشون "type hints" یا "type annotations" هم میگن) پشتیبانی میکنه.
این "نوعنماها" یا annotationها یه سینتکس خاص هستن که بهت اجازه میدن نوع یه متغیر رو مشخص کنی.
با مشخص کردن نوع متغیرها، ویرایشگرها و ابزارها میتونن پشتیبانی بهتری بهت بدن.
این فقط یه آموزش سریع / یادآوری در مورد نوعنماهای پایتونه. فقط حداقل چیزایی که برای استفاده ازشون با FastAPI لازمه رو پوشش میده... که در واقع خیلی کمه.
FastAPI کاملاً بر پایه این نوعنماهاست و این بهش کلی مزیت و فایده میده.
ولی حتی اگه هیچوقت از FastAPI استفاده نکنی، بازم یادگیری یه کم در موردشون به نفعته.
Note
اگه حرفهای پایتونی و همهچیز رو در مورد نوعنماها میدونی، برو سراغ فصل بعدی.
انگیزه¶
بیاید با یه مثال ساده شروع کنیم:
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
چون ویرایشگر نوع متغیرها رو میدونه، فقط تکمیل خودکار نمیگیری، بلکه چک خطاها هم داری:
حالا میدونی که باید درستش کنی، age
رو با str(age)
به یه رشته تبدیل کنی:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
تعریف نوعها¶
تازه اصلیترین جا برای تعریف نوعنماها رو دیدی. بهعنوان پارامترهای تابع.
این هم اصلیترین جاییه که با FastAPI ازشون استفاده میکنی.
نوعهای ساده¶
میتونی همه نوعهای استاندارد پایتون رو تعریف کنی، نه فقط 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
. و مقدارهای داخلیشون هم میتونن نوع خودشون رو داشته باشن.
به این نوعها که نوعهای داخلی دارن میگن "عمومی" یا "generic". و میشه اونا رو تعریف کرد، حتی با نوعهای داخلیشون.
برای تعریف این نوعها و نوعهای داخلیشون، میتونی از ماژول استاندارد پایتون typing
استفاده کنی. این ماژول مخصوص پشتیبانی از نوعنماهاست.
نسخههای جدیدتر پایتون¶
سینتکس با استفاده از typing
با همه نسخهها، از پایتون 3.6 تا جدیدترینها، از جمله پایتون 3.9، 3.10 و غیره سازگاره.
با پیشرفت پایتون، نسخههای جدیدتر پشتیبانی بهتری برای این نوعنماها دارن و توی خیلی موارد حتی لازم نیست ماژول typing
رو وارد کنی و ازش برای تعریف نوعنماها استفاده کنی.
اگه بتونی برای پروژهات از یه نسخه جدیدتر پایتون استفاده کنی، میتونی از این سادگی اضافه بهره ببری.
توی همه مستندات، مثالهایی هستن که با هر نسخه پایتون سازگارن (وقتی تفاوتی هست).
مثلاً "Python 3.6+" یعنی با پایتون 3.6 یا بالاتر (مثل 3.7، 3.8، 3.9، 3.10 و غیره) سازگاره. و "Python 3.9+" یعنی با پایتون 3.9 یا بالاتر (مثل 3.10 و غیره) سازگاره.
اگه بتونی از جدیدترین نسخههای پایتون استفاده کنی، از مثالهای نسخه آخر استفاده کن، چون اونا بهترین و سادهترین سینتکس رو دارن، مثلاً "Python 3.10+".
لیست¶
مثلاً، بیایم یه متغیر تعریف کنیم که یه list
از str
باشه.
متغیر رو با همون سینتکس دونقطه (:
) تعریف کن.
بهعنوان نوع، 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)
متغیر رو با همون سینتکس دونقطه (:
) تعریف کن.
بهعنوان نوع، List
رو که از typing
وارد کردی بذار.
چون لیست یه نوعه که نوعهای داخلی داره، اونا رو توی کروشهها میذاری:
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Info
اون نوعهای داخلی توی کروشهها بهشون "پارامترهای نوع" میگن.
توی این مورد، str
پارامتر نوعیه که به List
(یا list
توی پایتون 3.9 و بالاتر) پاس داده شده.
یعنی: "متغیر items
یه list
هست، و هر کدوم از آیتمهای این لیست یه str
هستن".
Tip
اگه از پایتون 3.9 یا بالاتر استفاده میکنی، لازم نیست List
رو از typing
وارد کنی، میتونی همون نوع معمولی list
رو به جاش استفاده کنی.
با این کار، ویرایشگرت حتی وقتی داری آیتمهای لیست رو پردازش میکنی بهت کمک میکنه:
بدون نوعها، رسیدن به این تقریباً غیرممکنه.
توجه کن که متغیر item
یکی از عناصر توی لیست items
هست.
و با این حال، ویرایشگر میدونه که یه str
هست و براش پشتیبانی میده.
تاپل و ست¶
برای تعریف 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
یهtuple
با 3 تا آیتمه، یهint
، یهint
دیگه، و یهstr
. - متغیر
items_s
یهset
هست، و هر کدوم از آیتمهاش از نوعbytes
هستن.
دیکشنری¶
برای تعریف یه dict
، 2 تا پارامتر نوع میدی، که با کاما از هم جدا شدن.
پارامتر نوع اول برای کلیدهای 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
هستن (مثلاً قیمت هر آیتم).
- کلیدهای این
اتحادیه¶
میتونی تعریف کنی که یه متغیر میتونه هر کدوم از چند تا نوع باشه، مثلاً یه int
یا یه str
.
توی پایتون 3.6 و بالاتر (از جمله پایتون 3.10) میتونی از نوع Union
توی typing
استفاده کنی و نوعهای ممکن رو توی کروشهها بذاری.
توی پایتون 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
¶
میتونی تعریف کنی که یه مقدار میتونه یه نوع باشه، مثلاً str
، ولی میتونه None
هم باشه.
توی پایتون 3.6 و بالاتر (از جمله پایتون 3.10) میتونی با وارد کردن و استفاده از Optional
از ماژول 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")
استفاده از Optional[str]
به جای فقط str
به ویرایشگر کمک میکنه خطاهایی که ممکنه فکر کنی یه مقدار همیشه str
هست رو پیدا کنه، در حالی که میتونه None
هم باشه.
Optional[Something]
در واقع میانبر برای Union[Something, None]
هست، این دو تا معادلن.
یعنی توی پایتون 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
¶
اگه از نسخه پایتون زیر 3.10 استفاده میکنی، یه نکته از دید خیلی شخصی خودم:
- 🚨 از
Optional[SomeType]
استفاده نکن - به جاش ✨ از
Union[SomeType, None]
استفاده کن ✨.
هر دو معادلن و زیر پوسته یکیان، ولی من Union
رو به Optional
ترجیح میدم چون کلمه "اختیاری" انگار暗示 میکنه که مقدار اختیاریه، در حالی که در واقع یعنی "میتونه None
باشه"، حتی اگه اختیاری نباشه و هنوز لازم باشه.
فکر میکنم Union[SomeType, None]
واضحتر نشون میده چی معنی میده.
فقط بحث کلمات و اسمهاست. ولی این کلمات میتونن رو طرز فکر تو و تیمت نسبت به کد تأثیر بذارن.
بهعنوان مثال، این تابع رو ببین:
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
🤓 Other versions and variants
def say_hi(name: str | None):
print(f"Hey {name}!")
پارامتر name
بهعنوان Optional[str]
تعریف شده، ولی اختیاری نیست، نمیتونی تابع رو بدون پارامتر صدا کنی:
say_hi() # اوه نه، این خطا میده! 😱
پارامتر name
هنوز لازمه (نه اختیاری) چون مقدار پیشفرض نداره. با این حال، name
مقدار None
رو قبول میکنه:
say_hi(name=None) # این کار میکنه، None معتبره 🎉
خبر خوب اینه که وقتی رو پایتون 3.10 باشی، لازم نیست نگران این باشی، چون میتونی بهسادگی از |
برای تعریف اتحادیه نوعها استفاده کنی:
def say_hi(name: str | None):
print(f"Hey {name}!")
🤓 Other versions and variants
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
اون موقع دیگه لازم نیست نگران اسمهایی مثل Optional
و Union
باشی. 😎
نوعهای عمومی¶
این نوعهایی که پارامترهای نوع رو توی کروشهها میگیرن بهشون نوعهای عمومی یا Generics میگن، مثلاً:
میتونی از همون نوعهای داخلی بهعنوان نوعهای عمومی استفاده کنی (با کروشهها و نوعها داخلشون):
list
tuple
set
dict
و همونطور که توی پایتون 3.8 بود، از ماژول typing
:
Union
Optional
(همونطور که توی پایتون 3.8 بود)- ...و بقیه.
توی پایتون 3.10، بهعنوان جایگزین برای استفاده از نوعهای عمومی Union
و Optional
، میتونی از خط عمودی (|
) برای تعریف اتحادیه نوعها استفاده کنی، که خیلی بهتر و سادهتره.
میتونی از همون نوعهای داخلی بهعنوان نوعهای عمومی استفاده کنی (با کروشهها و نوعها داخلشون):
list
tuple
set
dict
و همونطور که توی پایتون 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 یه کتابخونه پایتونه برای اعتبارسنجی دادهها.
"شکل" دادهها رو بهعنوان کلاسهایی با ویژگیها تعریف میکنی.
و هر ویژگی یه نوع داره.
بعد یه نمونه از اون کلاس رو با یه سری مقدار میسازی و اون مقدارها رو اعتبارسنجی میکنه، به نوع مناسب تبدیلشون میکنه (اگه لازم باشه) و یه شیء با همه دادهها بهت میده.
و با اون شیء نهایی همه پشتیبانی ویرایشگر رو میگیری.
یه مثال از مستندات رسمی 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
برای اطلاعات بیشتر در مورد Pydantic، مستنداتش رو چک کن.
FastAPI کاملاً بر پایه Pydantic هست.
توی آموزش - راهنمای کاربر خیلی بیشتر از اینا رو توی عمل میبینی.
Tip
Pydantic یه رفتار خاص داره وقتی از Optional
یا Union[Something, None]
بدون مقدار پیشفرض استفاده میکنی، میتونی توی مستندات Pydantic در مورد فیلدهای اختیاری لازم بیشتر بخونی.
نوعنماها با Annotationهای متادیتا¶
پایتون یه قابلیت هم داره که بهت اجازه میده متادیتا اضافی رو توی این نوعنماها بذاری با استفاده از Annotated
.
توی پایتون 3.9، Annotated
بخشی از کتابخونه استاندارده، پس میتونی از typing
واردش کنی.
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
توی نسخههای زیر پایتون 3.9، Annotated
رو از typing_extensions
وارد میکنی.
با FastAPI از قبل نصب شده.
from typing_extensions import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
خود پایتون با این Annotated
کاری نمیکنه. و برای ویرایشگرها و ابزارهای دیگه، نوع هنوز str
هست.
ولی میتونی از این فضا توی Annotated
استفاده کنی تا به FastAPI متادیتای اضافی در مورد اینکه چطور میخوای برنامهات رفتار کنه بدی.
نکته مهم اینه که اولین پارامتر نوع که به Annotated
میدی، نوع واقعی هست. بقیش فقط متادیتا برای ابزارهای دیگهست.
الان فقط باید بدونی که Annotated
وجود داره، و اینکه پایتون استاندارده. 😎
بعداً میبینی که چقدر قوی میتونه باشه.
Tip
اینکه این پایتون استاندارده یعنی هنوز بهترین تجربه توسعهدهنده رو توی ویرایشگرت، با ابزارهایی که برای تحلیل و بازسازی کدت استفاده میکنی و غیره میگیری. ✨
و همینطور کدت با خیلی از ابزارها و کتابخونههای دیگه پایتون خیلی سازگار میمونه. 🚀
نوعنماها توی FastAPI¶
FastAPI از این نوعنماها استفاده میکنه تا چند تا کار بکنه.
با FastAPI پارامترها رو با نوعنماها تعریف میکنی و اینا رو میگیری:
- پشتیبانی ویرایشگر.
- چک نوعها.
...و FastAPI از همون تعریفها برای اینا استفاده میکنه:
- تعریف نیازها: از پارامترهای مسیر درخواست، پارامترهای کوئری، هدرها، بدنهها، وابستگیها و غیره.
- تبدیل داده: از درخواست به نوع مورد نیاز.
- اعتبارسنجی داده: که از هر درخواست میاد:
- تولید خطاهای خودکار که به کلاینت برمیگرده وقتی داده نامعتبره.
- مستندسازی API با استفاده از OpenAPI:
- که بعدش توسط رابطهای کاربری مستندات تعاملی خودکار استفاده میشه.
اینا شاید همهش انتزاعی به نظر بیاد. نگران نباش. همه اینا رو توی عمل توی آموزش - راهنمای کاربر میبینی.
نکته مهم اینه که با استفاده از نوعهای استاندارد پایتون، توی یه جا (به جای اضافه کردن کلاسهای بیشتر، دکوراتورها و غیره)، FastAPI کلی از کار رو برات انجام میده.
Info
اگه همه آموزش رو گذروندی و برگشتی که بیشتر در مورد نوعها ببینی، یه منبع خوب "تقلبنامه" از mypy
هست.