پرش به محتویات

مقدمه‌ای بر انواع نوع در پایتون

پایتون از "نوع‌نما"های اختیاری (که بهشون "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 هست.