# Decorators in Python: A Complete Guide

## **Introduction to Decorators**

> Decorators in Python are a powerful tool that allows **modifying or extending the behavior of functions** without altering their actual implementation. They make code more **readable, reusable, and maintainable** by allowing modifications to functions in a clean and efficient way.

## **Why Use Decorators?**

* **Code Reusability:** Avoid repeating code for similar functionalities.
    
* **Separation of Concerns:** Keeps function logic independent of additional behavior.
    
* **Enhanced Readability:** Makes functions cleaner by abstracting modifications.
    
* **Useful for Logging, Authentication, Timing Functions, and More.**
    

## **Basic Syntax of a Decorator**

A **decorator** is a function that takes another function as input and extends its behavior.

### **Step 1: Creating a Basic Decorator**

```python
def my_decorator(func):
    def wrapper():
        print("Something before the function runs.")
        func()
        print("Something after the function runs.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello, World!")

say_hello()
```

**Output:**

```plaintext
Something before the function runs.
Hello, World!
Something after the function runs.
```

### **How It Works**

* `my_decorator(func)` receives the function `say_hello()`.
    
* `wrapper()` adds additional behavior before and after calling `func()`.
    
* `@my_decorator` applies the decorator to `say_hello()`, modifying its behavior.
    

## **Decorators with Arguments**

If the decorated function takes arguments, use `*args` and `**kwargs` inside the wrapper.

```python
def smart_decorator(func):
    def wrapper(*args, **kwargs):
        print("Executing decorated function...")
        result = func(*args, **kwargs)
        print("Finished execution.")
        return result
    return wrapper

@smart_decorator
def add(a, b):
    return a + b

print(add(5, 3))
```

**Output:**

```plaintext
Executing decorated function...
Finished execution.
8
```

## **Built-in Decorators in Python**

Python provides several built-in decorators, commonly used for modifying methods in classes.

### **1\.** `@staticmethod`

Defines a function inside a class that doesn’t require access to instance attributes.

```python
class MathOperations:
    @staticmethod
    def add(a, b):
        return a + b

print(MathOperations.add(5, 3))  # Output: 8
```

### **2\.** `@classmethod`

Allows methods to receive the class itself (`cls`) instead of instance (`self`).

```python
class MyClass:
    class_var = "Hello"

    @classmethod
    def show_class_var(cls):
        return cls.class_var

print(MyClass.show_class_var())  # Output: Hello
```

### **3\.** `@property`

Used to define **read-only properties** in a class.

```python
class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

p = Person("Mahesh")
print(p.name)  # Output: Mahesh
```

## **Using Multiple Decorators**

You can stack multiple decorators on a single function:

```python
def decorator1(func):
    def wrapper():
        print("Decorator 1 before function")
        func()
        print("Decorator 1 after function")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2 before function")
        func()
        print("Decorator 2 after function")
    return wrapper

@decorator1
@decorator2
def hello():
    print("Hello, World!")

hello()
```

**Output:**

```plaintext
Decorator 1 before function
Decorator 2 before function
Hello, World!
Decorator 2 after function
Decorator 1 after function
```

## **Real-World Applications of Decorators**

### **1\. Logging Function Execution**

```python
import time

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Executing {func.__name__} at {time.strftime('%X')}")
        return func(*args, **kwargs)
    return wrapper

@log_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Mahesh")
```

### **2\. Checking User Authentication**

```python
def requires_authentication(func):
    def wrapper(user, *args, **kwargs):
        if user == "admin":
            return func(*args, **kwargs)
        else:
            print("Access Denied")
    return wrapper

@requires_authentication
def secure_action():
    print("Performing secure action...")

secure_action("user")   # Output: Access Denied
secure_action("admin")  # Output: Performing secure action...
```

### **3\. Timing Function Execution**

```python
import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timer_decorator
def slow_function():
    time.sleep(2)
    print("Finished processing.")

slow_function()
```

**Output:**

```plaintext
Finished processing.
slow_function took 2.0001 seconds
```
