بعد أن تعرّفنا في الدّرس السابق على أساسيات البرمجة كائنية التوجّه، سنُكمل في هذا الجزء ما بدأناه وسنتعلّم بعض المبادئ المتقدّمة حول البرمجة كائنيّة التّوجه.
تغيير قيمة متغير الصنف
في الدّرس السّابق تعلّمنا كيفيّة تعريف مُتغيّر داخل صنف وكيفيّة الوصول إليه، في هذا الجزء من هذا الدّرس سنتعرّف على كيفيّة استخدام هذه المُتغيّرات بشكل أكثر واقعيّة، لنقل مثلا بأنّنا نمتلك صنفًا باسم Car (سيارة)، ونريد أن ننشئ كائنات من هذا الصّنف، بحيثُ يكون لكل كائن قيم مُختلفة لمُتغيّرات الصّنف، انظر ما يلي:
class Car:
name = None
speed = 0
brand = None
def specs(self):
print '{} speed is {}Km/h and it\'s brand is {}'.format(self.name, self.speed, self.brand)
أنشأنا أعلاه صنفا بسيطا باسم Car مع ثلاثة مُتغيّرات name لاسم السّيارة، speed لسرعة السّيارة و brand لاسم العلامة التّجاريّة، كلمة None تُشير إلى أنّ المُتغيّر مُعرّف لكنّه لا يحمل أية قيمة. بعدها عرّفنا تابعا specs (اختصار لـ specifications) لطباعة مواصفات السّيارة.
لننشئ كائنات مُتعدّدة من الصّنف الذّي كتبناه للتو.
# Toyota Corolla
corolla = Car()
corolla.name = 'Toyota Corolla'
corolla.speed = 200
corolla.brand = 'Toyota'
# Ford Focus
focus = Car()
focus.name = 'Ford Focus'
focus.speed = 220
focus.brand = 'Ford'
# Honda Civic
civic = Car()
civic.name = 'Honda Civic'
civic.speed = 210
civic.brand = 'Honda'
بعد أن أنشأنا الصّنف والكائنات المندرجة تحت هذا الصّنف، وقمنا بتعيين قيم المُتغيّرات التي تُعتبر مواصفات كلّ سيارة، يُمكننا أخيرا طباعة مواصفات كل سيارة (كائن) وذلك باستدعاء التابع specs.
corolla.specs()
focus.specs()
civic.specs()
وكما تتوقّع، المُخرج سيكون كالتّالي:
Toyota Corolla speed is 200Km/h and it's brand is Toyota
Ford Focus speed is 220Km/h and it's brand is Ford
Honda Civic speed is 210Km/h and it's brand is Honda
إرجاع الصنف داخل تابع
يُمكنك أن تقوم بكتابة تابع داخل صنف، بحيث يقوم التابع بتنفيذ شيفرة ما ثمّ إعادة الصّنف نفسه (ما نُشير إليه بالكلمة self) وبهذا ستتمكّن من استدعاء التابع أكثر من مرّة في سطر واحد، تأمل ما يلي:
class Car:
def start(self):
print 'Starting engine…'
return self
ما فعلناه في الشّيفرة أعلاه ليس بالشيء الكثير، أولا نقوم بتعريف الصّنف ثمّ بعد ذلك نُعرّف التّابع start المسؤول عن تشغيل السّيارة، وبعدها نطبع جملة تُفيد بأنّ مُحرّك السّيارة قيد التّشغيل، بعد تنفيذ الشيفرة سيُرجع التّابع الصّنف نفسه. ما سيُمكّننا من استدعاء التّابع أكثر من مرّة في سطر واحد كما يلي:
corolla = Car()
corolla.start().start().start()
المُخرج سيكون كالتّالي:
Starting engine…
Starting engine…
Starting engine…
كما تُلاحظ لقد نُفّذت الشّيفرة الموجودة داخل التّابع start ثلاث مرّات.
قد تتساءل عن أهميّة الأمر، وأتّفق معك في أنّ الأمر يُمكن أن لا يكون مُفيدا. لكنّه يكون مُفيدا إذا أردت أن تقوم بتنفيذ تابعين بشكل تسلسلي، لنقل بأنّ لدينا تابعا آخر باسم move ولنفرض بأنّه سيكون مسؤولا عن تحريك السّيارة، سيكون من الأفضل لو استطعنا أن نستدعي تابع الحركة مُباشرة بعد استدعاء تابع التّشغيل. بحيث يبدو الاستعمال كالتّالي:
corolla.start().move()
يُمكننا أن نقوم بالأمر ببساطة، وفي الحقيقة لن نحتاج إلا لإضافة التابع move لما كتبناه سابقا.
class Car:
def start(self):
print 'Starting engine…'
return self
def move(self):
print 'The car is moving…'
بهذه الطّريقة سنتمكن من تنفيذ الشيفرة دون مشاكل:
Car().start().move()
المُخرج:
Starting engine…
Starting engine…
The car is moving…
لكن لاحظ هذه المرّة بأنّنا لم نُرجع كلمة self في التّابع move ما يعني بأنّنا لن نتمكن من القيام باستدعاء التابع أكثر من مرّة:
Car().start().move().move()
إذا حاولت أن تُنفّذ الأمر السّابق ستحصل على خطأ كالتّالي:
AttributeError: 'NoneType' object has no attribute 'move'
وهذا راجع لكوننا لم نُرجع الصنف في آخر التّابع move.
الوراثة
مفهوم الوراثة في البرمجة كائنيّة التوجه لا يختلف كثيرا عن مفهوم الوراثة في العالم الواقعي، الصّنف الابن يرث جميع المتغيرات والتوابع من الصّنف الأب، يُمكن التّفكير في الأمر على أنّه نسخ. أي أنّ الصّنف الابن مُجرّد نُسخة طبق الأصل من الصّنف الأصلي، الفرق هنا هو أنّك تستطيع أن تُضيف المزيد من الخصائص والتوابع للصّنف الابن. وطريقة إنشاء صنف يرث من صنف آخر هي بوضع الصّنف الأب بين قوسين عند إنشاء الصّنف الابن. انظُر المثال التّالي:
class Parent:
a = 1
b = 2
class Child(Parent):
c = 3
في المثال أعلاه، قُمنا بإنشاء صنف باسم Parent مع مُتغيّرين a و b، وبعدها أنشأنا صنفا Child الذي يرث خصائص الصّنف السابق، وبالتّالي فسنتمكّن من الوصول إلى مُتغيّرات الصّنف Parent من خلال الصّنف Child، ما يعني بأنّ الشيفرة التّالية ستعمل دون مشاكل:
child = Child()
print child.a
print child.b
print child.c
المُخرج:
1
2
3
كما تُلاحظ فإنّنا قد استطعنا الوصول إلى المتغيّرين a و b رغم أنّهما لم يُعرّفا مُباشرة داخل الصّنف Child.
يُمكن أن نجعل صنفا يرث توابع من صنف آخر بنفس الطّريقة:
class Person:
name = 'Person'
def say_hello(self):
name = self.name
print 'Hello my name is {}'.format(name)
class Abdelhadi(Person):
name = 'Abdelhadi'
استدعاء التّابع say_hello من كائن من الصّنف Abdelhadi.
me = Abdelhadi()
me.say_hello()
المُخرج:
Hello my name is Abdelhadi
في الشيفرة أعلاه، قُمنا أولا بتعريف صنف "شخص" باسم Person ثمّ عرّفنا المُتغيّر name داخل الصّنف، وأعطيناه قيمة افتراضيّة، بعدها عرّفنا التّابع say_hello ليطبع جُملة التّرحيب.
متغيرات الصنف المبنية مسبقا
توجد مُتغيّرات مبنية مُسبقا في لغة بايثون، وتُسمى هذه المُتغيّرات بمُتغيّرات الصّنف، وما نعنيه بأنّها مبنية مُسبقا هو أنّك لا تحتاج إلى تعريفها، وتمتاز هذه المُتغيّرات بأنّها مُحاطة بتسطيرين سُفليّين Underscores وإليك بعضا من هذه المُتغيّرات:
__doc__
سلسلة توثيق الصنف (Documentation string) وهو ما يُكتب مُباشرة بعد تعريف الصنف، ويحتوي في الغالب معلومات حول وظيفة الصّنف. القيمة الافتراضية لهذا المتغير هي None ألق نظرة على الصّنف التالي:
class Person:
''' This class does nothing '''
pass
لاحظ بأنّ سلسلة التوثيق مُحاطة بثلاث علامات تنصيص
إذا أردنا الوصول إلى هذا التوثيق فكلّ ما علينا فعله هو استخدام الصّفة __doc__:
print Person.__doc__
المُخرج:
This class does nothing
__module__
الوحدة التي عُرّفَ فيها الصّنف، وقيمتها الافتراضية هي'__main__'.
print Person.__module__
المخرج:
__main__
__dict__
هذا المُتغيّر عبارة عن قاموس يحمل ما سبق كقيم بدئية، كما يحمل أسماء المتغيّرات التي تُنشؤها أنت وقيمها وكذلك أسماء التوابع، لاحظ المثال التالي (نفّذه مُباشرة على مُفسّر بايثون لتُلاحظ النّتيجة):
>>> class Math: ... x = 1 ... y = 2 ... def x_plus_y(self): ... return self.x + self.y ... >>> Math.__dict__ {'y': 2, 'x': 1, '__module__': '__main__', '__doc__': None, 'x_plus_y': <function x_plus_y at 0x7f186e76a6e0>}
كما تُلاحظ مُخرجات القاموس تحتوي على كل من قيم المُتغيرات الخاصّة بالصّنف سواء التي عرّفناها أو المبنية مُسبقا، لاحظ كذلك العنصر الأخير من القاموس:
'x_plus_y': <function x_plus_y at 0x7f186e76a6e0>}
هذا العنصر يحمل مفتاحا باسم التابع x_plus_y الذي عرّفناه، وقيمة هذه المفتاح تُشير إلى أنّه دالة.
__name__
اسم الصّنف، يُمكنك تطبيقه على أي صنف للحصول على اسمه، انظر المثال التالي:
>>> a = Math
>>> a.__name__
'Math'
كما تُلاحظ فقد حصلنا على اسم الصنف Math.
الوصول إلى الصنف الخاص بكائن ما
المُتغيّرات التي ذكرناها سابقا خاصّة بالصّنف فقط ولا يُمكن الوصول إليها من كائن من هذا الصّنف. تأمّل الصّنف التالي:
class Person:
name = 'Abdelhadi'
لننشئ الآن كائنا من هذا الصّنف:
me = Person()
يُمكننا الآن الوصول إلى قيمة المُتغيّر name كالتالي:
>>> me.name
'Abdelhadi'
لكننّا لن نستطيع الوصول إلى مُتغيّرات الصّنف من الكائن، بل من الصّنف فقط:
person_class = Person
person_class.__name__ # الشيفرة صحيحة
person_object = Person()
person_object.__name__ # الشيفرة خاطئة لأنّنا نحاول الوصول إلى مُتغيّر غير مُعرّف داخل الكائن
ستحصل على خطأ من نوع AttributeError عند تنفيذ السّطر الأخير:
AttributeError: Person instance has no attribute '__name__'
الحل الأمثل هو أن نصل إلى الصّنف انطلاقا من الكائن، وبعدها سنتمكّن من الوصول إلى مُتغيّرات/صفات الصّنف دون مشاكل، وطريقة الوصول إلى صنف كائن هي بإلحاقه بكلمة __class__، انظر المثال التّالي (سنعتمد على الصنف الذي أنشأناه أعلاه):
person_object = Person()
person_object_class = person_object.__class__ # إسناد صنف الكائن لمتغيّر
print person_object_class.__name__ # اسم الصّنف
print person_object_class.__dict__ # قاموس يحتوي على بيانات الصّنف
بدأنا بإنشاء كائن person_object ثمّ استخدام جُملة __class__ للوصول إلى الصّنف.
المُخرج:
Person
{'__module__': '__main__', 'name': 'Abdelhadi', '__doc__': None}
يُمكنك كذلك القيام بالأمر داخل تابع في الصّنف:
class Person:
def say_hello(self):
print 'Hi I am a {}'.format(self.__class__.__name__)
person_obj = Person()
person_obj.say_hello()
لاحظ استخدام self.__class__.__name__ للوصول إلى اسم الصّنف في التّابع say_hello.
المُخرج:
Hi I am a Person
التوابع الخاصة
يُمكن استخدام بعض التّوابع الخاصّة لتنفيذ شيفرة عند القيام بإجراء معيّن، وإليك قائمة بأهم هذه التّوابع (لاحظ بأنّها مُحاطة بتسطيرين سُفليّين Underscores).
- التابع init: تُنفَّذُ الشيفرة التي بداخله عند إنشاء كائن من الصّنف، ويُسمّى أيضا بتابع البناء Contractor .
- التابع del: تُنفّذ شيفرته عند حذف كائن أو عند استدعاء دالة الخروج exit.
- التابع repr: تُنفّذ الشيفرة عند استدعاء الكائن، ويُستخدم هذا التابع لإرجاع معلومات حول الكائن في الوضع التّفاعلي (من مُفسّر لغة بايثون مُباشرة)
مثال:
# استدعاء __init__
person_obj = Person()
# استدعاء __repr__
person_obj
repr(person_obj)
# حذف الكائن واستدعاء __del__
del(person_obj)
التابع init
هذا مثال بسيط على إنشاء واستدعاء هذا التّابع:
class Person:
def __init__(self):
print 'A new object was created'
me = Person()
مُخرج البرنامج سيكون جُملة تُفيد بأنّ كائنا قد أنشئ، لاحظ أنّ الجملة A new object was created قد طُبعت رغم عدم استدعاء التّابع init بشكل صريح، ويُمكن الحصول على نفس المُخرج باستدعاء التّابع كالتّالي:
me.__init__()
حسنا تعلّمنا الآن بأنّنا نستطيع تنفيذ الشيفرة الموجودة بداخل التّابع __init__ بمُجرّد إنشاء كائن من الصّنف، ولكن ماذا عن المُعاملات؟
يُمكن تمرير المُعاملات للتّابع __init__ عند إنشاء صنف كالتّالي:
me = Person('Abdelhadi')
وفي التّابع سيكون الأمر كالتّالي:
def __init__(self, name):
name = self.name
print 'Hello My name is {}'.format(name)
الشيفرة الكاملة:
class Person:
def __init__(self, name):
self.name = name
print 'Hello My name is {}'.format(name)
me = Person('Abdelhadi')
المُخرج:
Hello My name is Abdelhadi
التابع repr
يُستعمل هذا التّابع لإرجاع معلومات قابلة للطّباعة، ويُستخدم كثيرا في الوضع التّفاعلي (مُفسّر بايثون)، ويجب أن تكون القيمة المُرجعَةُ عبارة عن سلسلة نصيّة. وإليك مثالا لكيفيّة استخدامه:
class Math:
x = 1
y = 2
def __repr__(self):
x = self.x
y = self.y
return 'x: {}, y: {}'.format(x, y)
x_y = Math()
print x_y
المُخرج:
x: 1, y: 2
يُمكننا الاستفادة من هذا التّابع للوصول إلى اسم الصنف عند الوراثة:
# تعريف الصّنف الرّئيسي
class Person:
def __repr__(self):
return 'Hi I am a {}'.format(self.__class__.__name__)
# الوراثة
class Writer(Person):
pass
class Student(Person):
pass
# إنشاء الكائنات
omar = Student()
abdelhadi = Writer()
# طباعة قيّم التّابع repr
print omar
print abdelhadi
المُخرج:
Hi I am a Student
Hi I am a Writer
لاحظ بأنّ الصّنفين Writer و Student لا يحتويان على أية شيفرة، ومع ذلك فقد نُفّذ التّابع repr واستجاب بطريقة مُختلفة مع كلّ كائن.
التابع del
تُنَفّذُ الشّيفرة الموجودة بداخل هذا التّابع عند حذف كائن باستعمال الدّالة del وهي دالة تُستعمل لإنجاز ما يُسمى بعمليّة جمع القُمامة Garbage Collecting والهدف الرئيسي من هذه العمليّة هو تحرير الذاكرة، ولحذف كائن يكفي أن تقوم باستدعاء الدّالة del مع تمرير الكائن كمُعامل:
class Math:
x = 1
y = 2
numbers = Math()
numbers.x # ==> 1
numbers.y # ==> 2
# حذف الكائن
del(numbers)
# خطأ
numbers.x # ==> NameError: name 'numbers' is not defined
بعد أن حذفنا الكائن numbers لم يعد بإمكاننا الوصول إلى قيمة المُتغيّر x، وأسفر الأمر عن خطأ من نوع NameError.
يُستدعى التابع del مُباشرة بعد حذف الكائن، ما يعني بأنّنا نستطيع أن نطبع جملة تُفيد بأنّ الكائن قد حُذف:
class Math:
x = 1
y = 2
def __del__(self):
print 'Object deleted!'
numbers = Math()
numbers.x # ==> 1
del(numbers)
المُخرج:
Object deleted!
تُنفَّذ شيفرة __del__ كذلك عند استدعاء الدالة exit للخروج من البرنامج:
class Math:
x = 1
y = 2
def exit_program(self):
exit()
def __del__(self):
print 'Object deleted!'
numbers = Math()
numbers.exit_program()
مُخرَج البرنامج أعلاه سيكون نفس الشيء رغم عدم حذف الكائن. والسبب راجع لاستخدام الدّالة exit داخل التّابع exit_program.
تمارين
تمرين 1
عد إلى الدّروس السّابقة وحاول تحويل برنامج تسجيل الدّخول إلى برنامج سهل القراءة والاستخدام باستعمال البرمجة كائنية التوجه، فمثلا يُمكن أن يستعمل من قبل مُبرمجين آخرين بالطّريقة التّالية:
user = User()
user.signup('username', 'password', 'password_confirmation')
user.login('username', 'password')
user.say_hello()
user.logout()
تمرين 2
أنشئ برنامجا يطبع جميع التوابع والمُتغيّرات الموجودة داخل الصّنف الذي أنشأته كحل للتّمرين الأول.
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.