[Python 100일 챌린지] Day 29 - 스코프와 전역 변수
변수는 어디서 어디까지 사용할 수 있을까? 스코프와 전역 변수의 모든 것을 배워봅시다! (30분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 24: 함수 정의하기 (def) - 함수의 기본 개념
- Day 25: 함수 매개변수와 반환값 - 함수 내부 변수
🎯 학습 목표 1: 변수 스코프의 개념 이해하기
한 줄 설명
스코프(Scope) = 변수가 유효한 범위, 변수를 사용할 수 있는 영역
변수마다 사용할 수 있는 범위가 정해져 있습니다.
실생활 비유
1
2
3
4
5
6
7
8
🏢 회사 출입증:
전역 변수 = 올 출입증
- 모든 층, 모든 방 출입 가능
지역 변수 = 부서별 출입증
- 해당 부서 방만 출입 가능
- 다른 부서에서는 사용 불가
🎯 학습 목표 2: 지역 변수와 전역 변수 구분하기
기본 개념
1
2
3
4
5
6
7
def my_function():
x = 10 # 지역 변수 (함수 안에서만 유효)
print(f"함수 안: {x}")
my_function() # 함수 안: 10
# print(x) # ❌ NameError: 함수 밖에서는 x를 사용할 수 없음!
지역 변수의 특징
1
2
3
4
5
6
7
8
9
10
11
12
def greet():
message = "안녕하세요!" # greet 함수의 지역 변수
print(message)
def farewell():
message = "안녕히 가세요!" # farewell 함수의 지역 변수
print(message)
greet() # 안녕하세요!
farewell() # 안녕히 가세요!
# 각 함수의 message는 서로 다른 변수!
매개변수도 지역 변수
1
2
3
4
5
6
7
8
9
def calculate(a, b): # a, b는 지역 변수
result = a + b # result도 지역 변수
return result
print(calculate(10, 20)) # 30
# print(a) # ❌ NameError
# print(b) # ❌ NameError
# print(result) # ❌ NameError
기본 개념
1
2
3
4
5
6
7
x = 100 # 전역 변수 (어디서든 접근 가능)
def show_x():
print(f"함수 안: {x}")
show_x() # 함수 안: 100
print(f"함수 밖: {x}") # 함수 밖: 100
전역 변수 읽기 vs 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
count = 0 # 전역 변수
def show_count():
print(count) # ✅ 읽기는 OK
def increment_wrong():
count = count + 1 # ❌ UnboundLocalError!
# Python은 count를 지역 변수로 간주
def increment_right():
global count # ✅ 전역 변수 사용 선언
count = count + 1
show_count() # 0
increment_right()
show_count() # 1
같은 이름의 변수
1
2
3
4
5
6
7
8
x = 100 # 전역 변수
def my_function():
x = 10 # 지역 변수 (전역 변수와 이름만 같을 뿐, 다른 변수!)
print(f"함수 안: {x}")
my_function() # 함수 안: 10 (지역 변수 사용)
print(f"함수 밖: {x}") # 함수 밖: 100 (전역 변수 사용)
전역 변수 가려지기 (Shadowing)
1
2
3
4
5
6
7
8
name = "전역" # 전역 변수
def show_name():
name = "지역" # 전역 변수를 가리는 지역 변수
print(name)
show_name() # 지역
print(name) # 전역
🎯 학습 목표 3: global과 nonlocal 키워드 사용하기
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
counter = 0 # 전역 변수
def increment():
global counter # "나는 전역 변수 counter를 사용할 거야!"
counter += 1
def decrement():
global counter
counter -= 1
print(counter) # 0
increment()
print(counter) # 1
increment()
print(counter) # 2
decrement()
print(counter) # 1
여러 전역 변수 선언
1
2
3
4
5
6
7
8
9
10
x = 0
y = 0
def update_both():
global x, y # 여러 변수를 한 번에
x = 10
y = 20
update_both()
print(x, y) # 10 20
global 없이 수정 시도
1
2
3
4
5
6
7
8
9
10
11
12
score = 0
def add_score(points):
# ❌ global 없이 수정하면 에러!
# score = score + points # UnboundLocalError
# ✅ 올바른 방법
global score
score = score + points
add_score(10)
print(score) # 10
nonlocal 키워드
중첩 함수에서의 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def outer():
count = 0 # outer 함수의 지역 변수
def inner():
nonlocal count # "바깥 함수의 count를 사용할 거야!"
count += 1
print(f"inner: {count}")
inner()
inner()
print(f"outer: {count}")
outer()
# inner: 1
# inner: 2
# outer: 2
global vs nonlocal 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
x = 100 # 전역 변수
def outer():
x = 10 # outer의 지역 변수
def inner_with_nonlocal():
nonlocal x # outer의 x 수정
x = 1
def inner_with_global():
global x # 전역 x 수정
x = 1
print(f"outer 시작: x = {x}") # 10
inner_with_nonlocal()
print(f"nonlocal 후: x = {x}") # 1
outer()
print(f"전역: x = {x}") # 100 (영향 없음)
# global 테스트
def outer2():
x = 10
def inner_with_global():
global x
x = 1
print(f"outer2 시작: x = {x}") # 10
inner_with_global()
print(f"global 후: x = {x}") # 10 (영향 없음)
outer2()
print(f"전역: x = {x}") # 1 (변경됨!)
🎯 학습 목표 4: 클로저와 스코프 활용하기
클로저란?
클로저(Closure) = 함수가 자신이 정의된 환경의 변수를 기억하는 것
중첩 함수가 외부 함수의 변수를 기억하고 사용할 수 있는 강력한 패턴입니다.
기본 예제
1
2
3
4
5
6
7
8
9
10
def outer():
message = "Hello" # Enclosing 변수
def inner():
print(message) # 외부 함수의 변수 접근 가능!
return inner
my_func = outer()
my_func() # Hello - outer()가 끝났는데도 message 기억!
클로저 카운터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def create_counter():
"""카운터 생성기 (클로저 패턴)"""
count = 0 # Enclosing 변수
def increment():
nonlocal count
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 = create_counter()
print(inc()) # 1
print(inc()) # 2
print(inc()) # 3
print(dec()) # 2
print(get()) # 2
클로저를 사용하는 이유
- 데이터 은닉: 외부에서 직접 접근할 수 없는 private 변수 생성
- 상태 유지: 함수 호출 간 상태 보존
- 팩토리 패턴: 설정이 다른 여러 함수 생성
1
2
3
4
5
6
7
8
9
10
11
def make_multiplier(n):
"""n을 곱하는 함수를 생성"""
def multiply(x):
return x * n
return multiply
times3 = make_multiplier(3)
times5 = make_multiplier(5)
print(times3(10)) # 30
print(times5(10)) # 50
🎯 학습 목표 5: LEGB 규칙과 스코프 체인 이해하기
변수 검색 순서 (LEGB)
Python이 변수를 찾는 순서:
- Local (지역): 현재 함수 내부
- Enclosing (둘러싼): 중첩 함수의 바깥 함수
- Global (전역): 모듈 전체
- Built-in (내장): Python 기본 함수/변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Built-in
len, print, max 등
# Global
x = "global"
def outer():
# Enclosing
x = "enclosing"
def inner():
# Local
x = "local"
print(x) # "local" 출력
inner()
print(x) # "enclosing" 출력
outer()
print(x) # "global" 출력
LEGB 실전 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
x = "Global X"
def outer():
x = "Enclosing X"
def inner():
x = "Local X"
print(f"1. {x}") # Local X
def inner2():
print(f"2. {x}") # Enclosing X
inner()
inner2()
print(f"3. {x}") # Enclosing X
outer()
print(f"4. {x}") # Global X
Built-in 스코프 주의사항
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Built-in 함수 max
print(max([1, 2, 3, 4, 5])) # 5
def my_function():
# ❌ max를 변수로 덮어쓰면 Built-in 함수 사용 불가!
max = 100
print(max) # 100
# print(max([1, 2, 3])) # TypeError: 'int' object is not callable
my_function()
# 함수 밖에서는 여전히 사용 가능
print(max([1, 2, 3, 4, 5])) # 5
스코프 체인 시각화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Built-in: len, print, max, min, sum...
count = 0 # Global
def outer():
total = 0 # Enclosing
def inner():
result = 0 # Local
# 검색 순서:
# 1. Local (result 찾음)
# 2. Enclosing (total 찾음)
# 3. Global (count 찾음)
# 4. Built-in (print 찾음)
print(f"{result}, {total}, {count}")
inner()
outer()
💡 실전 예제
예제 1: 게임 점수 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
score = 0
high_score = 0
def add_points(points):
"""점수 추가"""
global score, high_score
score += points
if score > high_score:
high_score = score
print(f"현재 점수: {score}, 최고 점수: {high_score}")
def reset_score():
"""점수 초기화"""
global score
score = 0
print("점수가 초기화되었습니다.")
# 게임 플레이
add_points(10) # 현재 점수: 10, 최고 점수: 10
add_points(20) # 현재 점수: 30, 최고 점수: 30
reset_score() # 점수가 초기화되었습니다.
add_points(15) # 현재 점수: 15, 최고 점수: 30
예제 2: 설정 관리자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
config = {
"theme": "light",
"language": "ko"
}
def get_config(key):
"""설정 값 조회"""
return config.get(key, "설정 없음")
def set_config(key, value):
"""설정 값 변경"""
global config
config[key] = value
print(f"{key} = {value}로 설정되었습니다.")
def reset_config():
"""설정 초기화"""
global config
config = {
"theme": "light",
"language": "ko"
}
print("설정이 초기화되었습니다.")
# 사용
print(get_config("theme")) # light
set_config("theme", "dark") # theme = dark로 설정되었습니다.
print(get_config("theme")) # dark
💡 실전 팁 & 주의사항
1. 전역 변수 최소화 원칙 (Best Practice)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# ❌ 나쁜 예: 전역 변수에 과도하게 의존
user_name = ""
user_age = 0
user_email = ""
def update_user(name, age, email):
global user_name, user_age, user_email
user_name = name
user_age = age
user_email = email
# ✅ 좋은 예 1: 반환값 활용
def create_user(name, age, email):
return {
"name": name,
"age": age,
"email": email
}
user = create_user("홍길동", 25, "hong@example.com")
# ✅ 좋은 예 2: 클래스 사용 (더 좋은 방법)
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
user = User("홍길동", 25, "hong@example.com")
이유: 전역 변수는 디버깅을 어렵게 만들고, 여러 함수에서 동시에 수정하면 예측하기 어려운 버그가 발생합니다.
2. 상수는 대문자로 (Convention)
1
2
3
4
5
6
7
8
9
# ✅ 상수는 대문자 + 언더스코어
MAX_RETRY_COUNT = 3
API_TIMEOUT = 30
DEFAULT_PORT = 8080
def connect(port=DEFAULT_PORT):
print(f"포트 {port}로 연결 중...")
connect() # 포트 8080로 연결 중...
이유: 대문자로 작성하면 “이 변수는 수정하지 말아야 한다”는 의도를 명확히 전달할 수 있습니다 (Python은 상수 키워드가 없음).
3. 가변 객체(list, dict) vs 불변 객체(int, str)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 리스트는 global 없이도 수정 가능
my_list = [1, 2, 3]
def append_item(item):
my_list.append(item) # ✅ 수정 OK
append_item(4)
print(my_list) # [1, 2, 3, 4]
# 하지만 재할당은 global 필요
def replace_list():
global my_list # ✅ 필수!
my_list = [5, 6, 7]
replace_list()
print(my_list) # [5, 6, 7]
# 숫자/문자열은 불변 객체라서 항상 global 필요
count = 0
def increment():
global count # ✅ 필수!
count += 1
increment()
print(count) # 1
핵심: 리스트의 메서드(append, extend 등) 는 global 없이 호출 가능하지만, 재할당(=) 은 global이 필요합니다.
4. nonlocal은 중첩 함수에서만
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ✅ nonlocal은 중첩 함수(nested function)에서만 의미 있음
def outer():
count = 0
def increment():
nonlocal count
count += 1
return count
print(increment()) # 1
print(increment()) # 2
outer()
# ❌ 최상위 레벨에서는 사용 불가
# count = 0
# nonlocal count # SyntaxError
이유: nonlocal은 “바로 바깥 함수의 변수”를 참조하는 키워드입니다. 전역 변수는 global을 사용합니다.
5. 함수 매개변수로 전달 (가장 권장)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ 전역 변수 사용
total = 0
def add_to_total(value):
global total
total += value
add_to_total(10)
add_to_total(20)
print(total) # 30
# ✅ 매개변수와 반환값 사용 (더 깔끔!)
def add(current, value):
return current + value
total = 0
total = add(total, 10)
total = add(total, 20)
print(total) # 30
이유: 함수가 독립적이고 테스트하기 쉬우며, 부작용(side effect)이 없어 안전합니다.
6. 디버깅 팁: locals()와 globals()
1
2
3
4
5
6
7
8
9
10
11
12
x = 100 # 전역 변수
def my_function():
y = 200 # 지역 변수
print("지역 변수:")
print(locals()) # {'y': 200}
print("\n전역 변수:")
print(globals()) # {..., 'x': 100, ...}
my_function()
활용: 스코프 문제가 발생하면 locals()와 globals()로 현재 사용 가능한 변수를 확인할 수 있습니다.
7. UnboundLocalError 주의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
count = 10 # 전역 변수
def increment():
# ❌ 이 코드는 에러!
# count = count + 1 # UnboundLocalError
# Python은 함수 내에서 count에 값을 할당하는 것을 보고
# count를 지역 변수로 간주합니다.
# 하지만 count + 1을 계산할 때는 아직 지역 변수 count가 정의되지 않았습니다!
# ✅ 해결책 1: global 사용
global count
count = count + 1
# ✅ 해결책 2: 새로운 변수 사용
def increment2():
new_count = count + 1
return new_count
핵심: Python은 함수 전체를 스캔해서 변수 할당이 있으면 지역 변수로 간주합니다.
8. 클로저로 private 변수 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def create_bank_account(initial_balance):
"""클로저를 사용한 은행 계좌 시뮬레이션"""
balance = initial_balance # private 변수
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if balance >= amount:
balance -= amount
return balance
else:
return "잔액 부족"
def get_balance():
return balance
# 딕셔너리로 반환
return {
"deposit": deposit,
"withdraw": withdraw,
"get_balance": get_balance
}
# 사용
account = create_bank_account(1000)
print(account["get_balance"]()) # 1000
print(account["deposit"](500)) # 1500
print(account["withdraw"](200)) # 1300
장점: balance 변수에 직접 접근할 수 없어 데이터 무결성을 보장합니다.
9. Built-in 함수 이름 덮어쓰기 금지
1
2
3
4
5
6
7
8
9
10
# ❌ 나쁜 예
max = 10 # Built-in 함수 max를 변수로 사용
# print(max([1, 2, 3])) # TypeError: 'int' object is not callable
# ✅ 좋은 예
maximum = 10
print(max([1, 2, 3])) # 3
# 흔히 덮어쓰는 Built-in 함수들
# list, dict, str, int, float, max, min, sum, len, id, type, input, print
이유: Built-in 함수를 변수로 사용하면 해당 함수를 더 이상 사용할 수 없습니다.
10. 반복문 변수는 함수 스코프를 따름
1
2
3
4
5
6
7
8
9
10
11
12
13
# Python의 for 루프 변수는 함수 스코프!
def process_numbers():
for i in range(3):
print(i)
# ✅ 루프가 끝나도 i는 여전히 접근 가능
print(f"마지막 i 값: {i}") # 2
process_numbers()
# 0
# 1
# 2
# 마지막 i 값: 2
참고: 일부 언어(C, Java)는 블록 스코프를 사용하지만, Python은 함수 스코프만 있습니다.
11. 람다 함수도 클로저를 만들 수 있음
1
2
3
4
5
6
7
8
9
def make_multiplier(n):
# 람다 함수가 n을 기억 (클로저)
return lambda x: x * n
times2 = make_multiplier(2)
times5 = make_multiplier(5)
print(times2(10)) # 20
print(times5(10)) # 50
활용: 간단한 팩토리 함수를 만들 때 유용합니다.
12. global 키워드는 함수 최상단에 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
count = 0
def increment():
# ✅ 함수 시작 부분에 선언
global count
# 여러 줄의 코드...
count += 1
count *= 2
return count
# ❌ 중간에 선언하면 가독성 떨어짐
def bad_example():
print("처리 중...")
global count # 너무 아래에 있어서 발견하기 어려움
count += 1
이유: 함수 시작 부분에 선언하면 전역 변수 사용 여부를 한눈에 파악할 수 있습니다.
13. LEGB 순서를 활용한 Built-in 함수 복구
1
2
3
4
5
6
7
8
9
10
11
# Built-in 함수 재정의 (신중하게 사용)
def custom_max(*args):
"""max 함수의 커스텀 버전"""
import builtins # Built-in 스코프 접근
# Built-in max 사용
result = builtins.max(*args)
print(f"최댓값: {result}")
return result
custom_max(1, 5, 3, 9, 2) # 최댓값: 9
활용: Built-in 함수를 변수로 덮어썼을 때 builtins 모듈로 원래 함수에 접근할 수 있습니다.
14. 중첩 함수의 장점: 헬퍼 함수 캡슐화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def process_data(data):
"""데이터 처리 함수"""
# 내부 헬퍼 함수 (외부에서 접근 불가)
def validate(item):
return isinstance(item, int) and item > 0
def transform(item):
return item * 2
# 메인 로직
valid_data = [x for x in data if validate(x)]
result = [transform(x) for x in valid_data]
return result
print(process_data([1, -2, 3, "4", 5])) # [2, 6, 10]
# ❌ validate, transform 함수는 외부에서 접근 불가
# validate(10) # NameError
장점: 관련 있는 헬퍼 함수를 캡슐화하여 네임스페이스 오염을 방지합니다.
15. 전역 변수가 필요한 경우: 설정과 상태
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# ✅ 전역 변수를 사용해도 괜찮은 경우
# 1. 애플리케이션 설정 (변경되지 않음)
DEBUG_MODE = True
DATABASE_URL = "postgresql://localhost/mydb"
MAX_CONNECTIONS = 100
# 2. 애플리케이션 상태 (신중하게)
_connection_pool = None # private 변수 (앞에 _ 붙임)
def get_connection():
global _connection_pool
if _connection_pool is None:
_connection_pool = "Connection Pool Created"
return _connection_pool
# 3. 캐시 (성능 최적화)
_cache = {}
def expensive_function(n):
if n in _cache:
return _cache[n]
# 비용이 큰 계산
result = sum(range(n))
_cache[n] = result
return result
가이드라인:
- 설정 값은 대문자 상수로
- 상태 변수는
_로 시작 (private 관례) - 클래스나 모듈로 캡슐화하는 것이 더 좋음
🔧 트러블슈팅
문제 1: UnboundLocalError
증상:
1
2
3
4
count = 0
def increment():
count = count + 1 # UnboundLocalError!
원인: count = ...가 있으면 Python은 count를 지역 변수로 간주
해결:
1
2
3
4
5
count = 0
def increment():
global count # 전역 변수 선언
count = count + 1
문제 2: 전역 변수 읽기는 되는데 쓰기가 안 됨
증상:
1
2
3
4
5
6
7
8
9
10
11
value = 100
def show():
print(value) # ✅ 읽기 OK
def modify():
value = 200 # ✅ 문법 에러는 없지만, 전역 변수가 안 바뀜!
show() # 100
modify()
show() # 여전히 100
해결:
1
2
3
4
5
6
def modify():
global value # 전역 변수 선언
value = 200
modify()
show() # 200
문제 3: nonlocal 잘못 사용
증상:
1
2
3
4
def outer():
def inner():
nonlocal x # SyntaxError: no binding for nonlocal 'x' found
x = 10
원인: nonlocal은 이미 존재하는 Enclosing 변수에만 사용 가능
해결:
1
2
3
4
5
6
7
8
9
10
11
def outer():
x = 0 # 먼저 변수 선언
def inner():
nonlocal x
x = 10
inner()
print(x) # 10
outer()
🧪 연습 문제
문제 1: 은행 계좌 시스템
다음 요구사항을 만족하는 은행 계좌 시스템을 작성하세요.
- 잔액(balance)을 전역 변수로 관리
- deposit(금액): 입금
- withdraw(금액): 출금 (잔액 부족 시 에러 메시지)
- get_balance(): 잔액 조회
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
balance = 10000 # 초기 잔액
def deposit(amount):
"""입금"""
global balance
balance += amount
print(f"{amount:,}원 입금 완료. 잔액: {balance:,}원")
def withdraw(amount):
"""출금"""
global balance
if amount > balance:
print(f"잔액 부족! 현재 잔액: {balance:,}원")
else:
balance -= amount
print(f"{amount:,}원 출금 완료. 잔액: {balance:,}원")
def get_balance():
"""잔액 조회"""
print(f"현재 잔액: {balance:,}원")
return balance
# 테스트
get_balance() # 현재 잔액: 10,000원
deposit(5000) # 5,000원 입금 완료. 잔액: 15,000원
withdraw(3000) # 3,000원 출금 완료. 잔액: 12,000원
withdraw(20000) # 잔액 부족! 현재 잔액: 12,000원
get_balance() # 현재 잔액: 12,000원
문제 2: 중첩 카운터
다음 요구사항을 만족하는 중첩 카운터 함수를 작성하세요.
- outer 함수에 count 변수
- inner 함수에서 nonlocal로 count 수정
- increment()와 get_count() 반환
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def create_counter(start=0):
"""카운터 생성기"""
count = start # Enclosing 변수
def increment(step=1):
"""카운트 증가"""
nonlocal count
count += step
return count
def get_count():
"""현재 카운트 조회"""
return count
return increment, get_count
# 테스트
inc, get = create_counter(10)
print(get()) # 10
print(inc()) # 11
print(inc()) # 12
print(inc(5)) # 17
print(get()) # 17
# 새로운 카운터
inc2, get2 = create_counter(100)
print(get2()) # 100
print(inc2(10)) # 110
❓ 자주 묻는 질문 (FAQ)
Q1. 전역 변수와 지역 변수의 이름이 같으면 어떻게 되나요?
지역 변수가 우선입니다. 함수 내부에서는 지역 변수를 사용하고, 전역 변수는 가려집니다 (Shadowing).
1
2
3
4
5
6
7
8
x = "전역"
def my_function():
x = "지역" # 전역 x를 가림
print(x) # "지역" 출력
my_function()
print(x) # "전역" 출력 (전역 변수는 영향 없음)
해결 방법: 전역 변수를 사용하려면 global 키워드를 명시하세요.
1
2
3
4
5
6
7
8
9
x = "전역"
def my_function():
global x
print(x) # "전역" 출력
x = "수정됨"
my_function()
print(x) # "수정됨" 출력
Q2. global과 nonlocal의 차이는 무엇인가요?
- global: 모듈 최상위의 전역 변수를 수정
- nonlocal: 중첩 함수에서 바깥 함수의 변수를 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
count = 0 # 전역 변수
def outer():
total = 0 # Enclosing 변수
def inner():
# global count를 사용하면 모듈 전역 변수 수정
global count
count += 1
# nonlocal total을 사용하면 outer의 변수 수정
nonlocal total
total += 1
print(f"count={count}, total={total}")
inner()
inner()
outer()
# count=1, total=1
# count=2, total=2
간단 정리:
global: 가장 바깥(모듈)의 변수nonlocal: 한 단계 바깥(enclosing) 함수의 변수
Q3. 함수 안에서 전역 변수를 읽기만 하면 global 없이 가능한가요?
네, 가능합니다! global은 전역 변수를 수정할 때만 필요합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
message = "안녕하세요"
def greet():
# global 없이 읽기 가능
print(message)
greet() # "안녕하세요" 출력
def change():
# ❌ global 없이 수정 시도 → UnboundLocalError
# message = message + "!" # 에러!
# ✅ global 키워드 사용
global message
message = message + "!"
change()
print(message) # "안녕하세요!" 출력
규칙:
- 읽기만:
global불필요 - 수정하려면:
global필수
Q4. LEGB 순서대로 같은 이름의 변수가 있으면 어떻게 되나요?
가장 가까운 스코프의 변수가 사용됩니다. Local이 가장 우선순위가 높습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
x = "Global"
def outer():
x = "Enclosing"
def inner():
x = "Local"
print(x) # "Local" 출력 (L이 최우선)
inner()
print(x) # "Enclosing" 출력 (E가 우선)
outer()
print(x) # "Global" 출력
검색 순서:
- Local: 현재 함수 내 → 찾으면 사용
- Enclosing: 바깥 함수 → 없으면 다음
- Global: 모듈 전역 → 없으면 다음
- Built-in: Python 내장 → 없으면 NameError
Q5. 클로저는 왜 사용하나요? 전역 변수와의 차이는?
클로저의 장점:
- 데이터 은닉: 외부에서 직접 수정 불가
- 독립적 상태: 여러 인스턴스가 각자의 상태 유지
- 전역 오염 방지: 전역 네임스페이스를 깔끔하게 유지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ❌ 전역 변수 방식 - 문제가 많음
count1 = 0
count2 = 0 # 카운터 추가할 때마다 전역 변수 증가
def inc1():
global count1
count1 += 1
return count1
def inc2():
global count2
count2 += 1
return count2
# ✅ 클로저 방식 - 깔끔하고 안전
def make_counter():
count = 0 # 외부에서 직접 접근 불가!
def increment():
nonlocal count
count += 1
return count
return increment
counter1 = make_counter()
counter2 = make_counter() # 독립적인 카운터
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1 (독립적!)
클로저가 더 나은 이유:
- 외부에서
count를 함부로 수정할 수 없음 (안전성) - 카운터를 여러 개 만들어도 서로 독립적 (확장성)
- 전역 변수가 늘어나지 않음 (깔끔함)
Q6. Built-in 함수 이름을 변수로 사용하면 어떻게 되나요?
해당 스코프에서 Built-in 함수를 사용할 수 없게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 위험한 예: max를 변수로 사용
def calculate():
max = 100 # Built-in max를 가림!
numbers = [1, 2, 3, 4, 5]
# print(max(numbers)) # TypeError: 'int' object is not callable
# max가 이제 정수 100이므로 함수가 아님!
calculate()
# 함수 밖에서는 여전히 사용 가능
print(max([1, 2, 3, 4, 5])) # 5
권장사항: Built-in 함수 이름은 변수명으로 사용하지 마세요!
흔한 Built-in 이름들 (피해야 할 변수명):
list,dict,set,tuplestr,int,float,boollen,max,min,sumprint,input,openrange,zip,map,filter
Q7. nonlocal은 여러 단계의 바깥 함수도 접근 가능한가요?
네, 가능합니다! nonlocal은 가장 가까운 Enclosing 스코프의 변수를 찾습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def level1():
x = "Level 1"
def level2():
x = "Level 2"
def level3():
# level2의 x를 수정
nonlocal x
x = "Modified in Level 3"
print(f"Level 3: {x}")
level3()
print(f"Level 2: {x}") # "Modified in Level 3"
level2()
print(f"Level 1: {x}") # "Level 1" (영향 없음)
level1()
여러 단계 건너뛰기:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def outer():
x = "Outer"
def middle():
# x를 선언하지 않음
def inner():
# middle에 x가 없으므로 outer의 x를 수정
nonlocal x
x = "Modified"
inner()
middle()
print(x) # "Modified"
outer()
규칙: nonlocal은 가장 가까운 Enclosing 스코프에서 변수를 찾습니다.
Q8. 람다 함수에서도 스코프 규칙이 동일한가요?
네, 동일합니다! 람다도 함수이므로 LEGB 규칙을 따릅니다.
1
2
3
4
5
6
7
8
9
10
11
12
x = 10 # Global
def outer():
x = 20 # Enclosing
# 람다에서도 LEGB 순서로 검색
func = lambda y: x + y # Enclosing의 x (20) 사용
return func
f = outer()
print(f(5)) # 25 (20 + 5)
클로저도 가능:
1
2
3
4
5
6
7
8
9
def make_adder(n):
# 람다가 n을 기억 (클로저)
return lambda x: x + n
add10 = make_adder(10)
add20 = make_adder(20)
print(add10(5)) # 15
print(add20(5)) # 25
주의: 람다에서는 global이나 nonlocal을 사용할 수 없습니다 (할당문 불가능).
📝 오늘 배운 내용 정리
- 스코프: 변수가 유효한 범위
- 지역 변수: 함수 내부에서만 유효
- 전역 변수: 어디서든 접근 가능
- global: 전역 변수 수정
- nonlocal: Enclosing 변수 수정
- LEGB 규칙: Local → Enclosing → Global → Built-in 순서로 검색
🎯 실습 과제
과제: 할 일 관리 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
todos = [] # 전역 변수
def add_todo(task):
"""할 일 추가"""
global todos
todos.append({"task": task, "done": False})
print(f"'{task}' 추가됨")
def complete_todo(index):
"""할 일 완료"""
global todos
if 0 <= index < len(todos):
todos[index]["done"] = True
print(f"'{todos[index]['task']}' 완료!")
else:
print("잘못된 인덱스입니다.")
def show_todos():
"""할 일 목록 표시"""
if not todos:
print("할 일이 없습니다.")
return
print("\n=== 할 일 목록 ===")
for i, todo in enumerate(todos):
status = "✅" if todo["done"] else "⬜"
print(f"{i}. {status} {todo['task']}")
print()
def remove_todo(index):
"""할 일 삭제"""
global todos
if 0 <= index < len(todos):
removed = todos.pop(index)
print(f"'{removed['task']}' 삭제됨")
else:
print("잘못된 인덱스입니다.")
# 테스트
add_todo("Python 공부하기")
add_todo("운동하기")
add_todo("책 읽기")
show_todos()
complete_todo(0)
show_todos()
remove_todo(1)
show_todos()
🔗 관련 자료
-
Python 스코프와 네임스페이스 (공식 문서) Python 공식 문서의 스코프와 네임스페이스 설명. LEGB 규칙과 변수 검색 순서에 대한 권위 있는 자료입니다.
-
Python LEGB 규칙 완벽 가이드 (Real Python) LEGB 규칙에 대한 상세한 설명과 실전 예제를 제공하는 Real Python의 튜토리얼입니다.
-
Python 클로저와 데코레이터 (Real Python) 중첩 함수, 클로저, 그리고 데코레이터 패턴에 대한 심화 학습 자료입니다.
-
global과 nonlocal 키워드 심화 (GeeksforGeeks) global과 nonlocal 키워드의 사용법과 주의사항을 다양한 예제와 함께 설명합니다.
-
Python 네임스페이스와 스코프 시각화 (Python Tutor) Python 코드를 단계별로 실행하며 스코프와 변수의 변화를 시각적으로 확인할 수 있는 도구입니다.
📚 이전 학습
Day 28: 람다 함수 ⭐
어제는 람다 함수로 간단한 함수를 만드는 방법을 배웠습니다!
📚 다음 학습
Day 30: 미니 프로젝트 - 계산기 ⭐⭐⭐
내일은 지금까지 배운 모든 것을 활용한 계산기 프로젝트를 만듭니다!
“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀
Day 29/100 Phase 3: 제어문과 함수 #100DaysOfPython
