المزخرفات في بايثون - كيف نتعامل مع دوال بايثون بحُرية أكثر

المزخرفات في بايثون – كيف نتعامل مع دوال بايثون بحُرية أكثر

المزخرفات في بايثون أو باللغة الانجليزية Decorators هي أحد أقوى وأجمل خصائص لغة البرمجة بايثون، ولكن البعض ينظر لها كموضوع يصعب الإلمام به أو فهمه بسهولة. المزخرفات هي موضوع مُتعلق بدوال بايثون، وتهدف إلى نقلك في التعامل مع الدوال إلى مستوى متقدم. أهلا بكم في بايثونات في هذا المقال الجديد الذي نشرح فيه أحد أشهر مواضيع البايثون وأكثرها ارتباكًا لدى البعض.

ما هي المزخرفات في بايثون ؟

المزخرفة في بايثون عبارة عن دالة يُمكن إسناد دالة أخرى لها كمُعطى. تعمل المزخرفة على تعديل الدالة المسندة إليها، وهذا يعني أنك عندما تستدعي دالة أُسندت لمزخرفة، فإنك تستدعي دالة تختلف في خصائصها عن الدالة الأصلية لها. حتى لا تبدأ عزيزي القارئ في الدخول في الإرباك المعهود في هذا الموضوع، من المهم أن نُراجع سويةً بعض المفاهيم الرئيسية في الدوال في البايثون والتي ستسهل علينا فهم الموضوع.

يجب أن نتذكر أن الدالة في بايثون عبارة عن كائن “object” وهذا يعني أنه يُمكن إسناد الدالة لمتغير ما كما في المثال التالي:

def fun1():
	print(" I am function # 1")
	
x = fun1

x()

أو تمرير الدالة لدالة أخرى:

def fun1():
	print("I am function 1")
	
def fun2( f ):
	print("I am function 2")
	f()
	

fun2(fun1)

أو إرجاع الدالة كنتيجة لدالة أخرى:

def fun1():
	def fun2():
		print("I am function 2")
	
	return fun2


x = fun1()

x()

من خلال الأمثلة الثلاثة السابقة، طبقنا 3 مفاهيم أساسية في التعامل مع الدوال ككائنات objects في البايثون:

  1. اسناد الدالة لمتغير.
  2. تمرير/إسناد الدالة كمُعطى.
  3. ارجاع الدالة كنتيجة لدالة أخرى.

دعونا نُطبق كافة المفاهيم الثلاثة السابقة في مثال واحد.

ما قبل المزخرفات في بايثون

سنُعرف دالة func1، بداخلها دالة func2، ويُمكن إسناد دالة لها func:

def func1( func ):
	
	def func2():
		print("Before func calling")
		func()
		print("After func calling")
		
	return func2

def my_function():
	print("This is my base function")
	

func = func1(my_function)
func()

في المثال السابق، عرفنا الدالة func1 والتي يُمكن إسناد دالة لها باسم func، ثم بداخل الدالة func1 عرفنا دالة باسم func2 وبداخلها استدعينا الدالة المُسندة func (طبعنا قبلها وبعدها نصوص معينة)، ثم أرجعنا الدالة func2 كنتيجة للدالة func1.

بعد ذلك، عرفنا الدالة my_function وأسندناها للدالة func1 ثم أسندنا النتيجة المرجعة للمتغير باسم func. النتيجة المُرجعة عبارة عن دالة، نستدعي النتيجة كدالة بإضافة الأقواس لها، وستكون النتيجة كم يلي:

Before func calling
This is my base function
After func calling

نُلاحظ أن نتيجة الدالة my_function سُبقت بنص Before func calling ثم تُليت بنص آخر After func calling. هذه النتيجة هي ما تقوم به المُزخرفة بالضبط، حيث نقوم بالتعديل على سلوك الدالة my_function التي تتضمن طباعة نص معين.

من خلال المثال السابق، نقول أن الدالة func2 مُحاطة wrapped بواسطة الدالة func1. الدالة func1 تلعب هنا دور المُزخرفة.

مثال المزخرفات في بايثون

اذًا فالمزخرفة عبارة عن دالة تُغير من سلوك دالة أخرى. سنُعدل على أسماء الدوال في المثال السابق لتكون أكثر وضوحًا وملائمةً:

def decorator( decorated_func ):
	
	def internal_func():
		print("Before func calling")
		decorated_func()
		print("After func calling")
		
	return internal_func

def my_function():
	print("This is my base function")
	

func = decorator(my_function)
func()

المثال السابق (بعد تعديل أسماء الدوال) هو التطبيق العملي للمزخرفات في بايثون. ولكن، طريقة اسناد الدالة للمزخرفة ثم استدعاء النتيجة باضافة الأقواس، هي طريقة غريبة وغير واضحة كما يلي:

func = decorator(my_function)
func()

تُقدم البايثون هنا طريقةً أكثر وضوحًا باستخدام الرمز @، وهي الطريقة المُعتمدة في تعريف المزخرفات واستدعاء الدوال عليها. سنُعدل المثال السابق ليُصبح متوافقًا مع الطريقة المعتمدة لتعريف واستخدام المزخرفات في بايثون:

def decorator( decorated_func ):
	
	def internal_func():
		print("Before func calling")
		decorated_func()
		print("After func calling")
		
	return internal_func

@decorator
def my_function():
	print("This is my base function")
	

my_function()

نُلاحظ أنه لاضافة المُزخرفة لدالة ما، نكتب في البداية الرمز @ ثم اسم الدالة التي تُمثل المزخرفة، وذلك قبل أول سطر في تعريف الدالة التي سنطبق المُزخرفة عليها. عند استدعاء الدالة المُطبق عليها المزخرفة، ستكون نتيجتها متغيرة وفقًا للتعديل الذي أضافته المزخرفة عليها.

تطبيقات عملية على المزخرفات في البايثون

بالمثال يتضح المقال.

بعد أن تعرفنا على كيفية بناء المزخرفة وتطبيقها على أي دالة نريد، يتبقى السؤال الدائم، والذي يسأله أي شخص، كيف يُمكننا الاستفادة من المزخرفات؟ وكيف يُمكن أن نُطبقها عمليًا في البرامج والتطبيقات التي نبنيها بلغة البايثون ؟

إليكم مثالين في موضوع المزخرفات:

قياس المدة الزمنية لتنفيذ دالة

def timeit(method):
     def timed(*args):
         ts = time.time()
         result = method(*args)
         te = time.time()
         print("Time taken to execute the function is {} ms".format((te-ts)*1000))
     return timed

@timeit
def sleepy(in_seconds):
     print('Hi...')
     time.sleep(in_seconds)
	
	
sleepy(3)

sleepy(5)

في البداية نُعرف المزخرف باسم timeit، ثم نُعرف الدالة الداخلية باسم timed والتي تأخذ مُعطيات سيتم إسنادها للدالة التي نُريد زخرفتها وفق ما تتطلبه من عمل. نُعرف متغير باسم ts ونُسند له الوقت الذي بدأت فيه الدالة بالتنفيذ، ثم نستدعي الدالة المُزخرفة، وبعد أن تنتهي من تنفيذ الشيفرة البرمجية بداخلها، نُعرف متغير جديد باسم te ونُسند له الوقت الذي انتهت فيه الدالة المُزخرفة من العمل. بعد ذلك نطبع فرق الوقت بين te و ts والذي يُمثل الوقت الذي استغرقته الدالة المُزخرفة في التنفيذ.

الان يُمكننا استخدام المُزخرف timeit مع أي دالة، ولتوضيح عمل المزخرف، نُعرف الدالة sleepy واستخدمنا المزخرف معها. الدالة sleepy هي دالة بسيطة تقوم باستدعاء الدالة sleep من الوحدة المعيارية في بايثون time والتي تعمل على وقف التنفيذ حسب المدة التي نُمررها لها.

نتيجة تنفيذ المثال السابق ستكون كالتالي:

Hi...
Time taken to execute the function is 3003.82137298584 ms
Hi...
Time taken to execute the function is 5001.584053039551 ms

اضافة التسجيل لدوال بايثون

نحتاج في الكثير من التطبيقات التي نبنيها أن نُفعل خاصية التسجيل Logging في بايثون. لو أردنا أن نُراقب الدالة sleepy في المثال السابق، بحيث نحصل على ملف نصي يُمثل تاريخ وسجل استدعاء الدالة. يُمكن تنفيذ الفكرة من خلال المزخرفات.

المزخرف التالي أطلقنا عليه اسم log_decorator وسيعمل على إنشاء مُسجل باسم الدالة، وسيعمل المُسجل على تسجيل الأوقات التي تم استدعاء الدالة فيها بالاضافة إلى تسجيل المدة الزمنية التي استغرقتها الدالة في التنفيذ بالاضافة للنتيجة، وهذه الفكرة مهمة اذا أردنا مراقبة أداء التطبيق الذي نبنيه.

import logging
import time

def log_decorator(func):
    def internal(*args, **kwargs):
        name = func.__name__
        logger = logging.getLogger(name)
        logger.setLevel(logging.INFO)
        
        file_handler = logging.FileHandler("{}.log".format(name))
        fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        formatter = logging.Formatter(fmt)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

        logger.info("Running function: {}".format(name))
        ts = time.time()
        result = func(*args, **kwargs)
        te = time.time()
        time_to_complete = (te - ts) * 1000
        logger.info("Time taken to complete: {} ms - Result is: {}".format(time_to_complete, result))
        return result
    return internal

عند استخدام المُزخرف log_decorator على أي دالة، وعند استدعاء هذه الدالة، سنحصل على سجل استدعاء الدالة والنتيجة الخاصة بها. الشيفرة البرمجية التالية نستخدم log_decorator في مراقبة الدالة double_function و sleepy:

@log_decorator
def double_function(a):
    return a*2

@log_decorator
def sleepy(in_seconds):
    print('Hi...')
    time.sleep(in_seconds)

عند الاستدعاء، سيتم انشاء ملفين نصيين، كل ملف خاص بكل دالة، وسنبدأ بالحصول على السجلات log records مع كل استدعاء:

الملف double_functio.log:

2020-09-05 11:06:33,417 - double_function - INFO - Running function: double_function
2020-09-05 11:06:33,417 - double_function - INFO - Result: 4

الملف sleepy.log:

2020-09-05 11:34:26,518 - sleepy - INFO - Running function: sleepy
2020-09-05 11:34:29,520 - sleepy - INFO - Time taken to complete: 3001.95574760437 ms - Result is: None

المثال بالشيفرة البرمجية كاملة:

import logging
import time

def log_decorator(func):
    def internal(*args, **kwargs):
        name = func.__name__
        logger = logging.getLogger(name)
        logger.setLevel(logging.INFO)
        
        file_handler = logging.FileHandler("{}.log".format(name))
        fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        formatter = logging.Formatter(fmt)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

        logger.info("Running function: {}".format(name))
        ts = time.time()
        result = func(*args, **kwargs)
        te = time.time()
        time_to_complete = (te - ts) * 1000
        logger.info("Time taken to complete: {} ms - Result is: {}".format(time_to_complete, result))
        return result
    return internal

@log_decorator
def double_function(a):
    return a*2

@log_decorator
def sleepy(in_seconds):
    print('Hi...')
    time.sleep(in_seconds)

if __name__ == "__main__":
    print(double_function(2))
    sleepy(3)

إلى هنا نكون قد إنتهينا من شرح موضوع المزخرفات في بايثون، ونأمل أن نكون وفقنا في ذلك. اذا وجدت أن المقال مفيد، شاركه مع أصدقاءك المبرمجين، أو الذي يرغبون بمعرفة المزيد عن المزخرفات. ونسعد بتفاعلكم معنا عبر منصات التواصل الاجتماعي.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *