الدالة reduce: فن تحويل المجموعات الضخمة إلى قيمة واحدة ذهبية

في الدروس السابقة، تعلمنا كيف نحول كل عنصر (map) وكيف نختار عناصر معينة (filter). لكن ماذا لو أردنا دمج كل عناصر المجموعة في نتيجة واحدة نهائية؟ مثلاً: حساب حاصل ضرب كل الأرقام، أو دمج قائمة كلمات في جملة واحدة، أو إيجاد القيمة العظمى بطريقة تراكمية. هنا يأتي دور الدالة الأسطورية reduce(). هي الأداة التي تقوم بـ "تقليص" أو "اختزال" المجموعة الضخمة إلى قيمة واحدة مركزة، وهي ركيزة أساسية في معالجة البيانات المتقدمة.

1. تعريف: ما هي دالة reduce وكيف تختلف عن غيرها؟

دالة reduce() تنتمي لمكتبة functools في بايثون 3. على عكس map() و filter() التي تعمل على كل عنصر بشكل مستقل، reduce() تعمل على زوج من العناصر في كل مرة، وتحتفظ بنتيجة تراكمية (Accumulator).

الفرق الجوهري:

  • map(): تحول N عنصر إلى N عنصر جديد (نفس العدد).
  • filter(): تختار بعض العناصر من N (عدد أقل أو مساوٍ).
  • reduce(): تدمج N عنصر في قيمة واحدة فقط (1).

آلية العمل التراكمية (خطوة بخطوة):

  1. تأخذ أول عنصرين من المجموعة وتطبق الدالة عليهما → تحصل على نتيجة مؤقتة.
  2. تأخذ النتيجة المؤقتة (المراكم - Accumulator) وتطبق الدالة معها والعنصر الثالث → نتيجة جديدة.
  3. تستمر في أخذ "النتيجة الحالية" مع "العنصر التالي" حتى تنتهي المجموعة تماماً.
  4. تُرجع النتيجة النهائية الوحيدة.
الصيغة العامة (Syntax):
from functools import reduce
reduce(function, iterable[, initializer])
  • function: دالة تستقبل معاملين: (المراكم/النتيجة الحالية، العنصر التالي). يجب أن تُرجع قيمة واحدة تصبح المراكم الجديد.
  • iterable: المجموعة التي تريد تقليصها (قائمة، tuple، إلخ).
  • initializer (اختياري): قيمة ابتدائية يبدأ بها الحساب. إذا لم تُحدد، يُستخدم أول عنصرين من المجموعة.
ملاحظة مهمة: في بايثون 2، كانت reduce() دالة مدمجة (built-in). في بايثون 3، تم نقلها إلى functools لأن Guido van Rossum (مبتكر بايثون) يفضل استخدام حلقات for الواضحة بدلاً من reduce() المعقدة أحياناً.

2. فهم آلية العمل: أمثلة توضيحية خطوة بخطوة

أ) المجموع التراكمي (لفهم المنطق):

لنبدأ بمثال بسيط يوضح كيف تعمل reduce() داخلياً.

how_reduce_works.py
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 + 2 = 3 نجمع 3 + 3 = 6 نجمع 6 + 4 = 10 النتيجة النهائية: 10 بالحلقة التقليدية: 10

شرح الكود سطراً بسطر:

  • السطر 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
  • الأسطر 16-19: نفس النتيجة بحلقة تقليدية للمقارنة.
ب) استخدام القيمة الابتدائية (Initializer):

إذا وضعت قيمة ابتدائية، ستبدأ reduce بها كأول معامل (المراكم الأولي).

initializer_example.py
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} ريال")
النتيجة
بدون initializer: 6 مع initializer=100: 106 إجمالي الأسعار + الشحن: 245 ريال

شرح الكود سطراً بسطر:

  • السطر 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.

factorial_example.py
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}")
النتيجة
5! = 120 7! = 5040 10! = 3628800 حاصل ضرب [1, 2, 3, 4, 5] = 120
ب) إيجاد القيمة العظمى والصغرى (بدون دوال مدمجة):
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)}")
النتيجة
أكبر قيمة: 89 أصغر قيمة: 2 باستخدام max(): 89 باستخدام min(): 2
ج) دمج القوائم المتداخلة (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}")
النتيجة
القوائم المتداخلة: [[1, 2], [3, 4], [5, 6], [7, 8]] بعد الدمج: [1, 2, 3, 4, 5, 6, 7, 8] مع initializer: [10, 20, 30, 40]
د) بناء قاموس من قوائم:
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}")
النتيجة
القاموس الناتج: {'name': 'أحمد', 'age': 25, 'city': 'الرياض'}

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}")
ملاحظة تاريخية: Guido van Rossum (مبتكر بايثون) قال في مقال له: "أنا لم أحب reduce() أبداً. إنها تجعل الكود أقل وضوحاً". لهذا السبب تم نقلها من الدوال الأساسية إلى functools في بايثون 3. استخدمها فقط عندما تكون الخيار الأوضح.

5. تطبيق عملي شامل: معالجة البيانات المالية

تخيل أن لديك قائمة من القواميس تمثل حركات مالية (إيداعات وسحوبات)، وتريد حساب الرصيد النهائي.

balance_calculator.py
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} ريال")
النتيجة
--- سجل الحركات المالية --- 💰 إيداع 1000 ريال (راتب) → الرصيد: 1000 💸 سحب 200 ريال (فواتير) → الرصيد: 800 💰 إيداع 500 ريال (مكافأة) → الرصيد: 1300 💸 سحب 100 ريال (تسوق) → الرصيد: 1200 💰 إيداع 300 ريال (هدية) → الرصيد: 1500 ✅ الرصيد النهائي في الحساب: 1500 ريال

شرح الكود سطراً بسطر:

  • الأسطر 3-9: قائمة من القواميس، كل قاموس يمثل عملية مالية.
  • السطر 11: نعرّف دالة update_balance تستقبل الرصيد الحالي والعملية.
  • الأسطر 13-19: نفحص نوع العملية ونحدث الرصيد وفقاً لذلك، مع طباعة تفاصيل كل خطوة.
  • السطر 24: نستخدم reduce مع initializer=0 (الرصيد الابتدائي). تمر الدالة على كل عملية وتحدث الرصيد تراكمياً.

6. دمج map و filter و reduce معاً

القوة الحقيقية تظهر عند دمج الثلاثة معاً لمعالجة بيانات معقدة:

combo_example.py
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} ريال")
النتيجة
إجمالي مبيعات المنتجات الغالية: 2925 ريال نفس النتيجة: 2925 ريال
ملخص الدرس
  • دالة reduce() تقوم بدمج عناصر المجموعة في قيمة واحدة نهائية.
  • تعمل بشكل تراكمي (زوجي) من اليسار إلى اليمين.
  • يجب استيرادها من مكتبة functools في بايثون 3.
  • تستقبل دالة بمعاملين: (المراكم، العنصر الحالي).
  • يمكن تحديد قيمة ابتدائية (initializer) اختيارية.
  • مفيدة جداً للعمليات الحسابية والمنطقية التراكمية المخصصة.
  • استخدم البدائل المدمجة (sum, max, min) عندما تكون متاحة.
  • تذكر دائماً أن الوضوح أهم من الاختصار؛ إذا كان الكود معقداً جداً بـ reduce، فكر في استخدام حلقة for عادية.

الخطوة التالية

بعد أن أتقنا معالجة المجموعات، لنتعلم كيف ننشئ بيانات "عند الطلب" لتوفير موارد الجهاز بشكل مذهل.

الدرس التالي: المولدات (Generators) وكفاءة الذاكرة
المحرر الذكي

اكتب الكود وشاهد النتيجة فوراً

جرب الآن مجاناً
قناة ديف عربي

تابع أحدث الدروس والتحديثات مباشرة على واتساب

انضم الآن