Decorators & Lambda
Decorators & Lambda
Lambda Functions
A lambda is a small, anonymous function defined with the lambda keyword. It is limited to a single expression:
square = lambda x: x ** 2
add = lambda x, y: x + y
no_args = lambda: "hello"Lambdas are most useful as short callbacks passed inline to sorted, map, filter, and similar functions. For anything more complex, a named def is clearer.
Decorators
A decorator is a higher-order function that wraps another function to extend or modify its behaviour — without permanently altering the original function's source code.
The @ Syntax
@my_decorator
def greet():
print("Hello!")This is exactly equivalent to:
def greet():
print("Hello!")
greet = my_decorator(greet)Writing a Decorator
import functools
def timer(func):
@functools.wraps(func) # preserve original metadata
def wrapper(*args, **kwargs):
import time
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
return a + bfunctools.wraps
Without @functools.wraps(func), wrapper.__name__ would be "wrapper" instead of the original function's name, which breaks debugging tools and docstring inspection. Always use it.
Stacking Decorators
Multiple decorators are applied bottom-up:
@decorator_A
@decorator_B
def my_func():
...
# Equivalent to: my_func = decorator_A(decorator_B(my_func))Decorators with Arguments
Wrap the decorator in an extra factory function:
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")*args and **kwargs Review
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. Decorator wrappers use them so the inner function can accept any signature:
def wrapper(*args, **kwargs):
return func(*args, **kwargs)Code Examples
# Lambda as a sort key
students = [
{"name": "Alice", "grade": 88},
{"name": "Bob", "grade": 95},
{"name": "Carol", "grade": 72},
]
students.sort(key=lambda s: s["grade"], reverse=True)
for s in students:
print(f"{s['name']}: {s['grade']}")
# Lambda with map and filter
numbers = [1, 2, 3, 4, 5, 6]
cubed = list(map(lambda x: x**3, numbers))
odds = list(filter(lambda x: x % 2 != 0, numbers))
print("Cubed:", cubed)
print("Odds:", odds)Lambdas shine when you need a short throwaway function. Using a full def for s['grade'] extraction would be verbose. For multi-step logic, always prefer a named function.
import functools
def log_calls(func):
"""Decorator that logs every call to the wrapped function."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
arg_str = ", ".join(str(a) for a in args)
kwarg_str = ", ".join(f"{k}={v}" for k, v in kwargs.items())
all_args = ", ".join(filter(None, [arg_str, kwarg_str]))
print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
@log_calls
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
add(3, 4)
greet("Alice", greeting="Hi")The wrapper uses *args and **kwargs to forward all arguments transparently. functools.wraps copies the original function's __name__, __doc__, and other attributes to the wrapper, which is essential for introspection and debugging.
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "**" + func(*args, **kwargs) + "**"
return wrapper
def uppercase(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@bold
@uppercase
def greet(name):
return f"hello, {name}"
print(greet("world")) # bold( uppercase(greet) )
# Parameterised decorator
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(msg):
print(msg)
say("Python!")Decorators are applied bottom-up: @uppercase wraps greet first, then @bold wraps the already-wrapped function. A parameterised decorator needs an extra outer function (the factory) that accepts the arguments and returns the actual decorator.
Quick Quiz
1. What does @functools.wraps(func) do inside a decorator?
2. Given @A and @B stacked above def f(), in what order are the decorators applied?
3. Which statement about lambda functions is TRUE?
Was this lesson helpful?