# الأصناف في Python: البرمجة الكائنية والوراثة وdataclasses بأمثلة عملية

URL: https://skillaria.top/ar/guides/python/classes-python

الصنف في Python قالب يصف الحقول والطرائق، والكائن نسخة مبنية منه. يُنشأ بالكلمة class، وتهيئ الحقول الطريقة __init__، ويشير self إلى الكائن الحالي. وتتيح الوراثة إنشاء صنف فرعي يرث ويعدّل السلوك، ويختصر @dataclass كتابة الأصناف التي تحفظ البيانات، وتُفضَّل التركيبة على الوراثة العميقة.

الصنف في Python قالب يصف البيانات والسلوك، والكائن نسخة مبنية منه. فالصنف هو الرسم الهندسي، والكائن هو المنتج المصنوع وفقه. تقوم البرمجة الكائنية في Python على ثلاثة أشياء: الأصناف مع طرائقها، والوراثة، والطرائق الخاصة مثل `__init__` و`__repr__`. وفيما يلي شرح لكل ذلك خطوة بخطوة، على مثال واحد يتكرر في الشرح هو تتبّع المصروفات.

## لماذا نحتاج الأصناف إذا كانت القواميس موجودة

يمكن حفظ البيانات في قاموس: `{"title": "قهوة", "amount": 250}`. المشكلة أن القاموس لا يعرف المفاتيح الصحيحة. فخطأ مطبعي مثل `expense.get("amout", 0)` لا يسبب أي خطأ، بل تعود القيمة صفراً، فيبدأ التقرير بالكذب بصمت.

أما كائن الصنف فحقوله معلنة مسبقاً. الوصول إلى خاصية غير موجودة يسبب `AttributeError` فوراً، وPython الحديث يقترح الاسم الصحيح أيضاً. وإضافة إلى ذلك يجمع الصنف البيانات والعمليات عليها في مكان واحد: المصروف يحسب المبلغ مع الضريبة بنفسه، والتقرير يحسب مجموعه بنفسه.

## كيف تنشئ صنفاً في Python

يُعرَّف الصنف بالكلمة `class`، ويُكتب اسمه بأسلوب CamelCase وفق PEP 8. أبسط مثال:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("قهوة", 250)
taxi = Expense("تاكسي", 300)
print(coffee.title, coffee.amount)   # قهوة 250
```

استدعاء `Expense("قهوة", 250)` ينشئ كائناً جديداً من الصنف. الكائنان `coffee` و`taxi` من النوع نفسه، لكل منهما قيم حقوله الخاصة. ويمكن التحقق من الانتماء بـ `isinstance(coffee, Expense)` فتعيد `True`.

### المُنشئ __init__: تهيئة الصنف

تسمى `__init__` غالباً المُنشئ، والأدق أنها المُهيِّئ. فالكائن نفسه تنشئه الطريقة `__new__`، أما `__init__` فتُستدعى بعدها مباشرة وتوزع الوسائط على الحقول. ولا يُشترط أن تعيد شيئاً (بل يجب أن تعيد `None`).

ويفيد `__init__` في التحقق من المدخلات، حتى لا تنشأ في البرنامج كائنات خاطئة منذ البداية:

```
class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("المبلغ لا يمكن أن يكون سالباً")
        self.title = title
        self.amount = amount
```

### ما هو self في Python

`self` مرجع إلى الكائن نفسه الذي تعمل عليه الطريقة في تلك اللحظة. عندما تكتب `coffee.with_tax()` ينفّذ Python فعلياً `Expense.with_tax(coffee)`، ويمرر الكائن كأول وسيطة. ولهذا يأتي `self` أولاً في تعريف الطريقة، ولا يُكتب عند الاستدعاء.

`self` ليس كلمة مفتاحية، بل اصطلاح: من الناحية التقنية يمكن تسمية المعامل بأي اسم، لكن لا أحد يفعل ذلك. ومن الأخطاء الشائعة نسيان `self` في تعريف الطريقة، فيسقط الاستدعاء بـ `TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given`.

## الطرائق والطرائق الخاصة

الطريقة دالة معرّفة داخل الصنف، ترى حقول الكائن عبر `self`:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("قهوة", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('قهوة', 250)
```

بدون `__repr__` سيظهر الكائن هكذا: `<__main__.Expense object at 0x7f...>`، وهذا غير مفيد للتصحيح. والطرائق التي تبدأ وتنتهي بشرطتين سفليتين تُسمى الطرائق الخاصة (dunder)، ويستدعيها Python بنفسه: `__str__` للعرض المفهوم للمستخدم، و`__eq__` للمقارنة بـ `==`، و`__len__` للدالة `len(obj)`.

### @classmethod وstaticmethod

الطريقة العادية تعمل مع الكائن. أما طريقة الصنف، المزينة بـ `@classmethod`، فتستقبل الصنف نفسه (`cls`) كأول وسيطة، وتُستخدم كثيراً كمُنشئ بديل. والطريقة الساكنة `@staticmethod` لا تستقبل الكائن ولا الصنف، فهي دالة عادية مرتبطة منطقياً بالصنف.

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("غداء;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False
```

### المُدمِّر __del__

يملك Python الطريقة `__del__` التي تُستدعى قبل إتلاف الكائن بواسطة جامع المهملات. لا تعتمد عليها لتحرير الموارد، لأن لحظة استدعائها غير مضمونة. استخدم للملفات والاتصالات مدير السياق `with` أو الطريقة الصريحة `close()`.

## خصائص الصنف وخصائص الكائن

الحقول التي تُعطى بـ `self.name = ...` تخص كائناً محدداً. أما المتغيرات المعلنة مباشرة في جسم الصنف فهي خصائص الصنف، وتشترك فيها كل الكائنات، وهكذا تُحفظ الثوابت عادةً.

```
class Expense:
    CURRENCY = "RUB"          # خاصية الصنف، ثابت

    def __init__(self, title, amount):
        self.title = title    # خصائص الكائن
        self.amount = amount

a = Expense("قهوة", 250)
b = Expense("شاي", 150)
print(a.CURRENCY, Expense.CURRENCY)   # RUB RUB
```

الفخ الخطير هو الخاصية القابلة للتعديل على مستوى الصنف. إذا كتبت في جسم الصنف `rows = []` وأضفت إليها عبر `self.rows.append(...)`، فستكون القائمة مشتركة بين كل الكائنات. أنشئ الحقول القابلة للتعديل دائماً داخل `__init__`.

لعرض حقول الكائن استخدم `vars(obj)` أو `obj.__dict__`، ولقراءة خاصية باسمها من نص استخدم `getattr(obj, "amount")`.

## خصائص محسوبة: @property

الخاصية property طريقة تُقرأ من الخارج كحقل عادي، دون أقواس. وهي مناسبة للقيم المحسوبة: لا داعي لتخزين مجموع التقرير منفصلاً، فلن تنسى تحديثه.

```
class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("قهوة", 250))
r.add(Expense("تاكسي", 300))
print(r.total)   # 550
```

إذا أردت التحكم في الكتابة فأضف مُعيِّناً (setter) بـ `@total.setter`. وبدونه يسبب الإسناد `r.total = 0` الخطأ `AttributeError`.

وبخصوص التغليف: لا توجد حقول خاصة بالمعنى الصارم في Python. الشرطة السفلية الواحدة (`_cache`) اتفاق على «لا تلمس من الخارج»، أما الشرطتان (`__secret`) فتفعّلان تشويه الاسم (name mangling)، حتى لا يكتب وريث الصنف فوق الحقل بالخطأ.

## وراثة الأصناف في Python

تتيح الوراثة إنشاء صنف فرعي من صنف أب: يرث الفرع كل الحقول والطرائق، ويعيد تعريف ما يحتاج إلى تغيير. يُكتب الصنف الأب بين قوسين.

```
class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # استدعاء __init__ الأب
        self.month = month

    def add(self, expense):       # إعادة تعريف الطريقة
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # الحفاظ على سلوك الأب

m = MonthlyReport("أكتوبر")
m.add(Expense("قهوة", 250))   # [أكتوبر] + Expense('قهوة', 250)
print(m.total)                # 250 — الخاصية موروثة
```

تعيد `super()` كائناً وسيطاً لاستدعاء طرائق الأب. ونسيان `super().__init__()` في مُهيِّئ الصنف الفرعي خطأ كلاسيكي: لا ينشأ الحقل `rows`، ويسقط أول استدعاء لـ `add` بـ `AttributeError`.

للتحقق من علاقة الوراثة استخدم `issubclass(MonthlyReport, Report)`. وترث كل الأصناف في Python 3 ضمنياً من `object`، ومنه تأتي الطرائق الأساسية مثل `__repr__` و`__eq__`. ويدعم Python الوراثة المتعددة (`class C(A, B)`)، وترتيب البحث عن الطرائق يظهر في `C.__mro__`.

### الصنف المجرد في Python

الصنف المجرد يحدد واجهة: الطرائق التي يجب على الأصناف الفرعية تنفيذها. ولا يمكن إنشاء كائن منه. ويُستخدم لذلك الوحدة `abc`:

```
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...
```

الصنف الفرعي الذي لا ينفّذ كل الطرائق المجردة يبقى مجرداً أيضاً، ويعطي عند إنشاء كائن منه الخطأ نفسه.

## التركيب بدل الوراثة العميقة

بعد تعلم الوراثة تُغري الإنسان بناء سلالم طويلة: تراكم أولاً، ثم تراكم بالروبية، ثم بالروبية الضريبية، وهكذا لخمسة مستويات. لكن Python يكتب ذلك نادراً. فالوراثة تعبّر عن علاقة «هو نوع من»، أما التركيب فيعبّر عن علاقة «يحتوي على»: التقرير ليس قائمة من المصروفات، بل يحتويها في الحقل `rows`.

قاعدة عملية: إذا أردت فقط استخدام قدرة كائن آخر، فضعه في حقل ولا ترث منه. وكثيراً ما تكفي dataclass للبيانات، والمنطق يبقى في دوال عادية.

## dataclass في Python: صنف بلا كود متكرر

للأصناف التي تحفظ البيانات في الأغلب، يُرهق كتابة `__init__` و`__repr__` و`__eq__` يدوياً. فالمزخرف `@dataclass` من الوحدة `dataclasses` يولّدها من التلميحات المكتوبة للحقول:

```
from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

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

e = Expense("قهوة", 250)
print(e)                           # Expense(title='قهوة', amount=250, tags=[])
print(e == Expense("قهوة", 250))   # True
```

التحقق من القيم الذي كان يُكتب في `__init__` يُنقل إلى `__post_init__`، وهي تُستدعى مباشرة بعد المُهيِّئ الذي تولده dataclass. أما القيم الافتراضية القابلة للتعديل فتُعطى فقط بـ `field(default_factory=list)`: الكتابة `tags: list = []` ترفضها dataclass وتعطي `ValueError`.

### frozen وslots

المعامل `frozen=True` يجعل الكائن غير قابل للتعديل، و`slots=True` (منذ Python 3.10) ينشئ `__slots__` فيوفر الذاكرة. ومحاولة تعديل حقل في كائن مجمّد تسبب `FrozenInstanceError`. للحصول على كائن بقيمة جديدة تُنشأ نسخة:

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("قهوة", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='قهوة', amount=250) Expense(title='قهوة', amount=300)
```

ومنذ Python 3.13 تفعل الدالة العامة `copy.replace()` الشيء نفسه. وكائنات dataclass المجمّدة يمكن وضعها في المجموعات واستخدامها مفاتيح للقواميس، لأن لها `__hash__`.

## جديد الأصناف: Self وoverride وslots

للتلميحات الخاصة بالأصناف أداتان مفيدتان. الرمز `typing.Self` (منذ Python 3.11، PEP 673) يعني «سيُعاد كائن من الصنف نفسه»، وهذا يعمل حتى في الطرائق الموروثة. والمزخرف `@override` (منذ Python 3.12، PEP 698) يعلّم طريقة يجب أن تعيد تعريف طريقة في الأب: إذا أخطأت في الاسم، فسيخبرك mypy أو pyright بأن الطريقة الأساسية غير موجودة.

```
from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # يمكن استدعاؤها بالتسلسل

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)
```

وفي Python 3.15 أُزيل قيد ضيق: يُسمح للأصناف الفرعية من `tuple` بـ `__slots__` غير فارغة، ويمكن ذكر `__dict__` و`__weakref__` في `__slots__` لأي صنف. أما في 3.14 فكان هذا التصريح عند وريث الصف يسبب `TypeError: nonempty __slots__ not supported for subtype of 'tuple'`.

## أخطاء شائعة مع الأصناف

| الخطأ | العَرَض | الحل |
| --- | --- | --- |

| نسيان `self` في الطريقة | `TypeError: ... takes 0 positional arguments but 1 was given` | أول معامل في الطريقة هو دائماً `self` |

| حقل بلا `self.` في `__init__` | تختفي القيمة بعد `__init__` | اكتب `self.amount = amount` |

| قائمة كخاصية للصنف | تتسرب البيانات بين الكائنات | أنشئ القوائم في `__init__` أو عبر `default_factory` |

| نسيان `super().__init__()` | `AttributeError` على حقول الأب | استدعِ مُهيِّئ الأب |

| استدعاء الخاصية بأقواس | `TypeError: 'int' object is not callable` | اقرأ الخاصية بلا أقواس: `r.total` |

## الخلاصة

البرمجة الكائنية في Python تبدأ ببساطة: الصنف يصف الحقول والطرائق، و`__init__` يملأ الحقول، و`self` يشير إلى الكائن الحالي. تتيح الوراثة و`super()` توسيع السلوك، ويحدد الصنف المجرد الواجهة، وتحسب `@property` الحقول المحسوبة. وللأصناف التي تحمل البيانات استخدم `@dataclass`، وفي الهياكل المعقدة فضّل التركيب على الوراثة. وقد شُرحت الأصناف وdataclasses عملياً في دورة بايثون، على مثال متتبّع المصروفات.

## FAQ

**ما الفرق بين الصنف والكائن في Python؟**

الصنف وصف للنوع: ما الحقول والطرائق التي يملكها. أما الكائن أو النسخة فيُنشأ باستدعاء الصنف، مثل Expense("قهوة", 250)، ويحمل قيماً محددة لحقوله.

**هل يجب كتابة __init__ في كل صنف؟**

لا. إذا لم تُعرَّف __init__ فيُستخدم الموروث من object الذي لا يقبل وسائط. وفي dataclass يولّدها المزخرف.

**ما الفرق بين __str__ وَ__repr__؟**

__repr__ تمثيل واضح للمطور والمصحح، أما __str__ فهي العرض المفهوم للمستخدم في print وf-strings. وإذا لم تكن __str__ معرّفة يستخدم Python الـ __repr__.

**متى أختار dataclass ومتى الصنف العادي؟**

الـ dataclass مناسب حين يحمل الصنف البيانات في الأساس ونريد __init__ و__repr__ والمقارنة جاهزة. والصنف العادي أنسب حين تكون منطق الإنشاء معقداً أو السلوك أهم من البيانات.

**هل توجد حقول خاصة فعلاً في Python؟**

لا توجد خصوصية صارمة. الشرطة السفلية الواحدة في بداية الاسم اتفاق بأنه لا يُستخدم من الخارج، والشرطتان تفعلان تشويه الاسم الذي يحمي الحقل من الكتابة فوقه في الوريث.


---
Source: Skillaria.Top (https://skillaria.top)
