الإجراء المؤجَّل

الإجراء النكرة

الإجراء النكرة (anonymous) هو فعل معيَّن بلا اسمٍ؛ يميَّز بكلمة lambda بخلاف المعرفة المميز بكلمة def. وهو -في باثيون- لا يقبل إلا جملة واحدةً وتكونُ هي جُملة الرجوع (return) ويستغني عن ذكرها. وهو مستعملٌ بكثرة في البرمجة، وإن كانت إضافته إلى اللغة ليست من باب الضرورة لكن من باب الاختصار في التعبير.

ويأتي على النحو التالي:

lambda parameters: returned_expression

فيجوز أن تكتب:

square = lambda x: x * x

فقد تمَّ تعيين الإجراء المُنكَر إلى متغيِّر. ثم يجري استعماله هكذا:

square(2)

ونمثل له بما قد سبق تعريفه في الإجراء داخل الإجراء المنشئ make_power الذي كان شكله هكذا:

def make_power(n):
    def power(x):
        return x ** n
    return power

فيمكننا أن نعرِّف الإجراء الذي في الداخل بلا اسمٍ فنختصر العبارة هكذا:

def make_power(n):
    return lambda x: x ** n

وطريقة استعماله كما سبق:

square = make_power(2)
cube = make_power(3)

assert square(2) == 4
assert cube(2) == 8

فعل التحويل

فعل التحويل أو التطبيق (map) يأخذُ فِعلاً ليطبِّقَهُ على مُكرَّر -بفتح الراء- أو أكثر (لوجود النجمة: *) ويُنتِجُ مُكرِّرًا -بكسر الراء- (Iterator). إذ وتعريفُه هكذا:

map(function, iterable, *iterables) -> iterator

أما المكررات، فقد مرَّ معنا أن القائمة، والصف، والقاموس، والنص من جنس ما يقبل التكرار. (راجع شجرة الجموع في الملحق: ?@fig-collections-tree)

وبالمثال يتضح المقال:

square = lambda x: x * x
xs = [2, 4, 6]
squared = map(square, xs)
print(squared)

لكن لاحظ أننا عند طباعة النتيجة، فإن الذي يظهرُ شيءٌ من النوع map وليست قائمة بتربيع الأعداد. وذلك لأن الإجراء مؤجَّل. وقد مرَّ معنا في باب الجمع المرتَّب أنَّ الإجراء range أيضًا من الإجراءات المؤجَّلة. ولتفعيله نستخدم أحد إجراءات القراءة التي منها: فعل الإنشاء list فنمرر له الإجراء هكذا list(squared) ليتمَّ إنشاء قائمة من المُكرَّر الناتج من عملية التحويل.

list(squared)

ولا فرق عنده بين الإجراء المنكر والمعرَّف:

def lower(x):
    return x.lower()

xs = ["The", "Cat", "Sat", "On", "The", "Mat"]
result = map(lower, xs)
list(result)

ويرد استعمال فعل التحويل بتعيين فعله نكرةً هكذا:

xs = ["The", "Cat", "Sat", "On", "The", "Mat"]
result = map(lambda x: x.lower(), xs)
list(result)

الإجراء المؤجَّل

الإجراء المؤجَّل (lazy evaluation) -بعكس الإجراء العادي الذي نتيجته عاجلة تأتي عند طلبه مباشرة- فإن نتيجته لا تحصُل إلا عند الحاجة إليها. وذلك للاقتصاد في المعالجَة والذاكرة إلى حين حدوث أحد أمرين: طلب قراءة أو طلب كتابة. وذلك يتمثل في:

  • الإشارة إلى عنصرٍ منه بالتكرار: for x in xs وحينها فإنَّ الذي يُحسبُ حقيقةً هو ما وصل إليه التكرار، ولا يُحسب ما بعده إلا عند الوصول إليه.
  • السؤال عن العضويَّة في المُكرر: x in xs
  • الإنشاء منه: list(xs) أو set(xs) ونحوهما
  • الإشارة (بالموضِع أو بالشريحة): xs[1] أو xs[2:4] (إن كان المكرر مرتبًا)

ننظر في هذا المثال:

xs = ["The", "Cat", "Sat", "On", "The", "Mat"]
result = map(lambda x: x.lower(), xs)

سنستعمل السؤال عن العضوية:

print('cat' in result)

ولو حاولت الآن للوصول إلى النتيجة فإنك ستفاجأ بأن العناصر إلى العنصر cat استنفذت (وهي كلمة the الأولى):

list(result)

وذلك لأن العناصر يتم معالجتها بالترتيب، فالسؤال عن العضويَّة أتم القدر الكافي للوصول إلى إجابته ثم توقف. ثم جاء فعل الإنجاء list ولا بدل له من قراءة جميع العناصر، ولذا ظهرت، لكنه لا يقرأ إلا متا تبقى منها.

دفعات المعالجة

ولعل علَّة الاقتصاد في خطوات المعالجة ومساحة الذاكرة غير مبررة بهذا المثال البسيط. لكنَّ لو أردنا معالجة قائمة مكونة من عشرة آلاف رابط من الصُّور عالية الدقَّة كبيرة الحجم؛ فإن إمكانيات الحاسب قد لا تتحمل؛ وحينها يجب الاقتصاد. فاستعمال الإجراءات المؤجَّلة تعني أننا ندخل العناصر في مسار المعالجة من أوِّلِه إلى آخره، ثم ندخل العنصر الثاني، والثالث، وهكذا … حتى ننتهي.

وإذا جعلتَ العُنصُرَ الواحدَ حِزمة عناصر، أصبح لديك دفعات من العناصر (Batches)؛ حيث تدخل عشرة عناصر، ثم العشرة الثانية، ثم الثالثة، وهكذا؛ وهو المعمول به في معالجة البيانات الضخمة (Big Data). ويتم اختيار حجم الدفعة بقدر لا يزيد على سعة الذاكرة.