نطاق التوابع
أهداف التعلّم
بعد إتمام هذا القسم
- ستعرف كيف يمكنك تحديد مدى رؤية تابع في Python
- ستكون قادرًا على كتابة التوابع الخاصة
التوابع المُعرَّفة داخل صنف يمكن إخفاؤها بالطريقة نفسها تمامًا التي أُخفيت بها الخصائص في القسم السابق. فإذا بدأ التابع بشَرطتَي سفلية __، فإنه لا يتوافر للعميل (client) مباشرة.
إذًا، فالتقنية واحدة في كلٍّ من التوابع والخصائص، لكن حالات الاستخدام تختلف عادةً قليلًا. فغالبًا ما تأتي الخصائص الخاصة مقترنةً بتوابع القراءة getter والكتابة setter للتحكم في الوصول إليها. أمّا التوابع الخاصة فإنها مخصّصة عادةً للاستخدام الداخلي فقط، بوصفها توابع مساعدة لعمليات لا يحتاج العميل إلى معرفتها.
يمكن استخدام التابع الخاص داخل الصنف تمامًا كما يُستخدم أي تابع آخر، مع تذكّر إضافة بادئة self بالطبع. فيما يلي صنف بسيط يمثّل مستلِم رسائل البريد الإلكتروني. ويتضمّن تابع مساعدًا خاصًا للتحقّق من أن عنوان البريد الإلكتروني في صيغة صالحة:
class Recipient:
def __init__(self, name: str, email: str):
self.__name = name
if self.__check_email(email):
self.__email = email
else:
raise ValueError("The email address is not valid")
def __check_email(self, email: str):
# A simple check: the address must be over 5 characters long
# and contain a dot and an @ character
return len(email) > 5 and "." in email and "@" in email
محاولة استدعاء التابع الخاص مباشرةً تُسبّب خطأ:
peter = Recipient("Peter Emailer", "peter@example.com")
peter.__check_email("someone@example.com")
AttributeError: 'Recipient' object has no attribute '__check_email'
داخل الصنف يمكن الوصول إلى التابع بشكل طبيعي، ومن المعقول استخدامه أيضًا لتعيين قيمة جديدة لعنوان البريد. لنتضمّن توابع القراءة والكتابة لعنوان البريد الإلكتروني:
class Recipient:
def __init__(self, name: str, email: str):
self.__name = name
if self.__check_email(email):
self.__email = email
else:
raise ValueError("The email address is not valid")
def __check_email(self, email: str):
# A simple check: the address must be over 5 characters long
# and contain a dot and an @ character
return len(email) > 5 and "." in email and "@" in email
@property
def email(self):
return self.__email
@email.setter
def email(self, email: str):
if self.__check_email(email):
self.__email = email
else:
raise ValueError("The email address is not valid")
نطاق Python ونطاق الأسماء
لقد واجهنا مصطلح النطاق (scope) سابقًا في الجزء 6 من هذه المادّة، وعرّفناه بأنه أجزاء البرنامج التي يكون فيها اسم (متغيّر) مرئيًا. وينظر إلى المصطلح من زاوية أخرى، فيشير أيضًا إلى ما يمكن رؤيته من نقطة معيّنة في شيفرة البرنامج. ومصطلح آخر مرتبط به هو نطاق الأسماء (namespace)، الذي يشير إلى الأسماء المتاحة تحديدًا داخل وحدة Python مُعرَّفة، مثل تعريف صنف أو تعريف دالة.
النطاق داخل التابع يختلف عن النطاق داخل الصنف، وهذا بدوره يختلف عن النطاق في شيفرة العميل التي تنشئ نسخة من الصنف. فالتابع يصل إلى متغيّراته المحلية، وكذلك إلى الخصائص والتوابع الأخرى في الصنف الذي ينتمي إليه، حتى لو كانت خاصة. والصنف يصل أيضًا إلى هذه الأعضاء التابعة له، لكنه لا يستطيع الوصول مباشرة إلى المتغيّرات المحلية داخل توابعه. أمّا شيفرة العميل فلا تصل إلا إلى التوابع والخصائص العامة المُعرَّفة في الصنف، لكنها بطبيعة الحال تصل أيضًا إلى بعض الأسماء الأخرى في البيئة التي توجد فيها.
قد يبدو غير منطقي ألّا يكون للصنف الوصول إلى كل محتوياته، لكن ذلك ضروري لضمان التماسك. فمثلًا، قد يكون من المعقول استخدام اسم متغيّر محلي واحد في توابع مختلفة داخل الصنف نفسه، إذا كانت تلك التوابع تؤدي وظائف مشابهة بطريقة ما. ولو كان للصنف الوصول المباشر إلى كل المتغيّرات المحلية داخل التوابع، لوجبت تسميتها بأسماء مختلفة، وإلا لما كان من الواضح أي نسخة من المتغيّر يُقصَد في كل موضع. وقد رأينا سابقًا مع الخصائص المُعلَنة بـ
selfأن متغيّرات المساعدة ينبغي ألّا تُتاح خارج التابع، لذا فإن إضافة تلك المتغيّرات على هيئة خصائص أو متغيّرات عامة لا ينبغي أن تكون خيارًا. لا بدّ من وجود طريقة لإبقاء الأسماء في أجزاء البرنامج المختلفة منفصلة، وهذا ما وُجد من أجله نطاقات الأسماء.فكرة نطاق الأسماء تساعد على فهم كيف يمكن لنفس الاسم أن يتعايش بسلام في دوال أو أصناف أو وحدات مختلفة في الوقت نفسه. فإذا كان الاسم محدّدًا لنطاق أسماء معيّن، كتعريف تابع مثلًا، فلن يمكن الوصول إليه مباشرةً خارج ذلك النطاق، وبالتالي لا يوجد سبب يمنع نطاق أسماء آخر من استخدام نفس الاسم. وإتقان نطاقات الأسماء والنطاقات شرط أساسي لتصبح مبرمجًا متمكّنًا، وستحصل على تدريب كثير في هذه الدورة.
هل أحتاج إلى تابع خاص؟
في المثال التالي يمثّل الصنف DeckOfCards رزمة من 52 ورقة لعب. ويتضمّن تابع المساعدة __reset_deck، الذي ينشئ رزمة أوراق جديدة مخلوطة. ويُستدعى التابع الخاص حاليًّا فقط داخل تابع الباني، لذا يمكن القول إن التنفيذ يمكن وضعه مباشرةً في الباني. غير أن استخدام تابع منفصل يجعل الشيفرة أسهل قراءة، ويتيح أيضًا الوصول إلى هذه الوظيفة لاحقًا في توابع أخرى عند الاقتضاء.
from random import shuffle
class DeckOfCards:
def __init__(self):
self.__reset_deck()
def __reset_deck(self):
self.__deck = []
# Add all 52 cards to the deck
suits = ["spades", "hearts", "clubs", "diamonds"]
for suit in suits:
for number in range(1, 14):
self.__deck.append((suit, number))
# Shuffle the deck
shuffle(self.__deck)
def deal(self, number_of_cards: int):
hand = []
# Move the top cards in the deck to the hand
for i in range(number_of_cards):
hand.append(self.__deck.pop())
return hand
لنختبر الصنف:
deck = DeckOfCards()
hand1 = deck.deal(5)
print(hand1)
hand2 = deck.deal(5)
print(hand2)
وبما أن الأيدي تُولَّد عشوائيًّا، فإن ما يلي مجرد مثال لما يمكن أن يُطبع:
[('spades', 7), ('spades', 11), ('hearts', 7), ('diamonds', 3), ('spades', 4)]
[('clubs', 8), ('spades', 12), ('diamonds', 13), ('clubs', 11), ('spades', 10)]
التوابع الخاصة أقلّ شيوعًا عمومًا من الخصائص الخاصة. والقاعدة العامة أن يُخفى التابع متى لم تكن لدى العميل حاجة إلى الوصول إليه مباشرة. وخاصّة حين من المحتمل أن يؤثّر العميل سلبًا على تماسك الكائن باستدعاء هذا التابع.
تمرين برمجي: رسم الخدمة
يُرجى إنشاء صنف اسمه
BankAccountيمثّل حسابًا مصرفيًّا. وينبغي أن يحوي الصنف
- بانيًا يأخذ اسم المالك (سلسلة نصية)، ورقم الحساب (سلسلة نصية)، والرصيد (عدد عشري) كوسيطات
- تابعًا
deposit(amount: float)لإيداع المال في الحساب - تابعًا
withdraw(amount: float)لسحب المال من الحساب - تابع قراءة اسمه
balanceيُرجع رصيد الحساب
ويُرجى أيضًا تضمين التابع الخاص
__service_charge()، الذي يقلّل رصيد الحساب بنسبة مئوية واحدة. ومتى استُدعى أيٌّ من تابعَيdepositأوwithdraw، ينبغي استدعاء هذا التابع أيضًا. ويُحسب رسم الخدمة ويُطرح فقط بعد اكتمال العملية الفعلية (أي بعد إضافة المبلغ المحدَّد إلى الرصيد أو طرحه منه).
وينبغي أن تكون كل الخصائص البيانية داخل تعريف الصنف خاصة.
يمكنك استخدام الشيفرة التالية لاختبار صنفك:
account = BankAccount("Randy Riches", "12345-6789", 1000)
account.withdraw(100)
print(account.balance)
account.deposit(100)
print(account.balance)
891.0
981.09