변수도 구역이 있다

지난 글에서는 함수가 계산한 값을 return으로 돌려주는 방법을 살펴봤어요. 그런데 함수 안에서 만든 변수는 함수 밖에서도 쓸 수 있을까요? 음... 직접 써 보면 의외로 헷갈리는 지점이 있습니다. 오늘은 변수가 어디에서 보이고 어디까지 살아 있는지, 스코프(scope)를 알아볼게요.

스코프는 변수의 활동 범위

변수의 스코프를 함수 바깥과 안쪽으로 나눈 그림

스코프는 쉽게 말해 변수를 사용할 수 있는 범위예요. 파이썬은 변수를 만든 위치에 따라 그 변수를 찾을 수 있는 곳이 달라집니다.

함수 안에서 만든 변수는 보통 그 함수 안에서만 쓸 수 있어요. 이런 변수를 지역 변수라고 합니다. 함수 바깥에서 만든 변수는 전역 변수라고 부르고, 함수 안에서도 값을 읽을 수 있죠.

name = "민지"  # 전역 변수

def say_hello():
    message = "안녕, " + name  # 지역 변수 message, 전역 변수 name 읽기
    print(message)

say_hello()
print(name)

함수 안에서 name을 읽을 때는 함수 바깥에 있는 전역 변수를 찾습니다. 반면 message는 함수 안에서 만들었으니 함수 밖에서 바로 사용할 수 없어요. 변수마다 자기 구역이 있는 셈이죠.

지역 변수는 함수 안에서만

함수 안에서만 쓸 수 있는 지역 변수 예시

이 코드를 실행하면 어떻게 될까요?

def make_message():
    message = "오늘도 한 줄씩!"
    print(message)

make_message()
print(message)  # 오류

print(message)가 함수 안에 있을 때는 잘 동작하지만, 마지막 줄에서는 message를 찾을 수 없어요. 함수가 실행될 때 만들어진 지역 변수는 그 함수 안에서 쓰기 위한 변수니까요.

처음엔 “분명 위에서 만들었는데 왜 못 찾아?” 싶을 수 있어요. 저도 예전에 함수 안에서 만든 임시 변수를 밖에서 꺼내 쓰려다 한참 헤맸습니다. 어? 변수가 증발한 것처럼 보였지만, 사실은 처음부터 바깥 구역에 놓인 적이 없었던 거예요.

함수 밖에서도 결과가 필요하다면 지난 글에서 본 return을 사용하면 됩니다.

def make_message():
    message = "오늘도 한 줄씩!"
    return message

result = make_message()
print(result)

지역 변수의 값을 반환받아 result에 담았어요. 함수가 내부에서 어떻게 계산했는지 감추고, 필요한 결과만 바깥에 전달하는 방식입니다.

전역 변수는 읽을 수 있지만 조심해서

전역 변수 읽기와 수정의 차이를 보여 주는 그림

전역 변수는 함수 바깥에서 만들고 여러 함수에서 읽을 수 있어요. 다만 함수 안에서 그 값을 바꾸려 할 때는 규칙이 하나 더 있습니다.

count = 0

def show_count():
    print(count)  # 전역 변수 읽기

show_count()

읽는 건 괜찮습니다. 그런데 함수 안에서 count = count + 1처럼 대입하면 파이썬은 함수 안의 count를 지역 변수로 판단해요. 아직 값이 정해지기 전에 읽으려는 상황이 되어 오류가 납니다.

count = 0

def add_one():
    count = count + 1  # 오류: 지역 변수로 취급되지만 먼저 읽고 있음

전역 변수 자체를 바꾸려면 global을 선언할 수 있어요.

count = 0

def add_one():
    global count
    count += 1

add_one()
print(count)  # 1

작동은 하지만, 함수가 바깥 상태를 직접 바꾸면 어느 곳에서 값이 변했는지 따라가기 어려워질 수 있어요. 여러 함수가 같은 전역 변수를 바꾸기 시작하면 “누가 숫자를 올렸지?” 하는 탐정 놀이가 열립니다. 솔직히 말하면, 디버깅할 때 그 탐정 모자는 별로 쓰고 싶지 않더라고요.

값은 인자로 받고 결과는 반환하기

전역 변수 대신 인자와 반환값을 사용하는 흐름

전역 변수에 기대는 대신, 필요한 값은 함수의 매개변수로 전달하고 바뀐 결과는 반환하는 방법이 읽기 쉽습니다.

def add_one(count):
    return count + 1

count = 0
count = add_one(count)
print(count)  # 1

이제 함수만 봐도 어떤 값을 받아 어떤 결과를 만드는지 알 수 있죠. 함수 바깥의 상태를 몰래 바꾸지 않아서, 다른 곳에서 재사용하거나 고칠 때도 부담이 적습니다.

물론 전역 변수가 항상 나쁜 건 아니에요. 프로그램 전체에서 바뀌지 않는 설정값처럼 여러 곳에서 읽는 값에는 쓸 수 있습니다. 다만 자주 바뀌는 값이라면 인자와 반환값으로 흐름을 드러내는 편이 대체로 편해요.

변수 범위를 알면 함수가 선명해져요

오늘 내용은 이렇게 기억하면 됩니다.

  • 함수 안에서 만든 변수는 지역 변수라서 보통 함수 안에서만 쓸 수 있어요.
  • 함수 밖에서 만든 전역 변수는 함수 안에서 읽을 수 있지만, 수정하려면 global 선언이 필요해요.
  • 함수 밖으로 값을 전달할 땐 전역 변수에 기대기보다 return으로 결과를 돌려주는 방법을 먼저 생각해 보세요.

앞에서 배운 매개변수와 반환값이 여기서도 꽤 든든하죠? 다음 글에서는 코드를 짧게 표현할 때 쓰는 람다 함수를 만나볼게요. 어디까지 짧게 줄여도 읽기 편할지, 같이 살펴봅시다.

AIWD 소모임AI 자동화 오픈톡