Python Decorators: إزاي تضيف سلوك لأي دالة بدون ما تلمسها
لو لقيت نفسك بتنسخ نفس الـ 5 سطور بتاعة الـ logging في 20 فانكشن، الـ decorators هتخلّيك تكتبها مرة واحدة بس. نفس المبدأ بيشتغل على caching وauthorization وقياس الزمن. الكلام مش نظري: هتلاقي في المقال decorator جاهز تنسخه تشغّله دلوقتي.
المشكلة باختصار
عندك 15 دالة، كل واحدة محتاجة تطبع اسمها وقت الاستدعاء ومدّة التنفيذ. لو حطيت نفس الـ time.perf_counter() والـ log.info في كل واحدة، خلقت 15 نقطة تعديل. لو حبيت تغيّر format الـ log بكرة، هتفتح 15 ملف. Decorator واحد بيلف الدالة ويحقن السلوك ده مرة واحدة بس من غير ما تمس جسم الدالة.
الـ Decorator ببساطة: مفيش سحر
الـ decorator ببساطة دالة بتاخد دالة، وترجّع نسخة ملفوفة منها. Python بيسهّل الصياغة بعلامة @ فوق تعريف الدالة. ركز: ده مجرد higher-order function، مش feature غامض.
def my_decorator(func):
def wrapper(*args, **kwargs):
# قبل الاستدعاء
result = func(*args, **kwargs)
# بعد الاستدعاء
return result
return wrapper
@my_decorator
def greet(name):
return f"Hello {name}"مثال تنفيذي: Logging وقياس الزمن
ده decorator جاهز تنسخه. بيسجّل اسم الدالة، arguments، والمدة بالميللي ثانية حتى لو الدالة رمت exception.
import functools
import time
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
def log_and_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
log.info(
"call func=%s args=%s kwargs=%s elapsed_ms=%.2f",
func.__name__, args, kwargs, elapsed_ms,
)
return wrapper
@log_and_time
def fetch_user(user_id: int):
time.sleep(0.05)
return {"id": user_id, "name": "Ahmed"}السطر @functools.wraps(func) مش اختياري. بدونه fetch_user.__name__ هيرجع "wrapper" بدل الاسم الأصلي، وده بيكسر أي monitoring بيعتمد على اسم الدالة زي Sentry و OpenTelemetry.