Decorators and Metaprogramming
Python decorators vs C# attributes — cross-cutting concerns and AOP
Introduction
In this lesson, you'll learn about decorators and metaprogramming in Python. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with python decorators vs c# attributes — cross-cutting concerns and aop.
Python has its own approach to python decorators vs c# attributes — cross-cutting concerns and aop, which we'll explore step by step.
The Python Way
Let's see how Python handles this concept. Here's a typical example:
import functools
import time
import logging
logger = logging.getLogger(__name__)
# Simple decorator (= C# attribute that ALSO executes code)
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.info(f"Calling {func.__name__}")
result = func(*args, **kwargs)
logger.info(f"Done {func.__name__}")
return result
return wrapper
# Decorator with arguments
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times - 1: raise
time.sleep(2 ** attempt)
return wrapper
return decorator
# Timing decorator
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
@log
@timed
@retry(times=3, exceptions=(IOError,))
def process_order(order):
# decorators applied bottom-up: retry → timed → log
fetch_inventory(order.id)Comparing to C#
Here's how you might have written similar code in C#:
// Attributes — metadata attached to types/methods
[Serializable]
[Obsolete("Use NewMethod instead")]
public class LegacyClass { }
// Custom attribute
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute
{
public string Level { get; }
public LogAttribute(string level = "Info") => Level = level;
}
// Applied to method
[Log("Debug")]
public void ProcessOrder(Order o) { }
// AOP via middleware (ASP.NET Core)
app.Use(async (context, next) => {
var sw = Stopwatch.StartNew();
await next(context);
Console.WriteLine($"Request took {sw.ElapsedMs}ms");
});
// Or via Scrutor / Castle DynamicProxy for method interception
// Most C# AOP requires external packages
[Required]
[StringLength(100, MinimumLength = 1)]
public string Name { get; set; }You may be used to different syntax or behavior.
Python decorators are functions that EXECUTE at call time; C# attributes are passive metadata
You may be used to different syntax or behavior.
C# attributes are read via reflection; Python decorators wrap the function directly
You may be used to different syntax or behavior.
Python decorators can add behavior (logging, retry, caching) without reflection
You may be used to different syntax or behavior.
C# AOP requires middleware or DI proxy libraries; Python AOP is built-in via decorators
You may be used to different syntax or behavior.
@functools.wraps preserves __name__ and __doc__ — always use it
Step-by-Step Breakdown
1. Decorator as Wrapper
A Python decorator wraps a function — it runs code before/after or instead of the original. C# attributes just tag metadata; behavior needs a framework to read them.
[Log("Debug")]
public void ProcessOrder(Order o) { }@log
def process_order(o):
...
# @log actually wraps process_order
# process_order = log(process_order)2. Decorator with Arguments
Decorators with args use three levels: factory → decorator → wrapper. The factory creates the decorator with the given config.
[Retry(3)]
public string Fetch() { }def retry(times=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
for i in range(times):
try: return func(*args, **kw)
except: pass
return wrapper
return decorator
@retry(times=3)
def fetch(): ...3. Stacking Decorators
Multiple decorators stack bottom-up. @a @b @c def f means f = a(b(c(f))). Order matters for wrappers.
[Log]
[Timed]
[Retry(3)]
public void Work() { }@log # outermost — runs first/last
@timed # middle
@retry(3) # innermost — wraps work directly
def work():
...
# equivalent to: work = log(timed(retry(3)(work)))4. Class Decorators
Decorators can also wrap classes. @dataclass, @singleton are common examples.
[Singleton]
public class Config { }def singleton(cls):
instances = {}
@functools.wraps(cls)
def wrapper(*a, **kw):
if cls not in instances:
instances[cls] = cls(*a, **kw)
return instances[cls]
return wrapper
@singleton
class Config: passCommon Mistakes
When coming from C#, developers often make these mistakes:
- Python decorators are functions that EXECUTE at call time; C# attributes are passive metadata
- C# attributes are read via reflection; Python decorators wrap the function directly
- Python decorators can add behavior (logging, retry, caching) without reflection
Key Takeaways
- Python decorators EXECUTE code — not just metadata like C# attributes
- Three levels for arg decorators: factory(args) → decorator(func) → wrapper(*args)
- Stack decorators bottom-up: @a @b def f → f = a(b(f))
- functools.wraps preserves function metadata — always use in wrappers