Decorators in Python - Interview Questions and Answers
A decorator in Python is a function that modifies the behavior of another function or class method without modifying its code. It allows adding functionality dynamically.
def my_decorator(func):
def wrapper():
print("Something before function execution")
func()
print("Something after function execution")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Something before function execution
Hello!
Something after function execution
Decorators are used for logging, enforcing access control, memoization, authentication, performance measurement, etc.
You can stack multiple decorators:
@decorator1
@decorator2
def my_function():
pass
The order of execution is decorator2
first, then decorator1
.
Yes, by wrapping it inside another function:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")
greet()
Output:
Hello!
Hello!
Hello!
It preserves the original function’s metadata when applying a decorator:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
class MyClass:
@staticmethod
@my_decorator
def my_method():
print("Inside method")
- Function decorators modify functions.
- Class decorators modify class behavior.
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Before function call")
result = self.func(*args, **kwargs)
print("After function call")
return result
@MyDecorator
def greet():
print("Hello!")
greet()
Yes, using @classmethod
, @staticmethod
, or custom decorators.
By encapsulating common functionalities like logging, caching, and validation without modifying function logic.
Yes, but typically they return a function.
By calling the original function directly (func.__wrapped__
if functools.wraps
is used).
A decorator that caches results to improve performance.
from functools import lru_cache
@lru_cache(maxsize=100)
def fib(n):
return n if n <= 1 else fib(n-1) + fib(n-2)
It will cause a TypeError
when the decorated function is called.
Yes, by catching exceptions inside the wrapper function.
def modify_args(func):
def wrapper(x):
return func(x * 2)
return wrapper
@modify_args
def print_num(n):
print(n)
print_num(5) # Outputs 10
Decorators that accept arguments to modify behavior.
A decorator that logs function calls and arguments:
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def add(a, b):
return a + b
Yes, by changing the return value in the wrapper.
@staticmethod
is a built-in decorator that defines a method inside a class that doesn’t operate on an instance or class itself. It behaves like a regular function inside the class.
class MathOperations:
@staticmethod
def add(a, b):
return a + b
print(MathOperations.add(5, 3)) # Output: 8
@classmethod
is a decorator that allows a method to operate on the class itself instead of an instance. It takes cls
as the first parameter.
class MyClass:
class_var = "Hello"
@classmethod
def print_class_var(cls):
print(cls.class_var)
MyClass.print_class_var() # Output: Hello
Yes, a decorator can modify instance attributes by accessing self
if applied to instance methods.
def modify_instance(func):
def wrapper(self, *args, **kwargs):
self.name = "Modified"
return func(self, *args, **kwargs)
return wrapper
class Example:
def __init__(self, name):
self.name = name
@modify_instance
def show(self):
print(self.name)
obj = Example("Original")
obj.show() # Output: Modified
If a decorator doesn’t return a function, it causes an error when trying to call the decorated function.
def invalid_decorator(func):
return "This is not a function"
@invalid_decorator
def test():
print("Hello")
test() # TypeError: 'str' object is not callable
Decorators can be applied dynamically by assigning them at runtime.
def decorator1(func):
def wrapper():
print("Decorator1")
func()
return wrapper
def decorator2(func):
def wrapper():
print("Decorator2")
func()
return wrapper
def my_func():
print("Original function")
my_func = decorator1(decorator2(my_func))
my_func()
Output:
Decorator1
Decorator2
Original function
A timing decorator can be used to measure the execution time of a function.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Execution time: {end - start:.5f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(2)
print("Finished")
slow_function()
Yes, but you need to declare them global
inside the decorator.
counter = 0
def increment_counter(func):
def wrapper():
global counter
counter += 1
return func()
return wrapper
@increment_counter
def greet():
print("Hello")
greet()
print(counter) # Output: 1
Using functools.wraps
preserves the original function signature.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def example(a, b):
return a + b
print(example.__name__) # Output: example
Yes, by calling the original function directly if functools.wraps
is used.
@my_decorator
def example():
print("Hello")
print(example.__wrapped__()) # Calls the undecorated function
Use an if
statement before applying decorators.
use_logging = True
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Logging {func.__name__}")
return func(*args, **kwargs)
return wrapper if use_logging else func
@log_decorator
def my_function():
print("Function executed")
my_function()
Lambdas can be decorated just like regular functions.
@log_decorator
lambda_func = lambda x: x * 2
print(lambda_func(5)) # Output: Logging <lambda> 10
Yes, a decorator can be defined inside another function.
def outer():
def decorator(func):
def wrapper():
print("Decorator inside outer")
return func()
return wrapper
return decorator
@outer()
def say_hello():
print("Hello!")
say_hello()
Wrap the decorator inside another function that accepts parameters.
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")
greet()
Yes, but it requires extra handling since built-ins lack __name__
attributes.
It can make testing harder by modifying function behavior. Using @wraps
helps preserve function metadata.
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
A Django decorator that checks user permissions before executing a view.
from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
pass
import asyncio
def async_decorator(func):
async def wrapper(*args, **kwargs):
print("Before async function")
result = await func(*args, **kwargs)
print("After async function")
return result
return wrapper
@async_decorator
async def my_async_function():
print("Inside async function")
asyncio.run(my_async_function())
import time
def retry(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception:
time.sleep(1)
return wrapper
return decorator
@retry(3)
def fetch_data():
print("Fetching data...")
raise ValueError("Network error!")
fetch_data()
Decorators can enforce authentication and validation before executing a function.
A class-based decorator defines __call__
to make the class behave like a function.
class Logger:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(f"Logging: {self.func.__name__}")
return self.func(*args, **kwargs)
@Logger
def greet():
print("Hello!")
greet()
Output:
Logging: greet
Hello!
Yes, using instance attributes.
class Counter:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call count: {self.count}")
return self.func(*args, **kwargs)
@Counter
def hello():
print("Hello!")
hello()
hello()
Output:
Call count: 1
Hello!
Call count: 2
Hello!
def logger(func):
def wrapper(*args, **kwargs):
print(f"Arguments: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"Return Value: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
add(3, 4)
def limit_calls(max_calls):
def decorator(func):
count = 0
def wrapper(*args, **kwargs):
nonlocal count
if count >= max_calls:
print("Function call limit reached!")
return
count += 1
return func(*args, **kwargs)
return wrapper
return decorator
@limit_calls(3)
def say_hello():
print("Hello!")
say_hello()
say_hello()
say_hello()
say_hello() # Will not execute
Feature | Function-based Decorator | Class-based Decorator |
---|---|---|
Simplicity | Easier to write | More complex |
State Persistence | Harder to store state | Can store state easily |
Reusability | Less flexible | More flexible |
__call__ method | Not required | Required |
Yes, but yield
should be handled properly.
def generator_decorator(func):
def wrapper(*args, **kwargs):
print("Before generator starts")
yield from func(*args, **kwargs)
print("After generator ends")
return wrapper
@generator_decorator
def count():
yield 1
yield 2
yield 3
for num in count():
print(num)
Use asyncio.iscoroutinefunction
to check for async functions.
import asyncio
import functools
def universal_decorator(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
print("Async function detected")
return await func(*args, **kwargs)
def sync_wrapper(*args, **kwargs):
print("Sync function detected")
return func(*args, **kwargs)
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
@universal_decorator
def sync_func():
print("I am sync!")
@universal_decorator
async def async_func():
print("I am async!")
sync_func()
asyncio.run(async_func())
They modify class behavior using metaclasses.
class MetaDecorator(type):
def __new__(cls, name, bases, dct):
dct['added_method'] = lambda self: "Added by metaclass"
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=MetaDecorator):
pass
obj = MyClass()
print(obj.added_method()) # Output: Added by metaclass
import time
def profile(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Execution Time: {end - start:.4f} sec")
return result
return wrapper
@profile
def slow_task():
time.sleep(2)
print("Task completed")
slow_task()
def validate_positive(func):
def wrapper(*args, **kwargs):
if any(arg < 0 for arg in args):
raise ValueError("Negative values not allowed")
return func(*args, **kwargs)
return wrapper
@validate_positive
def add(a, b):
return a + b
print(add(3, 5)) # Works
# print(add(-1, 2)) # Raises ValueError
Yes, by changing the function's output before returning.
def modify_return(func):
def wrapper(*args, **kwargs):
return f"Modified: {func(*args, **kwargs)}"
return wrapper
@modify_return
def greet():
return "Hello"
print(greet()) # Output: Modified: Hello
@app.route("/")
→ Define routes@login_required
→ Enforce authentication@cache
→ Cache responses
from functools import lru_cache
@lru_cache(maxsize=10)
def expensive_calculation(n):
print("Computing...")
return n * n
print(expensive_calculation(5))
print(expensive_calculation(5)) # Cached result
def exception_handler(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error: {e}")
return wrapper
@exception_handler
def divide(a, b):
return a / b
print(divide(10, 0)) # Output: Error: division by zero
A function that returns a decorator.
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
say_hi()
Store execution details in a log file or global list.
Yes, to modify operator behavior in classes.
def doc_modifier(new_doc):
def decorator(func):
func.__doc__ = new_doc
return func
return decorator
@doc_modifier("New documentation")
def example():
"""Old doc"""
pass
print(example.__doc__) # Output: New documentation
They can ensure thread safety by synchronizing access.
Yes, by restoring the original function reference.
A dynamic decorator factory is a function that generates decorators dynamically based on input arguments.
def dynamic_decorator(prefix):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"{prefix}: Executing {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@dynamic_decorator("INFO")
def greet():
print("Hello!")
greet()
Using a time-based check to limit function calls.
import time
def rate_limiter(max_calls, period):
calls = []
def decorator(func):
def wrapper(*args, **kwargs):
now = time.time()
calls[:] = [t for t in calls if now - t < period]
if len(calls) >= max_calls:
raise Exception("Rate limit exceeded!")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limiter(3, 5) # Max 3 calls per 5 seconds
def api_request():
print("API request sent")
api_request()
Yes, but yield
should be handled properly.
def generator_decorator(func):
def wrapper(*args, **kwargs):
print("Before generator starts")
yield from func(*args, **kwargs)
print("After generator ends")
return wrapper
@generator_decorator
def count():
yield 1
yield 2
yield 3
for num in count():
print(num)
def type_enforced(func):
def wrapper(*args):
if not all(isinstance(arg, int) for arg in args):
raise TypeError("Only integers allowed!")
return func(*args)
return wrapper
@type_enforced
def add(a, b):
return a + b
print(add(3, 5)) # Works
# print(add(3, "5")) # Raises TypeError
Decorators can be used to intercept function calls, similar to middleware in web frameworks.
def middleware(func):
def wrapper(*args, **kwargs):
print("Middleware: Pre-processing request")
response = func(*args, **kwargs)
print("Middleware: Post-processing response")
return response
return wrapper
@middleware
def handler():
print("Handling request...")
handler()
Yes, using functools.wraps
to preserve the original name.
import functools
def change_name(new_name):
def decorator(func):
func.__name__ = new_name
return func
return decorator
@change_name("custom_function")
def test():
pass
print(test.__name__) # Output: custom_function
Use unittest
and mock decorators if needed.
import unittest
def debug(func):
def wrapper(*args, **kwargs):
print("Debugging...")
return func(*args, **kwargs)
return wrapper
@debug
def add(a, b):
return a + b
class TestDecorator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
unittest.main()
Django provides @transaction.atomic
to ensure database consistency.
from django.db import transaction
@transaction.atomic
def update_user():
# All operations inside this function are either committed or rolled back
pass
Flask uses decorators to define routes.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Hello, Flask!"
FastAPI uses decorators for API routing.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello FastAPI"}
Yes, decorators work normally in Jupyter notebooks.
wrapt
ensures correctness in complex decorators by preserving function metadata.
They modify class behavior using metaclasses.
class MetaDecorator(type):
def __new__(cls, name, bases, dct):
dct['extra_method'] = lambda self: "Added by metaclass"
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=MetaDecorator):
pass
obj = MyClass()
print(obj.extra_method()) # Output: Added by metaclass
@dataclass
is implemented using the dataclasses
module.
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
Function attributes store metadata inside decorators.
def add_metadata(func):
func.version = "1.0"
return func
@add_metadata
def test():
pass
print(test.version) # Output: 1.0
Yes, by injecting dependencies dynamically.
Decorators can alter MRO by modifying __mro__
.
Yes, by dynamically changing the class’s base classes.
def modify_base(cls):
cls.__bases__ = (NewBaseClass,)
return cls
Use function attributes or functools.wraps
.
They simplify resource management.
from contextlib import contextmanager
@contextmanager
def open_file(name):
f = open(name, 'w')
try:
yield f
finally:
f.close()
Tutorials
Random Blogs
- Avoiding the Beginner’s Trap: Key Python Fundamentals You Shouldn't Skip
- AI in Cybersecurity: The Future of Digital Protection
- Datasets for Speech Recognition Analysis
- Deep Learning (DL): The Core of Modern AI
- Datasets for Natural Language Processing
- Types of Numbers in Python
- Role of Digital Marketing Services to Uplift Online business of Company and Beat Its Competitors
- Transforming Logistics: The Power of AI in Supply Chain Management
- Best Platform to Learn Digital Marketing in Free
- Top 10 Blogs of Digital Marketing you Must Follow