작성하는 프로그램이 복잡해지고 소스의 규모가 늘어날 수록 적절한 모듈화가 중요하다. 파이썬에서 어떻게 기능들을 모듈화 시키고 어떤 원리로 모듈의 사용이 가능한지 알아보자.
특정 파이썬 파일에서 생성된 객체들은 그 파일의 네임스페이스에 등록되어있다. 어떤 객체들이 있는지는 내장함수인 dir
를 통해 확인할 수 있다.
# my_module.py
a = 3
b = "me me"
def myFunc():
pass
class MyClass():
pass
print(dir())
➜ python my_module.py
['MyClass', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'myFunc']
한 파이썬 파일의 네임스페이스에 등록된 객체들 (변수, 함수, 클래스 등등..)을 다른 파일에서 쓰기 위해서는 해당 네임스페이스를 다른 파일에서 접근해야 한다.
다른 파일(모듈)의 네임스페이스를 가져오는 방법은 여러가지가 있다.
가져온 파일의 네임스페이스는 <가져온 모듈 이름>.객체이름
으로 접근 가능하다.
하나의 인터프리터 세션에서 같은 모듈을 여러 번 임포트 하더라도 모듈은 처음 한 번만 로드된다. 만일 중간에 모듈을 리로드(Re-Load) 하고 싶다면,
importlib
의reload
함수를 쓰도록 하자.
# other_module.py
import my_module
print(my_module.a)
print(my_module.b)
print(dir())
➜ python other_module.py
['MyClass', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'myFunc']
3
me me
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'my_module']
## my_module이 other_module 파일의 네임스페이스에 등록되었다.
모듈을 임포트 하는 방법에는 여러가지가 있다.
모듈 이름을 그대로 네임스페이스에 등록한다.
from <module_name> import *
모듈의 네임스페이스에 존재하는 객체를 그대로 등록한다. 가져온 객체 이름이 로컬 네임스페이스의 객체 이름과 겹칠 수 있어 추천하지 않는 방법이다.
모듈에서 가져온 객체의 이름을 변경해 로컬 네임스페이스에 등록한다.
모듈의 이름을 변경해 등록한다.
__name__
속성 설정하기
위의my_module.py
이 임포트되었을 때, 해당 모듈에 있는 프린트문까지other_module.py
에서 실행되는 것을 볼 수 있다. 보통의 경우 그런 내용은 모듈이 유닛테스트나 기타 용도로 쓰기 위해 스크립트로 실행되었을 때 필요하기 때문에 모듈로서 임포트 했을 때는 제외하고 싶은 행동이다.
이때는 모듈 안에if (__name__ == '__main__')
일 경우에만 실행되도록 해당 조건문 블록 안에서 작성 해주면 된다.__name__
속성은 해당 파일이 단독 실행되었을 때만__main__
값을 가지기 때문이다.
우리가 특정 이름의 모듈을 import
하면, 파이썬은 아래와 같은 순서로 해당 이름을 가진 모듈을 검색한다. 검색하다가 같은 이름의 모듈이 발견되면 해당 시점에서 검색을 멈추게 된다.
sys.modules
에서 이전에 캐시된 모듈 중에 있는지 확인sys.path
에 존재하는 경로의 디렉터리 sys
모듈을 예로들자면, 이전에 캐시된 적이 있다면 1번에서 로드되고 아니라면 2번의 빌트인 모듈에서 경로를 찾아 로드 될 것이다.
sys.path vs. sys.modules
sys.modules
은 인터프리터가 이전에 로드했었던 모듈들을 저장하고 있는 딕셔너리이다. 모듈의 빠른 로드를 위한 캐시라고 보아도 무방하다.
sys.path
는 운영체제의 환경 변수 중PYTHONPATH
와 설치시 의존 경로를 포함하는 파이썬이 모듈 검색 시 살펴보는 경로의 모음이다.
파이썬 빌트인 모듈이나 패키지 관리자(pip)로 설치한 모듈은 쉽게 찾을 수 있지만 나머지 직접 만든 모듈은 직접 경로 설정을 통해 임포트해줘야 한다.
경로는 두 가지 방식으로 제공할 수 있다.
.
├── main.py
└── myPkg
├── __init__.py
├── mod1.py
├── mod2.py
├── subPkg1
│ └── mod3.py
└── subPkg2
└── mod4.py
패키지가 포함된 소스 폴더 최상단을 기준으로 절대 경로로 접근한다.
예를들어 main.py
에서 mod3.py
를 절대경로로 임포트하려면 from myPkg.subPkg.mod3 import *
와 같이 하면 된다.
import
문을 쓰고자 하는 파일의 위치를 기준으로 상대 경로를 통해 다른 파일에 접근한다.
현재 위치 계층을 .
, 한 단계 위 계층을 ..
으로 표현한다.
예를들어 mod4.py
에서 mod3.py
에 접근하려면, from ..subPkg2.mod4 import *
과 같이 하면 된다.
만들게 되는 모듈이 기능 별로 많아지면서 서로 이름이 겹치거나 기능을 계층적으로 분리해야 하는 경우가 생긴다. 패키지는 이러한 많은 모듈을 계층적으로 분리해 서로간의 네임스페이스 충돌을 막을 수 있는 방법을 제공한다.
패키지는 여러 모듈을 운영체제 상의 하나의 폴더에 계층적으로 구성함으로써 만들 수 있다.
.
├── main.py
└── myPkg
├── mod1.py
└── mod2.py
위와 같은 구성에서,
# myPkg/mod1.py
def introduce():
print("I am a function in mod1.py")
# myPkg/mod2.py
def introduce():
print("I am a function in mod2.py")
# main.py
import myPkg.mod1, myPkg.mod2
# import myPkg => 구문상 에러는 없으나 이 방법으로는 패키지의 모듈을 로컬 네임스페이스로 불러올 수는 없다.
myPkg.mod1.introduce()
myPkg.mod2.introduce()
'''
➜ python main.py
I am a function in mod1.py
I am a function in mod2.py
'''
__init__.py
파이썬 3.3 이전에는 패키지를 만들기 위해서는 빈 파일이더라도 반드시 __init__.py
의 존재가 필요했지만 3.3 버젼부터 반드시 존재할 필요는 없어졌다. 하지만 패키지의 디렉토리에 __init__.py
를 만들어 주면 패키지나 모듈이 임포트 될 때 해당 파일이 실행된다. 이 성질을 이용해 패키지 사용을 더 간편하게 할 수 있다.
# __init__.py
SOMETHING = ["SOMETHING", "TO", "SHARE", "AMONG", "MODULES"]
# mod1.py
from myPkg import SOMETHING
# 패키지 안의 모듈이 공유하며 사용 가능..!
print(SOMETHING)
원래 import myPkg
만 실행하면 로컬 네임스페이스에 myPkg
라는 이름은 올라가지만 다른 패키지 내의 다른 모듈들을 가져오지는 못한다.
하지만 __init__.py
내부에서 필요한 모듈들을 미리 가져와주어서 자동 임포트를 해주면 패키지만 임포트해도 해당 모듈들을 사용할 수 있다.
# __init__.py
import myPkg.mod1, myPkg.mod2
import myPkg
# 원래 안됐는데 된다!
myPkg.mod1.introduce()
myPkg.mod2.introduce()
모듈 레벨에서는 from module import *
과 같이 임포트시 모듈 안에 정의된 객체들을 로컬 네임스페이스로 가지고 온다. 하지만 패키지에서 같은 일을 하면 아무 일도 일어나지 않는다.
from pkg import *
과 같은 구문을 통해 패키지의 모듈을 로컬로 가져오고 싶으면, __init__.py
내부에 __all__
객체를 정의해야 한다.
# __init__.py
__all__ = ["mod1", "mod2"]
from myPkg import *
mod1.introduce()
mod2.introduce()
참고로 모듈 안에서도
__all__
객체를 만들어주면 전체 임포트를 했을때 로컬 네임스페이스로 제공되는 객체를 제한할 수 있다.
패키지는 그 아래에 또 다른 서브 패키지를 하위 계층으로 가질 수 있다.
.
├── __init__.py
├── mod1.py
├── mod2.py
├── subPkg1
│ └── mod3.py
└── subPkg2
└── mod4.py
서브패키지도 별 다른 것 없이 패키지와 서브패키지 이름을 dot notation으로 이어서 접근할 수 있다. 또한 서브패키지의 모듈들 끼리도 서로 상호 참조 할 수 있다. (절대경로 상대경로 상관 없음.)