Python Lambda Function - بايثون لامدا

hocine
Hocine G
Back-end Developer (Python/Django)
05/03/2023 |برمجة 💻
Python Lambda Function - بايثون لامدا

بايثون لامدا هي وظائف (Function) صغيرة مجهولة وتخضع إلى بناء جملة أكثر تقييدًا ولكن أكثر إيجازًا من وظائف Python العادية.

هذا الموضوع مخصص بشكل أساسي لمبرمجي بايثون المتوسطين وذوي الخبرة ، ولكنه متاح للمهتمين بالبرمجة وحساب لامدا.

 

Lambda Calculus - تكامل لامدا

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

تاريخها

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

المثال الأول

بستعمال الكلمة المفتاحية def سنقوم بتعريف الدالة المطابقة و التي يربط فيها كل عنصر بنفسه.

def identity(x):
    return x
Identity () تأخذ الوسيطة x وترجعها عند الاستدعاء.
 
في المقابل، إذا اتستخدمنا بنية Python lambda، فسنحصل على ما يلي:
lambda x: x

في المثال أعلاه، يتكون التعبير من:

  • الكلمة المفتاحية (The keyword): lambda
  • متغير المرتبط (A bound variable): x
  • نص التعبير (A body): x

في سياق هذه المقالة، المتغير المرتبط هو وسيطة لدالة lambda. 

في المقابل، المتغير الحر غير ملزم ويمكن الإشارة إليه في نص التعبير. يمكن أن يكون المتغير الحر ثابتًا أو متغيرًا محددًا في نطاق الوظيفة.

يمكنك كتابة مثال أكثر تفصيلاً قليلاً، وظيفة تضيف 1 إلى الوسيط، على النحو التالي:

lambda x: x + 1

 يمكنك تطبيق الوظيفة أعلاه على وسيطة من خلال إحاطة الدالة ووسيطتها بأقواس:

(lambda x: x + 1)(2) # 3

 الاختزال هو استراتيجية حساب لامدا لحساب قيمة التعبير. في المثال الحالي، يتكون من استبدال المتغير المرتبط x بالوسيطة 2:

(lambda x: x + 1)(2) = lambda 2: 2 + 1
                     = 2 + 1
                     = 3

 نظرًا لأن دالة lambda عبارة عن تعبير، فيمكن تسميتها. لذلك يمكنك كتابة الكود السابق كما يلي:

 

add_one = lambda x: x + 1
add_one(2) # 3

 تكافئ وظيفة لامدا المذكورة أعلاه الكتابة التالية:

def add_one(x):
    return x + 1

 كل هذه الوظائف تأخذ حجة واحدة. ربما لاحظت، في تعريف لامدا، أن الحجج لا تحتوي على أقواس حولها. يتم التعبير عن الدوال متعددة الوسائط (الدوال التي تتطلب أكثر من وسيط) في Python lambdas من خلال سرد الوسائط وفصلها بفاصلة (،) ولكن دون إحاطة الأقواس بها:

full_name = lambda first, last: f'Full name: {first.title()} {last.title()}'
full_name('guido', 'van rossum')
# 'Full name: Guido Van Rossum'

Python Lambda and Regular Functions - بايثون لامدا والوظائف العادية

تسلط الأقسام التالية الضوء على القواسم المشتركة والاختلافات الدقيقة بين دوال بايثون العادية ووظائف لامدا.

Functions - الوظائف

في هذه المرحلة، قد تتساءل ما الذي يميز بشكل أساسي دالة لامدا المرتبطة بمتغير عن وظيفة عادية ذات سطر إرجاع واحد: تحت السطح، لا شيء تقريبًا. دعنا نتحقق من كيفية رؤية Python لوظيفة مبنية بعبارة إرجاع واحدة مقابل وظيفة تم إنشاؤها كتعبير (lambda).

تعرض وحدة dis وظائف لتحليل Python bytecode الذي تم إنشاؤه بواسطة مترجم Python:

>>> import dis
>>> add = lambda x, y: x + y
>>> type(add)
<class 'function'>
>>> dis.dis(add)
  1           0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE
>>> add
<function <lambda> at 0x7f4ae5b22710>

 يمكنك أن ترى أن dis () يعرض نسخة قابلة للقراءة من Python bytecode مما يسمح بفحص الإرشادات ذات المستوى المنخفض التي سيستخدمها مترجم Python أثناء تنفيذ البرنامج.

شاهده الآن بكائن دالة عادي:

>>> import dis
>>> def add(x, y): return x + y
>>> type(add)
<class 'function'>
>>> dis.dis(add)
  1           0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE
>>> add
<function add at 0x7f30c6ce9f28>

 bytecode الذي فسره بايثون هو نفسه لكلتا الوظيفتين. لكن قد تلاحظ أن التسمية مختلفة: يُضاف اسم الوظيفة لوظيفة محددة بـ def ، بينما يُنظر إلى دالة Python lambda على أنها lambda.

Traceback

 لقد رأيت في القسم السابق أنه في سياق وظيفة lambda، لم تقدم Python اسم الوظيفة، ولكن فقط <lambda>. يمكن أن يكون هذا قيدًا يجب مراعاته عند حدوث exceptions، ولا يظهر التتبع سوى <lambda>:

>>> div_zero = lambda x: x / 0
>>> div_zero(2)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 1, in <lambda>
ZeroDivisionError: division by zero

 إن تتبع الاستثناء (exception) الذي تم رفعه أثناء تنفيذ وظيفة lambda يحدد فقط الوظيفة التي تسبب الاستثناء كـ <lambda>.

إليك نفس الاستثناء الذي طرحته دالة عادية:

>>> def div_zero(x): return x / 0
>>> div_zero(2)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 1, in div_zero
ZeroDivisionError: division by zero

 تتسبب الوظيفة العادية في حدوث خطأ مشابه ولكنها تؤدي إلى تتبع أكثر دقة لأنها تعطي اسم الوظيفة ، div_zero.

Syntax - بناء الجملة

 كما رأيت في الأقسام السابقة، يقدم نموذج لامدا اختلافات نحوية عن الوظيفة العادية. على وجه الخصوص، تتميز وظيفة لامدا بالخصائص التالية:

  • يمكن أن يحتوي فقط على تعبيرات ولا يمكن أن يتضمن عبارات في نص التعبير.
  • هو مكتوب كسطر واحد للتنفيذ.
  • لا يدعم كتابة الانواع التوضيحية.
  • يمكن استدعاءه على الفور (IIFE:  Immediately Invoked Function Expression).

No Statements - لا توجد بيانات

لا يمكن أن تحتوي دالة lambda على أي عبارات. في دالة lambda ، ستؤدي عبارات مثل return ، أو pass ، أو assert ، أو lift ، إلى ظهور استثناء SyntaxError. إليك مثال على إضافة assert إلى جسم لامدا:

>>> (lambda x: assert x == 2)(2)
  File "<input>", line 1
    (lambda x: assert x == 2)(2)
                    ^
SyntaxError: invalid syntax

 يهدف هذا المثال إلى assert أن x لها قيمة 2. ولكن ، يقوم interpreter بتعريف خطأ نحوي (SyntaxError) أثناء تحليل الشفرة التي تتضمن assert في جسم lambda.

Single Expression - تعبير واحد

 على عكس الوظيفة العادية، فإن دالة Python lambda هي تعبير واحد. على الرغم من أنه في جسم لامدا يمكنك نشر التعبير عبر عدة أسطر باستخدام الأقواس أو سلسلة متعددة الأسطر ، إلا أنه يظل تعبيرًا واحدًا:

>>> (lambda x:
... (x % 2 and 'عدد فردي' or 'عدد زوجي'))(3)
'عدد فردي'

 يعرض المثال أعلاه السلسلة "عدد فردي" عندما تكون وسيطة lambda فردية ، و "عدد زوجي" عندما تكون الوسيطة زوجية. ينتشر عبر سطرين لأنه موجود في مجموعة من الأقواس ، لكنه يظل تعبيرًا واحدًا.

 Type Annotations - الانواع التوضيحية

 إذا كنت قد بدأت في استخدام تلميحات الكتابة ، والتي تتوفر الآن في Python ، فلديك سبب وجيه آخر لتفضيل الوظائف العادية على وظائف Python lambda. في دالة لامدا، لا يوجد ما يعادل ما يلي:

def full_name(first: str, last: str) -> str:
    return f'{first.title()} {last.title()}'

 يمكن اكتشاف أي خطأ مع full_name () بواسطة أدوات مثل mypy أو pyre، بينما يظهر خطأ SyntaxError مع وظيفة lambda المكافئة في وقت التشغيل:

>>> lambda first: str, last: str: first.title() + " " + last.title() -> str
  File "<stdin>", line 1
    lambda first: str, last: str: first.title() + " " + last.title() -> str

SyntaxError: invalid syntax

 مثل محاولة تضمين عبارة في lambda، تؤدي إضافة نوع توضيحي على الفور إلى خطأ في SyntaxError في وقت التشغيل.

IIFE -  immediately invoked function execution

لقد رأيت بالفعل عدة أمثلة لتنفيذ الوظيفة التي تم استدعاؤها فورًا IIFE:

>>> (lambda x: x * x)(3)
9

 خارج مترجم (interpreter) Python، ربما لا يتم استخدام هذه الميزة تطبيقيا. نتيجة مباشرة لكون دالة lambda قابلة للاستدعاء كما تم تعريفها. على سبيل المثال، يسمح لك هذا بتمرير تعريف تعبير Python lambda إلى وظيفة ذات ترتيب أعلى مثل map () أو filter () أو functools.reduce () أو إلى وظيفة رئيسية.

 Arguments - معلمات

 مثل كائن الوظيفة العادية المعرف بـ def ، تدعم تعبيرات Python lambda جميع الطرق المختلفة لتمرير المعلمات. هذا يتضمن:

  •  الحجج (arguments) الموضعية
  • الوسائط المسماة (keyword arguments)
  • قائمة متغيرة من الوسائط (يشار إليها غالبًا باسم varargs)
  • قائمة متغيرة من وسيطات الكلمات الرئيسية
  • وسيطات الكلمات الرئيسية فقط

توضح الأمثلة التالية الخيارات المفتوحة لك لتمرير الوسائط إلى تعبيرات lambda:

 

>>> (lambda x, y, z: x + y + z)(1, 2, 3)
6
>>> (lambda x, y, z=3: x + y + z)(1, 2)
6
>>> (lambda x, y, z=3: x + y + z)(1, y=2)
6
>>> (lambda *args: sum(args))(1,2,3)
6
>>> (lambda **kwargs: sum(kwargs.values()))(one=1, two=2, three=3)
6
>>> (lambda x, *, y=0, z=0: x + y + z)(1, y=2, z=3)
6

 Decorators

 في بايثون ، decorator هو تنفيذ نمط يسمح بإضافة سلوك إلى وظيفة أو class. يتم التعبير عنها عادةً باستخدام @decoratorالذي يسبق دالة. إليك مثال:

 

def some_decorator(f):
    def wraps(*args):
        print(f"Calling function '{f.__name__}'")
        return f(args)
    return wraps

@some_decorator
def decorated_function(x):
    print(f"With argument '{x}'")

 في المثال أعلاه ، فإن some_decorator () هي وظيفة تضيف سلوكًا إلى decorated_function()، بحيث ينتج عن استدعاء decorated_function("Python") الناتج التالي:

# Shell
Calling function 'decorated_function'
With argument 'Python'

  decorated_function() يطبع فقط باستخدام الوسيطة 'Python' ، لكن decorator يضيف سلوكًا إضافيًا يطبع أيضًا وظيفة الاستدعاء 'decorated_function'.

 يمكن تطبيق decorator على لامدا. على الرغم من أنه من غير الممكن decorate لامدا بصيغة @decorator، فإن decorator مجرد وظيفة، لذلك يمكنه استدعاء وظيفة lambda:

# تحديد decorator
def trace(f):
    def wrap(*args, **kwargs):
        print(f"[TRACE] func: {f.__name__}, args: {args}, kwargs: {kwargs}")
        return f(*args, **kwargs)

    return wrap

# تطبيق decorator على الوظيفة
@trace
def add_two(x):
    return x + 2

# استدعاء الوظيفة decorator 
add_two(3)

# وضع decorator على لامدا
print((trace(lambda x: x ** 2))(3))

# [TRACE] func: add_two, args: (3,), kwargs: {}
# [TRACE] func: <lambda>, args: (3,), kwargs: {}
# 9

 

 كما رأيت بالفعل، يظهر اسم وظيفة lambda بالشكل <lambda>، بينما يتم تحديد add_two بوضوح للوظيفة العادية.

قد يكون decorator وظيفة lambda بهذه الطريقة مفيدًا لأغراض التصحيح (debugging)، ربما لتصحيح سلوك دالة lambda المستخدمة في سياق وظيفة ذات ترتيب أعلى أو وظيفة رئيسية. دعونا نرى مثالاً على map():

list(map(trace(lambda x: x*2), range(3)))

 الوسيطة الأولى map() هي لامدا التي تضرب القيمة الممرة لها في 2. هذه اللمدا مزينة (decorated) بـ trace(). عند التنفيذ ، فإن المثال أعلاه ينتج ما يلي:

 

# [TRACE] Calling <lambda> with args (0,) and kwargs {}
# [TRACE] Calling <lambda> with args (1,) and kwargs {}
# [TRACE] Calling <lambda> with args (2,) and kwargs {}
# [0, 2, 4]

 النتيجة [0، 2، 4] هي قائمة تم الحصول عليها من ضرب كل عنصر من عناصر range(3). في الوقت الحالي ، ضع في اعتبارك range(3) المكافئ للقائمة [0 ، 1 ، 2].

 يمكن أيضًا استخدام لامدا كـ decorator ، لكن لا يوصى بذلك. إذا وجدت نفسك بحاجة إلى القيام بذلك ، فاستشر توصيات البرمجة PEP 8.

 خاتمة

 نتمنى ان يكون هذا الموضوع مفيد, لا تترد في ترك تعليق.

يمكنك تفقد موضوع المتغيرات في بايثون - Python Variables.

 

تذكر أن المساهمات في هذا الموضوع يجب أن تتبع إرشادات المجتمع.


التعليقات:

    لا توجد تعليقات بعد.