الدالة reduce: فن تحويل المجموعات الضخمة إلى قيمة واحدة ذهبية
في الدروس السابقة، تعلمنا كيف نحول كل عنصر (map) وكيف نختار عناصر معينة (filter). لكن ماذا لو أردنا دمج كل عناصر المجموعة في نتيجة واحدة نهائية؟ مثلاً: حساب حاصل ضرب كل الأرقام، أو دمج قائمة كلمات في جملة واحدة، أو إيجاد القيمة العظمى بطريقة تراكمية. هنا يأتي دور الدالة الأسطورية reduce(). هي الأداة التي تقوم بـ "تقليص" أو "اختزال" المجموعة الضخمة إلى قيمة واحدة مركزة، وهي ركيزة أساسية في معالجة البيانات المتقدمة.
1. تعريف: ما هي دالة reduce وكيف تختلف عن غيرها؟
دالة reduce() تنتمي لمكتبة functools في بايثون 3. على عكس map() و filter() التي تعمل على كل عنصر بشكل مستقل، reduce() تعمل على زوج من العناصر في كل مرة، وتحتفظ بنتيجة تراكمية (Accumulator).
الفرق الجوهري:
- map(): تحول N عنصر إلى N عنصر جديد (نفس العدد).
- filter(): تختار بعض العناصر من N (عدد أقل أو مساوٍ).
- reduce(): تدمج N عنصر في قيمة واحدة فقط (1).
آلية العمل التراكمية (خطوة بخطوة):
- تأخذ أول عنصرين من المجموعة وتطبق الدالة عليهما → تحصل على نتيجة مؤقتة.
- تأخذ النتيجة المؤقتة (المراكم - Accumulator) وتطبق الدالة معها والعنصر الثالث → نتيجة جديدة.
- تستمر في أخذ "النتيجة الحالية" مع "العنصر التالي" حتى تنتهي المجموعة تماماً.
- تُرجع النتيجة النهائية الوحيدة.
الصيغة العامة (Syntax):
from functools import reduce
reduce(function, iterable[, initializer])
- function: دالة تستقبل معاملين: (المراكم/النتيجة الحالية، العنصر التالي). يجب أن تُرجع قيمة واحدة تصبح المراكم الجديد.
- iterable: المجموعة التي تريد تقليصها (قائمة، tuple، إلخ).
- initializer (اختياري): قيمة ابتدائية يبدأ بها الحساب. إذا لم تُحدد، يُستخدم أول عنصرين من المجموعة.
reduce() دالة مدمجة (built-in). في بايثون 3، تم نقلها إلى functools لأن Guido van Rossum (مبتكر بايثون) يفضل استخدام حلقات for الواضحة بدلاً من reduce() المعقدة أحياناً.
2. فهم آلية العمل: أمثلة توضيحية خطوة بخطوة
أ) المجموع التراكمي (لفهم المنطق):
لنبدأ بمثال بسيط يوضح كيف تعمل reduce() داخلياً.
from functools import reduce
def add(x, y):
"""دالة جمع مع طباعة لفهم الخطوات"""
print(f"نجمع {x} + {y} = {x + y}")
return x + y
numbers = [1, 2, 3, 4]
# العملية التراكمية: ((1+2)+3)+4
result = reduce(add, numbers)
print(f"\nالنتيجة النهائية: {result}")
# نفس النتيجة بطريقة تقليدية للمقارنة
total = 0
for num in numbers:
total += num
print(f"بالحلقة التقليدية: {total}")
شرح الكود سطراً بسطر:
- السطر 1: نستورد
reduceمنfunctools. - الأسطر 3-6: نعرّف دالة
addتستقبل معاملين وتطبعهما لنرى الخطوات. - السطر 8: قائمة أرقام للتجربة.
- السطر 11: نستخدم
reduce(add, numbers). الخطوات:- الخطوة 1:
add(1, 2)→ 3 - الخطوة 2:
add(3, 3)→ 6 - الخطوة 3:
add(6, 4)→ 10
- الخطوة 1:
- الأسطر 16-19: نفس النتيجة بحلقة تقليدية للمقارنة.
ب) استخدام القيمة الابتدائية (Initializer):
إذا وضعت قيمة ابتدائية، ستبدأ reduce بها كأول معامل (المراكم الأولي).
from functools import reduce
nums = [1, 2, 3]
# بدون قيمة ابتدائية
result1 = reduce(lambda x, y: x + y, nums)
print(f"بدون initializer: {result1}") # 6
# مع قيمة ابتدائية 100
# العملية: ((100 + 1) + 2) + 3
result2 = reduce(lambda x, y: x + y, nums, 100)
print(f"مع initializer=100: {result2}") # 106
# مثال عملي: إضافة رسوم ثابتة
prices = [50, 75, 100]
# نضيف رسوم شحن 20 ريال
total_with_shipping = reduce(lambda x, y: x + y, prices, 20)
print(f"\nإجمالي الأسعار + الشحن: {total_with_shipping} ريال")
شرح الكود سطراً بسطر:
- السطر 6: بدون initializer، تبدأ بأول عنصرين (1 و 2).
- السطر 11: مع
100كـ initializer، تبدأ بـ (100 و 1)، ثم (101 و 2)، ثم (103 و 3). - الأسطر 15-17: مثال عملي: نجمع الأسعار ونضيف رسوم شحن ثابتة.
ج) فهم الفرق بين وجود وعدم وجود Initializer:
from functools import reduce
# قائمة فارغة - ماذا يحدث؟
empty = []
# بدون initializer - سيحدث خطأ!
try:
result = reduce(lambda x, y: x + y, empty)
except TypeError as e:
print(f"خطأ: {e}")
# مع initializer - يعمل بشكل صحيح
result = reduce(lambda x, y: x + y, empty, 0)
print(f"مع initializer: {result}") # 0
3. تطبيقات عملية متقدمة
أ) حساب المضروب (Factorial):
حساب حاصل ضرب كل الأرقام في قائمة هو أحد أشهر استخدامات reduce.
from functools import reduce
def factorial(n):
"""حساب المضروب باستخدام reduce"""
if n == 0 or n == 1:
return 1
numbers = range(1, n + 1)
return reduce(lambda x, y: x * y, numbers)
# أمثلة
print(f"5! = {factorial(5)}") # 120
print(f"7! = {factorial(7)}") # 5040
print(f"10! = {factorial(10)}") # 3628800
# مثال مباشر
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, numbers)
print(f"\nحاصل ضرب {numbers} = {product}")
ب) إيجاد القيمة العظمى والصغرى (بدون دوال مدمجة):
from functools import reduce
data = [12, 45, 2, 89, 34, 67, 23]
# إيجاد الأكبر
max_val = reduce(lambda a, b: a if a > b else b, data)
print(f"أكبر قيمة: {max_val}") # 89
# إيجاد الأصغر
min_val = reduce(lambda a, b: a if a < b else b, data)
print(f"أصغر قيمة: {min_val}") # 2
# مقارنة مع الدوال المدمجة
print(f"\nباستخدام max(): {max(data)}")
print(f"باستخدام min(): {min(data)}")
ج) دمج القوائم المتداخلة (Flatten):
from functools import reduce
# قوائم متداخلة
nested = [[1, 2], [3, 4], [5, 6], [7, 8]]
# دمجها في قائمة واحدة
flattened = reduce(lambda acc, lst: acc + lst, nested)
print(f"القوائم المتداخلة: {nested}")
print(f"بعد الدمج: {flattened}")
# مع initializer
nested2 = [[10, 20], [30, 40]]
flattened2 = reduce(lambda acc, lst: acc + lst, nested2, [])
print(f"\nمع initializer: {flattened2}")
د) بناء قاموس من قوائم:
from functools import reduce
# قائمة من tuples (key, value)
pairs = [("name", "أحمد"), ("age", 25), ("city", "الرياض")]
# تحويلها لقاموس
dictionary = reduce(
lambda acc, pair: {**acc, pair[0]: pair[1]},
pairs,
{}
)
print(f"القاموس الناتج: {dictionary}")
4. متى تستخدم reduce ومتى تتجنبها؟
رغم قوتها، إلا أن reduce قد تجعل الكود صعب القراءة إذا أسيء استخدامها. إليك دليل عملي:
| الموقف | استخدم reduce | استخدم البديل |
|---|---|---|
| حساب المجموع | ❌ | ✅ sum() |
| إيجاد الأكبر/الأصغر | ❌ | ✅ max() / min() |
| دمج النصوص | ❌ | ✅ "".join() |
| حاصل الضرب التراكمي | ✅ | أو حلقة for واضحة |
| عمليات تراكمية مخصصة | ✅ | - |
أمثلة على البدائل الأفضل:
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# ❌ سيء - استخدام reduce للمجموع
bad = reduce(lambda x, y: x + y, numbers)
# ✅ جيد - استخدام sum المدمجة
good = sum(numbers)
print(f"reduce: {bad}")
print(f"sum: {good}")
# ❌ سيء - دمج نصوص بـ reduce
words = ["مرحبا", "بك", "في", "بايثون"]
bad_join = reduce(lambda x, y: x + " " + y, words)
# ✅ جيد - استخدام join
good_join = " ".join(words)
print(f"\nreduce: {bad_join}")
print(f"join: {good_join}")
reduce() أبداً. إنها تجعل الكود أقل وضوحاً". لهذا السبب تم نقلها من الدوال الأساسية إلى functools في بايثون 3. استخدمها فقط عندما تكون الخيار الأوضح.
5. تطبيق عملي شامل: معالجة البيانات المالية
تخيل أن لديك قائمة من القواميس تمثل حركات مالية (إيداعات وسحوبات)، وتريد حساب الرصيد النهائي.
from functools import reduce
transactions = [
{"type": "deposit", "amount": 1000, "desc": "راتب"},
{"type": "withdrawal", "amount": 200, "desc": "فواتير"},
{"type": "deposit", "amount": 500, "desc": "مكافأة"},
{"type": "withdrawal", "amount": 100, "desc": "تسوق"},
{"type": "deposit", "amount": 300, "desc": "هدية"}
]
def update_balance(current_balance, transaction):
"""تحديث الرصيد بناءً على نوع العملية"""
amount = transaction["amount"]
if transaction["type"] == "deposit":
new_balance = current_balance + amount
print(f"💰 إيداع {amount} ريال ({transaction['desc']}) → الرصيد: {new_balance}")
else:
new_balance = current_balance - amount
print(f"💸 سحب {amount} ريال ({transaction['desc']}) → الرصيد: {new_balance}")
return new_balance
# نبدأ برصيد ابتدائي 0
print("--- سجل الحركات المالية ---")
final_balance = reduce(update_balance, transactions, 0)
print(f"\n✅ الرصيد النهائي في الحساب: {final_balance} ريال")
شرح الكود سطراً بسطر:
- الأسطر 3-9: قائمة من القواميس، كل قاموس يمثل عملية مالية.
- السطر 11: نعرّف دالة
update_balanceتستقبل الرصيد الحالي والعملية. - الأسطر 13-19: نفحص نوع العملية ونحدث الرصيد وفقاً لذلك، مع طباعة تفاصيل كل خطوة.
- السطر 24: نستخدم
reduceمع initializer=0 (الرصيد الابتدائي). تمر الدالة على كل عملية وتحدث الرصيد تراكمياً.
6. دمج map و filter و reduce معاً
القوة الحقيقية تظهر عند دمج الثلاثة معاً لمعالجة بيانات معقدة:
from functools import reduce
# بيانات المبيعات
sales = [
{"product": "Laptop", "price": 1200, "quantity": 2},
{"product": "Mouse", "price": 25, "quantity": 5},
{"product": "Keyboard", "price": 75, "quantity": 3},
{"product": "Monitor", "price": 300, "quantity": 1}
]
# الخطوة 1: filter - المنتجات الغالية (سعر > 50)
expensive = filter(lambda s: s["price"] > 50, sales)
# الخطوة 2: map - حساب الإجمالي لكل منتج
totals = map(lambda s: s["price"] * s["quantity"], expensive)
# الخطوة 3: reduce - جمع كل الإجماليات
grand_total = reduce(lambda acc, total: acc + total, totals, 0)
print(f"إجمالي مبيعات المنتجات الغالية: {grand_total} ريال")
# نفس النتيجة في سطر واحد
oneliner = reduce(
lambda acc, s: acc + (s["price"] * s["quantity"]),
filter(lambda s: s["price"] > 50, sales),
0
)
print(f"نفس النتيجة: {oneliner} ريال")
ملخص الدرس
- دالة reduce() تقوم بدمج عناصر المجموعة في قيمة واحدة نهائية.
- تعمل بشكل تراكمي (زوجي) من اليسار إلى اليمين.
- يجب استيرادها من مكتبة
functoolsفي بايثون 3. - تستقبل دالة بمعاملين: (المراكم، العنصر الحالي).
- يمكن تحديد قيمة ابتدائية (initializer) اختيارية.
- مفيدة جداً للعمليات الحسابية والمنطقية التراكمية المخصصة.
- استخدم البدائل المدمجة (
sum,max,min) عندما تكون متاحة. - تذكر دائماً أن الوضوح أهم من الاختصار؛ إذا كان الكود معقداً جداً بـ
reduce، فكر في استخدام حلقةforعادية.
الخطوة التالية
بعد أن أتقنا معالجة المجموعات، لنتعلم كيف ننشئ بيانات "عند الطلب" لتوفير موارد الجهاز بشكل مذهل.
الدرس التالي: المولدات (Generators) وكفاءة الذاكرة