البرمجة كائنية التوجه (OOP) في لغة بايثون

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

ستكتشف كيفية تعريف الفئات، وإنشاء الفئات لإنشاء الكائنات، والاستفادة من الميراث لبناء أنظمة قوية في Python.

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

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

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

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

يوجد OOP أيضًا في لغات برمجة أخرى وغالبًا ما يتم وصفه بأنه يتمحور حول الركائز الأربع، أو أربعة مستأجرين لـ OOP:

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

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

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

كيف تقوم بتعريف فئة في بايثون؟

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

class Employee:
    def __init__(self, name, age):
        self.name =  name
        self.age = age

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

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

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

إحدى الطرق للقيام بذلك هي تمثيل كل موظف كقائمة:

kirk = ["James Kirk", 34, "Captain", 2265]
spock = ["Spock", 35, "Science Officer", 2254]
mccoy = ["Leonard McCoy", "Chief Medical Officer", 2266]

هناك عدد من المشاكل مع هذا النهج.

أولاً، قد يُصعّب هذا إدارة ملفات التعليمات البرمجية الكبيرة. إذا أشرت إلى kirk[0] على بُعد عدة أسطر من مكان إعلان قائمة kirk، فهل ستتذكر أن العنصر ذي الفهرس 0 هو اسم الموظف؟

ثانيًا، قد يُسبب أخطاءً إذا لم يكن عدد العناصر في قوائم الموظفين متساويًا. في قائمة ماكوي أعلاه، العمر مفقود، لذا سيُرجع  mccoy[1] “المسؤول الطبي الرئيسي” بدلًا من عمر الدكتور ماكوي.

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

الفئات مقابل المثيلات

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

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

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

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

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

تعريف الفئة

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

فيما يلي مثال لفئة Dog:

class Dog:
    pass

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

ملاحظة: تُكتب أسماء فئات بايثون بترميز “الكلمات الكبيرة” اعتياديًا. على سبيل المثال، تُكتب فئة لسلالة كلاب معينة، مثل جاك راسل تيرير، بالشكل التالي: JackRussellTerrier.

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

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

يمكنك إعطاء .__init__() أي عدد من المعلمات، لكن المعلمة الأولى ستكون دائمًا متغيرًا يُسمى self. عند إنشاء مثيل جديد للفئة، يُمرر بايثون المثيل تلقائيًا إلى المعلمة self في .__init__() ليتمكن بايثون من تعريف السمات الجديدة للكائن.

قم بتحديث فئة Dog باستخدام التابع .__init__() التي تنشئ سمات .name و.age:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

تأكد من وضع مسافة بادئة لعلامة الدالة .__init__() بأربع مسافات، ونص الدالة بثمان مسافات. هذه المسافة البادئة بالغة الأهمية، فهي تُعلم بايثون أن الدالة .__init__() تنتمي إلى فئة Dog.

في نص .__init__()، هناك عبارتان تستخدمان المتغير self:

  • self.name = name ينشئ سمة تسمى name ويعين لها قيمة معلمة name.
  • self.age = age ينشئ سمة تسمى age ويعين لها قيمة معلمة age.

السمات المُنشأة في .__init__() تُسمى سمات المثيل. قيمة سمة المثيل خاصة بمثيل مُحدد من الفئة. جميع كائنات  Dog لها اسم وعمر، ولكن قيم سمتي الاسم والعمر تختلف باختلاف مثيل  Dog.

من ناحية أخرى، سمات الفئة هي سمات تحمل القيمة نفسها لجميع مثيلات الفئة. يمكنك تعريف سمة فئة بتعيين قيمة لاسم متغير خارج .__init__().

على سبيل المثال، تحتوي فئة Dog التالية على سمة فئة تسمى species بقيمة “Canis familiaris”:

class Dog:
    species = "Canis familiaris"

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

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

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

الآن بعد أن أصبح لديك فئة Dog، حان الوقت لإنشاء بعض الكلاب!

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

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

>>> class Dog:
...     pass
...
>>> Dog()
<__main__.Dog object at 0x106702d30>

أولاً، قم بإنشاء فئة Dog جديدة بدون أي سمات أو توابع، ثم قم بإنشاء مثيل لفئة Dog لإنشاء كائن Dog.

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

قم الآن بإنشاء فئة Dog للمرة الثانية لإنشاء كائن Dog آخر:

>>> Dog()
<__main__.Dog object at 0x0004ccc90>

يقع مثيل Dog الجديد في عنوان ذاكرة مختلف. وذلك لأنه مثيل جديد تمامًا وفريد ​​تمامًا من نوعه عن أول كائن Dog أنشأته.

لرؤية هذا بطريقة أخرى، اكتب ما يلي:

>>> a = Dog()
>>> b = Dog()
>>> a == b
False

في هذا الكود، تُنشئ كائنين جديدين من فئة Dog وتُسندهما إلى المتغيرين a وb. عند مقارنة a وb باستخدام عامل ==، تكون النتيجة False. على الرغم من أن a وb هما مثيلان لفئة Dog، إلا أنهما يُمثلان كائنين مختلفين في الذاكرة.

سمات الفئة والمثيل

الآن قم بإنشاء فئة Dog جديدة مع سمة فئة تسمى .species وسمات مثيلتين تسمى .name و.age:

>>> class Dog:
...     species = "Canis familiaris"
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
...

لإنشاء مثيل لفئة Dog هذه، عليك إدخال قيمتي الاسم والعمر. إذا لم تفعل ذلك، فسيُثير بايثون TypeError:

>>> Dog()
Traceback (most recent call last):
  ...
TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'

لتمرير الوسائط إلى معلمات الاسم والعمر، ضع القيم بين قوسين بعد اسم الفئة:

>>> miles = Dog("Miles", 4)
>>> buddy = Dog("Buddy", 9)

يؤدي هذا إلى إنشاء مثيلين جديدين لل Dog — أحدهما لكلب يبلغ من العمر أربع سنوات يُدعى مايلز والآخر لكلب يبلغ من العمر تسع سنوات يُدعى Buddy.

يحتوي التابع .__init__() الخاصة بفئة Dog على ثلاثة معلمات، فلماذا تمرر إليها وسيطتين فقط في المثال؟

عند إنشاء مثيل لفئة Dog، يُنشئ بايثون مثيلًا جديدًا لها ويمررها إلى أول معامل في .__init__().. هذا يُلغي معامل self، لذا لن تحتاج سوى إلى الاهتمام بمعاملي name وage.

بعد إنشاء مثيلات الكلب Dog، يمكنك الوصول إلى سمات مثيلاتها باستخدام تدوين النقاط:

>>> miles.name
'Miles'
>>> miles.age
4

>>> buddy.name
'Buddy'
>>> buddy.age
9

يمكنك الوصول إلى سمات الفصل بنفس الطريقة:

>>> buddy.species
'Canis familiaris'

من أهم مزايا استخدام الفئات لتنظيم البيانات ضمان حصول المثيلات على السمات المتوقعة. جميع مثيلات Dog تحتوي على سمات .species و.name و.age، لذا يمكنك استخدام هذه السمات بثقة، مع العلم أنها ستعيد قيمة دائمًا.

على الرغم من ضمان وجود السمات، إلا أن قيمها يمكن أن تتغير ديناميكيًا:

>>> buddy.age = 10
>>> buddy.age
10

>>> miles.species = "Felis silvestris"
>>> miles.species
'Felis silvestris'

في هذا المثال، غيّرت سمة .age لكائن buddy إلى 10. ثم غيّرت سمة .species لكائن miles إلى “Felis silvestris”، وهو نوع من القطط. هذا يجعل Miles كلبًا غريبًا نوعًا ما، ولكنه صحيح في بايثون!

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

توابع المثيل

توابع المثيل هي دوال تُعرّفها داخل فئة، ولا يُمكن استدعاؤها إلا على مثيل من تلك الفئة. وكما هو الحال مع .__init__()، يأخذ تابع المثيل دائمًا self كمعاملها الأول.

افتح نافذة محرر جديدة في IDLE واكتب فئة Dog التالية:

class Dog:
    species = "Canis familiaris"

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

    # Instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # Another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

تحتوي فئة Dog هذه على تابعيين للمثيل:

  1. .description() ترجع سلسلة تعرض اسم وعمر الكلب.
  2. تحتوي الدالة .speak() على معلمة واحدة تسمى sound وترجع سلسلة تحتوي على اسم الكلب والصوت الذي يصدره الكلب.

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

>>> miles = Dog("Miles", 4)

>>> miles.description()
'Miles is 4 years old'

>>> miles.speak("Woof Woof")
'Miles says Woof Woof'

>>> miles.speak("Bow Wow")
'Miles says Bow Wow'

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

عند إنشاء كائن list، يمكنك استخدام print() لعرض سلسلة تبدو مثل القائمة:

>>> names = ["Miles", "Buddy", "Jack"]
>>> print(names)
['Miles', 'Buddy', 'Jack']

قم بطباعة كائن miles لرؤية النتيجة التي ستحصل عليها:

>>> print(miles)
<__main__.Dog object at 0x00aeff70>

عند طباعة miles، ستظهر لك رسالة غامضة تُخبرك أن miles هي كائن Dog في عنوان الذاكرة 0x00aeff70. هذه الرسالة ليست مفيدة. يمكنك تغيير ما يُطبع بتحديد دالة مثيل خاصة تُسمى .__str__().

في نافذة المحرر، قم بتغيير اسم التابع .description() الخاصة بفئة Dog إلى .__str__():

class Dog:
    # ...

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

احفظ الملف واضغط على F5. عند طباعة miles، ستحصل على نتيجة سهلة:

>>> miles = Dog("Miles", 4)
>>> print(miles)
'Miles is 4 years old'

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

كيف ترث من فئة أخرى في بايثون؟

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

تمكنك الوراثة من فئة الأصل عن طريق إنشاء فئة جديدة ووضع اسم فئة الأصل بين قوسين:

class Parent:
    hair_color = "brown"

class Child(Parent):
    pass

في هذا المثال البسيط، يرث الصنف الفرعي Child من الصنف الأساسي Parent. ولأن الأصناف الفرعية تأخذ سمات وتوابع الأصناف الأساسية، فإن Child.hair_color يكون أيضًا “بنيًا” دون تعريفه صراحةً.

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

على الرغم من أن القياس ليس مثاليًا، يمكنك أن تفكر في وراثة الأشياء مثل الوراثة الجينية.

ربما ورثتِ لون شعركِ من والدتكِ، فهو سمةٌ وُلدتِ بها. ولكن ربما تُقرر صبغ شعركِ باللون الأرجواني. بافتراض أن والديك ليسا كذلك، فأنتِ بذلك قد تجاوزتِ سمة لون الشعر التي ورثتها عنهما:

class Parent:
    hair_color = "brown"

class Child(Parent):
    hair_color = "purple"

إذا قمت بتغيير مثال الكود مثل هذا، فسيكون Child.hair_color “أرجوانيًا”.

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

class Parent:
    speaks = ["English"]

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.speaks.append("German")

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

مثال: حديقة الكلاب

تخيل للحظة أنك في حديقة كلاب. ستجد في الحديقة العديد من الكلاب من سلالات مختلفة، وكل منها يمارس سلوكيات مختلفة.

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

يمكنك تعديل فئة Dog في نافذة المحرر عن طريق إضافة سمة .breed:

class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed

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

    def speak(self, sound):
        return f"{self.name} says {sound}"

اضغط F5 لحفظ الملف. الآن، يمكنك تصميم حديقة الكلاب من خلال إنشاء مجموعة من الكلاب المختلفة في النافذة التفاعلية:

>>> miles = Dog("Miles", 4, "Jack Russell Terrier")
>>> buddy = Dog("Buddy", 9, "Dachshund")
>>> jack = Dog("Jack", 3, "Bulldog")
>>> jim = Dog("Jim", 5, "Bulldog")

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

باستخدام فئة Dog فقط، يجب عليك توفير سلسلة لحجة الصوت الخاصة بـ .speak() في كل مرة تستدعيها في مثيل Dog:

>>> buddy.speak("Yap")
'Buddy says Yap'

>>> jim.speak("Woof")
'Jim says Woof'

>>> jack.speak("Woof")
'Jack says Woof'

تمرير سلسلة نصية إلى كل استدعاء لـ .speak() أمرٌ مُكرر وغير مُريح. علاوةً على ذلك، يجب أن تُحدد سمة .breed السلسلة النصية التي تُمثل الصوت الذي تُصدره كل نسخة من Dog، ولكن هنا يجب عليك تمرير السلسلة النصية الصحيحة يدويًا إلى .speak() في كل مرة تستدعيها.

يمكنك تبسيط تجربة العمل مع فئة الكلاب بإنشاء فئة فرعية لكل سلالة كلاب. يتيح لك هذا توسيع نطاق الوظائف التي ترثها كل فئة فرعية، بما في ذلك تحديد وسيطة افتراضية لـ .speak().

فئات الوالدين وفئات الطفل

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

للتوضيح، إليك التعريف الكامل لفئة Dog التي تعمل بها حاليًا:

class Dog:
    species = "Canis familiaris"

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

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

    def speak(self, sound):
        return f"{self.name} says {sound}"

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

لإنشاء فئة فرعية، أنشئ فئة جديدة باسمها الخاص، ثم ضع اسم الفئة الأصلية بين قوسين. أضف ما يلي إلى ملف dog.py لإنشاء ثلاث فئات فرعية جديدة لفئة Dog:

# ...

class JackRussellTerrier(Dog):
    pass

class Dachshund(Dog):
    pass

class Bulldog(Dog):
    pass

اضغط F5 لحفظ الملف وتشغيله. بعد تحديد الفئات الفرعية، يمكنك الآن إنشاء كلاب من سلالات محددة في النافذة التفاعلية:

>>> miles = JackRussellTerrier("Miles", 4)
>>> buddy = Dachshund("Buddy", 9)
>>> jack = Bulldog("Jack", 3)
>>> jim = Bulldog("Jim", 5)

ترث حالات الفئات الفرعية جميع السمات والتوابع الخاصة بالفئة الأصلية:

>>> miles.species
'Canis familiaris'

>>> buddy.name
'Buddy'

>>> print(jack)
Jack is 3 years old

>>> jim.speak("Woof")
'Jim says Woof'

لتحديد الفئة التي ينتمي إليها كائن معين، يمكنك استخدام type():

>>> type(miles)
<class '__main__.JackRussellTerrier'>

ماذا لو أردتَ تحديد ما إذا كان miles أيضًا مثيلًا لفئة Dog؟ يمكنك القيام بذلك باستخدام الدالة المضمنة isinstance():

>>> isinstance(miles, Dog)
True

لاحظ أن دالة ()isinstance تأخذ وسيطتين: كائن وفئة. في المثال أعلاه، تتحقق دالة ()isinstance مما إذا كانت miles نسخة من فئة Dog، وتعيد القيمة True.

إن كائنات miles وbuddy وjack وjim هي جميعها حالات Dog، ولكن miles ليست حالة Bulldog، وjack ليست حالة Dachshund:

>>> isinstance(miles, Bulldog)
False

>>> isinstance(jack, Dachshund)
False

بشكل عام، كل الكائنات التي تم إنشاؤها من فئة فرعية هي حالات للفئة الأصلية، على الرغم من أنها قد لا تكون حالات لفئات فرعية أخرى.

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

تمديد وظيفة الفئة الأصلية

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

لتجاوز دالة مُعرّفة في الفئة الأصلية، عليك تعريف دالة بنفس الاسم في الفئة الفرعية. إليك ما يبدو عليه الأمر بالنسبة لفئة JackRussellTerrier:

# ...

class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return f"{self.name} says {sound}"

# ...

الآن تم تعريف .speak() على فئة JackRussellTerrier مع تعيين الوسيطة الافتراضية للصوت على “Arf”.

حدّث ملف dog.py بفئة JackRussellTerrier الجديدة، ثم اضغط على F5 لحفظ الملف وتشغيله. يمكنك الآن استدعاء دالة .speak() على نسخة JackRussellTerrier دون تمرير أي وسيطة إلى sound:

>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'

في بعض الأحيان تصدر الكلاب أصواتًا مختلفة، لذلك إذا غضب مايلز وبدأ يزأر، فلا يزال بإمكانك استدعاء .speak() بصوت مختلف:

>>> miles.speak("Grrr")
'Miles says Grrr'

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

على سبيل المثال، في نافذة المحرر، قم بتغيير السلسلة التي تم إرجاعها بواسطة .speak() في فئة Dog:

class Dog:
    # ...

    def speak(self, sound):
        return f"{self.name} barks: {sound}"

# ...

احفظ الملف واضغط على F5. الآن، عند إنشاء نسخة جديدة من Bulldog باسم jim، تُرجع الدالة jim.speak() السلسلة النصية الجديدة:

>>> jim = Bulldog("Jim", 5)
>>> jim.speak("Woof")
'Jim barks: Woof'

ومع ذلك، فإن استدعاء.speak() على مثيل JackRussellTerrier لن يعرض النمط الجديد للإخراج:

>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'

أحيانًا يكون من المنطقي تجاوز دالة من فئة رئيسية تمامًا. ولكن في هذه الحالة، لا ترغب في أن تفقد فئة JackRussellTerrier أي تغييرات قد تُجريها على تنسيق سلسلة إخراج Dog.speak().

للقيام بذلك، ما زلتَ بحاجة إلى تعريف دالة .speak() في فئة JackRussellTerrier الفرعية. ولكن بدلًا من تعريف سلسلة الإخراج صراحةً، عليك استدعاء دالة .speak() الخاصة بفئة Dog من داخل دالة .speak() الخاصة بالفئة الفرعية باستخدام نفس الوسائط التي مررتها إلى دالة JackRussellTerrier.speak().

يمكنك الوصول إلى الفصل الأصلي من داخل طريقة الفصل الفرعي باستخدام super():

# ...

class JackRussellTerrier(Dog):
    def speak(self, sound="Arf"):
        return super().speak(sound)

# ...

عندما تقوم باستدعاء super().speak(sound) داخل JackRussellTerrier، يبحث Python في الفصل الرئيسي، Dog، عن التابع .speak() ويدعوه بالمتغير sound.

حدّث dog.py بفئة JackRussellTerrier الجديدة. احفظ الملف واضغط F5 لتتمكن من اختباره في النافذة التفاعلية:

>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles barks: Arf'

الآن عندما تقوم باستدعاء miles.speak()، سوف ترى إخراجًا يعكس التنسيق الجديد في فئة Dog.

عمل رائع! في هذا القسم، تعلمتَ كيفية تجاوز وتوسيع وتوابع فئة رئيسية، وعملتَ على مثال عملي صغير لتعزيز مهاراتك الجديدة.

في هذا البرنامج التعليمي، تعلّمتَ البرمجة الكائنية التوجه (OOP) في بايثون. تتبع العديد من لغات البرمجة الحديثة، مثل جافا، وسي شارب، وسي++، مبادئ البرمجة الكائنية التوجه، لذا ستكون المعرفة التي اكتسبتها هنا قابلة للتطبيق أينما توجهت في مسيرتك البرمجية.


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

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

اترك تعليقاً

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

Scroll to Top

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

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

Continue reading