21. 변수 스코프와 이름 해석
Python에서 변수를 만들면, 그 변수는 어디에 “존재”할까요? 함수가 함수 밖에서 생성된 변수를 볼 수 있을까요? 함수 밖의 코드가 함수 안에서 생성된 변수에 접근할 수 있을까요? 이런 질문들은 스코프(scope)—이름이 보이고 사용할 수 있는 프로그램의 영역—에 관한 것입니다.
스코프를 이해하는 것은 올바르고 예측 가능하게 동작하는 함수를 작성하는 데 매우 중요합니다. 이 지식이 없으면, 변수가 기대한 값이 아닌 값을 갖거나, 변수에 대한 변경이 의도한 대로 유지되지 않는 버그를 실수로 만들 수 있습니다.
이 장에서는 Python이 어떤 이름이 어떤 변수를 가리키는지 어떻게 결정하는지, 변수가 접근 가능한 범위를 어떻게 제어하는지, 그리고 이름을 삭제하면 어떤 일이 일어나는지 살펴보겠습니다. 마지막에는 Python 프로그램에서 변수 가시성을 지배하는 규칙을 이해하게 될 것입니다.
21.1) 지역 변수와 전역 변수
Python의 모든 변수는 특정한 스코프(scope)—해당 변수 이름이 정의되고 접근 가능한 코드 영역—안에 존재합니다. 가장 기본적인 두 가지 스코프는 지역(local)과 전역(global)입니다.
전역 스코프 이해하기
프로그램의 최상위 수준—함수 밖—에서 생성된 변수는 전역 스코프(global scope)에 존재합니다. 이런 변수들을 전역 변수(global variables)라고 하며, 정의된 이후에는 모듈 어디에서든 접근할 수 있습니다.
# 전역 변수 - 모듈 수준에서 정의됨
total_users = 0
def show_user_count():
# 이 함수는 전역 변수를 READ할 수 있음
print(f"Total users: {total_users}")
show_user_count() # Output: Total users: 0
print(total_users) # Output: 0이 예제에서 total_users는 전역 변수입니다. 함수 show_user_count()와 모듈 수준의 코드 모두가 이에 접근할 수 있습니다. 전역 변수는 프로그램 파일 전체에서 보이는 것으로 생각할 수 있습니다.
지역 스코프 이해하기
함수 내부에서 생성된 변수는 그 함수의 지역 스코프(local scope)에 존재합니다. 이런 변수들을 지역 변수(local variables)라고 하며, 정의된 함수 안에서만 접근할 수 있습니다. 함수 실행이 끝나면 지역 변수는 사라집니다.
def calculate_discount(price):
# discount_rate는 이 함수에 LOCAL임
discount_rate = 0.15
discount_amount = price * discount_rate
return discount_amount
result = calculate_discount(100)
print(result) # Output: 15.0
# 이것은 오류를 발생시킵니다 - discount_rate는 여기에는 존재하지 않습니다
# print(discount_rate) # NameError: name 'discount_rate' is not defined변수 discount_rate와 discount_amount는 calculate_discount()가 실행되는 동안에만 존재합니다. 함수가 반환되면 이 이름들은 더 이상 존재하지 않습니다. 이는 사실 좋은 점입니다. 함수가 임시 변수로 프로그램을 어지럽히는 것을 막아주기 때문입니다.
지역 스코프가 중요한 이유
지역 스코프는 캡슐화(encapsulation)를 제공합니다—각 함수는 자신만의 사적인 작업 공간을 갖습니다. 즉, 서로 다른 함수에서 같은 변수 이름을 충돌 없이 사용할 수 있습니다:
def calculate_tax(amount):
rate = 0.08 # 지역 변수
return amount * rate
def calculate_shipping(weight):
rate = 5.00 # 같은 이름을 가진 다른 지역 변수
return weight * rate
tax = calculate_tax(100)
shipping = calculate_shipping(3)
print(f"Tax: ${tax}") # Output: Tax: $8.0
print(f"Shipping: ${shipping}") # Output: Shipping: $15.0두 함수 모두 rate라는 변수를 사용하지만, 서로 다른 지역 스코프 안의 완전히 별개 변수입니다. 한 함수에서 rate를 변경해도 다른 함수의 rate에는 영향을 주지 않습니다. 이런 격리는 함수를 더 신뢰할 수 있고 이해하기 쉽게 만듭니다.
함수에서 전역 변수 읽기
함수는 특별한 문법 없이도 전역 변수를 읽을 수 있습니다:
# 전역 설정
max_login_attempts = 3
def check_login(password):
# 전역 변수 읽기
if password == "secret123":
return "Login successful"
else:
return f"Invalid password. You have {max_login_attempts} attempts."
result = check_login("wrong")
print(result) # Output: Invalid password. You have 3 attempts.함수 check_login()은 max_login_attempts가 전역 변수이기 때문에 이를 읽을 수 있습니다. 하지만 우리가 이해해야 할 중요한 제한이 하나 있습니다.
대입은 지역 변수를 만든다는 규칙
여기서 스코프가 까다로워집니다. 함수 안에서 어떤 변수 이름에 대입하면, 같은 이름의 전역 변수가 존재하더라도 Python은 그 이름으로 새 지역 변수를 만듭니다:
counter = 0 # 전역 변수
def increment_counter():
# 경고: 이는 counter라는 NEW 지역 변수를 만듭니다 - 시연용
# 문제: 지역에 대입하기 전에 counter를 읽으려고 함
counter = counter + 1 # UnboundLocalError: local variable 'counter' referenced before assignment
print(counter)
# increment_counter() # 이 줄은 UnboundLocalError를 발생시킴이 코드는 Python이 대입문 counter = counter + 1을 보고 counter가 지역 변수라고 결정하기 때문에 실패합니다. 그런데 counter + 1을 평가하려 할 때 지역 변수 counter에는 아직 값이 없습니다—대입하기 전에 사용하려는 셈입니다.
이는 흔한 혼란의 원인입니다. 규칙은 다음과 같습니다: 함수 본문 어디에서든 어떤 변수 이름에 대입하면, 그 이름은 전체 함수에서(대입 이전이라도) 지역 변수로 취급됩니다.
이를 더 명확히 보겠습니다:
message = "Hello" # 전역 변수
def show_message():
print(message) # 이건 동작합니다 - 전역을 그냥 읽음
def change_message():
# 경고: 이는 흔한 오류를 보여줍니다 - 시연용
# 문제: 아래의 대입을 보고 Python은 message를 함수 전체에서 지역으로 취급함
print(message) # UnboundLocalError!
message = "Goodbye" # 이로 인해 message는 함수 전체에서 지역변수로 취급하게 됨
show_message() # Output: Hello
# change_message() # 이 줄은 UnboundLocalError를 발생시킴함수 show_message()는 message를 읽기만 하므로 문제없이 동작합니다. 하지만 change_message()는 두 번째 줄의 대입 때문에 Python이 print() 문을 포함해 함수 전체에서 message를 지역으로 취급하므로 실패합니다.
매개변수는 지역 변수입니다
함수 매개변수는 함수가 호출될 때 전달된 인자에서 초기 값을 받는 지역 변수입니다:
def greet(name): # 'name'은 지역 변수
greeting = f"Hello, {name}!" # 'greeting'도 지역 변수
return greeting
message = greet("Alice")
print(message) # Output: Hello, Alice!
# 'name'과 'greeting'은 여기에는 존재하지 않습니다
# print(name) # NameError매개변수 name은 greet() 함수 내부에서만 존재합니다. 함수가 호출될 때 생성되며, 함수가 반환되면 사라집니다.
실용 예제: 장바구니 계산
현실적인 시나리오에서 지역과 전역 스코프가 함께 어떻게 동작하는지 보겠습니다:
# 전역 설정
tax_rate = 0.08
free_shipping_threshold = 50
def calculate_total(subtotal):
# 이 계산을 위한 지역 변수
tax = subtotal * tax_rate # 전역 tax_rate 읽기
# 배송비 결정
if subtotal >= free_shipping_threshold: # 전역 threshold 읽기
shipping = 0
else:
shipping = 5.99
total = subtotal + tax + shipping
return total
# 서로 다른 장바구니 값에 대해 계산
cart1 = calculate_total(30)
cart2 = calculate_total(60)
print(f"Cart 1 total: ${cart1:.2f}") # Output: Cart 1 total: $38.39
print(f"Cart 2 total: ${cart2:.2f}") # Output: Cart 2 total: $64.80이 예제에서:
tax_rate와free_shipping_threshold는 전역 설정 값입니다subtotal,tax,shipping,total은 각calculate_total()호출에 대한 지역 변수입니다- 각 함수 호출은 서로 분리된 지역 변수 세트를 가집니다
- 함수는 전역 설정을 읽을 수 있지만 수정하지는 않습니다
이 관심사의 분리는 코드를 명확하게 만듭니다. 전역 변수는 어디에서나 적용되는 설정을 담고, 지역 변수는 각 함수 호출에 특화된 임시 계산 결과를 담습니다.
21.2) 이름 해석을 위한 LEGB 규칙
Python이 변수 이름을 만나면, 당신이 어떤 변수를 가리키는지 어떻게 알까요? Python은 LEGB 규칙(LEGB rule)이라고 불리는 특정한 탐색 순서를 따릅니다. LEGB는 Local, Enclosing, Global, Built-in의 약자이며, Python이 그 순서대로 검색하는 네 가지 스코프를 의미합니다.
LEGB의 네 가지 스코프
LEGB 계층에서 각 스코프를 이해해 봅시다:
- Local (L): 현재 함수의 스코프
- Enclosing (E): 바깥쪽에 둘러싼(enclosing) 함수들의 스코프(현재 함수를 포함하는 함수)
- Global (G): 모듈 수준 스코프
- Built-in (B):
print,len,int같은 Python 내장 이름
변수 이름을 사용할 때, Python은 L → E → G → B 순서로 이 스코프들을 검색합니다. 처음으로 일치하는 것을 사용하고 검색을 멈춥니다.
지역 스코프: Python이 가장 먼저 찾는 곳
Python은 항상 지역 스코프를 먼저 확인합니다:
def calculate_price():
price = 100 # 지역 변수
tax = 0.08 # 지역 변수
total = price * (1 + tax)
return total
result = calculate_price()
print(result) # Output: 108.0Python이 calculate_price() 안에서 price, tax, total을 보면 지역 스코프에서 이를 찾고 해당 값을 사용합니다. 검색은 지역 스코프에서 멈춥니다—더 바깥을 볼 필요가 없습니다.
전역 스코프: 지역에 없을 때
이름이 지역에서 발견되지 않으면 Python은 전역 스코프를 확인합니다:
# 전역 변수
default_tax_rate = 0.08
default_currency = "USD"
def calculate_price(amount):
# 'amount'는 지역이므로 즉시 발견됨
# 'default_tax_rate'는 지역이 아니므로 전역 스코프에서 발견됨
total = amount * (1 + default_tax_rate)
return total
result = calculate_price(100)
print(result) # Output: 108.0Python이 함수 안에서 default_tax_rate를 만나면 지역에서 찾지 못하므로 전역 스코프로 가서 거기서 찾습니다.
내장 스코프: Python이 미리 정의한 이름들
이름이 지역이나 전역 스코프에 없으면 Python은 내장 스코프를 확인합니다—Python이 자동으로 제공하는 이름들입니다:
def process_data(numbers):
# 'numbers'는 지역
# 'len'은 지역/전역이 아니라 내장(built-in)임
count = len(numbers)
# 'max'도 내장임
maximum = max(numbers)
return count, maximum
data = [10, 25, 15, 30, 20]
result = process_data(data)
print(result) # Output: (5, 30)len과 max는 코드에서 정의하지 않았습니다—Python이 제공하는 내장 함수입니다. Python이 이 이름들을 지역이나 전역에서 찾지 못하면 내장 스코프를 확인해 그곳에서 찾습니다.
둘러싼(Enclosing) 스코프: 중첩 함수
둘러싼(enclosing) 스코프는 중첩 함수—다른 함수 안에 정의된 함수—가 있을 때 작동합니다. 여기서 LEGB의 “E”가 중요해집니다:
def outer_function():
outer_var = "I'm from outer" # inner_function의 enclosing 스코프에 있음
def inner_function():
inner_var = "I'm from inner" # inner_function의 지역
# inner_function은 inner_var(지역)과 outer_var(enclosing) 둘 다 볼 수 있음
print(inner_var) # Output: I'm from inner
print(outer_var) # Output: I'm from outer
inner_function()
outer_function()inner_function()에서 outer_function()의 스코프는 둘러싼 스코프입니다. inner_function()이 outer_var를 참조하면, Python은 다음 순서로 검색합니다:
inner_function()의 지역 스코프 - 없음outer_function()의 둘러싼 스코프 - 발견! 이 값을 사용
LEGB 동작 예: 간단한 예제
네 가지 스코프가 함께 동작하는 모습을 명확하고 간단한 예제로 보겠습니다:
# Built-in: len (Python이 제공함)
# Global: multiplier
multiplier = 10
def outer(x):
# inner에 대한 둘러싼 스코프
y = 5
def inner(z):
# 지역 스코프
# z는 지역(L)
# y는 둘러싼 스코프(E)
# multiplier는 전역 스코프(G)
# len은 내장 스코프(B)
result = len([z, y, multiplier]) # 네 가지 스코프를 모두 사용!
return z + y + multiplier
return inner(3)
answer = outer(100)
print(answer) # Output: 18Python이 inner() 안에서 z + y + multiplier를 평가할 때:
- L (Local):
z = 3을 찾음 - E (Enclosing):
outer()에서y = 5를 찾음 - G (Global):
multiplier = 10을 찾음 - B (Built-in):
len함수를 찾음
이 예제는 Python이 이름을 해석하기 위해 네 가지 스코프를 어떻게 검색하는지 명확히 보여줍니다.
섀도잉(Shadowing): 안쪽 스코프가 바깥쪽 이름을 가릴 때
같은 이름이 여러 스코프에 존재하면, 가장 안쪽 스코프가 “이깁니다”—이를 섀도잉(shadowing)이라고 합니다:
value = "global"
def outer():
value = "enclosing"
def inner():
value = "local"
print(value) # 어떤 value일까요?
inner()
print(value) # 어떤 value일까요?
outer()
print(value) # 어떤 value일까요?Output:
local
enclosing
global각 print() 문이 다른 value를 보는 이유는 Python이 첫 번째 일치에서 멈추기 때문입니다:
inner()내부:value를 지역에서 찾음 → "local" 출력inner()밖이지만outer()내부:outer()스코프에서value를 찾음 → "enclosing" 출력- 모듈 수준: 전역에서
value를 찾음 → "global" 출력
LEGB 검색 순서를 시각화하기
이 다이어그램은 Python의 검색 과정을 보여줍니다. Python은 가장 안쪽 스코프에서 시작해 바깥으로 나아갑니다. 어떤 스코프에서도 이름을 찾지 못하면 Python은 NameError를 발생시킵니다.
함수 작성에서 LEGB가 중요한 이유
LEGB를 이해하면 다음을 할 수 있습니다:
- 변수 값을 예측: Python이 정확히 어떤 변수를 사용할지 알 수 있습니다
- 이름 충돌 방지: 이름이 언제 서로를 가리는지 이해합니다
- 더 나은 함수 설계: 각 변수에 어떤 스코프가 적절한지 결정할 수 있습니다
- 스코프 문제 디버깅: 변수가 기대한 값을 갖지 않을 때 LEGB를 따라 추적할 수 있습니다
LEGB 규칙은 Python이 이름을 해석하는 방식의 핵심입니다. 변수를 사용할 때마다 Python은 뒤에서 이 규칙을 따르고 있습니다.
21.3) global 키워드를 신중하게 사용하기
함수가 전역 변수를 읽을 수 있다는 것을 보았습니다. 하지만 함수 안에서 전역 변수를 수정해야 한다면 어떻게 할까요? 그럴 때 global 키워드가 필요합니다—하지만 이는 절제해서 신중하게 사용해야 합니다.
문제: 대입은 지역 변수를 만든다
앞에서 배웠듯이, 함수 안에서 변수에 대입하면 지역 변수가 생성됩니다:
counter = 0 # 전역 변수
def increment():
# 경고: 이는 counter라는 NEW 지역 변수를 만듭니다 - 시연용
# 문제: 지역에 대입하기 전에 counter를 읽으려고 함
counter = counter + 1 # UnboundLocalError!
# increment() # UnboundLocalError 발생 유발이는 Python이 대입을 보고 counter를 함수 전체에서 지역으로 취급하기 때문에 실패합니다. 그런데 우리는 지역에 대입하기 전에 counter를 읽으려고 하고 있습니다.
이는 전역 변수를 다룰 때 가장 흔한 오류 중 하나입니다. 오류 메시지 UnboundLocalError: local variable 'counter' referenced before assignment는 무슨 일이 일어났는지 정확히 말해줍니다. Python이 counter를 지역으로 결정했는데(대입 때문에), 값을 주기 전에 사용하려고 했다는 뜻입니다.
해결: 변수를 전역으로 선언하기
global 키워드는 Python에 이렇게 말합니다: “이 이름으로 새 지역 변수를 만들지 마. 대신 전역 변수를 사용해.”
counter = 0 # 전역 변수
def increment():
global counter # Python에게 전역 counter를 사용하라고 알림
counter = counter + 1 # 이제 전역 변수를 수정함
print(f"Before: {counter}") # Output: Before: 0
increment()
print(f"After: {counter}") # Output: After: 1
increment()
print(f"After again: {counter}") # Output: After again: 2global counter 선언은 변수를 사용하기 전에 나와야 합니다. 이 선언은 이 함수에서 counter에 대한 모든 대입이 지역 변수를 만들지 않고 전역 변수를 수정하도록 Python에 알려줍니다.
여러 전역 변수
한 문장에서 여러 변수를 global로 선언할 수 있습니다:
total_sales = 0
total_customers = 0
def record_sale(amount):
global total_sales, total_customers
total_sales += amount
total_customers += 1
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $0, Customers: 0
record_sale(25.50)
record_sale(30.00)
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $55.5, Customers: 2total_sales와 total_customers 모두 전역으로 선언되었으므로, 함수가 둘 다 수정할 수 있습니다.
global을 써야 할 때: 공유 상태
global 키워드는 공유 상태(shared state)—여러 함수가 접근하고 수정해야 하는 데이터—를 유지해야 할 때 적절합니다:
# 게임 상태
player_score = 0
player_lives = 3
game_over = False
def award_points(points):
global player_score
player_score += points
print(f"Score: {player_score}")
def lose_life():
global player_lives, game_over
player_lives -= 1
print(f"Lives remaining: {player_lives}")
if player_lives <= 0:
game_over = True
print("Game Over!")
def check_game_status():
# 전역을 읽기만 함 - global 키워드 필요 없음
if game_over:
return "Game Over"
else:
return f"Playing - Score: {player_score}, Lives: {player_lives}"
# 게임 플레이
award_points(100) # Output: Score: 100
award_points(50) # Output: Score: 150
lose_life() # Output: Lives remaining: 2
print(check_game_status()) # Output: Playing - Score: 150, Lives: 2이 예제는 global을 적절하게 사용하는 모습을 보여줍니다. 여러 함수가 공유 게임 상태를 수정해야 합니다. 하지만 check_game_status()는 변수들을 읽기만 하므로 global이 필요 없다는 점을 주목하세요.
global을 신중하게 써야 하는 이유
global은 때때로 필요하지만, 과도하게 사용하면 코드를 이해하고 유지보수하기 어려워질 수 있습니다. 이유는 다음과 같습니다:
문제 1: 숨겨진 의존성
함수가 전역 변수를 수정하면, 함수 호출만 보고는 무엇이 바뀌는지 분명하지 않습니다:
total = 0
def add_to_total(value):
global total
total += value
# 이 함수는 무엇을 할까요? 코드를 읽지 않으면 알 수 없습니다
add_to_total(10)값을 반환하는 함수와 비교해 보세요:
def add_to_total(current_total, value):
return current_total + value
total = 0
total = add_to_total(total, 10) # 명확함: total이 업데이트되고 있음두 번째 버전은 total이 수정된다는 사실을 명시적으로 보여줍니다.
문제 2: 테스트가 더 어려워짐
전역 상태를 수정하는 함수는 전역 변수를 설정하고 리셋해야 하므로 테스트하기 더 어렵습니다:
# 테스트하기 어려움 - 전역 상태에 의존함
score = 0
def add_score(points):
global score
score += points
# 각 테스트는 score를 리셋해야 함
# Test 1
score = 0
add_score(10)
assert score == 10
# Test 2 - score를 다시 리셋해야 함
score = 0
add_score(20)
assert score == 20문제 3: 함수 재사용성이 떨어짐
특정 전역 변수에 의존하는 함수는 다른 프로그램에서 쉽게 재사용할 수 없습니다:
# 이 함수는 'inventory'라는 전역 변수가 있을 때만 동작함
inventory = []
def add_item(item):
global inventory
inventory.append(item)global의 더 나은 대안
많은 경우, 반환값과 매개변수를 사용하면 global을 피할 수 있습니다:
전역 상태를 수정하는 대신:
# global 사용(덜 이상적)
balance = 1000
def withdraw(amount):
global balance
if amount <= balance:
balance -= amount
return True
return False
withdraw(100)
print(balance) # Output: 900반환값 사용:
# 반환값 사용(더 나음)
def withdraw(balance, amount):
if amount <= balance:
return balance - amount, True
return balance, False
balance = 1000
balance, success = withdraw(balance, 100)
print(balance) # Output: 900두 번째 버전이 더 유연하고, 테스트하기 쉽고, 재사용도 쉽습니다.
global이 실제로 적절한 경우
global에는 정당한 사용 사례가 있습니다:
- 정말로 전역이어야 하는 설정:
# 애플리케이션 전역 설정
debug_mode = False
log_level = "INFO"
def enable_debug():
global debug_mode, log_level
debug_mode = True
log_level = "DEBUG"- 디버깅 또는 통계를 위한 카운터:
# 디버깅을 위해 함수 호출 횟수 추적
_function_call_count = 0
def tracked_function():
global _function_call_count
_function_call_count += 1
# ... 나머지 함수global에 대한 핵심 요약
global은 모듈 수준 상태를 정말로 수정해야 할 때만 사용하세요- 대신 반환값과 매개변수 사용을 우선하세요
global을 사용한다면 왜 필요한지 문서화하세요global을 피할 수 있도록 설계를 개선할 수 있는지 고려하세요- 기억하세요: 전역 변수를 읽는 데는
global키워드가 필요 없고, 수정할 때만 필요합니다
21.4) 둘러싼 함수의 변수를 수정하기 위해 nonlocal 사용하기
중첩 함수를 사용할 때, 둘러싼(enclosing) 함수 스코프의 변수를 수정해야 할 수도 있습니다. nonlocal 키워드는 이 목적을 위해 존재합니다—전역 스코프 대신 둘러싼 함수 스코프를 대상으로 하는 global과 비슷한 역할입니다.
문제: 둘러싼 변수 수정하기
기본적으로 대입이 지역 변수를 만드는 것처럼, 둘러싼 스코프에서도 같은 문제가 발생합니다:
def outer():
count = 0 # outer의 스코프에 있는 변수
def inner():
# 경고: 이는 count라는 NEW 지역 변수를 만듭니다 - 시연용
# 문제: 지역에 대입하기 전에 count를 읽으려고 함
count = count + 1 # UnboundLocalError!
print(count)
inner()
# outer() # UnboundLocalError 발생 유발Python은 inner() 안에서 count에 대입하는 것을 보고 이를 지역 변수로 취급합니다. 하지만 지역에 대입하기 전에 읽으려고 해서 오류가 발생합니다.
해결: nonlocal 키워드
nonlocal 키워드는 Python에 이렇게 말합니다: “이 변수는 지역이 아니야—둘러싼 함수 스코프에서 찾아서 그 변수를 사용해.”
def outer():
count = 0 # outer의 스코프에 있는 변수
def inner():
nonlocal count # outer의 스코프에 있는 count 사용
count = count + 1
print(f"Count in inner: {count}")
print(f"Count before: {count}") # Output: Count before: 0
inner() # Output: Count in inner: 1
print(f"Count after: {count}") # Output: Count after: 1
outer()이제 inner()는 outer() 스코프의 count 변수를 수정할 수 있습니다. 둘러싼 스코프의 실제 변수를 수정하고 있기 때문에, inner()가 반환된 뒤에도 변경이 유지됩니다.
nonlocal이 유용한 이유: 상태를 기억하는 함수
nonlocal 키워드는 내부 함수가 둘러싼 스코프의 상태를 유지하고 수정할 수 있게 하는 강력한 패턴을 가능하게 합니다. 23장에서 클로저(closure)와 팩토리 함수(factory functions)를 자세히 배우겠지만, 지금은 nonlocal이 내부 함수가 둘러싼 스코프의 변수를 수정할 수 있게 한다는 점을 이해하세요.
다음은 nonlocal이 어떻게 동작하는지 보여주는 간단한 예제입니다:
def create_counter():
count = 0 # 이 변수는 increment에 대한 enclosing 스코프에 있음
def increment():
nonlocal count # enclosing 스코프의 count를 수정
count += 1
return count
return increment # 내부 함수를 반환
# 카운터 생성
counter1 = create_counter()
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
# 또 다른 독립적인 카운터 생성
counter2 = create_counter()
print(counter2()) # Output: 1
print(counter2()) # Output: 2create_counter()를 호출할 때마다 새로운 count 변수와, nonlocal을 사용해 그 특정 count를 수정할 수 있는 새로운 increment() 함수가 만들어집니다.
nonlocal vs global
차이를 이해하는 것이 중요합니다:
x = "global"
def outer():
x = "enclosing"
def use_global():
global x # 전역 x를 참조함
print(f"use_global sees: {x}") # Output: use_global sees: global
def use_nonlocal():
nonlocal x # outer의 x를 참조함
print(f"use_nonlocal sees: {x}") # Output: use_nonlocal sees: enclosing
use_global()
use_nonlocal()
outer()global은 항상 모듈 수준 스코프를 참조합니다nonlocal은 가장 가까운 둘러싼 함수 스코프를 참조합니다
nonlocal을 사용할 수 없는 경우
nonlocal 키워드는 둘러싼 함수 스코프에서만 동작합니다. 다음에는 사용할 수 없습니다:
- 전역 스코프(대신
global사용):
x = "global"
def func():
nonlocal x # SyntaxError: no binding for nonlocal 'x' found
x = "modified"- 어떤 둘러싼 스코프에도 존재하지 않는 변수:
def outer():
def inner():
nonlocal count # SyntaxError: no binding for nonlocal 'count' foundnonlocal에 대한 핵심 요약
nonlocal은 둘러싼 함수 스코프의 변수를 수정할 때 사용합니다nonlocal은 전역 스코프가 아니라 둘러싼 함수 스코프를 검색합니다- 둘러싼 변수를 읽는 데는
nonlocal이 필요 없고, 수정할 때만 필요합니다 nonlocal은 비공개 상태를 가진 함수를 만드는 강력한 패턴을 가능하게 합니다- 23장에서 클로저와 팩토리 함수를 더 배웁니다
nonlocal 키워드는 우리가 본 카운터, 계좌, 이벤트 트래커 예제처럼 비공개 상태를 유지하는 함수를 만들 때 특히 유용합니다.
21.5) del로 이름(객체가 아님)을 삭제하기와 그 의미
때로는 프로그램의 네임스페이스에서 변수를 제거해야 할 수 있습니다. 예를 들어, 장시간 실행되는 프로그램에서 메모리를 확보하거나, 임시 변수를 정리하거나, 컬렉션에서 항목을 제거할 때입니다. Python의 del 문이 이런 작업을 처리하지만, 정확히 무엇을 하고 무엇을 하지 않는지 이해하는 것이 중요합니다.
Python의 del 문은 종종 오해됩니다. 이는 객체를 삭제하지 않습니다—이름(names)(변수 바인딩)을 삭제합니다. 이 구분을 이해하는 것은 Python이 메모리와 참조를 어떻게 관리하는지 이해하는 데 매우 중요합니다.
del이 실제로 하는 일
del 문은 현재 스코프에서 이름을 제거합니다:
x = 42
print(x) # Output: 42
del x
# print(x) # NameError: name 'x' is not defineddel x 이후에는 현재 스코프에서 x라는 이름이 더 이상 존재하지 않습니다. 이를 사용하려 하면, 이름이 더 이상 정의되어 있지 않기 때문에 Python은 NameError를 발생시킵니다.
이름 삭제 vs 객체 삭제
여기서 핵심 통찰은 다음과 같습니다: del은 이름을 제거할 뿐, 그 이름이 가리키는 객체를 반드시 삭제하는 것은 아닙니다:
# 리스트 하나와 이를 참조하는 두 이름을 생성
original = [1, 2, 3]
reference = original # 두 이름 모두 같은 리스트를 참조함
print(original) # Output: [1, 2, 3]
print(reference) # Output: [1, 2, 3]
# 이름 하나 삭제
del original
# 'reference'가 여전히 참조하고 있으므로 리스트는 여전히 존재함
print(reference) # Output: [1, 2, 3]
# print(original) # NameError: name 'original' is not defined리스트 [1, 2, 3]는 reference가 여전히 이를 참조하므로 계속 존재합니다. original을 삭제한 것은 그 이름만 제거한 것이지, 리스트 객체 자체를 삭제한 것이 아닙니다.
객체가 실제로 삭제되는 시점
Python은 어떤 객체가 더 이상 어떤 이름에서도 참조되지 않을 때 자동으로 객체를 삭제합니다. 이를 가비지 컬렉션(garbage collection)이라고 합니다:
data = [1, 2, 3] # 리스트가 생성되고 'data'가 이를 참조함
del data # 'data' 이름이 삭제됨
# 이제 리스트는 참조가 없으므로 Python은 결국 이를 삭제할 것입니다
# (이는 자동으로 발생합니다 - 아무것도 할 필요가 없음)data를 삭제하면 리스트 [1, 2, 3]는 남은 참조가 없으므로, Python의 가비지 컬렉터가 결국 메모리를 회수합니다. 하지만 이는 자동으로 일어나며—언제 일어날지는 제어할 수 없습니다.
컬렉션에서 항목 삭제하기
del 문은 컬렉션에서 항목을 제거할 수도 있지만, 이는 근본적으로 이름을 삭제하는 것과 다릅니다. 컬렉션 인덱싱이나 슬라이싱과 함께 del을 사용하면, 이름을 삭제하는 것이 아니라 컬렉션 자체를 수정하는 것입니다.
이 점이 중요한 구분입니다. del numbers[2]라고 쓰면, 리스트 객체의 특수 메서드를 호출해 요소를 제거하는 것입니다. numbers라는 이름은 여전히 존재하고, 여전히 같은 리스트 객체를 참조합니다—단지 리스트의 요소가 줄어들었을 뿐입니다.
# 인덱스로 리스트 요소 삭제
numbers = [10, 20, 30, 40, 50]
del numbers[2] # 인덱스 2의 요소 제거
print(numbers) # Output: [10, 20, 40, 50]
# 리스트 슬라이스 삭제
numbers = [10, 20, 30, 40, 50]
del numbers[1:3] # 인덱스 1부터 3(미포함)까지 요소 제거
print(numbers) # Output: [10, 40, 50]
# 딕셔너리 항목 삭제
person = {'name': 'Alice', 'age': 30, 'city': 'Boston'}
del person['age']
print(person) # Output: {'name': 'Alice', 'city': 'Boston'}