183 lines
3.3 KiB
Markdown
183 lines
3.3 KiB
Markdown
# 08 – Decorators in Python
|
||
|
||
This document explains **decorators**, how they work, and how they are used to extend function behavior without modifying the original function code.
|
||
|
||
---
|
||
|
||
## 1. What Is a Decorator?
|
||
|
||
A **decorator** is a function that:
|
||
|
||
* Takes another function as input
|
||
* Adds extra behavior
|
||
* Returns a new function
|
||
|
||
Decorators are commonly used for:
|
||
|
||
* Input validation
|
||
* Logging
|
||
* Authentication
|
||
* Performance measurement
|
||
* Access control
|
||
|
||
---
|
||
|
||
## 2. Basic Decorator Structure
|
||
|
||
A decorator has three layers:
|
||
|
||
1. The decorator function
|
||
2. The wrapper function
|
||
3. The original function
|
||
|
||
### General Pattern
|
||
|
||
```python
|
||
def decorator(func):
|
||
def wrapper(*args, **kwargs):
|
||
# extra behavior
|
||
return func(*args, **kwargs)
|
||
return wrapper
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Example: Input Validation Decorator
|
||
|
||
### Code
|
||
|
||
```python
|
||
def check_number(func):
|
||
def wrapper(a, b):
|
||
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
|
||
print("Input must be numbers")
|
||
return
|
||
return func(a, b)
|
||
return wrapper
|
||
```
|
||
|
||
### Explanation
|
||
|
||
* `check_number` is the decorator.
|
||
* `func` is the function being decorated.
|
||
* `wrapper` replaces the original function.
|
||
* `a` and `b` are the arguments passed to the original function.
|
||
* `isinstance(a, (int, float))` ensures inputs are numeric.
|
||
* If validation fails, execution stops.
|
||
* If validation passes, the original function is called.
|
||
|
||
---
|
||
|
||
## 4. Using the Decorator with `@` Syntax
|
||
|
||
### Code
|
||
|
||
```python
|
||
@check_number
|
||
def bemola(a, b):
|
||
try:
|
||
res = a / b
|
||
print(res)
|
||
except ZeroDivisionError:
|
||
print("Zero Number Detected")
|
||
except Exception as e:
|
||
print(f"Error Detected {e}")
|
||
```
|
||
|
||
### What Happens Internally
|
||
|
||
This line:
|
||
|
||
```python
|
||
@check_number
|
||
```
|
||
|
||
Is equivalent to:
|
||
|
||
```python
|
||
bemola = check_number(bemola)
|
||
```
|
||
|
||
The function `bemola` is replaced by `wrapper`.
|
||
|
||
---
|
||
|
||
## 5. Execution Flow
|
||
|
||
When calling:
|
||
|
||
```python
|
||
bemola(10, 2)
|
||
```
|
||
|
||
The flow is:
|
||
|
||
1. `wrapper(10, 2)` is called
|
||
2. Inputs are validated
|
||
3. `func(10, 2)` is executed
|
||
4. Result is printed
|
||
|
||
If calling:
|
||
|
||
```python
|
||
bemola(10, "a")
|
||
```
|
||
|
||
The output will be:
|
||
|
||
```text
|
||
Input must be numbers
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Why Use Decorators?
|
||
|
||
Without decorators, input validation would need to be repeated in every function.
|
||
|
||
Decorators allow:
|
||
|
||
* Reusable logic
|
||
* Cleaner code
|
||
* Separation of concerns
|
||
|
||
---
|
||
|
||
## 7. Limitations in This Example
|
||
|
||
* The decorator only works with exactly two arguments.
|
||
* It does not preserve the original function’s metadata (`__name__`, `__doc__`).
|
||
|
||
---
|
||
|
||
## 8. Improved Version (Best Practice)
|
||
|
||
```python
|
||
from functools import wraps
|
||
|
||
def check_number(func):
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
if not all(isinstance(x, (int, float)) for x in args):
|
||
print("Input must be numbers")
|
||
return
|
||
return func(*args, **kwargs)
|
||
return wrapper
|
||
```
|
||
|
||
### Improvements
|
||
|
||
* Supports any number of arguments
|
||
* Preserves function name and documentation
|
||
* More reusable and professional
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
* Decorators modify function behavior without changing its code
|
||
* They wrap functions inside another function
|
||
* `@decorator` is syntactic sugar
|
||
* Commonly used for validation, logging, and access control
|
||
* Best practice is to use `*args`, `**kwargs`, and `functools.wraps`
|