Kodax

Dataclasses — البرمجة الكائنية

Dataclasses

كثير من الأصناف هدفها الأساسي حمل البيانات: منتج، نقطة، طلب. كتابة __init__ و __repr__ و __eq__ لكل منها مملّ ومعرّض للأخطاء. @dataclass تولّدها تلقائياً من تعريف الحقول.

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    quantity: int = 0

يولّد تلقائياً:

- __init__(self, name, price, quantity=0)

- __repr__: Product(name='قلم', price=1.5, quantity=0)

- __eq__: مقارنة الحقول.

أنواع الحقول (: str) مطلوبة لكي تتعرف dataclass على الحقل، لكن Python لا تتحقق منها عند التشغيل؛ هي توثيق وأداة لمحررات الكود.

القيم الافتراضية القابلة للتعديل

from dataclasses import field

@dataclass
class Order:
    items: list = []                           # ❌ ValueError
    items: list = field(default_factory=list)  # ✅ قائمة جديدة لكل كائن

لو سُمح بـ [] لتشاركت كل الطلبات نفس القائمة. dataclass تمنع هذا الخطأ صراحة.

خيارات مفيدة

| الخيار | التأثير |

|---|---|

| @dataclass(frozen=True) | الكائن غير قابل للتعديل، ويصلح مفتاحاً في قاموس |

| @dataclass(order=True) | يولّد < و > للمقارنة والترتيب حسب الحقول بالترتيب |

| field(repr=False) | إخفاء حقل من التمثيل النصي (مثل كلمة مرور) |

__post_init__ للتحقق

def __post_init__(self):
    if self.price < 0:
        raise ValueError("السعر لا يمكن أن يكون سالباً")

تُستدعى بعد __init__ المولّد مباشرة.

إضافة دوال

dataclass صنف عادي، يمكنك إضافة دوال وخصائص:

@property
def total(self):
    return self.price * self.quantity

dataclass أم namedtuple أم dict؟

| الأداة | متى |

|---|---|

| dict | بيانات مؤقتة بسيطة أو قادمة من JSON |

| namedtuple | سجل صغير ثابت خفيف |

| dataclass | بنية بيانات حقيقية في برنامجك، مع دوال أو تحقق |

تمرين

أنشئ dataclass باسم Book (العنوان، المؤلف، السنة، تقييمات كقائمة) مع خاصية محسوبة للتقييم المتوسط، وتحقق في __post_init__ أن السنة منطقية، ورتّب قائمة كتب حسب السنة.

from dataclasses import dataclass, field
from typing import List

# بدون dataclass (الطريقة القديمة)
class PersonOld:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email
    
    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"
    
    def __eq__(self, other):
        return (self.name == other.name and 
                self.age == other.age and 
                self.email == other.email)

# مع dataclass (الطريقة الجديدة)
@dataclass
class Person:
    name: str
    age: int
    email: str = "غير محدد"  # قيمة افتراضية

# الاستخدام
p1 = Person("أحمد", 25)
p2 = Person("أحمد", 25)
print(p1)           # Person(name='أحمد', age=25, email='غير محدد')
print(p1 == p2)     # True

# مع قائمة (استخدم field)
@dataclass
class Student:
    name: str
    grade: int
    subjects: List[str] = field(default_factory=list)
    
    def add_subject(self, subject):
        self.subjects.append(subject)

student = Student("محمد", 10)
student.add_subject("رياضيات")
student.add_subject("علوم")
print(student)

# Frozen (غير قابل للتغيير)
@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(3.0, 4.0)
# p.x = 5  # FrozenInstanceError!

# مع الترتيب
@dataclass(order=True)
class Product:
    name: str = field(compare=False)  # لا يُستخدم في المقارنة
    price: float

products = [
    Product("قلم", 2.5),
    Product("دفتر", 5.0),
    Product("ممحاة", 1.0)
]
products.sort()
for p in products:
    print(p)

# Post-init للمعالجة بعد الإنشاء
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # لا يُمرر في __init__
    
    def __post_init__(self):
        self.area = self.width * self.height

rect = Rectangle(4, 5)
print(rect.area)  # 20.0
اختبر فهمك