Lesson ১৫৮ মিনিট পড়া

ডেকোরেটর এবং ক্লোজার (Decorators & Closures)

ফাংশনের কোড স্পর্শ না করেই সেটিতে সুপারপাওয়ার যোগ করুন

ফাংশন হলো ফার্স্ট-ক্লাস সিটিজেন বা প্রথম শ্রেণীর নাগরিক (Functions Are First-Class Citizens)

পাইথনে ফাংশনগুলো অন্য যেকোনো ভ্যালুর মতোই কেবল একেকটি অবজেক্ট (object)। আপনি এগুলোকে ভ্যারিয়েবলে সংরক্ষণ করে রাখতে পারেন, আর্গুমেন্ট হিসেবে পাস করতে পারেন এবং অন্য ফাংশন থেকে রিটার্নও করতে পারেন। এটি প্রথমে অদ্ভুত লাগতে পারে, তবে এটিই এই লেসনের সবকিছুর মূল ভিত্তি।

এভাবে চিন্তা করুন: একটি ফাংশন হলো একটি রেসিপি কার্ডের (recipe card) মতো। আপনি চাইলে কার্ডটি অন্য কাউকে দিতে পারেন (আর্গুমেন্ট হিসেবে পাস করা), এর ফটোকপি করতে পারেন (অন্য ভ্যারিয়েবলে অ্যাসাইন করা), বা এমনকি একটি ফ্যাক্টরির ভেতরে নতুন রেসিপি কার্ড তৈরি করতে পারেন (একটি ফাংশন থেকে অন্য ফাংশন রিটার্ন করা)।

অবজেক্ট হিসেবে ফাংশন (Functions as Objects)

# Functions can be assigned to variables
def shout(text):
return text.upper() + "!!!"
yell = shout # No parentheses — we're passing the function itself
print(yell("hello"))
# Functions can be passed as arguments
def apply_twice(func, value):
return func(func(value))
def add_exclamation(text):
return text + "!"
result = apply_twice(add_exclamation, "wow")
print(result) # wow + ! + !
# Functions can be stored in data structures
operations = {
"double": lambda x: x * 2,
"square": lambda x: x ** 2,
"negate": lambda x: -x,
}
for name, func in operations.items():
print(f"{name}(5) = {func(5)}")
Output
HELLO!!!
wow!!
double(5) = 10
square(5) = 25
negate(5) = -5

ক্লোজার — যে ফাংশনগুলো মনে রাখে (Closures — Functions That Remember)

ক্লোজার (closure) হলো এমন একটি ফাংশন যা যেখানে তৈরি হয়েছিল, সেই জায়গার ভ্যারিয়েবলগুলোকে "মনে রাখে," এমনকি সেই জায়গাটির আর কোনো অস্তিত্ব না থাকলেও। এটি অনেকটা স্নোগ্লোবের (snowglobe) মতো — যার ভেতরের ছোট্ট জগতটি বরফে জমে আছে, কিন্তু আপনি এখনও বাইরে থেকে সেটিকে দেখতে পাচ্ছেন।

ক্লোজারের তৈরি তখনই হয় যখন একটি ভেতরের ফাংশন, তার বাইরের ফাংশনের স্কোপ (scope) থেকে একটি ভ্যারিয়েবল রেফারেন্স (reference) হিসেবে গ্রহণ করে। ভেতরের ফাংশনটি সেই ভ্যারিয়েবলগুলোকে ঘিরে ধরে বা ক্লোজ ওভার (closes over) করে এবং সেগুলোকে নিজের সাথে বহন করে নিয়ে যায়।

ক্লোজারের ব্যবহার (Closures in Action)

# A function that creates customized functions
def make_multiplier(factor):
def multiply(x):
return x * factor # 'factor' is remembered!
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(100)) # 200
# The closure remembers 'factor' even though make_multiplier finished!
print()
# Practical closure: a counter
def make_counter(start=0):
count = [start] # Using a list so the inner function can modify it
def increment():
count[0] += 1
return count[0]
return increment
page_views = make_counter()
print(f"Visit 1: {page_views()}")
print(f"Visit 2: {page_views()}")
print(f"Visit 3: {page_views()}")
Output
10
15
200

Visit 1: 1
Visit 2: 2
Visit 3: 3

ডেকোরেটর — মূল আকর্ষণ (Decorators — The Main Event)

ডেকোরেটর (decorator) হলো এমন ফাংশন যা অন্য একটি ফাংশন গ্রহণ করে, এতে কিছু বাড়তি কাজ বা আচরণ যোগ করে এবং একটি নতুন ও উন্নত করা ফাংশন রিটার্ন করে। এটি অনেকটা কোনো উপহারকে গিফট-র‍্যাপিং বা মোড়ক দিয়ে পেঁচানোর মতো — ভেতরের উপহারটি একই থাকে, তবে মোড়কটি এতে বাড়তি কিছু যোগ করে (যেমন একটি ফিতা, ট্যাগ, বা চুমকি)।

@decorator এর লেখার নিয়মটি কেবলই একটি সিনট্যাক্টিক সুগার (syntactic sugar)। কোনো ফাংশনের সংজ্ঞার ওপরে @my_decorator লেখা আর my_func = my_decorator(my_func) লেখা, দুটো আসলে একদম একই জিনিস।

একটি ডেকোরেটর তৈরি করা (Building a Decorator)

import functools
import time
# A simple timer decorator
def timer(func):
@functools.wraps(func) # Preserves the original function's name/docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # Call the original function
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_greeting(name):
"""Greet someone slowly."""
time.sleep(0.1) # Simulate slow work
return f"Hello, {name}!"
# Using the decorated function
result = slow_greeting("Anika")
print(result)
print(f"Function name: {slow_greeting.__name__}") # Thanks to @wraps
print(f"Docstring: {slow_greeting.__doc__}")
Output
slow_greeting took 0.1003s
Hello, Anika!
Function name: slow_greeting
Docstring: Greet someone slowly.

বাস্তব জীবনের ডেকোরেটর প্যাটার্নগুলো (Real-World Decorator Patterns)

প্রফেশনাল পাইথন কোডে ডেকোরেটরের দেখা সব জায়গায় মিলে। নিচে এর কিছু সাধারণ ব্যবহার বা প্যাটার্ন দেওয়া হলো:

  • লগিং (Logging) — ফাংশনগুলো কখন এবং কী কী আর্গুমেন্ট নিয়ে কল করা হলো তা নজরে বা ট্র্যাকে (track) রাখা
  • অ্যাউথেন্টিকেশন (Authentication) — কোনো ব্যবহারকারীর ফাংশনটিকে কল করার অনুমতি আছে কি না তা যাচাই করা
  • ক্যাশিং/মেমোইজেশন (Caching/Memoization) — একই কাজ বা হিসাব পুনরায় করা এড়াতে রেসাল্ট বা ফলাফল মনে রাখা
  • রিট্রাই লজিক (Retry logic) — কোনো অপারেশন ব্যর্থ হলে স্বয়ংক্রিয়ভাবে আবার বা রিট্রাই (retry) করার চেষ্টা করা
  • ভ্যালিডেশন (Validation) — ফাংশন রান করার আগে ইনপুট ঠিক আছে কি না তা যাচাই করা

প্র্যাকটিক্যাল ডেকোরেটরসমূহ (Practical Decorators)

import functools
# 1. Logging decorator
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_str = ", ".join(map(repr, args))
print(f"Calling {func.__name__}({args_str})")
result = func(*args, **kwargs)
print(f" → returned {result!r}")
return result
return wrapper
# 2. Simple memoization (caching) decorator
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 4)
print()
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Without memoize, fib(35) would take forever. With it: instant!
print(f"fib(10) = {fibonacci(10)}")
print(f"fib(35) = {fibonacci(35)}")
print()
# Python has a built-in version!
@functools.lru_cache(maxsize=128)
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
print(f"20! = {factorial(20)}")
Output
Calling add(3, 4)
  → returned 7

fib(10) = 55
fib(35) = 9227465

20! = 2432902008176640000

আর্গুমেন্টসহ ডেকোরেটর (Decorators with Arguments)

মাঝেমধ্যে আপনি কোনো ডেকোরেটরকে কনফিগার (configure) করতে চাইতে পারেন — যেমন সেটিকে জানিয়ে দেওয়া যে কতবার রিট্রাই (retry) করতে হবে, বা ঠিক কোন মেসেজটি লগ (log) করতে হবে। এর জন্য আপনার একটি ডেকোরেটর ফ্যাক্টরির (decorator factory) প্রয়োজন হবে: এমন একটি ফাংশন যা আর্গুমেন্ট গ্রহণ করে এবং একটি ডেকোরেটর রিটার্ন করে।

প্যারামিটারাইজড ডেকোরেটর (Parameterized Decorators)

import functools
def repeat(n):
"""Decorator factory: repeat a function n times."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # repeat(3) returns the actual decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("World")
print()
# Stacking decorators — they apply bottom-up
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold # Applied second (outer)
@italic # Applied first (inner)
def greet(name):
return f"Hello, {name}"
print(greet("Anika")) # bold(italic(greet))("Anika")
Output
Hello, World!
Hello, World!
Hello, World!

<b><i>Hello, Anika</i></b>
Note: আপনার ডেকোরেটরের র‍্যাপার (wrapper) ফাংশনের ভেতরে সবসময় @functools.wraps(func) ব্যবহার করবেন। এটি ছাড়া, ডেকোরেট করা ফাংশনটি তার আসল নাম এবং ডকস্ট্রিং (docstring) হারিয়ে ফেলে, যা ডিবাগিং করাকে এক দুঃস্বপ্নে পরিণত করে দেয়। এক লাইনের এই কোডটি আপনার ঘণ্টার পর ঘণ্টা বিভ্রান্তি বা কনফিউশন দূর করতে সাহায্য করবে।
চ্যালেঞ্জ

ছোট কুইজ

ক্লোজার বা Closure কী?
Lambda, Map & Filter