Python Decorators: Arguments සහ functools.wraps - සම්පූර්ණ මාර්ගෝපදේශය | SC Guide

කොහොමද යාලුවනේ? ඔන්න අදත් ඔයාලට අලුත් දෙයක් කියලා දෙන්නයි මම ආවේ. Programmers ලා විදියට අපි හැමෝම code ලියනවා. ඒත් අපි ලියන code, clean, reusable, සහ extendable වෙන්න ඕනේ, නේද? ඒකට අපිට Python වල තියෙන powerful features ගොඩක් උදව් වෙනවා. Decorators කියන්නේ එහෙම එක විශේෂ feature එකක්. ගොඩක් දෙනෙක් Decorators ගැන දන්නවා ඇති, ඒත් සමහරවිට ඒක තව ටිකක් ගැඹුරට තේරුම් ගන්න ඕනේ වෙන්න ඇති.
අද අපි කතා කරන්නේ සාමාන්ය Decorators ගැන නෙවෙයි. Decorators වලට arguments දාන්නේ කොහොමද කියලා සහ ඒකෙන් අලුතින් ඇතිවෙන පොඩි ප්රශ්නයක් විසඳගන්න `functools.wraps` කොහොමද පාවිච්චි කරන්නේ කියලයි. මේක ඔයාලගේ Python skills වලට හොඳ boost එකක් වෙයි කියලා මම හිතනවා. මොකද මේ concepts තේරුම් ගත්තාම ඔයාලට reusable code ලියන්න, debugging කරද්දී වුණත් පහසුවෙන් වැඩ කරන්න පුළුවන් වෙනවා.
හරි, එහෙනම් වැඩේට බහිමුද? අදටත් කෝපි එකක් හරි තේ එකක් හරි ලෑස්ති කරගෙන ඉඳගන්න! 😉
1. Decorators කියන්නේ මොනවද? (සරල හැඳින්වීමක්)
කෙටියෙන් කිව්වොත්, Decorator එකක් කියන්නේ වෙන function එකක් modify කරන, එහෙමත් නැත්නම් wrap කරන function එකක්. ඒකෙන් අපිට පුළුවන් original function එකේ code එක වෙනස් නොකරම, ඒකට අලුත් functionality එකක් add කරන්න. හිතන්න, ඔයාගේ ගෙදරට party එකකට යන්න ලෑස්ති වෙනවා කියලා. ඔයා ලස්සනට ඇඳුම් ඇඳලා ඉන්නවා. දැන් ඔයාට party එකට යන්න කලින් තව විශේෂ දෙයක් කරන්න ඕනේ - ඒ තමයි perfume ටිකක් ගහගන්න එක. Perfume එක කියන්නේ ඔයාගේ original appearance එක වෙනස් නොකර, ඒකට අලුත් effect එකක් දෙන 'decorator' එකක් වගේ. ඒක ඔයාගේ ඇඳුම උඩින් යනවා මිසක් ඇඳුම වෙනස් කරන්නේ නෑනේ.
Python වල Decorators වැඩ කරන්නේ functions as first-class objects කියන concept එක මත. ඒ කියන්නේ functions වලට variables වගේම pass කරන්න, return කරන්න, සහ වෙන functions ඇතුලේ define කරන්න පුළුවන්.
මෙන්න සරල Decorator එකක උදාහරණයක්:
def my_simple_decorator(func):
def wrapper(*args, **kwargs):
print("Function එක call කරන්න කලින් මේක පින්ට් වෙනවා.")
result = func(*args, **kwargs)
print("Function එක call කරලා ඉවර වුණාට පස්සේ මේක පින්ට් වෙනවා.")
return result
return wrapper
@my_simple_decorator
def greet(name):
print(f"ආයුබෝවන්, {name}!")
# Decorator එක නැතුව පාවිච්චි කරනවා නම් මෙහෙම:
# decorated_greet = my_simple_decorator(greet)
# decorated_greet("සීතා")
greet("කමල්")
# Output:
# Function එක call කරන්න කලින් මේක පින්ට් වෙනවා.
# ආයුබෝවන්, කමල්!
# Function එක call කරලා ඉවර වුණාට පස්සේ මේක පින්ට් වෙනවා.
මේ උදාහරණයේදී, `my_simple_decorator` කියන්නේ `greet` function එක modify කරන Decorator එක. `greet` function එක execute වෙන්න කලින් සහ පස්සේ print statements add කරලා තියෙනවා. මේක `greet` function එකේ original code එකට කිසිම බලපෑමක් නොකරම කරපු වැඩක්.
2. Decorators වලට Arguments දාන්නේ කොහොමද?
හරි, දැන් ඔයාට Decorators ගැන පොඩි අවබෝධයක් ඇතිනේ. ඒත් සමහර වෙලාවට අපිට ඕනේ වෙනවා Decorator එකටම values ටිකක් pass කරන්න. උදාහරණයක් විදියට, function එකක් execute වෙන්න ගතවෙන වෙලාව log කරන Decorator එකක් හදනවා නම්, log message එක මොන වගේ එකක්ද වෙන්න ඕනේ කියලා Decorator එකටම කියන්න පුළුවන් නම් හොඳයි නේද? එහෙම නැත්නම්, function එකක් fail වුණොත්, කී පාරක් retries කරන්න ඕනේද කියලා Decorator එකටම කියන්න පුළුවන් නම්?
සාමාන්ය Decorators වලට කෙලින්ම arguments pass කරන්න බෑ. මොකද Decorator එකක් කියන්නේ function එකක් return කරන function එකක්. Argument එක්ක Decorator එකක් කියන්නේ ඊටත් එහාට ගිහින් 'Decorator Factory' එකක් වගේ. ඒ කියන්නේ, arguments ගන්නා function එකක්, ඒ arguments පාවිච්චි කරලා Decorator එකක් හදලා return කරනවා.
මේක තේරුම් ගන්න, අපිට functions ත්රිත්වයක් ගැන හිතන්න වෙනවා:
- Outer function (Decorator factory): මේක තමයි Decorator එකට අවශ්ය arguments ගන්නේ. මේ function එක Decorator එකක් return කරනවා.
- Middle function (Actual Decorator): මේක තමයි original function එක wrap කරන function එක. මේක arguments ගන්නේ නෑ, හැබැයි outer function එකෙන් ලැබුණ arguments ටික access කරන්න පුළුවන්.
- Inner function (Wrapper): මේක තමයි original function එක call කරලා, ඒ වටේට අලුත් functionality එක add කරන්නේ.
මෙන්න උදාහරණයක්. අපි `retry` කියලා Decorator එකක් හදමු, ඒකට fail වුණොත් කී පාරක් retries කරන්න ඕනේද කියන එක argument එකක් විදියට දාන්න පුළුවන් වෙන්න:
def retry(num_retries):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(num_retries + 1):
try:
print(f"Attempting to call {func.__name__} (Attempt {attempt + 1})")
return func(*args, **kwargs)
except Exception as e:
print(f"Error in {func.__name__}: {e}")
if attempt == num_retries:
raise
print("Retrying...")
return wrapper
return decorator
@retry(num_retries=3)
def unstable_function(x):
import random
if random.random() < 0.7:
raise ValueError("Something went wrong!")
return x * 2
@retry(num_retries=1)
def another_unstable_function():
import random
if random.random() < 0.9:
raise ConnectionError("Network is down!")
print("Successfully connected!")
print("--- Calling unstable_function ---")
try:
print(f"Result: {unstable_function(5)}")
except Exception as e:
print(f"Function failed after all retries: {e}")
print("\n--- Calling another_unstable_function ---")
try:
another_unstable_function()
except Exception as e:
print(f"Function failed after all retries: {e}")
මේ code එකේ `retry(num_retries=3)` කියලා ලියද්දී, `retry` කියන outer function එක run වෙනවා. ඒක `decorator` කියන middle function එක return කරනවා. ඊට පස්සේ Python ඒ `decorator` function එක `unstable_function` එකට apply කරනවා. ඒක ඇතුලේ තියෙන `wrapper` function එක තමයි ඇත්තටම `unstable_function` එක execute කරන්නේ. `num_retries` කියන argument එක closure එකක් විදියට `decorator` සහ `wrapper` functions වලට access කරන්න පුළුවන්.
3. `functools.wraps` මොකටද? Metadata ප්රශ්නය
අපි දැන් argument එක්ක Decorators හදන්න දන්නවා. ඒත් මෙතන පොඩි 'අනතුරක්' තියෙනවා. අපි කලින් හදපු `retry` Decorator එක ආයෙත් බලමු. ඒකේ `func.__name__` කියලා එකක් පාවිච්චි කළා, මතකද? ඒක හරියට වැඩ කළා. ඒත් සමහර වෙලාවට Decorator එකක් පාවිච්චි කරාම, original function එකේ metadata (නාමය, docstring එක, module එක වගේ දේවල්) නැති වෙලා, wrapper function එකේ metadata replace වෙනවා. මේක debugging වලදී, documentation tools පාවිච්චි කරද්දී, සහ introspection කරද්දී ලොකු ප්රශ්නයක් වෙන්න පුළුවන්.
උදාහරණයක් විදියට, අපි කලින් `my_simple_decorator` එක ආයෙත් බලමු, ඒකේ `__name__` සහ `__doc__` print කරලා:
def my_simple_decorator(func):
def wrapper(*args, **kwargs):
"""මම wrapper function එකේ docstring එක"""
print("Function එක call කරන්න කලින් මේක පින්ට් වෙනවා.")
result = func(*args, **kwargs)
print("Function එක call කරලා ඉවර වුණාට පස්සේ මේක පින්ට් වෙනවා.")
return result
return wrapper
@my_simple_decorator
def say_hello(name):
"""This function says hello to the given name."""
print(f"Hello, {name}!")
print(f"Decorated function name: {say_hello.__name__}")
print(f"Decorated function docstring: {say_hello.__doc__}")
# Output:
# Decorated function name: wrapper
# Decorated function docstring: මම wrapper function එකේ docstring එක
දැක්කනේ? `say_hello` function එකේ original name එක (`say_hello`) සහ docstring එක (`This function says hello...`) වෙනුවට Decorator එක ඇතුලේ තිබ්බ `wrapper` function එකේ name එකයි docstring එකයි replace වෙලා. මේක සමහර වෙලාවට ලොකු headache එකක් වෙන්න පුළුවන්, විශේෂයෙන්ම විශාල projects වලදී.
මේ ප්රශ්නය විසඳන්න තමයි Python වල `functools` module එකේ `wraps` Decorator එක තියෙන්නේ. `functools.wraps` කියන්නේ Decorator එකක් ඇතුලේ තියෙන Decorator එකක්. ඒකෙන් කරන්නේ, wrapper function එක original function එකේ metadata ටික copy කරන්න උදව් වෙන එකයි. ඒක `__name__`, `__doc__`, `__module__`, `__annotations__`, සහ `__dict__` වගේ attributes copy කරනවා.
මෙන්න `functools.wraps` පාවිච්චි කරලා ඒ ප්රශ්නය විසඳන හැටි:
import functools
def my_simple_decorator_with_wraps(func):
@functools.wraps(func) # මෙන්න Magic එක!
def wrapper(*args, **kwargs):
"""මම wrapper function එකේ docstring එක"""
print("Function එක call කරන්න කලින් මේක පින්ට් වෙනවා.")
result = func(*args, **kwargs)
print("Function එක call කරලා ඉවර වුණාට පස්සේ මේක පින්ට් වෙනවා.")
return result
return wrapper
@my_simple_decorator_with_wraps
def say_hello_again(name):
"""This function says hello to the given name, again."""
print(f"Hello again, {name}!")
print(f"Decorated function name: {say_hello_again.__name__}")
print(f"Decorated function docstring: {say_hello_again.__doc__}")
# Output:
# Decorated function name: say_hello_again
# Decorated function docstring: This function says hello to the given name, again.
දැන් හරි නේද? `say_hello_again` කියන function එකේ original name එකයි, docstring එකයි `functools.wraps` නිසා ආරක්ෂා වුණා. මේක බොහොම සරල පියවරක් වුණත්, Python වල professional Decorators ලියද්දී අනිවාර්යයෙන්ම කරන්න ඕනේ දෙයක්. මතක තියාගන්න: Decorator එකක් හදනවා නම්, අනිවාර්යයෙන්ම `functools.wraps` පාවිච්චි කරන්න!
4. ප්රායෝගික උදාහරණයක්: Time-logging Decorator (Arguments එක්ක)
දැන් අපි මේක හොඳ practical example එකකින් බලමු. අපි හදමු function එකක් execute වෙන්න ගතවෙන වෙලාව log කරන Decorator එකක්. මේ Decorator එකට අපි log message එකට අමතරව prefix එකක් දෙන්නත් පුළුවන් වෙන්න හදමු.
import time
import functools
def time_logger(prefix="Execution"): # Outer function (Decorator Factory)
def decorator(func): # Middle function (Actual Decorator)
@functools.wraps(func) # Wrapper function එක original function එකේ metadata preserve කරන්න
def wrapper(*args, **kwargs): # Inner function (Wrapper)
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
duration = end_time - start_time
print(f"[{prefix} Time Log] '{func.__name__}' ran in {duration:.4f} seconds.")
return result
return wrapper
return decorator
@time_logger(prefix="Data Processing")
def process_large_data(data_size):
"""Simulates processing a large amount of data."""
print(f"Processing {data_size} MB of data...")
time.sleep(data_size / 1000) # Simulate work
print("Data processing complete.")
return f"Processed {data_size} MB"
@time_logger()
def calculate_complex_math(iterations):
"""Performs a complex mathematical calculation."""
print(f"Starting complex calculation with {iterations} iterations...")
total = 0
for i in range(iterations):
total += (i ** 0.5) * (i ** 0.3)
print("Calculation finished.")
return total
@time_logger(prefix="API Call")
def fetch_user_data(user_id):
"""Fetches user data from a simulated API."""
print(f"Fetching data for user ID: {user_id}...")
time.sleep(0.05 + user_id / 1000) # Simulate API latency
print("User data fetched.")
return {"id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"}
print("\n--- Running Data Processing ---")
processed_result = process_large_data(250)
print(f"Result: {processed_result}\n")
print("\n--- Running Complex Math ---")
math_result = calculate_complex_math(1000000)
print(f"Result: {math_result}\n")
print("\n--- Running API Call ---")
user_data = fetch_user_data(123)
print(f"Result: {user_data}\n")
# Check metadata
print(f"Name of process_large_data: {process_large_data.__name__}")
print(f"Docstring of process_large_data: {process_large_data.__doc__}")
මේ උදාහරණයේදී, `time_logger` Decorator එක `prefix` කියන argument එක ගන්නවා. ඒක පාවිච්චි කරලා, log message එක customize කරන්න පුළුවන්. `process_large_data` function එකට `Data Processing` කියලා prefix එකක් දුන්නා. `calculate_complex_math` function එකට කිසිම argument එකක් දුන්නේ නැති නිසා, ඒක default `Execution` prefix එක පාවිච්චි කරනවා. `fetch_user_data` එකටත් `API Call` කියලා වෙන prefix එකක් දුන්නා. මේ හැම Decorator එකක් ඇතුලෙම `functools.wraps` පාවිච්චි කරපු නිසා, original function එකේ name එකයි docstring එකයි හරියටම preserve වෙනවා.
මේ වගේ time-logging Decorators, performance monitoring වලට, debugging වලට, සහ operations වලදී බොහොම ප්රයෝජනවත් වෙනවා.
5. Debugging සහ Best Practices
Decorators පාවිච්චි කරද්දී ඒවා `functools.wraps` වලින් wrap කරන්නේ නැතුව ඉන්න එකෙන් වෙන්න පුළුවන් ප්රශ්න අපි කලින් කතා කළා. Debugging වලදී මේක ලොකු ප්රශ්නයක්. Traceback එකක් ආවොත්, ඒක සමහරවිට Decorator එකේ wrapper function එකට point කරයි, original function එකට නෙවෙයි. ඒත් `functools.wraps` පාවිච්චි කළොත්, traceback එකේ original function name එක පෙන්නන නිසා, ප්රශ්නය තියෙන තැන හරියටම හොයාගන්න ලේසියි.
Best Practices:
- සැමවිටම `functools.wraps` පාවිච්චි කරන්න: මේක අතිශයින්ම වැදගත්. Decorator එකක් හදනවා නම්, wrapper function එක `functools.wraps` වලින් decorate කරන්න අමතක කරන්න එපා. මේකෙන් ඔයාගේ code එක maintain කරන්න, debug කරන්න, සහ තේරුම් ගන්න පහසු වෙනවා.
- Decorator නාමකරණය (Naming): Decorators වලට පැහැදිලි, තේරුම් ගත හැකි නම් දෙන්න. ඒකෙන් code එකේ readability එක වැඩි වෙනවා.
- Simple Decorators: පුළුවන් තරම් Decorators සරලව තියාගන්න බලන්න. එක Decorator එකක් එක task එකක් විතරක් කරන විදියට හදනවා නම් හොඳයි.
- Error Handling: Decorator එකක් ඇතුලේ error handling add කරනවා නම්, ඒක original function එකට බලපාන්නේ නැති විදියට carefully handle කරන්න.
- Documentation: ඔයාගේ Decorators වලට හොඳ docstrings ලියන්න. මොකද Decorator එකක් කරන දේ සහ ඒකේ arguments මොනවද කියන එක අනිත් programmers ලට තේරුම් ගන්න ඒක උදව් වෙනවා.
`functools` module එකේ `wraps` විතරක් නෙවෙයි, තව utility functions ගොඩක් තියෙනවා. උදාහරණයක් විදියට `functools.partial` වගේ ඒවා. ඒවා ගැනත් ඔයාලට ඉස්සරහට හොයලා බලන්න පුළුවන්.
අවසාන වශයෙන්...
Decorators, විශේෂයෙන්ම arguments එක්ක Decorators සහ `functools.wraps` පාවිච්චි කරන එක, Python වල advanced programming concept එකක්. මේවා හරියට තේරුම් ගත්තාම ඔයාලට reusable, clean, සහ maintainable code ලියන්න පුළුවන් වෙනවා. Software Engineering වලදී මේ වගේ patterns දැනගෙන ඉන්න එක බොහොම වටිනවා.
මතක තියාගන්න, practice makes perfect. අද කතා කරපු concepts ඔයාලගේම project වලට add කරලා බලන්න. පොඩි Decorators හදන්න උත්සහ කරන්න. Time-logging, Caching, Authentication, Authorization වගේ දේවල් වලට Decorators කොහොමද පාවිච්චි කරන්නේ කියලා බලන්න. එතකොට මේ concept එක තවත් හොඳින් තේරෙයි.
මේ post එක ගැන ඔයාලගේ අදහස්, ප්රශ්න පහලින් comment කරන්න. ඔයාලට තව මොනවා ගැනද දැනගන්න ඕනේ, ඒවත් කියන්න. අපි ඊළඟ post එකෙන් හමුවෙමු! Happy Coding! 👋