Python의 모듈 간 의존성 문제를 해결하기 위해서 virtualenv나 venv를 많이 사용하실 겁니다. 시스템에 설치된 모듈들과 virtual environment 내에 설치된 모듈들을 분리해주기 때문에 모듈들이 꼬일 걱정 없이 사용할 수 있어서 참 좋습니다. 개인적으로 저는 새 Python 프로젝트를 시작하면 venv와 git 초기화를 꼭 실행합니다.
그런데 가끔 내부적으로 어떻게 이 기능이 작동하는지 궁금할 때가 있습니다. 그래서 오늘은 venv가 어떻게 작동하는지 한번 알아보겠습니다. (virtualenv도 작동 방식은 거의 동일합니다.)
1. venv를 만들자
먼저 아래 명령으로 virtual environment를 만듭니다.
python3 -m venv env
그럼 현재 디렉터리에 env 라는 디렉터리는 만들어지고, venv가 필요한 내용들을 채워줍니다. env의 내부를 보면 아래와 같습니다.
env
|-- bin
| |-- activate
| |-- activate.csh
| |-- activate.fish
| |-- easy_install
| |-- easy_install-3.7
| |-- pip
| |-- pip3
| |-- pip3.7
| |-- python -> python3
| `-- python3 -> /usr/local/bin/python3
|-- include
|-- lib
| `-- python3.7
| `-- site-packages
| |-- __pycache__
| |-- easy_install.py
| |-- pip
| |-- pip-19.2.3.dist-info
| |-- pkg_resources
| |-- setuptools
| `-- setuptools-41.2.0.dist-info
`-- pyvenv.cfg
- bin에는 python, pip 등 실행 파일들이 위치합니다. virtual env 환경을 활성화시키는 activate script도 여기에 있습니다.
python은 venv를 실행시켰을 때 사용한 python 인터프리터로 symbolic link가 걸려있습니다. - include는 비어있는 디렉토리입니다.
- lib/python3.7/site-packages는 앞으로 모듈들이 설치될 곳입니다.
- env의 가장 상위 디렉터리에 설정 파일인 pyvenv.cfg가 있습니다. 이 파일은 venv에 특화된 설정 파일이 아니고, Python이 일반적으로 사용하는 설정 파일입니다.
2. activate이 하는 일은 뭘까
보통 작업을 시작할 때 source env/bin/activate을 실행시켜서 virtual environment로 들어갑니다. 그런데 virtual environment로 들어간다는 것이 뭘까요? Virtual Machine과 같은 어렵고 무거운 개념은 물론 아닙니다. activate script가 하는 일은 생각보다(?) 아주 간단합니다.
activate script의 코드 일부를 살펴보겠습니다.
VIRTUAL_ENV="/Users/ceongjeein/Workspace/venv_demo/env"
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
기존의 PATH(실행 파일들의 위치) 환경 변수를 _OLD_VIRTUAL_PATH라는 새 환경 변수에 백업을 하고, PATH 앞에다가 현재 venv의 bin 디렉터리를 추가합니다. 이렇게 하면 시스템 상의 명령보다 현재 venv의 bin 디렉터리에 있는 명령들이 먼저 실행이 됩니다. 즉, "python"이라고 실행하면 시스템의 python이 아닌, 현재 venv의 python이 실행이 됩니다.
백업해 둔 _OLD_VIRTUAL_PATH는 아래와 같이 나중에 deactivate 명령 시에 PATH를 원상 복귀하는 데 사용합니다.
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
이게 전부입니다. Prompt를 약간 손 본다거나, PYTHONHOME 환경 변수를 끈다거나 하는 몇 가지 동작이 있지만 가장 중요한 것은 저것이 전부입니다. venv가 이렇게 간단할 수 있는 이유는 모듈을 로딩하고 관리하는 것 자체는 Python이 담당하기 때문입니다.
위 코드에서도 볼 수 있지만 원리적으로는 사실 source env/bin/activate 명령을 사용하지 않아도 됩니다. python 코드를 실행할 때 env/bin/python 이라고만 쓰면 똑같거든요.
3. 그럼 모듈 관리는 어떻게 할까?
Python이 모듈을 로딩할 때 가장 중요한 역할을 하는 것은 sys.path입니다. 알쏭달쏭 Python import - sys.path에 자세한 내용이 있으니 한번 읽어보시길 권해드립니다. 한 줄 요약하자면 Python이 import 문을 만나서 모듈을 로딩해야 하면, Python은 sys.path에 지정된 경로들에서 순차적으로 그 모듈을 찾으려고 합니다.
그럼 venv를 쓸 때 sys.path를 확인해볼까요?
➜ pwd
/Users/ceongjeein/Workspace/venv_demo
➜ env/bin/python -m site
sys.path = [
'/Users/ceongjeein/Workspace/venv_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',
'/Users/ceongjeein/Workspace/venv_demo/env/lib/python3.7/site-packages',
]
USER_BASE: '/Users/ceongjeein/Library/Python/3.7' (exists)
USER_SITE: '/Users/ceongjeein/Library/Python/3.7/lib/python/site-packages' (exists)
ENABLE_USER_SITE: False
source env/bin/activate을 하지 않고 직접 venv 내의 Python을 사용했습니다. 물론 source env/bin/activate 하신 후에 "python -m site"라고 하셔도 됩니다.
위의 실행 결과를 보면 sys.path 마지막에 env 내에 lib/python3.7/site-packages가 추가된 것을 알 수 있습니다. 바로 이것 때문에 시스템에 설치된 모듈과 별개로 venv 내부의 모듈을 사용할 수 있는 것이죠. 그렇다면 누가, 어떻게 sys.path에 env/lib/python3.7/site-packages를 추가했을까요? 이 부분을 이해하기 위해서는 python의 site 모듈을 알아야 합니다.
4. site 모듈
내부적으로 sys.path가 채워지는 과정은 수많은 규칙이 포함된 복잡한 과정입니다. 이 과정에 참여하는 모듈이 site입니다. 이 포스트에서는 이 중에서 venv를 이해하는데 필요한 부분만 알아보겠습니다.
* site 모듈은 Python이 실행될 때 자동으로 import 되는 모듈 중 하나입니다.
site가 하는 일은 순서대로 아래와 같습니다.
- sys.prefix와 sys.exec_prefix의 값을 결정합니다. 이 과정 또한 복잡하지만 우리가 관심을 가지는 규칙은 아래와 같습니다.
규칙: Python 인터프리터의 경로 (sys.executable)보다 한 단계 상위 디렉터리에 "pyvenv.cfg"가 있다면, sys.prefix와 sys.exec_prefix의 값을 그 상위 디텍 터리로 설정합니다.
우리의 예를 보면 Python이 env/bin/ 디렉터리에 있고, 그 상위 디렉터리인 env/를 보니 pyvenv.cfg가 덜컥 있습니다. 즉, sys.prefix와 sys.exec_prefix는 env/ (full path는 /Users/ceongjeein/Workspace/venv_demo/env)가 됩니다. - 다음으로 아래 경로를 조합으로 만듭니다.
- sys.prefix + "lib/pythonX.Y/site-packages" (X.Y는 Python 버전)
- sys.exec_prefix + "lib/pythonX.Y/site-packages"
- 그런데 우리 경우에는 sys.prefix와 sys.exec_prefix의 값이 같기 때문에 실제로는 /Users/ceongjeein/Workspace/venv_demo/env/lib/python3.7/site-packages 만 만들어집니다.
- sys.path에 위에서 만든 경로를 더합니다.
이렇게 sys.path에 venv 내부 site-packages가 추가되었습니다.
5. pip는 어떻게 저기에 설치할까요
그렇다면 pip는 어떻게 저 경로를 알고 설치할까요?
또 다른 비밀은 sys.base_prefix입니다. sys.prefix는 앞에서 말씀드린 대로 현재 실행된 Python 인터프리터의 경로와 관계가 있습니다. 위의 경우에는 상위 디렉터리에 pyvenv.cfg 파일의 유무에 따라 결정이 되었고요. 하지만 이렇게 동적으로 결정된 경로와 관계없이 원본(?) Python 인터프리터의 값이 sys.base_prefix에 저장됩니다.
➜ env/bin/python -c "import sys; print(sys.prefix + '\n' + sys.base_prefix)"
/Users/ceongjeein/Workspace/venv_demo/env
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7
➜ python3 -c "import sys; print(sys.prefix + '\n' + sys.base_prefix)"
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7
/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7
같은 코드를 venv 내에 있는 Python을 사용해서 실행하면 sys.prefix와 sys.base_prefix의 값이 다르지만, 시스템 상의 Python을 사용하면 두 변수의 값이 같습니다. pip는 이를 이용해서 sys.prefix와 sys.base_prefix의 값이 다르면 현재 virtual environment를 사용하고 있다고 판단하고, virtual environment내의 site-packages에 모듈들을 설치합니다.
➜ env/bin/pip --version
pip 19.2.3 from /Users/ceongjeein/Workspace/venv_demo/env/lib/python3.7/site-packages/pip (python 3.7)
참고로 pip도 Python의 모듈이기 때문에 특정 Python 인터프리터와 위와 같이 연결이 되어있습니다.
6. 마무리
어떻게 보면 복잡한데요, 간단히 요약하면 아래와 같습니다.
- venv 모듈이 실행 파일에 대한 symbolic links, pyvenv.cfg와 같은 기본 뼈대를 만든다.
- activate 명령은 PATH 환경 변수를 비롯한 몇 가지를 변경한다. (사용은 선택사항)
- Python은 pyvenv.cfg의 위치를 사용해서 venv 내의 site-packages 경로를 sys.path에 추가한다.
- pip는 venv 사용 시 sys.prefix와 sys.base_prefix가 다르다는 사실을 이용해서, venv 내에 새 모듈을 설치한다.
사실 venv를 사용하는 데는 내부가 어떻게 동작하는지 전혀 알 필요가 없습니다. 하지만 이런 내부 내용을 알면 문제가 생겼을 때나 해결한다거나, 새로운 시스템을 설계할 때 참고를 할 수 있으니 알아두면 좋고요.
그럼 오늘도 내일도 모레도 즐거운 Python 생활하시길 바랍니다.
'Python' 카테고리의 다른 글
pyc 파일에 대해서 (5) | 2020.06.03 |
---|---|
-m 실행 옵션과 __name__ (6) | 2020.05.28 |
bad magic number in 'application': b'\x03\xf3\r\n': ImportError (0) | 2020.05.19 |
알쏭달쏭 Python import - sys.path (7) | 2020.05.15 |
함수의 인자를 특정 값으로 고정하기 - functools.partial (0) | 2020.05.12 |