এই পর্বে যা শিখবেন
- ফাংশন কেন object, আর কীভাবে একটি ফাংশন আরেকটি ফাংশন নিয়ে নতুন ফাংশন ফেরত দেয়
- নিজের হাতে decorator লেখা,
@চিহ্ন আসলে কী, আর decorator ঠিক কখন চলে functools.wrapsকোন সমস্যা সমাধান করে — আগে সমস্যা, তারপর সমাধান- Argument-সহ decorator (factory), কয়েকটি decorator একসাথে লাগানোর ক্রম, method আর class-এ decorator
- বাস্তব decorator: লগ, গণনা, সময়, লগইন যাচাই, ইনপুট যাচাই, retry আর
lru_cache; কখন decorator ক্ষতি করে, আর AI-এর লেখা decorator কীভাবে যাচাই করবেন
১ · মূল ভাবনা — কাজের চারপাশে একটি মোড়ক
ধরুন আপনি একটি মোবাইল wallet অ্যাপের ব্যাকএন্ড লিখছেন। একটি ফাংশন আছে pay(sender, receiver, amount) — এক জনের কাছ থেকে আরেক জনের কাছে টাকা পাঠায়। এখন তিনটি নতুন দাবি এল: (ক) প্রতিটি লেনদেনের লগ রাখতে হবে, (খ) গুনতে হবে দিনে কতবার ফাংশনটি ডাকা হলো, (গ) শুধু লগইন করা ব্যবহারকারীই টাকা পাঠাতে পারবেন। আর একই তিনটি দাবি আছে আরও বিশটি ফাংশনের জন্য — withdraw, recharge, pay_bill…
সহজ পথ হলো প্রতিটি ফাংশনের শুরুতে আর শেষে একই পাঁচ-ছয় লাইন কপি করে বসানো। কিন্তু তাতে তিনটি সমস্যা: প্রতিটি ফাংশনের আসল কাজ (টাকা পাঠানো) চাপা পড়ে যায় পাহারাদারি কোডের নিচে; লগের ধরন বদলাতে হলে বিশ জায়গায় বদলাতে হয়; আর একটি জায়গায় ভুলে গেলে সেই ফাংশনটি পাহারা ছাড়াই চলে।
এই ধরনের কাজকে বলে cross-cutting concern — যে কাজ কোনো একটি ফাংশনের নিজস্ব নয়, বরং অনেক ফাংশনের ভেতর দিয়ে আড়াআড়ি চলে যায়: লগ, সময় মাপা, অনুমতি যাচাই, cache, retry. Python-এর উত্তর হলো decorator: একটি ফাংশন, যা আরেকটি ফাংশন নিয়ে তার চারপাশে নতুন আচরণ মুড়ে দেয়, আর মূল ফাংশনের কোড একটুও বদলায় না.
ঢাকার একটি অফিস ভবনের কথা ভাবুন। ভেতরে হিসাব বিভাগ তাদের আসল কাজ করে — বেতন হিসাব। কিন্তু কেউ হিসাব বিভাগে ঢোকার আগে গেটের নিরাপত্তারক্ষী পরিচয়পত্র দেখেন, রেজিস্টারে নাম আর সময় লেখেন, আর বেরোনোর সময় আবার সই নেন। হিসাব বিভাগের কাজের পদ্ধতিতে কিছুই বদলায়নি; তারা জানেই না গেটে কী হচ্ছে। নিরাপত্তারক্ষী হলেন decorator: আসল কাজের আগে কিছু করেন (পরিচয় যাচাই, নাম লেখা), কাজটি হতে দেন, আর পরে কিছু করেন (বেরোনোর সময় লেখা)। একই রক্ষী ভবনের দশটি বিভাগকে পাহারা দিতে পারেন।
ভিত্তি: ফাংশনও একটি object
Decorator সম্ভব হয় পর্ব ১৩ আর ২০-এর একটি তথ্যের জন্য: Python-এ ফাংশন একটি first-class function — সাধারণ মানের মতোই একটি object. def pay(...) চললে Python একটি function object বানায় আর pay নামটি তার গায়ে লাগায়। তাই ফাংশন দিয়ে তিনটি কাজ করা যায়, যেগুলো decorator-এর তিনটি খুঁটি:
- অন্য নামে রাখা:
send = pay— এখনsend(...)আরpay(...)একই ফাংশন ডাকে। বন্ধনী ছাড়াpayমানে ফাংশন object নিজে,pay()মানে তাকে ডাকা। - argument হিসেবে পাঠানো:
sorted(students, key=by_gpa)-এ আপনিby_gpaফাংশনটিকেই পাঠাচ্ছেন। যে ফাংশন অন্য ফাংশন নেয় বা ফেরত দেয়, তাকে বলে higher-order function. - ফাংশন থেকে ফাংশন ফেরত দেওয়া: পর্ব ২০-এর factory function —
make_vat(0.15)একটি নতুন ফাংশন বানিয়ে ফেরত দিয়েছিল, যা closure হিসেবে হারটি মনে রাখত।
পর্ব ২০-এ আরেকটি হাতিয়ারও দেখেছিলেন: wrapper function, যা *args আর **kwargs দিয়ে যা আসে সব নিয়ে নেয়, নিজের কিছু কাজ করে, তারপর হুবহু আরেকটি ফাংশনে পাঠিয়ে দেয়। Decorator হলো এই তিনটি জিনিসের মিলন: একটি higher-order function, যা ফাংশন নেয়, ভেতরে একটি wrapper closure বানায়, আর সেই wrapper ফেরত দেয়. নতুন কোনো ভাষা-বৈশিষ্ট্য নেই — শুধু @ চিহ্নটি ছাড়া, আর সেটিও নিছক একটি সংক্ষেপ।
Decorator = “ফাংশন নাও, মোড়াও, মোড়ানোটি ফেরত দাও”. মূল ফাংশন অক্ষত থাকে; বদলায় শুধু তার নামটি কোন object-কে দেখায়। এই একটি বাক্য বুঝলে বাকি পর্বের সবকিছু — factory, stacking, cache, Flask-এর route — এরই রকমফের।
২ · নিজের হাতে প্রথম decorator — আর @ চিহ্নের আসল মানে
প্রথমে @ ছাড়া, পুরো হাতে-কলমে। লক্ষ্য: যেকোনো ফাংশন ডাকার আগে তার নাম আর argument ছাপা, আর পরে ফলাফল ছাপা।
def log(func): # func = যে ফাংশন মোড়াব
def wrapper(*args, **kwargs): # নতুন ফাংশন: যেকোনো argument নেয়
print("→", func.__name__, args)
result = func(*args, **kwargs) # আসল কাজ — হুবহু ফরোয়ার্ড
print("←", result)
return result # আসল ফল ফেরত দিতে ভুলবেন না
return wrapper # ডাকা নয় — ফাংশনটিই ফেরত
def pay(to, amount):
return amount + 5 # ৫ টাকা চার্জসহ মোট
pay = log(pay) # মোড়ানো ফাংশন দিয়ে নাম বদল
total = pay("করিম", 500)তিনটি জিনিস খেয়াল করুন। এক, log নিজে কিছু ছাপে না; সে শুধু একটি নতুন ফাংশন (wrapper) বানিয়ে ফেরত দেয়। দুই, wrapper একটি closure — সে বাইরের func-কে মনে রাখে, তাই পরে ডাকলেও জানে কোন আসল ফাংশন চালাতে হবে। তিন, শেষের pay = log(pay) লাইনটি: ডান দিকে পুরোনো pay (আসল ফাংশন) ঢুকছে, আর বাঁ দিকে pay নামটি লাগছে wrapper-এর গায়ে। আসল ফাংশন হারিয়ে যায়নি — সে এখন wrapper-এর closure-এ func নামে বসে আছে।
@ — একই কথা, সুন্দর করে
pay = log(pay) লেখার দুটি অসুবিধা আছে: ফাংশনের নাম তিনবার লিখতে হয়, আর লাইনটি থাকে ফাংশনের শেষে — বড় ফাংশন হলে পড়ার সময় চোখেই পড়ে না যে এটি মোড়ানো। তাই Python 2.4 (২০০৪) থেকে একটি সংক্ষিপ্ত লেখা এসেছে:
@log
def pay(to, amount):
return amount + 5
# উপরের তিন লাইন = def pay(...) ... তারপর pay = log(pay)এ ধরনের লেখাকে বলে syntactic sugar — যে লেখা নতুন কোনো ক্ষমতা দেয় না, শুধু একই জিনিস লেখা সহজ করে। (একটি সূক্ষ্ম পার্থক্য আছে: @ দিলে আসল ফাংশনটি কখনো pay নামে বসেই না; সরাসরি decorator-এর ফল নামটি পায়।) এবার ধাপে ধাপে দেখুন, এই কয়েক লাইনের পেছনে আসলে কী কী ঘটে। “পরে” চাপুন:
1def log(func):2 def wrapper(*args, **kwargs):3 print("→", func.__name__, args)4 result = func(*args, **kwargs)5 print("←", result)6 return result7 return wrapper89@log10def pay(to, amount):11 return amount + 51213total = pay("করিম", 500)
| log | |
| pay | |
| func (cell) | |
| args | |
| result / total |
- log তৈরি।
def logচলল — একটি সাধারণ ফাংশন object তৈরি হলো,logনামে। এর body এখনো চলেনি; চলবে কেউlog(...)ডাকলে। - আসল pay তৈরি, কিন্তু নামহীন। লাইন ১০–১১-এর
defচলল, একটি function object তৈরি হলো। কিন্তু@logথাকায় Python এটিকে এখনইpayনামে বসায় না — আগে decorator-এর হাতে তুলে দেবে। - log(আসল pay) ডাকা হলো। এটাই
@log-এর কাজ।log-এর ভেতরেfunc= আসল pay. লাইন ২-এরdef wrapperএকটি নতুন ফাংশন বানাল, যার closure-এfuncরাখা আছে। লাইন ৭ সেই wrapper ফেরত দিল। লক্ষ করুন: এখনো কোনো টাকা পাঠানো হয়নি, কিছু ছাপাও হয়নি। - নাম বসল wrapper-এ।
logযা ফেরত দিল, সেটাইpayনাম পেল — অর্থাৎpay = log(pay). এখন থেকে প্রোগ্রামে যে কেউpayডাকবে, সে আসলে wrapper ডাকবে। আসল ফাংশন আছে শুধু wrapper-এর closure-এ। - ডাক এল wrapper-এ। লাইন ১৩:
pay("করিম", 500). নামটি wrapper-কে দেখায়, তাই wrapper চলল।*argsদুটি argument-কে tuple('করিম', 500)হিসেবে জমা করল। লাইন ৩ আসল কাজের আগের অংশ:→ pay ('করিম', 500)ছাপল।func.__name__এখনো'pay', কারণfuncআসল ফাংশন। - আসল ফাংশন চলল। লাইন ৪:
func(*args, **kwargs)tuple খুলে হুবহু আসল pay-কে দিল —to="করিম",amount=500. আসল pay লাইন ১১ চালিয়ে 505 ফেরত দিল, wrapper সেটিresult-এ রাখল। - পরের কাজ, তারপর ফেরত। লাইন ৫ আসল কাজের পরের অংশ:
← 505ছাপল। লাইন ৬resultফেরত দিল, তাই বাইরেtotal = 505. ডাকার মানুষটি টেরই পেলেন না যে মাঝে একজন পাহারাদার ছিল — শুধু দুটি লগ লাইন বাড়তি।
Decorator চলে একবারই — def-এর সময়
স্টেপারের ধাপ ৩ থেকে একটি গুরুত্বপূর্ণ কথা বেরিয়ে আসে: decorator ফাংশনটি (log) চলে ফাংশন সংজ্ঞায়িত হওয়ার মুহূর্তে, একবারই — সাধারণত module import হওয়ার সময়। প্রতিটি ডাকে চলে শুধু wrapper. তাই log-এর ভেতরে (wrapper-এর বাইরে) একটি print রাখলে সেটি ফাইল চালানোর সাথে সাথেই একবার ছাপা হবে, ফাংশন কেউ না ডাকলেও।
এই “একবার, সংজ্ঞার সময়” আচরণটি নিজেই কাজে লাগে। অনেক decorator wrapper বানায়ই না — শুধু ফাংশনটিকে কোথাও নিবন্ধন করে, তারপর আসল ফাংশনই ফেরত দেয়। যেমন একটি bot-এর command তালিকা:
COMMANDS = {}
def command(func):
COMMANDS[func.__name__] = func # def-এর সময় একবার: তালিকায় তোলা
return func # মোড়ানো নয় — আসল ফাংশনই ফেরত
@command
def balance(user): ...
@command
def recharge(user, amount): ...
# এখন COMMANDS = {'balance': ..., 'recharge': ...}এই ধাঁচকে বলে registry pattern. পর্ব ৪৮-এ দেখবেন, FastAPI-র @app.get("/") আর Flask-এর @app.route("/") মূলত এই কাজটিই করে: “এই URL-এ অনুরোধ এলে এই ফাংশনটি ডাকবে” — নিবন্ধন হয়ে যায় import-এর সময়েই।
৩ · functools.wraps — হারিয়ে যাওয়া নাম ফিরিয়ে আনা
আমাদের প্রথম log-এ একটি লুকানো সমস্যা আছে। মোড়ানোর পরে pay নামটি wrapper-কে দেখায় — তাহলে ফাংশনটির পরিচয়ও তো এখন wrapper-এর! দেখুন:
@log # wraps ছাড়া লেখা log
def pay(to, amount):
"""এক wallet থেকে আরেকটিতে টাকা পাঠায়।"""
return amount + 5
print(pay.__name__) # wrapper ← pay নয়!
print(pay.__doc__) # None ← docstring হারিয়ে গেছে
help(pay) # wrapper(*args, **kwargs) — আসল parameter-ও নেইএটা শুধু দেখতে খারাপ নয়, বাস্তবে ক্ষতিও করে:
- ডিবাগ কঠিন: traceback আর লগে বিশটি আলাদা ফাংশনের সবার নাম আসে
wrapper— কোনটিতে সমস্যা, বোঝা যায় না। - ডকুমেন্টেশন হারায়: docstring চলে যায়, তাই
help(), editor-এর tooltip আর স্বয়ংক্রিয় ডকুমেন্টেশন টুল কিছুই দেখায় না। - যে টুল নাম দেখে কাজ করে, সে ভেঙে যায়: উপরের
COMMANDS[func.__name__]ধরনের registry-তে সবাই'wrapper'নামে বসে একে অপরকে মুছে দেয়। pytest বা ওয়েব framework-ও অনেক সময় ফাংশনের নাম ব্যবহার করে।
সমাধান স্ট্যান্ডার্ড লাইব্রেরিতেই আছে: functools.wraps. মজার ব্যাপার, এটি নিজেই একটি decorator — wrapper-এর ওপরে বসে আসল ফাংশনের পরিচয় (__name__, __qualname__, __doc__, __module__, annotation ইত্যাদি) wrapper-এ কপি করে দেয়। সঙ্গে wrapper-এ একটি __wrapped__ attribute বসায়, যা আসল ফাংশনকে দেখায় — দরকার হলে মোড়ক খুলে সরাসরি আসলটি ডাকা যায়, আর inspect.signature, help() এর সাহায্যে আসল parameter দেখাতে পারে।
import functools
def log(func):
@functools.wraps(func) # শুধু এই একটি লাইন যোগ
def wrapper(*args, **kwargs):
...
return wrapperনিয়ম হিসেবে মনে রাখুন: wrapper বানানো প্রতিটি decorator-এ @functools.wraps(func) লিখুন — ব্যতিক্রম নেই। এটি এক লাইনের খরচ, আর না লিখলে যে বাগ হয় তা প্রায়ই অনেক পরে, অন্য কোনো টুলে, রহস্যময়ভাবে দেখা দেয়। AI-এর লেখা decorator-এ এই লাইনটি প্রায়ই বাদ পড়ে (অংশ ৯)।
লক্ষ করুন, wraps শুধু পরিচয়ের লেবেল কপি করে; wrapper এখনো আলাদা একটি ফাংশন। pay is pay.__wrapped__ হবে False. পরিচয় মিলল, অস্তিত্ব নয়।
এবার প্রথম runnable প্রোগ্রাম: wraps-সহ লগ decorator, যা সঙ্গে গুনেও রাখে ফাংশনটি কতবার ডাকা হলো। ফাংশনও object, তাই তার গায়ে নিজের attribute (wrapper.calls) লাগানো যায় — global ভ্যারিয়েবল লাগে না। চালানোর আগে শেষ দুই লাইনের আউটপুট অনুমান করুন:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
print(f"→ {func.__name__}{args}")
result = func(*args, **kwargs)
print(f"← {result}")
return result
wrapper.calls = 0
return wrapper
@log
def pay(sender, receiver, amount):
"""এক wallet থেকে আরেকটিতে টাকা পাঠায়।"""
return amount + 5
pay("রহিম", "করিম", 500)
pay("সুমি", "জামাল", 200)
print(pay.calls, pay.__name__, pay.__doc__)
print(pay.__wrapped__("রহিম", "করিম", 100))আউটপুট ছয় লাইনের। প্রথম চার লাইন দুটি লেনদেনের লগ: → pay('রহিম', 'করিম', 500), ← 505, তারপর → pay('সুমি', 'জামাল', 200), ← 205. আসল pay-এর ভেতরে লগের একটি লাইনও নেই, তবু প্রতিটি ডাক লগ হলো। পঞ্চম লাইন 2 pay এক wallet থেকে আরেকটিতে টাকা পাঠায়। — calls গুনেছে ২টি ডাক, আর wraps-এর কারণে নাম pay আর docstring অটুট। @functools.wraps(func) লাইনটি মুছে চালালে দেখবেন 2 wrapper None. শেষ লাইন 105 — __wrapped__ দিয়ে মোড়ক খুলে সরাসরি আসল ফাংশন ডাকা হয়েছে, তাই কোনো লগ ছাপা হয়নি আর গণনাও বাড়েনি। (calls = 0 লাইনটি def wrapper-এর পরে, কারণ attribute বসাতে হলে ফাংশনটি আগে তৈরি থাকতে হয়।)
৪ · Argument-সহ decorator — তিন স্তরের বাক্স
এখন পর্যন্ত decorator-এর কোনো সেটিং ছিল না। কিন্তু বাস্তবে প্রায়ই দরকার হয়: “নেটওয়ার্ক ডাক ব্যর্থ হলে ৩ বার চেষ্টা করো”, বা “এই ফাংশন শুধু admin ডাকতে পারবে”. লিখতে চাই এভাবে:
@retry(times=3)
def fetch_rate(currency): ...
@require_role("admin")
def delete_account(user, account_id): ...এখানে একটি নিয়ম মনে রাখলে সব পরিষ্কার: @-এর পরে যা লেখা, Python আগে সেই expression-টি হিসাব করে, আর তার ফলকে decorator হিসেবে ব্যবহার করে. @log-এ expression হলো শুধু log — একটি ফাংশন। কিন্তু @retry(times=3)-এ expression হলো একটি ডাক: retry(times=3). তাই retry নিজে decorator নয়; retry(3) যা ফেরত দেয়, সেটি decorator. যে ফাংশন decorator বানিয়ে ফেরত দেয়, তাকে বলে decorator factory.
ফলে নেস্টিং হয় তিন স্তরের। নামগুলো মনে রাখার একটি সহজ উপায় — প্রতিটি স্তর একটি জিনিস নেয়:
- বাইরের স্তর —
retry(times): সেটিং নেয়। ফেরত দেয় decorator. - মাঝের স্তর —
decorator(func): ফাংশন নেয়। ফেরত দেয় wrapper. (এটিই আমাদের আগেরlog-এর মতো সাধারণ decorator.) - ভেতরের স্তর —
wrapper(*args, **kwargs): প্রতিটি ডাকের argument নেয়। ফেরত দেয় আসল ফল।
অর্থাৎ @retry(times=3) লেখা মানে fetch_rate = retry(times=3)(fetch_rate) — দুই জোড়া বন্ধনী পরপর: প্রথম জোড়া সেটিং দিয়ে decorator আনে, দ্বিতীয় জোড়া সেই decorator-কে ফাংশন দেয়। ধাপে ধাপে দেখুন:
- retry(times=3)
times = 3সেটিং নেয় - decorator(func)
func = fetch_rateফাংশন নেয় - wrapper
times, func মনে রাখেনামটি এটিই পায় - fetch_rate("USD")
wrapper চলেপ্রতিটি ডাকে
dec = retry(times=3) # retry-র body চলে
# ভেতরে def decorator
# ফেরত: decorator (times মনে রাখে)
dec = retry(times=3) w = dec(fetch_rate) # decorator-এর body চলে # ভেতরে def wrapper # ফেরত: wrapper
fetch_rate = retry(times=3)(fetch_rate) # wrapper-এর closure-এ দুটি cell: # times = 3 (বাইরের স্তর থেকে) # func = আসল fetch_rate
fetch_rate("USD") # আসলে wrapper("USD")
চেষ্টা 1: USD: timeout # ধরা হলো, আবার
চেষ্টা 2: USD: timeout # ধরা হলো, আবার
USD রেট: 121.5 # তিনবারই ব্যর্থ হলে: শেষে ConnectionError # ওপরে পাঠানো হয় — চুপচাপ None নয়
- retry(3) চলল — factory.
@-এর পরের expression হলো একটি ডাক, তাই Python প্রথমেইretry(times=3)চালায়। এর body কোনো ফাংশন মোড়ায় না; শুধু ভেতরেdecoratorনামের একটি ফাংশন বানিয়ে ফেরত দেয়, যার closure-এtimes = 3. এখনোfetch_rate-এর সাথে কোনো যোগাযোগ নেই। - decorator(func) চলল। এবার সাধারণ decorator-এর গল্প: ফেরত পাওয়া
decorator-কে আসলfetch_rateদেওয়া হলো। সে ভেতরে@functools.wraps(func)-সহwrapperবানাল আর ফেরত দিল। এই ধাপ আর ধাপ ১, দুটোই ঘটে একবারই —def-এর সময়। - নামটি wrapper পেল। পুরো ব্যাপারটি দাঁড়াল
fetch_rate = retry(times=3)(fetch_rate). wrapper দুই স্তর বাইরের দুটি জিনিস মনে রাখে:times(সবচেয়ে বাইরের ফাংশন থেকে) আরfunc(মাঝের ফাংশন থেকে)। পর্ব ২০-এর LEGB-র E স্তর একাধিক ধাপ বাইরে পর্যন্ত খোঁজে — তাই এটি সম্ভব। - প্রতিটি ডাক যায় wrapper-এ।
fetch_rate("USD")ডাকলে wrapper চলে:for attempt in range(1, times + 1). প্রতিটি চেষ্টায়try-এর ভেতরে আসল ফাংশন ডাকে। নেটওয়ার্ক timeout হলেConnectionErrorধরা পড়ে, একটি লাইন ছাপা হয়, লুপ পরের চেষ্টায় যায়। - সফল হলে ফল, নইলে error ওপরে। তৃতীয় চেষ্টায় আসল ফাংশন 121.5 ফেরত দিল,
returnসেটিকেই সঙ্গে সঙ্গে বাইরে পাঠাল। তিনবারই ব্যর্থ হলে লুপ শেষে wrapper একটিConnectionErrorraise করে — ব্যর্থতা লুকায় না। লক্ষ করুন, ধরা হচ্ছে শুধুConnectionError;TypeError-এর মতো প্রোগ্রামের বাগ retry করলে সারে না, তাই সেগুলো প্রথমবারেই ওপরে চলে যায়।
দ্বিতীয় runnable প্রোগ্রামে ঠিক এটিই চলছে। নেটওয়ার্ক তো আর এখানে ব্যবহার করা যায় না, আর আসল নেটওয়ার্কের ব্যর্থতা এলোমেলো — তাই একটি iterator দিয়ে “সার্ভারের উত্তর” আগে থেকে ঠিক করে রাখা হয়েছে: প্রথম দুবার চুপ (None), তৃতীয়বার রেট। এভাবে নকল করে বানানো ব্যর্থতা প্রতিবার একই ফল দেয়, তাই পরীক্ষা করা সহজ — টেস্টিংয়েও এই কৌশল চলে (পর্ব ৩৯)।
import functools
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except ConnectionError as err:
print(f"চেষ্টা {attempt}: {err}")
raise ConnectionError(f"{times} বার চেষ্টাতেও ব্যর্থ")
return wrapper
return decorator
replies = iter([None, None, 121.5]) # None মানে সার্ভার চুপ
@retry(times=3)
def fetch_rate(currency):
if (rate := next(replies)) is None:
raise ConnectionError(f"{currency}: timeout")
return rate
print("USD রেট:", fetch_rate("USD"), fetch_rate.__name__)আউটপুট তিন লাইনের: চেষ্টা 1: USD: timeout, চেষ্টা 2: USD: timeout, আর USD রেট: 121.5 fetch_rate. প্রথম দুটি ডাকে next(replies) দিল None, তাই আসল ফাংশন ConnectionError raise করল; wrapper সেটি ধরে লাইন ছাপল আর আবার চেষ্টা করল। তৃতীয় ডাকে 121.5 এল, return সেটি ফেরত দিল। শেষের fetch_rate নামটি প্রমাণ যে তিন স্তরের ভেতরেও wraps কাজ করেছে। (:= হলো পর্ব ০৮-এর walrus — মান রাখা আর পরীক্ষা এক লাইনে।) replies-এ [None, None, None] দিলে তৃতীয় লাইনের বদলে traceback আসবে, শেষ লাইন ConnectionError: 3 বার চেষ্টাতেও ব্যর্থ — ঠিক এটাই চাই।
বাস্তবের retry আরও একটু বুদ্ধিমান: আসল সার্ভারে প্রতিটি ব্যর্থ চেষ্টার পরে একটু অপেক্ষা করা হয়, আর অপেক্ষার সময় প্রতিবার দ্বিগুণ হয় (১, ২, ৪ সেকেন্ড… একে বলে exponential backoff), যাতে চাপে থাকা সার্ভারকে আরও চাপ না দেওয়া হয়। আর retry করা নিরাপদ শুধু সেই কাজে, যা দুবার হলেও ক্ষতি নেই — রেট পড়া নিরাপদ, কিন্তু pay()-কে অন্ধভাবে retry করলে গ্রাহকের টাকা দুবার কাটতে পারে! এই গুণকে বলে idempotent. বাস্তব প্রজেক্টে নিজে না লিখে অনেকে tenacity-র মতো পরীক্ষিত লাইব্রেরি ব্যবহার করেন।
আরেকটি factory: @require_role("admin")
একই তিন-স্তরের ছাঁচে অনুমতি যাচাই। ধরা যাক প্রতিটি সংবেদনশীল ফাংশনের প্রথম argument হলো বর্তমান ব্যবহারকারী, একটি dict:
import functools
def require_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if role not in user.get("roles", ()):
raise PermissionError(f"{func.__name__}: '{role}' অনুমতি লাগবে")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_account(user, account_id):
...এখানে wrapper প্রথম argument user-কে নাম ধরে আলাদা করে নিয়েছে, যাতে পরীক্ষা করা যায়; বাকিগুলো আগের মতোই *args, **kwargs-এ। অনুমতি না থাকলে আসল ফাংশন চলেই না — decorator শুধু আগে-পরে কাজ জুড়তে পারে তা নয়, পুরো ডাকটি আটকেও দিতে পারে। @require_login-ও একই রকম, শুধু শর্তটি হবে “ব্যবহারকারী লগইন করেছেন কি না”. ভাবুন, পঞ্চাশটি admin ফাংশনে এই যাচাই হাতে লিখলে একটিতে ভুলে যাওয়ার সম্ভাবনা কত — decorator থাকলে প্রতিটি ফাংশনের ঠিক ওপরে চোখের সামনেই লেখা থাকে কে ডাকতে পারবে।
সাধারণ ভুল — বন্ধনী ভুলে যাওয়া: factory-কে বন্ধনী ছাড়া @retry লিখলে Python retry(fetch_rate) ডাকবে — অর্থাৎ times হয়ে যাবে আপনার ফাংশন, আর নামটি পাবে decorator! কোনো error সঙ্গে সঙ্গে আসে না; আসে পরে, অদ্ভুত জায়গায়। উল্টোটাও হয়: সাধারণ @log-কে @log() লিখলে TypeError, কারণ log একটি argument (func) চায়। মনে রাখুন: factory-তে বন্ধনী লাগে, সাধারণ decorator-এ লাগে না. (@functools.lru_cache-এর মতো কিছু decorator দুই রূপেই চলে, কারণ ভেতরে পরীক্ষা করে নেয় কী এল।)
৫ · একাধিক decorator একসাথে — কে আগে, কে পরে
একটি ফাংশনের ওপরে কয়েকটি decorator সাজিয়ে লেখা যায় — একে বলে decorator stacking:
@require_login
@log
def pay(user, receiver, amount): ...
# মানে: pay = require_login(log(pay))নিয়ম দুটি, আর দুটি আপাতদৃষ্টিতে উল্টো:
- মোড়ানো হয় নিচ থেকে ওপরে: ফাংশনের সবচেয়ে কাছের (নিচের) decorator আগে লাগে, তারপর তার ফলকে ওপরেরটি মোড়ায় — যেমন উপহার আগে রঙিন কাগজে মোড়ানো হয়, তারপর সেটি একটি বাক্সে।
- ডাকার সময় চলে ওপর থেকে নিচে: নামটি এখন সবচেয়ে বাইরের (ওপরের) wrapper-কে দেখায়, তাই ডাক এলে প্রথমে সেটিই চলে, তারপর ভেতরেরটি, শেষে আসল ফাংশন। উপহার খোলার সময় আগে বাক্স, তারপর কাগজ।
সহজ একটি উদাহরণে দেখুন — একটি decorator ফলাফলের দুই পাশে তারা বসায়, আরেকটি বসায় বন্ধনী:
1def stars(func):2 def wrapper():3 return "*" + func() + "*"4 return wrapper5def brackets(func):6 def wrapper():7 return "[" + func() + "]"8 return wrapper9@stars10@brackets11def greet():12 return "হ্যালো"13print(greet())
| greet | |
| brackets-এর func | |
| stars-এর func | |
| ফেরত মান |
- আসল greet তৈরি। লাইন ১১–১২-এর
defএকটি function object বানাল। ওপরে দুটি decorator, তাই নাম এখনো বসল না। - নিচেরটি আগে লাগল। ফাংশনের সবচেয়ে কাছের decorator
@brackets. Python ডাকলbrackets(আসল greet); ফেরত এল brackets-এর wrapper, যার closure-এfunc= আসল greet. - তারপর ওপরেরটি। এবার
stars(brackets-এর wrapper). stars মোড়াল আসল ফাংশনকে নয়, ইতিমধ্যে মোড়ানো জিনিসটিকে। ফল stars-এর wrapper, আর সেটিইgreetনাম পেল। পুরোটা মিলেgreet = stars(brackets(greet))— লেখার ক্রম আর বন্ধনীর ক্রম একই, ওপর থেকে নিচে = বাইরে থেকে ভেতরে। - ডাক — বাইরেরটি আগে চলে।
greet()মানে stars-এর wrapper. লাইন ৩ চলতে শুরু করল, কিন্তু"*" + func() + "*"হিসাব করতে আগেfunc()লাগবে — আর stars-এরfuncহলো brackets-এর wrapper. তাই stars অপেক্ষায় রইল। - ভেতরে ঢোকা — তারপর আসল ফাংশন। brackets-এর wrapper চলল (লাইন ৭), সেও আগে নিজের
func()ডাকল — এবার আসল greet, যা'হ্যালো'দিল। brackets সেটিকে বন্ধনীতে মুড়ে'[হ্যালো]'ফেরত দিল। - বাইরে বেরিয়ে আসা। stars এতক্ষণ যার অপেক্ষায় ছিল, পেল
'[হ্যালো]'; দুই পাশে তারা বসিয়ে দিল'*[হ্যালো]*'. ছাপা হলো*[হ্যালো]*. বন্ধনী ভেতরে, তারা বাইরে — কারণ brackets ছিল ভেতরের মোড়ক। দুই decorator-এর লাইন অদলবদল করলে ফল হতো[*হ্যালো*].
তারা আর বন্ধনীতে ক্রম শুধু চেহারা বদলায়, কিন্তু বাস্তবে ক্রম ভুল হলে নিরাপত্তা আর হিসাব দুটোই ভাঙে:
@require_loginওপরে,@logনিচে: লগইন যাচাই আগে হয়; অননুমোদিত ডাক লগে পৌঁছায়ই না। উল্টো ক্রমে প্রতিটি চেষ্টা (অননুমোদিতও) আগে লগ হয়, তারপর আটকায় — যদি আপনি সন্দেহজনক চেষ্টাও রেকর্ড করতে চান, সেটাই ঠিক। কোনটি চাই, তা আগে ঠিক করুন।- সময় মাপা আর cache:
@timerওপরে আর@cacheনিচে থাকলে timer মাপে cache-সহ সময় (দ্বিতীয় ডাকে প্রায় শূন্য)। উল্টো ক্রমে cache থাকে timer-এর বাইরে, তাই cache থেকে উত্তর এলে timer চলেই না। - Built-in decorator-এর ক্ষেত্রে:
@classmethodবা@staticmethod-এর সাথে অন্য decorator মেশালে সাধারণত@classmethodসবার ওপরে রাখতে হয়, কারণ সে ফাংশনকে একটি বিশেষ object-এ বদলে দেয়, যা অন্য decorator সাধারণ ফাংশন ভেবে মোড়াতে পারে না।
৬ · কাজের decorator — লগ, সময়, যাচাই আর cache
এ পর্যন্ত লগ, গণনা, retry আর অনুমতি দেখলেন। বাস্তবের আরও কয়েকটি পরিচিত decorator, আর শেষে স্ট্যান্ডার্ড লাইব্রেরির সবচেয়ে কাজের একটি।
সময় মাপা — @timer
কোন ফাংশন ধীর, তা জানতে প্রতিটি ডাকের সময় মাপা। পর্ব ২৭-এর time.perf_counter() এর জন্য সঠিক ঘড়ি — দুটি মানের পার্থক্যই সময়:
import functools, time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally: # error হলেও সময় ছাপা হবে
ms = (time.perf_counter() - start) * 1000
print(f"{func.__name__}: {ms:.1f} ms")
return wrapperএখানে try/finally-এর ব্যবহারটি লক্ষ করুন (পর্ব ২৪): আসল ফাংশন exception দিলেও সময় ছাপা হয়, আর exception নিজে চুপচাপ হারায় না, ওপরে চলে যায়। finally-এর আগেই return থাকলেও finally চলে, তারপর ফল ফেরত যায়। বড় প্রোগ্রামে print-এর বদলে logging module ব্যবহার করুন (পর্ব ৪০)।
ইনপুট যাচাই — @validate_positive
টাকার পরিমাণ শূন্য বা ঋণাত্মক হলে কোনো লেনদেন চলবে না — এই নিয়ম বিশটি ফাংশনে একই। একটি decorator wrapper-এর শুরুতে amount পরীক্ষা করে ValueError দিতে পারে, আসল ফাংশন ডাকার আগেই। একে বলে input validation. তবে সাবধান: যাচাইয়ের নিয়ম যত জটিল হয়, decorator-এ লুকিয়ে রাখা তত কম পরিষ্কার। ফাংশনটি পড়ে কেউ যেন বুঝতে পারেন কী কী শর্ত আছে — জটিল নিয়ম ফাংশনের ভেতরেই বা আলাদা যাচাই-ফাংশনে রাখা ভালো, অথবা pydantic-এর মতো লাইব্রেরি।
আগের উত্তর মনে রাখা — functools.lru_cache আর cache
কিছু ফাংশন একই ইনপুটে সবসময় একই উত্তর দেয় আর হিসাবে অনেক সময় নেয় — যেমন একটি জটিল ফর্মুলায় মুদ্রার রূপান্তর হার, বা পরিচিত উদাহরণ Fibonacci. একই প্রশ্নের উত্তর বারবার হিসাব না করে প্রথমবারের উত্তর লিখে রাখা আর পরে খাতা দেখে বলে দেওয়া — এই কৌশলের নাম memoization. ইনপুট → উত্তরের যে খাতা, তাকে বলে cache.
নিজে dict দিয়ে লেখা যায়, কিন্তু Python-এ তৈরি decorator আছে: functools.lru_cache (Python 3.2 থেকে) আর তার সহজ রূপ functools.cache (Python 3.9 থেকে)। Recursion-এর ক্ষেত্রে এর প্রভাব নাটকীয়, কারণ ফাংশনের ভেতরের নিজেকে-ডাকাও আসলে নাম ধরে ডাকা — আর নামটি এখন cache-ওয়ালা wrapper-কে দেখায়। ধাপে ধাপে fib(10)-এ মোট কতটি ডাক হয় দেখুন:
1@functools.cache # ধাপ ১–৩-এ এই লাইন নেই2def fib(n):3 if n < 2:4 return n5 return fib(n - 1) + fib(n - 2)67print(fib(10), fib.cache_info())
| পদ্ধতি | |
| পুনরাবৃত্তি | |
| মোট ডাক | |
| misses / hits |
- cache ছাড়া — একই প্রশ্ন বারবার।
fib(10)জানতে চায়fib(9)আরfib(8). কিন্তুfib(9)নিজেও জানতে চায়fib(8)আরfib(7). অর্থাৎfib(8)পুরোটা হিসাব হবে দুবার, একে অপরের খবর না রেখে। - পুনরাবৃত্তি ফুলে ওঠে। যত নিচে যাবেন, তত বেশি:
fib(7)৩ বার,fib(6)৫ বার,fib(5)৮ বার…fib(2)৩৪ বার,fib(1)৫৫ বার। লক্ষ করুন, গণনাগুলো নিজেরাই Fibonacci সংখ্যা! - মোট ১৭৭টি ডাক। উত্তর মাত্র 55, কিন্তু সেটি পেতে ফাংশনটি ১৭৭ বার চলেছে। ডাকের সংখ্যা n-এর সাথে প্রায় ১.৬ গুণ হারে বাড়ে:
fib(30)-এ ২৬ লাখের বেশি (2,692,537) ডাক। এটি exponential বৃদ্ধি। - @functools.cache যোগ করলে — নিচে নামা। এবার
fibনাম cache-ওয়ালা wrapper-কে দেখায়।fib(10)খাতায় নেই (miss), তাই আসল ফাংশন চলে, যা ডাকেfib(9)— সেটিও miss… এভাবেfib(1)আরfib(0)পর্যন্ত। ১১টি আলাদা মান, প্রতিটি একবার হিসাব হয়ে খাতায় ওঠে: misses = 11. - ফেরার পথে — খাতা থেকে। ওপরে ফেরার সময় প্রতিটি
fib(n)-এর দ্বিতীয় অংশfib(n - 2)ইতিমধ্যে খাতায় আছে (hit) — আর হিসাব নয়। এমন hit হয় ৮টি। মোট ১৯টি ডাক, ১৭৭টির বদলে।cache_info()ঠিক এটাই দেখায়:hits=8, misses=11.fib(30)-এ ৩১টি miss আর ২৮টি hit — ২৬ লাখের বদলে মাত্র ৫৯টি ডাক। - দ্বিতীয়বার জিজ্ঞেস করলে। আবার
fib(10)ডাকলে আসল ফাংশন একবারও চলে না — wrapper খাতা দেখে সঙ্গে সঙ্গে 55 দেয়। শুধু hits এক বাড়ে, 9 হয়। এক লাইনের একটি decorator হিসাবের ধরনটিই exponential থেকে linear বানিয়ে দিল।
cache_info()-এর চারটি সংখ্যার মানে: hits = কতবার খাতা থেকে উত্তর দেওয়া গেছে, misses = কতবার আসল হিসাব করতে হয়েছে, maxsize = খাতায় সর্বোচ্চ কতটি উত্তর রাখা যাবে (None মানে সীমা নেই), currsize = এখন কতটি আছে। খাতা খালি করতে fib.cache_clear().
cache আর lru_cache-এর পার্থক্য খাতার আকারে। @functools.cache সব উত্তর চিরকাল রাখে — ছোট ইনপুট-জগতের জন্য ঠিক, কিন্তু লাখো আলাদা ইনপুট এলে মেমরি শুধু বাড়তেই থাকে। @functools.lru_cache(maxsize=128) সর্বোচ্চ ১২৮টি রাখে; জায়গা ফুরালে সবচেয়ে দীর্ঘ সময় ব্যবহার না হওয়া উত্তরটি ফেলে দেয় — LRU মানে “Least Recently Used”. maxsize না দিলে default ১২৮।
Cache-এর তিনটি শর্ত: (১) argument-গুলো hashable হতে হবে, কারণ খাতাটি ভেতরে একটি dict — fib([1, 2]) দিলে TypeError: unhashable type: 'list'. (২) ফাংশনটি pure function হতে হবে — একই ইনপুটে সবসময় একই উত্তর, কোনো side effect নয়। আজকের মুদ্রা-রেট সার্ভার থেকে আনা ফাংশনে চিরস্থায়ী cache দিলে কাল-পরশুও সে গতকালের রেট দেখাবে। (৩) ফেরত মান mutable (list, dict) হলে সব ডাক একই object পায় — একজন বদলালে সবার উত্তর বদলায় (পর্ব ১৯)। আর class-এর method-এ lru_cache দিলে খাতা self-কেও ধরে রাখে, ফলে object মেমরি থেকে মুছতে পারে না — এ জন্য functools.cached_property বা অন্য পথ ভাবুন।
৭ · method, class আর আপনার ইতিমধ্যে দেখা decorator
Method-কে decorate করা — self চলে যায় *args-এ
Class-এর method-ও আসলে class-এর ভেতরে লেখা একটি সাধারণ ফাংশন, যার প্রথম parameter self (পর্ব ২৯)। তাই আমাদের @log method-এর ওপরেও কোনো বদল ছাড়াই কাজ করে: wallet.pay("করিম", 500) ডাকলে wrapper পায় args = (wallet, "করিম", 500) — self হলো args-এর প্রথম উপাদান, আর হুবহু আসল method-এ চলে যায়। এজন্যই wrapper-এ *args, **kwargs এত গুরুত্বপূর্ণ: সে জানে না, জানার দরকারও নেই, ফাংশনটি সাধারণ ফাংশন নাকি method. শুধু একটি জিনিস খেয়াল রাখবেন — লগে self ছাপলে object-এর __repr__ (পর্ব ৩০) দেখাবে, যেখানে ব্যবহারকারীর গোপন তথ্য থাকলে সেটি লগে চলে যেতে পারে।
Class decorator — পুরো class-কে মোড়ানো
@ শুধু ফাংশনের ওপরে নয়, class-এর ওপরেও বসে। তখন decorator একটি class নেয়, আর (সাধারণত সেই একই class, কিছু যোগ করে) ফেরত দেয়: @register class Plugin: ... মানে Plugin = register(Plugin). একে বলে class decorator. আপনি ইতিমধ্যে সবচেয়ে বিখ্যাতটি দেখেছেন — পর্ব ৩২-এর @dataclass. সে class-এর ফিল্ডগুলো পড়ে নিজে থেকে __init__, __repr__, __eq__ লিখে class-এ বসিয়ে দেয়, তারপর একই class ফেরত দেয় — কোনো wrapper নেই। Class decorator এসেছে Python 2.6 আর 3.0-তে।
যে decorator-গুলো আপনি আগেই ব্যবহার করেছেন
মডিউল ৫-এ বেশ কয়েকবার @ দেখেছেন, হয়তো ভেবেছেন এটি class-এর কোনো বিশেষ নিয়ম। এখন জানেন — সবই সাধারণ decorator, সবই নাম = decorator(নাম):
| Decorator | কোথায় দেখেছেন | কী করে |
|---|---|---|
@classmethod | পর্ব ২৯ | method প্রথম argument হিসেবে object নয়, class পায় (cls) — বিকল্প constructor বানাতে |
@staticmethod | পর্ব ২৯ | কোনো স্বয়ংক্রিয় প্রথম argument নেই; class-এর নামের নিচে রাখা সাধারণ ফাংশন |
@property | পর্ব ৩২ | method-কে attribute-এর মতো পড়া যায় (acc.balance, বন্ধনী ছাড়া); setter দিয়ে যাচাই |
@dataclass | পর্ব ৩২ | class decorator; __init__, __repr__, __eq__ নিজে লিখে দেয় |
@functools.total_ordering | পর্ব ৩০ — তুলনার dunder-এর সঙ্গী | class decorator; __eq__ আর একটি তুলনা (যেমন __lt__) দিলে বাকি <=, >, >= বানিয়ে দেয় |
@functools.cache / lru_cache | এই পর্ব | আগের উত্তর মনে রাখে |
@functools.wraps(func) | এই পর্ব | wrapper-এ আসল ফাংশনের পরিচয় কপি করে |
@contextlib.contextmanager | পর্ব ৩৬ | একটি generator-কে with-এ ব্যবহারযোগ্য বানায় |
@property, @classmethod, @staticmethod wrapper ফাংশন ফেরত দেয় না; ফেরত দেয় বিশেষ ধরনের object (descriptor, পর্ব ৩২-এর ঝলক), যা attribute পড়ার সময় আচরণ বদলায়। নিয়ম তবু একটাই — decorator যা ফেরত দেয়, নামটি সেটাই পায়। সেটি ফাংশন, class, descriptor বা যা খুশি হতে পারে।
Framework-এ decorator — সামনে যা দেখবেন
বড় Python লাইব্রেরিগুলো decorator-কে তাদের “ভাষা” বানিয়েছে। পর্ব ৪৮-এ FastAPI-তে লিখবেন @app.get("/balance") — এটি একটি decorator factory, যা ফাংশনটিকে একটি URL-এর সাথে নিবন্ধন করে (অংশ ২-এর registry). পর্ব ৩৯-এ pytest-এ দেখবেন @pytest.fixture (পরীক্ষার আগে ডেটা প্রস্তুত করা ফাংশন চিহ্নিত করা) আর @pytest.mark.parametrize(...) (একই টেস্ট অনেক ইনপুটে চালানো)। এমনকি AI লাইব্রেরিতেও আছে — PyTorch-এর @torch.no_grad(). এখন এগুলো দেখলে আর জাদু মনে হবে না: প্রতিটি একটি ফাংশন, যা আপনার ফাংশন নিয়ে কিছু করে।
৮ · কখন decorator ক্ষতি করে — সরল রাখার নিয়ম
Decorator শক্তিশালী, আর ঠিক এই কারণেই অপব্যবহার সহজ। ক্ষতির জায়গাগুলো চিনে রাখুন:
- লুকানো জাদু:
def pay(...)পড়ে মনে হয় এটি শুধু টাকা পাঠায়। কিন্তু ওপরের পাঁচটি decorator হয়তো ডাকটি আটকায়, argument বদলায়, ফল বদলায় বা ডাকটি তিনবার চালায়। আচরণ জানতে হলে প্রতিটি decorator-এর কোড খুলে পড়তে হয়। যে decorator ফাংশনের অর্থ বদলে দেয় (যেমন argument-এর type চুপচাপ বদলে দেওয়া), সেটি সবচেয়ে বিপজ্জনক। - লম্বা traceback আর কঠিন ডিবাগ: error হলে traceback-এ প্রতিটি wrapper-এর একটি করে স্তর যোগ হয়। ডিবাগারে step করলে আপনি আসল ফাংশনের আগে কয়েকটি wrapper-এর ভেতর দিয়ে যান।
wrapsনাম ঠিক রাখে, কিন্তু স্তরগুলো থেকেই যায়। - Exception গিলে ফেলা: wrapper-এর ভেতরে
except Exception: return Noneলিখলে আসল ফাংশনের সব error অদৃশ্য — পর্ব ২৪-এর swallowing exceptions. টাকা পাঠানো ব্যর্থ হলো, অথচ কেউ জানল না। - পরীক্ষা কঠিন:
@require_login-ওয়ালা ফাংশন unit test-এ ডাকতে হলে আগে নকল লগইন সাজাতে হয়।wraps-এর দেওয়া__wrapped__দিয়ে আসল ফাংশন আলাদাভাবে পরীক্ষা করা যায় — আরেকটি কারণwrapsভুলবেন না। - অতিরিক্ত কৌশল: একটি ফাংশনে মাত্র একবার দরকার এমন কাজের জন্য decorator লেখা, বা চার স্তরের factory যা কেউ পড়ে বুঝতে পারে না। Decorator-এর লাভ আসে তখনই, যখন একই কাজ অনেক ফাংশনে লাগে।
ভালো decorator: (১) একটি কাজ করে, নাম শুনেই বোঝা যায় কী (@log_calls, @require_login); (২) ফাংশনের argument আর ফলের অর্থ বদলায় না; (৩) সবসময় wraps ব্যবহার করে আর আসল ফল ফেরত দেয়; (৪) exception গেলে না, দরকার হলে লগ করে আবার ওপরে পাঠায়; (৫) এক ফাংশনে দু-তিনটির বেশি stack হয় না। এর বাইরে গেলে ভাবুন — একটি সাধারণ ফাংশন ডাক বা একটি class কি বেশি পরিষ্কার হতো?
C-তে decorator-এর মতো কোনো ব্যবস্থা নেই। সবচেয়ে কাছের জিনিস হলো ফাংশন পয়েন্টার দিয়ে একটি ফাংশন আরেকটিকে ডাকা (C-র ফাংশন পয়েন্টার ও callback), অথবা preprocessor macro দিয়ে প্রতিটি ফাংশনের আগে-পরে কোড বসানো — কিন্তু C-তে চলার সময় নতুন ফাংশন বানানো যায় না, তাই “ফাংশন নিয়ে নতুন ফাংশন ফেরত দেওয়া” সম্ভব নয়। C++-এ lambda আর template দিয়ে প্রায় একই কাজ হয়: auto logged(F f) একটি lambda ফেরত দিতে পারে, যা f-কে capture করে আগে-পরে কাজ জোড়ে (C++-এর lambda ও function object). তবে C++-এ @-এর মতো সংক্ষেপ নেই, আর মোড়ানো হয় compile-এর সময়, Python-এ চলার সময়।
- Decorator কী? — যে callable একটি ফাংশন (বা class) নিয়ে নতুন ফাংশন/object ফেরত দেয়, মূল কোড না বদলে আচরণ যোগ করতে;
@d def fমানেf = d(f). functools.wrapsকেন লাগে? — না দিলে মোড়ানো ফাংশনের__name__,__doc__wrapper-এর হয়ে যায়; wraps আসলটির পরিচয় কপি করে আর__wrapped__বসায়।- Argument-সহ decorator কীভাবে লেখেন? — তিন স্তর: factory (সেটিং নেয়) → decorator (ফাংশন নেয়) → wrapper (ডাকের argument নেয়)।
@a@bdef f-এর ক্রম? —f = a(b(f)): b আগে মোড়ায়, কিন্তু ডাকলে a-র wrapper আগে চলে।lru_cacheআরcache-এর পার্থক্য? — lru_cache-এর সীমা আছে (default ১২৮, পুরোনো উত্তর ফেলে দেয়); cache সীমাহীন,lru_cache(maxsize=None)-এর সমান; দুটিতেই argument hashable হতে হয়।
৯ · AI-এর যুগে — AI-এর লেখা decorator যাচাই করা
“একটি logging decorator লিখে দাও” — AI কয়েক সেকেন্ডে লিখে দেয়, আর সেটি চলেও। সমস্যা হলো, decorator-এর ভুলগুলো প্রথম পরীক্ষায় ধরা পড়ে না। নাম হারানো, error গিলে ফেলা বা ফল হারিয়ে যাওয়া দেখা দেয় পরে, অন্য কোনো ফাংশনে, অন্য কোনো টুলে। AI-এর লেখা decorator-এ বারবার যে ভুলগুলো দেখা যায়:
| AI প্রায়ই যা করে | সমস্যা | কী চাইবেন |
|---|---|---|
@functools.wraps নেই | সব ফাংশনের নাম wrapper; docstring, help(), registry, লগ সব বিভ্রান্ত | প্রতিটি wrapper-এ @functools.wraps(func) |
func(*args, **kwargs) ডাকে কিন্তু return করে না | মোড়ানো প্রতিটি ফাংশন None ফেরত দেয় — হিসাব নিঃশব্দে ভুল | result = func(...), পরে return result |
wrapper-এ except Exception: print(e) | error গিলে ফেলে; ব্যর্থ লেনদেন “সফল” দেখায়, ফল None | লগ করে খালি raise দিয়ে আবার পাঠানো, বা নির্দিষ্ট exception ধরা |
wrapper-এ নির্দিষ্ট parameter (def wrapper(x)) | অন্য signature-এর ফাংশন বা method-এ লাগালে TypeError | *args, **kwargs দিয়ে হুবহু ফরোয়ার্ড |
সব কিছুতে retry, এমনকি pay()-তে | টাকা দুবার কাটা; বাগ (TypeError) বারবার চালানো | শুধু idempotent কাজে, শুধু নির্দিষ্ট নেটওয়ার্ক-error-এ |
| পাঁচ-ছয়টি decorator, factory-র ভেতরে factory | কেউ পড়ে বোঝে না; ক্রম ভুল হলে নিরাপত্তা ফাঁক | দু-তিনটির বেশি নয়; ক্রমের কারণ comment-এ |
বদলানো ডেটা আনার ফাংশনে @cache | পুরোনো ফল চিরকাল; সীমাহীন মেমরি | pure ফাংশনেই cache; নইলে সময়সীমাসহ cache বা lru_cache(maxsize=...) |
AI-কে decorator লিখতে বলার সময় শর্তগুলো আগেই দিয়ে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসান:
AI-এর লেখা decorator পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১@ চিহ্নটি কোথা থেকে এল? PEP 318 আর Python 2.4-এর আগে মানুষ কীভাবে লিখত, আর এই চিহ্ন নিয়ে এত বিতর্ক কেন হয়েছিল?
গল্পটি শুরু Python 2.2 (২০০১) থেকে, যখন classmethod আর staticmethod ভাষায় এল। তখন কোনো বিশেষ চিহ্ন ছিল না; লিখতে হতো ফাংশনের পরে: def from_csv(cls, line): ..., তারপর body-র শেষে from_csv = classmethod(from_csv). ঠিক এই পর্বের “হাতে-কলমে” রূপ। ছোট ফাংশনে চলে, কিন্তু ত্রিশ লাইনের একটি method-এর শেষে লুকানো এই লাইনটি পড়ার সময় চোখ এড়িয়ে যেত। অথচ এটি ফাংশনের সবচেয়ে গুরুত্বপূর্ণ তথ্য — “এটি আসলে classmethod”. নামটিও তিনবার লিখতে হতো, আর বানান ভুল হলে নিঃশব্দে নতুন একটি নাম তৈরি হতো।
এর সমাধানের প্রস্তাব PEP 318 — “Decorators for Functions and Methods”. এর লেখক হিসেবে Kevin D. Smith, Jim J. Jewett আর Skip Montanaro-র নাম আছে, আর পরে Anthony Baxter যুক্ত হন। লক্ষ্য একটাই: রূপান্তরের কথাটি ফাংশনের শুরুতে, def-এর কাছে লেখা।
কিন্তু কোন চিহ্নে? এখানেই বিতর্ক, আর সেটি চলেছে মাসের পর মাস, python-dev মেইলিং লিস্টে শত শত বার্তায়। Python wiki-তে বিকল্পগুলোর একটি লম্বা তালিকা তৈরি হয়েছিল। প্রস্তাবগুলোর মধ্যে ছিল def লাইনের শেষে বর্গবন্ধনীতে তালিকা — def f(cls) [classmethod]:; def-এর আগে বর্গবন্ধনী; | বা অন্য চিহ্ন; আর একটি নতুন কিওয়ার্ডসহ ব্লক। সমাজের একটি বড় অংশ “J2” নামের একটি প্রস্তাব সমর্থন করেছিল, যেখানে decorate: ধরনের কিওয়ার্ড দিয়ে আলাদা ব্লক লেখা হতো। অনেকের আপত্তি ছিল @ চিহ্নে — এটি Python-এর মতো দেখায় না, Perl-এর মতো লাগে, আর চিহ্নটি অন্য টুলে (যেমন IPython-এ) ব্যবহার হচ্ছিল।
শেষ পর্যন্ত সিদ্ধান্ত দেন Guido van Rossum — তখনকার “BDFL”. ২০০৪ সালের মাঝামাঝি তিনি @ বেছে নেন, আর আলফা version-এ এটি যোগ হয়ে Python 2.4-এ (২০০৪ সালের শেষে) প্রকাশিত হয়। তাঁর যুক্তির মূল কথা: decorator লাইন def-এর আগে আলাদা লাইনে থাকলে লম্বা argument-সহ decorator-ও সহজে পড়া যায়; আর একটি অপরিচিত চিহ্ন নিশ্চিত করে যে কেউ এটিকে সাধারণ expression ভেবে বিভ্রান্ত হবেন না। পছন্দটি নিখুঁত ছিল না — PEP-টি নিজেই স্বীকার করে যে কোনো প্রস্তাবই সবাইকে খুশি করেনি।
পরের ধাপগুলো ছোট: Python 2.6 আর 3.0-তে PEP 3129 class-এর ওপরেও decorator বসানোর অনুমতি দেয়, আর Python 3.9-এ PEP 614 @-এর পরে প্রায় যেকোনো expression লেখার সুযোগ দেয় (প্রশ্ন ৩)। আজ বিশ বছর পরে @ Python-এর সবচেয়ে চেনা চিহ্নগুলোর একটি — Flask, FastAPI, pytest সবাই এর ওপর দাঁড়িয়ে।
মূল উপলব্ধি: @ কোনো নতুন ক্ষমতা আনেনি — f = d(f) আগেও লেখা যেত। এটি এনেছে পড়ার সুবিধা: গুরুত্বপূর্ণ তথ্যটি সামনে। আর এর ইতিহাস দেখায়, একটি ছোট চিহ্নের নকশাও কত বড় সামাজিক সিদ্ধান্ত হতে পারে।
প্র ০২“Decorator” নামটি কি বিখ্যাত Gang of Four বইয়ের Decorator design pattern থেকে এসেছে? দুটির মধ্যে মিল আর অমিল কোথায়?
১৯৯৪ সালে চারজন লেখক — Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software বইটি প্রকাশ করেন। লেখকদের ডাকনাম হয়ে যায় “Gang of Four” (GoF), আর বইটি অবজেক্ট-ওরিয়েন্টেড নকশার একটি ক্লাসিক। বইয়ের ২৩টি design pattern-এর একটি হলো Decorator: একটি object-কে আরেকটি object দিয়ে মুড়ে চলার সময় নতুন দায়িত্ব যোগ করা, যেখানে মোড়কটির ইন্টারফেস ভেতরের object-এর হুবহু একই, তাই ব্যবহারকারী পার্থক্য টের পান না।
সবচেয়ে চেনা উদাহরণ Java-র I/O: new BufferedReader(new InputStreamReader(new FileInputStream("data.txt"))). প্রতিটি স্তর একই ধরনের “stream” ইন্টারফেস দেয়, আর নিচের stream-কে মুড়ে একটি কাজ যোগ করে — একটি buffering, একটি অক্ষর-রূপান্তর। মোড়কগুলো যেকোনো ক্রমে, যেকোনো সংখ্যায় সাজানো যায়। ধারণাটি এই পর্বের stacking-এর সাথে স্পষ্টতই মেলে।
তাহলে কি Python নামটি সেখান থেকে নিয়েছে? মজার ব্যাপার, PEP 318 নিজেই এই প্রশ্নের মুখোমুখি হয়েছিল। PEP-এ লেখা আছে, নামটি নিয়ে অনেক অভিযোগ এসেছে, যার প্রধানটি হলো এটি GoF বইয়ের ব্যবহারের সাথে মেলে না; আর নামটি সম্ভবত বেশি এসেছে compiler-এর জগৎ থেকে, যেখানে syntax tree ঘুরে ঘুরে তাতে “annotate” বা সাজসজ্জা যোগ করা হয়। অর্থাৎ নামের উৎস ঠিক GoF নয়, আর মিলটি আংশিক।
অমিলগুলো গুরুত্বপূর্ণ:
- কখন: GoF Decorator চলার সময় যেকোনো মুহূর্তে একটি নির্দিষ্ট object-কে মোড়ায়। Python decorator সাধারণত একবারই, সংজ্ঞার সময় চলে, আর ফাংশন বা class-কে মোড়ায় — সেই নামের প্রতিটি ব্যবহার মোড়ানো হয়ে যায়।
- ইন্টারফেস একই রাখা বাধ্যতামূলক নয়: GoF-এ মোড়কের ইন্টারফেস মূলের সমান হতেই হবে। Python decorator যা খুশি ফেরত দিতে পারে —
@propertyফাংশনকে descriptor বানায়,@dataclassclass বদলে দেয়, registry decorator কিছুই মোড়ায় না। - ভাষার সমর্থন: GoF pattern-টি Java বা C++-এ বাস্তবায়ন করতে অনেক class লাগে (একটি সাধারণ interface, একটি abstract decorator class…)। Python-এ ফাংশন first-class হওয়ায় একই কাজ কয়েক লাইনের closure.
শেষ বিষয়টি একটি বড় কথা বলে। অনেক design pattern মূলত ভাষার কোনো অভাব পূরণের কৌশল। ১৯৯৬ সালে Peter Norvig একটি উপস্থাপনায় দেখিয়েছিলেন, GoF-এর ২৩টি pattern-এর অনেকগুলো Lisp বা Dylan-এর মতো ডায়নামিক ভাষায় অনেক সরল হয়ে যায় বা প্রায় অদৃশ্য হয়ে যায়। Python-এর decorator তার একটি উদাহরণ।
মূল উপলব্ধি: নাম এক হলেও Python decorator আর GoF Decorator এক জিনিস নয়। প্রথমটি একটি ভাষা-বৈশিষ্ট্য (সংজ্ঞার সময় নাম বদলানো), দ্বিতীয়টি একটি নকশার ধাঁচ (চলার সময় object মোড়ানো)। Python decorator দিয়ে GoF ধাঁচটি সহজে বানানো যায় — কিন্তু তার চেয়ে অনেক বেশি কিছুও করা যায়।
প্র ০৩Python 3.9-এর আগে @-এর পরে যা খুশি লেখা যেত না। PEP 614 কোন সীমা তুলে দিল, আর প্রথমে সীমাটি রাখা হয়েছিলই বা কেন?
Python 2.4 থেকে 3.8 পর্যন্ত @-এর পরে লেখার নিয়ম ছিল খুব সংকীর্ণ: একটি নাম, বিন্দু দিয়ে যুক্ত নামের শৃঙ্খল (@functools.wraps, @app.route), আর শেষে বড়জোর একটি ডাক (@app.route("/"))। এর বাইরে কিছু লিখলে SyntaxError. যেমন @buttons[0].clicked.connect (list থেকে index দিয়ে নেওয়া), @(lambda f: f), বা @handlers["save"] — সবই নিষিদ্ধ।
এই সীমা ইচ্ছাকৃত ছিল। PEP 318-এর আলোচনায় Guido van Rossum বলেছিলেন, তাঁর একটি “অন্তরের অনুভূতি” (gut feeling) আছে যে সীমা রাখা ভালো — যেকোনো expression অনুমতি দিলে মানুষ @-এর পরে জটিল, অপাঠ্য কোড লিখবে। সাবধানী থাকার আরেকটি যুক্তি: শুরুতে সীমা রাখলে পরে শিথিল করা সহজ, কিন্তু একবার শিথিল করলে আর ফেরানো যায় না, কারণ পুরোনো কোড ভাঙবে।
বাস্তবে সীমাটি সবচেয়ে বেশি বিরক্ত করত GUI প্রোগ্রামারদের। PyQt-এর মতো লাইব্রেরিতে একটি বোতামের “clicked” সংকেতে ফাংশন যুক্ত করতে স্বাভাবিকভাবে লিখতে ইচ্ছা হয় @buttons[0].clicked.connect — কিন্তু index থাকায় এটি নিষিদ্ধ ছিল। মানুষ ঘুরপথ বের করেছিল: আগে একটি অস্থায়ী নামে রাখা (btn0 = buttons[0].clicked.connect, তারপর @btn0), অথবা একটি “identity” ফাংশন বানানো — def _(x): return x, তারপর @_(buttons[0].clicked.connect) — যা ডাকের নিয়ম মেনেই যেকোনো expression ঢুকিয়ে দেয়। অর্থাৎ সীমাটি কাউকে থামাতে পারেনি, শুধু কোড আরও কুৎসিত করেছে।
এই যুক্তিতেই Brandt Bucher লেখেন PEP 614 — “Relaxing Grammar Restrictions On Decorators”, যা Python 3.9 (২০২০)-এ গৃহীত হয়। এখন @-এর পরে যেকোনো বৈধ expression লেখা যায় (ব্যাকরণের ভাষায়, যা একটি assignment expression হতে পারে)। Python-এর মূল নিয়ম একই রইল — “expression-টি হিসাব করো, ফল দিয়ে ফাংশন মোড়াও” — শুধু কোন expression লেখা যাবে, সেই তালিকা বড় হলো।
একটি সময়গত তথ্যও লক্ষ করার মতো: Python 3.9-এ CPython নতুন PEG parser-এ চলে যায় (PEP 617), যা ব্যাকরণে এ ধরনের পরিবর্তন সহজ করে দেয়। তবে PEP 614-এর যুক্তি ছিল মূলত ব্যবহারিক — কৃত্রিম সীমা যখন শুধু ঘুরপথ তৈরি করে, তখন সেটি রাখার কারণ নেই।
তবু Guido-র সতর্কতাটি এখনো মূল্যবান। নিয়ম শিথিল হয়েছে বলেই @(lambda f: retry(3)(log(f))) লেখা উচিত নয়। এই স্বাধীনতা পরিষ্কারতার জন্য ব্যবহার করুন — যেমন GUI-র সেই index-ওয়ালা লাইন — জটিলতার জন্য নয়।
মূল উপলব্ধি: ভাষার নকশায় “শুরুতে কড়া, পরে শিথিল” একটি সাধারণ কৌশল। একটি সীমা যখন বাস্তবে শুধু ঘুরপথ জন্ম দেয়, তখন সেটি তুলে দেওয়াই ভালো — কিন্তু পাঠযোগ্যতার মূল উদ্বেগটি প্রোগ্রামারের দায়িত্বে রয়ে যায়।
প্র ০৪Memoization-এর ধারণা কত পুরোনো? Python-এ lru_cache (3.2) আর cache (3.9) কেন আর কীভাবে এল, আর “LRU” নীতিটি কেন বেছে নেওয়া হলো?
“Memoization” শব্দটি এসেছে ব্রিটিশ কম্পিউটার বিজ্ঞানী ও কৃত্রিম বুদ্ধিমত্তার পথিকৃৎ Donald Michie-র কাছ থেকে। ১৯৬৮ সালে Nature পত্রিকায় তাঁর লেখা “'Memo' Functions and Machine Learning”-এ তিনি এমন ফাংশনের কথা বলেন, যা নিজের আগের উত্তর মনে রাখে — “memo” মানে মনে রাখার চিরকুট। তাঁর কাছে এটি ছিল শেখার একটি সরল রূপ: যন্ত্র অভিজ্ঞতা থেকে দ্রুত হয়। এই কৌশলটি dynamic programming-এর খুব কাছের আত্মীয় — Richard Bellman ১৯৫০-এর দশকে যে পদ্ধতি দাঁড় করান, তার মূল ভাবনাও “ছোট সমস্যার উত্তর একবার হিসাব করে রেখে দাও”.
Python-এ বহু বছর ধরে মানুষ নিজের হাতে memoization লিখত — একটি global dict, বা পর্ব ২০-এর সেই _memo={} mutable default-এর কৌশল, বা নিজের লেখা একটি @memoize decorator. প্রায় প্রতিটি বড় প্রজেক্টে এর একটি নিজস্ব সংস্করণ থাকত, আর বেশিরভাগেরই একই দুর্বলতা: খাতা অসীম বাড়ে, thread-এ নিরাপদ নয়, আর কতটা কাজে লাগছে তা মাপার উপায় নেই।
এই অবস্থায় Python core developer Raymond Hettinger functools.lru_cache যোগ করেন, যা Python 3.2 (২০১১)-এ প্রকাশিত হয়। তিনটি জিনিস একসাথে এল: আকারের সীমা (maxsize, default ১২৮), পরিসংখ্যান (cache_info()), আর খাতা খালি করা (cache_clear())। পরে এর ভেতরের কাজ C-তে লেখা হয় গতি বাড়াতে। Python 3.8 থেকে বন্ধনী ছাড়া @lru_cache লেখাও চলে। আর Python 3.9-এ এল functools.cache — আসলে lru_cache(maxsize=None)-এর একটি সহজ নাম, কারণ বহু মানুষ সীমাহীন cache-ই চাইতেন আর প্রতিবার maxsize=None লিখতেন। সীমা না থাকলে “কোনটি ফেলব” হিসাবের দরকার নেই, তাই এটি একটু দ্রুতও।
LRU কেন? খাতা ভরে গেলে কোন উত্তরটি ফেলবেন — এটি cache-নকশার পুরোনো প্রশ্ন, CPU cache আর অপারেটিং সিস্টেমের page replacement-এ দশকের পর দশক ধরে পরীক্ষিত। আদর্শ উত্তর হলো “যেটি সবচেয়ে দেরিতে আবার লাগবে”, কিন্তু ভবিষ্যৎ জানা যায় না। LRU একটি ভালো অনুমান করে: যেটি সবচেয়ে বেশি সময় ধরে ব্যবহার হয়নি, সেটি সম্ভবত শিগগির লাগবেও না — কারণ বাস্তব প্রোগ্রামে সাম্প্রতিক জিনিস আবার ব্যবহারের প্রবণতা থাকে (locality). আর এটি দক্ষতার সাথে বাস্তবায়ন করা যায়: একটি dict আর একটি doubly linked list দিয়ে প্রতিটি কাজ গড়ে ধ্রুব সময়ে।
সীমাবদ্ধতাও জেনে রাখুন। lru_cache thread-নিরাপদ এই অর্থে যে খাতা কখনো ভাঙে না, কিন্তু দুটি thread একই সময়ে একই নতুন ইনপুট চাইলে আসল ফাংশন দুবার চলতে পারে। আর method-এ এটি self-কে ধরে রাখে — ruff/flake8-bugbear-এর মতো linter এ জন্য সতর্ক করে।
মূল উপলব্ধি: @lru_cache একটি ৫০ বছরের পুরোনো ধারণার (memoization) আর একটি পরীক্ষিত নীতির (LRU) এক লাইনের রূপ। Decorator হওয়ায় এটি ফাংশনের কোড না ছুঁয়েই লাগানো বা সরানো যায় — cross-cutting concern-এর নিখুঁত উদাহরণ।
প্র ০৫“Aspect-oriented programming” কী, আর “cross-cutting concern” সমস্যাটির সমাধানে Python-এর decorator কোথায় দাঁড়ায়?
১৯৯০-এর দশকে বড় সফটওয়্যারে একটি সমস্যা স্পষ্ট হয়ে ওঠে। Object-oriented নকশা কোডকে সুন্দরভাবে ভাগ করে “জিনিস” অনুযায়ী — Account, Customer, Transaction. কিন্তু কিছু কাজ কোনো একটি জিনিসের নয়: লগ রাখা, নিরাপত্তা যাচাই, ডেটাবেসের transaction, cache, পারফরম্যান্স মাপা। এগুলো শত শত method-এর ভেতরে ছড়িয়ে থাকে — একই কোড সব জায়গায় (scattering), আর প্রতিটি method-এর আসল কাজ এসব পাহারাদারি কোডের সাথে জট পাকায় (tangling). এদেরই বলে cross-cutting concern — এই পর্বের অংশ ১-এর wallet সমস্যা।
এর একটি পদ্ধতিগত সমাধান প্রস্তাব করেন Gregor Kiczales আর তাঁর সহকর্মীরা, Xerox PARC গবেষণাগারে। ১৯৯৭ সালের ECOOP সম্মেলনে তাঁদের গবেষণাপত্র “Aspect-Oriented Programming” এই নামটি পরিচিত করে। ভাবনাটি: cross-cutting কাজটিকে একটি আলাদা একক — aspect — হিসেবে একবার লিখুন, আর একটি নিয়ম দিয়ে বলে দিন সেটি কোথায় কোথায় প্রয়োগ হবে। তাঁদের শব্দভান্ডার: join point (প্রোগ্রামের যে বিন্দুতে কিছু জোড়া যায়, যেমন একটি method ডাক), pointcut (join point বাছাইয়ের নিয়ম, যেমন “Account class-এর সব public method”), advice (যা জোড়া হবে — before, after, বা around), আর weaving (aspect-কে মূল কোডের সাথে বুনে দেওয়া)। Java-র জন্য এর বাস্তব রূপ AspectJ, যা পরে Eclipse প্রকল্পের অংশ হয়। আজ Java-র Spring framework-এ @Transactional বা নিরাপত্তার annotation-গুলো এই ধারণারই উত্তরসূরি।
Python-এর decorator এই কাঠামোয় কোথায়? একটি wrapper হলো খাঁটি “around advice” — আসল কাজের আগে, পরে, এমনকি বদলে কিছু করা। কিন্তু একটি বড় পার্থক্য আছে: Python-এ কোনো pointcut নেই। প্রতিটি ফাংশনের ওপরে নিজের হাতে @log লিখতে হয়। AOP-র সমর্থকরা একে বলবেন সীমাবদ্ধতা; Python-এর দর্শনে এটি গুণ — PEP 20-এর “explicit is better than implicit”. ফাংশন পড়লেই চোখের সামনে দেখা যায় তার ওপর কী কী প্রয়োগ হচ্ছে।
AspectJ-ধাঁচের pointcut-এর বিরুদ্ধে প্রধান সমালোচনাও ঠিক এটাই: একটি method পড়ে বোঝা যায় না অন্য কোনো ফাইলের কোনো aspect তার আচরণ বদলে দিচ্ছে কি না — একে কেউ কেউ “দূর থেকে কাজ” বলেন। ডিবাগ কঠিন হয়, কারণ যা চলছে তা কোডে লেখা নেই। অংশ ৮-এর “লুকানো জাদু”-র চরম রূপ। Python চাইলে এ ধরনের জাদু করতে পারে (metaclass বা monkey-patching দিয়ে সব method স্বয়ংক্রিয়ভাবে মোড়ানো), কিন্তু Python-সমাজে এটি সাধারণত নিরুৎসাহিত।
আপনি এই পর্বে যত decorator দেখলেন — লগ, গণনা, সময়, অনুমতি, retry, cache — প্রতিটিই একটি cross-cutting concern. AOP-র মূল শিক্ষাটি তাই decorator ব্যবহারে প্রাসঙ্গিক: পাহারাদারি কোড আসল কাজ থেকে আলাদা রাখুন, একবার লিখুন, কিন্তু কোথায় প্রয়োগ হচ্ছে তা যেন দৃশ্যমান থাকে।
মূল উপলব্ধি: Decorator হলো aspect-oriented ভাবনার একটি হালকা, স্পষ্ট রূপ — “around advice”, কিন্তু অদৃশ্য pointcut ছাড়া। ক্ষমতা কিছুটা কম, কিন্তু কোড পড়েই বোঝা যায় কী ঘটছে — Python এই বিনিময়টিই বেছে নিয়েছে।
অনুশীলন
বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।
-
কখন কী ছাপা হয়: একটি decorator
announce(func)-এর ভেতরে প্রথম লাইনেprint("মোড়ানো হচ্ছে", func.__name__), তারপর একটি wrapper যার প্রথম লাইনprint("ডাকা হচ্ছে"), তারপরreturn func(). ফাইলে@announceদিয়েdef hello(): return "hi"লেখা, আর শেষেhello()দুবার ডাকা। খাতায় পুরো আউটপুট লিখুন, ক্রমসহ।আউটপুট তিন লাইন:
মোড়ানো হচ্ছে hello, তারপরডাকা হচ্ছে,ডাকা হচ্ছে. প্রথম লাইনটি আসেdef helloচলার মুহূর্তে — decorator ফাংশনটি সংজ্ঞার সময় একবারই চলে,hello()কেউ না ডাকলেও। প্রতিটি ডাকে চলে শুধু wrapper, তাই “ডাকা হচ্ছে” দুবার। লক্ষ করুন,"hi"ছাপা হয় না, কারণhello()-এর ফল কোথাওprintকরা হয়নি। এই কারণেই বড় প্রজেক্টে decorator-এর বাইরের অংশে ভারী কাজ (ডেটাবেস খোলা ইত্যাদি) রাখলে import ধীর হয়ে যায়। -
বাগ খুঁজুন: একটি AI লিখেছে —
def safe(func):, ভেতরেdef wrapper(*args, **kwargs):, যার bodytry: func(*args, **kwargs)আরexcept Exception as e: print("error:", e), শেষেreturn wrapper. এটি@safe def transfer(amount): return amount - 10-এর ওপর লাগানো। এখানে কয়টি সমস্যা দেখছেন?transfer(100)কী ফেরত দেবে?তিনটি সমস্যা। (১) ফল ফেরত নেই:
func(...)ডাকা হয়েছে কিন্তুreturnকরা হয়নি, তাইtransfer(100)ফেরত দেয়None, 90 নয় — সফল ডাকেও ফল হারায়। (২) error গিলে ফেলা: যেকোনো exception ধরে শুধু ছাপা হচ্ছে, তারপরওNoneফেরত — ব্যর্থ লেনদেন বাকি প্রোগ্রামের কাছে অদৃশ্য। (৩)functools.wrapsনেই:transfer.__name__হবে'wrapper'. ঠিক রূপ:@functools.wraps(func)যোগ,return func(*args, **kwargs), আর except-এ লগ করে খালিraise— অথবা এমন decorator-ই না রাখা। -
ক্রম ভাবুন: একটি admin রিপোর্ট ফাংশনে তিনটি decorator লাগাতে হবে —
@require_role("admin"),@timerআর@functools.lru_cache(maxsize=32). চান: (ক) অনুমতি ছাড়া কেউ cache থেকেও উত্তর পাবে না, (খ) timer শুধু আসল হিসাবের সময় মাপবে, cache থেকে আসা উত্তরের নয়। ওপর থেকে নিচে কোন ক্রমে লিখবেন, আর কেন?ওপর থেকে নিচে:
@require_role("admin"), তারপর@functools.lru_cache(maxsize=32), তারপর@timer. অর্থাৎreport = require_role("admin")(lru_cache(maxsize=32)(timer(report))). ডাক এলে প্রথমে অনুমতি যাচাই (সবচেয়ে বাইরে) — তাই cache-এ উত্তর থাকলেও অনুমতিহীন কেউ পৌঁছাতে পারে না। তারপর cache: উত্তর থাকলে সেখান থেকেই ফেরত, timer চলেই না। না থাকলে timer-এর wrapper চলে আর আসল হিসাবের সময় মাপে।lru_cacheসবার ওপরে দিলে অনুমতিপ্রাপ্ত একজনের ক্যাশ করা উত্তর অনুমতিহীন কেউ পেয়ে যেতে পারত — একটি বাস্তব নিরাপত্তা-ছিদ্র। (আরও একটি সূক্ষ্মতা: cache-এর খাতার চাবিতেuserargument-ও থাকবে, তাই user dict হলে hashable নয় — বাস্তবে user id দেওয়াই ভালো।) -
একটি factory লিখুন (এবং AI-কে যাচাই করুন):
@limit_calls(n)লিখুন — মোড়ানো ফাংশনটি সর্বোচ্চnবার চলবে, তারপর প্রতিটি ডাকেRuntimeErrorদেবে (যেমন বিনামূল্যের একটি API প্ল্যানে দিনে ৩টি ডাক)। তারপর একই কাজ কোনো AI-কে দিয়ে লিখিয়ে অংশ ৯-এর চেকলিস্ট ধরে মিলিয়ে নিন।একটি সম্ভাব্য সমাধান:
import functools def limit_calls(n): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if wrapper.calls >= n: raise RuntimeError(f"{func.__name__}: {n} বারের সীমা শেষ") wrapper.calls += 1 return func(*args, **kwargs) wrapper.calls = 0 return wrapper return decorator @limit_calls(3) def ask_api(question): return f"উত্তর: {question}"তিন স্তর:
limit_calls(3)সেটিং নেয়,decoratorফাংশন নেয়,wrapperপ্রতিটি ডাক সামলায়। গণনা রাখা হয়েছে wrapper-এর attribute-এ, তাই প্রতিটি মোড়ানো ফাংশনের নিজস্ব গণনা — দুটি ফাংশনে@limit_calls(3)দিলে একটির ডাক অন্যটির সীমা খায় না। সীমা পার হলে আসল ফাংশন চলেই না।nonlocal countদিয়েও লেখা যায় (পর্ব ২০)। AI-এর কোডে খেয়াল করুন: গণনা কি একটি global-এ রাখা (তাহলে সব ফাংশন একই সীমা ভাগ করবে)?wrapsআছে? ফলreturnহচ্ছে?
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রম@log লেখা একটি def pay ফাংশন সংজ্ঞায়িত হয়ে তারপর একবার ডাকা হলে যে ক্রমে ঘটনাগুলো ঘটে, সেই ক্রমে সাজান (▲▼ চেপে সরান)
- def চলে আসল ফাংশন object তৈরি হয়
- Python আসল ফাংশন দিয়ে log ডাকে
- log ভেতরে wrapper বানিয়ে ফেরত দেয়
- pay নামটি wrapper-এর গায়ে লাগে
- pay(...) ডাকলে wrapper চলে, আগের কাজ করে
- wrapper আসল ফাংশন ডেকে তার ফল ফেরত দেয়
Python-এ একটি ফাংশন সংজ্ঞার ঠিক ওপরে @audit লেখা থাকলে সেটি কোন লেখার সমান?
- ফাংশনের পরে
audit()ডাকা - ফাংশনের body-র শুরুতে
audit-এর কোড কপি করে বসানো f = audit(f)- প্রতিবার
f()ডাকার সময়audit(f)()ডাকা
@a, তার নিচে @b, তার নিচে def f(): ... লেখা। এটি কোনটির সমান, আর ডাকার সময় কার wrapper আগে চলে?
f = a(b(f)); ডাকলে a-র wrapper আগে চলেf = b(a(f)); ডাকলে b-র wrapper আগে চলেf = a(b(f)); ডাকলে b-র wrapper আগে চলে- a আর b আলাদা দুটি কপি বানায়, যেকোনোটি আগে চলতে পারে
একটি decorator-এর wrapper-এ functools.wraps ব্যবহার করা হয়নি। মোড়ানো ফাংশনের নাম report হলে report.__name__ কী দেখাবে?
'report'- decorator ফাংশনের নিজের নাম
AttributeError'wrapper'
Python-এ @retry(times=3)-এর মতো argument-সহ decorator-এ retry(times=3) ডাকলে কী ফেরত আসে?
- মোড়ানো ফাংশনের প্রথম চেষ্টার ফল
- একটি decorator — যে ফাংশন নিয়ে wrapper ফেরত দেবে
- সরাসরি wrapper ফাংশন
SyntaxError, কারণ@-এর পরে ডাক লেখা যায় না
একটি decorator-এর wrapper-এ func(*args, **kwargs) ডাকা আছে, কিন্তু তার আগে return লেখা নেই। মোড়ানো def add(a, b): return a + b-এ add(2, 3) কী ফেরত দেয়?
5TypeErrorNone- wrapper ফাংশন object
@functools.lru_cache দেওয়া একটি ফাংশনকে একটি list argument (যেমন f([1, 2])) দিয়ে ডাকলে কী হয়?
- স্বাভাবিকভাবে cache হয়, পরের একই ডাক খাতা থেকে আসে
TypeError: unhashable type: 'list'- list নিজে থেকে tuple-এ বদলে cache হয়
- cache ছাড়াই আসল ফাংশন চলে, কোনো সতর্কতা ছাড়া
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Decorator = যে ফাংশন একটি ফাংশন নিয়ে তার চারপাশে আচরণ মুড়ে নতুন ফাংশন ফেরত দেয়; মূল কোড অক্ষত।
@dমানেf = d(f), আর decorator চলে একবারই — সংজ্ঞার সময়। - Wrapper-এর নিয়ম:
*args, **kwargsনিন, আসল ফাংশনে হুবহু পাঠান, ফলreturnকরুন, exception গিলবেন না — আর সবসময়@functools.wraps(func). - Argument-সহ decorator তিন স্তরের: factory → decorator → wrapper.
@retry(times=3)মানেf = retry(times=3)(f). - Stack করলে নিচেরটি আগে মোড়ায়, ওপরেরটি আগে চলে (
f = a(b(f))); অনুমতি, cache আর লগের ক্রম ভেবে সাজান। lru_cache/cachepure ফাংশনের পুনরাবৃত্ত হিসাব বাঁচায় (fib(10): ১৭৭ থেকে ১৯ ডাক); argument hashable হতে হয়।@property,@classmethod,@dataclass— সবই একই নিয়মের decorator. সরল রাখুন: এক কাজ, দৃশ্যমান, দু-তিনটির বেশি stack নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৬ · context manager — with-এর ভেতরের কথাপরবর্তী পর্বসম্পদ খোলা-বন্ধের নিশ্চিত ব্যবস্থা,
__enter__/__exit__আর@contextmanager. - Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- পর্ব ২০ · scope, *args/**kwargs, closure আর default-এর ফাঁদঝালিয়ে নিনclosure, cell আর argument ফরোয়ার্ড করা — decorator-এর তিন ভিত্তি।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্কপ্রয়োগ দেখুনযেখানে route, middleware আর অনুমতি যাচাই decorator দিয়েই লেখা হয়।