اذهب إلى المحتوى

البرمجة كائنية التوجه في بايثون: الخاصيات Properties


Naser Dakhel

لدى العديد من اللغات البرمجية ميزات البرمجة كائنية التوجه ولكن تمتلك لغة بايثون Python أفضلها. سيساعدك تعلم كيفية استخدام هذه التقنيات الخاصة ببايثون على كتابة شيفرة مختصرة وسهلة القراءة.

تسمح الخاصيات بتنفيذ شيفرة معينة في كل مرة تُقرأ أو تُعدل أو تُحذف سمة كائن معين لضمان عدم وضع الكائن في حالة غير صالحة. تسمى هذه التوابع في لغات البرمجة الأخرى بالجالبة getters أو الضابطة setters. تسمح لك التوابع السحرية Dunder methods باستخدام الكائن الخاص بك مع عوامل بايثون، مثل عامل + ويسمح لك ذلك بجمع كائني datetime.timedelta مثل datetime.timedelta(days=2)‎‏‏‏‏‏‏ و datetime.timedelta(days=3)‎‏ لإنشاء كائن datetime.timedelta(days=5)‎.

سنتوسع في الصنف WizCoin الذي بدأنا به سابقًا بالإضافة لاستخدام أمثلة أخرى، وذلك بإضافة الخاصيات وإعادة تعريف العوامل في التابع السحري. ستجعل هذه الميزات كائنات WizCoin معبرةً بصورةٍ أكبر وأسهل للاستخدام في أي تطبيق يستورد وحدة wizcoin.

الخاصيات Properties

وسَمَ الصنف BankAccount المستخدم سابقًا السمة ‏‏balance_ بأنها خاصة private عن طريق وضع شرطة سفلية في بداية الاسم، ولكن تذكر أن وصف سمة أنها خاصة هو اصطلاح. كل السمات في بايثون هي تقنيًا عامة، يعني أنه يمكن الوصول لها من شيفرة خارج الصنف، كما أنه لا توجد طريقة لمنع الشيفرة من تغيير سمة balance_ سواءً عن قصد أو بغير قصد إلى قيمة غير صالحة.

يمكن منع التغييرات العرضية لهذه السمات الخاصة عن طريق الخواص. إذ أن الخاصيات في بايثون هي سمات خُصصت لها توابع جالبة وضابطة وحاذفة، وهذه التوابع بدورها تنظم كيفية قراءة السمة وتغييرها وحذفها. مثلًا، إذا كان لدى السمة فقط قيم أعداد صحيحة سيتسبب تحويلها إلى سلسلة نصية 42 بأخطاء. تستدعي الخاصية تابع الضبط لتنفيذ الشيفرة التي تحل أو على الأقل تنبه مبكرًا على ضبط قيمة غير صالحة. إذا فكرت "لطالما أردت تنفيذ شيفرة في كل مرة يجري فيها الوصول إلى السمة أو تعديلها بتعليمة إسناد أو حذفها بتعليمة del" إذًا عليك استخدام الخواص.

تحويل السمة إلى خاصية

لننشئ أولًا صنفًا بسيطًا لديه سمة عادية بدلًا من خاصية. افتح نافذة محرر ملفات جديدة وأدخِل الشيفرة التالية واحفظه على النحو التالي regularAttributeExample.py:

class ClassWithRegularAttributes:
    def __init__(self, someParameter):
        self.someAttribute = someParameter

obj = ClassWithRegularAttributes('some initial value')
print(obj.someAttribute)  # ‫يطبع‏ 'some initial value'
obj.someAttribute = 'changed value'
print(obj.someAttribute)  # يطبع‫ 'changed value'
del obj.someAttribute  # يحذف السمة‫ someAttribute

يحتوي الصنف ClassWithRegularAttributes على سمة عادية اسمها someAttribute. يضبط التابع ‎__init__()‎ السمة someAttribute إلى some initial value، ومن ثم مباشرة نغير قيمة السمة إلى changed value، وعند تنفيذ البرنامَج ستكون المُخرجات على النحو التالي:

some initial value
changed value

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

لنعيد كتابة هذا الصنف باستخدام الخواص، وذلك عن طريق تطبيق الخطوات التالية على سمة تُدعى someAttribute:

  1. أعِد تسمية السمة مع بادئة هي شرطة سفلية ‎_someAttribute
  2. أنشئ تابعًا اسمه someAttribue مع المزخرف @property. لدى هذا التابع الجالب المعامل self الموجود لدى كل التوابع.
  3. أنشئ تابع آخر اسمه someAttribute مع المزخرف someAttribute.setter@. لدى هذا التابع الضابط المعاملين self و value.
  4. أنشئ تابع آخر اسمه someAttribute مع المزخرف someAttribute.deleter@. لدى هذا التابع الحاذف المعامل self الموجود لدى كل التوابع.

افتح نافذة محرر ملفات جديدة وادخل الشيفرة التالية واحفظها على النحو التالي propertiesExample.py:

class ClassWithProperties:
    def __init__(self):
        self.someAttribute = 'some initial value'

    @property
    def someAttribute(self): # هذا التابع هو الجالب
        return self._someAttribute

    @someAttribute.setter
    def someAttribute(self, value): # هذا التابع الضابط
        self._someAttribute = value

    @someAttribute.deleter
    def someAttribute(self): # هذا التابع الحاذف
        del self._someAttribute

obj = ClassWithProperties()
print(obj.someAttribute) # ‫يطبع 'some initial value'
obj.someAttribute = 'changed value'
print(obj.someAttribute) # يطبع‫ 'changed value'
del obj.someAttribute # يحذف السمة‫ _someAttribute

خرج هذا البرنامج هو خرج الشيفرة في regularAttributeExample.py ذاتها لأنهما ينفذان المهمة ذاتها وهي طباعة السمة الأولية للكائن ومن ثم تحديث السمة وطباعتها مجددًا.

لاحظ أن الشيفرة خارج الصنف لا تصل مباشرةً إلى السمة someAttribute_ (لأنها خاصة)، ولكنها تصل إلى خاصية someAttribute. مكونات هذه الخاصية هي مجردة نوعًا ما، وهي التوابع الجالبة والضابطة والحاذفة. عندما نعيد تسمية سمة اسمها someAttribute إلى ‎_someAttribute أثناء إنشاء توابع جالية وضابطة وحاذفة نسمي ذلك خاصية someAttribute.

تسمى في هذا السياق سمة ‎_someAttribute حقل الرجوع backing field أو متغير الرجوع backing variable وهي السمة التي تُبنى عليها الخاصية. لمعظم الخاصيات متغير رجوع ولكن ليس جميعها، سننشئ خاصيات بدون متغير رجوع لاحقًا.

  • عندما تنفذ بايثون شيفرة تصل إلى تابع مثل print(obj.someAttribute)‎، فإنها تستدعي في الخلفية تابع الجلب وتستخدم القيمة المعادة.
  • عندما تنفذ بايثون تعليمة إسناد مع خاصية، مثل 'obj.someAttribute = 'changed value، فإنها تستدعي في الخلفية تابع الضبط وتمرر السلسلة النصية 'changed value' من أجل المعامل value.
  • عندما تنفذ بايثون تعليمة del مع خاصية مثل del obj.someAttribute، تستدعي في الخلفية تابع الحذف.

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

افتح محرر النصوص وادخل الشيفرة التالية واحفظ التالي في badPropertyExample.py.

class ClassWithBadProperty:
    def __init__(self):
        self.someAttribute = 'some initial value'

    @property
    def someAttribute(self):  # التابع الجالب
        # نسينا هنا استخدام الشرطة السفلية (_) مما تسبب باستخدامنا للخاصية واستدعاء التابع الجالب مجددًا
        return self.someAttribute  # هذا يستدعي التابع الجالب مجددًا

    @someAttribute.setter
    def someAttribute(self, value):  # التابع الضابط
        self._someAttribute = value

obj = ClassWithBadProperty()
print(obj.someAttribute)  # ينتج خطأ هنا بسبب استدعاء الدالة الجالبة للدالة الجالبة

يستمر الجالب باستدعاء نفسه عند تنفيذ هذه الشيفرة إلى أن يعطي بايثون الاستثناء recursionError:

Traceback (most recent call last):
  File "badPropertyExample.py", line 16, in <module>
    print(obj.someAttribute)  # ينتج خطأ هنا بسبب استدعاء الدالة الجالبة للدالة الجالبة
  File "badPropertyExample.py", line 9, in someAttribute
    return self.someAttribute  # يستدعي هذا السطر الجالب مجددًا 
  File "badPropertyExample.py", line 9, in someAttribute
    return self.someAttribute  # يستدعي هذا السطر الجالب مجددًا 
  File "badPropertyExample.py", line 9, in someAttribute
    return self.someAttribute  # يستدعي هذا السطر الجالب مجددًا 
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

يجب على الشيفرة داخل توابع الجلب والضبط والحذف أن تعمل دائمًا على متغير الرجوع لمنع هذا التكرار من الحصول (الذي في بداية اسمه شرطة سفلية) وليس على الخاصية. كما لا يوجد ما يمنع من كتابة الشيفرة على متغير الرجوع حتى مع وجود بادئة الشرطة السفلية التي تجعل الوصول خاص.

استخدام الضوابط للتحقق من البيانات

الغرض الأساسي من استخدام الخاصيات هي للتحقق من البيانات أو التأكد من أنها في الصيغة المرغوبة. ربما لا تريد لشيفرة خارج الصنف أن تضبط سمة لأي قيمة لأن هذا قد يؤدي إلى أخطاء. يمكنك استخدام الخاصيات لإضافة فحوصات checks تضمن أن القيمة التي تسند إلى السمة هي القيمة الصحيحة. تسمح هذه الفحوصات ملاحظة الأخطاء في مراحل تطوير الشيفرة المبكرة لأنها تسمح برفع استثناء عندما تُضبط أي قيمة غير صالحة.

لنحدث ملف wizcoin.py الذي أنشأناه سابقًا ليُحوّل السمات galleons و sickles و knuts إلى خاصيات. سنغير ضابطة هذه الخاصيات لتكون الأرقام الصحيحة الموجبة فقط صالحة. تمثل WizCoin كمية النقود ولا يمكن أن تحتوي نصف قطعة نقدية أو أي قيمة أقل من الصفر، سنرفع استثناء WizCoinException في حال حاولت شيفرة خارج الصنف أن تضبط الخاصيات galleons أو sickles أو knuts إلى قيمة غير صالحة .

افتح ملف wizoin.py الذي حفظته سابقًا وعدله ليصبح على النحو التالي:

1 class WizCoinException(Exception):
2     """The wizcoin module raises this when the module is misused."""
    pass

class WizCoin:
    def __init__(self, galleons, sickles, knuts):
        """Create a new WizCoin object with galleons, sickles, and knuts."""
3         self.galleons = galleons
        self.sickles  = sickles
        self.knuts    = knuts
        # NOTE: __init__() methods NEVER have a return statement.

--snip--

    @property
4     def galleons(self):
        """Returns the number of galleon coins in this object."""
        return self._galleons

    @galleons.setter
5     def galleons(self, value):
6         if not isinstance(value, int):
7             raise WizCoinException('galleons attr must be set to an int, not a ' + value.__class__.__qualname__)
8         if value < 0:
            raise WizCoinException('galleons attr must be a positive int, not ' + value.__class__.__qualname__)
        self._galleons = value

--snip--

تضيف التغييرات الجديدة صنف WizCoinException الذي يرث من صنف Exception المبني مسبقًا في بايثون. توضح سلسلة توثيق النصية docstring الخاصة بالصنف كيف تستخدمه وحدة wizcoin. تُعد هذه ممارسة جيدة لوحدات بايثون يمكن أن ترفعها كائنات صنف wizcoin عندما يُساء استخدامها، وهكذا عندما يرفع كائن WizCoin أصناف استثناءات أخرى مثل ValueError و TypeError، سيشير هذا غالبًا إلى خطأ في صنف WizCoin.

ضبطنا في التابع ‎__‎init__()‎ الخاصيات self.galleons و slef.sickles و self.knuts إلى المعاملات الموافقة.

أضفنا في آخر الملف تابع جالب وضابط للسمة self._galleons بعد التابعين total()‎ و weight()‎. يعيد هذا الجالب القيمة في self._galleons ويتحقق التابع الضابط إذا كان القيمة المسندة إلى الخاصية galleons هي عدد صحيح وموجب، إذا فشل واحد من التحقيقين تُرفع WizCoinException برسالة خطأ، كما يمنع هذا التحقق ‎_galleons من أن تُضبط بقيمة غير صالحة طالما تستخدم الشيفرة الخاصية galleons.

لدى كل كائنات بايثون تلقائيًا سمة __class__ التي تشير إلى صنف الكائن. بمعنى أخر، __value.__class هي نفس صنف الكائن الذي يعيده type(value)‎ ، كما أنه لدى كائن الصنف هذا سمة __qualname__ التي هي سلسلة نصية لاسم الصنف. تحديدًا هو الاسم المؤهل للصنف الذي يتضمن أسماء أي أصناف يكون كائن الصنف متداخلًا فيها. الأصناف المتداخلة Nested classes محدودة الاستخدام وخارج نطاق موضوعنا. فمثلًا إذا كانت value قد خزّنت الكائن date المعاد بالصيغة datetime.date(2021, 1, 1)‎، ستكون __value.__class__.__qualname هي السلسلة النصية 'date'. تستخدِم رسالة الاستثناء __value.__class__.__qualname (في السطر 7) للوصول إلى السلسلة النصية لقيمة اسم الكائن، إذ يجعل اسم الكائن رسالة الخطأ هذه أكثر إفادة للمبرمج الذي يقرأها لأنها تحدّد أن الوسيط 'value' ليس من النوع الصحيح، وتحدد أيضًا ما هو نوعه السابق وما النوع الذي يجب أن يكون.

ستحتاج لنسخ الشيفرة من الجالب والضابط ليستخدمها ‎_galleons ومن أجل سمات ‎_sickles و ‎_knuts أيضًا، إذ تكون شيفراتهم نفسها ما عدا أنها تستخدم السمات ‎_sickles و ‎_knuts بدلًا من ‎_galleons للمتغيرات الراجعة.

خاصيات القراءة فقط

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

مثلًا، يعيد التابع total()‎ في الصنف WizCoin قيمة الكائن في knuts. يمكننا تغيير ذلك من تابع عادي لخاصية القراءة فقط لأنه في النهاية لا توجد طريقة منطقية لضبط total لكائن WizCoin؛ فإذا ضبطنا total إلى العدد الصحيح 1000، هل ذلك يعني ‎1000 knuts؟ أو ‎1 galleon و ‎493 knuts؟ أو أي تشكيلة أخرى؟ لهذا السبب سنجعل total خاصية للقراءة فقط عن طريق إضافة الشيفرة بالخط الغامق في ملف wizcoin.py:

@property
    def total(self):
        """Total value (in knuts) of all the coins in this WizCoin object."""
        return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts)

    # Note that there is no setter or deleter method for `total`.

بعد إضافة مزخرف التابع porperty@ أمام total()‎، سيستدعي بايثون تابع total()‎ عند الوصول إلى total، ولأنه لا يوجد تابع ضابط ولا حاذف، ترفع بايثون AtrributeError إذا حاولت أي شيفرة تعديل أو حذف total باستخدامه في وسيط أو تعليمة delعلى التتالي. تعتمد قيمة الخاصية total على قيمة الخاصيات galleons و sickles و knuts ولا تعتمد الخاصية على متغير الرجوع المسمى ‎_total .

أدخل التالي في الصَدَفة التفاعلية:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> purse.total
1141
>>> purse.total = 1000
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

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

لا تخلط بين خاصيات للقراءة فقط والمتغيرات الثابتة؛ فالمتغيرات الثابتة تكون مكتوبة كلها بحروف كبيرة وتعتمد على المبرمج بعدم تعديلها، ويُفترض أن تبقى قيمتها ثابتة طول فترة تنفيذ البرنامج. خاصية للقراءة فقط هي مثل أي سمة تكون مرتبطة مع كائن. لا يمكن ضبط أو حذف خاصيات للقراءة فقط مباشرةً ولكنها يمكن أن تُقَيّم مع قيمة متغيرة. تتغير الخاصية total للصنف الخاص بنا WizCoin، إذا تغيرت الخاصيات galleons و sickles و knuts.

أين تستخدم الخواص

كما رأينا في القسم السابق، تقدم الخاصيات سيطرةً أكثر على كيفية استخدام سمات الصنف، وهذه طريقة خاصة ببايثون لكتابة الشيفرة. تشير التوابع المسماة ‏‏‏‏‏‏‏‏getSomeAttribute()‎‏‏‏‏‏‎‎‎‎‎ و setSomeAttribute()‎ أنه يجب استخدام الخاصيات بدلًا عن ذلك.

هذا لا يعني أن كل نسخة من تابع مبدوء بجلب get أو ضبط set يجب استبداله مباشرةً بخاصية، بل هناك مواقف يجب فيها استخدام التابع حتى لو بدأ اسمه بجلب أو ضبط وهذه بعض الأمثلة:

  • العمليات البطيئة التي تستغرق أكثر من ثانية أو ثانيتين، مثل تنزيل أو رفع الملفات.
  • العمليات التي لديها آثار جانبية، مثل حدوث تغييرات لسمات وكائنات أخرى.
  • العمليات التي تتطلب وسائط إضافية لتمرر إلى عمليات الجلب أو الضبط، مثل استدعاء تابع emailObj.getFileAttachment(filename)‎.

الخلاصة

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

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

تطبّق بايثون الخاصيات كائنية التوجه بصورةٍ مختلفة عن باقي لغات البرمجة كائنية التوجه مثل جافا و C++‎، فبدلًا من تقديم توابع جالبة وضابطة محددة، لدى بايثون خاصيات تسمح لك بتدقيق السمات وجعلها للقراءة فقط.

ترجمة -وبتصرف- لقسم من الفصل Pythonic OOP: Properties and dunder methods من كتاب Beyond the Basic Stuff with Python.

اقرأ المزيد


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...