Logo

파이썬의 global과 nonlocal 키워드 사용법

파이썬에는 globalnonlocal이라는 아주 많이 쓰이지는 않지만 종종 만나게 되는 재미있는 키워드가 있습니다. 이 두 키워드를 한글로 해석하면 각각 ‘전역’, ‘비지역’으로 비슷한 의미인 것 같아서 많은 분들이 햇갈려하시는데요.

이번 포스팅에서는 이 두 개의 키워드가 왜 필요하고 어떤 경우에 사용해야되는지에 대해서 알아보도록 하겠습니다.

변수의 범위(scope)

globalnonlocal 키워드에 대해서 이해하려면 먼저 변수의 범위(scope)에 대한 개념을 간단히 짚고 넘어가야할 것 같습니다.

비단 파이썬 뿐만 아니라 대부분의 프로그래밍 언어에서 변수의 범위라는 것은 해당 변수를 어디에서 선언하느냐에 따라서 결정이 됩니다. 아주 단순하게 두 구역으로 나누면 함수 외부를 전역(global/module) 범위라고 하고, 함수 내부를 지역(local/function) 범위라고 합니다. 또한 함수를 중첩했을 때 외부 함수와 내부 함수의 사이에서 생겨나는 비지역(nonlocal/enclosing) 범위라는 것도 있습니다.

이 세 구역의 범위를 각 함수의 입장에서 간단하게 주석으로 나타내보면 다음과 같습니다.

# outer(), inner() 함수 입장에서 전역(global) 범위
def outer():
    # outer() 함수 입장에서 지역(local) 범위
    # inner() 함수 입장에서 비지역(nonlocal) 범위
    def inner():
        # inner 함수 입장에서 지역(local) 범위

(파이썬에서는 추가적으로 내장(built-in) 범위라는 것도 있지만 globalnonlocal 키워드를 이해햐는데 크게 도움이 되지 않으므로 다루지 않겠습니다.)

같은 범위 내에서는 자유롭게 변수에 접근이 가능하지만 다른 범위에서 선언된 변수에 접근할 때는 정해진 제약을 따르게 됩니다. 기본적으로 바깥 쪽 범위 내에서 선언된 변수를 안 쪽 범위에서는 접근할 수 있지만, 반대로 안 쪽 범위 내에서 선언된 변수를 바깥 쪽 범위에서 접근하는 것은 불가능합니다. 예를 들어, outer() 함수 밖에서 선언한 전역 변수는 outer() 함수 내부와 inner() 함수 내부에서 접근이 가능합니다. 하지만 outer() 함수 안에서 안에서는 선언한 outer() 함수 외부에서는 접근할 수 없으며, outer() 함수와 inner() 함수 안에서는 접근 가능이 가능합니다.

global_var = "전역 변수"

def outer():
    nonlocal_var = "비전역 변수"
    print(global_var) # 가능
    print(nonlocal_var) # 가능

    def inner():
        local_var = "지역 변수"
        print(global_var) # 가능
        print(nonlocal_var) # 가능
        print(local_var) # 가능

    print(local_var) # 불가능 (NameError: name 'local_var' is not defined)

print(nonlocal_var) # 불가능 (NameError: name 'nonlocal_var' is not defined)
print(local_var) # 불가능 (NameError: name 'local_var' is not defined)

변수의 범위의 다른 중요한 특성은 서로 다른 범위에서는 변수 이름 충돌이 발생하지 않으며 안 쪽 범위에서 바깥 쪽 범위에서 선언된 변수와 똑같은 이름의 변수를 생성할 수 있다는 것입니다. 예를 들어, 아래에 선언된 3개의 var 변수는 이름만 같을 뿐 서로 다른 값을 저장할 수 있는 엄연히 다른 변수입니다.

var = "지역 변수"

def outer():
    var = "비지역 변수"

    def inner():
        var = "지역 변수"

이러한 현상을 소위 variable shadowing이라고 부르기도 하는데요. 코드 가독성을 해치기 때문에 일반적으로 피해야하는 코딩 관행으로 여겨지고 있지만 지금부터 설명드릴 globalnonlocal 키워드가 왜 필요한지 이해하는데 중요한 개념입니다.

global 키워드

아래 코드를 실행하면 무엇이 출력이 될까요?

num = 0 # 전역 변수

def change_num():
    print(num)
change_num()

print(num)

0이 두 번 연속으로 출력이 될 것입니다. num는 전역 변수이기 함수 안에서 접근하든 밖에서 접근하든 동일한 값을 담고 있습니다.

그러면 아래와 같이 num를 함수 안에서 갱신하면 무엇이 출력이 될까요?

num = 0

def change_num():
    num = 100    print(num)

change_num()

print(num)

많은 분들이 100이 두 번 연속으로 출력될 것이라고 생각하시지만, 실제로 실행을 해보면 1000이 출력이 됩니다. 위에서 설명드린 소위 variable shadowing 현상 때문에 함수 내부에 있는 num이라는 지역 변수는 함수 외부에 있는 num이라는 전역 변수와 엄연히 다른 변수입니다.

그러면 이와 같이 동일한 이름의 지역 변수를 생성하지 않고, 전역 변수의 값을 함수 내부에서 변경하고 싶다면 어떻게 해야할까요❓ 네, 이럴 때 사용하는 것이 global 키워드입니다❗

함수 안에서 변수 앞에 global 키워드를 붙여주면 해당 변수는 새로운 지역 변수가 되지 않고 함수 밖에서 이미 선언된 전역 변수를 가리키게 됩니다.

num = 0

def chnage_num():
    global num    num = 100
    print(num)

chnage_num()

print(num)

위 코드를 실행하면 처음에 예상했던 데로 100이 두 번 연속으로 출력되는 것을 불 수 있습니다.

nonlocal 키워드

nonlocal 키워드도 global 키워드와 같이 동일한 이름의 새로운 변수가 생성되는 것을 방지하기 위해서 사용됩니다. 이 두 키워드의 차이점은 global 키워드는 일반 함수 내에서 전역(global/module) 변수를 대상으로 사용하는 반면에 nonlocal 키워드는 중첩 함수 내에서 비지역(nonlocal/closing) 변수를 대상으로 사용한다는 것입니다.

글로만 설명하면 이해가 어려울 수 있으니 예제 코드를 한번 같이 볼까요?

아래 두 개의 함수는 중첩이 되어 있습니다. 여기서 num 변수는 print_nums() 함수의 안 쪽, change_num() 함수의 바깥 쪽에서 선언이 되어 있는데요. 따라서 이 변수는 print_nums() 함수 입장에서 보면 지역 변수이고, change_num() 함수 입장에서 보면 비지역 변수입니다.

def print_nums():
    num = 0 # 비지역 변수

    def change_num():
        print(num)
    change_num()

    print(num)

print_nums()

이 코드를 실행해보면 0이 연속으로 두 번 출력되는데요. 위에서 설명드린 변수 범위의 특징에 따라서 print_nums() 함수에서 선언된 num 변수를 change_num() 함수 안에서 그대로 접근하기 때문입니다.

이 번에는 change_num() 함수 내에서 num 변수의 값을 변경해보았는데요. 무엇이 출력될까요?

def print_nums():
    num = 0

    def change_num():
        num = 100        print(num)

    change_num()

    print(num)

print_nums()

이 번에는 1000이 출력이 됩니다. 이는 variable shadowing 현상으로 인해서 change_num() 함수 안에서 num이라는 새로운 지역 변수가 생성되었기 때문입니다.

만약에 change_num() 함수 바깥에서 선언된 num이라는 비지역 변수의 값을 change_num() 함수 안에서 갱신하고 싶은 경우에는 어떻게 해야할까요❓ num 변수 앞에 nonlocal 키워드만 붙여주면 됩니다❗

def print_nums():
    num = 0

    def change_num():
        nonlocal num        num = 100
        print(num)

    change_num()

    print(num)

print_nums()

변수 앞에 nonlocal 키워드를 붙여주면 해당 변수는 새로운 지역 변수가 되지 않고 함수 밖에서 이미 선언된 비전역 변수를 가리키게 됩니다.

위 코드를 실행하면 처음에 의도했던 데로 100이 두 번 연속으로 출력되는 것을 불 수 있습니다.

실전 예제

global 키워드나 nonlocal 키워드를 사용하지 않아서 실제로 파이썬으로 코딩할 때 겪을 수 있는 대표적인 문제에 대해서 다루면 좋을 것 같습니다.

아래 counter() 함수는 cnt 변수 값을 변경하기 위해서 내부에 선언된 _increment() 함수를 반환하고 있습니다.

def counter():
    cnt = 0

    def _increment(step = 1):
        cnt += step # 오류 발생
        return cnt

    return _increment

count_up = counter()
print(count_up())
print(count_up(2))
print(count_up(3))

얼핏보면 이 코드는 정상적으로 동작할 것 같지만 실행을 해보면 다음과 같은 오류가 발생을 합니다.

UnboundLocalError                         Traceback (most recent call last)
<ipython-input-14-ee29d3b8aa10> in <module>
      9
     10 count_up = counter()
---> 11 print(count_up())
     12 print(count_up(2))
     13 print(count_up(3))

<ipython-input-14-ee29d3b8aa10> in _increment(inc)
      3
      4     def _increment(inc = 1):
----> 5         cnt += inc
      6         return cnt
      7

UnboundLocalError: local variable 'cnt' referenced before assignment

왜 그럴까요? cnt 변수는 _increment() 함수 입장에서 봤을 때 바깥에서 선언된 비지역(nonlocal) 변수입니다. 하지만 cnt 변수의 값을 변경하려는 순간 함수 내부에서는 같은 cnt 이름을 가진 지역(local) 변수가 새롭게 생성이 되려고 할 것입니다.

cnt += stepcnt = cnt + step와 동일하기 때문에 cnt라는 지역 변수에 미처 초기값도 할당하지도 않은 체로 접근하려고 해서 문제가 발생하는 것입니다.

이 문제를 해결하기 위해서는 _increment() 함수 안에서 cnt 변수 앞에 nonlocal 키워드를 붙여주면 됩니다.

def counter():
    cnt = 0

    def _increment(step = 1):
        nonlocal cnt        cnt += step
        return cnt

    return _increment

count_up = counter()
print(count_up())
print(count_up(2))
print(count_up(3))

cnt 변수 앞에 nonlocal 키워드를 붙여주면 같은 이름의 새로운 지역 변수가 생성되는 대신에 함수 외부에 선언된 비지역 변수인 cnt의 값을 변경되기 때문입니다.

코드를 수정 후에 실행해보면 다음과 같이 cnt 변수의 값이 변경되어 출력되는 것을 볼 수 있습니다.

1
3
6

전체코드

본 포스팅에서 제가 작성한 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

https://deepnote.com/project/Blog-Yd3-DsV_QeGqo4AUZ7FyHg/%2Fpython-global-nonlocal.ipynb

마치면서

외부 범위에서 선언된 변수를 내부 범위에서 변경할 수 있다는 것은 프로그래밍을 할 때 양날의 검으로 작용할 수 있습니다. 예를 들어, 전역 변수의 값이 여러 함수에 의해서 변경될 수 있다면 해당 전역 변수의 값이 어떻게 변해갈지 예측하기가 쉽지 않을 것입니다. 그러므로 예상치 못한 버그가 발생할 확률이 높아지고 코드의 복잡도는 올라가며 유지보수는 힘들어 질 것 입니다.

이 것이 아마도 파이썬에서 globalnonlocal 키워드를 아주 자주는 볼 수 없는 이유일 것입니다. globalnonlocal 키워드를 사용하기 전에 반드시 필요한 상황인지 재고해보시면 납용과 오용을 줄이는데 도움이 될 것입니다.