المولدات (Generators) والمُكرّرات (Iterators): معالجة البيانات الضخمة بدون استهلاك الذاكرة
ماذا سنتعلم اليوم؟
سنتعلم كيفية استخدام المولدات (Generators) والمُكرّرات (Iterators) في بايثون لمعالجة مجموعات البيانات الكبيرة بكفاءة عالية، وتجنب استهلاك الذاكرة المرتفع الذي قد يؤدي إلى تعطل التطبيقات.
الخطوة 1: فهم المُكرّرات (Iterators)
المُكرّر هو كائن يمثل تدفقًا من البيانات. يمكننا المرور عبر عناصر المُكرّر عنصرًا تلو الآخر. في بايثون، أي كائن يدعم بروتوكول التكرار (أي لديه الميثودين الخاصين __iter__() و __next__()) هو مُكرّر.
ملاحظة تقنية: الميثود
__iter__()تعيد الكائن نفسه (أو مُكرّر جديد)، بينما الميثود__next__()تعيد العنصر التالي في التسلسل. عندما لا توجد عناصر أخرى، ترفع استثناءStopIteration.
لنقم بإنشاء مُكرّر بسيط خاص بنا:
class MyRangeIterator:
def __init__(self, start, end):
self.current = start # بداية النطاق الحالي
self.end = end # نهاية النطاق
def __iter__(self):
return self # المُكرّر يعيد نفسه
def __next__(self):
if self.current < self.end:
num = self.current # حفظ القيمة الحالية
self.current += 1 # الانتقال إلى العنصر التالي
return num # إرجاع القيمة المحفوظة
raise StopIteration # رفع استثناء عند انتهاء العناصر
# استخدام المُكرّر
print("تجربة المُكرّر اليدوي:")
for num in MyRangeIterator(1, 5):
print(num)
الخطوة 2: تقديم المولدات (Generators)
المولدات هي طريقة أبسط وأكثر إيجازًا لإنشاء المُكرّرات. بدلاً من تعريف فئة كاملة مع __iter__() و __next__()، يمكنك ببساطة تعريف دالة تستخدم الكلمة المفتاحية yield.
ملاحظة تقنية: عندما يتم استدعاء دالة مولد، فإنها لا تنفذ الكود على الفور. بدلاً من ذلك، تعيد كائن مولد (وهو في حد ذاته مُكرّر). يتم تنفيذ الكود فقط عند طلب عنصر باستخدام
next()أو التكرار عليه في حلقةfor. تتوقف الدالة مؤقتًا عندyieldوتحفظ حالتها، وتستأنف من تلك النقطة في الاستدعاء التالي.
لنقم بتحويل مثالنا السابق إلى دالة مولد:
def my_range_generator(start, end):
current = start # بداية النطاق
while current < end:
yield current # إرجاع القيمة الحالية وتجميد تنفيذ الدالة
current += 1 # استئناف التنفيذ من هنا في الاستدعاء التالي
# استخدام دالة المولد
print("\nتجربة دالة المولد:")
for num in my_range_generator(1, 5):
print(num)
الخطوة 3: معالجة البيانات الضخمة بكفاءة
تكمن القوة الحقيقية للمولدات في معالجة البيانات التي قد تكون كبيرة جدًا بحيث لا تتناسب مع الذاكرة (مثل قراءة ملفات ضخمة جدًا سطرًا بسطر). بدلاً من تحميل الملف بأكمله، يمكننا استخدام مولد لقراءة سطر واحد في كل مرة.
لنقم بمحاكاة قراءة ملف كبير:
# لنقم بإنشاء ملف وهمي كبير لأغراض الاختبار
with open("large_data.txt", "w") as f:
for i in range(1, 1000001):
f.write(f"هذا هو السطر رقم {i}\n")
def read_large_file_generator(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f: # التكرار على الملف سطرًا بسطر (وهو في حد ذاته مولد)
yield line.strip() # إرجاع السطر بعد إزالة المسافات البيضاء والأسطر الجديدة
# استخدام المولد لقراءة الملف
print("\nقراءة الأسطر الأولى من ملف كبير باستخدام مولد:")
line_count = 0
for line in read_large_file_generator("large_data.txt"):
print(line)
line_count += 1
if line_count >= 5: # طباعة أول 5 أسطر فقط لتجنب الإخراج الطويل
break
print(f"... تم قراءة {line_count} أسطر حتى الآن (يمكن متابعة القراءة عند الحاجة).")
الكود النهائي الكامل
هذا هو السكربت الكامل الذي يجمع كل ما تعلمناه:
# class MyRangeIterator:
# def __init__(self, start, end):
# self.current = start
# self.end = end
# def __iter__(self):
# return self
# def __next__(self):
# if self.current < self.end:
# num = self.current
# self.current += 1
# return num
# raise StopIteration
# print("تجربة المُكرّر اليدوي:")
# for num in MyRangeIterator(1, 5):
# print(num)
def my_range_generator(start, end):
"""دالة مولد بسيطة تحاكي 'range'"""
current = start # بداية النطاق
while current < end:
yield current # إرجاع القيمة الحالية وتجميد تنفيذ الدالة
current += 1 # استئناف التنفيذ من هنا في الاستدعاء التالي
# استخدام دالة المولد
print("تجربة دالة المولد:")
for num in my_range_generator(1, 5):
print(num)
# لنقم بإنشاء ملف وهمي كبير لأغراض الاختبار
# هذا الجزء سيقوم بإنشاء ملف بحجم حوالي 20 ميجابايت (1,000,000 سطر)
# يمكن حذفه إذا كان الملف موجودًا بالفعل أو لأغراض الاختبار السريع
print("\nإنشاء ملف large_data.txt (مليون سطر) لأغراض التجربة...")
with open("large_data.txt", "w", encoding='utf-8') as f:
for i in range(1, 1000001):
f.write(f"هذا هو السطر رقم {i}\n")
print("تم إنشاء الملف بنجاح.")
def read_large_file_generator(file_path):
"""مولد لقراءة ملف كبير سطرًا بسطر لتوفير الذاكرة"""
with open(file_path, 'r', encoding='utf-8') as f:
for line in f: # التكرار على الملف سطرًا بسطر (وهو في حد ذاته مولد)
yield line.strip() # إرجاع السطر بعد إزالة المسافات البيضاء والأسطر الجديدة
# استخدام المولد لقراءة الملف
print("\nقراءة الأسطر الأولى من ملف كبير باستخدام مولد:")
line_count = 0
for line in read_large_file_generator("large_data.txt"):
print(line)
line_count += 1
if line_count >= 5: # طباعة أول 5 أسطر فقط لتجنب الإخراج الطويل
break
print(f"... تم قراءة {line_count} أسطر من الملف (يمكن متابعة القراءة عند الحاجة).")
# مثال على استخدام المولد في عملية معالجة بيانات حقيقية
# حساب مجموع الأرقام الزوجية في نطاق كبير جدًا دون تخزينها كلها
def even_numbers_generator(limit):
num = 0
while num < limit:
if num % 2 == 0:
yield num
num += 1
print("\nحساب مجموع الأرقام الزوجية حتى 1000000:")
total_even = 0
for even_num in even_numbers_generator(1000001):
total_even += even_num
print(f"مجموع الأرقام الزوجية هو: {total_even}")
النتيجة المتوقعة
عند تشغيل السكربت، ستلاحظ أولاً إنشاء ملف باسم large_data.txt. بعد ذلك، سيتم طباعة الآتي على الشاشة:
تجربة دالة المولد: 1 2 3 4 إنشاء ملف large_data.txt (مليون سطر) لأغراض التجربة... تم إنشاء الملف بنجاح. قراءة الأسطر الأولى من ملف كبير باستخدام مولد: هذا هو السطر رقم 1 هذا هو السطر رقم 2 هذا هو السطر رقم 3 هذا هو السطر رقم 4 هذا هو السطر رقم 5 ... تم قراءة 5 أسطر من الملف (يمكن متابعة القراءة عند الحاجة). حساب مجموع الأرقام الزوجية حتى 1000000: مجموع الأرقام الزوجية هو: 500000500000
توضح هذه النتيجة كيف يمكن للمولدات معالجة أجزاء فقط من البيانات الكبيرة عند الحاجة، مما يحافظ على الذاكرة ويجعل التطبيقات أكثر كفاءة واستجابة.