خصائص الصنف
أهداف التعلّم
بعد إتمام هذا القسم
- ستألف مصطلحي متغيّر الصنف وتابع الصنف
- ستعرف كيف تختلف السمات الساكنة عن سمات النسخ
- ستكون قادرًا على إضافة سمات ساكنة إلى أصنافك الخاصة
السمات (traits) مفهوم جوهري في البرمجة الكائنية. ويشمل المصطلح التوابع والمتغيّرات المُعرَّفة في تعريف الصنف. ولا يستعمل كل مبرمجي Python مصطلح «السمات»، بل يفضّل بعضهم «الخصائص» أو «الميزات» أو «الأعضاء». وقد رُحب بمصطلح السمات على نحو واسع في عالم البرمجة الكائنية الأوسع، ولهذا فهو المصطلح الذي نستعمله هنا.
وإلى الآن تعاملنا مع سمات الكائنات في معظمها. وتشمل التوابع والخصائص المتاحة في أي نسخة من صنف. وفي الحقيقة، يمكن أن يكون للصنف نفسه أيضًا سمات، تُسمّى أحيانًا السمات الساكنة، أو بتحديد أكثر متغيّرات الصنف و_توابع الصنف_.
متغيّرات الصنف
لكل نسخة من صنف قيمها المحدَّدة الخاصة بكل خاصية مُعرَّفة في الصنف، كما رأينا في أمثلة الأقسام السابقة. لكن ماذا لو أردنا أن نملك بعض البيانات التي تتشاركها النسخ المختلفة؟ هنا يظهر متغيّر الصنف، المعروف أيضًا بالمتغيّر الساكن. فمتغيّر الصنف متغيّر يُوصَل إليه من خلال الصنف نفسه، لا من خلال النسخ المُنشأة بناءً عليه. وفي أي لحظة محدَّدة أثناء تنفيذ البرنامج يكون لمتغيّر الصنف قيمة واحدة فقط، مهما كان عدد النسخ المُنشأة من الصنف.
يُعلن متغيّر الصنف دون بادئة self، وعادةً خارج أي تعريف تابع، لأنه ينبغي أن يكون متاحًا من أي مكان داخل الصنف، بل وحتى من خارج الصنف.
class SavingsAccount:
general_rate = 0.03
def __init__(self, account_number: str, balance: float, interest_rate: float):
self.__account_number = account_number
self.__balance = balance
self.__interest_rate = interest_rate
def add_interest(self):
# The total interest rate equals
# the general rate + the interest rate of the account
total_interest = SavingsAccount.general_rate + self.__interest_rate
self.__balance += self.__balance * total_interest
@property
def balance(self):
return self.__balance
وبما أن المتغيّر general_rate مُعرَّف داخل الصنف لكنه خارج أي تعريفات التوابع، ولا يستخدم بادئة self، فهو متغيّر صنف.
ويُوصَل إلى متغيّر الصنف من خلال اسم الصنف، فمثلًا هكذا:
# The general rate exists independently of any object instances
print("The general interest rate is", SavingsAccount.general_rate)
account = SavingsAccount("12345", 1000, 0.05)
# Add the total interest accrued to the balance on the account
account.add_interest()
print(account.balance)
The general interest rate is 0.03
1080.0
إذًا، تُوصَل متغيّرات الصنف من خلال اسم الصنف، فمثلًا عبر SavingsAccount.general_rate، بينما تُوصَل متغيّرات النسخ من خلال اسم متغيّر الكائن، مثل account.balance. ومتغيّر النسخ لا يوجد بطبيعته إلا حين تُنشأ نسخة من الصنف، لكن متغيّر الصنف متاح في كل مكان وفي كل لحظة يكون فيها الصنف نفسه متاحًا.
تكون متغيّرات الصنف مفيدة حين تُرد قيم تتشاركها كل نسخات الصنف. وفي المثال أعلاه افترضنا أن نسبة الفائدة الكلية لكل حسابات الادخار تتكوّن من مكوّنين: نسبة الفائدة العامة تتشاركها كل الحسابات، لكن لكل حساب أيضًا نسبة فائدة تخصّه في متغيّر نسخة. وقد تتغيّر النسبة العامة أيضًا، لكن التغيير يؤثر حينها في كل نسخات الصنف بالتساوي.
class SavingsAccount:
general_rate = 0.03
def __init__(self, account_number: str, balance: float, interest_rate: float):
self.__account_number = account_number
self.__balance = balance
self.__interest_rate = interest_rate
def add_interest(self):
# The total interest rate equals
# the general rate + the interest rate of the account
total_interest = SavingsAccount.general_rate + self.__interest_rate
self.__balance += self.__balance * total_interest
@property
def balance(self):
return self.__balance
@property
def total_interest(self):
return self.__interest_rate + SavingsAccount.general_rate
account1 = SavingsAccount("12345", 100, 0.03)
account2 = SavingsAccount("54321", 200, 0.06)
print("General interest rate:", SavingsAccount.general_rate)
print(account1.total_interest)
print(account2.total_interest)
# The general rate of interest is now 10 percent
SavingsAccount.general_rate = 0.10
print("General interest rate:", SavingsAccount.general_rate)
print(account1.total_interest)
print(account2.total_interest)
General interest rate: 0.03
0.06
0.09
General interest rate: 0.1
0.13
0.16
ومتى تغيّرت نسبة الفائدة العامة، تغيّرت نسبة الفائدة الكلية لكل نسخات الصنف. وكما ترى أعلاه، يمكن إضافة تابع قراءة باستخدام زينة @property حتى وإن لم تكن هناك خاصية بنفس الاسم في الصنف. وهذا التابع يُرجع مجموع نسبة الفائدة العامة ونسبة فائدة الحساب الخاصة به.
لننظر إلى مثال آخر. يُستخدم الصنف PhoneNumber لتعريف رقم هاتف واحد، لكنه يحوي أيضًا بعض رموز الدول في قاموس. وهذا القاموس متغيّر صنف، وبذلك يتشاركه كل نسخات الصنف، لأن رمز الدولة لأرقام الهاتف من دولة واحدة يكون دائمًا نفسه.
class PhoneNumber:
country_codes = {"Finland": "+358", "Sweden": "+46", "United States": "+1"}
def __init__(self, name: str, phone_number: str, country: str):
self.__name = name
self.__phone_number = phone_number
self.__country = country
@property
def phone_number(self):
# When the country code prefix is added
# the initial zero is removed from the phone number
return PhoneNumber.country_codes[self.__country] + " " + self.__phone_number[1:]
paulas_no = PhoneNumber("Paula Pythons", "050 1234 567", "Finland")
print(paulas_no.phone_number)
+358 50 1234 567
يحوي كل كائن PhoneNumber اسم المالك، والرقم نفسه، ودولة رقم الهاتف. وحين يُوصَل إلى الخاصية التي تحوي رقم الهاتف عبر تابع القراءة، يُستخرج رمز الدولة المناسب من قاموس متغيّر الصنف بناءً على خاصية الدولة، ويُضاف الناتج في مقدمة الرقم.
تنفيذ المثال أعلاه ليس عمليًّا على نحو يُذكر في بقية الجوانب. وفي المثال التالي أضفنا توابع قراءة وكتابة لكل الخصائص:
class PhoneNumber:
country_codes = {"Finland": "+358", "Sweden": "+46", "United States": "+1"}
def __init__(self, name: str, phone_number: str, country: str):
self.__name = name
# This is a call to the phone_number.setter method
self.phone_number = phone_number
# This is a call to the country.setter method
self.country = country
# the getter method for phone_number combines the country code
# and the attribute phone_number
@property
def phone_number(self):
# the initial zero is removed as the country code is prefixed
return PhoneNumber.country_codes[self.__country] + " " + self.__phone_number[1:]
@phone_number.setter
def phone_number(self, number):
# Making sure the number contains only numbers and space characters
for character in number:
if character not in "1234567890 ":
raise ValueError("A phone number can only contain numbers and spaces")
self.__phone_number = number
# a getter for only the number itself without the country code
@property
def local_number(self):
return self.__phone_number
@property
def country(self):
return self.__country
@country.setter
def country(self, country):
# Making sure the country is a key in the dictionary of country codes
if country not in PhoneNumber.country_codes:
raise ValueError("This country is not on the list.")
self.__country = country
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
self.__name = name
def __str__(self):
return f"{self.phone_number} ({self.__name})"
if __name__ == "__main__":
pn = PhoneNumber("Peter Pythons", "040 111 1111", "Sweden")
print(pn)
print(pn.phone_number)
print(pn.local_number)
+46 40 111 1111 (Peter Pythons)
+46 40 111 1111
040 111 1111
تمرين برمجي: الرموز البريدية
يحوي قالب التمرين تعريف الصنف
Cityالذي يمثّل مدينة واحدة.
يُرجى إضافة متغيّر صنف اسمه postcodes يشير إلى قاموس. مفاتيح القاموس أسماء مدن، والقيم المرتبطة بها الرموز البريدية لتلك المدن. وكلاهما سلسلة نصية.
وينبغي أن يحوي القاموس (على الأقل) الرموز البريدية التالية:
- هلسنكي 00100
- توركو 20100
- تامبيري 33100
- روفانييمي 96100
- أولو 90100
ولا تحتاج إلى تنفيذ أي وظيفة أخرى.
توابع الصنف
تابع الصنف، المسمّى أيضًا تابعًا ساكنًا، تابع لا يرتبط بأي نسخة مفردة من الصنف. ويمكن استدعاء تابع الصنف دون إنشاء أي نسخة من الصنف.
التوابع الساكنة عادةً أدوات تتعلق بغرض الصنف، لكنها منفصلة عن النسخ بمعنى أنه لا ينبغي أن يكون من الضروري إنشاء نسخ من الصنف لتتمكن من استدعائها. والتوابع الساكنة عادةً عامة، بحيث يمكن استدعاؤها من خارج الصنف ومن داخله، بما في ذلك من داخل نسخ الصنف.
يُعرَّف تابع الصنف بالزينة @classmethod. والمعامل الأول دائمًا cls. واسم المتغيّر cls يشبه المعامل self. والفرق أن cls يشير إلى الصنف بينما self يشير إلى نسخة من الصنف. ولا يُدرج أيٌّ من المعاملَين في قائمة الوسائط حين تُستدعى الدالة؛ بل تملأ Python القيمة المناسبة تلقائيًّا.
في المثال التالي لدينا صنف ينمذج تسجيلات المركبات. ويحوي الصنف Registration تابعًا ساكنًا للتحقّق من صحة لوحة التسجيل. وهذا التابع تابع صنف ساكن لأنه من المفيد التحقّق من صحة لوحة التسجيل حتى قبل إنشاء أي كائن Registration على الإطلاق:
class Registration:
def __init__(self, owner: str, make: str, year: int, license_plate: str):
self.__owner = owner
self.__make = make
self.__year = year
# Call the license_plate.setter method
self.license_plate = license_plate
@property
def license_plate(self):
return self.__license_plate
@license_plate.setter
def license_plate(self, plate):
if Registration.license_plate_valid(plate):
self.__license_plate = plate
else:
raise ValueError("The license plate is not valid")
# A class method for validating the license plate
@classmethod
def license_plate_valid(cls, plate: str):
if len(plate) < 3 or "-" not in plate:
return False
# Check the beginning and end sections of the plate separately
letters, numbers = plate.split("-")
# the beginning section can have only letters
for character in letters:
if character.lower() not in "abcdefghijklmnopqrstuvwxyzåäö":
return False
# the end section can have only numbers
for character in numbers:
if character not in "1234567890":
return False
return True
registration = Registration("Mary Motorist", "Volvo", "1992", "abc-123")
if Registration.license_plate_valid("xyz-789"):
print("This is a valid license plate!")
This is a valid license plate!
يمكن التحقّق من صحة لوحة التسجيل حتى دون إنشاء أي نسخة من الصنف، فمثلًا عبر Registration.license_plate_valid("xyz-789"). ونفس التابع يُستدعى داخل باني الصنف. لاحظ: حتى داخل الباني يُوصَل إلى هذا التابع من خلال اسم الصنف، لا self!
تمرين برمجي: مساعد القائمة
يُرجى إنشاء صنف اسمه
ListHelperيحوي التابعَين الصنفيين التاليين.
greatest_frequency(my_list: list)يُرجع العنصر الأكثر تكرارًا في القائمةdoubles(my_list: list)يُرجع عدد العناصر الفريدة التي تظهر مرتين على الأقل في القائمة
وينبغي أن يمكن استخدام هذين التابعين دون إنشاء نسخة من الصنف. وهذا مثال على كيفية استخدام هذين التابعين:
numbers = [1, 1, 2, 1, 3, 3, 4, 5, 5, 5, 6, 5, 5, 5]
print(ListHelper.greatest_frequency(numbers))
print(ListHelper.doubles(numbers))
5
3