Functions to Classes¶
Understanding the evolution from functions → closures → classes reveals Python's design philosophy.
The Evolution¶
\[\begin{array}{ccccccccccccccc}
\text{Function}&\Rightarrow&\text{Closure}&\Rightarrow&\text{Class}\\
&&\uparrow&&\uparrow&\\
&&\text{Free Variables}&&\text{Attributes}\\
&&&&\text{Methods}\\
\end{array}\]
Functions¶
1. First-Class Objects¶
Functions can be assigned, passed, and returned.
def square(x):
return x * x
f = square
print(f(4)) # 16
2. Stateless¶
Functions don't retain state between calls.
def add_ten(x):
return x + 10
result = add_ten(5) # 15
# No memory of previous calls
3. Limitations¶
Cannot encapsulate data with behavior.
Closures¶
1. Capturing Variables¶
def make_multiplier(factor):
def multiply(x):
return x * factor # factor is captured
return multiply
times3 = make_multiplier(3)
print(times3(10)) # 30
2. Free Variables¶
factor is a free variable in multiply:
- Used inside the function
- Not defined locally
- Captured from enclosing scope
3. State Retention¶
times3 = make_multiplier(3)
del make_multiplier # Can delete outer function
print(times3(10)) # 30 - still works!
Free Variables¶
1. Definition¶
A variable that is: - Referenced in a function - Not bound (defined) in that function - Comes from an enclosing scope
2. Example¶
x = 10 # global
def my_function(y):
return x + y # x is FREE, y is BOUND
3. In Closures¶
def outer(x):
def inner(y):
return x + y # x is FREE in inner
return inner
From Closure to Class¶
1. Closure Version¶
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
times3 = make_multiplier(3)
print(times3(10)) # 30
2. Class Version¶
class Multiplier:
def __init__(self, factor):
self.factor = factor # attribute instead of free variable
def multiply(self, x):
return x * self.factor
def __call__(self, x):
return self.multiply(x)
times3 = Multiplier(3)
print(times3(10)) # 30
3. Key Differences¶
- Closure: captures free variables
- Class: stores attributes explicitly
Class Advantages¶
1. Multiple Methods¶
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
def reset(self):
self.count = 0
2. Named State¶
class Rectangle:
def __init__(self, width, height):
self.width = width # explicit names
self.height = height
3. Special Methods¶
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
Closure Limitations¶
1. Single Function¶
Closures typically return one function.
2. Implicit State¶
State is captured implicitly through free variables.
3. No Introspection¶
Harder to inspect what's captured.
# What's in times3?
times3 = make_multiplier(3)
# Not obvious from outside
Class After Deletion¶
1. Instance Survives¶
class Multiplier:
class_var = "I exist"
def __init__(self, factor):
self.factor = factor
times3 = Multiplier(3)
del Multiplier # Delete class
2. Methods Still Work¶
print(times3.factor) # ✅ Works: 3
3. Class Attributes Lost¶
print(times3.class_var) # ❌ AttributeError
# new_obj = Multiplier(5) # ❌ NameError
When to Use Each¶
1. Use Functions¶
Simple, stateless operations.
2. Use Closures¶
Encapsulate simple state with single behavior.
3. Use Classes¶
- Multiple methods needed
- Complex state
- Need inheritance
- Need special methods
Key Takeaways¶
- Functions → Closures → Classes progression.
- Closures capture free variables.
- Classes provide explicit attributes.
- Classes offer more features (methods, inheritance).
- Choose based on complexity needs.