Python

알쏭달쏭 Python import - sys.path

둔진 2020. 5. 15. 04:01

  Python으로 만들고 있는 프로그램이 커지기 시작하면 모듈화를 합니다. 이 기능, 저 기능을 모아서 하나의 파일로 합쳐서 모듈(*.py)로 만들지요. 모든 모듈들이 한 디렉터리에 있을 때는 아무 문제없이 행복합니다. 그러다가 조금 더 프로그램이 커지면 모듈들을 모아서 패키지(디렉터리)로 만들고 슬슬 문제가 시작됩니다. 멀쩡히 import 되던 모듈들이 찾을 수 없다며 import가 되지 않거나 , 엉뚱하게 동작하는 일들이 생기죠. 이것이 다 python의 sys.path 때문입니다.

1. sys.path 누구냐 넌

  sys.path는 모듈을 import 할 때 모듈을 찾아야할 경로들을 저장해둔 list입니다. 예를 들어 sys.path가 ['directory_A', 'directory_B', 'directory_C']라고 가정해보겠습니다. import super_util이라는 코드를 만나면 Python은 먼저 directory_A 디렉터리 내에서 super_util 모듈을 찾습니다. 찾으면 import 하고요. 없으면 directory_B, directory_C를 검색해보고, 그래도 없으면 ModuleNotFoundError 예외를 발생시킵니다.

 

  그럼 sys.path는 어떻게 만들어질까요?

  • 먼저 최초 실행된 Python 스크립트가 위치한 디렉토리를 더합니다. 이때 interactive shell (python or python3)으로 실행한 경우는 ''(빈 스트링)를 사용합니다.
  • 환경 변수 중 PYTHONPATH의 값을 가져옵니다.
  • 그리고 여기에 OS나 Python 배포판이 설정해 둔 값들을 더합니다.

  아래의 코드를 /Users/ceongjeein/Workspace/pydev/main.py로 저장하고 같은 디렉토리에서 python3 main.py로 실행해보겠습니다.

import sys

print('sys.path from main.py')
print('\n'.join(sys.path))
➜  python3 main.py
sys.path from main.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

  main.py가 위치한 /Users/ceongjeein/Workspace/pydev가 첫번째값이 됩니다. 나머지는 시스템의 값들이고요.

2. 첫번째 값의 비밀

  여기에서 중요한 점은 sys.path의 첫 번째 값이 Python 스크립트를 실행한 현재 디렉터리가 아니고, 그 스크립트가 위치한 디렉터리라는 점입니다. 즉, 어디에서 스크립트를 실행시켰는지는 중요하지 않습니다.

 

  위 코드를 /Users/ceongjeein/Workspace/pydev/module_demo/main.py로 저장한 후, /Users/ceongjeein/Workspace/pydev 에서 python3 module_demo/main.py로 실행해보겠습니다.

➜  pwd
/Users/ceongjeein/Workspace/pydev

➜  python3 module_demo/main.py
sys.path from main.py
/Users/ceongjeein/Workspace/pydev/module_demo
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

  보시는 것처럼 첫번째가 /Users/ceongjeein/Workspace/pydev/module_demo로 변경되었습니다. 스크립트를 실행한 위치는 /Users/ceongjeein/Workspace/pydev지만, 이 사실은 아무 영향이 없습니다. 처음에 이런 현상을 접하면 조금 헷갈리는데 사실 모듈을 만드는 입장에서는 아주 좋은 설계입니다. 모듈을 작성하는 사람은, 사용자의 환경을 신경 쓰지 않고 모듈을 구성할 수 있기 때문이죠. 사용자의 현재 디렉터리에 따라 모듈을 검색하는 방식이 달라진다면 골치 아픈 일이 이만저만이 아닐 겁니다.

3. 패키지 안에서는 어떻게 작동할까

  module_demo라는 디렉터리 안에 아래 코드를 library.py라고 만들어보겠습니다.

import sys

print('sys.path from library.py')
print('\n'.join(sys.path))
print()

print('__name__ of library.py')
print(__name__)
print()

  __name__은 현재 모듈(*.py 파일)의 이름입니다. sys.modules에 key로 저장이 돼있고요. 함수 이름으로 함수 호출하기에 관련 내용이 있습니다. 그리고 현재 디렉토리에 main.py라는 이름으로 아래 코드를 저장합니다.

import sys

print('sys.path from main.py')
print('\n'.join(sys.path))
print()

print('__name__ of main.py')
print(__name__)
print()

from module_demo import library

  현재 두 개 파일이 있습니다.

  • /Users/ceongjeein/Workspace/pydev/main.py
  • /Users/ceongjeein/Workspace/pydev/module_demo/library.py

  그리고 main.py가 저장된 곳에서 python3 main.py를 실행시킵니다.

➜   pwd
/Users/ceongjeein/Workspace/pydev

➜   python3 main.py
sys.path from main.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of main.py
__main__

sys.path from library.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of library.py
module_demo.library

  먼저 sys.path를 보면 main.py가 위치한 디렉터리인 /Users/ceongjeein/Workspace/pydev가 추가되어있습니다. 한번 스크립트가 실행되면 sys.path는 공유되기 때문에 library.py에서도 같은 sys.path를 사용합니다. 앞에서 python3 module_demo/library.py를 실행했을 때는 sys.path의 첫 번째 값이 /Users/ceongjeein/Workspace/pydev/module_demo 였던 것과는 다릅니다.

  다시 한번 강조하자면, sys.path의 첫번째 값은 최초 실행시킨 스크립트가 위치한 디렉터리입니다. (어디에서 python3 main.py를 실행시켰는지는 무관합니다)

 

  __name__을 살펴보면 main.py의 __name__은 __main__이고, library.py는 module_demo.library입니다. 어떤 스크립트가 python 명령으로 직접 실행되면 __main__이라는 이름을 가지고, import로 실행됐다면 그 모듈의 디렉토리.파일(또는 package.module)을 이름으로 가집니다.

  진짜인지 궁금하니 확인해볼까요?

➜   pwd
/Users/ceongjeein/Workspace/pydev

➜   python3 module_demo/library.py
sys.path from library.py
/Users/ceongjeein/Workspace/pydev/module_demo
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of library.py
__main__

  같은 library.py지만 python 명령으로 직접 실행했더니 sys.path와 __name__이 변경되었습니다.

4. 문제아 등판

  이번에는 module_demo 디렉터리 아래에 util.py라는 이름으로 아래 코드를 저장합니다.

import sys

print('sys.path from util.py')
print('\n'.join(sys.path))
print()

print('__name__ of util.py')
print(__name__)
print()

import library

  그리고 실행해보겠습니다.

➜   pwd
/Users/ceongjeein/Workspace/pydev

➜   python3 module_demo/util.py
sys.path from util.py
/Users/ceongjeein/Workspace/pydev/module_demo
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of util.py
__main__

sys.path from library.py
/Users/ceongjeein/Workspace/pydev/module_demo
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of library.py
library

  아주 훌륭하게 작동하는 군요. 

 

  그럼 main.py를 아래와 같이 살짝 고쳐서 실행해보겠습니다. main.py에서 library.py에 추가로 util.py도 쓰고 싶다는 가정입니다.

import sys

print('sys.path from main.py')
print('\n'.join(sys.path))
print()

print('__name__ of main.py')
print(__name__)
print()

from module_demo import library
from module_demo import util
➜   pwd

/Users/ceongjeein/Workspace/pydev
➜   python3 main.py
sys.path from main.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of main.py
__main__

sys.path from library.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of library.py
module_demo.library

sys.path from util.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of util.py
module_demo.util

Traceback (most recent call last):
  File "main.py", line 12, in <module>
    from module_demo import util
  File "/Users/ceongjeein/Workspace/pydev/module_demo/util.py", line 11, in <module>
    import library
ModuleNotFoundError: No module named 'library'

  ModuleNotFoundError가 util.py에서 났습니다. 

  분명히 util.py를 실행시켰을 때는 잘 됐는데!!! 범인은 바로 sys.path입니다. sys.path의 첫 번째 값은 /Users/ceongjeein/Workspace/pydev입니다. 왜냐면 우리는 main.py를 실행시켰기 때문이죠. 이 값은 이후에 import 되는 util.py를 포함해서 모든 모듈에서 쓰이고요. util.py에서 import library를 실행하려는데, sys.path에 있는 디렉터리들 어디에서 library.py는 없습니다. 그래서 ModuleNotFoundError가 발생한거죠.

 

  해결방법은 relative import를 사용하는 것입니다. relative import를 사용하면 현재 모듈이 위치한 곳을 기준으로 다른 모듈을 import 할 수 있습니다. util.py를 아래와 같이 살짝 고쳐보겠습니다.

import sys

print('sys.path from util.py')
print('\n'.join(sys.path))
print()

print('__name__ of util.py')
print(__name__)
print()

from . import library
➜   pwd
/Users/ceongjeein/Workspace/pydev

➜   python3 main.py
sys.path from main.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of main.py
__main__

sys.path from library.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of library.py
module_demo.library

sys.path from util.py
/Users/ceongjeein/Workspace/pydev
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of util.py
module_demo.util

  잘 되는군요. util.py에서 library.py를 import할 때 현재 내 위치와 같은 곳에 있다고 알려줬기 때문이죠.

 

  하지만 직접 util.py를 실행시키면 다른 에러가 발생합니다.

➜   pwd
/Users/ceongjeein/Workspace/pydev

➜   python3 module_demo/util.py
sys.path from util.py
/Users/ceongjeein/Workspace/pydev/module_demo
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages

__name__ of util.py
__main__

Traceback (most recent call last):
  File "module_demo/util.py", line 11, in <module>
    from . import library
ImportError: attempted relative import with no known parent package

  이것은 Python의 설계이기 때문에 어떤 스크립트를 모듈로 사용할지 아니면 직접 실행할지에 따라서 구성을 해야 합니다.

5. 그래도 난 둘 다 쓰고 싶은데!

  진리의 둘 다!

  방법은 sys.path에 module_demo 디렉터리를 추가해서 import 시에 저 디렉터리도 찾게 만드는 것입니다. 먼저, util.py를 원래대로 돌려놓고요.

import sys

print('sys.path from util.py')
print('\n'.join(sys.path))
print()

print('__name__ of util.py')
print(__name__)
print()

import library

  그리고 난 후 main.py를 아래와 같이 수정합니다.

import sys
import os

module_demo_path = os.path.join(os.getcwd(), 'module_demo')
sys.path.append(module_demo_path)

print('sys.path from main.py')
print('\n'.join(sys.path))
print()

print('__name__ of main.py')
print(__name__)
print()

from module_demo import library # 이제 그냥 import libray도 가능
from module_demo import util # 이제 그냥 import util도 가능

  이렇게 하면 python3 main.py, python3 module_demo/util.py가 모두 문제없이 실행됩니다. sys.path는 일반적인 list이기 때문에 얼마든지 수정이 가능합니다.

 

  또 다른 방법은 PYTHONPATH 환경 변수를 이용하는 방법입니다 (첫 번째 세션 참조).

  main.py에서 sys.path를 수정하는 부분을 없애고요.

import sys
import os

# module_demo_path = os.path.join(os.getcwd(), 'module_demo')
# sys.path.append(module_demo_path)

print('sys.path from main.py')
print('\n'.join(sys.path))
print()

print('__name__ of main.py')
print(__name__)
print()

from module_demo import library # 이제 그냥 import libray도 가능
from module_demo import util # 이제 그냥 import util도 가능

  아래와 같이 실행합니다.

➜   export PYTHONPATH=/Users/ceongjeein/Workspace/pydev/module_demo

➜   python3 main.py

  결과적으로는 두 방법 모두 sys.path에 /Users/ceongjeein/Workspace/pydev/module_demo를 추가하는 것인데 전자는 코드로, 후자는 환경 변수를 이용한 것입니다.

6. 마무리

  Python의 module import는 잘 설계된 것은 맞지만, 처음에는 생각과 좀 다르게 작동하는 부분들이 있어서 혼동스럽기 쉽습니다. Python으로 모듈화 된 큰 프로그램보다는 짧은 스크립트를 작성하는 경우가 많기 때문에 이런 구조가 익숙하지 않기도 하고요. 모쪼록 Python을 이해하는데 조금이나마 도움이 되었으면 좋겠습니다.