Introduction
In Python, functions are full-fledged objects -- you can assign them to variables, store them in lists, and pass them to other functions. This lesson covers first-class functions, lambda expressions, higher-order functions, and closures.
Key Concepts
- First-class functions: Functions are objects that can be assigned, passed, and returned like any other value.
- Higher-order function: A function that takes another function as an argument or returns one.
- Lambda: An anonymous, single-expression function created with the
lambdakeyword. - Closure: A function that captures and remembers variables from its enclosing scope.
Real World Context
Callbacks, event handlers, and middleware chains all depend on first-class functions. Frameworks like Flask use decorators (built on closures) to register routes, and libraries like sorted() accept key functions to customize ordering. Understanding closures is also essential for writing factories, memoizers, and configuration builders.
Deep Dive
In Python, functions are objects. They can be:
- Assigned to variables
- Passed as arguments to other functions
- Returned from functions
- Stored in data structures
pythondef greet(name): return f"Hello, {name}" # Assign to variable say_hello = greet print(say_hello("Alice")) # "Hello, Alice" # Store in list operations = [str.upper, str.lower, str.title] for op in operations: print(op("hello world"))
Higher-Order Functions
Functions that take or return functions:
pythondef apply_twice(func, value): return func(func(value)) def add_one(x): return x + 1 apply_twice(add_one, 5) # 7
Lambda Functions
Anonymous functions for simple operations:
python# Syntax: lambda arguments: expression square = lambda x: x ** 2 add = lambda x, y: x + y # Common use: sorting users = [{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}] sorted(users, key=lambda u: u["age"]) # [{"name": "Alice", ...}, {"name": "Bob", ...}]
Closures
Functions that capture variables from their enclosing scope:
pythondef make_multiplier(n): def multiplier(x): return x * n # n is captured from outer scope return multiplier double = make_multiplier(2) triple = make_multiplier(3) double(5) # 10 triple(5) # 15
Common Pitfalls
- Writing multi-line logic in a lambda -- Lambdas are limited to a single expression. If you need statements, conditions across multiple lines, or error handling, use a regular
deffunction. - Late binding in closures over loop variables -- A closure created inside a loop captures the variable itself, not its current value. All closures end up sharing the final loop value. Fix by using a default argument:
lambda x, n=n: x * n. - Assigning lambdas to names --
square = lambda x: x**2is less readable thandef square(x): return x**2and loses the function name in tracebacks. PEP 8 discourages this pattern.
Best Practices
- Use lambdas only for short, throwaway functions -- A
key=lambda x: x.namein asorted()call is ideal. Anything longer deserves a named function. - Prefer
operatormodule functions over trivial lambdas --operator.itemgetter('age')is faster and clearer thanlambda u: u['age']for simple attribute or item access.
Summary
- Functions in Python are first-class objects: assignable, passable, and returnable.
- Higher-order functions accept or return other functions, enabling powerful abstractions.
- Lambdas are single-expression anonymous functions, best used as short callbacks.
- Closures capture variables from enclosing scopes -- beware of late binding in loops.
- Prefer named functions over lambdas for anything beyond a trivial one-liner.
Code Examples
# Practical closure example: counter factory
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1()) # 1, 2, 3
print(c2()) # 1 (independent counter)