포스트

[Python 100일 챌린지] Day 29 - 스코프와 전역 변수

[Python 100일 챌린지] Day 29 - 스코프와 전역 변수

변수는 어디서 어디까지 사용할 수 있을까? 스코프와 전역 변수의 모든 것을 배워봅시다! (30분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

  1. 변수 스코프의 개념 이해하기
  2. 지역 변수와 전역 변수 구분하기
  3. global과 nonlocal 키워드 사용하기
  4. 클로저와 스코프 활용하기
  5. LEGB 규칙과 스코프 체인 이해하기

📚 사전 지식


🎯 학습 목표 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

클로저를 사용하는 이유

  1. 데이터 은닉: 외부에서 직접 접근할 수 없는 private 변수 생성
  2. 상태 유지: 함수 호출 간 상태 보존
  3. 팩토리 패턴: 설정이 다른 여러 함수 생성
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이 변수를 찾는 순서:

  1. Local (지역): 현재 함수 내부
  2. Enclosing (둘러싼): 중첩 함수의 바깥 함수
  3. Global (전역): 모듈 전체
  4. 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" 출력

검색 순서:

  1. Local: 현재 함수 내 → 찾으면 사용
  2. Enclosing: 바깥 함수 → 없으면 다음
  3. Global: 모듈 전역 → 없으면 다음
  4. Built-in: Python 내장 → 없으면 NameError
Q5. 클로저는 왜 사용하나요? 전역 변수와의 차이는?

클로저의 장점:

  1. 데이터 은닉: 외부에서 직접 수정 불가
  2. 독립적 상태: 여러 인스턴스가 각자의 상태 유지
  3. 전역 오염 방지: 전역 네임스페이스를 깔끔하게 유지
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, tuple
  • str, int, float, bool
  • len, max, min, sum
  • print, input, open
  • range, 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을 사용할 수 없습니다 (할당문 불가능).


📝 오늘 배운 내용 정리

  1. 스코프: 변수가 유효한 범위
  2. 지역 변수: 함수 내부에서만 유효
  3. 전역 변수: 어디서든 접근 가능
  4. global: 전역 변수 수정
  5. nonlocal: Enclosing 변수 수정
  6. 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()

🔗 관련 자료


📚 이전 학습

Day 28: 람다 함수

어제는 람다 함수로 간단한 함수를 만드는 방법을 배웠습니다!


📚 다음 학습

Day 30: 미니 프로젝트 - 계산기 ⭐⭐⭐

내일은 지금까지 배운 모든 것을 활용한 계산기 프로젝트를 만듭니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 29/100 Phase 3: 제어문과 함수 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.