Python의 전처리기
(pydong.org)- Python은 첫 두 줄의 소스 인코딩 매직 주석과 사용자 정의 codec을 이용해, 파일 내용을 실행 전에 바꾸거나 완전히 다른 코드로 대체할 수 있음
- 사용자 정의 codec은
.pth경로 설정 파일의import실행을 통해 인터프리터 초기화 중 등록할 수 있고,codecs.register로 검색 함수를 추가함 - codec 구현에는
decode(data: bytes) -> tuple[str, int]와 증분 디코더가 필요하며, 예외 처리를 하지 않으면 원인 대신SyntaxError: encoding problem: your_codec만 보일 수 있음 ++/--증감 연산자, 중괄호 기반 Python,cppyy를 통한 C/C++ 실행, TOML을 JSON Schema로 검증하는 방식까지 같은 진입점으로 구현할 수 있음- 장난성 예제를 넘어
pythonql,future-typing,future-fstrings,future-annotations같은 Python 확장·백포트에도 쓰일 수 있으며,magic_codec은 반복 작업을 줄여줌
소스 인코딩을 전처리 진입점으로 쓰기
- PEP-0263에 따라 Python 파일의 첫 두 줄 중 하나에 소스 코드 인코딩을 지정할 수 있음
- 예:
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- 예:
- 매직 라인은 정규식
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)에 맞아야 함- codec 이름은
[-_.a-zA-Z0-9]+에 맞아야 함
- codec 이름은
- 사용자 정의 codec은 소스를 디코딩하는 데서 그치지 않고, 소스 문자열을 바꾼 뒤 Python 인터프리터에 전달할 수 있음
.pth 파일로 codec 등록하기
- Python 인터프리터가
-S옵션 없이 시작되면site패키지가 초기화 중 로드됨 site-packages안의.pth경로 설정 파일은 빈 줄과#로 시작하는 줄을 제외한 내용을 모듈 검색 경로에 추가함- Python 문서에 따르면
import뒤에 공백이나 탭이 오는 줄은 실행됨- 예:
packagename.pth에import packagename.register_codec를 넣으면 초기화 중 해당 모듈이 import됨
- 예:
- import된 모듈은
codecs.register를 호출해 codec 검색 함수를 등록할 수 있음- import는 한 번만 실행되므로 검색 함수도 한 번만 등록됨
사용자 정의 codec 구현
- 사용자 정의 codec에는 두 가지가 필요함
decode(data: bytes) -> tuple[str, int]- 증분 디코더 클래스
decode함수는codecs.utf_8_decode로 실제 UTF-8 디코딩을 수행하고, 결과 문자열을 전처리 함수에 넘길 수 있음- codec 내부 예외가 잡히지 않으면 일반적인 traceback 대신
SyntaxError: encoding problem: your_codec만 출력될 수 있음- 전처리 함수에서 발생한 예외는
traceback.print_exc()로 직접 출력한 뒤 다시 raise하는 편이 좋음
- 전처리 함수에서 발생한 예외는
- 증분 디코더는 파일 전체를 버퍼에 모아두고 마지막
decode호출에서 한 번만 전처리할 수 있음- 예시 구현은
codecs.BufferedIncrementalDecoder를 상속하고decode(self, data, final=False)에서final일 때만 처리함
- 예시 구현은
- 전처리 결과는 원본 파일 내용을 꼭 사용할 필요가 없으며, 완전히 임의의 Python 코드를 반환해도 됨
- 단, 첫 줄은 매직 라인으로 기대되기 때문에 삭제되고, 결과는 유효한 Python이어야 함
Python 문법 확장 예제
-
++와--증감 연산자- Python에는 단항 증감 연산자가 없음
x++,x--는 문법적으로 유효하지 않음++x,--x는 문법적으로는 유효하지만 각각x.__pos__().__pos__(),x.__neg__().__neg__()호출이 됨- 전처리기는 토큰 스트림을 바꿔 증감 연산자처럼 동작하게 만들 수 있음
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- 이 변환은 Python의 할당 표현식인 바다코끼리 연산자(walrus operator) 를 사용함
- 단순 토큰 치환만으로는
x++ - -y같은 표현에서 실패할 수 있으며,x++ - (-y)처럼 괄호로 모호성을 줄일 수 있음 - incdec.py는 정규식으로 치환하지만 문자열 리터럴 내부 치환을 피하려 해도 취약할 수 있음
- 토큰 스트림을 직접 수정하는 구현은 magic.incdec에 있음
-
중괄호 기반 Python
from __future__ import braces는SyntaxError: not a chance를 발생시킴- 전처리기는 토큰 스트림을 수정해 중괄호 스코프를 들여쓰기 기반 Python으로 바꿀 수 있음
- 구현 흐름은 다음과 같음
tokenize.generate_tokens로 토큰을 생성함- 입력 문자열은
io.StringIO의readline으로 한 줄씩 제공함 - 기존
INDENT,DEDENT토큰은 제거함 {를 만나면 들여쓰기 레벨을 올리고:를 출력함}를 만나면 들여쓰기 레벨을 내림NL뒤에는 현재 들여쓰기 레벨에 맞는INDENT토큰을 추가함
- Python의 딕셔너리 리터럴과 충돌을 줄이려면
{뒤가 newline일 때만 들여쓰기 레벨을 조정하고,}앞이 newline일 때만 닫는 스코프로 처리할 수 있음 - 여러 줄 딕셔너리는 백슬래시를 쓰면 중괄호 내부에 newline 토큰이 생기지 않아 사용할 수 있음
다른 언어를 Python으로 실행하기
-
C와 C++
#로 주석을 쓰는 셸 스크립트, CMake 스크립트, PHP, Ruby 같은 언어는 shebang과 함께 인코딩 매직 라인을 넣기 쉬움- C와 C++는 주석이
/* ... */또는// ...이지만, 전처리 지시문이#로 시작하므로 인코딩 정규식에 맞출 수 있음 - 예시 매직 라인은 C/C++ 소스에서도 유효하면서 Python의 인코딩 패턴에도 맞음
#define CODEC "coding:magic.cpp"
- cppyy를 사용하면 Python에서 C/C++ 코드를 해석하고 바인딩을 생성할 수 있음
cppyy는 내부적으로cling을 사용함
- 전처리 결과는 대략 다음 Python 코드가 됨
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main__name__ == "__main__"일 때 C/C++의main()호출
- 예시 구현은 magic.cpp에 있음
TOML 검증 도구로 쓰기
- TOML은
#로 주석을 시작하므로# coding: magic.toml같은 인코딩 매직 라인을 넣을 수 있음 - 전처리 결과를 Python 검증 스크립트로 바꾸면 Python 인터프리터를 TOML 검증 도구처럼 사용할 수 있음
- 검증 예시는 다음 모듈을 사용함
tomllib로 TOML 파일을 읽음json으로 JSON Schema 파일을 읽음- jsonschema로 검증함
- 실행 예:
python tests/toml/data_valid.toml -s tests/toml/schema.json- 유효하면
Successfully validated.를 출력함
- 잘못된 TOML 예시에서는
scores배열 안의 문자열'20'이 숫자가 아니라는 검증 오류가 출력됨 - 구현 예시는 magic.toml에 있음
실제 활용과 magic_codec
- 사용자 정의 codec과
.pth파일을 함께 쓰면 Python 인터프리터의 동작을 크게 바꿀 수 있음 - 예제 대부분은 재미를 위한 것이지만, 실제 활용 사례도 있음
- pythonql: Python용 쿼리 언어 확장
- future-typing: Python 3.6+에 제네릭 타입 힌트와
|union 문법을 백포트 - future-fstrings
- future-annotations
site-packages를 직접 건드리거나.pth파일과 반복 코드를 직접 작성하지 않으려면 magic_codec를 사용할 수 있음magic_codec확장은magic_접두사가 붙은 Python 패키지로 만들 수 있음- 파일 codec을
magic_foo로 설정하면magic_foo패키지를 로드함 - 해당 패키지에
preprocess함수가 있는지 확인함
- 파일 codec을
- 기대되는
preprocess시그니처는 다음과 같음def preprocess(data: str) -> str:
- 확장 예시는 example/에 있음
댓글과 토론
Hacker News 의견들
-
from __future__ import braces를 실행하면SyntaxError: not a chance가 나오는 이 웃긴 오류 문구는 CPython에 2001년부터 하드코딩되어 있었음
https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
작성자인 Jeremy Hylton은 지금 Google에서 AI 검색 품질을 담당하는 Principal Engineer로 일하고 있는데, 24년 사이에 금지된 문법을 장난스럽게 기념하던 일에서 전용 문법이 필요 없는 보편적 질의 시스템으로 커리어가 이어진 게 꽤 인상적임break rust;가 Rust 컴파일러에서 내부 컴파일러 오류를 내던 일이 떠오름. 다른 언어에도 비슷한 이스터에그가 얼마나 있을지 궁금함- 그게 왜 놀라운지 모르겠음. 2001년에 아무나 Python에 뭔가를 넣을 수 있었던 게 아니라, 당시엔 틈새 프로젝트였고 기여하던 사람은 똑똑하고 헌신적이라 영향력 있는 커리어를 가질 가능성이 높았을 것임
장난스럽고 비공식적인 취미 해킹이 실제 전문 개발과 별개의 세계라는 건 오해라고 봄 - 순진한 시절이었음. Hylton이 Tim Peters의 정의를 위해 불신임 투표 움직임에 동참할 수도 있겠음
https://news.ycombinator.com/item?id=41314393 - 이런 이스터에그를 보는 건 늘 멋짐. 예전만큼 흔하지 않아져서 아쉬움
-
import hook으로 장난치는 게 해고당하는 가장 창의적인 방법이라고 생각했는데, 그건 순진한 생각이었음. codec 정규식 때문에
μtf8같은 걸로 제대로 트롤링하지 못할 것 같아 아쉽고, 이제 import hook, 전처리기,sys.settrace를 써서 모든 함수를 직전에 호출된 함수로 몽키패치하고 17분마다 stdout과 stderr를 바꾸는 수밖에 없겠음- 좋은 언어답게 중괄호 사용도 강제해야 함
-
Python이 전처리기 훅을 일부러 드러내지 않은 데는 좋은 이유가 있고, 합리적인 어른이라면 멀리해야 한다고 생각하는 부분이 있음
하지만 다른 한편으로는 합리적인 어른들과는 엮이고 싶지 않음. 정말 재미있는 짓을 많이 할 수 있을 듯함- Python은 “동의한 성인들(consenting adults)”을 위한 언어라는 철학이 있고, 그래서
public/private가시성 지정자가 없으며 아마 온갖 메타프로그래밍 마법도 열어둔 듯함. “합리적인 어른”을 걱정했다면 이런 설계 결정들은 좀 이상했을 것임 :) - 사람들이 이걸 쉽고 명백한 방식으로 못 하게 막으면, 더 나쁘고 해키한 방식으로 시도할 것임
- Python은 “동의한 성인들(consenting adults)”을 위한 언어라는 철학이 있고, 그래서
-
이건 편리하고 정말 유용해 보임. 말도 안 되는 import 해킹을 할 때는 보통 모듈을 import해서
ast모듈로 코드를 고쳐 쓰고exec한 뒤exit()을 끼워 넣었는데, 전처리기가 있으면 훨씬 쓰기 좋겠음
dict가 모두 순서를 보장하기 전에는ast재작성으로 리스트 리터럴을 ordered dict 호출로 바꾸는 데 주로 썼고, 실제로 유용했음
Python의 유연함이 좋음. 내가 한 가장 저주받은 짓은 문자열을 제자리에서 변경한 것이고, 결국mmap까지 남용해서 스스로를 변경하는 스크립트를 쓰게 됐음. 이제 producer로 Lisp 인터프리터를 작성해야 할 것 같은 느낌임- “문자열을 제자리에서 변경”했다니, 문자열은 불변임.
ctypes같은 걸로 메모리 위치에 직접 쓴 건가?
- “문자열을 제자리에서 변경”했다니, 문자열은 불변임.
-
지금까지 찾은 최고의 사용처는 JSX에서 영감을 받은 pyxl임: https://github.com/dropbox/pyxl
이런 코드를 쓸 수 있음
# coding: pyxl
print Hello World! -
이게 Python 2에서 3으로의 전환을 더 잘 처리하는 데 쓰일 수 있었는지 궁금함. 예를 들어
# coding: six.python2가 Python 2 코드를 유효한 Python 3 코드로 맞춰 주거나,# coding: six.python3가 Python 3 코드를 Python 2에서 돌게 바꾸는 식임
b"..."나u"..."접두사를 추가하거나 제거하는 것도 가능했을 듯함- 도움이 될 수는 있지만, 도움이 되는 부분은 쉬운 부분임. Python 2에서 3으로의 어려움은 런타임 동작 변화였음. Python 2에서는 ASCII를 담은 Unicode와 일반 문자열이 같은 문자열처럼 동작해서
dict키로 쓰면 같은 항목을 가리켰지만, Python 3에서는 같은 ASCII 내용의bytes와str이 같은dict안에서 서로 다른 항목을 가리킴
더 까다로운 변화도 있음..keys()와.values()같은 여러 내장 기능은 Python 2에서는 리스트를 반환하지만 Python 3에서는 반복자를 반환함.six유틸리티나 다른 우회책으로 코드를 안전하게 번역하려 하면 코드가 매우 장황해짐. 대부분은 한 번만 쓰이지만 가끔은 두 번 쓰이는 경우가 있기 때문임
import 시점에 코드를 고쳐 쓸 수 있는 도구가 있다면, 차라리 변환된 코드를 커밋하고 점진적으로 정리하는 편이 낫다고 봄. 어려운 부분은str대bytes처럼 멀리 떨어진 코드까지 영향을 주는 동작 변화임
- 도움이 될 수는 있지만, 도움이 되는 부분은 쉬운 부분임. Python 2에서 3으로의 어려움은 런타임 동작 변화였음. Python 2에서는 ASCII를 담은 Unicode와 일반 문자열이 같은 문자열처럼 동작해서
-
이 coding hook 전략으로 생긴 의존성은
pip freeze나uv가 잡아내나?
아니라면 즐거운 시간이 될 듯함 :). 누군가 이런 걸 넣어놨다면 또 어떤 함정이 있을지 거의 보장된 셈이라, 그런 용들과 싸우느니 라이브러리를 다시 쓰는 게 더 쉬울 것 같음 -
의사코드 Python을 만들고, 그걸 LLM이 디코딩하게 하면 꽤 웃길 것 같음. 당연히 끔찍하겠지만 재미는 있을 듯함