الكلمة المفتاحية yield: زر الإيقاف المؤقت السحري في بايثون
في البرمجة التقليدية، تعلمنا أن الدالة هي طريق ذو اتجاه واحد: تدخل إليها المعطيات، وتخرج منها نتيجة واحدة باستخدام return، ثم تنتهي الدالة وتختفي من الذاكرة. لكن بايثون تقدم لنا مفهوماً أكثر ذكاءً ومرونة من خلال الكلمة المفتاحية yield. هذه الكلمة ليست مجرد بديل لـ return، بل هي أداة تسمح للدالة بأن "تتوقف مؤقتاً" وتُسلمك قيمة، ثم "تستيقظ" لاحقاً لتكمل عملها من حيث توقفت بالضبط. في هذا الدرس، سنغوص في أعماق yield لنفهم كيف تغير قواعد اللعبة في التعامل مع البيانات.
1. تعريف: ما هي yield وكيف تختلف عن return؟
yield هي كلمة مفتاحية في بايثون تُستخدم داخل الدوال لتحويلها إلى مولدات (Generators). عندما تستخدم yield بدلاً من return، فإنك تخبر بايثون: "لا تنهِ الدالة، بل أوقفها مؤقتاً واحفظ حالتها".
الفكرة الأساسية: بينما return تُنهي الدالة وتُعيد قيمة واحدة، yield تُوقف الدالة مؤقتاً وتُعيد قيمة، لكن الدالة تبقى "حية" في الذاكرة منتظرة الاستدعاء التالي.
مثال بسيط للتوضيح:
# دالة بـ return - تنتهي فوراً
def with_return():
print("بدأت الدالة")
return 1
print("هذا السطر لن يُنفذ أبداً!") # لن يُطبع
result = with_return()
print(f"النتيجة: {result}")
print(f"النوع: {type(result)}")
print("\n" + "="*40 + "\n")
# دالة بـ yield - تتحول لمولد
def with_yield():
print("بدأت الدالة")
yield 1
print("استيقظت الدالة!")
yield 2
print("انتهت الدالة")
gen = with_yield()
print(f"النوع: {type(gen)}") #
print(f"القيمة الأولى: {next(gen)}")
print(f"القيمة الثانية: {next(gen)}")
شرح الكود سطراً بسطر:
- الأسطر 2-5: دالة عادية بـ
return. السطر بعدreturnلن يُنفذ أبداً. - السطر 7: استدعاء الدالة يُنفذها كاملة ويُرجع القيمة فوراً.
- الأسطر 14-19: دالة بـ
yield. لاحظ وجودyieldمرتين. - السطر 21: استدعاء الدالة لا يُنفذها! بل يُنشئ كائن مولد.
- السطر 23: أول
next()يُنفذ الدالة حتى أولyield. - السطر 24: ثاني
next()يستأنف من بعد أولyield.
2. الفرق الجوهري: yield مقابل return
لفهم yield، يجب أن نقارنها بـ return. الفرق ليس في النتيجة فقط، بل في دورة حياة الدالة بالكامل.
| وجه المقارنة | الكلمة return | الكلمة yield |
|---|---|---|
| الاستمرارية | تنهي الدالة تماماً وتخرج منها. | توقف الدالة مؤقتاً وتحفظ مكانها. |
| الذاكرة | تُحذف جميع المتغيرات المحلية فوراً. | تحتفظ بجميع المتغيرات المحلية وحالتها. |
| عدد النتائج | تعيد قيمة واحدة (أو مجموعة واحدة). | يمكنها إعادة سلسلة من القيم (واحدة تلو الأخرى). |
| نقطة البداية | تبدأ دائماً من السطر الأول عند كل استدعاء. | تستأنف من السطر الذي يلي yield مباشرة. |
| نوع النتيجة | تُرجع القيمة مباشرة. | تُرجع كائن Generator. |
| الاستخدام | للحسابات العادية. | لتوليد سلاسل بيانات كبيرة أو لا نهائية. |
مثال مقارنة عملي:
# بـ return - تُنشئ قائمة كاملة في الذاكرة
def squares_with_return(n):
result = []
for i in range(n):
result.append(i ** 2)
return result # تُرجع القائمة كاملة
# بـ yield - تُنتج قيمة واحدة في كل مرة
def squares_with_yield(n):
for i in range(n):
yield i ** 2 # تُرجع قيمة واحدة وتتوقف
# الاستخدام
list_result = squares_with_return(5)
print(f"return: {list_result}") # [0, 1, 4, 9, 16]
print(f"النوع: {type(list_result)}") #
gen_result = squares_with_yield(5)
print(f"\nyield: {gen_result}") #
print(f"النوع: {type(gen_result)}") #
print(f"القيم: {list(gen_result)}") # [0, 1, 4, 9, 16]
3. كيف تعمل yield من الداخل؟ (تتبع الحالة)
عندما يرى مفسر بايثون كلمة yield داخل دالة، فإنه لا ينفذها كدالة عادية، بل يحولها إلى Generator Object. هذا الكائن يمتلك دالة داخلية تسمى __next__() هي المسؤولة عن إيقاظ الدالة.
آلية العمل خطوة بخطوة:
- الاستدعاء الأول: عند استدعاء الدالة، لا يُنفذ أي كود! بل يُنشأ كائن مولد فقط.
- أول next(): يبدأ تنفيذ الدالة من السطر الأول حتى يصل لأول
yield، يُرجع القيمة ويتجمد. - ثاني next(): يستأنف من السطر التالي لآخر
yieldحتى يصل للـyieldالتالية. - النهاية: عندما تنتهي الدالة (لا توجد
yieldأخرى)، يُرمى استثناءStopIteration.
def step_by_step():
print("1. بدأت الدالة عملها...")
yield "الخطوة الأولى"
print("2. استيقظت الدالة لتكمل...")
yield "الخطوة الثانية"
print("3. الدالة في مرحلتها الأخيرة...")
yield "الخطوة الثالثة"
print("4. انتهت الدالة تماماً")
# إنشاء المولد (لم يُنفذ أي كود بعد!)
gen = step_by_step()
print(f"أنشأنا المولد: {gen}\n")
# الآن نبدأ بطلب القيم
print(f"استلمنا: {next(gen)}")
print("-" * 40)
print(f"استلمنا: {next(gen)}")
print("-" * 40)
print(f"استلمنا: {next(gen)}")
print("-" * 40)
# محاولة طلب قيمة رابعة
try:
print(f"استلمنا: {next(gen)}")
except StopIteration:
print("❌ لا توجد قيم أخرى - المولد انتهى!")
شرح الكود سطراً بسطر:
- السطر 14: استدعاء
step_by_step()لا يُنفذ الدالة! بل يُنشئ كائن مولد فقط. - السطر 18: أول
next()يبدأ التنفيذ من السطر 2 حتى السطر 3 (أول yield). - السطر 20: ثاني
next()يستأنف من السطر 5 حتى السطر 6. - السطر 22: ثالث
next()يستأنف من السطر 8 حتى السطر 9. - السطر 27: رابع
next()يطبع السطر 11 ثم يرميStopIteration.
لاحظ جيداً: الجمل المطبوعة (print) لم تُنفذ كلها مرة واحدة، بل نُفذت بالتدريج مع كل استدعاء لـ next(). هذا هو سحر الحفاظ على الحالة (State Preservation).
4. استخدام yield داخل الحلقات (النمط الأكثر شيوعاً)
القوة الحقيقية لـ yield تظهر عند استخدامها داخل حلقة for أو while لإنتاج بيانات بناءً على خوارزمية معينة.
مثال 1: مولد متسلسلة فيبوناتشي (Fibonacci):
def fibonacci(limit):
"""مولد أرقام فيبوناتشي"""
a, b = 0, 1
count = 0
while count < limit:
yield a # أرجع القيمة الحالية
a, b = b, a + b # احسب القيمة التالية
count += 1
# طباعة أول 10 أرقام من متسلسلة فيبوناتشي
print("أول 10 أرقام فيبوناتشي:")
for num in fibonacci(10):
print(num, end=" ")
print("\n\nأول 15 رقم:")
fib_gen = fibonacci(15)
print(list(fib_gen))
مثال 2: مولد الأعداد الأولية:
def prime_numbers(max_num):
"""مولد الأعداد الأولية حتى max_num"""
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
for num in range(2, max_num + 1):
if is_prime(num):
yield num
# استخدام
print("الأعداد الأولية حتى 30:")
for prime in prime_numbers(30):
print(prime, end=" ")
# النتيجة: 2 3 5 7 11 13 17 19 23 29
مثال 3: مولد نطاقات مخصصة:
def custom_range(start, end, step=1):
"""مولد نطاق مخصص مثل range لكن بمزيد من المرونة"""
current = start
if step > 0:
while current < end:
yield current
current += step
else:
while current > end:
yield current
current += step
# أمثلة
print("من 0 إلى 10 بخطوة 2:")
print(list(custom_range(0, 10, 2))) # [0, 2, 4, 6, 8]
print("\nمن 10 إلى 0 بخطوة -1:")
print(list(custom_range(10, 0, -1))) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
5. تطبيقات عملية متقدمة
أ) معالجة تدفق البيانات (Data Streaming):
تخيل أنك تراقب حساسات حرارة ترسل بيانات كل ثانية. لا تريد تخزين كل البيانات، بل تريد معالجة كل قراءة فور وصولها.
import random
import time
def temperature_sensor(duration=10):
"""محاكاة حساس حرارة يرسل قراءات"""
start_time = time.time()
while time.time() - start_time < duration:
# محاكاة قراءة حساس الحرارة
temp = random.uniform(20.0, 35.0)
timestamp = time.strftime("%H:%M:%S")
yield (timestamp, round(temp, 2))
time.sleep(1) # انتظر ثانية قبل القراءة التالية
# الاستخدام
print("مراقبة الحرارة لمدة 5 ثواني:")
for timestamp, temp in temperature_sensor(5):
if temp > 30:
print(f"⚠️ [{timestamp}] تحذير: حرارة عالية! {temp}°C")
else:
print(f"✅ [{timestamp}] الحرارة طبيعية: {temp}°C")
ب) قراءة ملفات ضخمة سطراً بسطر:
def read_large_file(file_path, chunk_size=1024):
"""قراءة ملف كبير بأجزاء صغيرة"""
with open(file_path, 'r', encoding='utf-8') as file:
while True:
chunk = file.read(chunk_size)
if not chunk:
break
yield chunk
# استخدام
# for chunk in read_large_file("huge_file.txt"):
# process(chunk) # معالجة كل جزء على حدة
def read_lines_with_numbers(file_path):
"""قراءة ملف مع ترقيم الأسطر"""
with open(file_path, 'r', encoding='utf-8') as file:
line_num = 1
for line in file:
yield (line_num, line.strip())
line_num += 1
# استخدام
# for num, line in read_lines_with_numbers("data.txt"):
# print(f"{num}: {line}")
ج) Pipeline معالجة البيانات:
def read_numbers():
"""قراءة أرقام من مصدر"""
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for num in numbers:
yield num
def filter_even(numbers):
"""تصفية الأرقام الزوجية"""
for num in numbers:
if num % 2 == 0:
yield num
def square(numbers):
"""تربيع الأرقام"""
for num in numbers:
yield num ** 2
def add_prefix(numbers):
"""إضافة بادئة"""
for num in numbers:
yield f"Result: {num}"
# Pipeline: قراءة → تصفية → تربيع → تنسيق
pipeline = add_prefix(square(filter_even(read_numbers())))
print("نتائج Pipeline:")
for result in pipeline:
print(result)
# النتيجة:
# Result: 4
# Result: 16
# Result: 36
# Result: 64
# Result: 100
6. ميزات متقدمة لـ yield (send و close و throw)
هل تعلم أن yield يمكنها أيضاً استقبال بيانات من الخارج؟ المولدات في بايثون هي اتصال ثنائي الاتجاه!
أ) استخدام send() لإرسال قيم للمولد:
def interactive_gen():
print("بدأ المولد...")
# yield يمكنها استقبال قيمة
val = yield "أعطني قيمة"
print(f"استلمت من الخارج: {val}")
val2 = yield f"شكراً على {val}، أعطني قيمة أخرى"
print(f"استلمت القيمة الثانية: {val2}")
yield f"النتيجة النهائية: {val + val2}"
# الاستخدام
g = interactive_gen()
# يجب استدعاء next() أولاً لبدء المولد
print(next(g)) # "أعطني قيمة"
# الآن يمكننا إرسال قيمة
print(g.send(100)) # يرسل 100 ويطبع: "شكراً على 100، أعطني قيمة أخرى"
# إرسال قيمة ثانية
print(g.send(50)) # يرسل 50 ويطبع: "النتيجة النهائية: 150"
ب) استخدام close() لإنهاء المولد:
def infinite_counter():
count = 0
while True:
yield count
count += 1
counter = infinite_counter()
print(next(counter)) # 0
print(next(counter)) # 1
print(next(counter)) # 2
# إنهاء المولد قسرياً
counter.close()
# محاولة استخدامه بعد الإغلاق
try:
print(next(counter))
except StopIteration:
print("المولد مُغلق!")
ج) استخدام throw() لرمي استثناء داخل المولد:
def error_handler():
try:
while True:
val = yield
print(f"استلمت: {val}")
except ValueError as e:
print(f"حدث خطأ: {e}")
yield "تم معالجة الخطأ"
gen = error_handler()
next(gen) # بدء المولد
gen.send(10) # استلمت: 10
gen.send(20) # استلمت: 20
# رمي استثناء
result = gen.throw(ValueError, "قيمة غير صحيحة!")
print(result) # تم معالجة الخطأ
7. نصائح وأفضل الممارسات
- استخدم yield للبيانات الكبيرة: عندما تكون البيانات ضخمة ولا تريد تحميلها كلها في الذاكرة.
- استخدم return للبيانات الصغيرة: إذا كانت البيانات صغيرة ويمكن تحميلها بسهولة.
- لا تخلط yield و return: يمكنك استخدام
returnفي نهاية المولد لإنهائه، لكن لا تُرجع قيمة معها (أو استخدمreturn valueفي Python 3.3+). - استخدم yield from: لتفويض التوليد لمولد آخر (سنتعلمها لاحقاً).
- تذكر أن المولدات تُستنفد: بعد الاستخدام، يجب إنشاء مولد جديد.
ملخص الدرس
- yield: تحول الدالة العادية إلى مولد (Generator).
- تسمح للدالة بإنتاج سلسلة من القيم مع الحفاظ على حالتها الداخلية.
- توفر الذاكرة بشكل هائل لأنها لا تحسب كل شيء مسبقاً (Lazy Evaluation).
- يمكنها استقبال قيم من الخارج باستخدام
send(). - يمكن إنهاؤها قسرياً باستخدام
close(). - تعتبر حجر الزاوية في البرمجة غير المتزامنة (Asynchronous Programming) والتعامل مع البيانات الضخمة.
- تذكر: بمجرد استنفاد المولد، لا يمكن إعادة استخدامه إلا بإنشاء كائن جديد.
🎉 تهانينا! لقد أتممت قمة البرمجة الوظيفية
أنت الآن تمتلك أدوات المبرمجين المحترفين. الخطوة الكبيرة القادمة هي الدخول إلى عالم البرمجة كائنية التوجه (OOP) لبناء أنظمة برمجية متكاملة.
الدرس التالي: البرمجة كائنية التوجه (OOP)