Recursion & Stack¶
Stack Depth¶
1. Recursion Limit¶
import sys
# Default limit
print(sys.getrecursionlimit()) # Usually 1000
2. Stack Overflow¶
def infinite():
return infinite()
try:
infinite()
except RecursionError as e:
print("Stack overflow!")
Recursive Frames¶
1. Frame Per Call¶
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
# factorial(5) creates 5 frames
Stack:
[factorial(5)]
[factorial(4)]
[factorial(3)]
[factorial(2)]
[factorial(1)] # Base case
2. Memory Growth¶
def deep_recursion(n):
if n == 0:
return
return deep_recursion(n - 1)
# Each call adds frame to stack
deep_recursion(100) # 100 frames
Tail Recursion¶
1. Not Optimized¶
Python doesn't optimize tail calls:
def factorial(n, acc=1):
if n <= 1:
return acc
return factorial(n - 1, n * acc)
# Still creates frames
2. Use Iteration¶
# Better: iterative
def factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
# No stack growth
Adjust Limit¶
1. Increase Limit¶
import sys
# Increase (carefully!)
sys.setrecursionlimit(2000)
# Now can recurse deeper
2. Dangers¶
# Too high risks:
# - Stack overflow crash
# - Memory exhaustion
# - System instability
# Use with caution!
Deep Recursion¶
1. Problem¶
def tree_depth(node):
if not node:
return 0
left = tree_depth(node.left)
right = tree_depth(node.right)
return 1 + max(left, right)
# Deep tree → stack overflow
2. Solution¶
# Use iteration + explicit stack
def tree_depth(root):
if not root:
return 0
stack = [(root, 1)]
max_depth = 0
while stack:
node, depth = stack.pop()
max_depth = max(max_depth, depth)
if node.left:
stack.append((node.left, depth + 1))
if node.right:
stack.append((node.right, depth + 1))
return max_depth
Stack Trace¶
1. View Stack¶
import traceback
def a():
b()
def b():
c()
def c():
traceback.print_stack()
a()
# Shows full call chain
2. Exception Stack¶
def outer():
middle()
def middle():
inner()
def inner():
raise ValueError("Error!")
try:
outer()
except ValueError:
traceback.print_exc()
# Shows: inner -> middle -> outer
Summary¶
1. Recursion¶
- Each call adds frame
- Limited by stack size
- Not tail-optimized
- Use iteration when deep
2. Stack Management¶
- Check recursion limit
- Increase carefully
- Prefer iteration
- Use explicit stack
Runnable Example: aliasing_and_copying.py¶
"""
04_intermediate_aliasing_copying.py
TOPIC: Aliasing, Shallow Copy, and Deep Copy
LEVEL: Intermediate
DURATION: 60-75 minutes
LEARNING OBJECTIVES:
1. Master the concept of aliasing and its implications
2. Understand shallow copy vs deep copy
3. Learn different ways to copy objects in Python
4. Recognize when to use each copying strategy
5. Debug common issues related to object sharing
KEY CONCEPTS:
- Aliasing: multiple names for the same object
- Shallow copy: copies the structure but not nested objects
- Deep copy: recursively copies all nested objects
- copy module: copy.copy() and copy.deepcopy()
- List/dict comprehensions and slicing for copying
"""
import copy
import sys
# ============================================================================
# SECTION 1: Aliasing Recap
# ============================================================================
if __name__ == "__main__":
print("=" * 70)
print("SECTION 1: Understanding Aliasing")
print("=" * 70)
# Aliasing: When two or more variables reference the same object
original = [1, 2, 3, 4, 5]
alias = original # alias points to the SAME object
print(f"original = {original}, id = {id(original)}")
print(f"alias = {alias}, id = {id(alias)}")
print(f"Are they the same object? {original is alias}")
# Modifying through alias affects original:
alias.append(6)
print(f"\nAfter alias.append(6):")
print(f"original = {original}")
print(f"alias = {alias}")
# MEMORY MODEL:
# ┌──────────┐ ┌────────────────────┐
# │ original ├────>│ [1, 2, 3, 4, 5, 6]│
# │ alias ├────>│ │
# └──────────┘ └────────────────────┘
# ============================================================================
# SECTION 2: Creating Independent Copies
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 2: Why We Need Copies")
print("=" * 70)
# Sometimes we want an INDEPENDENT copy, not an alias
# Question: How do we create a true copy?
# Let's try the wrong way first (aliasing):
list1 = [1, 2, 3]
list2 = list1 # This is aliasing, not copying!
list1.append(4)
print(f"list1 = {list1}")
print(f"list2 = {list2} # Oops! Changed too")
# ============================================================================
# SECTION 3: Shallow Copy - Method 1: Slicing
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 3: Shallow Copy Using Slicing")
print("=" * 70)
# For lists, slicing creates a new list
original = [1, 2, 3, 4, 5]
copy_by_slice = original[:] # [:] creates a shallow copy
print(f"original = {original}, id = {id(original)}")
print(f"copy_by_slice = {copy_by_slice}, id = {id(copy_by_slice)}")
print(f"Are they the same object? {original is copy_by_slice}")
# Now modifications are independent:
copy_by_slice.append(6)
print(f"\nAfter copy_by_slice.append(6):")
print(f"original = {original} (unchanged)")
print(f"copy_by_slice = {copy_by_slice} (changed)")
# MEMORY MODEL:
# ┌──────────┐ ┌───────────────┐
# │ original ├────>│ [1, 2, 3, 4, 5]│
# └──────────┘ └───────────────┘
# ┌──────────────┐ ┌─────────────────┐
# │copy_by_slice ├>│ [1, 2, 3, 4, 5, 6]│
# └──────────────┘ └─────────────────┘
# ============================================================================
# SECTION 4: Shallow Copy - Method 2: list() Constructor
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 4: Shallow Copy Using Constructor")
print("=" * 70)
# Using type constructors also creates shallow copies
original = [1, 2, 3]
copy_by_constructor = list(original)
print(f"original = {original}, id = {id(original)}")
print(f"copy_by_constructor = {copy_by_constructor}, id = {id(copy_by_constructor)}")
print(f"Same object? {original is copy_by_constructor}")
# Works for other types too:
original_dict = {"a": 1, "b": 2}
copy_dict = dict(original_dict)
print(f"\nDict copy: {original_dict is copy_dict} (False - different objects)")
original_set = {1, 2, 3}
copy_set = set(original_set)
print(f"Set copy: {original_set is copy_set} (False - different objects)")
# ============================================================================
# SECTION 5: Shallow Copy - Method 3: copy.copy()
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 5: Shallow Copy Using copy.copy()")
print("=" * 70)
# The copy module provides a general-purpose shallow copy function
original = [1, 2, 3, 4]
shallow_copy = copy.copy(original)
print(f"original = {original}, id = {id(original)}")
print(f"shallow_copy = {shallow_copy}, id = {id(shallow_copy)}")
print(f"Same object? {original is shallow_copy}")
# copy.copy() works with any object:
original_dict = {"x": 10, "y": 20}
shallow_copy_dict = copy.copy(original_dict)
print(f"\nDict: original is copy? {original_dict is shallow_copy_dict}")
# ============================================================================
# SECTION 6: The Problem with Shallow Copies
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 6: Shallow Copy Limitation - Nested Objects")
print("=" * 70)
# SHALLOW COPY creates a new outer container
# BUT nested objects are still SHARED (aliased)!
original = [[1, 2], [3, 4], [5, 6]]
shallow = original[:] # Shallow copy
print("Original and shallow copy:")
print(f"original = {original}, id = {id(original)}")
print(f"shallow = {shallow}, id = {id(shallow)}")
print(f"Different objects? {original is not shallow}")
# Check nested lists:
print(f"\nAre nested lists the same?")
print(f"original[0] is shallow[0]? {original[0] is shallow[0]}") # True!
print(f"original[1] is shallow[1]? {original[1] is shallow[1]}") # True!
# Modify a nested list:
shallow[0].append(99)
print(f"\nAfter shallow[0].append(99):")
print(f"original = {original} # Changed!")
print(f"shallow = {shallow} # Changed!")
print("\nWHY? The outer lists are different, but they share nested lists!")
# MEMORY MODEL:
# ┌──────────┐ ┌─────────┐ ┌──────────┐
# │ original ├────>│ List A ├────>│ [1, 2, 99]│<─┐
# └──────────┘ │ (outer) │ ┌─>│ [3, 4] │<─┼─┐
# └─────────┘ │ │ [5, 6] │<─┼─┼─┐
# │ └──────────┘ │ │ │
# ┌──────────┐ ┌─────────┐ │ │ │ │
# │ shallow ├────>│ List B ├─┘ │ │ │
# └──────────┘ │ (outer) │──────────────────┘ │ │
# │ │────────────────────┘ │
# └─────────┘──────────────────────┘
#
# Lists A and B are different (shallow copy worked for outer list)
# BUT they share the same nested lists (shallow copy limitation)
# ============================================================================
# SECTION 7: Deep Copy - The Solution
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 7: Deep Copy Using copy.deepcopy()")
print("=" * 70)
# DEEP COPY recursively copies ALL objects, including nested ones
original = [[1, 2], [3, 4], [5, 6]]
deep = copy.deepcopy(original)
print("Original and deep copy:")
print(f"original = {original}, id = {id(original)}")
print(f"deep = {deep}, id = {id(deep)}")
print(f"Different outer objects? {original is not deep}")
# Check nested lists:
print(f"\nAre nested lists the same?")
print(f"original[0] is deep[0]? {original[0] is deep[0]}") # False!
print(f"original[1] is deep[1]? {original[1] is deep[1]}") # False!
# Now modifications are truly independent:
deep[0].append(99)
print(f"\nAfter deep[0].append(99):")
print(f"original = {original} # Unchanged!")
print(f"deep = {deep} # Changed!")
# MEMORY MODEL:
# ┌──────────┐ ┌─────────┐ ┌──────────┐
# │ original ├────>│ List A ├────>│ [1, 2] │
# └──────────┘ │ │ │ [3, 4] │
# └─────────┘ │ [5, 6] │
# └──────────┘
# ┌──────────┐ ┌─────────┐ ┌──────────┐
# │ deep ├────>│ List B ├────>│ [1, 2, 99]│
# └──────────┘ │ │ │ [3, 4] │
# └─────────┘ │ [5, 6] │
# └──────────┘
# Everything is copied - complete independence!
# ============================================================================
# SECTION 8: Shallow vs Deep Copy - Side by Side Comparison
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 8: Shallow vs Deep Copy Comparison")
print("=" * 70)
# Create a nested structure
original = {
"name": "Alice",
"scores": [95, 87, 92],
"metadata": {"grade": "A", "year": 2024}
}
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print("Testing modifications:\n")
# 1. Modify top-level value (immutable string):
shallow["name"] = "Bob"
print("After shallow['name'] = 'Bob':")
print(f" original['name'] = {original['name']} (unchanged)")
print(f" shallow['name'] = {shallow['name']} (changed)")
print(" Reason: Reassignment creates new reference\n")
# 2. Modify nested list through shallow copy:
shallow["scores"].append(100)
print("After shallow['scores'].append(100):")
print(f" original['scores'] = {original['scores']} (CHANGED!)")
print(f" shallow['scores'] = {shallow['scores']} (changed)")
print(" Reason: Nested list is shared (shallow copy)\n")
# 3. Modify nested list through deep copy:
deep["scores"].append(200)
print("After deep['scores'].append(200):")
print(f" original['scores'] = {original['scores']} (unchanged)")
print(f" deep['scores'] = {deep['scores']} (changed)")
print(" Reason: Nested list is independent (deep copy)\n")
# ============================================================================
# SECTION 9: When to Use Each Copying Method
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 9: Choosing the Right Copy Method")
print("=" * 70)
print("""
ALIASING (no copy):
original = my_list
When to use:
- You WANT shared state
- Passing to functions that should modify original
- Memory efficiency is critical
- You want changes to propagate
SHALLOW COPY:
copy = my_list[:]
copy = list(my_list)
copy = copy.copy(my_list)
When to use:
- Simple structures (no nested mutable objects)
- You want to modify top-level independently
- Nested objects can be shared
- Performance matters (faster than deep copy)
DEEP COPY:
copy = copy.deepcopy(my_list)
When to use:
- Complex nested structures
- Complete independence required
- Nested mutable objects (lists, dicts)
- Safety over performance
""")
# ============================================================================
# SECTION 10: Performance Comparison
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 10: Performance Comparison")
print("=" * 70)
import time
# Create a nested structure
nested_list = [[i for i in range(100)] for j in range(100)]
# Measure aliasing (no copy):
start = time.time()
for _ in range(1000):
alias = nested_list
alias_time = time.time() - start
# Measure shallow copy:
start = time.time()
for _ in range(1000):
shallow = nested_list[:]
shallow_time = time.time() - start
# Measure deep copy:
start = time.time()
for _ in range(1000):
deep = copy.deepcopy(nested_list)
deep_time = time.time() - start
print(f"Aliasing: {alias_time:.6f} seconds (fastest)")
print(f"Shallow copy: {shallow_time:.6f} seconds (medium)")
print(f"Deep copy: {deep_time:.6f} seconds (slowest)")
print(f"\nDeep copy is {deep_time/shallow_time:.1f}x slower than shallow copy")
# ============================================================================
# SECTION 11: Copying Different Data Structures
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 11: Copying Various Data Structures")
print("=" * 70)
# LISTS:
print("Lists:")
orig_list = [1, 2, [3, 4]]
print(f" Shallow: {orig_list[:] is orig_list} (different)")
print(f" Deep: {copy.deepcopy(orig_list) is orig_list} (different)")
# DICTIONARIES:
print("\nDictionaries:")
orig_dict = {"a": 1, "b": [2, 3]}
print(f" Shallow: {orig_dict.copy() is orig_dict} (different)")
print(f" Deep: {copy.deepcopy(orig_dict) is orig_dict} (different)")
# SETS:
print("\nSets:")
orig_set = {1, 2, 3}
print(f" Shallow: {orig_set.copy() is orig_set} (different)")
print(f" Deep: {copy.deepcopy(orig_set) is orig_set} (different)")
# TUPLES (interesting case!):
print("\nTuples:")
orig_tuple = (1, 2, [3, 4])
shallow_tuple = copy.copy(orig_tuple)
deep_tuple = copy.deepcopy(orig_tuple)
print(f" Shallow: {shallow_tuple is orig_tuple} (SAME!)")
print(f" Deep: {deep_tuple is orig_tuple} (different)")
print(" Note: Shallow copy of tuple returns the same object (immutable)")
print(" But nested list is still shared!")
# ============================================================================
# SECTION 12: Custom Objects and Copying
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 12: Copying Custom Objects")
print("=" * 70)
class Person:
def __init__(self, name, friends):
self.name = name
self.friends = friends # list of friend names
def __repr__(self):
return f"Person('{self.name}', {self.friends})"
# Create original
alice = Person("Alice", ["Bob", "Charlie"])
print(f"Original: {alice}, id = {id(alice)}")
# Shallow copy
alice_shallow = copy.copy(alice)
print(f"Shallow: {alice_shallow}, id = {id(alice_shallow)}")
print(f"Different objects? {alice is not alice_shallow}")
print(f"Share friends list? {alice.friends is alice_shallow.friends}")
# Modify friends through shallow copy:
alice_shallow.friends.append("Dave")
print(f"\nAfter alice_shallow.friends.append('Dave'):")
print(f" alice.friends: {alice.friends} # Changed!")
print(f" alice_shallow.friends: {alice_shallow.friends}")
# Deep copy
alice_deep = copy.deepcopy(alice)
print(f"\nDeep: {alice_deep}, id = {id(alice_deep)}")
print(f"Different objects? {alice is not alice_deep}")
print(f"Share friends list? {alice.friends is alice_deep.friends}")
alice_deep.friends.append("Eve")
print(f"\nAfter alice_deep.friends.append('Eve'):")
print(f" alice.friends: {alice.friends} # Unchanged")
print(f" alice_deep.friends: {alice_deep.friends}")
# ============================================================================
# SECTION 13: Common Pitfalls and Debugging
# ============================================================================
print("\n" + "=" * 70)
print("SECTION 13: Common Pitfalls")
print("=" * 70)
print("""
PITFALL 1: Thinking = creates a copy
wrong = original # This is aliasing!
correct = copy.copy(original)
PITFALL 2: Using shallow copy for nested structures
nested = [[1, 2], [3, 4]]
copy = nested[:] # Nested lists still shared!
Use: copy = copy.deepcopy(nested)
PITFALL 3: Forgetting that tuple copying returns same object
t = (1, 2, 3)
t_copy = copy.copy(t) # t_copy is t!
PITFALL 4: Deep copying when not needed (performance cost)
simple = [1, 2, 3, 4] # No nested structures
copy = copy.deepcopy(simple) # Overkill! Use simple[:]
DEBUGGING TIP:
Use 'is' to check if objects are shared:
print(f"Shared? {obj1 is obj2}")
Use id() to track objects:
print(f"obj1 id: {id(obj1)}")
print(f"obj2 id: {id(obj2)}")
""")
# ============================================================================
# SECTION 14: Key Takeaways
# ============================================================================
print("\n" + "=" * 70)
print("KEY TAKEAWAYS")
print("=" * 70)
print("""
1. Aliasing: = creates a reference, not a copy
2. Shallow copy: Creates new outer object, shares nested objects
3. Deep copy: Recursively copies all objects
4. Shallow copy methods: [:], list(), dict.copy(), copy.copy()
5. Deep copy method: copy.deepcopy()
6. Shallow copy is faster but limited for nested structures
7. Deep copy provides complete independence but is slower
8. Always use 'is' to check if objects are shared
9. Tuples are special: shallow copy returns the same object
10. Choose copying strategy based on structure and requirements
DECISION TREE:
- Need shared state? → Use aliasing (=)
- Simple structure? → Use shallow copy ([:], .copy())
- Nested mutable objects? → Use deep copy (deepcopy())
- Unsure? → Use deep copy (safer)
""")
# ============================================================================
# PRACTICE EXERCISES
# ============================================================================
print("\n" + "=" * 70)
print("PRACTICE EXERCISES")
print("=" * 70)
print("""
Master copying with these exercises:
1. Create a 3-level nested list. Show that shallow copy fails but
deep copy succeeds in creating independence.
2. Create a custom class with nested attributes. Demonstrate the
difference between shallow and deep copy.
3. Write a function that takes a list and returns a modified copy
without changing the original. Test with nested lists.
4. Measure the performance difference between shallow and deep copy
for various structure sizes (10x10, 100x100, 1000x1000).
5. Create a dictionary with nested lists and dicts. Show what happens
with aliasing, shallow copy, and deep copy when you modify values.
6. Debug a piece of code where unwanted aliasing causes bugs.
See exercises_02_intermediate.py for complete practice problems!
""")