تقييم الموضوع :
  • 0 أصوات - بمعدل 0
  • 1
  • 2
  • 3
  • 4
  • 5
الفئة، المثيل، الوظائف، السمات في بيثون
#1
الفئة، المثيل، الوظائف، السمات
سنمر، بدون مزيد من الانتظار، للحديث عن الفئات، وسنحاول أن نفهم آليات البرمجة الكائنية في بيثون.
## الفئات، عالم مختلف؟
تحدثت في موضوع سابق عن الفئات بشكل سريع وغير متعمق، ورأينا في عدة مواضيع كائنات لها تصرفات سميناها وظائف. في الواقع وخلف عبارة البرمجة الكائنية (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."
في IDLE نحفظ وننفذ باستخدام F5.
كود :
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
# 1- علينا الا ننسى القوسين عندما نقوم بانشاء مثيل من الكلاس
# 2- مجال التسمية، سواء الخاص بالكلاس او الخاص بالمثيل، نصل اليه من خلال الدالة vars المدمجة في بيثون، أو من خلال السمة الخاصة __dict__ حيث نرى بالنسبة للكلاس Sentence أن مجال التسمية يحتوي على عدد من السمات والوظائف في شكل مفاتيح:قيم لم نعرّف منها سوى السمة mysentence،
# 3- في المقابل مجال التسمية الخاص بالمثيل مباشرة بعد انشاء الكائن مازال فارغا. لكن علينا أن نتذكر أن هناك علاقة وراثية بين الكلاس والمثيل؛
# 4- بحث بيثون عن السمة mysentence في مجال التسمية الخاص بالكائن s لكننا راينا أنه فارغ، فبحث في مجال التسمية الاشمل وهو هنا المجال الخاص بالكلاس فوجد السمة وتمكن من رد قيمة
يوجد اذا علاقة بين الفئة باعتبارها النموذج والمثيل باعتباره الكائن الذي صنعناه اعتمادا على النموذج: هي علاقة توريث بين الكلاس والكائن المثيل: سيرث المثيل جميع سمات ووظائف الفئة. مفهوم التوريث مرتبط في الواقع بمجالات التسمية: للفئة مجال تسمية خاص بها، وللمثيل مجال تسمية خاص به. عندما نرغب في البحث عن سمة في الكائن المثيل سنبحث عنها في مجال التسمية الخاص بذلك الكائن، وعندما لا نجد السمة في مجال التسمية المحلي للكائن المثيل، سنبحث في مجال التسمية الاشمل وهو هنا مجال الكلاس.
## الفئة والمثيل وقابلية التعديل
نتذكر أن انواع الكائنات في بيثون تنقسم الى كائنات قابلة للتعديل ( القائمة والقاموس والمجموعة...) وكائنات غير قابلة للتعديل (الاعداد والسلاسل النصية...). الكلاس والمثيل هما أيضا كائنان قابلان للتعديل:
كود :
Sentence.words = Sentence.mysentence.split()    # 5
Sentence.words                                  # 6
s.words                                         # 6
vars(s)                                         # 7
# 5- لاحظ كيف يمكن اضافة سمة للكلاس حتى بعد استيرادها واستخدامها لانشاء كائنات
# 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'
## الوظيفة __init__
هي الدالة التي تقوم ببناء الكائن (constructor). عندما نطلب انشاء كائن من نوع Board يقوم بيثون باستدعاء هذه الدالة لتهيئة الكائن بصدد الانشاء. بالسطر التالي نطلب انشاء ذلك الكائن:
كود :
my_board = Board()
your_board = Board()
my_board.height
my_board.color
my_board.chalk
طبعا هنا my_board و your_board متشابهان تماما عند الانشاء لأننا حددنا السمات بشكل صارم داخل الفئة. سنغير لون الخط:
كود :
my_board.chalk = 'red'
هذه التعليمة ستكون مفاجئة لمن جرب البرمجة الكائنية بلغات أخرى كالسي++ او الجافا، فهو بالنسبة لهم يخرق مبدأ أساسيا يتمثل في التغليف (encapsulation)، حيث تعتمد تلك اللغات وظائف خاصة لعرض أو تعديل سمة كائن (getter and setter). حسنا هذا أيضا موجود في بيثون وسنتحدث عنه قريبا ان شاء الله (عبر properties)، لكن علينا من الآن أن نفهم أن الوظائف getter و setter ليست من صميم لغة بيثون، وأن التخلص من المفاهيم المسبقة فيما يتعلق بالبرمجة الكائنية ضروري. ولذلك نبهت منذ بداية الموضوع أن فلسفة البرمجة الكائنية في بيثون مختلفة.
بقي علينا أن نكتب وظيفة تتولى بناء صبورات بشكل أذكى قليلا مما فعلنا سابقا. سنعدل الوظيفة __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
وهكذا نكون قد حددنا سمات افتراضية عند عدم تخصيصها بشكل صريح. علينا ألا ننسى أن الوسيط الاول يجب أن يشير الى الكائن بصدد الانشاء (self)، ثم يمكننا انشاء كائنات كالتالي:
كود :
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
# 1- صبورة عدلنا ارتفاعها وعرضها وتركنا بقية السمات كما هي افتراضيا
# 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
# 3- سمة الكلاس (class attribute) : نعرف سمة فئة قبل تعريف الوظيفة __init__ هنا هو عداد يحسب لنا عدد الكائنات (الصبورات) التي انشأناها.
# 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
لاحظ كيف استخدمنا الوظيفة write على الكلاس نفسها وهنا حددنا بشكل صريح على اي كائن يجب تنفيذ الوظيفة، لكن النتيجة هي نفسها كما لو قلنا:
كود :
blackboard.write("blablabla")
blackboard.surface
وقبل أن نمر الى الحديث عن جانب آخر، نذكر أن self هي كلمة توافقية تشير الى الكائن نفسه الذي نعمل عليه، ورغم أنه من الممكن استخدام كلمة اخرى او حتى الحرف s فقط مثلا، الا ان احترام ما توافق عليه من سبقنا يجعل ما نكتبه اكثر مقروئية ووضوحا لنا ولغيرنا.
## وظائف الكلاس (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)
# 6- استخدمنا decorator لنبين أننا بصدد وظيفة كلاس (يوجد طريقة أخرى تعتمد دالة classmethod لكنها قديمة ومهملة)
# 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
# 8- نستخدم decorator لنبين اننا بصدد كتابة وظيفة ساكنة
# 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__
يوجد عدة مزايا للـdocstring:
- الدالة help تعرضها في الطرفية
- أدوات البرمجة بلغة بيثون سواء مفسر بيثون او البرامج (integrated development environment <=> IDE) تعرضها للمبرمج الذي لا يرغب في قراءة الكود البرمجي ويريد فقط أن يعرف ماذا تفعل الكلاس/الوظيفة/الدالة
- يمكن تكوين دليل جديد عبر أوامر تستخرج تلك السطور من المساعدة
- اذا كان الكود معقدا يمكن كتابة مثال للاستخدام في ذلك الدليل السريع
- يمثل آلية قياسية في كتابة دليل الاستخدام: الجميع يعلم أين يجدها وكيف يجدها
انظر في PEP257 حول توافقات على كيفية كتابتها: https://www.python.org/dev/peps/pep-0257
## خاتمة
تحدثنا في هذا الموضوع عن عدة عناصر نذكر منها بالاساس:
- تعريف الكلاس والمثيل والوظيفة والسمة
- مجالات التسمية والوراثة
- بناء كلاس وانشاء امثلة منها
- سمة المثيل وسمة الكلاس
- وظائف المثيل ووظائف الكلاس
- الوظائف الساكنة
الرد


التنقل السريع :


مستخدمين يتصفحوا هذا الموضوع: 1 ضيف