10 Python @decorator vs C# Decorator Pattern
You’re right to notice the similarity! They share the same core concept (wrapping to add behavior), but they work at different levels. Let me break it down:
10.1 The Key Similarity
BOTH wrap something to add behavior without modifying it
Python @decorator C# Decorator Pattern
───────────────── ────────────────────
Wraps functions/methods Wraps objects/classes
At definition time At runtime
Function-level Object-level
10.2 Python @decorator - Function Wrapping
# Python decorator (function-level)
def logging_decorator(func):
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@logging_decorator # Syntactic sugar for: save_patient = logging_decorator(save_patient)
def save_patient(patient_name):
print(f"Saving {patient_name} to database")
return "Success"
# Usage
save_patient("John Doe")
# Output:
# [LOG] Calling save_patient
# Saving John Doe to database
# [LOG] save_patient returned Success10.3 C# Decorator Pattern - Object Wrapping
// C# Decorator pattern (object-level)
public interface IRepository
{
string SavePatient(string patientName);
}
public class SqlRepository : IRepository
{
public string SavePatient(string patientName)
{
Console.WriteLine($"Saving {patientName} to database");
return "Success";
}
}
public class LoggingRepositoryDecorator : IRepository
{
private readonly IRepository _inner;
public LoggingRepositoryDecorator(IRepository inner)
{
_inner = inner;
}
public string SavePatient(string patientName)
{
Console.WriteLine($"[LOG] Calling SavePatient");
var result = _inner.SavePatient(patientName);
Console.WriteLine($"[LOG] SavePatient returned {result}");
return result;
}
}
// Usage - manual wrapping at runtime
IRepository repo = new SqlRepository();
repo = new LoggingRepositoryDecorator(repo);
repo.SavePatient("John Doe");10.4 Side-by-Side Comparison
PYTHON @DECORATOR C# DECORATOR PATTERN
═════════════════════════════════════════════════════════════
Definition Time Runtime
─────────────────────────────────────────────────────────────
@logging IRepository repo =
@caching new SqlRepository();
@retry repo = new CachingDecorator(repo);
def save_patient(): repo = new LoggingDecorator(repo);
... repo = new RetryDecorator(repo);
Function-based Object-based
─────────────────────────────────────────────────────────────
Wraps individual functions Wraps entire objects with
all their methods
Syntax Composition
─────────────────────────────────────────────────────────────
Built into language (@) Manual object composition
Declarative Explicit
Scope Interface
─────────────────────────────────────────────────────────────
Works on functions/methods Requires interface/base class
Less formal structure More formal OOP structure
10.5 Python CAN Do Object-Level Decoration!
Python’s @decorator syntax can actually implement the GoF Decorator pattern:
# Python implementing GoF Decorator pattern (object-level)
from abc import ABC, abstractmethod
# Interface
class IRepository(ABC):
@abstractmethod
def save_patient(self, patient_name):
pass
# Core implementation
class SqlRepository(IRepository):
def save_patient(self, patient_name):
print(f"Saving {patient_name} to database")
return "Success"
# Decorator class (like C#)
class LoggingRepositoryDecorator(IRepository):
def __init__(self, inner_repository):
self._inner = inner_repository
def save_patient(self, patient_name):
print(f"[LOG] Calling save_patient")
result = self._inner.save_patient(patient_name)
print(f"[LOG] Returned {result}")
return result
class CachingRepositoryDecorator(IRepository):
def __init__(self, inner_repository):
self._inner = inner_repository
self._cache = {}
def save_patient(self, patient_name):
if patient_name in self._cache:
print(f"[CACHE HIT] {patient_name}")
return self._cache[patient_name]
result = self._inner.save_patient(patient_name)
self._cache[patient_name] = result
return result
# Usage - exactly like C#!
repo = SqlRepository()
repo = CachingRepositoryDecorator(repo)
repo = LoggingRepositoryDecorator(repo)
repo.save_patient("John Doe")10.6 Visual Comparison
PYTHON FUNCTION DECORATOR
═══════════════════════════════════════════════════
@decorator3 ┌─────────────────────┐
@decorator2 │ decorator3( │
@decorator1 ───→ │ decorator2( │ ─→ Wraps at
def func(): │ decorator1( │ definition time
... │ func │
│ ) │
│ ) │
│ ) │
└─────────────────────┘
C# OBJECT DECORATOR
═══════════════════════════════════════════════════
obj = Core() ┌─────────────────────┐
obj = Dec1(obj) │ Decorator3( │
obj = Dec2(obj) │ Decorator2( │ ─→ Wraps at
obj = Dec3(obj) │ Decorator1( │ runtime
│ Core │
│ ) │
│ ) │
│ ) │
└─────────────────────┘
10.7 Python’s Function Decorators for Cross-Cutting Concerns
Python’s @decorator is actually MORE convenient for function-level cross-cutting concerns:
# Python makes this super easy!
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.2f}s")
return result
return wrapper
def retry_decorator(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Retry {attempt + 1}/{max_attempts}")
return wrapper
return decorator
# Stack them easily!
@timing_decorator
@retry_decorator(max_attempts=3)
def process_dicom_study(study_id):
print(f"Processing {study_id}")
return "Done"
# Clean and declarative!In C#, you’d need aspect-oriented programming libraries or manual decorator classes for this.
10.8 When to Use Each
┌──────────────────────────────────────────────────────────┐
│ USE PYTHON @DECORATOR WHEN: │
│ ═══════════════════════════════════════════ │
│ │
│ ✓ Wrapping individual functions/methods │
│ ✓ Cross-cutting concerns at function level │
│ ✓ Simple, declarative syntax preferred │
│ ✓ Same decorator applied to many functions │
│ │
│ Examples: │
│ @login_required │
│ @cache_result │
│ @measure_time │
│ @validate_input │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ USE C# DECORATOR PATTERN WHEN: │
│ ═══════════════════════════════════════════ │
│ │
│ ✓ Wrapping entire objects/services │
│ ✓ Runtime composition needed │
│ ✓ Dependency Injection integration │
│ ✓ Complex object behavior modification │
│ │
│ Examples: │
│ IRepository → LoggingDecorator → CachingDecorator │
│ IDicomProcessor → AnonymizingDecorator │
│ IEmailService → RetryDecorator │
└──────────────────────────────────────────────────────────┘
10.9 Real-World Python + C# Example (Your Context!)
10.9.1 Python (FastAPI for AI model serving)
from functools import wraps
import time
# Function decorators for cross-cutting concerns
def log_inference(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[INFERENCE] Starting {func.__name__}")
result = func(*args, **kwargs)
print(f"[INFERENCE] Completed {func.__name__}")
return result
return wrapper
def measure_performance(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
print(f"[PERF] {func.__name__}: {duration:.3f}s")
return result
return wrapper
@log_inference
@measure_performance
def predict_lung_nodule(dicom_image):
# AI inference logic
return {"nodule_detected": True, "confidence": 0.95}
# Usage
predict_lung_nodule(image_data)10.9.2 C# (.NET API for PACS integration)
// Object decorators for service composition
public interface IPacsClient
{
DicomStudy FetchStudy(string studyId);
}
public class VnaPacsClient : IPacsClient
{
public DicomStudy FetchStudy(string studyId) { ... }
}
public class LoggingPacsDecorator : IPacsClient
{
private readonly IPacsClient _inner;
public LoggingPacsDecorator(IPacsClient inner) => _inner = inner;
public DicomStudy FetchStudy(string studyId)
{
Console.WriteLine($"[LOG] Fetching {studyId}");
var result = _inner.FetchStudy(studyId);
Console.WriteLine($"[LOG] Fetched {studyId}");
return result;
}
}
public class RetryPacsDecorator : IPacsClient
{
private readonly IPacsClient _inner;
public RetryPacsDecorator(IPacsClient inner) => _inner = inner;
public DicomStudy FetchStudy(string studyId)
{
// Retry logic
for (int i = 0; i < 3; i++)
{
try { return _inner.FetchStudy(studyId); }
catch { if (i == 2) throw; }
}
throw new Exception("Max retries");
}
}
// Compose at runtime
IPacsClient client = new VnaPacsClient();
client = new RetryPacsDecorator(client);
client = new LoggingPacsDecorator(client);10.10 The Conceptual Link
┌─────────────────────────────────────────────────────────┐
│ SAME CORE IDEA: Wrapping to Add Behavior │
│ ═══════════════════════════════════════════ │
│ │
│ Python @decorator: │
│ function → wrapped_function │
│ Simple, declarative, function-level │
│ │
│ C# Decorator Pattern: │
│ object → wrapped_object │
│ Flexible, compositional, object-level │
│ │
│ Both allow: │
│ ✓ Stacking/chaining │
│ ✓ Adding behavior without modification │
│ ✓ Separation of concerns │
│ ✓ Open/Closed Principle │
└─────────────────────────────────────────────────────────┘
10.11 Summary
Yes, you’re absolutely right that they’re conceptually similar!
- Python’s
@decoratoris syntactic sugar for function wrapping, making it super convenient for function-level cross-cutting concerns - C# Decorator Pattern is the formal GoF pattern for object wrapping, requiring more boilerplate but offering more structure for complex object composition
Think of Python’s @decorator as a language feature that makes a specific use case (function decoration) really easy, while C#’s Decorator Pattern is a design pattern that you implement manually for object composition.
You can actually use both in the same project: Python @decorators for your AI inference functions, and C# Decorator Pattern for your PACS/repository services!