الفئة، المثيل، الوظائف، السمات
سنمر، بدون مزيد من الانتظار، للحديث عن الفئات، وسنحاول أن نفهم آليات البرمجة الكائنية في بيثون.## الفئات، عالم مختلف؟
تحدثت في موضوع سابق عن الفئات بشكل سريع وغير متعمق، ورأينا في عدة مواضيع كائنات لها تصرفات سميناها وظائف. في الواقع وخلف عبارة البرمجة الكائنية (OOP) توجد فلسفة تختلف باختلاف لغة البرمجة.
أذا كان لديك خلفية برمجية بلغات عالية المستوى مثل سي++ او جافا، فأنت ربما تعرف مفاهيم الفئة (class) والمثيل (instance) والوظيفة (method) والوظائف الخاصة (special methods) والسمة (attribute)، او مفاهيم أخرى مرتبطة بالبرمجة الكائنية كالـ constructror و inheritance و overload و abstraction و encapsulation و polymorphism و public و private و protected ...
اذا كنت تعرف البعض من ذلك في لغات برمجة أخرى فهذا قد يساعدك وقد يمثل لك مشكلة بحسب قدرتك على التخلص من المفاهيم المسبقة التي امتلكتها عند تدربك على لغة برمجة اخرى، ذلك أن لكل لغة برمجة فلسفتها في تطبيق مفهوم البرمجة الكائنية، وبيثون لا يشذ عن هذه القاعدة. ما يساعد على تعلم البرمجة الكائنية في بيثون بسرعة هو أن كل شيء في بيثون هو كائن، حتى العدد الصحيح int فهو في بيثون كائن والكلاس نفسها هي كائن.
الفئة او الكلاس هي وسيلة نمكننا من تعريف انواع مخصصة وخاصة بنا، وهي عبارة عن النموذج الذي نصنع منه أمثلة من الكائنات، هي نموذج بواسطته نعرف بيانات أكثر تعقيدا من مجرد اعداد او سلاسل نصية. يوجد بالطبع في بيثون عدة فئات مدمجة في اللغة بواسطتها استطعنا تعريف الاعداد والسلاسل النصية والقوائم والصفوف والقواميس والمجموعات... كما يوجد عدد كبير من الفئات نستطيع استيرادها واستخدامها كما يحلو لنا. رغم ذلك، سنضيق على أنفسنا كثيرا اذا لم نتعلم انشاء فئاتنا الخاصة.
## علاقة الوراثة بين الفئة والمثيل
سنبدأ بمثال بسيط جدا توقفنا عنده في موضوعنا السابق: مدخل الى الفئات:
كود :
class Sentence:
mysentence = "Hello I'm learning Python."
كود :
Sentence # <class '__main__.Sentence'>
s = Sentence() # 1
s # <__main__.Sentence object at 0x7f2366e5a588>
Sentence.__dict__ # 2
vars(Sentence) # 2
vars(s) # 3
s.mysentence # 4
# 2- مجال التسمية، سواء الخاص بالكلاس او الخاص بالمثيل، نصل اليه من خلال الدالة vars المدمجة في بيثون، أو من خلال السمة الخاصة __dict__ حيث نرى بالنسبة للكلاس Sentence أن مجال التسمية يحتوي على عدد من السمات والوظائف في شكل مفاتيح:قيم لم نعرّف منها سوى السمة mysentence،
# 3- في المقابل مجال التسمية الخاص بالمثيل مباشرة بعد انشاء الكائن مازال فارغا. لكن علينا أن نتذكر أن هناك علاقة وراثية بين الكلاس والمثيل؛
# 4- بحث بيثون عن السمة mysentence في مجال التسمية الخاص بالكائن s لكننا راينا أنه فارغ، فبحث في مجال التسمية الاشمل وهو هنا المجال الخاص بالكلاس فوجد السمة وتمكن من رد قيمة
يوجد اذا علاقة بين الفئة باعتبارها النموذج والمثيل باعتباره الكائن الذي صنعناه اعتمادا على النموذج: هي علاقة توريث بين الكلاس والكائن المثيل: سيرث المثيل جميع سمات ووظائف الفئة. مفهوم التوريث مرتبط في الواقع بمجالات التسمية: للفئة مجال تسمية خاص بها، وللمثيل مجال تسمية خاص به. عندما نرغب في البحث عن سمة في الكائن المثيل سنبحث عنها في مجال التسمية الخاص بذلك الكائن، وعندما لا نجد السمة في مجال التسمية المحلي للكائن المثيل، سنبحث في مجال التسمية الاشمل وهو هنا مجال الكلاس.
## الفئة والمثيل وقابلية التعديل
نتذكر أن انواع الكائنات في بيثون تنقسم الى كائنات قابلة للتعديل ( القائمة والقاموس والمجموعة...) وكائنات غير قابلة للتعديل (الاعداد والسلاسل النصية...). الكلاس والمثيل هما أيضا كائنان قابلان للتعديل:
كود :
Sentence.words = Sentence.mysentence.split() # 5
Sentence.words # 6
s.words # 6
vars(s) # 7
# 6- يمكن استخدام السمة في المثيل حتى لو كان انشاء المثيل سابقا لاضافة السمة. يبطل العجب اذا تذكرنا مرة أخرى ان الكلاس والمثيل كائنان قابلان للتعديل وأن المثيل يرث سمات ووظائف من الكلاس
# 7- يظهر لنا استعراض مجال تسمية المثيل انه لازال فارغا فالكائن s قد ورث السمة words من الكلاس ديناميكيا.
## توافقات
يجدر بنا ونحن نتعلم لغة بيثون أن نحترم ما توافق عليه من سبقونا حتى ولو لم نعرف الغاية من ذلك. قد يتبين لنا بعد ذلك أهمية احترام تلك التوافقات ولن نندم.
من بين ما توافق عليه المطورون قبلنا هو كيفية تسمية الفئات، واختاروا الصيغة المسماة CamelCase (تسمى أيضا CapWords) وهي كما يبين اسمها تتمثل في كتابة الحرف الاول من اسم الفئة وكل كلمة مكونة له بالحرف الكبير: MyClass و Persons... انظر الفقرة المتعلقة بالفئات في PEP8 من الرابط التالي: https://www.python.org/dev/peps/pep-0008/#class-names
الكلمة self كذلك هي مما توافق عليه المطورون قبلنا ليشيروا الى الكائن نفسه؛
## وظيفة البناء constructor
كمثال عملي ثان لنا سنبرمج نموذجا 'لصنع' صبورات! هو مجرد مثال مبسط، فالصبورة كائن له طول وعرض ولون وطباشير...
كود :
class Board:
"""Class defining a surface on which we can write, read and
erase, by set of methods. The possible attributes are:
- height
- width
- color
- chalk
"""
def __init__(self):
""" Constructor of our class.
Each attribute will be instantiated with a default value ...
"""
self.height = 10
self.width = 20
self.color = 'black'
self.chalk = 'white'
هي الدالة التي تقوم ببناء الكائن (constructor). عندما نطلب انشاء كائن من نوع Board يقوم بيثون باستدعاء هذه الدالة لتهيئة الكائن بصدد الانشاء. بالسطر التالي نطلب انشاء ذلك الكائن:
كود :
my_board = Board()
your_board = Board()
my_board.height
my_board.color
my_board.chalk
كود :
my_board.chalk = 'red'
بقي علينا أن نكتب وظيفة تتولى بناء صبورات بشكل أذكى قليلا مما فعلنا سابقا. سنعدل الوظيفة __init__ كما يلي:
كود :
def __init__(self, height=10, width=20, color='black', chalk='white'):
""" Constructor of our class.
Each attribute will be instantiated with a default value ...
"""
self.height = height
self.width = width
self.color = color
self.chalk = chalk
كود :
blackboard = Board(30, 50) # 1
whiteboard = Board(color='white', chalk='blue') # 2
blackboard.height
blackboard.width
blackboard.color
blackboard.chalk
whiteboard.height
whiteboard.width
whiteboard.color
whiteboard.chalk
# 2- صبورة عدلنا لونها ولون طباشيرها
يمكنك أن تجرب أكثر ليتوضح لك أكثر مفهوم انشاء مثيل من فئة
## سمات الفئة/الكلاس class attribute
السمات في مثالنا السابق، مضمنة في الكائن الذي أنشأناه. يمكن أن ننشئ عدة صبورات لها سمات مختلفة من كائن الى آخر. لكن يمكننا ايضا ان نعرف سمة خاصة بالفئة، على سبيل المثال لنعرف عدد الكائنات التي أنشأناها من نفس الفئة:
كود :
class Board:
"""Class defining a surface on which we can write, read and
erase, by set of methods. The possible attributes are:
- height
- width
- color
- chalk
"""
counter = 0 # 3
def __init__(self, height=10, width=20, color='black', chalk='white'):
""" Constructor of our class.
Each attribute will be instantiated with a default value ...
"""
self.height = height
self.width = width
self.color = color
self.chalk = chalk
Board.counter += 1 # 4
كود :
bb = Board()
wb = Board(color='white', chalk='blue')
gb = Board(color='green', chalk='yellow')
rb = Board(chalk='red')
Board.counter # 5
# 4- عندما نرغب في تعديل قيمة سمة كلاس نستخدم اسم الفئة. هنا طلبنا اضافة 1 للعداد عند كل عملية تهيئة لصبورة جديدة وكتبنا التعليمة في كتلة الوظيفة __init__
# 5- نستدعي سمة الكلاس عبر اسم الكلاس (يمكن ايضا عبر اسم أحد الكائنات)
لعلك لاحظت أن السمات في أمثلتنا السابقة غير محمية من التعديل المباشر ، حيث يمكنك تغيير لون الصبورة أو مقاييسها مثلا وحتى عدد الكائنات بمجرد كتابة سطر مثل:
كود :
bb.color = 'orange'
bb.counter = 10
Board.counter = 40
سننقاش في موضوع لاحق ان شاء الله كيف نحمي السمات من التعديل غير المرغوب فيه.
## الوظائف
الى حد الآن لا تملك صبوراتنا اية مهام/وظائف تميزها. يمكن اعتبار الوظائف أعمالا او تصرفات تستطيع الكائنات تنفيذها. على سبيل المثال تقوم الوظيفة append باضافة عنصر الى القائمة. بالنسبة لصبورتنا يمكننا مثلا الكتابة عليها أو مسحها. سنضيف سمة أخرى surface حيث يمكننا أن نكتب ونمسح. عند انشاء صبورة جديدة تكون مساحة الكتابة فارغة، ثم نكتب فيها عبر وظيفة write ونمسحها عبر وظيفة erase:
كود :
class Board:
"""Class defining a surface on which we can write, read and
erase, by set of methods. The possible attributes are:
- height
- width
- color
- chalk
"""
counter = 0
def __init__(self, height=10, width=20, color='black', chalk='white'):
""" Constructor of our class.
Each attribute will be instantiated with a default value ...
"""
self.height = height
self.width = width
self.color = color
self.chalk = chalk
self.surface = "" # By default, our surface is empty
Board.counter += 1
def write(self, text):
"""Method for writing on the surface of the board.
If the surface is not empty, we skip a line before
adding the new message.
"""
if self.surface != "":
self.surface += "\n"
self.surface += text
def erase(self):
"""Method for erasing the surface of the board."""
self.surface = ""
كود :
blackboard = Board()
blackboard.surface
blackboard.write("I'm learning to program in Python.")
blackboard.surface
blackboard.write("OOP in Python is not really hard.")
blackboard.surface
print(blackboard.surface)
blackboard.erase()
blackboard.surface
vars(Board) # مجال تسمية الكلاس
vars(blackboard) # مجال تسمية المثيل
نجد في تعريف وظائف الكائن من جديد الوسيط self. نذكر بأن هذا الوسيط يشير الى الكائن المثيل نفسه الذي نعمل عليه. في وظائف المثيل والتي تسمى ايضا وظائف الكائن ، اول وسيط يتم تمريره هو مؤشر الى الكائن نفسه. عندما نقوم بانشاء كائن جديد (وهو في امثلتنا صبورة جديدة) تكون سمات الكائن خاصة به وحده، وهذا منطقي: راينا كيف يمكننا انشاء عدة صبورات بسمات مختلفة، وبالتالي فالسمات مضمنة في الكائن (انظر نتيجة vars).
في المقابل، تكون الوظائف مضمنة في الفئة (الكلاس) التي تعرف الكائن. هذا مهم جدا. عندما نكتب blackboard.write(".....") سيبحث بيثون عن الوظيفة write في الكلاس Board وليس في الكائن وقارن بين المجموعتين التاليتين.
كود :
Board.write(blackboard, "blablabla")
blackboard.surface
كود :
blackboard.write("blablabla")
blackboard.surface
## وظائف الكلاس (class methods)
مثلما راينا امكانية تعريف سمات كلاس، نستطيع أيضا تعريف وظائف كلاس. لا تختلف وظائف الكلاس عن وظائف الكائن الا بالكلمة self حيث نكتب مكانها cls، ثم نستعين بـdecorator لنبين أننا بصدد وظيفة كلاس
كود :
class Board:
#i لا تعديلات في الجزء السابق من السكربت
#i وظيفة كلاس: لاحظ للوسيط الاول
@classmethod # 6
def nb_instances(cls): # 7
"""method that prints the number of instances already created"""
print("The number of instances already created =", cls.counter)
# 7- لاحظ اننا استخدمنا cls عوض self: العبارتان توافقيتان لا غير
كود :
Board.nb_instances()
bb = Board()
wb = Board()
gb = Board()
Board.nb_instances()
bb.nb_instances()
## الوظائف الساكنة (static methods)
لا يختلف هذا النوع من الوظائف عن سابقه سوى بعدم استخدام وسيط للكلاس ولا وسيط للكائن (لا cls ولا self) فالوظيفة الساكنة أشبه ما يكون بدالة عادية لكنها معرفة داخل كلاس، فهي تعمل بشكل مستقل عن أي كائن. حيث لا استحضر شيئا يمكن استخدامه بشكل مستقل سنكتب وظيفة ساكنة ترد قيمة بوليانية (True) اذا كان العدد أكبر من صفر. قد يكون المعنى من تعريفها داخل الكلاس كوظيفة ساكنة أي لا علاقة لها لا بالكلاس ولا بالكائن هو رغبتنا مثلا في التثبت من ارتفاع وعرض الصبورة، حيث من غير المنطقي أن يكون احدهما او كلاهما اصغر من صفر (لكنه مثال للشرح فقط).
كود :
class Board:
#i لا تعديلات في الجزء السابق من السكربت
@staticmethod # 8
def is_positive(x): # 9
"""Static method that returns a Boolean depending on whether
the argument passed to it is positive or negative ."""
return x >= 0
# 9- الوظيفة مستقلة عن الكلاس وعن الكائن لذلك لا نستخدم cls ولا self
كود :
Board.is_postive(-10)
bb = Board()
Board.is_postive(bb.height)
bb.is_positive(bb.height)
## المساعدة السريعة docstring
لاحظ في الختام أنني كتبت تعليقا docstring للكلاس ولكل وظيفة من وظائف الكلاس. بصفة عامة ينصح بشدة بكتابة مثل هذه المساعدة السريعة على أن تكون مختصرة وواضحة والتي يمكن قراءتها عبر:
كود :
help(Board)
help(bb)
help(bb.write)
bb.__doc__
bb.erase.__doc__
- الدالة help تعرضها في الطرفية
- أدوات البرمجة بلغة بيثون سواء مفسر بيثون او البرامج (integrated development environment <=> IDE) تعرضها للمبرمج الذي لا يرغب في قراءة الكود البرمجي ويريد فقط أن يعرف ماذا تفعل الكلاس/الوظيفة/الدالة
- يمكن تكوين دليل جديد عبر أوامر تستخرج تلك السطور من المساعدة
- اذا كان الكود معقدا يمكن كتابة مثال للاستخدام في ذلك الدليل السريع
- يمثل آلية قياسية في كتابة دليل الاستخدام: الجميع يعلم أين يجدها وكيف يجدها
انظر في PEP257 حول توافقات على كيفية كتابتها: https://www.python.org/dev/peps/pep-0257
## خاتمة
تحدثنا في هذا الموضوع عن عدة عناصر نذكر منها بالاساس:
- تعريف الكلاس والمثيل والوظيفة والسمة
- مجالات التسمية والوراثة
- بناء كلاس وانشاء امثلة منها
- سمة المثيل وسمة الكلاس
- وظائف المثيل ووظائف الكلاس
- الوظائف الساكنة