~/icsd.ir — bash
SYSTEM_ONLINE

Type Hints و Static Typing

Type Hints (یا Type Annotations) از پایتون 3.5 معرفی شدند و امکان مشخص کردن نوع متغیرها، پارامترها و خروجی توابع را می‌دهند. در این فصل با ماژول typing، ابزار mypy، Generics، Protocol و TypedDict آشنا می‌شویم.

Type Hints (یا Type Annotations) از پایتون 3.5 معرفی شدند و امکان مشخص کردن نوع متغیرها، پارامترها و خروجی توابع را می‌دهند. در این فصل با ماژول typing، ابزار mypy، Generics، Protocol و TypedDict آشنا می‌شویم.

چرا Type Hints؟

  • کاتچ کردن خطاها قبل از اجرا (با mypy)
  • IDE هوشمند: autocomplete و refactoring بهتر
  • مستندات خودکار
  • کد قابل فهم‌تر در پروژه‌های بزرگ
نکته مهم: پایتون به‌صورت پیش‌فرض type hints را در زمان اجرا چک نمی‌کند. این فقط یک annotation است. برای چک، از ابزارهای static analysis مثل mypy استفاده می‌شود.

مبانی Type Hints

# متغیرها
name: str = "علی"
age: int = 30
price: float = 99.99
is_active: bool = True

# توابع
def greet(name: str, age: int) -> str:
    return f"{name} ({age} ساله)"

def add(a: int, b: int) -> int:
    return a + b

# بدون return type
def log(message: str) -> None:
    print(message)

# با parameter پیش‌فرض
def fetch(url: str, timeout: int = 10) -> dict:
    pass

Generic Types

از پایتون 3.9 می‌توان مستقیم از انواع built-in به‌صورت generic استفاده کرد:

# Python 3.9+
def get_users() -> list[str]:
    return ["علی", "سارا"]

def get_prices() -> dict[str, float]:
    return {"فرش": 5_000_000.0, "گلیم": 1_500_000.0}

scores: list[int] = [85, 92, 78]
config: dict[str, str | int] = {"name": "app", "port": 8080}

# Tuple - با طول ثابت
point: tuple[int, int] = (10, 20)
rgba: tuple[int, int, int, int] = (255, 128, 0, 200)

# Tuple با هر تعداد
log: tuple[str, ...] = ("INFO", "ERROR", "DEBUG")  # هر تعداد str

# Set
unique_ids: set[int] = {1, 2, 3}

برای Python 3.8 و قبل

from typing import List, Dict, Tuple, Set

def get_users() -> List[str]: ...
def get_prices() -> Dict[str, float]: ...
point: Tuple[int, int] = (10, 20)

Optional و Union

# Optional - یعنی None هم ممکن است
def find_user(user_id: int) -> str | None:
    if user_id == 1:
        return "علی"
    return None

# معادل قدیمی
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]: ...

# Union - چند نوع ممکن
def parse(value: int | float | str) -> str:
    return str(value)

# قدیمی
def parse(value: Union[int, float, str]) -> str: ...

# Literal - فقط مقادیر خاص
from typing import Literal
def set_mode(mode: Literal["read", "write", "append"]) -> None:
    pass

set_mode("read")    # OK
# set_mode("delete")  # mypy خطا

Callable – توابع به‌عنوان پارامتر

from typing import Callable

# تابعی که تابع می‌گیرد
def apply(func: Callable[[int, int], int], x: int, y: int) -> int:
    return func(x, y)

def add(a: int, b: int) -> int:
    return a + b

result = apply(add, 3, 5)  # 8

# هر signature
handler: Callable[..., None]  # هر ورودی، خروجی None

# تابع بدون پارامتر
callback: Callable[[], str]

Generics سفارشی – TypeVar

from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T:
    """اولین آیتم - نوع خروجی برابر با نوع آیتم لیست"""
    return items[0]

# پایتون متوجه می‌شود
x: int = first([1, 2, 3])
y: str = first(["a", "b"])

# محدود کردن TypeVar
Number = TypeVar("Number", int, float)

def maximum(a: Number, b: Number) -> Number:
    return a if a > b else b

maximum(1, 2)        # int
maximum(1.5, 2.7)    # float
# maximum("a", "b")  # mypy خطا

Generic Classes

from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    
    def push(self, item: T) -> None:
        self._items.append(item)
    
    def pop(self) -> T:
        return self._items.pop()
    
    def peek(self) -> T:
        return self._items[-1]

# استفاده با نوع مشخص
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
x: int = int_stack.pop()  # 2

str_stack: Stack[str] = Stack()
str_stack.push("hello")

Python 3.12+ – سینتکس جدید

# Python 3.12+ - بدون TypeVar
def first[T](items: list[T]) -> T:
    return items[0]

class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []
    
    def push(self, item: T) -> None:
        self._items.append(item)

Protocol – Duck Typing تایپ‌محور

Protocol اجازه می‌دهد بدون وراثت، نیازمندی‌ها را تعریف کنیم:

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("○")

class Square:
    def draw(self) -> None:
        print("□")

def render(shape: Drawable) -> None:
    shape.draw()

# بدون نیاز به ارث‌بری از Drawable
render(Circle())   # ○
render(Square())   # □

runtime_checkable

from typing import Protocol, runtime_checkable

@runtime_checkable
class Comparable(Protocol):
    def __lt__(self, other) -> bool: ...

# حالا isinstance هم کار می‌کند
print(isinstance(5, Comparable))      # True
print(isinstance("a", Comparable))    # True

TypedDict – دیکشنری با ساختار

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str
    email: str
    is_active: bool

# استفاده
user: User = {
    "id": 1,
    "name": "علی",
    "email": "ali@example.com",
    "is_active": True
}

def greet_user(u: User) -> str:
    return f"سلام {u['name']}"

greet_user(user)

# کلیدهای اختیاری (Python 3.11+)
class Config(TypedDict):
    host: str
    port: int
    timeout: NotRequired[int]  # اختیاری

@dataclass با Type Hints

from dataclasses import dataclass, field

@dataclass
class Product:
    id: int
    name: str
    price: float
    tags: list[str] = field(default_factory=list)
    discount: float = 0.0

p = Product(id=1, name="فرش", price=5_000_000)

انواع پیشرفته

Final – مقدار غیرقابل تغییر

from typing import Final

MAX_RETRIES: Final = 3
API_URL: Final[str] = "https://api.example.com"

# MAX_RETRIES = 5  # mypy خطا

NewType – نوع مجزا

from typing import NewType

UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)

def get_user(uid: UserId) -> str:
    return "user"

user_id = UserId(123)
product_id = ProductId(456)

get_user(user_id)        # OK
# get_user(product_id)   # mypy خطا - نوع متفاوت
# get_user(123)          # mypy خطا - باید UserId باشد

Annotated – متادیتا اضافه

from typing import Annotated

# با pydantic یا FastAPI
def create_user(
    name: Annotated[str, "نام کاربر، حداکثر 50 کاراکتر"],
    age: Annotated[int, "سن - بزرگتر از 0"]
) -> None:
    pass

mypy – بررسی Type

pip install mypy

# بررسی یک فایل
mypy script.py

# بررسی پروژه
mypy myproject/

# تنظیمات سختگیرانه
mypy --strict script.py

پیکربندی mypy.ini

[mypy]
python_version = 3.12
strict = True
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_unimported = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True

[mypy-third_party.*]
ignore_missing_imports = True

نادیده گرفتن خط

x: int = "string"  # type: ignore
y = some_legacy_function()  # type: ignore[no-any-return]

مثال جامع

from dataclasses import dataclass
from typing import Protocol, TypeVar, Generic

T = TypeVar("T")

class Repository(Protocol, Generic[T]):
    def get(self, id: int) -> T | None: ...
    def save(self, item: T) -> None: ...
    def list_all(self) -> list[T]: ...
    def delete(self, id: int) -> bool: ...

@dataclass
class User:
    id: int
    name: str
    email: str

class UserRepository:
    def __init__(self) -> None:
        self._users: dict[int, User] = {}
    
    def get(self, id: int) -> User | None:
        return self._users.get(id)
    
    def save(self, user: User) -> None:
        self._users[user.id] = user
    
    def list_all(self) -> list[User]:
        return list(self._users.values())
    
    def delete(self, id: int) -> bool:
        return self._users.pop(id, None) is not None

# تست
repo: Repository[User] = UserRepository()
repo.save(User(1, "علی", "ali@example.com"))
user = repo.get(1)
if user:
    print(user.name)

بهترین شیوه‌ها

  • برای پروژه‌های جدید، از روز اول type hints بنویسید
  • برای پروژه‌های قدیمی، تدریجی اضافه کنید (gradual typing)
  • mypy را در CI اجرا کنید
  • از سینتکس مدرن (Python 3.10+): X | None به‌جای Optional[X]
  • برای interface، Protocol بهتر از ABC است
  • TypedDict برای dict‌های با ساختار مشخص
  • Final برای ثابت‌ها

جمع‌بندی

  • Type Hints کیفیت کد را به‌شدت بالا می‌برند
  • پایتون در زمان اجرا چک نمی‌کند، باید از mypy استفاده کرد
  • سینتکس مدرن (3.9+): list[int]، X | None
  • TypeVar و Generic برای کد قابل استفاده مجدد
  • Protocol برای duck typing تایپ‌محور
  • TypedDict برای dict‌های ساختارمند

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.