Skip to content

Closure Fundamentals

클로저는 자신이 정의된 스코프의 변수를 캡처하는 함수입니다.

What is a Closure?

Definition

def outer():
    x = 10  # Enclosing variable

    def inner():
        return x  # Captures x

    return inner

f = outer()
print(f())  # 10 — x is still accessible

outer()가 반환된 후에도 innerx에 접근할 수 있습니다. 이것이 클로저입니다.

Key Terms

Term Definition
Closure Function with captured variables from enclosing scope
Free Variable Variable used in function but defined in enclosing scope
Cell CPython object that stores captured variable
Enclosing Scope Parent function's local scope

Free Variables and Cells

Free Variables

변수가 함수 내에서 사용되지만 로컬에서 정의되지 않은 경우:

def outer():
    x = 10

    def inner():
        return x  # x is "free" in inner

    return inner

f = outer()
print(f.__code__.co_freevars)  # ('x',)

Cell Objects

CPython은 캡처된 변수를 cell 객체에 저장합니다:

def outer():
    x = 10

    def inner():
        return x

    return inner

f = outer()
print(f.__closure__)  # (<cell at 0x...>,)
print(f.__closure__[0].cell_contents)  # 10

Cellvars vs Freevars

같은 변수가 두 가지 관점에서 보입니다:

def outer():
    x = 10  # cellvar in outer

    def inner():
        return x  # freevar in inner

    return inner

# In outer: x is a cellvar (being captured)
print(outer.__code__.co_cellvars)  # ('x',)

# In inner: x is a freevar (captured from outside)
f = outer()
print(f.__code__.co_freevars)  # ('x',)
Perspective Variable Type Meaning
Enclosing function cellvar "I'm being captured"
Inner function freevar "I captured this"

Multi-Level Nesting

Three-Level Example

def level1():
    x = "L1"

    def level2():
        y = "L2"

        def level3():
            return x, y  # Both are free variables

        return level3

    return level2()

f = level1()
print(f())  # ('L1', 'L2')
print(f.__code__.co_freevars)  # ('x', 'y')

Only Used Variables Are Captured

def outer():
    x = "used"
    y = "unused"

    def inner():
        return x  # Only x is captured

    return inner

f = outer()
print(f.__code__.co_freevars)  # ('x',) — y not captured

Variable Shadowing

def outer():
    x = "outer"

    def middle():
        x = "middle"  # Shadows outer's x

        def inner():
            return x  # Gets nearest x

        return inner

    return middle()

f = outer()
print(f())  # 'middle'

Python은 안쪽에서 바깥쪽으로 변수를 찾습니다.


Scope Chain (LEGB)

변수 조회 순서:

Local → Enclosing → Global → Builtin
x = "global"

def level1():
    x = "level1"

    def level2():
        x = "level2"

        def level3():
            print(x)  # "level2" (nearest enclosing)

        level3()

    level2()

level1()

Skipping Levels

def outer():
    x = 10

    def middle():
        # No x defined here

        def inner():
            return x  # Skips middle, uses outer's x

        return inner

    return middle()

f = outer()
print(f())  # 10

Closure Inspection

Complete Inspection Example

def make_counter(start=0):
    count = start

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

    return increment

counter = make_counter(10)

# Inspect
print(counter.__closure__)  # (<cell ...>,)
print(counter.__code__.co_freevars)  # ('count',)

for var, cell in zip(counter.__code__.co_freevars, counter.__closure__):
    print(f"{var} = {cell.cell_contents}")
# count = 10

Summary

Concept Description
Closure Function + captured environment
Free variable Referenced but not locally defined
Cell CPython's storage for captured variables
Cellvar Variable being captured (in outer function)
Freevar Captured variable (in inner function)
LEGB Local → Enclosing → Global → Builtin

Runnable Example: closures_examples.py

"""
Python Closures - Practical Examples
This file contains various practical examples of closures in Python.
Run this file to see closures in action!
"""

if __name__ == "__main__":

    print("=" * 70)
    print("PYTHON CLOSURES - EXAMPLES")
    print("=" * 70)

    # ============================================================================
    # EXAMPLE 1: Basic Closure
    # ============================================================================
    print("\n1. BASIC CLOSURE")
    print("-" * 70)

    def outer(message):
        # message is captured by the closure
        def inner():
            print(message)
        return inner

    greet = outer("Hello, World!")
    greet()  # Prints: Hello, World!

    print("\nEven though outer() finished, inner() remembers 'message'")

    # ============================================================================
    # EXAMPLE 2: Multiple Closures with Different Environments
    # ============================================================================
    print("\n\n2. MULTIPLE CLOSURES WITH DIFFERENT ENVIRONMENTS")
    print("-" * 70)

    def make_multiplier(factor):
        def multiply(number):
            return number * factor
        return multiply

    times_2 = make_multiplier(2)
    times_3 = make_multiplier(3)
    times_10 = make_multiplier(10)

    print(f"5 * 2 = {times_2(5)}")
    print(f"5 * 3 = {times_3(5)}")
    print(f"5 * 10 = {times_10(5)}")
    print("\nEach closure maintains its own 'factor' value")

    # ============================================================================
    # EXAMPLE 3: Using nonlocal to Modify Enclosing Variables
    # ============================================================================
    print("\n\n3. USING NONLOCAL TO MODIFY ENCLOSING VARIABLES")
    print("-" * 70)

    def make_counter(start=0):
        count = start

        def increment():
            nonlocal count  # Allow modification of outer variable
            count += 1
            return count

        def decrement():
            nonlocal count
            count -= 1
            return count

        def get_count():
            return count

        return increment, decrement, get_count

    inc, dec, get = make_counter(10)
    print(f"Initial count: {get()}")
    print(f"After increment: {inc()}")
    print(f"After increment: {inc()}")
    print(f"After increment: {inc()}")
    print(f"After decrement: {dec()}")
    print(f"Final count: {get()}")

    # ============================================================================
    # EXAMPLE 4: Factory Functions
    # ============================================================================
    print("\n\n4. FACTORY FUNCTIONS")
    print("-" * 70)

    def make_power(exponent):
        def power(base):
            return base ** exponent
        return power

    square = make_power(2)
    cube = make_power(3)
    fourth_power = make_power(4)

    number = 3
    print(f"{number}^2 = {square(number)}")
    print(f"{number}^3 = {cube(number)}")
    print(f"{number}^4 = {fourth_power(number)}")

    # ============================================================================
    # EXAMPLE 5: Data Encapsulation (Private Variables)
    # ============================================================================
    print("\n\n5. DATA ENCAPSULATION (PRIVATE VARIABLES)")
    print("-" * 70)

    def make_account(initial_balance):
        balance = initial_balance  # Private variable

        def deposit(amount):
            nonlocal balance
            if amount > 0:
                balance += amount
                return f"Deposited ${amount}. New balance: ${balance}"
            return "Invalid amount"

        def withdraw(amount):
            nonlocal balance
            if amount > balance:
                return "Insufficient funds"
            if amount > 0:
                balance -= amount
                return f"Withdrew ${amount}. New balance: ${balance}"
            return "Invalid amount"

        def get_balance():
            return f"Current balance: ${balance}"

        return {
            'deposit': deposit,
            'withdraw': withdraw,
            'get_balance': get_balance
        }

    account = make_account(1000)
    print(account['get_balance']())
    print(account['deposit'](500))
    print(account['withdraw'](200))
    print(account['withdraw'](2000))  # Insufficient funds
    print(account['get_balance']())

    # Note: There's no way to directly access 'balance' from outside!

    # ============================================================================
    # EXAMPLE 6: Function Decorators (Built on Closures)
    # ============================================================================
    print("\n\n6. FUNCTION DECORATORS (BUILT ON CLOSURES)")
    print("-" * 70)

    def make_logger(func):
        def wrapper(*args, **kwargs):
            print(f"🔍 Calling {func.__name__} with args={args}, kwargs={kwargs}")
            result = func(*args, **kwargs)
            print(f"✅ {func.__name__} returned {result}")
            return result
        return wrapper

    @make_logger
    def add(a, b):
        return a + b

    @make_logger
    def multiply(x, y):
        return x * y

    result1 = add(3, 5)
    print(f"Result: {result1}\n")
    result2 = multiply(4, 7)
    print(f"Result: {result2}")

    # ============================================================================
    # EXAMPLE 7: Configuration Functions
    # ============================================================================
    print("\n\n7. CONFIGURATION FUNCTIONS")
    print("-" * 70)

    def make_formatter(prefix, suffix):
        def format_text(text):
            return f"{prefix}{text}{suffix}"
        return format_text

    html_bold = make_formatter("<b>", "</b>")
    html_italic = make_formatter("<i>", "</i>")
    parentheses = make_formatter("(", ")")
    quotes = make_formatter('"', '"')

    text = "Hello"
    print(f"Original: {text}")
    print(f"Bold: {html_bold(text)}")
    print(f"Italic: {html_italic(text)}")
    print(f"Parentheses: {parentheses(text)}")
    print(f"Quoted: {quotes(text)}")

    # ============================================================================
    # EXAMPLE 8: Closures with Multiple Free Variables
    # ============================================================================
    print("\n\n8. CLOSURES WITH MULTIPLE FREE VARIABLES")
    print("-" * 70)

    def make_linear_function(slope, intercept):
        """Create a linear function: y = slope * x + intercept"""
        def linear(x):
            return slope * x + intercept
        return linear

    line1 = make_linear_function(2, 3)   # y = 2x + 3
    line2 = make_linear_function(-1, 5)  # y = -x + 5

    x_value = 4
    print(f"For x = {x_value}:")
    print(f"  y = 2x + 3 = {line1(x_value)}")
    print(f"  y = -x + 5 = {line2(x_value)}")

    # ============================================================================
    # EXAMPLE 9: Closures for Callbacks with State
    # ============================================================================
    print("\n\n9. CLOSURES FOR CALLBACKS WITH STATE")
    print("-" * 70)

    def make_click_handler(element_id):
        click_count = 0

        def handle_click():
            nonlocal click_count
            click_count += 1
            print(f"  {element_id} clicked {click_count} time(s)")

        return handle_click

    button1 = make_click_handler("Button 1")
    button2 = make_click_handler("Button 2")

    print("Simulating button clicks:")
    button1()  # Button 1 clicked 1 time
    button1()  # Button 1 clicked 2 times
    button2()  # Button 2 clicked 1 time
    button1()  # Button 1 clicked 3 times
    button2()  # Button 2 clicked 2 times

    # ============================================================================
    # EXAMPLE 10: Closure vs Class Comparison
    # ============================================================================
    print("\n\n10. CLOSURE VS CLASS COMPARISON")
    print("-" * 70)

    # Using a closure
    def make_counter_closure(start=0):
        count = start
        def increment():
            nonlocal count
            count += 1
            return count
        return increment

    # Using a class
    class CounterClass:
        def __init__(self, start=0):
            self.count = start

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

    print("Closure implementation:")
    counter_closure = make_counter_closure(5)
    print(f"  {counter_closure()}")
    print(f"  {counter_closure()}")

    print("\nClass implementation:")
    counter_class = CounterClass(5)
    print(f"  {counter_class.increment()}")
    print(f"  {counter_class.increment()}")

    print("\nBoth achieve the same result!")

    # ============================================================================
    # EXAMPLE 11: Closures in Loops (Common Pitfall and Solution)
    # ============================================================================
    print("\n\n11. CLOSURES IN LOOPS (PITFALL AND SOLUTION)")
    print("-" * 70)

    # WRONG WAY
    print("❌ Common mistake:")
    def create_multipliers_wrong():
        multipliers = []
        for i in range(5):
            multipliers.append(lambda x: x * i)
        return multipliers

    funcs_wrong = create_multipliers_wrong()
    print(f"  Expected: 0 * 2 = 0, Got: {funcs_wrong[0](2)}")
    print(f"  Expected: 1 * 2 = 2, Got: {funcs_wrong[1](2)}")
    print("  All closures reference the same 'i', which is 4 after the loop!")

    # RIGHT WAY - Solution 1: Default argument
    print("\n✅ Solution 1: Default argument:")
    def create_multipliers_correct1():
        multipliers = []
        for i in range(5):
            multipliers.append(lambda x, i=i: x * i)  # Capture current i
        return multipliers

    funcs_correct1 = create_multipliers_correct1()
    print(f"  0 * 2 = {funcs_correct1[0](2)}")
    print(f"  1 * 2 = {funcs_correct1[1](2)}")
    print(f"  2 * 2 = {funcs_correct1[2](2)}")

    # RIGHT WAY - Solution 2: Factory function
    print("\n✅ Solution 2: Factory function:")
    def make_multiplier(i):
        return lambda x: x * i

    def create_multipliers_correct2():
        return [make_multiplier(i) for i in range(5)]

    funcs_correct2 = create_multipliers_correct2()
    print(f"  0 * 2 = {funcs_correct2[0](2)}")
    print(f"  1 * 2 = {funcs_correct2[1](2)}")
    print(f"  2 * 2 = {funcs_correct2[2](2)}")

    # ============================================================================
    # EXAMPLE 12: Memoization Using Closures
    # ============================================================================
    print("\n\n12. MEMOIZATION USING CLOSURES")
    print("-" * 70)

    def make_memoized(func):
        cache = {}

        def memoized(*args):
            if args in cache:
                print(f"  💾 Cache hit for {args}")
                return cache[args]
            print(f"  🔄 Computing for {args}")
            result = func(*args)
            cache[args] = result
            return result

        return memoized

    @make_memoized
    def fibonacci(n):
        if n < 2:
            return n
        return fibonacci(n-1) + fibonacci(n-2)

    print("Calculating fibonacci(5):")
    result = fibonacci(5)
    print(f"Result: {result}")

    print("\nCalculating fibonacci(5) again:")
    result = fibonacci(5)
    print(f"Result: {result}")

    # ============================================================================
    # EXAMPLE 13: Partial Function Application
    # ============================================================================
    print("\n\n13. PARTIAL FUNCTION APPLICATION")
    print("-" * 70)

    def partial(func, *fixed_args, **fixed_kwargs):
        def wrapper(*args, **kwargs):
            return func(*fixed_args, *args, **fixed_kwargs, **kwargs)
        return wrapper

    def greet(greeting, name, punctuation="!"):
        return f"{greeting}, {name}{punctuation}"

    # Create specialized greeting functions
    say_hello = partial(greet, "Hello")
    say_goodbye = partial(greet, "Goodbye")
    say_hi_excited = partial(greet, "Hi", punctuation="!!!")

    print(say_hello("Alice"))
    print(say_goodbye("Bob"))
    print(say_hi_excited("Charlie"))

    # ============================================================================
    # EXAMPLE 14: Function Composition
    # ============================================================================
    print("\n\n14. FUNCTION COMPOSITION")
    print("-" * 70)

    def compose(f, g):
        def composed(x):
            return f(g(x))
        return composed

    def add_10(x):
        return x + 10

    def multiply_2(x):
        return x * 2

    def square(x):
        return x ** 2

    # Create composed functions
    add_then_multiply = compose(multiply_2, add_10)
    multiply_then_square = compose(square, multiply_2)

    x = 5
    print(f"Input: {x}")
    print(f"Add 10, then multiply by 2: {add_then_multiply(x)}")  # (5+10)*2 = 30
    print(f"Multiply by 2, then square: {multiply_then_square(x)}")  # (5*2)^2 = 100

    # ============================================================================
    # EXAMPLE 15: Stateful Generators with Closures
    # ============================================================================
    print("\n\n15. STATEFUL GENERATORS WITH CLOSURES")
    print("-" * 70)

    def make_id_generator(prefix="ID"):
        counter = 0

        def generate_id():
            nonlocal counter
            counter += 1
            return f"{prefix}-{counter:04d}"

        return generate_id

    user_id = make_id_generator("USER")
    order_id = make_id_generator("ORDER")

    print("Generating user IDs:")
    for _ in range(3):
        print(f"  {user_id()}")

    print("\nGenerating order IDs:")
    for _ in range(3):
        print(f"  {order_id()}")

    print("\n" + "=" * 70)
    print("END OF EXAMPLES")
    print("=" * 70)