Python

pyc 파일에 대해서

둔진 2020. 6. 3. 03:20

1. *.pyc 파일

  Python으로 코딩을 하다 보면 내가 만들지 않은 *.pyc 파일들이 만들어져 있는 것을 볼 수 있습니다. 가끔은 *.pyc가 문제를 일으키기도 하고요 (bad magic number in 'application': b'\x03\xf3\r\n': ImportError)

  *.pyc는 Python이 *.py를 읽어서 실행시킬 때 자동 생성되는 파일인데, 이는 Python 프로그램이 어떻게 구동되는지와 관련이 있습니다.

  (이야기를 진행하기에 앞서 한가지 명확히 해야 할 것이 있습니다. 우리가 보통 Python이라고 이야기하지만, Python이라는 것은 프로그래밍 언어이기 때문에 앞으로 이야기할 내용은 엄밀히는 CPython에 대한 이야기입니다. CPython은 Python이라는 언어를 실제로 구현한 결과 중 하나이고, 이 외에도 IronPython, PyPy, Jython 등 여러 가지 구현이 있습니다. CPython이 표준 구현이고 다른 구현들의 레퍼런스이기 때문에 이번 이야기는 CPython을 기준으로 진행하고, 편의상 Python이라고 부르겠습니다.)

 

  *.py는 아시는 것처럼 Python 소스코드입니다. 흔히 Python이 인터프리터라고 하지만 실제로 Python(CPython)이 작동하는 방식은 전통적인 인터프리터와는 다릅니다. 기억을 되짚어 보면, 컴파일러는 소스코드를 기계어로 먼저 번역한 후 실행을 하고, 인터프리터는 이 번역 과정 없이 바로 실행한다는 점이 다릅니다. 하지만 Python은 *.py 파일을 실행시킬 때 내부적으로 아래 두 단계를 거칩니다.

  1. *.py를 Python Virtual Machine(PVM)이 이해할 수 있는 Byte codes 형태로 컴파일
  2. 컴파일된 Byte codes를 PVM이 단계별로 실행

  이 과정은 사용자에게는 보이지 않기 때문에 소스코드에서 직접 실행되는 것처럼 보이지만(인터프리터), 내부적으로는 Byte code로 번역이 됩니다(컴파일러). 이런 방식 자체는 새로운 것은 아니고, Java나 V8 같은 곳에서 쓰이는 방식입니다.

  이렇게 두 단계로 나누었을 때 장점은 무엇일까요? 컴파일러든 인터프리터든 수행을 위해서는 소스코드를 실제 기계(VM일 수도, 실제 CPU일 수도 있습니다)가 이해할 수 있는 형태로 번역하는 과정이 필요합니다. CPU가 소스 코드를 이해할 수는 없으니까요. 인터프리터라고 하더라도 내부적으로는 위 두 단계를 매번 해야 하는 셈이죠. 그리고 이 중 특히 소스코드를 기계가 이해할 수 있는 형태로 번역하는 과정에 시간이 오래 걸립니다. 그래서 나온 아이디어가 1번 단계가 끝나고 나면, 결과를 임시파일(*.pyc)로 저장해 두고 다음부터는 1번을 생략하고 2번으로 바로 가자입니다.    

2. pyc 파일 생성

  하지만 세상이 그리 호락호락한가요. 이런 저런 문제들이 생깁니다.

 

  첫번째 문제는 Byte Code가 Python 버전마다 다릅니다. 예를 들어 Python 3.1에서 만들어진 *.pyc 파일은 Python 3.2와는 호환되지 않을 수 있습니다. 만약에 Python 3.1을 써서 Python 프로그램을 실행했는데 (그럼 Python 3.1용 *.pyc), 나중에 Python을 업데이트해서 Python 3.2로 같은 프로그램을 실행하면 문제가 생길 겁니다. 왜냐면 이미 *.pyc 파일이 있기 때문에 새로 생성하지 않고 있는 *.pyc를 쓸 텐데 이 파일의 내용(Byte code)이 Python 3.2와 호환되지 않기 때문입니다.

  이 문제를 해결하기 위해서 Python은 *.pyc를 생성할 때 사용한 Python의 버전을 파일 이름에 포함시킵니다. magic tag라고 부르는 표시인데요. 예를 들어 mymodule.py를 CPython 3.7을 사용해서 컴파일했다면 mymodule.cpython-37.pyc와 같은 형태가 됩니다. 즉, 같은 *. py이더라도 하더라도 실행할 때 사용한 Python 버전에 따라 mymodule.cpython-36.pyc, mymodule.cpython-37.pyc와 같이 여러 *.pyc 파일이 있을 수 있습니다.

 

  또다른 문제는 소스 파일 (*.py)과 *.pyc 파일의 내용이 일치하지 않을 수 있다는 점입니다. 최초 프로그램을 실행한 뒤에 (*.pyc가 생성된 뒤에) *.py를 고치고 다시 실행을 했다고 가정해보겠습니다. 이미 해당하는 *.pyc 파일이 있기 때문에 최신 내용을 담은 *.py는 해석되지 않고, 예전 내용인 *.pyc가 실행될 겁니다.

  이 문제를 해결하기 위해서 Python은 *.py와 *.pyc의 최근 수정 시간을 확인하고, 만약에 *.py 파일이 더 최신 파일이라면 *.pyc를 새로 생성합니다.  

 

  마지막 문제는 원래 작업 공간의 일부가 아닌 *.pyc 파일이 생기다 보니 작업 공간이 좀 정신없어질 수 있습니다. Python의 해결 방법은 생성된 *.pyc 파일들을 __pycache__라는 디렉터리를 만들어서 그것에 다 모아두는 것입니다. __pycache__는 패키지별로 생성됩니다.

3. pyc 파일 로딩

  Python이 *.py를 실행시킬 때 간략한 알고리즘을 통해서 *.pyc를 새로 만들거나 찾아서 로딩합니다.

  1. sys.path를 따라가면서 *.py를 찾습니다. (알쏭달쏭 Python import - sys.path 참조)
  2. *.py를 찾았다면,
    1. __pycache__에 *.py에 맞는 (magic tag, 수정 시간) *.pyc 파일이 있는지 확인합니다. 있다면 *.pyc를 로딩하고 끝.
    2. *.py에 맞는 *.pyc가 없는 경우 (magic tag, 수정 시간) *.py를 읽어서 해석 후 *.pyc로 저장합니다. __pycache__ 디렉터리가 없다면 __pycache__ 디렉터리도 함께 만듭니다. 이미 Byte code가 로딩되어 있으므로 끝.
  3. *.py는 없지만 *.py에 맞는 *.pyc가 있는 경우,
    1. *.pyc가 __pycache__에 있다면, ImportError를 발생시킵니다. (즉, 소스 없는 __pycache__/*.pyc는 없는 취급...)
    2. *.pyc가 *.py와 같은 디렉터리에 있다면 *.pyc를 로딩하고 끝.

   3.1과 3.2과 조금 애매합니다. 왜 *.pyc가 위치한 디렉터리에 따라 다르게 처리를 할까요? 

4. Python 2 vs Python 3

  위에서 3.1은 원본 소스가 없다면 컴파일된 *.pyc는 무시하겠다는 의미이고, 3.2는 원본 소스가 없다더라도 컴파일된 *.pyc를 사용하겠다는 의미입니다. 서로 모순된 정책처럼 보이는데요. 이유는 Python 2에 있습니다.

  Python 2 시절에도 *.pyc가 있었지만, 그 당시에는 __pychche__가 없었습니다. *.cpython-37.* 같은 magic tag도 없었고요. Python 2에서 *.pyc는 *.py와 같은 디렉터리에 생성됐고 magic tag가 없이 그냥 *.py 대신에 *.pyc로 생성됐습니다. (mymodule.py → mymodules.pyc). 이 때문에 "2. pyc 파일 생성"에서 언급한 버전, 작업 영역 문제들이 생겼고, Python 3에서 이 문제들을 해결하기 위해서 __pycache__와 magic tag를 도입했습니다.

  Python 2 때는 *.py가 없더라도 *.pyc가 (같은 디렉터리에) 있다면 *.pyc를 대신 로딩했습니다. 이 때문에 *.py를 생략하고 *.pyc만 배포하는 사례들이 생겼습니다 (source-less distribution). 이런 경우가 꽤 많기 때문에 Python 3에서도 이를 지원하기 위해서 3.2 경우를 제공하고 있습니다.

5. 수동으로 컴파일하기: py_compile, compileall

  기본적으로 *.pyc는 Python 모듈을 import 할 때 생성되지만, 배포 후 최초 실행 시 속도를 높이기 위해서 미리 생성할 수도 있습니다.

  첫번째 방법은 py_compile을 사용하는 것입니다. 아래와 같이 하면 *.py를 컴파일할 수 있습니다.

In [1]: import py_compile

In [2]: py_compile.compile('split_file.py')
Out[2]: '__pycache__/split_file.cpython-37.pyc'

  두번째 방법은 compileall을 사용하는 것입니다. compileall은 모듈 형태로 사용할 수도 있지만, -m 옵션으로 커맨드라인에서 사용할 수도 있습니다. (-m 실행 옵션과 __name__

python3 -m compileall .

  이렇게 실행하면 현재 디렉터리 내의 모든 *.py를 *.pyc로 변환해서 적절한 __pycache__에 저장합니다.

'Python' 카테고리의 다른 글

*args, **kwargs  (2) 2020.06.23
Python Reflection 맛보기  (0) 2020.06.09
-m 실행 옵션과 __name__  (5) 2020.05.28
venv는 내부적으로 어떻게 작동할까?  (2) 2020.05.19
bad magic number in 'application': b'\x03\xf3\r\n': ImportError  (0) 2020.05.19