ডেকোরেটর হলো এমন ফাংশন, যা আরেকটি ফাংশনকে আর্গুমেন্ট হিসেবে নেয়, যাতে পাস করা ফাংশনটির আচরণ বাড়ানো বা প্রতিস্থাপন করা যায়।
যদি A ফাংশনটি একটি ডেকোরেটর হয় আর B ফাংশনটি তার আর্গুমেন্ট হয়, তাহলে A ফাংশনটি B ফাংশনের আচরণ পরিবর্তন, বর্ধিত বা প্রতিস্থাপন করে, অথচ B ফাংশনের কোড পরিবর্তন করে না।
আমরা বলি যে ডেকোরেটর ফাংশন A, B ফাংশনটিকে র্যাপ করে।
যদিও আমরা আচরণ "পরিবর্তনের" কথা বলি, র্যাপ করা ফাংশনটি আসলে পরিবর্তিত হয় না।
আচরণ হয় র্যাপ করা ফাংশনের চারপাশে (এবং এটি যা রিটার্ন করে তার চারপাশে) যোগ করা হয়, নয়তো র্যাপ করা ফাংশনের আচরণকে অন্য কোনো আচরণ দিয়ে প্রতিস্থাপন করা হয়।
একটি হায়ার-অর্ডার ফাংশন হলো এমন একটি ফাংশন, যা এক বা একাধিক ফাংশন আর্গুমেন্ট হিসেবে নেয় এবং/অথবা এক বা একাধিক ফাংশন রিটার্ন করে। অন্য একটি ফাংশনের আর্গুমেন্ট হিসেবে ব্যবহৃত বা সেখান থেকে রিটার্ন করা একটি ফাংশন হলো একটি ফার্স্ট-ক্লাস ফাংশন। একটি Python ফাংশন, একটি কলেবল অবজেক্ট হিসেবে, একটি ফার্স্ট-ক্লাস ফাংশন, যাকে অন্য যেকোনো মান বা অবজেক্টের মতোই একটি ভ্যারিয়েবলে রাখা যায় বা আর্গুমেন্ট হিসেবে ব্যবহার করা যায়। হায়ার-অর্ডার ফাংশন আর ফার্স্ট-ক্লাস ফাংশন একসাথে কাজ করে ডেকোরেটরকে সম্ভব করে তোলে।
ডেকোরেটর ফাংশনের নামের আগে @ চিহ্নটি বসানো হয় এবং যে ফাংশনটি ডেকোরেট করতে হবে তার ঠিক উপরে রাখা হয়, এভাবে:
@decorator
def decorated_function():
pass
কিছু ডেকোরেটর আর্গুমেন্ট গ্রহণ করে:
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
যদি কোনো ডেকোরেটরে ডিফল্ট আর্গুমেন্ট ডিফাইন করা থাকে, তাহলে ডেকোরেটরটি কাজ করার জন্য @decorator() কলে বন্ধনী ব্যবহার করতে হবে, যেমনটা যেকোনো ফাংশন কল করার সময় করেন:
@decorator_with_default_arg()
def decorated_function3():
pass
যদি কোনো ডেকোরেটর পজিশনাল আর্গুমেন্ট নেয়, আর আর্গুমেন্টগুলো সরবরাহ না করা হয়, তাহলে একটি এরর হবে, যা দেখতে কিছুটা এমন হবে:
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
@decorator সিনট্যাক্স হলো সিনট্যাকটিক সুগার বা একটি সংক্ষিপ্ত রূপ, যেখানে ডেকোরেটিং ফাংশনটি কল করা হয় এবং ডেকোরেট করা ফাংশনটি তার কাছে আর্গুমেন্ট হিসেবে পাস করা হয়।
ডেকোরেটর কল করার বিকল্প উপায়ের উদাহরণ নিচে দেওয়া হলো:
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
বেশিরভাগ ডেকোরেটরই অন্য একটি ফাংশনের আচরণ বাড়ানোর বা প্রতিস্থাপনের জন্য তৈরি, তবে কিছু ডেকোরেটর কেবল যে ফাংশনগুলোকে র্যাপ করছে সেগুলোই রিটার্ন করা ছাড়া আর কিছুই করে না।
ডেকোরেটর হলো এমন ফাংশন, যা অন্তত একটি আর্গুমেন্ট নেয়, অর্থাৎ যে ফাংশনটিকে তারা র্যাপ করছে সেটি। তারা সাধারণত হয় র্যাপ করা ফাংশনটি রিটার্ন করে, নয়তো র্যাপ করা ফাংশন ব্যবহার করে এমন একটি এক্সপ্রেশনের ফলাফল রিটার্ন করে।
একটি সরল ডেকোরেটর, যা কেবল তার র্যাপ করা ফাংশনটিই রিটার্ন করে, এভাবে লেখা যায়:
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
কোনো ডেকোরেটর কেবল সাইড এফেক্ট যোগ করতে পারে, যেমন লগিংয়ের জন্য ব্যবহৃত অতিরিক্ত তথ্য:
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
একটি ডেকোরেটর নিজেকে রিটার্ন করে না। এটি তার ফাংশন আর্গুমেন্ট, অন্য একটি ফাংশন, বা পাস করা বা ডেকোরেট করা ফাংশনের রিটার্নের জায়গা নেয় এমন এক বা একাধিক মান রিটার্ন করতে পারে। যদি কোনো ডেকোরেটর অন্য একটি ফাংশন রিটার্ন করে, তাহলে সাধারণত এটি একটি ইনার ফাংশন রিটার্ন করে।
একটি ফাংশনের ভেতরে আরেকটি ফাংশন ডিফাইন করা যায়। এমন নেস্টেড ফাংশনকে ইনার ফাংশন বলা হয়। একটি ডেকোরেটর তার ফাংশন আর্গুমেন্ট র্যাপ করতে একটি ইনার ফাংশন ব্যবহার করতে পারে। এরপর ডেকোরেটর তার ইনার ফাংশনটি রিটার্ন করে। এরপর ইনার ফাংশনটি মূল ফাংশন আর্গুমেন্টটি রিটার্ন করতে পারে।
ভ্যালিডেশনের জন্য ব্যবহৃত একটি ডেকোরেটরের উদাহরণ নিচে দেওয়া হলো:
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
প্রথম লাইনে আছে ডেকোরেটরের func আর্গুমেন্টের ডেফিনিশন।
পরের লাইনে আছে ডেকোরেটরের ইনার ফাংশনের ডেফিনিশন, যা func আর্গুমেন্টটিকে র্যাপ করে।
যেহেতু ইনার ফাংশনটি ডেকোরেটরের func আর্গুমেন্টকে র্যাপ করে, তাই func-কে যে আর্গুমেন্ট পাস করা হয়, ঠিক সেই আর্গুমেন্টই এর কাছেও পাস করা হয়।
খেয়াল করুন, func-এ যে আর্গুমেন্ট ডিফাইন করা হয়েছিল, র্যাপারের সেটির জন্য একই নাম ব্যবহার করা বাধ্যতামূলক নয়।
মূল ফাংশন planet ব্যবহার করে আর ডেকোরেটর world ব্যবহার করে, তবুও ডেকোরেটর কাজ করে।
ইনার ফাংশনটি হয় func রিটার্ন করে, নয়তো, যদি world == "Pluto" হয়, তাহলে প্রিন্ট করে যে প্লুটো একটি গ্রহ নয়।
চাইলে এর বদলে ValueError রেইজ করার মতো করেও কোড লেখা যেত।
তাই ইনার ফাংশনটি func-কে র্যাপ করে, আর হয় func রিটার্ন করে, নয়তো এমন কিছু করে যা func যা করত তার বিকল্প হিসেবে কাজ করে।
ডেকোরেটরটি তার ইনার ফাংশন রিটার্ন করে।
ইনার ফাংশনটি মূল, পাস করা ফাংশনটি রিটার্ন করতে পারে, আবার নাও করতে পারে।
র্যাপার ফাংশনে বা ইনার ফাংশনে কোন কোডটি শর্তসাপেক্ষে এক্সিকিউট হয় তার ওপর নির্ভর করে func রিটার্ন হতে পারে, একটি এরর রেইজ হতে পারে, অথবা func-এর রিটার্ন টাইপের একটি মান রিটার্ন হতে পারে।
*args আর **kwargs সিনট্যাক্স ব্যবহার করে যেকোনো সংখ্যক আর্গুমেন্ট নেয় এমন ফাংশনের জন্যও ডেকোরেটর লেখা যায়।
যেকোনো সংখ্যক আর্গুমেন্ট নেয় এমন একটি ফাংশনের জন্য ডেকোরেটরের উদাহরণ নিচে দেওয়া হলো:
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
এটি ফাংশন আর্গুমেন্টের রিটার্ন ভ্যালু দ্বিগুণ করার জন্য কাজ করে। আমরা যদি রিটার্ন ভ্যালু তিনগুণ, চারগুণ ইত্যাদি করতে চাই, তাহলে ডেকোরেটরের নিজের মধ্যেই একটি প্যারামিটার যোগ করতে পারি, যেমনটা পরের অংশে দেখানো হয়েছে।
এমন একটি ডেকোরেটরের উদাহরণ নিচে দেওয়া হলো, যাকে কনফিগার করে ডেকোরেট করা ফাংশনের রিটার্ন ভ্যালুকে যেকোনো গুণিতক দিয়ে গুণ করা যায়:
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
প্রথম লাইনগুলো যাচাই করে যে factor হলো 0 নয়।
এরপর আউটার র্যাপার ডিফাইন করা হয়।
এর সিগনেচার এমনই, যেটা আমরা প্যারামিটারবিহীন ডেকোরেটরের জন্য আশা করি।
আউটার র্যাপারের মধ্যে একটি ইনার ফাংশন আছে, যার সিগনেচার মূল ফাংশনের মতোই।
ইনার র্যাপারটি মূল ফাংশন থেকে রিটার্ন করা মানকে ডেকোরেটরে পাস করা আর্গুমেন্ট দিয়ে গুণ করার কাজটি করে।
আউটার র্যাপারটি ইনার র্যাপার রিটার্ন করে, আর ডেকোরেটরটি আউটার র্যাপার রিটার্ন করে।
এমন একটি প্যারামিটারযুক্ত ডেকোরেটরের উদাহরণ নিচে দেওয়া হলো, যা নিয়ন্ত্রণ করে যে মূল ফাংশনে পাস করা আর্গুমেন্টটি এটি ভ্যালিডেট করবে কি না:
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
এটি Pluto চেক করা আর না চেক করার মধ্যে সহজে টগল করার সুযোগ দেয়, আর তা করা হয় my_func পরিবর্তন না করেই।