مفهوم الـ Overriding في بايثون
في الدرس السابق, شاهدت كيف أن الكلاس الإبن ( Subclass ) يرث كل المتغيرات و الدوال الموجودة في الكلاس الأب ( Superclass ). و تعلمت أيضاً طريقة تطبيق مبدأ إخفاء البيانات من أجل ضمان أن لا يحدث تضارب في أسماء المتغيرات و الدوال الموجودة في الكلاس الإبن و الكلاس الأب.
مبدأ إخفاء البيانات يتيح لنا الوصول للأشياء الموجودة في الكائن سواء أصلها من الكلاس الإبن أو الكلاس الأب.
Override: تعني تعريف الدالة التي ورثها الكلاس الإبن من الكلاس الأب من جديد, هذه الدالة الجديدة تكون مشابهة للدالة الموروثة من حيث الشكل فقط, أي لها نفس الإسم و النوع و عدد الباراميترات, لكن محتواها مختلف.
إذاً, الهدف الحقيقي من الـ Overriding هو إتاحة الفرصة للكلاس الإبن لأن يعرف الدوال حسب حاجته.
في دروس لاحقة, سنرث من كلاسات جاهزة في بايثون, و نفعل Override للدوال الموجودة فيها لكي تناسب التطبيقات التي سنقوم ببنائها.
شروط الـ Overriding
لا يمكنك أن تفعل Override لدالة معرفة في الأساس بشكل خفي لأن إخفاء الأسماء يضمن عدم حدوث مشكلة في تضارب الأسماء.
عدد و نوع باراميترات الدالة الجديدة يجب أن يطابق عدد و نوع باراميترات الدالة القديمة.
عدم تحقق هذين الشرطين يعني أنك تعرف دالة جديدة في الكلاس الإبن و الدالة التي ورثها من الكلاس الأب يستطيع الكائن الذي تنشئه من الكلاس الإبن أن يقوم باستدعائها.
مثال
الآن لنفترض أننا قمنا بتعريف كلاس إسمه CountryInfo, يحتوي على دالة إسمها print_language().
بعدها قمنا بتعريف ثلاث كلاسات, و كلها ترث من الكلاس CountryInfo. إذاً كل كلاس منهم سيحتوي على الدالة print_language().
هنا الفكرة أن أي كلاس يرث من كلاس CountryInfo قد يضطر إلى تعريف الدالة print_language() من جديد حتى تناسبه.
مثال
# هذا الكلاس يعتبر الكلاس الأساسي لأي دولة في العالم, إذاً يجب أن يرثه أي كلاس يمثل دولة
class CountryInfo:
def print_language(self):
print('English') # هنا قمنا بوضع اللغة الإنجليزية كلغة إفتراضية لجميع البلدان
# حتى نستطيع الوراثة منه PrintInfo.py الموجود في الملف PrintInfo هنا قمنا بتضمين الكلاس from CountryInfo import CountryInfo # 'English' التي تطبع كلمة print_language() و بالتالي ورث الدالة CountryInfo يرث من الكلاس Australia هنا قمنا بتعريف كلاس إسمه class Australia(CountryInfo): pass # من جديد لأن اللغة الإنجليزية هي لغة أستراليا print_language() هنا لا داعي لتعريف الدالة
# حتى نستطيع الوراثة منه PrintInfo.py الموجود في الملف PrintInfo هنا قمنا بتضمين الكلاس
from CountryInfo import CountryInfo
# 'English' التي تطبع كلمة print_language() و بالتالي ورث الدالة CountryInfo يرث من الكلاس Lebanon هنا قمنا بتعريف كلاس إسمه
class Lebanon(CountryInfo):
# لأن اللغة العربية هي لغة لبنان 'English' بدل كلمة 'Arabic' من جديد لجعلها تطبع كلمة print_language() هنا يجب تعريف الدالة
def print_language(self):
print('Arabic')
# حتى نستطيع الوراثة منه PrintInfo.py الموجود في الملف PrintInfo هنا قمنا بتضمين الكلاس
from CountryInfo import CountryInfo
# 'English' التي تطبع كلمة print_language() و بالتالي ورث الدالة CountryInfo يرث من الكلاس Spain هنا قمنا بتعريف كلاس إسمه
class Spain(CountryInfo):
# لأن اللغة الإسبانية هي لغة إسبانيا 'English' بدل كلمة 'Spanish' من جديد لجعلها تطبع كلمة print_language() هنا يجب تعريف الدالة
def print_language(self):
print('Spanish')
# حتى نستطيع إنشاء كائنات منهم Spain و Lebanon ,Australia هنا قمنا بتضمين الكلاسات from Australia import Australia from Lebanon import Lebanon from Spain import Spain # Spain و Lebanon ,Australia هنا قمنا بإنشاء كائنات من البلدان الثلاثة au = Australia() lb = Lebanon() sp = Spain() # من كل كائن لعرض لغة كل بلد print_language() هنا قمنا باستدعاء الدالة au.print_language() lb.print_language() sp.print_language()
•سنحصل على النتيجة التالية عند تشغيل الملف Test.
Arabic
Spanish
إذا فهمت المثال السابق جيداً, فأنت الآن فهمت حتماً لما قد تحتاج أن تفعل Override للدوال في الكلاس الإبن.
معلومة تقنية
إذا عدت للكلاس Lebanon, ستجد أننا فعلنا Override للدالة print_language() لكي تطبع كلمة Arabic بدل كلمة English في حال إنشاء كائن من الكلاس Lebanon و إستدعاءها منه.
فعلياً, الكلاس Lebanon أصبح يملك دالتين إٍسمهما print_language() و ليس دالة واحدة كالتالي:
الأولى هي التي ورثها في الأساس من الكلاس
CountryInfo.الثانية هي التي قمنا بتعريفها فيه من جديد في الكلاس
Lebanon( أي عندما فعلنا Override ).
بما أن الكلاس Lebanon يملك دالتين لهما نفس الإسم و عدد الباراميترات, كيف عرف مفسّر بايثون أي دالة منهما سينفذ عند تشغيل الكود؟
مفسّر بايثون يبحث في المرتبة الأولى عن الدالة التي قمت باستدعائها في الكلاس الذي تم منه إنشاء الكائن.
إذا وجد الدالة في الكلاس فإنه يقوم بتنفيذها. إن لم يجد يجدها يبحث عنها في الكلاس الأب. إن وجدها في الكلاس الأب يقوم بتنفيذها.
ملاحظة
في الكلاس Lebanon يمكنك إستدعاء الدالة print_language() التي ورثها من الكلاس CountryInfo بالإعتماد على دالة إسمها super().
لا تقلق إذا لم تفهم المقصود الآن لأنك ستتعرف على هذه الدالة بعد قليل.
الدالة super() في بايثون
هذه الدالة تجعلك قادر على الوصول إلى الدالة المعرفة في الكلاس الأب ( Superclass ) من الكلاس الإبن ( Subclass ) سواء كان يوجد تضارب في الأسماء أو لا يوجد و بالتالي يصبح الكلاس الإبن قادر على أن يعيد كتابة الدالة التي ورثها من الكلاس الأب و أن يصل للدالة التي ورثها من الكلاس الأب أيضاً.
و بمعنى آخر فإن هذه الدالة أن تجعل المبرمج قادر على الإستفادة من الدالة الموجودة في الكلاس الأب و إضافة أشياء جديدة عليها في الكلاس الإبن.
ستتعلم من المثال التالي كيف تقوم باستدعاء الدالة __init__() الموجودة في الكلاس الأب بشكل تلقائي من الدالة __init__() الموجودة في الكلاس الإبن.
تذكر
الباراميترات التي تعرفها بداخل الدالة __init__() يتم تحويلها إلى خصائص للكائن الذي يتم إنشاؤه و هذه الدالة تستدعى بشكل تلقائي عند إنشاء الكائن.
مثال
الآن لنفترض أننا قمنا بتعريف كلاس إسمه Person, يحتوي على خاصيّتين ( name و age ) يتم إنشاؤهما في لحظة إنشاء كائن من الكلاس لأننا وضعناهما في الدالة __init__().
بعدها قمنا بتعريف كلاس إسمه Student يرث من الكلاس Person. في هذا الكلاس قمنا بتعريف الدالة __init__() من جديد و وضعنا فيها نفس الباراميترات الموجودة في الدالة __init__() التي ورثها الكلاس و أضفنا فيها باراميتر جديد إسمه specialization.
كما أننا قمنا بتعريف دالة إسمها print_info() تعرض كل قيم الخصائص الموجودة في الكلاس.
مثال
# هذا الكلاس يعتبر الكلاس الأساسي لأي إنسان حيث أن كل إنسان يملك إسم و عمر class Person: # التي سيملكها أي كائن ننشئه منه Person التي وضعنا فيها خصائص الكلاس __init__() هنا قمنا بتعريف الدالة def __init__(self, name, age): self.name = name self.age = age
# حتى نستطيع الوراثة منه Person.py الموجود في الملف Person هنا قمنا بتضمين الكلاس
from Person import Person
# لأنه يمكنه الإستفادة منه Person يعتبر الكلاس الأساسي لإنشاء أي طالب, هذا الكلاس جعلناه يرث من الكلاس Student هنا قمنا بتعريف كلاس إسمه
class Student(Person):
# و التي سيملكها أي كائن ننشئه منه Student التي وضعنا فيها خصائص الكلاس __init__() هنا قمنا بتعريف الدالة
def __init__(self, name, age, specialization):
# اللتين ندخلهما عند إنشاء كائن age و قيمة الباراميتر name و مررنا لها قيمة البارميتر Person الموجودة في الكلاس __init__() هنا قمنا باستدعاء الدالة
super().__init__(name, age)
# التي ذكرناها في هذا الكلاس سيتم إعطائها آخر قيمة نمررها في أقواس الكائن الذي ننشئه من هذا الكلاس specialization الخاصية
self.specialization = specialization
# Student هنا قمنا بتعريف دالة تطبع كل قيم الخصائص التي يملكها أي كائن ننشئه من الكلاس
def print_info(self):
print('name:', self.name)
print('age:', self.age)
print('specialization:', self.specialization)
# حتى نستطيع إنشاء كائن منه Student.py الموجود في الملف Student هنا قمنا بتضمين الكلاس
from Student import Student
# s و خزناه في Student هنا قمنا بإنشاء كائن من الكلاس
s = Student('Mhamad', 24, 'Computer Science')
# حتى تعرض قيم خصائصه s من الكائن print_info() هنا قمنا باستدعاء الدالة
s.print_info()
•سنحصل على النتيجة التالية عند تشغيل الملف Test.
age: 24
specialization: Computer Science
مفهوم الدوال الثابتة في بايثون
الدالة الثابتة ( Static Method ) عبارة عن دالة يمكن الوصول إليها مباشرةً من الكلاس الذي يحتويها بدون الحاجة إلى إنشاء كائن منه.
إذاً الفكرة الأساسية من تعريف الدالة كدالة ثابتة هي جعل الدالة قابلة للإستدعاء بشكل مباشر من الكلاس الموضوعة بداخله.
أهم شيء عليك معرفته هو أن الدوال الثابتة مصممة للتعامل مع الكلاس الموضوعة فيه بشكل عام و ليس مع الكائنات المنشئة من هذا الكلاس بشكل خاص.
كما أنه حتى إن تم إستدعاء الدوال الثابتة من كائن من هذا الكلاس, فإنه لن يتم التعامل مع هذه الدالة بشكل خاص بالنسبة للكائن بل سيبقى كأنك تستدعيها بشكل مباشر من الكلاس. و بما أن الدوال الثابتة هي دوال غير مصممة للتعامل مع الكائنات بشكل خاص, فلهذا السبب لا يمكن أبداً وضع الكلمة self بداخل دالة ثابتة.
شروط تعريف دالة ثابتة
الدالة الثابتة يتم تعريفها تماماً كأي دالة عادية مع وضع الكلمة
@staticmethodفوقها كمؤشر على انها دالة ثابتة.يمنع وضع الكلمة
selfبداخلها لأن هذه الكلمة تستخدم في الأساس للوصول للمتغيرات الموضوعة كخصائص بالنسبة للكائنات التي سيتم إنشاءها من الكلاس.للوصول إلى أي متغير موضوع في الكلاس من الدالة الثابتة, نكتب إسم الكلاس, ثم نضع نقطة, ثم نضع إسم المتغير.
لا تقلق إن لم تفهم كل تم ذكره حتى الآن لأنك ستفهم كل شيء لاحقاً من الأمثلة.
أمثلة شاملة
في المثال التالي قمنا بتعريف كلاس إسمه StaticExample وضعنا فيه دالة ثابتة إسمها print_msg() مهتمها فقط طباعة جملة عادية.
المثال الأول
# StaticExample هنا قمنا بتعريف الكلاس
class StaticExample:
# هنا قمنا بتعريف دالة ثابتة في الكلاس عند إستدعاءها تقوم بطباعة جملة عادية فقط
@staticmethod
def print_msg():
print("Static method can be called directly from the class.")
# حتى نستطيع التعامل معه StaticExample هنا قمنا بتضمين الكلاس from StaticExample import StaticExample # بدون الحاجة لإنشاء كائن منه StaticExample بشكل مباشر من الكلاس print_msg() هنا قمنا باستدعاء الدالة الثابتة StaticExample.print_msg()
•سنحصل على النتيجة التالية عند تشغيل الملف Test.
في المثال التالي قمنا بتعريف كلاس إسمه Session وضعنا فيه 4 متغيرات ( title - language - teacher - credits ) و دالة ثابتة إسمها print_info() مهتمها فقط طباعة قيم هذه المتغيرات بطريقة مرتبة.
المثال الثاني
# Session هنا قمنا بتعريف الكلاس
class Session:
# هنا قمنا بوضع 4 متغيرات ( أي خصائص ) في الكلاس
title = 'Python for beginners'
language = 'English'
teacher = 'Sara Smith'
credits = 5
# هنا قمنا بتعريف دالة ثابتة في الكلاس عند إستدعاءها تقوم بطباعة قيم الخصائص بشكل مرتب
# لاحظ أنك مجبر على وضع إسم الكلاس ثم نقطة ثم إسم المتغير الذي تريد الوصول إليه من داخل الدالة
@staticmethod
def print_info():
print('Title:', Session.title)
print('Language:', Session.language)
print('Teacher:', Session.teacher)
print('Credits Number:', Session.credits)
# حتى نستطيع التعامل معه Session هنا قمنا بتضمين الكلاس from Session import Session # Session مباشرةً من الكلاس print_info() هنا قمنا باستدعاء الدالة الثابتة Session.print_info()
•سنحصل على النتيجة التالية عند تشغيل الملف Test.
Language: English
Teacher: Sara Smith
Credit Number: 5
في المثال التالي قمنا بتعريف كلاس إسمه MyTools وضعنا فيه دالة ثابتة إسمها print_words_count() الهدف منها طباعة عدد الكلمات الموجودة في النص الذي نمرره لها عند استدعاءها.
المثال الثالث
# MyTools هنا قمنا بتعريف الكلاس
class MyTools:
# و تحتوي على باراميتر واحد print_words_count هنا قمنا بتعريف دالة ثابتة إسمها
@staticmethod
def print_words_count(val):
# هنا قلنا إذا كانت القيمة التي مررناها لها عبارة عن قيمة نصية
if isinstance(val, str):
# إذا كانت هذه القيمة النصية عبارة عن نص فارغ سيتم طباعة أن النص عبارة عن نص فارغ و أن عدد الكلمات فيه هو صفر
if val == '':
print('Empty string, so number of words is 0')
# و من ثم طباعة قيمته words_count إذا لم تكن هذه القيمة النصية عبارة عن نص فارغ سيتم تخزين عدد الكلمات الموجودة فيه في المتغير
else:
words_count = len(val.split())
print('Number of words is:', words_count)
# عبارة عن قيمة نصية سيتم طباعة الجملة التالية val إذا لم تكن قيمة الباراميتر
else:
print('Oops..', val, 'is not a string!')
# حتى نستطيع التعامل معه MyTools هنا قمنا بتضمين الكلاس from MyTools import MyTools # هنا قمنا بتعريف متغير نصي يحتوي على مجموعة كلمات text = 'Today, you are studying static methods.' # text لمعرفة عدد الكلمات الموجودة في المتغير MyTools من الكلاس print_words_count() هنا قمنا باستدعاء الدالة الثابتة MyTools.print_words_count(text)
•سنحصل على النتيجة التالية عند تشغيل الملف Test.
المثال التالي يثبت لك فكرة أنه إن تم إستدعاء الدالة الثابتة من كائن من الكلاس الموجودة فيه, فإنه لن يتم التعامل مع هذه الدالة بشكل خاص بالنسبة للكائن. أي سواء قمت باستدعاءها من الكائن أو من الكلاس ستحصل على نفس النتيجة مع الإشارة إلى أنه يفضل دائماً إستدعاء الدالة الثابتة بشكل مباشر من الكلاس الموجودة فيه و ليس من كائن منه.
المثال الرابع
# StaticExample هنا قمنا بتعريف الكلاس
class StaticExample:
# هنا قمنا بتعريف متغير ( أي خاصية ) في الكلاس و حددنا أن قيمته تساوي 5
x = 5
# الموجود مباشرةُ في الكلاس و ليس في كائن منه x تعرض قيمة المتغير print_x هنا قمنا بتعريف دالة ثابتة إسمها
@staticmethod
def print_x():
print('StaticExample.x =', StaticExample.x)
# حتى نستطيع الوصول للدالة الموجودة فيه و إنشاء كائن منه أيضاً StaticExample هنا قمنا بتضمين الكلاس
from StaticExample import StaticExample
# obj إسمه StaticExample هنا قمنا بإنشاء كائن من الكلاس
obj = StaticExample()
# من 5 إلى 10 obj الخاصة بالكائن x هنا قمنا بتغيير قيمة
obj.x = 10
# الخاصة بالكائن و التي تساوي 10 x الخاصة بالكلاس, أي 5 و ليس قيمة x لاحظ أن سيتم طباعة قيمة .obj من الكائن print_x() هنا قمنا باستدعاء الدالة الثابتة
obj.print_x()
# الخاصة بالكلاس x بشكل مباشر و بالتالي سيتم أيضاً طباعة قيمة StaticExample من الكلاس print_x() هنا قمنا باستدعاء الدالة الثابتة
StaticExample.print_x() # الخاصة بالكلاس لأننا نستدعي الدالة الثابتة مباشرةً من الكلاس
# print_x() هي إستدعاءها منه كالتالي و ليس عن طريق الدالة الثابتة obj الخاصة بالكائن x الطريقة الوحيدة لطباعة قيمة
print('obj.x =', obj.x)
•سنحصل على النتيجة التالية عند تشغيل الملف Test.
StaticExample.x = 5
obj.x = 10