프로그래밍 언어/Python

[Python] namespace와 함수 (전역변수의 참조와 변경, 그리고 오류가 나는 이유)

바보1 2022. 5. 7. 01:05

우선 코드를 보시죠

def func():
    if 10 > m:
        cnt += 1


m = 5
cnt = 0
func()

print(cnt)

실행될까요?

안 됩니다. 오류가 납니다.

UnboundLocalError: local variable 'cnt' referenced before assignment

라는 오류가 납니다.

 

 

아니 m은 잘만 참조하면서 cnt는 왜 참조 못하지? 라는 생각이 드실겁니다.

결론부터 말하자면 '참조'는 가능하지만 '변경'은 불가능합니다.

 

사소하지만 매우 중요한 지식입니다.

 

 

1. namespace를 나눈 이유

 

 

전역 namespace의 변수를 지역 namespace로 가져와서 변경을 못하게 하는 이유는 간단합니다.

그렇게 지역과 전역을 혼동되게 사용한다면 굳이 namespace를 나눈 의미가 없어지기 때문입니다.

 

또한, 파이썬의 대입은 대부분 메모리를 새로 만듭니다.

예를 들어 cnt = cnt + 1이 있다면, 좌변 cnt의 메모리를 새로 만들어서 대입하고, 우변 cnt는 garbage collector가 수거해갑니다.

근데 정확한 namespace를 규정하지 않는다면, 같은 name이라도 다른 공간을 가리키기 때문에 프로그램 입장에선 치명적인 오류가 발생하는거죠.

 

따라서 namespace를 정확히 나눈 겁니다.


2. 그렇다면 위의 문제는 왜 오류가 나는가?

 

 

파이썬은 인터프리터 언어입니다.

따라서 한 줄씩 해석을 합니다.

 

맨 처음에 파이썬이 위에서 아래로 내려오면서 해석을 하는데, 이때 func()를 호출하기 전에 먼저 func() 함수를 봅니다.

이때 m은 단순히 '참조'한다고 인터프리터는 해석합니다.

하지만 cnt = cnt + 1이므로 cnt는 '지역 변수'라고 해석합니다.

즉, cnt = 을 해줌으로써 func() 안에서 '선언'해버린 꼴이 되버리는 겁니다.

 

아무튼 쭉쭉 내려가다가 func() 함수가 호출이 되면 다시 func() 함수가 실행이 되죠?

근데 m이 지역 변수 안에 선언되어 있지 않으니까 전역 namespace로 가서 찾습니다.

 

이제 m = 5라는 값을 찾았습니다.

 

따라서 if True이므로 cnt += 1을 실행해야죠.

 

하지만 cnt = cnt + 1 이 문장을 자세히 보면,

cnt = 이 문장으로 인터프리터는 좌측의 cnt가 지역 변수에 선언되었다고 해석했고, 이후 우측의 cnt도 당연히 지역 변수라고 해석합니다.

(참고로 대입과 같은 양변이 있는 명령어는 양쪽이 지역이거나 전역 둘 중에 하나여야 합니다.)

근데 우측의 cnt를 지역 namespace에서 찾으면?

당연히 없죠. 선언 되기전에 참조를 해버린겁니다.

 

즉, 인터프리터 입장에서는 좌변의 cnt는 지역 변수므로, 당연히 우변의 cnt도 지역 변수겠거니와 생각을 했는데, 없으니까 오류가 뜨는 겁니다.

 

하지만 우리는 다른 의도로 하고싶잖아요?

좌변의 cnt와 우변의 cnt를 모두 전역 변수로 사용하고 싶잖아요?


3. 오류 해결법

 

 

간단합니다. 이 cnt를 global 변수라고 못 박아놓는겁니다.

어떻게?

바로 함수의 맨 처음에 global cnt를 넣어주면 됩니다.

 

그러면 인터프리터는 해석을 하다가, global cnt를 만나면 이렇게 해석합니다.

cnt는 전역 변수에 있는 변수구나.

그러면 cnt = cnt + 1은 어떻게 되죠?

우변의 cnt가 전역 변수니까, 당연히 좌변의 cnt도 전역 변수겠지.

그러면 전역 변수에 있는 cnt에 전역 변수의 cnt에 +1을 한 값을 저장하면 되겠다!

라고 생각합니다.

 

즉 우리가 생각하는 의도대로 돌아가는겁니다.

 

코드는 아래와 같습니다.

def func():
    global cnt
    if 10 > m:
        cnt += 1


m = 5
cnt = 0
func()

print(cnt)
1

간단하죠?

 

하지만 매우 중요합니다.

안 되니까 global을 쓰는 것과 왜 안 되는지 알고 global을 쓰는 것과는 차원이 다른 문제입니다.


4. 그렇다면 다른 예시는?

 

 

우선 코드를 보겠습니다.

def func():
    global temp
    if 10 > m:
        temp = cnt + 1


m = 5
cnt = 10
temp = 0
func()

print(temp)

위에서 설명한 내용을 바탕으로 한 번 생각해보세요.

 

m, cnt는 단순히 '참조'하는 반면, temp는 값을 '변경'하고 있죠?

이때 변경한 temp를 다시 전역 namespace에서 참조하고 싶다면 global temp를 적어줘야합니다.

이해하니까 간단하죠?

 

 


5. 추가

 

def func():
    lst = lst + [0]
    lst.append(10)

lst = [10]
func()

print(lst)

참고로 위의 lst = lst + [0]은 오류가 나고, 아래의 lst.append(10)은 오류가 안 납니다.

위의 lst = lst + [0]은 위에서 설명한 내용과 같지만, 아래는 왜 될까요?

 

간단하게 말하자면, lst를 우선 '참조'를 해야하는데, 지역 namespace에 없으니까 전역 namespace로 가서 찾습니다.

그리고 거기에 append를 하는거죠.

 

따라서 저기도 global lst를 해주면 해결됩니다.

 

엥? 근데 파라미터로 lst를 넘기면 안 되냐구요?

한 번 해보세요 ㅎㅎ 안 됩니다.

def func(lst):
    lst = lst + [0]
    lst.append(10)
    

lst = [10]
func(lst)

print(lst)

저렇게 되면 첫 번째의 lst = lst + [0]은 지역 namespace에 할당이 되어버립니다.

따라서 lst.append(10)도 지역 변수의 lst에 해당하는 명령문이 되고요.

신기하죠?

저렇게 되면 func안의 lst만 [10, 0, 10]이 되버립니다.

따라서 print(lst)를 하면 [10]만 출력이 됩니다.

 

근데 이때 lst = lst + [0]을 없앤다면?

def func(lst):
    # lst = lst + [0]
    lst.append(10)


lst = [10]
func(lst)

print(lst)

의도한 대로 [10, 10]이 출력이 됩니다. ㅎㅎ

 

 

감사합니다.

 

지적 환영합니다.