우리는 프로그램을 작성하면서 여러 가지 데이터에 여러 가지 이름을 붙인다. 그런데 이름이란 문맥에 따라서 가리키는 대상이 다르기 마련이다. 예를 들어, ‘key’라는 이름은 키보드의 입력을 제어하는 함수에서는 입력된 키를 가리키겠지만, 암호를 푸는 함수에서는 비밀번호를 가리킬 것이다. 이 두 함수를 모두 사용하는 프로그램을 만들려면, ‘key’라는 이름이 문맥에 따라 다른 데이터를 가리킬 수 있어야 한다.

프로그래밍에서는 ‘이름공간(namespace)’이라는 개념을 이용해서 이름의 문맥을 구별한다. 이름공간은 변수의 이름을 정의해 둔 공간이다. 이름공간은 프로그램 전체 범위의 이름을 담는 전역 이름공간과 한정적인 문맥의 이름을 담는 지역 이름공간으로 구별된다.

전역 이름공간에 정의되어, 프로그램 어디서든 부를 수 있는 이름을 전역변수(global variable)라고 한다. 함수 밖에서 변수를 정의하면 전역변수가 된다. 반면에 지역 이름공간에 정의되어, 그 문맥 속에서만 부를 수 있는 이름을 지역변수(local variable)라고 한다. 모든 함수는 자신만의 지역 이름공간을 가지며, 함수 속에서 작성한 변수는 그 함수의 지역변수가 된다.

3.4.1 지역변수는 함수만의 것

함수의 지역변수는 함수가 실행되는 동안에만 존재한다. 각 함수가 호출되어 실행될 때 만들어지고, 함수의 실행이 끝나면 모두 삭제된다. 그래서 지역변수는 그 변수가 속한 함수의 밖이나 다른 함수에서는 부를 수 없다. 매개변수도 함수 안에 정의되므로 지역변수다. 다음 예제에서 전역변수와 지역변수를 구별해 보자.

코드 3-11 전역변수와 지역변수가 함께 사용된 프로그램

seconds_per_minute = 60  # 1분은 60초 ❶

def minutes_to_seconds(minutes):
    """분을 입력받아 같은 시간만큼의 초를 반환한다."""
    seconds = minutes * seconds_per_minute  # ❷
    return seconds

print(minutes_to_seconds(3))  # 화면에 180이 출력된다
print(seconds)  # ❸ 오류! 함수 밖에서 지역변수를 불렀다

실행 결과:

180
NameError: name 'seconds' is not defined

❶에서 정의한, 1분이 몇 초인지 나타내는 seconds_per_minute 변수는 프로그램 어디에서든 사용할 수 있는 전역변수다. minutes_to_seconds() 함수 안의 ❷에서도 이 변수를 읽고 있다. 반면, 분을 입력받는 매개변수 minutes와 함수 안에서 중간 계산 결과를 저장하는 변수 seconds는 지역변수다. ❸과 같이 seconds 변수를 그 변수가 존재하는 문맥(함수) 밖에서 읽으려고 하면, 문맥 밖에는 그 변수가 존재하지 않기 떄문에 이름 오류가 발생한다.

전역변수는 어디에서나 읽을 수 있지만, 함수 안에서 전역변수에 새로운 값을 대입하는 것은 금지된다. (잠시후 설명할 global 문을 사용하면 예외적으로 가능해진다.) 표 3-1은 지역변수와 전역변수의 접근 조건을 표로 정리한 것이다.

특징 전역변수 지역변수
함수 안에서 읽기 가능 가능
함수 안에서 수정 불가(*) 가능
함수 밖에서 읽기 가능 불가
함수 밖에서 수정 가능 불가

표 3-1 전역변수와 지역변수의 접근 조건 (*: global 문 사용시 가능)

개념 정리

  • 전역변수: 함수 밖, 전역 이름공간에 정의된 변수
  • 지역변수: 함수 안, 지역 이름공간에 정의된 변수
  • 지역변수는 그 변수가 정의된 함수 안에서만 읽을 수 있다.
  • 전역변수는 프로그램 어디서든 읽을 수 있다. 단, 함수 안에서 전역변수에 새로운 값을 대입할 수는 없다.

연습문제

연습문제 3-9 전역변수와 지역변수 구별하기

다음 프로그램에서 사용된 전역변수와 지역변수를 각각 나열해 보아라. 각 지역변수가 어느 함수에 속하는지도 구분해 보아라.

pi = 3.141592653589793

def area_of_circle(radius):
    """원의 반지름(radius)을 입력받아 넓이를 반환한다."""
    area = radius * radius * pi
    return area

def volume_of_cylinder(radius, height):
    """원기둥의 반지름(radius)과 높이(height)를 입력받아
    부피를 반환한다."""
    top_area = area_of_circle(radius)
    volume = top_area * height
    return volume

result = volume_of_cylinder(5, 10)
print(result)

3.4.2 지역변수의 생존 기간

지역변수는 함수가 실행될 때마다 새로 만들어지고, 함수의 실행이 종료되면 삭제된다. 코드 3-11을 예로 들면, minutes_to_seconds() 함수의 실행이 종료된 후에는 seconds라는 변수가 더이상 존재하지 않는다. 함수 밖에서 지역변수를 사용할 수 없는 것은 이 이유 때문이기도 하다. 함수가 실행중일 때만 지역변수가 존재하는데, 어떻게 존재하지 않는 변수를 사용할 수 있겠는가?

이 점은 함수를 실행할 때마다 함수가 이전에 계산했던 내용을 다 잊어버린 채 새로 실행된다는 뜻이기도 하다. 함수의 이전 실행 결과를 기억해야 한다면 함수의 밖에서 결과를 보관해주어야 한다. 함수의 실행 결과를 기억하려면 결과 = 함수()와 같이 함수를 호출하는 곳에서 함수의 실행 결과를 변수에 대입하면 된다.

3.4.3 관심의 분리

처음에는 전역변수와 지역변수를 구별하는 것이 번거로울 수 있다. 모든 변수가 전역변수라면 어디서든 데이터를 자유롭게 읽고 수정할 수 있을 텐데, 전역변수와 지역변수를 구별하여 사용하는 이유는 무엇일까?

함수는 문제를 작은 문제로 나누어 해결하기 위해서 사용한다. 문제를 나누어 해결하려면, 다른 문제를 배제하고 지금 다루는 문제만을 생각할 수 있어야 한다. 함수는 프로그램 전체를 구성하는 일부이지만, 각각의 함수는 자기가 맡은 작은 문제만을 해결할 뿐이다. 함수는 자기 문제에 필요한 데이터만을 조작해야 하고 관련이 없는 프로그램 전체 데이터나 다른 함수의 데이터에 손을 대면 안 된다.

그러므로 함수 안에서는 지역변수만을 사용하는 것이 바람직하다. 전역변수는 프로그램 전체에서 공통적으로 사용되고 잘 변하지 않는 데이터를 담는 데만 써야 한다. 전역변수가 수시로 변하고 여러 함수에서 저마다 손을 댄다면 프로그램의 흐름을 파악하기 어렵다. 함수의 입력 통로와 출력 통로를 매개변수와 return 문으로 제한하는 것도 프로그램의 흐름을 파악하기 쉽게 하기 위한 것이다. 함수 안에서 함수 밖의 데이터를 건드리는 것은 물이 흐르는 파이트에 구멍을 꿇는 것과 같다. 구멍이 많으면 그 물이 어디로 흐를지 예측하기 어렵다.

개념 정리

  • 지역변수를 이용해 그 데이터와 관련된 문제를 함수 내부의 문제로 국한시킬 수 있다.
  • 전역변수를 함수 안에서 수정하는 것은 좋지 않다.

3.4.4 함수 안에서 전역변수 수정하기

함수 안에서 전역변수를 수정할 수 없다. 다만, 꼭 필요하다면 global 문을 이용해 이 규칙을 어길 수 있다. global 문이 필요한 상황과 global 문을 사용하는 방법을 알아보자.

함수의 실행 결과 누적하기

함수가 전역변수를 수정해야 하는 대표적인 상황으로 함수의 실행 결과를 누적해야 하는 경우가 있다. 다음은 쿠폰 도장이 찍힌 횟수를 전역변수에 기록하고 화면에 출력하는 stamp() 함수다.

코드 3-12 함수 안에서 전역변수를 수정하려는 오류

num_stamp = 0  # 쿠폰 스탬프가 찍힌 횟수 (전역변수)

def stamp():
    """쿠폰 스탬프가 찍힌 횟수를 증가시키고, 화면에 출력한다."""
    num_stamp = num_stamp + 1  # ❶ 전역변수를 수정하려고 시도함
    print(num_stamp)

실행 결과:

UnboundLocalError: local variable 'num_stamp' referenced before assignment

이 함수를 실행하면 ❶에서 오류가 발생한다. “지역 변수 num_stamp에 값을 대입하기도 전에 참조했다”라는 오류 메시지가 출력된다.

오류가 발생한 과정을 생각해 보자. num_stamp = 값을 대입할 때, 함수 안에서는 num_stamp라는 새로운 지역변수가 생성된다. 그런데 num_stamp에 대입할 값이 공교롭게도 num_stamp + 1이다. 아직 만들어지지 않은 지역변수를 읽으려 한 것이다.

원래 의도는 전역변수 num_stamp의 값을 읽으려는 것이었다. 하지만 함수 안에서 num_stamp에 무언가를 대입하려 했기 때문에 이 변수는 지역변수로 해석되었다. 함수 안에서는 지역변수에만 값을 대입할 수 있기 때문이다.

이 문제를 해결하려면 global 문을 이용해 num_stamp 변수가 전역변수임을 명시적으로 밝혀야 한다.

global 문

global 문을 사용하면 함수 안에서도 전역변수의 값을 수정할 수 있다. 함수 안에서 global 변수이름 명령을 실행하면 그 이름이 전역변수임을 분명히 밝히게 되며, 그리고 전역변수의 값을 수정하는 것도 가능해진다. stamp() 함수에 global 문을 삽입해 수정해 보자.

코드 3-13 global 문의 사용

num_stamp = 0  # 쿠폰 스탬프가 찍힌 횟수 (전역변수)

def stamp():
    """쿠폰 스탬프가 찍힌 횟수를 증가시키고, 화면에 출력한다."""
    global num_stamp           # ❶ num_stamp는 전역변수다
    num_stamp = num_stamp + 1  # 이제 오류가 발생하지 않는다
    print(num_stamp)

stamp()  # 화면에 1이 출력된다
stamp()  # 화면에 2가 출력된다

실행 결과:

1
2

❶의 global 문을 실행함으로써 함수 안에서도 함수 밖의 전역변수 num_stamp를 수정할 수 있게 되었다. 원래 의도한 함수를 만드는 데 성공했다.

global 문은 사용하지 않는 것이 좋다

global 문을 배웠지만, 역시 이 명령은 사용하지 않는 것이 좋다. global 문을 사용하는 것은 함수가 매개변수와 반환값을 이용해 외부와 소통하는 자연스러운 흐름을 깨트리는 일이다.

함수 안에서 전역변수를 수정하지 않고, 매개변수와 반환값만 이용하더라도 함수의 실행 결과를 누적하는 데 부족함이 없다. 다음 예제는 전역변수를 직접 수정하는 대신, 매개변수와 반환값을 이용하도록 stamp() 함수를 수정한 버전이다.

코드 3-14 매개변수와 반환을 이용한 stamp() 함수

num_stamp = 0  # ❶ 쿠폰 스탬프가 찍힌 횟수 (전역변수)

def stamp(num_stamp):  # ❷ 지역변수(매개변수) num_stamp
    """쿠폰 스탬프가 찍힌 횟수를 증가시키고, 화면에 출력한다."""
    num_stamp = num_stamp + 1
    print(num_stamp)
    return num_stamp

num_stamp = stamp(num_stamp)  # ❸ 전역변수에 함수의 반환값을 대입한다
num_stamp = stamp(num_stamp)

실행 결과:

1
2

위 코드에는 두 개의 num_stamp 변수가 등장한다. 하나는 ❶ 전역변수 num_stamp이고, 다른 하나는 ❷ 좌변의 stamp() 함수의 지역변수이자 매개변수인 num_stamp다. 두 변수는 이름은 같지만 (이름을 다르게 지어도 된다) 존재하는 공간이 다르기 때문에, 서로 전혀 다른 변수다. stamp() 함수는 자신의 지역변수인 num_stamp만을 수정하고, 전역변수 num_stamp는 건드리지 않는다. 전역변수 num_stamp는 함수 밖(❸)에서만 수정되고 있다.

이처럼 함수 안에서 전역변수를 수정하지 않아도 함수의 실행 결과를 누적할 수 있다. 다른 사람의 프로그램을 읽을 수 있도록 global 문의 사용법을 알아두되, 가급적이면 사용하지 않도록 하자.