পাইথন ডেকরেটর‍্স‍

“ডেকরেটর! ওই যে বিভিন্ন অনুষ্ঠানের সাজ সজ্জা করে মানে ডেকরেট করে….. তো পাইথনে আবার তাদের কি কাজ!”

পোস্টের টাইটেল দেখে যদি আপনার মনে এরকম প্রশ্নের উদয় হয় তাহলেও দোষের কিছু নেই। পাইথনে ডেকরেটর কিছুটা এডভান্স আর কমপ্লেক্স টপিক। তবে চিন্তা নেই, এই পোস্টে আমরা একটু সহজ ভাবে, ধাপে ধাপে জানার চেষ্টা করবো পাইথনে ডেকরেটর জিনিষটা কী, কীভাবে কাজ করে আর কীভাবেই বা ব্যাবহার করে।

Python Decorator

ঠিক ঠাক ভাবে বললে, ডেকরেটর হল এক ধরনের callable যা অন্য callable এর ফাংশনালিটিকে মডিফাই করে। আরেকটু সহজ করে বললে, ডেকরেটর হল এক ধরনের ফাংশন যা অন্য ফাংশনের ফাংশনালিটিকে মডিফাই করে। [ডেকরেটর ক্লাসও হয়, তবে এ পোস্টে সেটা আলোচনা করবো না।] তো ধাপে ধাপে শুরু করা যাক। আমি ধরে নিচ্ছি ভ্যারিয়েবলের স্কোপ রেজ্যুলেশন সম্পর্কে আমাদের মোটামুটি ভাল ধারনা আছে, তাই এ সম্পর্কে আর বাড়তি কিছু লিখলাম না।

ফাংশনের কারিকুরি

এই কোড ব্লক টা দেখি:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def hello():
    print("Hello World!")


hello()
# output: Hello World!

hi = hello    # interesting!

print(hi)
# output: <function hello at 0x7ff330b412f0>

hi()
# output: Hello World!

8 নম্বর লাইন থেকে কোডটা ইন্টারেস্টিং হওয়া শুরু করেছে। আমরা জানি পাইথনে সব কিছুই এক একটা অবজেক্ট। ফাংশনও। এই লাইনে আমরা hello কে hi তে এসাইন করেছি। লক্ষনীয়, এখানে hello এর পাশে () (ব্র্যাকেট/প্যারেন্থেসিস) দেই নি। অর্থাৎ এখানে hello ফাংশনটি এক্সিকিউট বা কল হয় নি। 10 নম্বর লাইনের আউটপুট দেখলে ব্যাপারটা আরো পরিষ্কার হবে। আর 13 নম্বর লাইনে hi কে কল করা হয়েছে, আউটপুট পেয়েছি ঠিক hello এর মত।

ফাংশনের ভেতর ফাংশন!

হ্যা, পাইথনে আমরা ফাংশনের ভেতর ফাংশন ডিফাইন করতে পারি। অন্যভাবে বললে আমরা নেস্টেড ফাংশন বানাতে পারি। এরকম:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def hello():
    print("Inside hello")

    def nested():
        print("Inside nested")

    nested()
    print("Inside hello again")


hello()

আউটপুট:

Inside hello
Inside nested
Inside hello again

আরেকটি কোড ব্লক দেখি:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def count(value):

    def increment():
        return value + 1

    print("Incremented value of {} is {}".format(value, increment()))


count(5)
# output: Incremented value of 5 is 6

শুরুতেই লিখেছিলাম ভ্যারিয়েবলের স্কোপ রেজ্যুলেশন সম্পর্কে লিখবো না। কোডটা একটু ভাল মত লক্ষ্য করলেই আশা করি বুঝতে পারবেন।

ফাংশন থেকে ফাংশন রিটার্ন!

ফাংশন থেকে ইচ্ছা করলে আমরা ফাংশন রিটার্নও করতে পারি! এই কোডটা দেখলে ব্যাপারটা পরিষ্কার হবে:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def hello():

    def nested():
        print("Inside nested")

    return nested


hi = hello()

print(hi)
# output: <function hello.<locals>.nested at 0x7f1ed4e94488>

hi()
# output: Inside nested

9 নম্বর লাইনে hello() কে কল করায় এটা nested কে রিটার্ন করেছে, তা এসাইন হয়েছে hi তে। পূর্বের কোড ব্লক গুলো ফলো করলে এটি সহজেই বোঝা যাবে।

আরেকটি কোড ব্লক দেখি:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def count(value):

    def increment(n):
        return value + n

    return increment


counter = count(5)
print(counter(1))    # Guess the output!

কি, আউটপুট গেস করেছেন? এটা আর এক্সপ্লেইন করবো না। আউটপুট হবে 6।

ফাংশনের আর্গুমেন্ট/প্যারামিটার হিসেবে ফাংশন

সরাসরি একটা কোড স্নিপেট দেখে ফেলি:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def hello():
    print("Hello World!")


def hi(func):
    print("Hi!")
    func()


hi(hello)

এখানে hi ফাংশনের প্যারামিটার হিসেবে hello কে পাস করা হয়েছে। hi এর ভেতর hello কল হয়েছে। আউটপুট হবে এরকম:

Hi!
Hello World!

ডেকরেটর

এখন হচ্ছে মূল বিষয়, ডেকরেটর। আমরা আগেই জেনেছি, ডেকরেটর হচ্ছে এমন ফাংশন যা অন্য ফাংশনের ফাংশনালিটি মডিফাই করে। এখন তাহলে একটু সাজানো গুছানো উদাহরণ দেখে নেই:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def mydecorator(func):

    def wrapper():
        print("Before calling func()")
        func()
        print("After calling func()")

    return wrapper


def hello():
    print("Hello World!")


hello = mydecorator(hello)
hello()

এখানে mydecorator ফাংশনটি প্যারামিটার হিসেবে আরেকটি ফাংশন এক্সপেক্ট করছে। এর মধ্যকার wrapper ফাংশনটিতে প্যারামিটারে পাওয়া ফাংশন কল করার আগে এবং পরে কিছু কাজ হচ্ছে। আর mydecorator থেকে wrapper কে রিটার্ন করা হচ্ছে। ১৫ নম্বর লাইনে mydecorator কে hello প্যারামিটার দিয়ে কল করা হয়েছে। রিটার্ন ভ্যালু এসাইন করা হয়েছে আবার hello তে। অর্থাৎ mydecorator এর মাধ্যমে hello মডিফাই হয়েছে। [প্রয়োজনে আবার খেয়াল করুন।] সব শেষ লাইনে hello() কল হয়েছে। আউটপুট হবে এরকম:

Before calling func()
Hello World!
After calling func()

নাম এবং কাজ দেখে বোঝাই যাচ্ছে mydecorator হচ্ছে আমাদের কাঙ্খিত সেই ডেকরেটর।

তবে ডেকরেটর ব্যাবহারের সুন্দর একটি সিনট্যাক্স আছে, @। উপরের কোড কে আমরা সুন্দর করে এভাবে লিখতে পারি:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def mydecorator(func):

    def wrapper():
        print("Before calling func()")
        func()
        print("After calling func()")

    return wrapper


@mydecorator
def hello():
    print("Hello World!")


hello()

অর্থাৎ, hello = mydecorator(hello) এই লাইনের পরিবর্তে আমরা hello ফাংশনটি ডিফাইনের ঠিক আগে @mydecorator লিখেছি। পূর্বের মত একই কাজ হবে।

বাস্তব উদাহরণ

এবার একটি বাস্তব উদাহরণ দেখা যাক। মনে করি আমাদের একটি ফাংশন আছে, আমরা চাই যখন এটি কল হবে ঠিক ওই সময় যেন লগ হিসবে একটা ফাইলে থাকে। এর সমাধান দেখে নেয়া যাক:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime
from time import sleep


def log(func):

    def wrapper():
        with open('log.txt', 'a') as file:
            now = datetime.now().strftime("%I:%M:%S%p on %d %B, %Y")
            file.write(now + '\n')
        func()

    return wrapper


@log
def hello():
    print('Hello World!')


for i in range(3):
    hello()
    sleep(5)

এই প্রোগ্রাম রান করলে কারেন্ট ওয়ার্কিং ডিরেক্টরিতে log.txt নামের একটা ফাইল তৈরি হবে, সেটি খুললে hello() এক্সিকিউট হওয়ার সময় গুলো পাওয়া যাবে।

প্যারামিটার/আর্গুমেন্ট সহ ডেকরেটর

যদি log ডেকরেটর টায় প্যারামিটার হিসেবে ফাইলের নাম দিয়ে দেয়া যেত, log.txt এর পরিবর্তে আমাদের প্রয়োজন মত নাম, তাহলে সুবিধা হতো না? হ্যা, ডেকরেটরে প্যারামিটার/আর্গুমেন্ট পাস করা সম্ভব। এজন্য আমাদের ডেকরেটরকে আরেকটা ফাংশনের মধ্যে নেস্টেড আকারে রাখা লাগবে। এরকম:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime
from time import sleep


def log(filename='log.txt'):
    def log_decorator(func):
        def wrapper():
            with open(filename, 'a') as file:
                now = datetime.now().strftime("%I:%M:%S%p on %d %B, %Y")
                file.write(now + '\n')
            func()
        return wrapper
    return log_decorator


@log(filename='history.txt')
def hello():
    print('Hello World!')


for i in range(3):
    hello()
    sleep(5)

এখন আমাদের লগ history.txt খুললে পাওয়া যাবে। আর যদি @log এ কোন প্যারামিটার পাস না করা হয়, তাহলে ডিফল্ট ভাবে log.txt তে লগ থাকবে।

এইত! এই ছিল পাইথনের ডেকরেটর‍্স‍ কনসেপ্ট। যদিও যেভাবে উপস্থাপন করতে চেয়েছিলাম সেভাবে পারি নি, তারপরও আশা করছি অপেক্ষাকৃত নতুনেরা উপকৃত হবে। সামনে কোন এক সময় ক্লাস ডেকরেটর নিয়ে লিখবো ইনশাআল্লাহ।

Load Comments?