Decorators في Python: حوّل دوالك العادية لأدوات بتقيس وتسجّل تلقائيًا
المستوى المطلوب: متوسط — المقال ده يفترض إنك مرتاح مع تعريف الدوال وتمرير المعاملات في Python، لكن لسه مش متعمق في الـ functional programming. لو لسه مبتدئ تمام، فيه قسم في الأول هيشرحلك الفكرة بمثال يومي قبل ما ندخل في الجزء التقني.
لو بتكتب نفس سطور الـ print("entered function X") وstart = time.time() في كل دالة عايز تراقبها، أنت بتدفع تكلفة صيانة كان ممكن تختفي بسطر واحد فوق الدالة. الـ Decorator في Python بيخليك تضيف logging أو timing أو auth لأي دالة بدون ما تلمس جسمها الأصلي.
المشكلة باختصار
عندك 30 endpoint في تطبيق Flask أو FastAPI. كل واحد محتاج: سطر يطبع وقت الدخول، سطر يقيس الزمن المستغرق، سطر يتأكد إن المستخدم مسجّل دخول. لو نسخت الـ 3 سطور دول جوّا كل دالة، أي تعديل بسيط بعد كده هيخليك تفتح 30 ملف. ده بالظبط الموقف اللي الـ Decorator اتعمل عشانه.
اشرحلي الفكرة كأني مبتدئ خالص
تخيّل إنك بتشتري قهوة من محل بيقدّم القهوة في كوب عادي. لو حبيت تشيلها معاك للشغل، هتحتاج: غطا، مصاصة، وحامل كرتون. بدل ما المحل يصنع 4 أنواع كاسات (عادي، بغطا، بغطا ومصاصة، بغطا ومصاصة وحامل)، هو بيصنع كوب واحد، ثم بيلف عليه طبقات إضافية حسب طلبك. القهوة جوّا ما تغيّرتش، الـ "تغليف" حواليها هو اللي تغيّر.
الـ Decorator بيعمل نفس الفكرة بالظبط: الدالة الأصلية ما تتغيرش، إنما بتتلف بدالة تانية بتضيف سلوك قبلها أو بعدها. وزي ما تقدر تحط غطا فوق غطا فوق كوب، تقدر تحط Decorator فوق Decorator فوق دالة.
التعريف العلمي الدقيق
الـ Decorator هو دالة (أو class) بتاخد دالة كمعامل، وبترجّع دالة جديدة بتضيف سلوك حول الأصلية. ده ممكن لأن الدوال في Python first-class objects: تقدر تمررها وترجّعها وتسندها لمتغير. الـ syntax بـ @decorator_name فوق الدالة هو مجرد اختصار لكتابة my_func = decorator_name(my_func).
مثال تنفيذي: timer + logger في 25 سطر
import time
import functools
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(message)s")
log = logging.getLogger("app")
def measure(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = func(*args, **kwargs)
return result
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
log.info("fn=%s elapsed=%.2fms args=%s", func.__name__, elapsed_ms, args[:2])
return wrapper
@measure
def fetch_user(user_id: int) -> dict:
time.sleep(0.12)
return {"id": user_id, "name": "ahmed"}
fetch_user(42)
# 2026-04-26 12:00:01 | fn=fetch_user elapsed=120.41ms args=(42,)