Kodokon kodokon.com

تلميحات الأنواع وdataclasses: تحديد أنواع Python الحديثة بجدّية

افهم ما تفعله التعليقات التوضيحية فعليًا في وقت التشغيل، واستفد إلى أقصى حدّ من dataclasses عبر frozen وslots وdefault_factory.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

إن التعليقات التوضيحية للأنواع بيانات وصفية، لا قيود: يخزّنها المفسّر في __annotations__ ولا يتحقّق منها أبدًا في وقت التشغيل. فالتحقّق مهمة محلّل ساكن مثل mypy أو pyright، قبل التنفيذ. والنتيجة المباشرة: برنامج سيّئ الأنواع يعمل دون أيّ اعتراض - وهذا مقصود بالتصميم. فتحديد الأنواع التدريجي يتيح لك توصيف قاعدة كود قائمة شيئًا فشيئًا دون كسر أيّ شيء.

PYTHON
from typing import get_type_hints

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

print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))
التعليقات التوضيحية لا تمنع شيئًا: الاستدعاء "الخاطئ" ينجح.

منذ Python 3.9، أصبحت الأنواع المدمجة عامة (generic) مباشرة: اكتب list[int] أو dict[str, float] دون استيراد typing.List. وتُكتب الاتحادات int | None منذ الإصدار 3.10. ولربط نوع المُدخَل بنوع المُخرَج، استخدم TypeVar - بل إن Python 3.12 يقدّم الصياغة المختصرة def first[T](items: list[T]) -> T. ونقطة كثيرًا ما تُهمَل: from __future__ import annotations تجعل كل تعليق توضيحي كسولًا (يُخزَّن كسلسلة نصية)، ما يسمح بالإشارة إلى أصناف مُعرَّفة لاحقًا ويخفّض الكلفة عند الاستيراد.

PYTHON
from collections.abc import Iterable, Iterator
from typing import TypeVar

T = TypeVar("T")

def dedupe(items: Iterable[T]) -> Iterator[T]:
    seen: set[T] = set()
    for item in items:
        if item not in seen:
            seen.add(item)
            yield item

print(list(dedupe([3, 1, 3, 2, 1])))
يضمن TypeVar أن dedupe(list[int]) تُنتِج أعدادًا صحيحة فعلًا.
PYTHON
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)
    total: int = 0

    def __post_init__(self) -> None:
        self.total = len(self.items)

point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))
الصنف Point غير قابل للتغيير، وقابل للتجزئة، وليس له __dict__ خاص بكل مثيل.

ثلاثة وسائط تحوّل @dataclass إلى أداة جاهزة للإنتاج. يجعل frozen=True المثيلات غير قابلة للتغيير، ومع المساواة المولَّدة تلقائيًا، قابلة للتجزئة - أي صالحة كمفاتيح للقواميس. ويزيل slots=True (منذ Python 3.10) القاموس __dict__ الخاص بكل مثيل: ذاكرة أقل، ووصول أسرع إلى الخصائص، وفشل أيّ إسناد لخاصية غير متوقّعة. وأخيرًا يفرض kw_only=True الاستدعاء بكلمات مفتاحية صريحة. أما __post_init__ فهو المكان المخصّص للثوابت المنطقية والحقول المشتقّة. وبالنسبة لتجميعة بسيطة غير قابلة للتغيير، يبقى NamedTuple خيارًا وجيهًا؛ وحالما يظهر سلوك أو تحقّق من الصحة، فضّل dataclass.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. ماذا يحدث في وقت التشغيل عندما تستدعي add('py', 'thon') بينما الدالة موصّفة بـ (a: int, b: int) -> int؟
    • تُطلق Python خطأ TypeError فورًا
    • يُنفَّذ الاستدعاء بشكل طبيعي: فالتعليقات التوضيحية لا يُتحقَّق منها أبدًا في وقت التشغيل
    • تُصدر Python تحذير RuntimeWarning ثم تُنفِّذ الاستدعاء
    • ترفض الوحدة أن تُستورَد
  2. لماذا يُمنع استخدام items: list[str] = [] كقيمة افتراضية لحقل في dataclass؟
    • لأن list[str] ليست تعليقًا توضيحيًا صالحًا لحقل
    • لأن dataclasses لا تقبل إلا الحقول التي بلا قيمة افتراضية
    • لأن هذه القيمة الافتراضية القابلة للتغيير ستكون مشتركة بين كل المثيلات: يُطلق dataclass خطأ ValueError ويستلزم field(default_factory=list)
    • لأن القيم الافتراضية يجب أن تُضبط في __post_init__
  3. ماذا يفعل frozen=True بـ dataclass؟
    • تصبح المثيلات غير قابلة للتغيير، ومع المساواة المولَّدة، قابلة للتجزئة - أي صالحة كمفاتيح للقواميس
    • لم يعد بالإمكان اشتقاق أصناف فرعية من الصنف
    • تصبح الحقول خصائص صنف مشتركة
    • يُتحقَّق من التعليقات التوضيحية عند كل إسناد