تعريف الأصناف في بايثون

من موسوعة حسوب

أبسط صيغة لتعريف الأصناف في بايثون هي:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

تعريف الأصناف

كما هو الحال مع عبارات تعريف الدوال (عبارات def) يجب تنفيذ عبارات تعريف الأصناف حتى يكون لها الأثر المطلوب. يمكن تعريف الأصناف في بايثون ضمن عبارات if أو حتى داخل الدوال.

عادة ما تستخدم الدوال داخل تعريف الصنف، ولكن من الممكن استخدام أنواع أخرى من العبارات، وتمتلك الدوال داخل الأصناف مجموعة من الوسائط الخاصّة بعملية استدعاء التوابع في بايثون.

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

عند الخروج من عبارة تعريف الصنف، ينشأ كائن صنف (class object) جديد، وتكون عبارة تعريف الصنف بمثابة حاوية لمحتوى مجال الأسماء الذي نشأ من تعريف الصنف الجديد. 

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

كائنات الأصناف instance methods

تدعم كائنات الأصناف نوعين من العمليات: الإشارة إلى الخواص (attribute references) والتمثيل (instantiation).

تستخدم عملية الإشارة إلى الخواص الصيغة القياسية المتّبعة في بايثون: obj.name، ويكون اسم الخاصية صالحًا عندما يكون موجودًا في مجال أسماء الصنف عند لحظة إنشاء كائن الصنف؛ لذا إن جرى تعريف صنف بالصورة التالية:

class MyClass:
    """A simple example class"""
    i = 12345
    def f(self):
        return 'hello world'

فإنّ الإشارات MyClass.i و MyClass.f هي إشارات صحيحة للخاصيات، وتعيدان عددًا صحيحًا وكائن دالة على التوالي، ومن الممكن أيضًا الإسناد إلى خاصيات الصنف، فيمكن مثلًا تغيير قيمة MyClass.i عن طريق عملية الإسناد.

الخاصّية __doc__ هي خاصية صحيحة كذلك، وتعيد سلسلة التوثيق النصية الخاصة بالصنف، وهي في المثال السابق: "A simple example class".

تستخدم عملية تمثيل الصنف الصيغة المتّبعة في استدعاء الدوال، ويمكن اعتبار كائن الصنف دالة خالية من الوسائط وتعيد نسخة جديدة من الصنف. فعلى سبيل المثال:

x = MyClass()

ينشأ من هذا المثال نسخة جديدة من الصنف ويسند هذا الكائن إلى المتغير المحلي x. تنشئ عملية التمثيل ("استدعاء" كائن صنف) كائنًا جديدًا فارغًا.

التابع ‎__init__()‎

يمكن تحديد تابع خاصّ يعمل تلقائيًا عند إنشاء نسخة جديدة من الصنف وكما في المثال التالي:

def __init__(self):
    self.data = []

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

x = MyClass()

يمكن تمرير الوسائط إلى التابع ‎__init__‎()‎، وفي هذه الحالة تمرّر الوسائط المقدّمة إلى عبارة تمثيل الصنف إلى التابع ‎__init__()‎، كما في المثال التالي:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

كائنات النسخ Instance Objects

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

تقابل خاصيات البيانات "متغيرات النسخ instance variables" في Smalltalk و"عناصر البيانات data members" في C++‎.

لا تحتاج خاصيات البيانات إلى التصريح عنها، فكما هو الحال مع المتغيرات المحلية، تظهر هذه الخاصيات إلى الوجود عند إجراء أول عملية إسناد، فعلى سبيل المثال، إن كان المتغير x نسخة من الصنف MyClass، فإن الشيفرة التالية ستطبع القيمة 16 دون أن تترك أي أثر ورائها:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

النوع الآخر من الإشارات إلى الخاصيات هو التابع method. والتابع هو دالة تنتمي إلى الكائن. (ليس المصطلح method حكرًا على نسخ الأصناف في بايثون، بل يمكن للأنواع الأخرى من الكائنات أن تمتلك توابع أيضًا. فعلى سبيل المثال، تمتلك كائنات القوائم توابع مثل append، و insert و remove و sort و غيرها. ولكن سنستخدم المصطلح method للإشارة إلى توابع الأصناف حصرًا في هذه الصفحة إلا إذا أشير إلى غير ذلك).

تعتمد الأسماء الصالحة للتوابع على الصنف الخاص بها. تعرّف جميع الخاصيات في الصنف والتي تكون كائنات دوال التوابع المقابلة؛ وهكذا ففي مثالنا السابق تكون x.f إشارة صالحة للتابع وذلك لأنّ MyClass.f دالة، ولكنّ x.i ليست إشارة صالحة لأنّ MyClass.i ليست دالة. ولكن x.f ليست مشابهة للإشارة MyClass.f، فهي كائن تابع وليست كائن دالة.

كائنات التوابع Method Objects

عادة ما يستدعى التابع بعد ربطه مباشرة:

x.f()

تعيد عملية الاستدعاء هذه في مثال الصنف MyClass السلسلة النصية 'hello world'. ولكن ليس من الضروري استدعاء التابع مباشرة، إذ إنّ x.f كائن تابع، ويمكن تخزينه واستدعاؤه في وقت لاحق:

xf = x.f
while True:
    print(xf())

تستمر الشيفرة السابقة بطباعة عبارة hello world إلى حين نفاد الوقت.

ربّما لاحظت أنّ استدعاء التابع قد تمّ دون ذكر أي وسائط، على الرغم من أنّ تعريف الدالة f()‎ في الصنف كان يتضمّن وسيطًا. تطلق بايثون استثناء عند استدعاء دالة ذات وسائط دون تمرير تلك الوسائط حتى لو لم تستخدم أصلًا. 

في الواقع تمتاز التوابع في أنّ كائن النسخة يمرّر كوسيط أوّل للدالة. وفي مثالنا هذا يكون الاستدعاء x.f()‎ مكافئًا تمامًا للاستدعاء MyClass.f(x)‎

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

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

وعند استدعاء كائن التابع مع قائمة من الوسائط، تنشئ اللغة قائمة وسائط جديدة من كائن النسخة وقائمة الوسائط، ويستدعى كائن الدالة مع قائمة الوسائط الجديدة.

متغيرات الأصناف والنسخ Class and Instance Variables

بصورة عامة تختصّ متغيّرات النسخ بالبيانات الخاصّة بكل نسخة، أما متغيّرات الأصناف فتستخدم للخاصيات والتوابع المشتركة بين جميع نسخ الصنف:

class Dog:
    kind = 'canine'         # متغير صنف مشترك بين جميع النسخ
    def __init__(self, name):
        self.name = name    # متغير نسخة خاصّ بكل نسخة
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # مشترك بين جميع الكلاب
'canine'
>>> e.kind                  # مشترك بين جميع الكلاب
'canine'
>>> d.name                  # d خاص بالصنف
'Fido'
>>> e.name                  # e خاص بالصنف
'Buddy'

يمكن للبيانات المشتركة أن تتسبب في نتائج مفاجئة عند استخدام كائنات قابلة للتغيير مثل القوائم والقواميس. فعلى سبيل المثال يجب أن لا تستخدم القائمة tricks في الشيفرة التالية كمتغيّر صنف لأنّ من الممكن مشاركة قائمة واحدة فقط بين جميع نسخ الصنف Dog:

class Dog:
    tricks = []             # استخدام خاطئ لمتغير الصنف
    def __init__(self, name):
        self.name = name
    def add_trick(self, trick):
        self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # تكون مشتركة بين كل الكلاب خلاف ما هو متوقع
['roll over', 'play dead']

الطريقة الصحيحة هي تعريف القائمة كمتغير النسخة:

class Dog:
    def __init__(self, name):
        self.name = name
        self.tricks = []    # إنشاء قائمة فارغة جديدة لكل نسخة
    def add_trick(self, trick):
        self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

ملاحظات عامة

  • يمكن لخاصيات البيانات أن تعيد تعريف خاصيات التوابع التي تحمل الاسم ذاته، ولتجنّب التضاربات الناجمة من تشابه الأسماء والذي قد يتسبب في حدوث مشاكل يصعب إيجادها في البرامج الكبيرة، ينصح باعتماد بعض الأساليب التي تقلّل من فرص حدوث هذه التضاربات. ومن الأساليب التي يمكن اتباعها استخدام الأحرف الكبيرة في أسماء التوابع، وإضافة سلسلة نصية فريدة وصغيرة قبل أسماء خاصيات البيانات (شرطة سفلية واحدة مثلًا)، أو استخدام الأفعال في تسمية التوابع والأسماء في تسمية خاصيات البيانات.
  • يمكن الإشارة إلى خاصيات البيانات من طرف التوابع ومن طرف مستخدمي الكائن أيضًا. وبعبارة أخرى، لا يمكن استخدام الأصناف لإنشاء أنواع بيانات مجرّدة بالكامل. في الواقع، لا يمكن لأي شيء في بايثون أن يجبر البيانات على الاختفاء. 
  • يجب على مستخدمي الشيفرة البرمجية التعامل مع خاصيات البيانات بحذر، فقد تتسبب إضافة خاصيات بيانات جديدة في حدوث مشاكل في خاصيات البيانات التي تتعامل معها التوابع. لاحظ أنّ بإمكان المستخدمين إضافة خاصيات البيانات إلى كائن النسخة الخاص بهم دون التأثير على الكائنات ما داموا يتجنّبون في ذلك حدوث أي تضارب في الأسماء، ويمكن لأساليب التسمية هنا أن توفّر الكثير من الوقت والجهد.
  • لا توجد طريقة مختصرة للإشارة إلى خاصيات البيانات (أو التوابع الأخرى) من داخل التوابع، ويمكن لهذا الأمر أن يزيد من مقروئية التوابع، إذ لا يمكن في هذه الحالة أن يحدث أي تضارب بين المتغيرات المحلية ومتغيرات النسخة عند قراءة شيفرة التابع.
  • يحمل الوسيط الأول في التابع غالبًا الاسم self، وهذا الاسم متّفق عليه وليس ثابتًا، ولا يحمل أي معنى بالنسبة إلى بايثون. ولكن يجب الانتباه إلى أنّه في حال عدم اتباع الأمور المتفق عليها فإنّ مقروئية شيفرتك ستتدنى بصورة ملحوظة لدى المبرمجين بلغة بايثون، ومن الممكن أن تعتمد بعض برامج استعراض الأصناف أو المحرّرات على هذه الأمور في عملها.
  • تعرّف جميع كائنات الدالة التي تكون خاصية الصنف تابعًا لنسخ ذلك الصنف. وليس من الضروري تعريف الدالة ضمن تعريف الصنف، بل يمكن إسناد كائن الدالة إلى متغيّر محلّي في الصنف، فمثلًا:
# الدالة معرّفة خارج الصنف
def f1(self, x, y):
    return min(x, x+y)
class C:
    f = f1
    def g(self):
        return 'hello world'
    h = g

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

  • يمكن للتوابع أن تستدعي توابع أخرى باستخدام خاصيات التوابع في الوسيط self:
class Bag:
    def __init__(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)
  • يمكن للتوابع أن تشير إلى أسماء عامة بنفس الطريقة التي تتبعها الدوال الاعتيادية. ويكون النطاق العام المرتبط بالتابع هو الوحدة التي تتضمن تعريف ذلك التابع، ولا يستخدم الصنف كنطاق عام على الإطلاق، ومع أن استخدام البيانات العامة في التوابع أمر نادر ولكن توفّر بايثون عددًا من الاستخدامات الصالحة للنطاق العام، منها أنّه يمكن للتوابع استخدام الدوال والوحدات المستوردة إلى النطاق العام إلى جانب الدوال والأصناف المعرّفة فيه، وعادة ما يجري تعريف الصنف الذي يحتوي على التابع في النطاق العام أيضًا.
  • كل قيمة هي كائن؛ لذا فإنّها تمتلك صنفًا (يدعى كذلك بالنوع)، ويخزّن بالصيغة object.__class__‎.

مصادر

  • صفحة Classes في توثيق بايثون الرسمي.