هل تساءلت يومًا كيف تجعل بايثون الكائنات تعمل مع العوامل مثل +
أو -
؟ أو كيف تعرف كيفية عرض الكائنات عند طباعتها؟ الجواب يكمن في طرق بايثون السحرية، المعروفة أيضًا باسم طرق دندر (double under).
طرق السحر هي طرق خاصة تتيح لك تعريف كيفية تصرف كائناتك استجابةً لعمليات مختلفة ووظائف مدمجة. إنها ما يجعل برمجة بايثون الشيئية قوية وبديهية للغاية.
في هذا الدليل، ستتعلم كيفية استخدام طرق السحر لإنشاء كود أكثر أناقة وقوة. سترى أمثلة عملية توضح كيفية عمل هذه الطرق في سيناريوهات العالم الحقيقي.
المتطلبات المسبقة
-
فهم أساسي لقواعد بايثون ومفاهيم البرمجة الشيئية.
-
إلمام بالفئات، الكائنات، والوراثة.
-
معرفة بأنواع البيانات المدمجة في بايثون (القوائم، القواميس، وهكذا).
-
يوصى بوجود تثبيت بايثون 3 يعمل للتفاعل بنشاط مع الأمثلة هنا.
فهرس المحتويات
ما هي الطرق السحرية؟
الطرق السحرية في بايثون هي طرق خاصة تبدأ وتنتهي بشرطتين سفليتين (__
). عندما تستخدم عمليات أو دوال معينة على كائناتك، تستدعي بايثون هذه الطرق تلقائيًا.
على سبيل المثال، عندما تستخدم عامل +
على كائنين، تبحث بايثون عن طريقة __add__
في المعامل الأيسر. إذا وجدتها، تستدعي تلك الطريقة مع المعامل الأيمن كوسيط.
إليك مثال بسيط يوضح كيف يعمل هذا:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # هذا يستدعي p1.__add__(p2)
print(p3.x, p3.y) # الناتج: 4 6
دعنا نفصل ما يحدث هنا:
-
نقوم بإنشاء فئة
Point
التي تمثل نقطة في الفضاء ثنائي الأبعاد -
تقوم طريقة
__init__
بتهيئة إحداثيات x و y -
تحدد طريقة
__add__
ما يحدث عندما نضيف نقطتين -
عندما نكتب
p1 + p2
، تستدعي بايثون تلقائيًاp1.__add__(p2)
-
النتيجة هي نقطة جديدة
Point
بإحداثيات (4، 6)
هذه مجرد البداية. لدى بايثون العديد من الطرق السحرية التي تتيح لك تخصيص كيفية تصرف الكائنات الخاصة بك في مواقف مختلفة. دعنا نستكشف بعضًا من أكثرها فائدة.
تمثيل الكائن
عند العمل مع الكائنات في بايثون، تحتاج غالبًا إلى تحويلها إلى سلاسل نصية. يحدث هذا عندما تقوم بطباعة كائن أو تحاول عرضه في وحدة التحكم التفاعلية. يوفر بايثون طريقتين سحريتين لهذا الغرض: __str__
و __repr__
.
str مقابل repr
تخدم طريقتا __str__
و __repr__
أغراضًا مختلفة:
-
__str__
: يتم استدعاؤها بواسطة دالةstr()
ودالةprint()
. يجب أن تعيد سلسلة نصية قابلة للقراءة للمستخدمين النهائيين. -
__repr__
: يتم استدعاؤها بواسطة دالةrepr()
وتستخدم في وحدة التحكم التفاعلية. يجب أن تعيد سلسلة نصية يمكن، في المثالي، استخدامها لإعادة إنشاء الكائن.
إليك مثال يوضح الفرق:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __str__(self):
return f"{self.celsius}°C"
def __repr__(self):
return f"Temperature({self.celsius})"
temp = Temperature(25)
print(str(temp)) # الناتج: 25°C
print(repr(temp)) # الناتج: Temperature(25)
في هذا المثال:
-
__str__
يعيد سلسلة سهلة القراءة تُظهر درجة الحرارة مع رمز الدرجة -
__repr__
يعيد سلسلة تُظهر كيفية إنشاء الكائن، مما يكون مفيدًا في تصحيح الأخطاء
يصبح الفرق واضحًا عندما تستخدم هذه الكائنات في سياقات مختلفة:
-
عندما تطبع درجة الحرارة، ترى النسخة السهلة القراءة:
25°C
-
عندما تفحص الكائن في وحدة تحكم بايثون، ترى النسخة التفصيلية:
Temperature(25)
مثال عملي: فئة خطأ مخصصة
دعنا ننشئ فئة خطأ مخصصة تقدم معلومات أفضل لتصحيح الأخطاء. هذا المثال يوضح كيف يمكنك استخدام __str__
و__repr__
لجعل رسائل الخطأ الخاصة بك أكثر فائدة:
class ValidationError(Exception):
def __init__(self, field, message, value=None):
self.field = field
self.message = message
self.value = value
super().__init__(self.message)
def __str__(self):
if self.value is not None:
return f"Error in field '{self.field}': {self.message} (got: {repr(self.value)})"
return f"Error in field '{self.field}': {self.message}"
def __repr__(self):
if self.value is not None:
return f"ValidationError(field='{self.field}', message='{self.message}', value={repr(self.value)})"
return f"ValidationError(field='{self.field}', message='{self.message}')"
# الاستخدام
try:
age = -5
if age < 0:
raise ValidationError("age", "Age must be positive", age)
except ValidationError as e:
print(e) # الناتج: خطأ في الحقل 'العمر': يجب أن يكون العمر موجبًا (القيمة: -5)
توفر هذه الفئة المخصصة من الأخطاء عدة فوائد:
-
تشمل اسم الحقل الذي حدث فيه الخطأ
-
تظهر القيمة الفعلية التي تسببت في الخطأ
-
توفر رسائل خطأ سهلة الاستخدام ومفصلة
-
تسهل تصحيح الأخطاء من خلال تضمين جميع المعلومات ذات الصلة
تحميل المشغل
يعتبر تحميل المشغل أحد أقوى ميزات طرق السحر في بايثون. يسمح لك بتعريف كيفية تصرف كائناتك عند استخدامها مع مشغلين مثل +
، -
، *
، و==
. وهذا يجعل كودك أكثر سهولة في الفهم وقراءة.
المشغلين الحسابيين
توفر بايثون طرق سحرية لجميع العمليات الحسابية الأساسية. إليك جدول يوضح أي طريقة تتوافق مع أي مشغل:
المشغل | طريقة السحر | الوصف |
+ |
__add__ |
الجمع |
- |
__sub__ |
الطرح |
* |
__mul__ |
الضرب |
/ |
__truediv__ |
القسمة |
// |
__floordiv__ |
القسمة الصحيحة |
% |
__mod__ |
الباقي |
** |
__pow__ |
الأس |
عمليات المقارنة
بالمثل، يمكنك تحديد كيفية مقارنة الكائنات الخاصة بك باستخدام هذه الأساليب السحرية:
العامل | الطريقة السحرية | الوصف |
== |
__eq__ |
مساو لـ |
!= |
__ne__ |
غير مساو لـ |
< |
__lt__ |
أقل من |
> |
__gt__ |
أكبر من |
<= |
__le__ |
أقل من أو يساوي |
>= |
__ge__ |
أكبر من أو يساوي |
مثال عملي: فئة النقود
دعونا ننشئ فئة Money
التي تتعامل مع عمليات العملات بشكل صحيح. توضح هذه المثال كيفية تنفيذ عدة عمليات والتعامل مع الحالات الحدية:
from functools import total_ordering
from decimal import Decimal
@total_ordering # تنفذ جميع طرق المقارنة بناءً على __eq__ و __lt__
class Money:
def __init__(self, amount, currency="USD"):
self.amount = Decimal(str(amount))
self.currency = currency
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(f"Cannot add different currencies: {self.currency} and {other.currency}")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(f"Cannot subtract different currencies: {self.currency} and {other.currency}")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, other):
if isinstance(other, (int, float, Decimal)):
return Money(self.amount * Decimal(str(other)), self.currency)
return NotImplemented
def __truediv__(self, other):
if isinstance(other, (int, float, Decimal)):
return Money(self.amount / Decimal(str(other)), self.currency)
return NotImplemented
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.currency == other.currency and self.amount == other.amount
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
raise ValueError(f"Cannot compare different currencies: {self.currency} and {other.currency}")
return self.amount < other.amount
def __str__(self):
return f"{self.currency} {self.amount:.2f}"
def __repr__(self):
return f"Money({repr(float(self.amount))}, {repr(self.currency)})"
دعونا نفصل الميزات الرئيسية لهذه الفئة Money
:
-
التعامل مع الدقة: نستخدم
Decimal
بدلاً منfloat
لتجنب مشاكل دقة النقطة العائمة في حسابات المال. -
سلامة العملات: تمنع الفئة العمليات بين العملات المختلفة لتجنب الأخطاء.
-
التحقق من النوع: تتحقق كل طريقة مما إذا كان المعامل الآخر من النوع الصحيح باستخدام
isinstance()
. -
NotImplemented: عندما لا يكون للعملية معنى، نعيد
NotImplemented
لندع بايثون يحاول العملية العكسية. -
@total_ordering: يقوم هذا المزين تلقائيًا بتنفيذ جميع طرق المقارنة بناءً على
__eq__
و__lt__
.
إليك كيفية استخدام فئة Money
:
# الحساب الحسابي الأساسي
wallet = Money(100, "USD")
expense = Money(20, "USD")
remaining = wallet - expense
print(remaining) # الناتج: USD 80.00
# العمل مع عملات مختلفة
salary = Money(5000, "USD")
bonus = Money(1000, "USD")
total = salary + bonus
print(total) # الناتج: USD 6000.00
# القسمة على عدد محدد
weekly_pay = salary / 4
print(weekly_pay) # الناتج: USD 1250.00
# المقارنات
print(Money(100, "USD") > Money(50, "USD")) # الناتج: True
print(Money(100, "USD") == Money(100, "USD")) # الناتج: True
# التعامل مع الأخطاء
try:
Money(100, "USD") + Money(100, "EUR")
except ValueError as e:
print(e) # الناتج: لا يمكن إضافة عملات مختلفة: USD و EUR
توضح فئة Money
العديد من المفاهيم الهامة:
-
كيفية التعامل مع أنواع مختلفة من العناصر
-
كيفية تنفيذ معالجة الأخطاء الصحيحة
-
كيفية استخدام مزين
@total_ordering
-
كيفية الحفاظ على الدقة في الحسابات المالية
-
كيفية توفير طرق السلسلة والتمثيل
طرق الحاوية
تتيح لك طرق الحاوية جعل كائناتك تتصرف مثل الحاويات المدمجة مثل القوائم أو القواميس أو المجموعات. هذا مفيد بشكل خاص عندما تحتاج إلى سلوك مخصص لتخزين واسترجاع البيانات.
بروتوكول التسلسل
لجعل كائنك يتصرف مثل تسلسل (مثل قائمة أو مجموعة)، تحتاج إلى تنفيذ هذه الطرق:
الطريقة | الوصف | مثال على الاستخدام |
__len__ |
يعيد طول الحاوية | len(obj) |
__getitem__ |
يسمح بالترميز باستخدام obj[key] |
obj[0] |
__setitem__ |
يسمح بالتعيين باستخدام obj[key] = value |
obj[0] = 42 |
__delitem__ |
يسمح بالحذف باستخدام del obj[key] |
del obj[0] |
__iter__ |
يعيد مكررًا للحاوية | for item in obj: |
__contains__ |
تنفيذ عامل in |
42 in obj |
بروتوكول الرسم
للحصول على سلوك شبيه بالقاموس ، ستحتاج إلى تنفيذ هذه الطرق:
الطريقة | الوصف | استخدام مثالي |
__getitem__ |
الحصول على القيمة بواسطة المفتاح | obj["key"] |
__setitem__ |
تعيين القيمة بواسطة المفتاح | obj["key"] = value |
__delitem__ |
حذف زوج المفتاح-القيمة | del obj["key"] |
__len__ |
الحصول على عدد أزواج المفتاح-القيمة | len(obj) |
__iter__ |
التكرار عبر المفاتيح | for key in obj: |
__contains__ |
التحقق مما إذا كان المفتاح موجودًا | "key" in obj |
مثال عملي: ذاكرة مخصصة
لنقم بتنفيذ ذاكرة معتمدة على الوقت تنتهي صلاحيتها تلقائيًا. يوضح هذا المثال كيفية إنشاء حاوية مخصصة تتصرف مثل قاموس ولكن بوظائف إضافية:
import time
from collections import OrderedDict
class ExpiringCache:
def __init__(self, max_age_seconds=60):
self.max_age = max_age_seconds
self._cache = OrderedDict() # {المفتاح: (القيمة، الطابع الزمني)}
def __getitem__(self, key):
if key not in self._cache:
raise KeyError(key)
value, timestamp = self._cache[key]
if time.time() - timestamp > self.max_age:
del self._cache[key]
raise KeyError(f"Key '{key}' has expired")
return value
def __setitem__(self, key, value):
self._cache[key] = (value, time.time())
self._cache.move_to_end(key) # انتقل إلى النهاية للحفاظ على ترتيب الإدخال
def __delitem__(self, key):
del self._cache[key]
def __len__(self):
self._clean_expired() # نظف العناصر المنتهية قبل الإبلاغ عن الطول
return len(self._cache)
def __iter__(self):
self._clean_expired() # نظف العناصر المنتهية قبل التكرار
for key in self._cache:
yield key
def __contains__(self, key):
if key not in self._cache:
return False
_, timestamp = self._cache[key]
if time.time() - timestamp > self.max_age:
del self._cache[key]
return False
return True
def _clean_expired(self):
"""Remove all expired entries from the cache."""
now = time.time()
expired_keys = [
key for key, (_, timestamp) in self._cache.items()
if now - timestamp > self.max_age
]
for key in expired_keys:
del self._cache[key]
دعونا نفصل كيف يعمل هذا التخزين المؤقت:
-
التخزين: يستخدم التخزين المؤقت
OrderedDict
لتخزين أزواج المفتاح والقيمة مع الطوابع الزمنية. -
الانتهاء: يتم تخزين كل قيمة كزوج مرتب من
(القيمة، الطابع الزمني)
. عند الوصول إلى قيمة، نتحقق مما إذا كانت قد انتهت صلاحيتها. -
طرق الحاوية: تقوم الفئة بتنفيذ جميع الطرق اللازمة لتتصرف كقاموس:
-
__getitem__
: يسترجع القيم ويتحقق من انتهاء الصلاحية -
__setitem__
: يخزن القيم مع الطابع الزمني الحالي -
__delitem__
: يزيل الإدخالات -
__len__
: يُرجع عدد الإدخالات غير المنتهية -
__iter__
: يتكرر على المفاتيح غير المنتهية -
__contains__
: يتحقق مما إذا كان المفتاح موجودًا
-
إليك كيفية استخدام الذاكرة المؤقتة:
# إنشاء ذاكرة تخزين مؤقتة بانتهاء صلاحية بعد ثانيتين
cache = ExpiringCache(max_age_seconds=2)
# تخزين بعض القيم
cache["name"] = "Vivek"
cache["age"] = 30
# الوصول إلى القيم
print("name" in cache) # الناتج: صحيح
print(cache["name"]) # الناتج: فيفيك
print(len(cache)) # الناتج: 2
# انتظر انتهاء الصلاحية
print("Waiting for expiration...")
time.sleep(3)
# التحقق من القيم المنتهية الصلاحية
print("name" in cache) # الناتج: غير صحيح
try:
print(cache["name"])
except KeyError as e:
print(f"KeyError: {e}") # الناتج: KeyError: 'name'
print(len(cache)) # الناتج: 0
توفر هذه الطريقة لتخزين البيانات المؤقتة عدة فوائد:
-
انتهاء تلقائي للبيانات القديمة
-
واجهة شبيهة بالقواميس لسهولة الاستخدام
-
كفاءة الذاكرة عن طريق إزالة البيانات المنتهية الصلاحية
-
عمليات آمنة للموضوع (بشرط الوصول من خلال خط واحد)
-
الحفاظ على ترتيب الإدخالات
الوصول إلى السمات
تتيح لك طرق الوصول إلى السمات التحكم في كيفية معالجة كائناتك للحصول على السمات وتعيينها وحذفها. هذا مفيد بشكل خاص لتنفيذ الخصائص والتحقق من صحة البيانات وتسجيل الأحداث.
getattr و getattribute
توفر بايثون طريقتين للتحكم في الوصول إلى السمات:
-
__getattr__
: يتم استدعاؤها فقط عندما يفشل البحث عن سمة (أي عندما لا توجد السمة) -
__getattribute__
: يتم استدعاؤها لكل وصول للسمة، حتى بالنسبة للسمات الموجودة
الفرق الرئيسي هو أن __getattribute__
يتم استدعاؤها لكل وصول للسمة، بينما يتم استدعاء __getattr__
فقط عندما لا يتم العثور على السمة من خلال الوسائل العادية.
إليك مثال بسيط يوضح الفرق:
class AttributeDemo:
def __init__(self):
self.name = "Vivek"
def __getattr__(self, name):
print(f"__getattr__ called for {name}")
return f"Default value for {name}"
def __getattribute__(self, name):
print(f"__getattribute__ called for {name}")
return super().__getattribute__(name)
demo = AttributeDemo()
print(demo.name) # المخرج: تم استدعاء __getattribute__ للاسم
# Vivek
print(demo.age) # المخرج: تم استدعاء __getattribute__ للعمر
# تم استدعاء __getattr__ للعمر
# القيمة الافتراضية للعمر
setattr و delattr
وبالمثل، يمكنك التحكم في كيفية تعيين السمات وحذفها:
-
__setattr__
: يتم استدعاؤها عند تعيين سمة -
__delattr__
: يتم استدعاؤها عند حذف سمة
تتيح لك هذه الأساليب تنفيذ التحقق من الصحة وتدقيق السجلات أو السلوك المخصص عند تعديل السمات.
مثال عملي: خصائص التسجيل التلقائي
لنقم بإنشاء فئة تقوم تلقائيًا بتسجيل جميع تغييرات الخاصية. هذا مفيد لأغراض تصحيح الأخطاء والتدقيق وتتبع تغييرات حالة الكائن:
import logging
# إعداد التسجيل
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class LoggedObject:
def __init__(self, **kwargs):
self._data = {}
# تهيئة السمات دون تنشيط __setattr__
for key, value in kwargs.items():
self._data[key] = value
def __getattr__(self, name):
if name in self._data:
logging.debug(f"Accessing attribute {name}: {self._data[name]}")
return self._data[name]
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
def __setattr__(self, name, value):
if name == "_data":
# السماح بتعيين سمة _data مباشرة
super().__setattr__(name, value)
else:
old_value = self._data.get(name, "<undefined>")
self._data[name] = value
logging.info(f"Changed {name}: {old_value} -> {value}")
def __delattr__(self, name):
if name in self._data:
old_value = self._data[name]
del self._data[name]
logging.info(f"Deleted {name} (was: {old_value})")
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
لنقم بتفكيك كيفية عمل هذه الفئة:
-
التخزين: تستخدم الفئة قاموسًا خاصًا
_data
لتخزين قيم السمات. -
الوصول إلى السمات:
-
__getattr__
: يعيد القيم من_data
ويسجل رسائل التصحيح -
__setattr__
: يخزن القيم في_data
ويسجل التغييرات -
__delattr__
: يقوم بإزالة القيم من_data
ويسجل عمليات الحذف
-
-
معالجة خاصة: يتم التعامل مع خاصية
_data
بشكل مختلف لتجنب التكرار اللانهائي.
إليك كيفية استخدام الفئة:
# إنشاء كائن مسجل بقيم أولية
user = LoggedObject(name="Vivek", email="[email protected]")
# تعديل الخصائص
user.name = "Vivek" # السجلات: تم تغيير الاسم: Vivek -> Vivek
user.age = 30 # السجلات: تم تغيير العمر: <غير محدد> -> 30
# الوصول إلى الخصائص
print(user.name) # الناتج: Vivek
# حذف الخصائص
del user.email # السجلات: تم حذف البريد الإلكتروني (كان: [email protected])
# حاول الوصول إلى الخاصية المحذوفة
try:
print(user.email)
except AttributeError as e:
print(f"AttributeError: {e}") # الناتج: AttributeError: لا يحتوي كائن 'LoggedObject' على خاصية 'email'
يوفر هذا التنفيذ العديد من الفوائد:
-
تسجيل تلقائي لجميع تغييرات الخصائص
-
تسجيل على مستوى تصحيح الأخطاء للوصول إلى الخصائص
-
رسائل خطأ واضحة لغياب الخصائص
-
تتبع سهل لتغييرات حالة الكائن
-
مفيد لتصحيح الأخطاء والتدقيق
مديري السياق
تعد مدراء السياقات ميزة قوية في لغة البرمجة Python تساعدك في إدارة الموارد بشكل صحيح. إنها تضمن الحصول على الموارد بشكل صحيح وإطلاقها، حتى إذا حدث خطأ. تعتبر البيانة with
هي الطريقة الأكثر شيوعًا لاستخدام مدراء السياقات.
دخول وخروج
لإنشاء مدير سياق، تحتاج إلى تنفيذ طريقتين سحريتين:
-
__enter__
: تُستدعى عند دخول البيانةwith
. يجب أن تقوم بإرجاع المورد الذي يجب إدارته. -
__exit__
: تُستدعى عند خروج البيانةwith
، حتى إذا حدث استثناء. يجب أن تتعامل مع النظافة.
تتلقى طريقة __exit__
ثلاثة معاملات:
-
exc_type
: نوع الاستثناء (إذا كان هناك) -
exc_val
: نسخة الاستثناء (إذا كان هناك) -
exc_tb
: سجل التتبع (إذا كان هناك)
مثال عملي: مدير اتصال قاعدة بيانات
لنقم بإنشاء مدير سياق لاتصالات قاعدة البيانات. يوضح هذا المثال كيفية إدارة الموارد الخاصة بقاعدة البيانات بشكل صحيح ومعالجة العمليات:
import sqlite3
import logging
# إعداد تسجيل الأحداث
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class DatabaseConnection:
def __init__(self, db_path):
self.db_path = db_path
self.connection = None
self.cursor = None
def __enter__(self):
logging.info(f"Connecting to database: {self.db_path}")
self.connection = sqlite3.connect(self.db_path)
self.cursor = self.connection.cursor()
return self.cursor
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
logging.error(f"An error occurred: {exc_val}")
self.connection.rollback()
logging.info("Transaction rolled back")
else:
self.connection.commit()
logging.info("Transaction committed")
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
logging.info("Database connection closed")
# إرجاع False لنشر الاستثناءات، و True لإخفائها
return False
لنقم بتفكيك كيفية عمل هذا المدير السياق:
-
التهيئة:
-
يستغرق الفئة مسار قاعدة البيانات
-
يقوم بتهيئة الاتصال والمؤشر على أنهما None
-
-
طريقة الدخول:
-
ينشئ اتصالًا بقاعدة البيانات
-
ينشئ مؤشرًا
-
يعيد المؤشر للاستخدام في كتلة
with
-
-
طريقة الخروج:
-
تدير إدارة المعاملات (التأكيد/التراجع)
-
تغلق المؤشر والاتصال
-
تسجيل جميع العمليات
-
تعيد False لنشر الاستثناءات
-
هنا كيفية استخدام مدير السياق:
# أنشئ قاعدة بيانات اختبار في الذاكرة
try:
# معاملة ناجحة
with DatabaseConnection(":memory:") as cursor:
# أنشئ جدولًا
cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
""")
# أدخل البيانات
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Vivek", "[email protected]")
)
# استعلم عن البيانات
cursor.execute("SELECT * FROM users")
print(cursor.fetchall()) # الناتج: [(1 ، 'Vivek' ، '[email protected]')]
# قم بإظهار التراجع في المعاملة في حالة حدوث خطأ
with DatabaseConnection(":memory:") as cursor:
cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
""")
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Wewake", "[email protected]")
)
# هذا سيتسبب في خطأ - الجدول 'nonexistent' غير موجود
cursor.execute("SELECT * FROM nonexistent")
except sqlite3.OperationalError as e:
print(f"Caught exception: {e}")
يوفر مدير السياق هذا عدة فوائد:
-
يتم إدارة الموارد تلقائيًا (على سبيل المثال: يتم إغلاق الاتصالات دائمًا).
-
مع سلامة المعاملات، يتم الالتزام بالتغييرات أو التراجع عنها بشكل مناسب.
-
يتم التقاط الاستثناءات ومعالجتها بشكل سلس
-
يتم تسجيل جميع العمليات لأغراض تصحيح الأخطاء
-
تجعل عبارة
with
الكود واضحًا وموجزًا
الكائنات القابلة للاستدعاء
تتيح لك طريقة السحر __call__
جعل كائنات فئتك تعمل مثل الدوال. هذا مفيد لإنشاء كائنات تحافظ على الحالة بين الاستدعاءات أو لتنفيذ سلوك مشابه للدوال مع ميزات إضافية.
استدعاء
يتم استدعاء طريقة __call__
عندما تحاول استدعاء كائن من فئتك كما لو كان دالة. إليك مثال بسيط:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
# إنشاء كائنات تعمل مثل الدوال
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # المخرجات: 10
print(triple(5)) # المخرجات: 15
يظهر هذا المثال كيف تتيح لك __call__
إنشاء كائنات تحافظ على الحالة (العامل) بينما يمكن استدعاؤها مثل الدوال.
مثال عملي: ديكور الميمو
لنقم بتنفيذ مزين memoization باستخدام __call__
. سيقوم هذا المزين بتخزين نتائج الدالة لتجنب الحسابات الزائدة:
import time
import functools
class Memoize:
def __init__(self, func):
self.func = func
self.cache = {}
# الحفاظ على بيانات الدالة (الاسم، الوثيقة، إلخ)
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
# إنشاء مفتاح من الوسائط
# لأسباب بساطة، نفترض أن جميع الوسائط قابلة للتجزئة
key = str(args) + str(sorted(kwargs.items()))
if key not in self.cache:
self.cache[key] = self.func(*args, **kwargs)
return self.cache[key]
# الاستخدام
@Memoize
def fibonacci(n):
"""Calculate the nth Fibonacci number recursively."""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# قياس وقت التنفيذ
def time_execution(func, *args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}({args}, {kwargs}) took {end - start:.6f} seconds")
return result
# بدون memoization، سيكون هذا بطيئًا للغاية
print("Calculating fibonacci(35)...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")
# الاستدعاء الثاني يكون فوريًا بسبب memoization
print("\nCalculating fibonacci(35) again...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")
لنقم بتفكيك كيف يعمل مزين memoization هذا:
-
التهيئة:
-
يأخذ دالة كوسيط
-
ينشئ قاموس ذاكرة مؤقتة لتخزين النتائج
-
يحافظ على بيانات الدالة باستخدام
functools.update_wrapper
-
-
طريقة الاستدعاء:
-
ينشئ مفتاحًا فريدًا من وسائط الدالة
-
يتحقق مما إذا كانت النتيجة مخزنة في الذاكرة المؤقتة
-
إذا لم تكن مخزنة، يحسب النتيجة ويقوم بتخزينها
-
يعيد النتيجة المخزنة في الذاكرة المؤقتة
-
-
الاستخدام:
-
تطبيقه كزخرفة على أي دالة
-
يخزن النتائج تلقائيًا للاستدعاءات المتكررة
-
يحتفظ ببيانات الدالة وسلوكها
-
من فوائد هذا التنفيذ:
-
أداء أفضل، حيث يتجنب الحسابات الزائدة
-
أفضل، شفافية، حيث يعمل بدون تعديل الوظيفة الأصلية
-
إنه مرن ويمكن استخدامه مع أي وظيفة
-
إنه فعال من حيث استخدام الذاكرة ويخزن النتائج لإعادة الاستخدام
-
إنه يحافظ على وثائق الوظيفة
طرق السحر المتقدمة
لنستكشف الآن بعض طرق السحر المتقدمة في لغة Python. توفر هذه الطرق التحكم الدقيق في إنشاء الكائنات واستخدام الذاكرة وسلوك القاموس.
الطريقة الجديدة لإنشاء الكائنات
تُستدعى الطريقة __new__
قبل __init__
وتكون مسؤولة عن إنشاء وإرجاع نسخة جديدة من الفئة. وهذا مفيد لتنفيذ أنماط مثل الكائنات المفردة أو الكائنات غير القابلة للتغيير.
فيما يلي مثال على نمط الكائن المفرد باستخدام __new__
:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name=None):
# سيتم استدعاء هذا في كل مرة يتم فيها استدعاء Singleton()
if name is not None:
self.name = name
# الاستخدام
s1 = Singleton("Vivek")
s2 = Singleton("Wewake")
print(s1 is s2) # الناتج: True
print(s1.name) # الناتج: Wewake (المرة الثانية للتهيئة قامت بالكتابة فوق الأولى)
لنقم بتفكيك كيف يعمل هذا الكائن المفرد:
-
متغير الفئة:
_instance
يخزن النسخة الوحيدة من الفئة -
طريقة الجديدة:
-
يتحقق مما إذا كانت النسخة موجودة
-
ينشئ واحدة إذا لم تكن موجودة
-
يعيد النسخة الموجودة إذا كانت موجودة
-
-
طريقة init:
-
تُستدعى في كل مرة يتم فيها استخدام المُنشئ
-
تحديث سمات النسخة
-
المنافذ لتحسين الذاكرة
متغير الفئة __slots__
يقيد السمات التي يمكن أن تمتلكها النسخة، مما يوفر الذاكرة. هذا مفيد بشكل خاص عندما يكون لديك العديد من النسخ من فئة تحتوي على مجموعة ثابتة من السمات.
إليك مقارنة بين الفئات العادية والفئات ذات المنافذ:
import sys
class RegularPerson:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
class SlottedPerson:
__slots__ = ['name', 'age', 'email']
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
# قارن استخدام الذاكرة
regular_people = [RegularPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
slotted_people = [SlottedPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
print(f"Regular person size: {sys.getsizeof(regular_people[0])} bytes") # الناتج: حجم الشخص العادي: 48 بايت
print(f"Slotted person size: {sys.getsizeof(slotted_people[0])} bytes") # الناتج: حجم الشخص المفصّل: 56 بايت
print(f"Memory saved per instance: {sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])} bytes") # الناتج: توفير الذاكرة لكل نسخة: -8 بايت
print(f"Total memory saved for 1000 instances: {(sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])) * 1000 / 1024:.2f} KB") # الناتج: إجمالي توفير الذاكرة لـ 1000 نسخة: -7.81 كيلوبايت
تشغيل هذا الكود ينتج نتيجة مثيرة للاهتمام:
Regular person size: 48 bytes
Slotted person size: 56 bytes
Memory saved per instance: -8 bytes
Total memory saved for 1000 instances: -7.81 KB
من المدهش، في هذا المثال البسيط، يكون حجم النسخة المفصلة في الواقع أكبر بـ 8 بايت من النسخة العادية! هذا يبدو يتعارض مع النصيحة الشائعة حول توفير الذاكرة باستخدام __slots__
.
فما الذي يحدث هنا؟ توفير الذاكرة الفعلي من __slots__
يأتي من:
-
التخلص من القواميس: تخزن كائنات Python العادية سماتها في قاموس (
__dict__
)، والذي يحمل تكاليف إضافية. دالةsys.getsizeof()
لا تأخذ في الاعتبار حجم هذا القاموس. -
تخزين السمات: بالنسبة للكائنات الصغيرة ذات عدد قليل من السمات، فإن تكاليف وصفات الفتحات قد تفوق توفير القاموس.
-
قابلية التوسع: الفائدة الحقيقية تظهر عندما:
-
لديك العديد من الكائنات (آلاف أو ملايين)
-
لديك العديد من السمات في كائناتك
-
أنت تضيف سمات بشكل ديناميكي
-
دعنا نرى مقارنة أكثر شمولاً:
# قياس ذاكرة أكثر دقة
import sys
def get_size(obj):
"""Get a better estimate of the object's size in bytes."""
size = sys.getsizeof(obj)
if hasattr(obj, '__dict__'):
size += sys.getsizeof(obj.__dict__)
# إضافة حجم محتويات القاموس
size += sum(sys.getsizeof(v) for v in obj.__dict__.values())
return size
class RegularPerson:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
class SlottedPerson:
__slots__ = ['name', 'age', 'email']
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
regular = RegularPerson("Vivek", 30, "[email protected]")
slotted = SlottedPerson("Vivek", 30, "[email protected]")
print(f"Complete Regular person size: {get_size(regular)} bytes") # الناتج: حجم شخص عادي كامل: 610 بايت
print(f"Complete Slotted person size: {get_size(slotted)} bytes") # الناتج: حجم شخص مقسم كامل: 56 بايت
مع هذا القياس الأكثر دقة، سترى أن الكائنات المقسمة تستخدم عادة ذاكرة أقل بشكل إجمالي، خاصة مع إضافة المزيد من السمات.
النقاط الرئيسية حول __slots__
:
-
فوائد ذاكرة حقيقية: تأتي التوفير الأساسي في الذاكرة من القضاء على
__dict__
الخاصة بالنسخة -
القيود الديناميكية: لا يمكنك إضافة سمات تعسفية إلى الكائنات المدرجة
-
اعتبارات الوراثة: استخدام
__slots__
مع الوراثة يتطلب تخطيطًا دقيقًا -
حالات الاستخدام: الأفضل للفئات ذات العديد من الحالات والسمات الثابتة
-
مكافأة الأداء: يمكن أيضًا توفير وصول أسرع للسمات في بعض الحالات
المفقود لقيم القاموس الافتراضية
يتم استدعاء الطريقة __missing__
بواسطة الفئات الفرعية للقواميس عند عدم العثور على مفتاح. هذا مفيد لتنفيذ القواميس بقيم افتراضية أو إنشاء مفاتيح تلقائيًا.
إليك مثال على قاموس يقوم بإنشاء قوائم فارغة تلقائيًا للمفاتيح المفقودة:
class AutoKeyDict(dict):
def __missing__(self, key):
self[key] = []
return self[key]
# الاستخدام
groups = AutoKeyDict()
groups["team1"].append("Vivek")
groups["team1"].append("Wewake")
groups["team2"].append("Vibha")
print(groups) # الناتج: {'فريق1': ['فيفيك', 'ويويك'], 'فريق2': ['فيبها']}
يوفر هذا التنفيذ العديد من الفوائد:
-
لا حاجة للتحقق من وجود مفتاح، مما يجعل الأمر أكثر راحة.
-
التهيئة التلقائية تنشئ القيم الافتراضية حسب الحاجة.
-
تقلل النص الزائد لتهيئة القاموس.
-
أكثر مرونة ويمكن تنفيذ أي منطق قيم افتراضية.
-
ينشئ القيم فقط عند الحاجة، مما يجعله أكثر كفاءة في الذاكرة.
اعتبارات الأداء
على الرغم من أن الطرق السحرية قوية، إلا أنها يمكن أن تؤثر على الأداء إذا لم تستخدم بعناية. دعونا نستكشف بعض الاعتبارات الشائعة للأداء وكيفية قياسها.
تأثير الطرق السحرية على الأداء
الطرق السحرية المختلفة لها تأثيرات مختلفة على الأداء:
طرق الوصول إلى السمات:
-
تُستدعى
__getattr__
،__getattribute__
،__setattr__
، و__delattr__
بشكل متكرر -
عمليات معقدة في هذه الطرق يمكن أن تبطئ بشكل كبير من أداء الكود
طرق الحاويات:
-
__getitem__
،__setitem__
، و__len__
يتم استدعاؤها في الحلقات بشكل متكرر -
التنفيذ غير الفعال يمكن أن يجعل الحاوية الخاصة بك أبطأ بكثير من أنواع البناء الداخلية
زيادة العمليات:
-
يتم استخدام المشغلات الحسابية والمقارنة بشكل متكرر
-
يمكن أن تجعل التنفيذات المعقدة العمليات البسيطة بطيئة بشكل غير متوقع
لنقيس تأثير الأداء لـ __getattr__
مقابل الوصول المباشر إلى السمات:
import time
class DirectAccess:
def __init__(self):
self.value = 42
class GetAttrAccess:
def __init__(self):
self._value = 42
def __getattr__(self, name):
if name == "value":
return self._value
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
# قياس الأداء
direct = DirectAccess()
getattr_obj = GetAttrAccess()
def benchmark(obj, iterations=1000000):
start = time.time()
for _ in range(iterations):
x = obj.value
end = time.time()
return end - start
direct_time = benchmark(direct)
getattr_time = benchmark(getattr_obj)
print(f"Direct access: {direct_time:.6f} seconds")
print(f"__getattr__ access: {getattr_time:.6f} seconds")
print(f"__getattr__ is {getattr_time / direct_time:.2f}x slower")
تشير هذه الاختبارات الحية إلى اختلافات أداء كبيرة:
Direct access: 0.027714 seconds
__getattr__ access: 0.060646 seconds
__getattr__ is 2.19x slower
كما يمكنك أن ترى، استخدام __getattr__
بطيء أكثر من ضعفين مقارنةً بالوصول المباشر إلى السمات. قد لا يهم هذا في السمات التي يتم الوصول إليها بشكل زائد، ولكنه يمكن أن يصبح مهمًا في الشفرة الحاسوبية ذات الأداء الحرج التي تقوم بالوصول إلى السمات في حلقات ضيقة.
استراتيجيات التحسين
من الحسن حظنا أن هناك طرق مختلفة يمكنك من خلالها تحسين الأساليب السحرية.
-
استخدم الفتحات لكفاءة الذاكرة: يقلل هذا من استخدام الذاكرة ويحسن سرعة الوصول إلى السمات. إنه الأفضل للفئات ذات العديد من الحالات.
-
خزن القيم المحسوبة: يمكنك تخزين نتائج العمليات المكلفة وتحديث التخزين المؤقت فقط عند الضرورة. استخدم
@property
للسمات المحسوبة. -
قلل من استدعاءات الأساليب: تأكد من تجنب استدعاءات الأساليب السحرية غير الضرورية واستخدم الوصول المباشر إلى السمات عند الإمكان. ضع في اعتبارك استخدام
__slots__
للسمات التي يتم الوصول إليها بشكل متكرر.
أفضل الممارسات
عند استخدام الأساليب السحرية، اتبع هذه الممارسات الأفضل لضمان أن يكون الكود الخاص بك قابل للصيانة، فعال، وموثوق.
1. كن متسقًا
عند تنفيذ الأساليب السحرية ذات الصلة، حافظ على الاتساق في السلوك:
from functools import total_ordering
@total_ordering
class ConsistentNumber:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, ConsistentNumber):
return NotImplemented
return self.value == other.value
def __lt__(self, other):
if not isinstance(other, ConsistentNumber):
return NotImplemented
return self.value < other.value
2. ارجع NotImplemented
عندما لا تكون العملية منطقية، ارجع NotImplemented
للسماح لـ Python بمحاولة العملية المعاكسة:
class Money:
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
# ... بقية التنفيذ
3. اجعلها بسيطة
يجب أن تكون الطرق السحرية بسيطة وقابلة للتنبؤ. تجنب المنطق المعقد الذي قد يؤدي إلى سلوك غير متوقع:
# جيد: بسيط وقابل للتنبؤ
class SimpleContainer:
def __init__(self):
self.items = []
def __getitem__(self, index):
return self.items[index]
# سيء: معقد وقد يكون مربكًا
class ComplexContainer:
def __init__(self):
self.items = []
self.access_count = 0
def __getitem__(self, index):
self.access_count += 1
if self.access_count > 100:
raise RuntimeError("Too many accesses")
return self.items[index]
4. توثيق السلوك
قم بتوثيق كيفية عمل الطرق السحرية الخاصة بك بوضوح، خاصة إذا كانت تنحرف عن التوقعات القياسية:
class CustomDict(dict):
def __missing__(self, key):
"""
Called when a key is not found in the dictionary.
Creates a new list for the key and returns it.
This allows for automatic list creation when accessing
non-existent keys.
"""
self[key] = []
return self[key]
5. النظر في الأداء
كن واعيًا لتداعيات الأداء، خاصة بالنسبة للطرق التي يتم استدعاؤها بشكل متكرر:
class OptimizedContainer:
__slots__ = ['items'] # استخدم __slots__ لتحسين الأداء
def __init__(self):
self.items = []
def __getitem__(self, index):
return self.items[index] # الوصول المباشر أسرع
6. التعامل مع الحالات الحدودية
دائمًا اعتبر الحالات الحدودية وتعامل معها بشكل مناسب:
class SafeContainer:
def __getitem__(self, key):
if not isinstance(key, (int, slice)):
raise TypeError("Index must be integer or slice")
if key < 0:
raise ValueError("Index cannot be negative")
# ... بقية التنفيذ
الخلاصة
توفر الطرق السحرية في بايثون وسيلة قوية لجعل الفئات الخاصة بك تتصرف مثل الأنواع المدمجة، مما يمكّن من كتابة كود أكثر حدسية وتعبيرية. على مدار هذا الدليل، استكشفنا كيفية عمل هذه الطرق وكيفية استخدامها بشكل فعال.
النقاط الأساسية
-
تمثيل الكائنات:
-
استخدم
__str__
للإخراج الصديق للمستخدم -
استخدم
__repr__
للتصحيح والتطوير
-
-
تحميل المشغل:
-
قم بتنفيذ العمليات الحسابية وعمليات المقارنة
-
قم بإرجاع
NotImplemented
للعمليات غير المدعومة -
استخدم
@total_ordering
لمقارنات متسقة
-
-
سلوك الحاوية:
-
قم بتنفيذ بروتوكولات التسلسل والتخطيط
-
ضع في اعتبارك الأداء للعمليات المستخدمة بشكل متكرر
-
تعامل مع الحالات الحدية بشكل مناسب
-
-
إدارة الموارد:
-
استخدم مديري السياق للتعامل المناسب مع الموارد
-
قم بتنفيذ
__enter__
و__exit__
للتنظيف -
تعامل مع الاستثناءات في
__exit__
-
-
تحسين الأداء:
-
استخدم
__slots__
لكفاءة الذاكرة -
قم بتخزين القيم المحسوبة عند الاقتضاء
-
قلل من استدعاءات الطرق في الشيفرة المستخدمة بشكل متكرر
-
متى تستخدم طرق السحر
تكون طرق السحر الأكثر فائدة عندما تحتاج إلى:
-
إنشاء هياكل بيانات مخصصة
-
تنفيذ أنواع محددة للنطاق
-
إدارة الموارد بشكل صحيح
-
إضافة سلوك خاص إلى الفئات الخاصة بك
-
جعل رمزك أكثر تقنية بايثون
متى يجب تجنب الأساليب السحرية
تجنب الأساليب السحرية عندما:
-
يكون الوصول البسيط إلى السمات كافياً
-
سيكون السلوك غامضًا أو غير متوقع
-
الأداء هو أمر حاسم وسيتسبب الأساليب السحرية في زيادة العبء
-
سيكون التنفيذ معقدًا جدًا
تذكر أن القوة الكبيرة تأتي مع مسؤولية كبيرة. استخدم الأساليب السحرية بحكمة، مع مراعاة آثار الأداء ومبدأ الأدنى من المفاجآت. عند استخدامها بشكل مناسب، يمكن للأساليب السحرية أن تعزز بشكل كبير قابلية قراءة رمزك وتعبيرية.
المراجع والقراءة الإضافية
الوثائق الرسمية لبايثون
-
نموذج البيانات في بايثون – الوثائق الرسمية – دليل شامل عن نموذج البيانات في بايثون والأساليب السحرية.
-
functools.total_ordering – وثائق لديكوريتور total_ordering الذي يملأ تلقائيا الأساليب المفقودة للمقارنة.
-
أسماء الأساليب الخاصة في بايثون – الإشارة الرسمية لمعرفات الأساليب الخاصة في بايثون.
-
الفصول المجردة للصنف التجميعات – تعرف على الفصول المجردة للحاويات التي تحدد الواجهات التي يمكن لصنف الحاوية الخاص بك تنفيذها.
مصادر المجتمع
- دليل لطرق السحر في لغة بايثون – ريف كتلر – أمثلة عملية للطرق السحرية وحالات الاستخدام الشائعة.
قراءة إضافية
إذا كنت قد استمتعت بهذه المقالة، فقد تجد المقالات المتعلقة بلغة بايثون على مدونتي الشخصية مفيدة:
-
تجارب عملية لتحسين استعلامات Django ORM – تعلم كيفية تحسين استعلامات Django ORM الخاصة بك من خلال أمثلة وتجارب عملية.
-
تكلفة عالية لمعالجة uWSGI المتزامنة – فهم تأثيرات الأداء للمعالجة المتزامنة في uWSGI وكيف يؤثر ذلك على تطبيقات الويب بلغة Python الخاصة بك.
Source:
https://www.freecodecamp.org/news/python-magic-methods-practical-guide/