المستوى: متوسط — المقال موجّه لمن يكتب Python بانتظام ويفهم الدوال والـ Recursion بشكل أساسي. القراءة حوالي 7 دقائق.
لو الدالة عندك بتحسب نفس الناتج آلاف المرات في الدقيقة، أنت بتدفع تكلفة CPU بدون مبرر. Memoization بتخلي الدالة تفتكر إجاباتها القديمة وترجّعها في O(1) بدل ما تعيد الحساب من الصفر.
Memoization: لما الدالة بتفتكر بدل ما تعيد الحساب
المشكلة باختصار
دوال كتير بتتنده بنفس المدخلات في الثانية الواحدة. كل استدعاء بيعيد نفس الحساب. لو الحساب رخيص، مفيش مشكلة. لو الحساب فيه recursion عميق أو lookup من قاعدة بيانات، الفاتورة بتكبر بسرعة.
ابدأ بمثال بسيط — قبل ما ندخل في الكود
تخيل إنك بتطلب من زميلك في المكتب يحسبلك ضرب الأرقام من 1 لـ 10 كل صباح. طريقة غبية: يعيد الحساب من الأول كل مرة. طريقة ذكية: يحسبها مرة واحدة، يكتبها في ورقة على المكتب، وكل صباح يبصلها ويقولك. الورقة دي هي الـ cache، والفعل ده بالظبط هو Memoization.
التعريف العلمي بدقة
Memoization تقنية تحسين بتخزّن نتائج الدوال الـ pure في جدول lookup مفهرس بمدخلات الدالة. لو نفس المدخلات اتطلبت مرة تانية، النتيجة بترجع من الجدول في O(1) بدل إعادة الحساب. الشرط الأساسي: الدالة لازم تكون pure — يعني نفس المدخلات بترجع نفس المخرجات بدون side effects (مفيش كتابة في ملف، مفيش تعديل لمتغير global، مفيش request شبكي).
المثال الكلاسيكي: فيبوناتشي بدون memoization
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
# fib(35) ≈ 3.4 ثانية على MacBook Pro M2
المشكلة: fib(35) بينده fib(34) و fib(33). و fib(34) بينده fib(33) تاني. النتيجة: حوالي 9 ملايين استدعاء، أغلبهم تكرار لنفس الأرقام. التعقيد O(2^n).
الحل بسطر واحد — @lru_cache
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
# fib(35) ≈ 7 ميكروثانية
الفرق: من 3,400 ميلي ثانية لـ 0.007 ميلي ثانية. ده ليس "أسرع شوية"، ده تسريع تقريبًا 485,000×. السبب: lru_cache بيخزّن نتيجة كل قيمة لـ n أول مرة بتتحسب، فالاستدعاءات اللي بعدها بترجع من الـ cache مباشرة. التعقيد بيرجع O(n) بدل O(2^n).