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 파일을 실행시킬 때 내부적으로 아래 두 단계를 거칩니다.
이 과정은 사용자에게는 보이지 않기 때문에 소스코드에서 직접 실행되는 것처럼 보이지만(인터프리터), 내부적으로는 Byte code로 번역이 됩니다(컴파일러). 이런 방식 자체는 새로운 것은 아니고, Java나 V8 같은 곳에서 쓰이는 방식입니다.
이렇게 두 단계로 나누었을 때 장점은 무엇일까요? 컴파일러든 인터프리터든 수행을 위해서는 소스코드를 실제 기계(VM일 수도, 실제 CPU일 수도 있습니다)가 이해할 수 있는 형태로 번역하는 과정이 필요합니다. CPU가 소스 코드를 이해할 수는 없으니까요. 인터프리터라고 하더라도 내부적으로는 위 두 단계를 매번 해야 하는 셈이죠. 그리고 이 중 특히 소스코드를 기계가 이해할 수 있는 형태로 번역하는 과정에 시간이 오래 걸립니다. 그래서 나온 아이디어가 1번 단계가 끝나고 나면, 결과를 임시파일(*.pyc)로 저장해 두고 다음부터는 1번을 생략하고 2번으로 바로 가자입니다.
하지만 세상이 그리 호락호락한가요. 이런 저런 문제들이 생깁니다.
첫번째 문제는 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__는 패키지별로 생성됩니다.
Python이 *.py를 실행시킬 때 간략한 알고리즘을 통해서 *.pyc를 새로 만들거나 찾아서 로딩합니다.