Skip to content

nonlocal and Mutation

클로저에서 외부 변수를 수정하는 방법입니다.

Rebinding vs Mutation

The Core Difference

def outer():
    x = [1, 2, 3]

    def rebind():
        x = [4, 5, 6]  # Creates NEW local variable!

    def mutate():
        x.append(4)  # Modifies SAME object

    rebind()
    print(x)  # [1, 2, 3] — unchanged

    mutate()
    print(x)  # [1, 2, 3, 4] — modified

outer()
Operation What Happens Works Without nonlocal?
x = value Creates new binding ❌ No
x.method() Mutates existing object ✅ Yes
x[i] = v Mutates existing object ✅ Yes

Why?

  • Assignment (=) creates a new local variable
  • Method calls operate on the looked-up object

The nonlocal Keyword

nonlocal은 내부 함수가 외부 변수를 재바인딩할 수 있게 합니다:

def outer():
    count = 0

    def increment():
        nonlocal count  # Required for rebinding
        count += 1
        return count

    return increment

counter = increment()
print(counter())  # 1
print(counter())  # 2

Without nonlocal — Error

def outer():
    count = 0

    def increment():
        count += 1  # UnboundLocalError!
        return count

    return increment

count += 1count = count + 1이므로, assignment가 count를 로컬로 만들어버립니다.


nonlocal Resolution

nonlocal은 가장 가까운 enclosing scope의 변수를 찾습니다:

def level1():
    x = 1

    def level2():
        x = 2

        def level3():
            nonlocal x  # Modifies level2's x (closest)
            x = 3

        level3()
        print(f"level2: {x}")  # 3

    level2()
    print(f"level1: {x}")  # 1 (unchanged)

level1()

Augmented Assignment Operators

With Mutable Objects (Works)

def outer():
    items = [1, 2, 3]

    def extend():
        items += [4, 5]  # Calls __iadd__, mutates in place

    extend()
    print(items)  # [1, 2, 3, 4, 5]

outer()

With Immutable Objects (Fails)

def outer():
    count = 0

    def increment():
        count += 1  # Rebinding! Error without nonlocal

    # increment()  # UnboundLocalError

outer()

Cell Sharing

여러 내부 함수가 같은 cell을 공유합니다:

def outer():
    x = 0

    def inc():
        nonlocal x
        x += 1
        return x

    def dec():
        nonlocal x
        x -= 1
        return x

    def get():
        return x

    return inc, dec, get

inc, dec, get = outer()

print(inc())  # 1
print(inc())  # 2
print(dec())  # 1
print(get())  # 1

Verify Same Cell

def outer():
    x = 10
    f1 = lambda: x
    f2 = lambda: x
    return f1, f2

a, b = outer()
print(a.__closure__[0] is b.__closure__[0])  # True — same cell

Workarounds Without nonlocal

Using Mutable Container

def outer():
    count = [0]  # Mutable container

    def increment():
        count[0] += 1  # Mutation, not rebinding
        return count[0]

    return increment

counter = outer()
print(counter())  # 1
print(counter())  # 2

Using Object Attribute

def outer():
    class State:
        count = 0

    def increment():
        State.count += 1
        return State.count

    return increment

Using Function Attribute

def counter():
    def inner():
        inner.count += 1
        return inner.count
    inner.count = 0
    return inner

c = counter()
print(c())  # 1
print(c())  # 2

Best Practices

When to Use nonlocal

✅ Simple state management in closures:

def make_counter(start=0):
    count = start

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

When to Avoid nonlocal

❌ Complex state → Use a class instead:

# Instead of multiple nonlocal variables
class Counter:
    def __init__(self, start=0):
        self.count = start

    def increment(self):
        self.count += 1
        return self.count

Summary

Concept Description
Rebinding x = value creates new variable; needs nonlocal
Mutation x.method() modifies object; works without nonlocal
nonlocal Allows rebinding of enclosing scope variable
Cell sharing Multiple closures from same function share cells
Workaround Use mutable container [value] if avoiding nonlocal

Runnable Example: closure_mutable_state_example.py

"""
Closures with Mutable State - Using Free Variables for Stateful Computation
This tutorial demonstrates how closures can maintain mutable state
by capturing variables from their enclosing scope.
Run this file to see closures with state in action!
"""

if __name__ == "__main__":

    print("=" * 70)
    print("CLOSURES WITH MUTABLE STATE - EXAMPLES")
    print("=" * 70)

    # ============================================================================
    # EXAMPLE 1: Understanding Closure State
    # ============================================================================
    print("\n1. UNDERSTANDING CLOSURE STATE")
    print("-" * 70)

    print("\nFrom the previous closure tutorial, we learned:")
    print("- Closures capture variables from their enclosing scope")
    print("- These captured variables are called FREE VARIABLES")
    print("- Free variables are stored in the closure object")
    print("\nBut can we use mutable objects (lists, dicts) in closures?")
    print("YES! And this lets us build stateful functions!\n")

    # ============================================================================
    # EXAMPLE 2: Simple Closure - Constant Free Variable
    # ============================================================================
    print("\n2. SIMPLE CLOSURE - CONSTANT FREE VARIABLE")
    print("-" * 70)

    def make_multiplier(factor):
        """Create a function that multiplies by a factor."""
        def multiply(x):
            return x * factor
        return multiply

    times3 = make_multiplier(3)
    times5 = make_multiplier(5)

    print("\nDefined make_multiplier(factor)")
    print("Created: times3 = make_multiplier(3)")
    print("Created: times5 = make_multiplier(5)\n")

    print(f"times3(10) = {times3(10)}")
    print(f"times5(10) = {times5(10)}")

    print("\nThese closures captured 'factor' as a free variable")
    print("But factor is immutable (int), so it never changes")

    # ============================================================================
    # EXAMPLE 3: Closure with Mutable State - The Accumulator
    # ============================================================================
    print("\n3. CLOSURE WITH MUTABLE STATE - THE ACCUMULATOR")
    print("-" * 70)

    def make_accumulator():
        """
        Create a function that accumulates values.

        The accumulator function maintains its own total,
        which persists between calls!
        """
        total = 0  # This variable is captured by the closure

        def accumulate(value):
            nonlocal total  # Declare that we're using the outer 'total'
            total += value  # Modify the captured variable!
            return total

        return accumulate

    print("\nDefined make_accumulator()")
    print("Key: uses 'nonlocal total' to modify the captured variable\n")

    acc = make_accumulator()

    print("Created: acc = make_accumulator()\n")

    print("Calling acc() multiple times:")
    print(f"  acc(1) = {acc(1)}")
    print(f"  acc(2) = {acc(2)}")
    print(f"  acc(5) = {acc(5)}")
    print(f"  acc(3) = {acc(3)}")

    print("\nNOTICE: The total PERSISTS between calls!")
    print("- Each call modifies the same 'total' variable")
    print("- This is stateful behavior without a class!")

    # ============================================================================
    # EXAMPLE 4: Understanding 'nonlocal'
    # ============================================================================
    print("\n4. UNDERSTANDING 'nonlocal'")
    print("-" * 70)

    print("\nThe 'nonlocal' keyword is crucial for modifying captured variables!\n")

    print("Without 'nonlocal':")
    print("""
    def make_acc_broken():
        total = 0
        def accumulate(value):
            total += value  # ERROR! total is local, not the outer one!
            return total
        return accumulate
    """)

    print("\nWith 'nonlocal':")
    print("""
    def make_accumulator():
        total = 0
        def accumulate(value):
            nonlocal total  # OK! Use the outer total
            total += value  # Modifies the outer total
            return total
        return accumulate
    """)

    print("\nWITHOUT 'nonlocal':")
    print("- Python thinks 'total' is a NEW local variable")
    print("- 'total += value' tries to read total before assigning")
    print("- UnboundLocalError!")

    print("\nWITH 'nonlocal':")
    print("- Python knows 'total' refers to the outer variable")
    print("- 'total += value' modifies the captured variable")
    print("- State persists across calls!")

    # ============================================================================
    # EXAMPLE 5: Closure with Mutable Container - The Running Average
    # ============================================================================
    print("\n5. CLOSURE WITH MUTABLE STATE - RUNNING AVERAGE")
    print("-" * 70)

    def make_averager():
        """
        Create a function that computes running average.

        Each call appends to a list and computes the average.
        The list is a MUTABLE object captured by the closure.
        """
        series = []  # Mutable list captured by closure

        def averager(new_value):
            series.append(new_value)  # Modify the mutable list
            total = sum(series)
            return total / len(series)

        return averager

    print("\nDefined make_averager()")
    print("Uses a mutable list to store all values\n")

    avg = make_averager()

    print("Created: avg = make_averager()\n")

    print("Computing running average:")
    result1 = avg(10)
    print(f"  avg(10) = {result1}")
    print(f"    All values so far: [10]")
    print(f"    Average: 10 / 1 = {result1}")

    result2 = avg(11)
    print(f"\n  avg(11) = {result2}")
    print(f"    All values so far: [10, 11]")
    print(f"    Average: (10 + 11) / 2 = {result2}")

    result3 = avg(12)
    print(f"\n  avg(12) = {result3}")
    print(f"    All values so far: [10, 11, 12]")
    print(f"    Average: (10 + 11 + 12) / 3 = {result3}")

    print("\nKEY INSIGHT:")
    print("- The list 'series' is MUTABLE")
    print("- We don't need 'nonlocal' because we're not reassigning it")
    print("- We're just modifying its contents with append()")
    print("- The list PERSISTS between calls!")

    # ============================================================================
    # EXAMPLE 6: Inspecting Closure Objects
    # ============================================================================
    print("\n6. INSPECTING CLOSURE OBJECTS")
    print("-" * 70)

    print("\nWe can inspect the closure to see captured variables!\n")

    print("For avg = make_averager():\n")

    print(f"avg.__code__.co_varnames = {avg.__code__.co_varnames}")
    print("  ^ Local variables in averager() function")

    print(f"\navg.__code__.co_freevars = {avg.__code__.co_freevars}")
    print("  ^ Names of free variables (captured from outer scope)")

    print(f"\navg.__closure__ = {avg.__closure__}")
    print("  ^ Tuple of cell objects holding the free variables")

    print(f"\nNumber of cells: {len(avg.__closure__)}")
    print(f"Contents of the 'series' cell:")
    print(f"  avg.__closure__[0].cell_contents = {avg.__closure__[0].cell_contents}")

    print("\nBEFORE MAKING CALLS: series is empty")
    print(f"AFTER calling avg(10), avg(11), avg(12):")
    print(f"  series still contains: {avg.__closure__[0].cell_contents}")

    # ============================================================================
    # EXAMPLE 7: Multiple Closures - Independent State
    # ============================================================================
    print("\n7. MULTIPLE CLOSURES - INDEPENDENT STATE")
    print("-" * 70)

    avg1 = make_averager()
    avg2 = make_averager()

    print("\nCreated: avg1 = make_averager()")
    print("Created: avg2 = make_averager()\n")

    print("They have SEPARATE series lists!\n")

    print("avg1(10):", avg1(10))
    print("avg1(20):", avg1(20))

    print("\navg2(100):", avg2(100))
    print("avg2(200):", avg2(200))

    print("\nEach closure has its own state:")
    print(f"avg1's series: {avg1.__closure__[0].cell_contents}")
    print(f"avg2's series: {avg2.__closure__[0].cell_contents}")

    print("\nMULTIPLE INSTANCES:")
    print("- Each call to make_averager() creates NEW variables")
    print("- Each returned function captures its OWN series list")
    print("- State is independent!")

    # ============================================================================
    # EXAMPLE 8: Practical Example - Event Logger with State
    # ============================================================================
    print("\n8. PRACTICAL EXAMPLE - EVENT LOGGER")
    print("-" * 70)

    def make_event_logger(name):
        """
        Create a function that logs events with timestamps.

        The logger maintains a list of events.
        """
        events = []

        def log_event(message):
            import time
            timestamp = time.strftime("%H:%M:%S")
            event = f"[{timestamp}] {message}"
            events.append(event)
            print(f"{name}: {event}")
            return len(events)

        def get_events():
            return events.copy()

        log_event.get_events = get_events  # Attach method to function
        return log_event

    print("\nDefined make_event_logger(name)")
    print("This creates a logger that maintains state\n")

    logger1 = make_event_logger("APP")
    logger2 = make_event_logger("AUTH")

    print("Created: logger1 = make_event_logger('APP')")
    print("Created: logger2 = make_event_logger('AUTH')\n")

    logger1("System started")
    logger1("Processing request")
    logger2("User login attempt")
    logger1("Request completed")
    logger2("User logged in")

    print("\nAll events logged by logger1:")
    for event in logger1.get_events():
        print(f"  {event}")

    print("\nAll events logged by logger2:")
    for event in logger2.get_events():
        print(f"  {event}")

    print("\nSEPARATE STATE:")
    print("- Each logger has its own events list")
    print("- Loggers are independent")
    print("- State persists across calls")

    # ============================================================================
    # SUMMARY: Closures with Mutable State
    # ============================================================================
    print("\n" + "=" * 70)
    print("SUMMARY - CLOSURES WITH MUTABLE STATE")
    print("=" * 70)

    print("""
    IMMUTABLE FREE VARIABLES:
      - Captured at closure creation time
      - Can read them, but can't reassign without 'nonlocal'
      - Example: factor in make_multiplier()

    IMMUTABLE FREE VARIABLES WITH 'nonlocal':
      - Can reassign using 'nonlocal' keyword
      - Example: total in make_accumulator()

    MUTABLE FREE VARIABLES:
      - Lists, dicts, objects, etc.
      - Can modify contents (append, add keys, etc.)
      - No 'nonlocal' needed for modifications!
      - Only need 'nonlocal' if reassigning the variable

    WHEN TO USE CLOSURES VS CLASSES:

    Use closures for:
      - Simple functions with minimal state
      - Lightweight stateful callbacks
      - Higher-order functions

    Use classes for:
      - Multiple methods on the same state
      - Complex state management
      - When inheritance is useful
      - Public API (classes are more explicit)

    CLOSURES ARE POWERFUL:
      - Maintain state without classes
      - Create factory functions
      - Implement decorators
      - Enable functional programming style

    KEY POINTS:
    - Free variables enable closures to have state
    - 'nonlocal' allows reassigning captured variables
    - Each closure instance has its own captured variables
    - Mutable objects can be modified without 'nonlocal'
    - Inspecting __code__ and __closure__ shows what's captured
    """)