فئات بايثون: قوة البرمجة الكائنية التوجه

تُشكّل فئات بايثون العمود الفقري للبرمجة كائنية التوجه، إذ تُمكّنك من تغليف البيانات والسلوك في كيان واحد. عند استخدام فئة بايثون، تُعرّف سمات لتخزين البيانات وتوابع لتنفيذ الإجراءات. يُتيح لك هذا الهيكل نمذجة كائنات واقعية وإنشاء شيفرة برمجية منظمة وقابلة لإعادة الاستخدام.

تُعدّ الفئات في بايثون بمثابة مخطط لإنشاء الكائنات، وهي نسخ من الفئات. تستخدم الفئات عند الحاجة إلى تغليف البيانات والوظائف ذات الصلة، مما يجعل شيفرتك معيارية ويسهل إدارتها. بتعريف الفئات، يمكنك إنشاء كائنات متعددة تشترك في نفس السمات والتوابع، مع الحفاظ على حالتها الفريدة.

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

للاستفادة القصوى من هذا الدرس، يجب أن تكون على دراية بمتغيرات بايثون وأنواع بياناتها ووظائفها. كما أن بعض الخبرة في البرمجة كائنية التوجه (OOP) تُعدّ ميزة إضافية. لا تقلق إذا لم تكن خبيرًا في البرمجة كائنية التوجه بعد. في هذا الدرس، ستتعلم المفاهيم الأساسية اللازمة للبدء، بالإضافة إلى المزيد. ستكتب أيضًا العديد من الأمثلة العملية التي تُعزز معرفتك بفئات بايثون.

البدء باستخدام فئات بايثون

بايثون لغة برمجة متعددة النماذج، تدعم البرمجة الكائنية التوجه (OOP) من خلال فئات يُمكن تعريفها باستخدام الكلمة المفتاحية class. يُمكنك اعتبار الفئة جزءًا من الشيفرة البرمجية يُحدد البيانات والسلوك اللذين يُمثلان ويُنمذجان نوعًا مُعينًا من الكائنات.

ما هي الفئة في بايثون؟ تشبيه شائع هو أن الفئة تُشبه مخططًا لمنزل. يمكنك استخدام المخطط لإنشاء عدة منازل، بل وحتى حيّ كامل. كل منزل هو كائن أو مثيل مُشتق من المخطط.

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

في البرمجة الكائنية التوجه، يُستخدم مصطلح “السمات” عادةً للإشارة إلى الخصائص أو البيانات المرتبطة بكائن معين من فئة معينة. أما في بايثون، فالسمات هي متغيرات مُعرّفة داخل فئة بغرض تخزين جميع البيانات اللازمة لتشغيلها.

وبالمثل، ستستخدم مصطلح “التوابع” للإشارة إلى السلوكيات المختلفة التي تُظهرها الكائنات. االتوابع هي دوال تُعرّفها داخل فئة. تعمل هذه الدوال عادةً على سمات المثيل أو الفئة الأساسية أو معها. يُشار إلى السمات والتوابع مجتمعةً كأعضاء في فئة أو كائن.

يمكنك كتابة فئات لمحاكاة العالم الحقيقي. ستساعدك هذه الفئات على تنظيم شفرتك البرمجية بشكل أفضل وحل مشاكل البرمجة المعقدة.

على سبيل المثال، يمكنك استخدام الفئات لإنشاء كائنات تحاكي الأشخاص، والحيوانات، والمركبات، والكتب، والمباني، والسيارات، أو غيرها من الكائنات. كما يمكنك نمذجة كائنات افتراضية، مثل خادم الويب، وشجرة المجلدات، وروبوتات الدردشة، ومدير الملفات، وغيرها.

أخيرًا، يمكنك استخدام الفئات لبناء تسلسلات هرمية للفئات. بهذه الطريقة، ستعزز إعادة استخدام الكود وتزيل التكرار في قاعدة بياناتك.

في هذا الدرس، ستتعلم الكثير عن الفئات وجميع الميزات الرائعة التي يمكنك استخدامها. للبدء، ستبدأ بتعريف أول فئة لك في بايثون. بعد ذلك، ستتعمق في مواضيع أخرى تتعلق بالحالات والسمات والتوابع.

تعريف الفئة في بايثون

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

class ClassName:
    <body>

في نصّ الفئة، يمكنك تعريف السمات والتوابع حسب الحاجة. كما تعلمت سابقًا، السمات هي متغيرات تحتوي على بيانات الفئة، بينما التوابع هي دوال تُوفّر سلوكًا وتؤثر عادةً على بيانات الفئة.

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

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def calculate_area(self):
        return math.pi * self.radius ** 2

في هذا المقطع البرمجي، تُعرّف Circle باستخدام الكلمة المفتاحية class. داخلها، تكتب دالة.__init__() و لها معنى خاص في فئات بايثون. تُعرف هذه الدالة باسم مُهيئ الكائن لأنها تُعرّف وتُعيّن القيم الأولية لسمات الكائن. ستتعرف على المزيد حول هذه الدالة في قسم سمات المثيل.

التابع الثاني من  Circle يسمى .calculate_area()، ويحسب مساحة دائرة معينة باستخدام نصف قطرها. في هذا المثال، استخدمت وحدة الرياضيات للوصول إلى ثابت pi كما هو مُعرّف فيها.

من الشائع أن تحتوي أسماء الدوال على فعل، مثل ” calculate“، لوصف إجراء تقوم به الدالة. لمعرفة المزيد حول الدوال في بايثون.

رائع! لقد كتبتَ أول فئة لك. الآن، كيف يمكنك استخدامها في شيفرتك لتمثيل عدة دوائر محددة؟ حسنًا، عليك إنشاء مثيل للفئة لإنشاء كائنات دوائر محددة منها.

إنشاء كائنات من فئة في بايثون

تُعرف عملية إنشاء كائنات ملموسة من فئة موجودة باسم الإنشاء. مع كل إنشاء، تُنشئ كائنًا جديدًا من الفئة المستهدفة. للبدء، أنشئ نسختين من Circle بتشغيل الكود التالي في جلسة Python لإنشاء كائن من فئة بايثون مثل Circle، يجب استدعاء مُنشئ الفئة Circle() باستخدام زوج من الأقواس ومجموعة من الوسائط المناسبة. ما هي الوسائط؟ في بايثون، يقبل مُنشئ الفئة نفس الوسائط التي تقبلها الدالة .init(). في هذا المثال، تتوقع فئة Circle وسيطة نصف القطر.

>>> from circle import Circle

>>> circle_1 = Circle(42)
>>> circle_2 = Circle(7)

>>> circle_1
<__main__.Circle object at 0x102b835d0>
>>> circle_2
<__main__.Circle object at 0x1035e3910>

لإنشاء كائن من فئة بايثون مثل Circle، يجب استدعاء مُنشئ الفئة ()Circle باستخدام زوج من الأقواس ومجموعة من الوسائط المناسبة. ما هي الوسائط؟ في بايثون، يقبل مُنشئ الفئة نفس الوسائط التي تقبلها الدالة.__init__(). في هذا المثال، تتوقع فئة Circle وسيطة radius.

يتيح لك استدعاء مُنشئ الفئة بقيم وسيطات مختلفة إنشاء كائنات أو مثيلات مختلفة للفئة المستهدفة. في المثال أعلاه، circle_1  و circle_2 هما مثيلان منفصلان لل Circle. بمعنى آخر، هما دائرتان مختلفتان ومحددتان، كما يمكنك الاستنتاج من مخرجات الكود.

رائع! أنت تعرف بالفعل كيفية إنشاء كائنات من فئة موجودة عن طريق استدعاء مُنشئ الفئة مع الوسائط المطلوبة. الآن، كيف يمكنك الوصول إلى سمات ووظائف فئة معينة؟ هذا ما ستتعلمه في القسم التالي.

الوصول إلى السمات والتوابع

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

obj.attribute_name

obj.method_name()

لاحظ أن النقطة (.) في هذا البناء النحوي تعني ببساطة إعطاء الخاصية أو الطريقة التالية من هذا الكائن. يُرجع السطر الأول القيمة المخزنة في الخاصية المستهدفة، بينما يصل السطر الثاني إلى التابع المستهدف ويستدعيه.

ملاحظة: تذكر أنه لاستدعاء دالة أو تابع، يجب عليك استخدام زوج من الأقواس وسلسلة من الوسائط، إذا لزم الأمر.

الآن عد إلى كائنات الدائرة الخاصة بك وقم بتشغيل الكود التالي:

>>> from circle import Circle

>>> circle_1 = Circle(42)
>>> circle_2 = Circle(7)

>>> circle_1.radius
42
>>> circle_1.calculate_area()
5541.769440932395

>>> circle_2.radius
7
>>> circle_2.calculate_area()
153.93804002589985

في أول سطرين بعد إنشاء المثال، يمكنك الوصول إلى سمة .radius على كائن circle_1. ثم تستدعي دالة .calculate_area() لحساب مساحة الدائرة. في الزوج الثاني من العبارات، تفعل الشيء نفسه ولكن على كائن circle_2.

يمكنك أيضًا استخدام تدوين النقاط وعامل التعيين لتغيير القيمة الحالية لسمة ما:

>>> circle_1.radius = 100
>>> circle_1.radius
100
>>> circle_1.calculate_area()
31415.926535897932

الآن، نصف قطر  circle_1 مختلف تمامًا. عند استدعاء دالة .calculate_area()، تعكس النتيجة هذا التغيير فورًا. لقد غيّرت الحالة الداخلية للكائن أو بياناته، مما يؤثر عادةً على سلوكياته أو أساليبه.

اتفاقيات التسمية في فئات بايثون

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

ملاحظة: يتبع معظم مبرمجي بايثون طريقة تسمية Snake_case، والتي تتضمن استخدام الشرطات السفلية (_) لفصل الكلمات المتعددة. تُستخدم هذه الطريقة في الدوال والتوابع. مع ذلك، يُنصح باستخدام PascalCase في تسمية فئات بايثون، حيث تُكتب كل كلمة بحرف كبير.

في القسمين التاليين، ستتعرف على اتفاقيتي تسمية مهمتين تنطبقان على سمات الفئة.

الأعضاء العامون مقابل الأعضاء غير العامين

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

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

عضوالتسميةExamples
عاماستخدم نمط التسمية العادي.radiuscalculate_area()
غير عامقم بتضمين شرطة سفلية بادئة في الأسماء._radius_calculate_area()

الأعضاء العامة جزء من الواجهة الرسمية (API) لفئاتك، بينما الأعضاء غير العامة غير مخصصة لتكون جزءًا من تلك الواجهة. هذا يعني أنه لا يجب عليك استخدام أعضاء غير عامة خارج فئتها المُعرّفة.

من المهم ملاحظة أن اتفاقية التسمية الثانية تشير فقط إلى أن السمة غير مخصصة للاستخدام مباشرةً من خارج الفئة التي تحتويها. مع ذلك، فهي لا تمنع الوصول المباشر. على سبيل المثال، يمكنك تشغيل أمر مثل obj._name للوصول إلى محتوى ._name. مع ذلك، هذه ممارسة سيئة، ويجب تجنبها.

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

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

تشويه الاسم

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

تشويه الأسماء هو تحويل تلقائي للأسماء يُضيف اسم الفئة إلى اسم العضو، كما في سمة ClassName_ أو التابع ClassName_. يؤدي هذا التحويل إلى إخفاء الأسماء. بمعنى آخر، لا تتوفر الأسماء المشوهة للوصول المباشر، وليست جزءًا من واجهة برمجة التطبيقات العامة للفئة.

على سبيل المثال، ضع في اعتبارك فئة العينة التالية:

>>> class SampleClass:
...     def __init__(self, value):
...         self.__value = value
...     def __method(self):
...         print(self.__value)
...

>>> sample_instance = SampleClass("Hello!")
>>> vars(sample_instance)
{'_SampleClass__value': 'Hello!'}

>>> vars(SampleClass)
mappingproxy({
    ...
    '__init__': <function SampleClass.__init__ at 0x105dfd4e0>,
    '_SampleClass__method': <function SampleClass.__method at 0x105dfd760>,
    '__dict__': <attribute '__dict__' of 'SampleClass' objects>,
    ...
})

>>> sample_instance = SampleClass("Hello!")
>>> sample_instance.__value
Traceback (most recent call last):
    ...
AttributeError: 'SampleClass' object has no attribute '__value'

>>> sample_instance.__method()
Traceback (most recent call last):
    ...
AttributeError: 'SampleClass' object has no attribute '__method'

في هذه الفئة، تحتوي .__value و.__method() على شرطتين سفليتين رئيسيتين، لذا تم تغيير اسميهما إلى ._SampleClass__value و._SampleClass__method()، كما هو موضح في الأسطر المميزة. أضاف بايثون تلقائيًا البادئة _SampleClass إلى كلا الاسمين. بسبب إعادة التسمية الداخلية هذه، لا يمكنك الوصول إلى السمات من خارج الفئة باستخدام أسمائها الأصلية. إذا حاولت القيام بذلك، فسيظهر خطأ في السمة.

السلوك الداخلي الموصوف أعلاه يُخفي الأسماء، مما يُوحي بوجود سمة أو تابع خاص. مع ذلك، فهي ليست خاصة تمامًا. يمكنك الوصول إليها من خلال أسمائها المُشوّهة:

>>> sample_instance._SampleClass__value
'Hello!'

>>> sample_instance._SampleClass__method()
Hello!

لا يزال من الممكن الوصول إلى السمات أو التوابع المشوهة باستخدام أسمائها المشوهة، مع أن هذه ممارسة سيئة، ويجب تجنبها في شيفرتك. إذا رأيت اسمًا يستخدم هذه القاعدة في شيفرة شخص آخر، فلا تحاول إجبار الشيفرة على استخدام الاسم من خارج الفئة التي تحتويه.

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

فهم فوائد استخدام الفئات في بايثون

هل استخدام الفئات في بايثون مفيد؟ بالتأكيد! الفئات هي أساس البرمجة كائنية التوجه في بايثون. فهي تتيح لك الاستفادة من إمكانيات بايثون أثناء كتابة وتنظيم شيفرتك. بتعلم الفئات، ستتمكن من الاستفادة من جميع مزاياها. باستخدام الفئات، يمكنك:

  • نمذجة وحل مشاكل واقعية معقدة: ستجد العديد من الحالات التي تتطابق فيها الكائنات في برمجتك مع كائنات واقعية. هذا سيساعدك على التفكير في مشاكل معقدة، مما يؤدي إلى حلول أفضل لمشكلات البرمجة لديك.
  • إعادة استخدام الكود وتجنب التكرار: يمكنك تعريف تسلسلات هرمية للفئات ذات الصلة. توفر الفئات الأساسية في أعلى التسلسل الهرمي دوال مشتركة يمكنك إعادة استخدامها لاحقًا في الفئات الفرعية أسفله. هذا يسمح لك بتقليل تكرار الكود وتعزيز إعادة استخدامه.
  • تغليف البيانات والسلوكيات ذات الصلة في كيان واحد: يمكنك استخدام فئات بايثون لتجميع السمات والتوابع ذات الصلة في كيان واحد، وهو الكائن. يساعدك هذا على تنظيم شفرتك البرمجية بشكل أفضل باستخدام كيانات معيارية ومستقلة، يمكنك إعادة استخدامها في مشاريع متعددة.
  • تلخيص تفاصيل تنفيذ المفاهيم والكائنات: يمكنك استخدام الفئات لتجريد تفاصيل تنفيذ المفاهيم والكائنات الأساسية. سيساعدك هذا على تزويد مستخدميك بواجهات برمجة تطبيقات سهلة الاستخدام لمعالجة البيانات والسلوكيات المعقدة.
  • تَحَلَّص من تعدد الأشكال باستخدام واجهات مشتركة: يمكنك تنفيذ واجهة مُحدَّدة في عدة فئات مختلفة قليلاً، واستخدامها بالتبادل في شيفرتك. هذا سيجعل شيفرتك أكثر مرونةً وقابليةً للتكيف.

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

في القسم التالي، سوف تستكشف بعض المواقف التي يجب عليك فيها تجنب استخدام الفئات في الكود الخاص بك.

تحديد متى يجب تجنب الفئات

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

عمليًا، ستواجه بعض المواقف التي تستدعي تجنب الفئات. على سبيل المثال، لا تستخدم الفئات العادية عند الحاجة إلى:

  • خزّن البيانات فقط. استخدم فئة بيانات، أو تعدادًا، أو مجموعة بيانات مُسمّاة.
  • تابع واحدة. استخدم دالة بدلاً منها.

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

إذا كانت فئتك تحتوي على دالة واحدة في واجهة برمجة التطبيقات (API)، فقد لا تحتاج إلى ئة. استخدم دالة بدلاً من ذلك إلا إذا كنت بحاجة إلى الاحتفاظ بحالة معينة بين الاستدعاءات. إذا ظهرت المزيد من الدوال لاحقًا، فيمكنك دائمًا إنشاء ئة. تذكر هذا المبدأ من فلسفة بايثون:

البسيط أفضل من المعقد. (المصدر)

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

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

  • برنامج أو نص برمجي صغير وبسيط لا يتطلب هياكل بيانات أو منطقًا معقدًا. في هذه الحالة، قد يكون استخدام الفئات مُبالغًا فيه.
  • برنامجٌ بالغ الأهمية للأداء. تُضيف الفئات عبئًا إضافيًا على برنامجك، خاصةً عند الحاجة إلى إنشاء العديد من الكائنات. قد يؤثر هذا على الأداء العام لشفرتك البرمجية.
  • قاعدة شيفرة قديمة. إذا كانت قاعدة الشيفرة الحالية لا تستخدم فئات، فلا يجب عليك إضافتها. سيؤدي ذلك إلى تغيير أسلوب البرمجة الحالي وإحداث خلل في اتساق الشيفرة.
  • فريقٌ بأسلوب برمجة مختلف. إذا كان فريقك الحالي لا يستخدم الفئات، فالتزم بأسلوب البرمجة الخاص به. هذا سيضمن الاتساق في جميع أنحاء المشروع.
  • قاعدة بيانات تستخدم البرمجة الوظيفية. إذا كانت قاعدة البيانات مكتوبة حاليًا بنهج وظيفي، فلا ينبغي عليك إضافة فئات. سيؤدي ذلك إلى كسر نموذج البرمجة الأساسي.

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

ربط البيانات بالفئات والمثيلات

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

أولاً، عليك أن تعلم أن فئاتك يمكن أن تحتوي على نوعين من السمات في بايثون:

  • سمات الفئة: متغير تُعرّفه مباشرةً في نص الفئة. تنتمي سمات الفئة إلى الفئة التي تحتويها. بياناتها مشتركة بين الفئة وجميع مثيلاتها.
  • سمات المثيل: متغير تُعرّفه داخل دالة المثيل باستخدام الوسيطة self وترميز النقاط، كما في self.attribute = value. تنتمي سمات المثيل إلى مثيل محدد من فئة معينة. بياناتها متاحة فقط لهذا المثيل، وتُعرّف حالته.

لكلا النوعين من السمات استخدامات خاصة. سمات المثيل هي بلا شك النوع الأكثر شيوعًا في البرمجة اليومية، ولكن سمات الفئة مفيدة أيضًا.

سمات الفئة

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

جميع الكائنات التي تُنشئها من فئة معينة تشترك في سمات الفئة نفسها، وبنفس القيم الأصلية. لذلك، إذا غيّرت سمة فئة، فسيؤثر هذا التغيير على جميع الكائنات المشتقة.

على سبيل المثال، لنفترض أنك تريد إنشاء فئة تحتفظ بعدد داخلي للمثيلات التي أنشأتها. في هذه الحالة، يمكنك استخدام سمة الفئة:

>>> class ObjectCounter:
...     num_instances = 0
...     def __init__(self):
...         ObjectCounter.num_instances += 1
...

>>> ObjectCounter()
<__main__.ObjectCounter object at 0x10392d810>
>>> ObjectCounter()
<__main__.ObjectCounter object at 0x1039810d0>
>>> ObjectCounter()
<__main__.ObjectCounter object at 0x10395b750>
>>> ObjectCounter()
<__main__.ObjectCounter object at 0x103959810>

>>> ObjectCounter.num_instances
4

>>> counter = ObjectCounter()
>>> counter.num_instances
5

يحتفظ ObjectCounter بسمة فئة .num_instances تعمل كعداد للمثيلات. عند تحليل بايثون لهذه الفئة، يُهيئ العداد إلى الصفر ويتركه كما هو. إنشاء مثيلات من هذه الفئة يعني استدعاء دالة .__init__() تلقائيًا وزيادة .num_instances بمقدار واحد.

من المهم ملاحظة أنه يمكنك الوصول إلى سمات الفئة إما باستخدام الفئة نفسها أو إحدى مثيلاتها. لذلك، يمكنك استخدام كائن العداد لاسترجاع قيمة .num_instances. ومع ذلك، إذا كنت بحاجة إلى تعديل سمة فئة، فيجب عليك استخدام الفئة نفسها بدلاً من إحدى مثيلاتها.

على سبيل المثال، إذا استخدمت self لتعديل .num_instances، فسوف تقوم بتجاوز سمة class الأصلية عن طريق إنشاء سمة مثيل جديدة:

>>> class ObjectCounter:
...     num_instances = 0
...     def __init__(self):
...         self.num_instances += 1
...

>>> ObjectCounter()
<__main__.ObjectCounter object at 0x103987550>
>>> ObjectCounter()
<__main__.ObjectCounter object at 0x1039c5890>
>>> ObjectCounter()
<__main__.ObjectCounter object at 0x10396a890>
>>> ObjectCounter()
<__main__.ObjectCounter object at 0x1036fa110>

>>> ObjectCounter.num_instances
0

لا يمكنك تعديل سمات الفئة من خلال مثيلات الفئة التي تحتويها. سيؤدي ذلك إلى إنشاء سمات مثيل جديدة بنفس اسم سمات الفئة الأصلية. لهذا السبب، تُرجع الدالة ObjectCounter.num_instances القيمة 0 في هذا المثال.

بشكل عام، يجب استخدام سمات الفئة لمشاركة البيانات بين مثيلاتها. أي تغييرات على سمة الفئة ستكون مرئية لجميع مثيلاتها.

سمات المثيل

سمات المثيلات هي متغيرات مرتبطة بكائن معين من فئة معينة. قيمة سمة المثيل مرتبطة بالكائن نفسه، وبالتالي، تكون قيمة السمة خاصة بالمثيل الذي يحتويها.

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

ملاحظة: على الرغم من إمكانية تعريف سمات المثيل داخل أي دالة مثيل، يُفضل تعريفها جميعًا في دالة .__init__()، وهي مُهيئ المثيل. يضمن هذا اتساق قيم جميع السمات عند إنشاء مثيل جديد. كما يُسهل هذا تنظيم الكود وتصحيح أخطائه.

خذ بعين الاعتبار فئة السيارة التالية، والتي تحدد مجموعة من سمات المثيل:

class Car:
    def __init__(self, make, model, year, color):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.started = False
        self.speed = 0
        self.max_speed = 200

في هذه الفئة، تُعرّف ما مجموعه سبع سمات مثيل داخل.__init__(). تأخذ السمات .make و.model و.year و.color قيمًا من وسيطات  .__init__()، وهي الوسيطات التي يجب تمريرها إلى مُنشئ الفئة، Car()، لإنشاء كائنات ملموسة.

بعد ذلك، يمكنك تهيئة السمات .started و.speed و.max_speed بشكل صريح باستخدام قيم معقولة لا تأتي من المستخدم.

ملاحظة: داخل أي فئة، يجب الوصول إلى جميع سمات المثيلات عبر الوسيطة self. تحتوي هذه الوسيطة على مرجع للمثيل الحالي، حيث توجد السمات. تلعب الوسيطة self دورًا أساسيًا في فئات بايثون. ستتعلم المزيد عن self في قسم “طرق المثيلات مع self”.

إليك كيفية عمل فئة Car عمليًا:

>>> from car import Car

>>> toyota_camry = Car("Toyota", "Camry", 2022, "Red")
>>> toyota_camry.make
'Toyota'
>>> toyota_camry.model
'Camry'
>>> toyota_camry.color
'Red'
>>> toyota_camry.speed
0

>>> ford_mustang = Car("Ford", "Mustang", 2022, "Black")
>>> ford_mustang.make
'Ford'
>>> ford_mustang.model
'Mustang'
>>> ford_mustang.year
2022
>>> ford_mustang.max_speed
200

في هذه الأمثلة، تُنشئ مثيلين مختلفين من Car. يأخذ كل مثيل وسيطات إدخال محددة في وقت الإنشاء لتهيئة جزء من سماته. لاحظ كيف تختلف قيم السمات المرتبطة وتختلف باختلاف المثيل المحدد.

بخلاف سمات الفئة، لا يمكنك الوصول إلى سمات المثيلات من خلال الفئة. بل يجب عليك الوصول إليها من خلال المثيل الذي يحتويها:

>>> Car.make
Traceback (most recent call last):
    ...
AttributeError: type object 'Car' has no attribute 'make'

سمات المثيلات خاصة بمثيل محدد من فئة معينة. لذا، لا يمكنك الوصول إليها من خلال كائن الفئة. إذا حاولت القيام بذلك، فستحصل على استثناء AttributeError.

السمة  .__dict__

في بايثون، لكلٍّ من الفئات والمثيلات سمة خاصة تُسمى .__dict__. تحتوي هذه السمة على قاموس يحتوي على العناصر القابلة للكتابة للفئة أو المثيل الأساسي. تذكر أن هذه العناصر يمكن أن تكون سمات أو توابع. يُمثل كل مفتاح في .__dict_ اسم سمة. تُمثل القيمة المرتبطة بمفتاح مُحدد قيمة السمة المُقابلة.

في الفئة، يحتوي .__dict__ على سمات الفئة وتوابعها. في المثيل، يحتوي .__dict__ على سمات المثيل.

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

وبالمثل، عند الوصول إلى عنصر مثيل من خلال مثيل فعلي لفئة، يبحث بايثون عن اسم العضو في المثيل  .__dict__. إذا لم يظهر الاسم هناك، يبحث بايثون في الفئة  .__dict__. إذا لم يُعثر على الاسم، فسيظهر خطأ NameError.

فيما يلي فئة ألعاب توضح كيفية عمل هذه الآلية:

class SampleClass:
    class_attr = 100

    def __init__(self, instance_attr):
        self.instance_attr = instance_attr

    def method(self):
        print(f"Class attribute: {self.class_attr}")
        print(f"Instance attribute: {self.instance_attr}")

في هذه الفئة، تُعرّف سمة فئة بقيمة ١٠٠. في دالة.__init__()، تُعرّف سمة مثيل تستمد قيمتها من مُدخلات المستخدم. وأخيرًا، تُعرّف دالة لطباعة كلتا السمتين.

الآن حان وقت التحقق من محتوى  .__dict__ في كائن الفئة. شغّل الكود التالي:

>>> from sample_dict import SampleClass

>>> SampleClass.class_attr
100

>>> SampleClass.__dict__
mappingproxy({
    '__module__': '__main__',
    'class_attr': 100,
    '__init__': <function SampleClass.__init__ at 0x1036c62a0>,
    'method': <function SampleClass.method at 0x1036c56c0>,
    '__dict__': <attribute '__dict__' of 'SampleClass' objects>,
    '__weakref__': <attribute '__weakref__' of 'SampleClass' objects>,
    '__doc__': None
})

>>> SampleClass.__dict__["class_attr"]
100

تُظهر الأسطر أن كلاً من سمة الفئة والتابع موجودتان في قاموس الفئة  .__dict__. لاحظ كيف يمكنك استخدام  .__dict__ للوصول إلى قيمة سمات الفئة بتحديد اسم السمة بين قوسين مربعين، كما هو الحال عادةً للوصول إلى المفاتيح في القاموس.

في بعض الحالات، سيحتوي القاموس  .__dict__ على سمات المثيل فقط:

>>> instance = SampleClass("Hello!")

>>> instance.instance_attr
'Hello!'
>>> instance.method()
Class attribute: 100
Instance attribute: Hello!

>>> instance.__dict__
{'instance_attr': 'Hello!'}

>>> instance.__dict__["instance_attr"]
'Hello!'

>>> instance.__dict__["instance_attr"] = "Hello, Pythonista!"
>>> instance.instance_attr
'Hello, Pythonista!'

يحتوي قاموس  .__dict__ في هذا المثال على .instance_attr وقيمته الخاصة بالكائن المعني. يمكنك أيضًا الوصول إلى أي سمة موجودة في المثيل باستخدام .__dict__  واسم السمة بين قوسين مربعين.

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

استخدام .__dict__ لتغيير قيمة سمات المثيلات يُمكّنك من تجنب استثناءات RecursionError عند ربط الوصافات في بايثون. ستتعلم المزيد عن الوصافات في قسم “الخصائص والسمات القائمة على الوصاف”.

السمات الديناميكية للفئة والمثيل

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

على سبيل المثال، يمكنك الاستفادة من ميزة Python هذه عندما لا تعرف السمات المطلوبة لفئة معينة في الوقت الذي تقوم فيه بتعريف تلك الفئة نفسها.

خذ بعين الاعتبار الفئة التالية، والتي تهدف إلى تخزين صف من البيانات من جدول قاعدة بيانات أو ملف CSV:

>>> class Record:
...     """Hold a record of data."""
...

في هذه الفئة، لم تُعرّف أي سمات أو توابع لأنك لا تعرف البيانات التي ستخزنها. لحسن الحظ، يمكنك إضافة سمات وحتى توابع إلى هذه الفئة ديناميكيًا.

على سبيل المثال، لنفترض أنك قرأت صفًا من البيانات من ملف employees.csv باستخدام csv.DictReader. تقرأ هذه الفئة البيانات وتُرجعها في كائن يشبه القاموس. لنفترض الآن أن لديك قاموس البيانات التالي:

>>> john = {
...     "name": "John Doe",
...     "position": "Python Developer",
...     "department": "Engineering",
...     "salary": 80000,
...     "hire_date": "2020-01-01",
...     "is_manager": False,
... }

بعد ذلك، سترغب في إضافة هذه البيانات إلى مثيل لفئة Record، وتحتاج إلى تمثيل كل حقل بيانات كسمة مثيل. إليك كيفية القيام بذلك:

>>> john_record = Record()

>>> for field, value in john.items():
...     setattr(john_record, field, value)
...

>>> john_record.name
'John Doe'
>>> john_record.department
'Engineering'

>>> john_record.__dict__
{
    'name': 'John Doe',
    'position': 'Python Developer',
    'department': 'Engineering',
    'salary': 80000,
    'hire_date': '2020-01-01',
    'is_manager': False
}

في هذا المقطع البرمجي، تُنشئ أولًا مثيلًا من كائن  Record باسم john_record. ثم تُشغّل حلقة for للتكرار على عناصر قاموس البيانات الخاص بك، john. داخل الحلقة، تستخدم الدالة ()setattr المُدمجة لإضافة كل حقل كسمة إلى كائن john_record بالتسلسل. عند فحص john_record، ستلاحظ أنه يُخزّن جميع البيانات الأصلية كسمات.

يمكنك أيضًا استخدام تدوين النقاط والتعيين لإضافة سمات وتوابع جديدة إلى فئة بشكل ديناميكي:

>>> class User:
...     pass
...

>>> # Add instance attributes dynamically
>>> jane = User()
>>> jane.name = "Jane Doe"
>>> jane.job = "Data Engineer"
>>> jane.__dict__
{'name': 'Jane Doe', 'job': 'Data Engineer'}

>>> # Add methods dynamically
>>> def __init__(self, name, job):
...     self.name = name
...     self.job = job
...
>>> User.__init__ = __init__

>>> User.__dict__
mappingproxy({
    ...
    '__init__': <function __init__ at 0x1036ccae0>
})

>>> linda = User("Linda Smith", "Team Lead")
>>> linda.__dict__
{'name': 'Linda Smith', 'job': 'Team Lead'}

هنا، تُنشئ أولاً فئة  User بسيطة بدون سمات أو توابع مخصصة. لتحديد محتوى الفئة، استخدمتَ عبارة  pass كعنصر نائب، وهي طريقة بايثون في عدم القيام بأي شيء.

ثم أنشئ كائنًا باسم jane. لاحظ كيف يمكنك استخدام تدوين النقاط والتعيين لإضافة سمات جديدة إلى المثيل. في هذا المثال، أضف سمات .name و.job بقيم مناسبة.

بعد ذلك، تُزوّد ​​فئة  User بمُبدئ أو دالة .__init__(). في هذه الدالة، تأخذ وسيطتي name و job، وتُحوّلهما إلى سمات مثيل في نص الدالة. ثم تُضيف الدالة إلى User ديناميكيًا. بعد هذه الإضافة، يُمكنك إنشاء كائنات User بتمرير الاسم والوظيفة إلى مُنشئ الفئة.

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

السمات القائمة على الخاصية والوصف

يتيح لك بايثون إضافة سلوكيات شبيهة بالوظائف إلى سمات المثيلات الحالية وتحويلها إلى سمات مُدارة. يمنعك هذا النوع من السمات من إدخال تغييرات جذرية على واجهات برمجة التطبيقات (APIs).

بمعنى آخر، باستخدام السمات المُدارة، يمكنك الحصول على سلوك مشابه للوظيفة ووصول مشابه للسمات في آنٍ واحد. لستَ بحاجة إلى تغيير واجهات برمجة التطبيقات (APIs) الخاصة بك عن طريق استبدال السمات باستدعاءات التوابع، مما قد يُسبب خللًا في شيفرة المستخدمين.

لإنشاء سمة مُدارة ذات سلوك يشبه الوظيفة في Python، يمكنك استخدام خاصية أو واصف، اعتمادًا على احتياجاتك المحددة.

على سبيل المثال، ارجع إلى فئة Circle وقل إنك بحاجة إلى التحقق من صحة نصف القطر لضمان تخزينه للأرقام الموجبة فقط. كيف يمكنك فعل ذلك دون تغيير واجهة الفئة؟ أسرع طريقة لحل هذه المشكلة هي استخدام خاصية وتطبيق منطق التحقق في دالة الضبط.

إليك الشكل الذي يمكن أن يبدو عليه الإصدار الجديد من Circle:

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if not isinstance(value, int | float) or value <= 0:
            raise ValueError("positive number expected")
        self._radius = value

    def calculate_area(self):
        return math.pi * self._radius**2

فئات خفيفة الوزن مع .__slots__

في فئة بايثون، يُساعد استخدام السمة  .__slots__ على تقليل استهلاك الذاكرة للمثيلات المقابلة. تمنع هذه السمة الإنشاء التلقائي لمثيل .__dict__.. يُعد استخدام .__slots__ مفيدًا بشكل خاص عند استخدام فئة ذات مجموعة ثابتة من السمات، وستستخدمها لإنشاء عدد كبير من الكائنات.

في المثال أدناه، لديك فئة Point بسمة  .__slots__ تتكون من مجموعة من السمات المسموح بها. كل سمة تمثل إحداثيًا ديكارتيًا:

>>> class Point:
...     __slots__ = ("x", "y")
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...

>>> point = Point(4, 8)
>>> point.__dict__
Traceback (most recent call last):
    ...
AttributeError: 'Point' object has no attribute '__dict__'

تُعرّف فئة الPoint  هذه  .__slots__ كمجموعة تحتوي على عنصرين. يُمثل كل عنصر اسم سمة مثيل. لذا، يجب أن تكون سلسلتين تحتويان على مُعرّفات بايثون صالحة.

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

لا تحتوي مثيلات فئة Point على  .__dict__، كما يوضح الكود. هذه الميزة تجعلها أكثر كفاءة في استخدام الذاكرة. لتوضيح هذه الكفاءة، يمكنك قياس استهلاك الذاكرة لمثيل Point. للقيام بذلك، يمكنك استخدام مكتبة Pympler، والتي يمكنك تثبيتها من PyPI باستخدام الأمر python -m pip install pympler.

>>> from pympler import asizeof

>>> asizeof.asizeof(Point(4, 8))
112

تشير دالة asizeof() من Pympler إلى أن الكائن Point(4, 8) يشغل 112 بايتًا في ذاكرة جهاز الكمبيوتر. الآن، ارجع إلى جلسة REPL وأعد تعريف Point دون إضافة السمة  .__slots__. بعد تثبيت هذا التحديث، أعد تشغيل فحص الذاكرة:

>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y

>>> asizeof.asizeof(Point(4, 8))
528

يستهلك الكائن نفسه، Point(4, 8)، الآن 528 بايتًا من الذاكرة. هذا الرقم أكبر بأربع مرات مما كان عليه في النسخة الأصلية من Point. تخيّل حجم الذاكرة الذي سيوفره  .__slots__ إذا اضطررت إلى إنشاء مليون نقطة في شفرتك.

تُضيف السمة .__slots__سلوكًا ثانيًا مثيرًا للاهتمام إلى فئاتك المُخصصة. فهي تمنعك من إضافة سمات مثيلات جديدة ديناميكيًا:

>>> class Point:
...     __slots__ = ("x", "y")
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...

>>> point = Point(4, 8)
>>> point.z = 16
Traceback (most recent call last):
    ...
AttributeError: 'Point' object has no attribute 'z'

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

تحذيرٌ ضروري، فالعديد من آليات بايثون المُدمجة تفترض ضمنيًا أن الكائنات تحتوي على السمة  .__dict__. عند استخدام .__slots__()، فإنك تتجاهل هذا الافتراض، مما يعني أن بعض هذه الآليات قد لا تعمل كما هو متوقع.

تزويد السلوك بالتوابع

تتيح لك فئات بايثون تجميع البيانات والسلوك معًا في كيان واحد من خلال السمات والتوابع، على التوالي. ستستخدم البيانات لتحديد الحالة الحالية للكائن والتوابع اللازمة للعمل على تلك البيانات أو الحالة.

التابع هو مجرد دالة تُعرّفها داخل فئة. بتعريفه هناك، تُصبح العلاقة بين الفئة والتابع واضحة وجلية.

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

في فئة Python، يمكنك تعريف ثلاثة أنواع مختلفة من التوابع:

  • توابع المثيل، التي تأخذ المثيل الحالي، self، كحجة أولى لها
  • توابع الفئة، التي تأخذ الفئة الحالية، cls، كحجة أولى لها
  • التوابع الثابتة، التي لا تأخذ المثيل أو الفئة كحجة

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

في الأقسام التالية، ستتعمق في كيفية عمل كلٍّ من هذه التوابع وكيفية إنشائها في فئاتك. للبدء، ستبدأ بتوابع المثيلات، وهي أكثر الطرق شيوعًا التي ستُعرّفها في فئاتك.

توابع المثيل مع self

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

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

على الرغم من أنه من الممكن استخدام أي اسم للوسيطة الأولى لطريقة المثيل، فإن استخدام self هو بالتأكيد الاختيار الصحيح لأنه سيجعل الكود الخاص بك يبدو مثل كود Python في نظر المطورين الآخرين.

تحتوي وسيطة self على مرجع للمثيل الحالي، مما يسمح لك بالوصول إليه من داخل الدوال. والأهم من ذلك، أنه من خلال self، يمكنك الوصول إلى سمات المثيل وتعديلها، واستدعاء دوال أخرى داخل الفئة.

لتعريف دالة مثيل، ما عليك سوى كتابة دالة عادية تقبل self كمعاملها الأول. حتى هذه النقطة، كتبتَ بالفعل بعض دوال المثيل. لمواصلة التعلّم عنها، ارجع إلى فئة Car.

لنفترض الآن أنك تريد إضافة دوال لبدء تشغيل السيارة وإيقافها وتسريعها وكبحها. للبدء، ابدأ بكتابة دوال .start() و.stop():

class Car:
    def __init__(self, make, model, year, color):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.started = False
        self.speed = 0
        self.max_speed = 200

    def start(self):
        print("Starting the car...")
        self.started = True

    def stop(self):
        print("Stopping the car...")
        self.started = False

التابعان .start() و.stop() بسيطتان للغاية. تأخذان المثيل الحالي، self، كمعاملهما الأول. داخل التابعين، تستخدم self للوصول إلى سمة .started في المثيل الحالي باستخدام تدوين النقاط. ثم تُغيّر القيمة الحالية لهذه السمة إلى True في .start() وإلى False في .stop(). تطبع كلتا التابعين رسائل إعلامية لتوضيح ما تفعله سيارتك.

ملاحظة: يجب أن تعمل دوال المثيل على سمات المثيل إما بالوصول إليها أو بتغيير قيمها. إذا وجدت نفسك تكتب دوال مثيل لا تستخدم self في نصها، فقد لا تكون هذه دوال مثيل. في هذه الحالة، يُنصح باستخدام دالة فئة أو دالة ثابتة، حسب احتياجاتك الخاصة.

الآن يمكنك إضافة لتابعين .accelerate() و.brake()، والتي ستكون أكثر تعقيدًا بعض الشيء:

class Car:
    def __init__(self, make, model, year, color):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.started = False
        self.speed = 0
        self.max_speed = 200

    # ...

    def accelerate(self, value):
        if not self.started:
            print("Car is not started!")
            return
        if self.speed + value <= self.max_speed:
            self.speed += value
        else:
            self.speed = self.max_speed
        print(f"Accelerating to {self.speed} km/h...")

    def brake(self, value):
        if self.speed - value >= 0:
            self.speed -= value
        else:
            self.speed = 0
        print(f"Braking to {self.speed} km/h...")

تأخذ دالة .accelerate() وسيطة تمثل زيادة السرعة التي تحدث عند استدعاء الدالة. للتبسيط، لم تُعيّن أي تحقق لزيادة السرعة المدخلة، لذا قد يُسبب استخدام قيم سالبة مشاكل. داخل الدالة، تتحقق أولًا من تشغيل محرك السيارة، ثم تعود فورًا إذا لم يكن كذلك.

ثم تحقق مما إذا كانت السرعة المتزايدة أقل من أو تساوي الحد الأقصى المسموح به لسيارتك. إذا تحقق هذا الشرط، فقم بزيادة السرعة. وإلا، فاضبط السرعة على الحد الأقصى المسموح به.

تعمل دالة .brake() بشكل مشابه. هذه المرة، تُقارن السرعة المُتناقصة بالقيمة 0، لأن السيارات لا يُمكن أن تكون سرعتها سالبة. إذا كان الشرط صحيحًا، فسيتم إنقاص السرعة وفقًا لمُعامل الإدخال. وإلا، فسيتم ضبط السرعة على الحد الأدنى لها، وهو 0. مرة أخرى، لا يوجد أي تحقق من صحة انخفاض السرعة المُدخل، لذا توخَّ الحذر مع القيم السالبة.

تحتوي فئتك الآن على أربع طرق تعمل على سماتها ومعها. حان وقت القيادة:

>>> from car import Car

>>> ford_mustang = Car("Ford", "Mustang", 2022, "Black")
>>> ford_mustang.start()
Starting the car...

>>> ford_mustang.accelerate(100)
Accelerating to 100 km/h...
>>> ford_mustang.brake(50)
Braking to 50 km/h...
>>> ford_mustang.brake(80)
Braking to 0 km/h...
>>> ford_mustang.stop()
Stopping the car...

>>> ford_mustang.accelerate(100)
Car is not started!

رائع! فئة  Car تعمل بشكل ممتاز! يمكنك تشغيل محرك سيارتك، وزيادة سرعتها، والفرملة، وإيقافها. كما أنك واثق من أنه لا يمكن لأحد زيادة سرعة السيارة إذا كان محركها متوقفًا. ما رأيك في هذا النموذج البسيط للسيارة؟

من المهم ملاحظة أنه عند استدعاء دالة مثيل على مثيل محدد مثل ford_mustang باستخدام نظام النقاط، لن تحتاج إلى توفير قيمة للوسيطة self. يتولى بايثون هذه الخطوة نيابةً عنك، حيث يمرر المثيل المستهدف تلقائيًا إلى self. لذا، ما عليك سوى توفير بقية الحجج.

مع ذلك، يمكنك توفير المثيل المطلوب يدويًا إذا أردت. للقيام بذلك، عليك استدعاء الدالة في الفئة:

>>> ford_mustang = Car("Ford", "Mustang", 2022, "Black")

>>> Car.start(ford_mustang)
Starting the car...
>>> Car.accelerate(ford_mustang, 100)
Accelerating to 100 km/h...
>>> Car.brake(ford_mustang, 100)
Braking to 0 km/h...
>>> Car.stop(ford_mustang)
Stopping the car...

>>> Car.start()
Traceback (most recent call last):
    ...
TypeError: Car.start() missing 1 required positional argument: 'self'

في هذا المثال، تستدعي دوال المثيل مباشرةً على الصنف. لكي يعمل هذا النوع من الاستدعاءات، يجب عليك تحديد قيمة مناسبة للوسيطة self. في هذا المثال، هذه القيمة هي مثيل ford_mustang. لاحظ أنه في حال عدم تحديد مثيل مناسب، سيفشل الاستدعاء مع خطأ TypeError. رسالة الخطأ واضحة جدًا: هناك وسيطة موضعية مفقودة، self.

التوابع والبروتوكولات الخاصة

يدعم بايثون ما يُسمى بالتوابع الخاصة، والتي تُعرف أيضًا باسم توابع dunder أو توابع magic. هذه التوابع عادةً ما تكون توابع مثيل، وهي جزء أساسي من آلية الصفوف الداخلية في بايثون. تشترك هذه التوابع في ميزة مهمة: يستدعيها بايثون تلقائيًا استجابةً لعمليات محددة.

يستخدم بايثون هذه التوابع لمهام متنوعة. فهي توفر مجموعة رائعة من الأدوات التي تتيح لك الاستفادة القصوى من إمكانيات الفئات في بايثون.

ستتعرف على الدوال الخاصة لأن أسمائها تبدأ وتنتهي بعلامة سفلية مزدوجة، وهو أصل اسمها الآخر، دوال dunder (علامة سفلية مزدوجة). يُقال إن .__init__() هي أكثر الدوال الخاصة شيوعًا في فئات بايثون. كما تعلم، تعمل هذه الدالة كمُهيئ للمثيلات. يستدعيها بايثون تلقائيًا عند استدعاء مُنشئ فئة.

قم بالمضي قدمًا وتحديث فئة Car الخاصة بك لإضافة هاتين التابعين:

class Car:
    # ...

    def __str__(self):
        return f"{self.make} {self.model}, {self.color} ({self.year})"

    def __repr__(self):
        return (
            f"{type(self).__name__}"
            f'(make="{self.make}", '
            f'model="{self.model}", '
            f"year={self.year}, "
            f'color="{self.color}")'
        )

توفر دالة .__str__() ما يُعرف بالتمثيل النصي غير الرسمي للكائن. يجب أن تُرجع هذه الدالة سلسلة نصية تُمثل الكائن بطريقة سهلة الاستخدام. يمكنك الوصول إلى التمثيل النصي غير الرسمي للكائن باستخدام إما str() أو print().

التابع.__repr__() مشابه، ولكنه يجب أن يرجع سلسلة نصية تسمح لك بإعادة إنشاء الكائن إن أمكن. لذا، يرجع هذه التابع ما يُعرف بالتمثيل النصي الرسمي للكائن. هذا التمثيل النصي مُوجّه في الغالب لمبرمجي بايثون، وهو مفيد جدًا عند العمل في جلسة REPL تفاعلية.

في الوضع التفاعلي، يعود بايثون إلى استدعاء دالة .__repr__() عند الوصول إلى كائن أو تقييم تعبير، مما يُصدر التمثيل النصي الرسمي للكائن الناتج. في وضع النص البرمجي، يمكنك الوصول إلى التمثيل النصي الرسمي للكائن باستخدام دالة repr() المدمجة.

شغّل الكود التالي لتجربة توابعك الجديدة. تذكر أنه يجب عليك إعادة تشغيل REPL أو إعادة تحميل car.py:

>>> from car import Car

>>> toyota_camry = Car("Toyota", "Camry", 2022, "Red")

>>> str(toyota_camry)
'Toyota Camry, Red (2022)'
>>> print(toyota_camry)
Toyota Camry, Red (2022)

>>> toyota_camry
Car(make="Toyota", model="Camry", year=2022, color="Red")
>>> repr(toyota_camry)
'Car(make="Toyota", model="Camry", year=2022, color="Red")'

تلخيص بناء الجملة واستخدام الفئة: مثال كامل

حتى هذه النقطة، تعلمتَ الكثير عن فئات بايثون: كيفية إنشائها، ومتى وكيف تستخدمها في شيفرتك، والمزيد. في هذا القسم، ستُراجع هذه المعرفة من خلال كتابة فئة تُدمج معظم قواعد اللغة والميزات التي تعلمتها حتى الآن.

ستمثل فئتك موظفًا في شركة معينة، وسيُطبّق سماتٍ وتوابع لإدارة بعض المهام ذات الصلة، مثل تتبع المعلومات الشخصية وحساب عمر الموظف. للبدء، شغّل مُحرّر الأكواد أو بيئة التطوير المُفضّلة لديك، وأنشئ ملفًا باسم employee.py. ثم أضف إليه الكود التالي:

class Employee:
    company = "Example, Inc."

    def __init__(self, name, birth_date):
        self.name = name
        self.birth_date = birth_date

في فئة Employee هذه، تُعرّف سمة فئة تُسمى .company. ستحمل هذه السمة اسم الشركة، وهو اسم مشترك بين جميع الموظفين في كشوف الرواتب.

بعد ذلك، عرّف المُبدئ .__init__()، الذي يأخذ اسم الموظف وتاريخ ميلاده كوسيطين. تذكّر أنه يجب عليك تمرير قيمتين مناسبتين لكلا الوسيطين عند استدعاء مُنشئ الفئة، Employee().

داخل  .__init__()، تُعرّف سمتين عامتين لتخزين اسم الموظف وتاريخ ميلاده. ستكون هاتان السمتان جزءًا من واجهة برمجة تطبيقات الفئة لأنها سمات عامة.

الآن لنفترض أنك تريد تحويل .birth_date إلى خاصية لتحويل تاريخ الإدخال تلقائيًا بتنسيق ISO إلى كائن datetime:

from datetime import datetime

class Employee:
    # ...

    @property
    def birth_date(self):
        return self._birth_date

    @birth_date.setter
    def birth_date(self, value):
        self._birth_date = datetime.fromisoformat(value)

هنا، تُعرّف خاصية.birth_date باستخدام مُزخرف @property. تُرجع دالة getter محتوى ._birth_date. ستحتفظ هذه الخاصية غير العامة بالبيانات الملموسة.

لتحديد دالة الضبط، استخدم مُزخرف @birth_date.setter. في هذه الدالة، تُعيِّن كائن datetime.datetime إلى ._birth_date. في هذا المثال، لا تُجري أي عملية تحقق على بيانات الإدخال، والتي يجب أن تكون سلسلة نصية تحتوي على التاريخ بتنسيق ISO. يمكنك تنفيذ عملية التحقق كتمرين.

بعد ذلك، لنفترض أنك تريد كتابة تابع مثيل عادي لحساب عمر الموظف من تاريخ ميلاده:

from datetime import datetime

class Employee:
    # ...

    def compute_age(self):
        today = datetime.today()
        age = today.year - self.birth_date.year
        birthday = datetime(
            today.year,
            self.birth_date.month,
            self.birth_date.day
        )
        if today < birthday:
            age -= 1
        return age

هنا، .compute_age() هي دالة مثيل لأنها تأخذ المثيل الحالي، self، كمعاملها الأول. داخل هذه الدالة، يمكنك حساب عمر الموظف باستخدام خاصية .birth_date كنقطة بداية.

لنفترض الآن أنك ستنشئ عادةً مثيلات لـ Employee من قواميس تحتوي على بيانات موظفيك. يمكنك إضافة دالة فئة ملائمة لبناء الكائنات بسرعة بهذه الطريقة:

from datetime import datetime

class Employee:
    # ...

    @classmethod
    def from_dict(cls, data_dict):
        return cls(**data_dict)

في مقتطف التعليمات البرمجية هذا، يمكنك تعريف تابع فئة باستخدام الديكور @classmethod. تأخذ هذه الدالة كائن قاموس يحتوي على بيانات موظف مُعيّن. ثم تُنشئ مثيلًا للموظف باستخدام وسيطة cls وتُفكّك القاموس.

أخيرًا، ستضيف توابع خاصة مناسبة.__str__() و .__repr__() لجعل فئتك صديقة للمستخدمين والمطورين على التوالي:

from datetime import datetime

class Employee:
    # ...

    def __str__(self):
        return f"{self.name} is {self.compute_age()} years old"

    def __repr__(self):
        return (
            f"{type(self).__name__}("
            f"name='{self.name}', "
            f"birth_date='{self.birth_date.strftime('%Y-%m-%d')}')"
        )

تُرجع دالة  .__str__() سلسلة نصية تصف الموظف الحالي بطريقة سهلة الاستخدام. وبالمثل، تُرجع دالة  .__repr__() سلسلة نصية تُتيح لك إعادة إنشاء الكائن الحالي، وهو أمر رائع من وجهة نظر المطور.

إليك كيفية استخدام Employee في الكود الخاص بك:

>>> from employee import Employee

>>> john = Employee("John Doe", "1998-12-04")
>>> john.company
Example, Inc.
>>> john.name
'John Doe'
>>> john.compute_age()
25
>>> print(john)
John Doe is 25 years old
>>> john
Employee(name='John Doe', birth_date='1998-12-04')

>>> jane_data = {"name": "Jane Doe", "birth_date": "2001-05-15"}
>>> jane = Employee.from_dict(jane_data)
>>> print(jane)
Jane Doe is 23 years old

رائع! فئة “الموظف” لديك تعمل بشكل ممتاز حتى الآن! فهي تتيح لك تمثيل الموظفين، والوصول إلى سماتهم، وحساب أعمارهم. كما توفر تمثيلات نصية أنيقة تجعل صفك يبدو أنيقًا وموثوقًا. عمل رائع! هل لديك أي أفكار لميزات رائعة يمكنك إضافتها إلى “الموظف”؟

تصحيح أخطاء فئات بايثون

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

على سبيل المثال، إذا حاولت الوصول إلى سمة أو تابع غير موجودة، فستحصل على استثناء AttributeError:

>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...

>>> point = Point(4, 8)
>>> point.z
Traceback (most recent call last):
    ...
AttributeError: 'Point' object has no attribute 'z'

لا تقوم فئة Point بتعريف سمة مثيل .z، لذا ستحصل على استثناء AttributeError إذا حاولت الوصول إلى تلك السمة.

ستجد بعض الاستثناءات التي قد تحدث عند العمل مع فئات بايثون. إليك بعضًا من أكثرها شيوعًا:

  • يحدث خطأ AttributeError عندما لا يُعرّف الكائن المُحدد السمة أو التابع التي تحاول الوصول إليها. على سبيل المثال، الوصول إلى .z على فئة  Point المُعرّفة في المثال أعلاه.
  • يحدث خطأ TypeError عند تطبيق عملية أو دالة على كائن لا يدعمها. على سبيل المثال، لنفترض استدعاء الدالة ()len المضمنة مع رقم كمعامل.
  • يحدث خطأ NotImplementedError عندما لا تُنفَّذ تابع مُجرَّد في فئة فرعية مُحدَّدة. ستتعرف على المزيد حول هذا الاستثناء في قسم “إنشاء فئات أساسية مُجرَّدة (ABC)” والواجهات.

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

  • نسيان تضمين وسيطة self في توابع المثيل
  • نسيان إنشاء مثيل للفئة عن طريق استدعاء منشئها باستخدام الوسائط المناسبة
  • الخلط بين سمات الفئة والمثيل وإساءة استخدامها
  • عدم اتباع أو احترام اتفاقيات التسمية للأعضاء
  • الوصول إلى الأعضاء غير العامة من خارج الفئة المحتوية
  • الإفراط في استخدام الميراث وإساءة استخدامه

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

استكشاف الفئات المتخصصة من المكتبة القياسية

ستجد في مكتبة بايثون القياسية العديد من الأدوات التي تحل مشاكل مختلفة وتتعامل مع تحديات متنوعة. من بين كل هذه الأدوات، ستجد بعضًا منها يزيد إنتاجيتك عند كتابة فئات مخصصة.

على سبيل المثال، إذا كنت تريد أداة توفر عليك عناء كتابة قدر كبير من التعليمات البرمجية النمطية المتعلقة بالفئة، فيمكنك الاستفادة من فئات البيانات ووحدة dataclasses.

وبالمثل، إذا كنت تبحث عن أداة تسمح لك بإنشاء عدادات ثابتة تعتمد على الفئة بسرعة، فيمكنك تحويل نظرك إلى وحدة enum وأنواعها المختلفة من فئات التعداد.

في الأقسام التالية، ستتعلم أساسيات استخدام فئات البيانات والعدادات لكتابة فئات قوية وموثوقة ومتخصصة بكفاءة في Python.

فئات البيانات

تتخصص فئات بيانات بايثون في تخزين البيانات. ومع ذلك، فهي أيضًا مُولِّدات أكواد تُنتج الكثير من أكواد الأكواد النمطية المتعلقة بالفئات، والتي تُمكِّنك من استخدامها في الخلفية.

على سبيل المثال، إذا استخدمتَ البنية التحتية لفئة البيانات لكتابة فئة مخصصة، فلن تحتاج إلى تنفيذ دوال خاصة مثل  .__init__() و .__repr__() و .__eq__() و.__hash__(). ستكتبها فئة البيانات نيابةً عنك. والأهم من ذلك، ستكتب هذه الدوال بتطبيق أفضل الممارسات وتجنب الأخطاء المحتملة.

كما تعلم، تدعم توابع خاصة وظائف مهمة في فئات بايثون. في حالة فئات البيانات، ستحصل على تمثيل دقيق للسلاسل، وإمكانات مقارنة، وقابلية تجزئة، وغيرها.

مع أن اسم فئة البيانات قد يوحي بأن هذا النوع من الفئات يقتصر على احتواء البيانات، إلا أنه قد يحتوي أيضًا على توابع. لذا، تُشبه فئات البيانات الفئات العادية، ولكن بميزات فائقة.

لإنشاء فئة بيانات، استورد مُزخرف @dataclass من وحدة dataclasses. ستستخدم هذا المُزخرف في تعريف فئتك. هذه المرة، لن تكتب دالة .__init__() ، بل ستُعرِّف حقول البيانات فقط كسمات فئة مع تلميحات النوع.

على سبيل المثال، إليك كيفية كتابة فئة ThreeDPoint كفئة بيانات:

from dataclasses import dataclass

@dataclass
class ThreeDPoint:
    x: int | float
    y: int | float
    z: int | float

    @classmethod
    def from_sequence(cls, sequence):
        return cls(*sequence)

    @staticmethod
    def show_intro_message(name):
        print(f"Hey {name}! This is your 3D Point!")

يستخدم هذا التطبيق الجديد لـ ThreeDPoint مُزيّن @dataclass في بايثون لتحويل الفئة العادية إلى فئة بيانات. بدلاً من تعريف دالة  .__init__()، يمكنك سرد السمات مع أنواعها المقابلة. ستتولى فئة البيانات كتابة مُبدئ مناسب لك. لاحظ أنك لا تُعرّف  .__iter__() أو  .__repr__() أيضًا.

بعد تحديد حقول البيانات أو السمات، يمكنك البدء بإضافة الدوال. في هذا المثال، ستحتفظ بدوالتي .from_sequence() و.show_intro_message() الثابتتين.

قم بالمضي قدمًا وتشغيل الكود التالي للتحقق من الوظيفة الإضافية التي أضافتها @dataclass إلى هذا الإصدار من ThreeDPoint:

>>> from dataclasses import astuple
>>> from point import ThreeDPoint

>>> point_1 = ThreeDPoint(1.0, 2.0, 3.0)
>>> point_1
ThreeDPoint(x=1.0, y=2.0, z=3.0)
>>> astuple(point_1)
(1.0, 2.0, 3.0)

>>> point_2 = ThreeDPoint(2, 3, 4)
>>> point_1 == point_2
False

>>> point_3 = ThreeDPoint(1, 2, 3)
>>> point_1 == point_3
True

يعمل صنف ThreeDPoint لديك بكفاءة عالية! فهو يوفر تمثيلًا نصيًا مناسبًا باستخدام دالة  .__repr__() المُولّدة تلقائيًا. يمكنك تكرار الحقول باستخدام دالة ()astuple من وحدة dataclasses. وأخيرًا، يمكنك مقارنة مثيلين من الصنف للمساواة (==). وكما تستنتج، فقد وفر عليك هذا الإصدار الجديد من ThreeDPoint كتابة عدة أسطر من أكواد جاهزة معقدة.

استخدام الميراث وبناء التسلسلات الهرمية للفئات

الوراثة ميزة فعّالة في البرمجة الكائنية التوجه. وهي عبارة عن إنشاء علاقات هرمية بين الفئات، حيث ترث الفئات الفرعية السمات والتوابع من الفئة الأم. في بايثون، يمكن أن يكون للفئة الواحدة عدة فئات أب، أو بمعنى أعم، أسلاف.

يُعرف هذا باسم وراثة التنفيذ، والذي يسمح لك بتقليل التكرار من خلال إعادة استخدام الكود. كما يمكن أن يجعل كودك أكثر قابلية للتكيف، وأفضل تنظيمًا، وأكثر قابلية للتوسع. ومع ذلك، ترث الفئات أيضًا الواجهة من خلال أن تصبح أنواعًا أكثر تخصصًا من أسلافها. في بعض الحالات، ستتمكن من استخدام مثيل فرعي حيث يُتوقع وجود سلف.

في الأقسام التالية، ستتعلم كيفية استخدام الوراثة في بايثون. ستبدأ بالوراثة البسيطة، ثم تنتقل إلى مفاهيم أكثر تعقيدًا. لذا، استعد! سيكون هذا ممتعًا!

الميراث البسيط

عندما يكون لديك فئة ترث من فئة رئيسية واحدة، فأنت تستخدم الوراثة أحادية القاعدة أو الوراثة البسيطة. لجعل فئة بايثون ترث من فئة أخرى، عليك إدراج اسم الفئة الرئيسية بين قوسين بعد اسم الفئة الفرعية في التعريف.

هذا هو بناء الجملة الذي يجب عليك استخدامه:

class Parent:
    # Parent's definition goes here...
    <body>

class Child(Parent):
    # Child definitions goes here...
    <body>

في هذا المقطع، “Parent” هو الفئة التي تريد وراثتها. عادةً ما توفر فئات Parent وظائف عامة وشائعة يمكنك إعادة استخدامها في فئات فرعية متعددة. ” Child” هو الفئة التي ترث السمات والتوابع من “Parent“. يوضح السطر المميز الصيغة المطلوبة.

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

ستوفر فئة Vehicle سمات مشتركة، مثل .make و.model و.year. كما ستوفر التابعين .start() و.stop() لبدء وإيقاف محرك المركبة، على التوالي:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self._started = False

    def start(self):
        print("Starting engine...")
        self._started = True

    def stop(self):
        print("Stopping engine...")
        self._started = False

في هذا الكود، تُعرّف فئة Vehicle بسمات وتوابع مشتركة بين جميع أنواع مركباتك. يمكنك القول إن Vehicle تُوفّر الواجهة المشتركة لجميع مركباتك. سترث من هذه الفئة إعادة استخدام هذه الواجهة ووظائفها في فئاتك الفرعية.

يمكنك الآن تعريف فئتي Car و Motorcycle. لكلٍّ منهما سمات وتوابع فريدة خاصة بنوع المركبة. على سبيل المثال، ستحتوي السيارة على سمة .num_seats وتابع .drive():

# ...

class Car(Vehicle):
    def __init__(self, make, model, year, num_seats):
        super().__init__(make, model, year)
        self.num_seats = num_seats

    def drive(self):
        print(f'Driving my "{self.make} - {self.model}" on the road')

    def __str__(self):
        return f'"{self.make} - {self.model}" has {self.num_seats} seats'

تستخدم فئة Car الخاصة بك الفئة Vehicle كفئة رئيسية. هذا يعني أن Car سترث تلقائيًا سمات .make و.model و.year، بالإضافة إلى سمة ._started غير العامة. كما سترث أيضًا التابعين .start() و.stop().

ملاحظة: كما هو الحال في علم الأحياء، فإن الوراثة في البرمجة الكائنية التوجه تسير في اتجاه واحد، من الآباء إلى الأبناء. بمعنى آخر، يرث الأبناء من آبائهم وليس العكس.

تُعرّف الفئة سمة .num_seats. كما تعلم، يجب تعريف سمات المثيل وتهيئة هذه السمات في دالة.__init__(). يتطلب هذا منك توفير دالة  .__init__() مخصصة في Car، والتي ستُخفي مُهيئ الفئة العليا.

كيف يمكنك كتابة دالة .__init__() في Car مع ضمان تهيئة سمات .make و.model و.year؟ هنا يأتي دور دالة ()super المدمجة. تتيح لك هذه الدالة الوصول إلى عناصر الفئة العليا، كما يوحي اسمها.

في Car، تستخدم ()super لاستدعاء دالة  .__init__() على Vehicle. لاحظ أنك تُمرر قيم الإدخال لـ .make و.model و.year حتى يتمكن Vehicle من تهيئة هذه السمات بشكل صحيح. بعد هذا الاستدعاء لـ ()super، تُضيف وتُهيئ سمة .num_seats، وهي خاصة بفئة Car.

أخيرًا، اكتب الدالة .drive()، وهي أيضًا خاصة بـ Car. هذه الدالة مجرد مثال توضيحي، لذا فهي تطبع رسالة على الشاشة فقط.

الآن حان وقت تعريف فئة  Motorcycle، والتي سترث من فئة  Vehicle أيضًا. ستحتوي هذه الفئة على سمة .num_wheels ودالة .ride():

# ...

class Motorcycle(Vehicle):
    def __init__(self, make, model, year, num_wheels):
        super().__init__(make, model, year)
        self.num_wheels = num_wheels

    def ride(self):
        print(f'Riding my "{self.make} - {self.model}" on the road')

    def __str__(self):
        return f'"{self.make} - {self.model}" has {self.num_wheels} wheels'

مرة أخرى، تستدعي الدالة ()super لتهيئة .make و.model و.year. بعد ذلك، تُعرّف وتُهيئ السمة .num_wheels. وأخيرًا، تكتب الدالة .ride(). هذه الدالة مجرد مثال توضيحي.

مع وضع هذا الكود في مكانه، يمكنك البدء في استخدام  Car و Motorcycle على الفور:

>>> from vehicles import Car, Motorcycle

>>> tesla = Car("Tesla", "Model S", 2022, 5)
>>> tesla.start()
Starting engine...
>>> tesla.drive()
Driving my "Tesla - Model S" on the road
>>> tesla.stop()
Stopping engine...
>>> print(tesla)
"Tesla - Model S" has 5 seats

>>> harley = Motorcycle("Harley-Davidson", "Iron 883", 2021, 2)
>>> harley.start()
Starting engine...
>>> harley.ride()
Riding my "Harley-Davidson - Iron 883" on the road.
>>> harley.stop()
Stopping engine...
>>> print(harley)
"Harley-Davidson - Iron 883" has 2 wheels

رائع! سيارتا تيسلا وهارلي ديفيدسون تعملان بكفاءة. يمكنك تشغيل محركاتهما، وقيادتهما، وما إلى ذلك. لاحظ كيف يمكنك استخدام السمات والتوابع الموروثة والخاصة في كلا الفئتين.

عادةً ما تستخدم الوراثة الفردية أو الوراثة بشكل عام عندما يكون لديك فئات تشترك في سمات وسلوكيات مشتركة وترغب في إعادة استخدامها في فئات مشتقة. لذا، تُعد الوراثة أداة رائعة لإعادة استخدام الكود. ترث الفئات الفرعية وظائف من فئاتها الأصلية وتعيد استخدامها.

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

التسلسلات الهرمية للفئات

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

الفئة أو الفئات في أعلى التسلسل الهرمي هي الفئات الأساسية، بينما الفئات التي تليها هي فئات مشتقة أو فئات فرعية. تُعبّر التسلسلات الهرمية القائمة على الوراثة عن علاقة “هو نوع” بين الفئات الفرعية وفئاتها الأساسية. على سبيل المثال، الطائر نوع من الحيوانات.

يرث كل مستوى في التسلسل الهرمي السمات والسلوكيات من المستويات الأعلى. لذلك، تُعدّ الفئات في أعلى التسلسل الهرمي فئات عامة ذات وظائف مشتركة، بينما تُعدّ الفئات في أسفله أكثر تخصصًا. ترث هذه الفئات السمات والسلوكيات من فئاتها العليا، وتُضيف أيضًا سماتها وسلوكياتها الخاصة.

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

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

class Animal:
    def __init__(self, name, sex, habitat):
        self.name = name
        self.sex = sex
        self.habitat = habitat

class Mammal(Animal):
    unique_feature = "Mammary glands"

class Bird(Animal):
    unique_feature = "Feathers"

class Fish(Animal):
    unique_feature = "Gills"

class Dog(Mammal):
    def walk(self):
        print("The dog is walking")

class Cat(Mammal):
    def walk(self):
        print("The cat is walking")

class Eagle(Bird):
    def fly(self):
        print("The eagle is flying")

class Penguin(Bird):
    def swim(self):
        print("The penguin is swimming")

class Salmon(Fish):
    def swim(self):
        print("The salmon is swimming")

class Shark(Fish):
    def swim(self):
        print("The shark is swimming")

في أعلى التسلسل الهرمي، توجد فئة  Animal. وهي الفئة الأساسية للتسلسل الهرمي. تحتوي على السمات .name و.sex و.habitat، وهي كائنات نصية. هذه السمات مشتركة بين جميع الحيوانات.

بعد ذلك، يمكنك تعريف فئاتMammal و Bird و Fish بالوراثة من فئة  Animal. تحتوي هذه الفئات على سمة   .unique_featureالتي تحمل السمة المميزة لكل مجموعة من الحيوانات.

ثم تُنشئ ثدييات ملموسة مثل Dog و Cat. لهذه الفئات توابع محددة مشتركة بين جميع الكلاب والقطط، على التوالي. وبالمثل، تُعرّف فئتين تَرِثان من Bird وفئتين أخريين تَرِثان من Fish.

فيما يلي رسم تخطيطي للفئة يشبه الشجرة والذي سيساعدك على رؤية العلاقة الهرمية بين الفئات:

يمكن لكل مستوى في التسلسل الهرمي – وهو ما سيفعله عادةً – إضافة سمات ووظائف جديدة إلى تلك التي توفرها مستوياته الأصلية. إذا تجولت في المخطط من الأعلى إلى الزر، فستنتقل من الفئات العامة إلى الفئات المتخصصة.

تُطبّق هذه الفئات الأخيرة توابع جديدة خاصة بالفئة المعنية. في هذا المثال، تطبع هذه التوابع بعض المعلومات على الشاشة وتُرجع تلقائيًا القيمة “None”، وهي القيمة الصفرية في بايثون.

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

الميراث المتعدد

في بايثون، يمكنك استخدام الوراثة المتعددة. يسمح لك هذا النوع من الوراثة بإنشاء صنف يرث من عدة أصناف رئيسية. ويتمتع الصنف الفرعي بإمكانية الوصول إلى السمات والتوابع من جميع أصنافه الرئيسية.

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

فيما يلي مثال صغير للوراثة المتعددة في بايثون:

class Vehicle:
    def __init__(self, make, model, color):
        self.make = make
        self.model = model
        self.color = color

    def start(self):
        print("Starting the engine...")

    def stop(self):
        print("Stopping the engine...")

    def show_technical_specs(self):
        print(f"Make: {self.make}")
        print(f"Model: {self.model}")
        print(f"Color: {self.color}")

class Car(Vehicle):
    def drive(self):
        print("Driving on the road...")

class Aircraft(Vehicle):
    def fly(self):
        print("Flying in the sky...")

class FlyingCar(Car, Aircraft):
    pass

في هذا المثال، تكتب فئة Vehicle بخصائص .make و.model و.color. تحتوي الفئة أيضًا على دوال .start() و.stop() و.show_technical_specs(). ثم تُنشئ فئة سيارة (Car) ترث من Vehicle وتوسّعها باستخدام دالة جديدة تُسمى .drive(). كما تُنشئ فئة  Aircraft ترث من Vehicle وتضيف دالة .fly().

أخيرًا، عرّف فئة FlyingCar لتمثيل سيارة يمكنك قيادتها على الطريق أو الطيران. أليس هذا رائعًا؟ لاحظ أن هذه الفئة تتضمن كلاً من Car وAircraft في قائمة فئاتها الأصلية. لذا، سترث وظائف من كلتا الفئتين الرئيسيتين.

إليك كيفية استخدام فئة FlyingCar:

>>> from crafts import FlyingCar

>>> space_flyer = FlyingCar("Space", "Flyer", "Black")
>>> space_flyer.show_technical_specs()
Make: Space
Model: Flyer
Color: Black

>>> space_flyer.start()
Starting the engine...
>>> space_flyer.drive()
Driving on the road...
>>> space_flyer.fly()
Flying in the sky...
>>> space_flyer.stop()
Stopping the engine...

في هذا المقطع البرمجي، تُنشئ أولًا نسخة من FlyingCar. ثم تستدعي جميع دوالها، بما فيها الدوال الموروثة. كما ترى، يُعزز الوراثة المتعددة إعادة استخدام الكود، مما يسمح لك باستخدام وظائف من عدة فئات أساسية في الوقت نفسه. بالمناسبة، إذا نجحت في جعل FlyingCar يطير، فتأكد من عدم إيقاف المحرك أثناء الطيران!

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


اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading