def make_power(n):
def power(x):
return x ** n
return powerالبرمجة بالإجراءات
من أنماط البرمجة أن يقبل الإجراءُ إجراءً في عوامله أو يُنتج هو إجراءً جديدًا. والتعبير عن ذلك بالبرمجة يسمى البرمجة الإجراءية (Functional Programming).
صناعة الإجراء
من خصائص الإجراءت في بايثون أنها أشياء (مثلها مثل الرقم) يُمكن إرجاعُها عند return. فمثلاً إجراء make_power هو إجراءٌ مُنشئٌ للإجراءت التي تحسِب القوى. المتغير n هو متغير محلي للإجراء make_power، ويمكن أن يستعمل في الإجراء power الذي يتم إرجاعُه:
فيُمكن إنشاء إجراء تربيعي وإجراء تكعيبي منه، على هذا النحو:
square = make_power(2)
cube = make_power(3)ثم يُمكن طلب هذا الإجراء من خلال القوسين ()، كما يلي:
print("Squares:", square(2), square(3))
print("Cubes:", cube(2), cube(3))Squares: 4 9
Cubes: 8 27
وإليك نفس الدالة، لكن الآن بذكر الأنواع في تعريفها:
def make_power(n: float) -> callable:
def power(x: float) -> float:
return x ** n
return powerأما callable فمعناه الشيء الذي يقبلُ النداء؛ ويتحقق النداء في بايثون بالقوسين (). ومما يقبَلُ النداء: الإجراءت.
والإجراء العالي (higher-order function): هو الإجراء الذي يقبلُ إجراءً أو يُنتِجُ إجراءً.
تعريف الإجراء من غير اسم
وقد تتم بالإجراء النكرة(lambda) الذي لا اسم له. ويجب حينها وضع جملة واحدة، هي الجملة التي يعود بها الناتج لوجود return مستترة.
وهو على هذا النحو:
lambda parameters: returned_expressionفيجوز أن تكتب:
square = lambda x: x * xفقد تمَّ تعيين الإجراء المُنكَر إلى متغيِّر. ثم يجري استعماله هكذا:
square(3)9
فيمكننا أن نعرِّف الإجراء الذي في الداخل بلا اسمٍ فنختصر العبارة هكذا:
def make_power(n):
return lambda x: x ** nانظر: مرجع الإجراء المؤجل للمزيد.
الإجراء ذو الحال
الحال (state) هي جميع المتغيرات التي في نطاق الإجراء.
فإذا أردنا أن يكون للإجراء متغيِّرًا يبني عليه معالجته اللاحقة بمعالجته السابقة، فإننا نضعه في نطاق الإجراء المنشئئ على النحو التالي. ونمثل على ذلك بإنشاء العداد make_counter:
def make_counter(start: int, step: int) -> callable:
count = start
def counter() -> int:
nonlocal count
count += step
return count
return counterففي القطعة السابقة:
- الإجراء المُنشئ هو
make_counterيقبل عاملين يحكُمان سلوك الإجراء الذي يتم إنشاؤه:counter - تبدأ قيمة
countبالقيمة المُعطاةstart - وفي تعريف
counter()نلاحظ كلمةnonlocalالتي تربط الاسمcountبالمتغير الذي في النطاق الأعم، أي: نطاق الإجراءmake_counter؛ وذلك حتى يُمكنه تغييره. فلولا هذه الكلمة لم يُمكن تغييره. - نلاحظ زيادة
countبالقيمة المعطاةstepفي إجراء الإنشاء
counter = make_counter(10, 2)
x = counter()
print(x)
y = counter()
print(y)12
14
الإجراء المغيِّر للإجراءات
المزيِّن (decorator) هو الإجراء الذي يأخذ إجراءً لينتج إجراءً جديدًا منه. وشكله العام نحو:
def decorator(func: callable) -> callable:
def wrapper(*args, **kwargs):
print("Before function execution")
result = func(*args, **kwargs)
print("After function execution")
return result
return wrapperمثلاً: يُمكننا أن نحفظ نتيجة نداء الإجراء بمدخلات معيَّنة، حتى إذا ما تكرر الطلب بنفس المدخلات؛ لا نحتاج إلى تنفيذ الإجراء بتفاصيله حقيقةً، وإنَّما نعيد القيمة التي سبق حفظها. وهذا يفيد في أمرين:
- الاقتصاد في موارد المعالجة
- تقليل وقت الاستجابة
ويُسمَّى الإجراء الذي يحفظ مدخلاته؛ إجراءً حافظًا (memoized). ويشترط فيه أن يكون الإجراء نقيًّا؛ وهو الإجراء الذي لا تعتمد مخرجاته إلا على مدخلاته المباشرة.
def memoize(f: callable) -> callable:
cache = {}
def memoized(x):
if x in cache:
print("Cache hit!")
return cache[x]
else:
print("miss!")
result = f(x)
cache[x] = result
return result
return memoizedفهذا الإجراء يُنشئُ إجراءً من إجراء.
- أنشأنا في البداية
cacheقاموسًا لحفظ نتائج الإجراء .. أيًّا ما كان الإجراء. - عرفنا إجراءً جديدًا يسمَّى
memoizedيحسب نتيجة الإجراء المُعطىfبالمدخلx، ويحفظها فيcacheإذا لم يكن موجودًا.
فلو أن لدينا إجراءً يأخذ مدخلاً واحدًا فقط، يُمكننا جعله إجراءً ذا حافظة، هكذا:
def square(x):
return x ** 2
square = memoize(square)
print(square(2))
print(square(2))
print(square(3))miss!
4
Cache hit!
4
miss!
9
- في أول مرة يرى فيها الإجراء
x=2فإنه يحسِبُ تلك القيمة ثم يحفظها ويرجعها - أما في المرة الثانية عند
x=2فإنه سبق حفظها فيcache؛ فلا نحتاج لتشغيل الإجراءsquareأصلاً، بل نسترجع النتيجة منcache - أما القيمة الجديدة
x=3فإنها كالقيمةx=2أول مرة
ولأن نمط التزيين شائع في البرمجة، فقد خصصت له بايثون علامة خاصة بها: @. ففي هذا المثال نزيِّن إجراء factorial بالإجراء memoize، ليكون ذا ذاكرة ولا يحتاج لحساب العمليات المتسلسلة أكثر من مرة:
@memoize
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)بل إن بايثون توفر هذه الخاصية للإجراءت عن طريق المزيِّن @functools.lru_cache، الذي يمكن استيراده من المكتبة الأساسية functools. وهو الذي يجب استعماله في الواقع؛ إذ يقبل أن يكون الإجراء له أي عدد من المدخلات، وليس كما اقتصرنا فيه بالمثال السابق على عامل واحد (x).
فهكذا تكون:
import functools
@functools.lru_cache
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)ومثل هذا يتسعمل كثيرًا:
| الإطار / المكتبة | المزخرف | الاستخدام |
|---|---|---|
| functools | @lru_cache |
التسريع: تخزين النتائج لتفادي تكرار الحسابات. |
| Python Core | @property |
التغليف: التعامل مع الدالة كأنها متغير (Attribute). |
| Flask / FastAPI | @app.route() |
التوجيه: ربط الرابط (URL) بدالة برمجية. |
| Django | @login_required |
التحقق: قصر الوصول على المستخدمين المسجلين. |
| Click / Typer | @click.command() |
واجهة أوامر: تحويل الدالة لأداة سطر أوامر. |
| Pytest | @pytest.fixture |
الاختبار: إعداد بيئة بيانات الاختبار تلقائياً. |
| Celery | @app.task |
مهام خلفية: تشغيل الدوال بشكل غير متزامن. |