알쏭달쏭 햇갈리는 파이썬 네임스페이스 (namespace) 이해하기
졸업 프로젝트에서 백엔드 웹 프레임워크로 FastAPI를 사용하게 되었습니다.
파이썬을 사용본 적이 있지만 언어 자체에 대한 공부는 하지 않았었는데, 이번 기회에 언어부터 차근차근 채워나가고자 하고 있습니다.
이번 포스트에서는 최근에 파이썬을 공부하면서 헷갈리면서도 중요하다고 생각되는 파이썬의 네임스페이스에 대해 알아보는 시간을 가져보겠습니다.
# 00. About Name
Name?
네임스페이스에 대해 알아보기 전에 먼저 파이썬에서의 이름(name)에 대해 이해할 필요가 있다고 생각합니다.
파이썬에서의 이름(name)은 코드에서 사용되는 '객체'들을 구분하기 위한 식별자입니다.
num1 = 1
숫자 1을 num1이라는 변수에 바인딩하는 코드입니다.
위 코드에서 1은 int형의 객체가 되는 것이고, num1은 객체 1이 할당되어 있는 이름(name)이 됩니다.
네임 바인딩 (Name Binding)
네임 바인딩은 assignment, import, class와 function 정의 등의 시기에 발생합니다.
# 1) Assignment
lie = "I love Python"
문자열 타입의 객체가 lie라는 변수에 바인딩됩니다.
# 2) Import
import math
모듈 또한 객체이며, import 시에 math 모듈이 'math'라는 이름(name)에 바인딩됩니다.
# 3) Class 정의
class Person:
pass
클래스 정의 시 Person 클래스 객체를 'Person' 이름(name)에 바인딩합니다.
# 4) Function 정의
def sayHello():
print("Hello!")
함수 정의 시에도 역시 sayHello 함수 객체를 'sayHello' 이름(name)에 바인딩합니다.
위의 모든 상황에서 네임 바인딩은 런타임 해당 구문이 실행될 때 일어납니다.
# 01. About Namespace
Namespace?
파이썬의 네임스페이스는 단어 이름에서 알 수 있듯이 위에서 말씀드린 파이썬의 이름(Name)들을 관리하는 논리적인 공간입니다.
파이썬 공식문서에서 정의하는 네임스페이스는 아래와 같습니다.
A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future.
즉, 이름(name)을 객체에 매핑하는 dictionary 타입의 구조라고 볼 수 있습니다.
Namespace 종류
파이썬의 네임스페이스에는 built-in, global, local 이렇게 세 가지 종류가 있습니다.
1. Built-in
먼저 Built-in 네임스페이스의 경우 sum(), print()와 같이 파이썬에서 기본적으로 사용할 수 있는 내장 함수, 클래스, 예외 등의 이름을 관리하고 있습니다.
인터프리터가 실행될 때부터 종료될 때까지 언제든 접근이 가능한 네임스페이스입니다.
def built_in():
import builtins
for name in sorted(dir(builtins)):
print(name)
if __name__ == "__main__":
built_in()
파이썬의 builtins 모듈을 이용해서 built-in 네임스페이스의 정보를 확인할 수 있습니다.
위와 같은 코드로 Built-in 네임스페이스서 관리하는 모든 이름(name)을 출력해 보면 Exception, AttributeError, EOFError와 같은 예외부터, bool, int와 같은 자료형, 그리고 종종 사용하는 len(), sum()과 같은 함수까지 확인하실 수 있습니다.
2. Global
Global 네임스페이스의 경우 모듈 레벨의 이름(name)을 관리하는 공간입니다.
모듈이 import 되었을 때부터 모듈이 언로드 되거나 인터프리터가 종료될 때까지 유지되며, 해당 모듈 최상위에서 정의된 모든 변수, 함수, 클래스 등의 이름을 포함합니다.
# my_module.py
language = 'python'
위와 같이 my_module이라는 이름의 모듈에서 language라는 변수에 'python' 문자열 객체를 바인딩했다면, my_module의 global 네임스페이스에 language라는 이름(name)을 저장하게 됩니다.
Global 네임스페이스에서 관리하는 이름(name)의 경우 globals() 함수를 이용해서 확인하실 수 있습니다.
# my_module.py
value = 100
def show_global():
names = sorted(globals().keys())
for name in names:
print(name)
if __name__ == "__main__":
show_global()
위와 같이 my_module 모듈에서 관리하는 global 네임스페이스 이름(name)들을 출력해 보면, value와 show_global이 출력되는 것을 볼 수 있습니다.
3. Local
Local 네임스페이스의 경우 함수 레벨의 이름(name)을 관리하는 공간입니다.
함수가 호출될 때마다 생성되고, 함수가 반환되거나 처리되지 않은 예외가 발생하면 삭제됩니다.
Local 네임스페이스에서 관리하는 이름(name)은 locals() 함수를 통해 출력할 수 있습니다.
value1 = 100
def show_local():
value2 = 200
value3 = 300
print(locals())
if __name__ == "__main__":
show_local()
# 결과 : {'value2': 200, 'value3': 300}
위 코드에서 show_local의 네임스페이스에 value2와 value3 이름(name)이 등록됩니다.
때문에 show_local() 함수 내에서 locals() 함수로 이름(name)을 출력해 보면, global 이름인 value1은 출력되지 않습니다.
번외로 함수 내에서 global 이름(name) 값을 조작하고 싶다면, globla 키워드를 사용해야 합니다.
value1 = 100
def change_value():
global value1
value1 = 200
print(locals()) # 여전히 value1은 local namespace에 등록되지 않음
if __name__ == "__main__":
change_value()
print(value1)
네임스페이스 검색 순서
코드에서 이름(name)이 사용되면 Local -> Global -> Built-in 순서로 검색을 합니다.
만약 현재 스코프가 Global이라면, Global -> Built-in 순으로 검색합니다.
def sum(a, b):
return a - b
def run_sum():
print(sum(5, 3))
if __name__ == "__main__":
run_sum() # 결과 : 2
위 상황의 경우를 정리하면 다음과 같습니다.
- run_sum() 스코프 내에서 sum() 함수 호출
- Local 네임스페이스에서 sum() 함수 검색 -> sum() 함수 없음
- Global 네임스페이스에서 sum() 함수 검색 -> sum() 함수 있음 ( a - b )
- sum() 함수 실행
이와 같은 과정을 거치기 때문에 결과가 8이 나오는 것이 아니라 Global에 정의되어 있는 sum()에 의해 2가 출력됨을 확인할 수 있습니다.
만약 Global 네임스페이스와 Built-in 네임스페이스의 이름(name)이 겹치는 상황에서 Built-in 함수를 사용하고 싶다면 아래와 같이 builtins 모듈을 통해 명시적으로 호출해야 합니다.
import builtins
def sum(a, b):
return a - b
def run_sum():
print(builtins.sum([5, 3]))
if __name__ == "__main__":
run_sum()
Namespace 구조
네임스페이스는 해당 스코프 내에서 이름(name)과 객체를 매핑하는 구조라고 정의했습니다.
'매핑'이라는 단어가 쓰인 이유가 무엇일까요?
dictionary 구조인 네임스페이스에서 특정 key(이름)에 해당하는 value에 대해 실제 값을 저장하고 있는 것이 아닙니다.
value에는 실제 값이 저장되어 있는 메모리의 주소값을 담고 있습니다.
그림으로 이해해보자면 아래와 같습니다.

위와 같은 상황에서 만약 value3에도 "Hello" 값을 저장하면 어떻게 될까요?
value1 = 100
value2 = "Hello"
value3 = "Hello"
def show_ids():
print(id(value1))
print(id(value2))
print(id(value3))
if __name__ == "__main__":
show_ids()
# 결과 :
# 4354157824
# 4342010320
# 4342010320
built in 함수 id()를 이용하면 실제 이름(name)의 메모리 참조 주소를 확인할 수 있습니다.
위 코드를 실행한 결과 value1과 value2의 id값은 다르지만, 같은 "Hello" 값을 매핑하고 있는 value2와 value3은 같은 id 값을 가지고 있는 것을 확인할 수 있습니다.
그림으로 정리하면 다음과 같습니다.

그렇다면 이번에는 value1의 값을 바꿔보겠습니다.
value1 = 100
value2 = "Hello"
value3 = "Hello"
value1 = 200
def show_id():
print(id(value1))
print(id(value2))
print(id(value3))
if __name__ == "__main__":
show_id()
# 결과 :
# 4384995712
# 4372617888
# 4372617888
value1의 id값이 바뀌었습니다.
이 말인즉슨, global 네임스페이스에서 value 이름(name)에 대한 value에 저장되어 있던 메모리 참조 주소가 바뀌었음을 의미합니다.

이때 value1은 메모리상에서 새로운 객체의 주소값과 매핑이 되고, 기존에 값 100을 저장하고 있던 객체는 메모리상에서 참조를 잃게 됩니다.
메모리에서 관리하고 있는 객체에 대해 ref_cnt라는 값도 저장하고 있는데, 이는 해당 객체가 몇 번 참조되었는지를 나타냅니다.
값 100을 나타내던 객체 (그림상에서 0x101번지)는 value1이 새로운 주소를 참조함에 따라 ref_cnt가 0이 되고, ref_cnt 값이 0인 객체는 Garbage Collector에 의해 자동으로 제거됩니다.
추가적으로, 위와 같은 상황에서 Global 네임스페이스의 경우 read/write 동작을 모두 수행할 수 있기 때문에 value 값들을 변경할 수 있었습니다.
Global과 Local 네임스페이스의 경우 read/write 동작을 모두 수행 가능하지만, Built-in 네임스페이스의 경우 read 동작만 수행할 수 있습니다.
# 02. if __name__ == '__main__': 문의 의미
이번 포스트에서 예시 코드를 작성할 때 if __name__ == '__main__': 문을 매번 활용했습니다.
파이썬 코드를 참고할 때 if__name__ == '__main__': 문을 종종 볼 수 있었는데요,
직관적으로 이해가 되는 코드가 아니라서 많이 헷갈렸는데, 네임스페이스의 이해와 함께 이 구문도 제대로 파악할 수 있게 되었습니다.
__name__ 변수는 파이썬 인터프리터가 각 모듈을 읽을 때, Global 네임스페이스에 자동으로 정의하는 특별 변수입니다.
모듈이 직접 실행될 때는 __name__의 값이 문자열 "__main__"이 됩니다.
만약 다른 모듈에서 import 될 때는 __name__ 값이 그 파일의 이름이 됩니다.
즉, if __name__ == '__main__': 문을 사용하게 되면, 해당 파일로 직접 실행할 때만 구문 안에 있는 스크립트를 실행하고, 다른 파일에서 import 할 때에는 스크립트를 실행하지 않게 하기 위함입니다.
이 구문을 왜 사용해야 하는지는, 구문을 사용하지 않는 예시 코드를 실행해 보면 느끼실 수 있습니다.
# module.py
def sayHello():
return "안녕하세요!"
print(sayHello())
# module2.py
import module
print(module.sayHello())
위 경우에서 만약 python module2.py로 module2를 실행하면 결과는 "안녕하세요!"를 두 번 출력합니다.

그 이유는 module2.py에서 module.py를 import 할 시에 top-level 코드를 한 번 실행하기 때문입니다.
결과적으로 module.py의 print() 문과 module2.py의 print() 문이 모두 실행되어서 결과가 두 번 출력되게 됩니다.
이는 모듈에 정의된 이름(name)들을 네임스페이스에 초기화하기 위함입니다.
모듈에 정의된 함수, 클래스, 변수 이름을 실제 객체에 바인딩하려면, 해당 정의문이 실행되어야 합니다.
이러한 이유로 if __name__ == '__main__': 구문이 사용되는 것을 이해할 수 있었습니다.
사실은 이번 주 포스트에서는 파이썬의 디스크립터 프로토콜에 대해 작성해보려고 했는데.... 내용이 어렵더라고요.
급하게 주제를 바꿔서 글을 작성했습니다.
조만간 꼭 디스크립터 제대로 이해하고 포스팅하는 걸 목표로 하겠습니다.