1. *.pyc 파일

Python으로 코딩을 하다 보면 내가 만들지 않은 *.pyc 파일들이 만들어져 있는 것을 볼 수 있습니다. 가끔은 *.pyc가 문제를 일으키기도 하고요.

*.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를 찾습니다.
  2. .py를 찾았다면,
    1. __pycache__에 *.py에 맞는 (magic tag, 수정 시간) *.pyc 파일이 있는지 확인합니다. 있다면 *.pyc를 로딩하고 끝.
    2. .py에 맞는 *.pyc가 없는 경우 (magic tag, 수정 시간) *.py를 읽어서 해석 후 *.pyc로 저장합니다. pycache 디렉터리가 없다면 pycache 디렉터리도 함께 만듭니다. 이미 Byte code가 로딩되어 있으므로 끝.