어느 논문에나 다른 곳에서——스크립트, 노트북, 스프레드시트에서——계산된 뒤 손으로 본문에 옮겨 적은 숫자가 들어 있습니다. 스크립트를 고치면 본문의 그 숫자는 조용히 거짓이 됩니다. PythonTeX는 바로 그 틈을 메우는 패키지로, LaTeX 소스 안에 쓴 Python을 조판 도중에 실행하고 돌아온 값을 그 자리에 조판합니다. 만든 사람은 Geoffrey M. Poore이며, 이름과 달리 Ruby, Julia, R, Octave, Bash, Rust, Perl, JavaScript도 구동할 수 있습니다. 이 페이지는 \usepackage{pythontex}에서 시작해 \py 계열 명령, 누구나 한 번은 걸리는 3단계 빌드, 그리고 “쓰지 말아야 할 상황”까지 차례로 살펴봅니다.
코드를 “보여 주는” listings와 코드를 “실행하는” PythonTeX
listings와 minted는 코드를 보이는 모양 그대로 조판할 뿐 한 줄도 실행하지 않습니다. PythonTeX가 다른 점은 코드를 실행하고 돌아온 문자열을 본문에 조판한다는 것입니다. 본문에 \py{2**10}이라고 쓰면 조판되는 것은 2**10이라는 글자가 아니라 계산 결과인 1024입니다. 불러오는 데는 \usepackage{pythontex} 한 줄이면 되고, 실제로 돌리려면 TeX 배포판과 함께 Python 본체가, 그리고 코드 색칠에 쓰는 Pygments가 설치되어 있어야 합니다.
\documentclass{article}
\usepackage{pythontex}
\begin{document}
% executed, but nothing is typeset from this block itself
\begin{pycode}
from math import sqrt
radius = 2.5
area = 3.14159 * radius**2
\end{pycode}
A circle of radius \py{radius} has area \py{round(area, 2)}.
\[ 2^{10} = \py{2**10}, \qquad \sqrt{3^2+4^2} = \py{sqrt(3**2 + 4**2)} \]
\end{document}이 문서는 “A circle of radius 2.5 has area 19.63.”으로 조판되고, 이어서 2¹⁰ = 1024와 √(3²+4²) = 5.0이 들어갑니다. 여기서 중요한 것은 19.63이라는 숫자가 원고 어디에도 적혀 있지 않다는 점입니다. radius를 3.0으로 바꾸고 다시 빌드하면 본문의 반지름과 넓이가 알아서 따라옵니다. 손으로 적은 숫자라면 한 곳쯤 고치는 것을 잊게 마련이지만, 이 방식에는 잊을 대상 자체가 없습니다. 논문의 숫자가 그 뒤의 코드와 어긋나지 않음을 이만한 비용으로 보장해 주는 수단은 그리 많지 않습니다.
이 발상 자체는 전혀 새롭지 않습니다. 도널드 커누스가 1984년에 내놓은 문학적 프로그래밍(literate programming) 의 도구 WEB은 Pascal 프로그램 안에 설명글을 쓰게 하고, tangle이 거기서 Pascal을, weave가 TeX을 뽑아내는 구조였습니다. PythonTeX가 하는 일은 그것을 뒤집은 것입니다. 주인이 되는 문서는 여전히 LaTeX이고, 그 안에 프로그램 쪽이 들어와 함께 삽니다. 방향이 어느 쪽이든 동기는 똑같습니다. 설명과 구현을 서로 다른 파일에 두면 언젠가는 반드시 어긋난다는 것입니다.
\py, \pyc, pycode, pyblock 가려 쓰기
이 이름들은 외우는 것이 아니라 두 가지 질문에 답하면 정해지는 것입니다. 그 코드를 실행할 것인가, 지면에 보여 줄 것인가. 두 답의 조합이 그대로 접미사가 됩니다. 기본 이름 py를 놓고 보면, 아무것도 붙이지 않으면 표현식의 값을 조판하고, c(code)는 실행만, v(verb)는 조판만, b(block)는 둘 다 합니다. 인라인에는 명령 형태(\pyc{…})를, 여러 줄에는 같은 이름의 환경(pycode)을 씁니다.
| 명령 | 대응하는 환경 | 실행과 조판 |
|---|---|---|
\py | — | 표현식을 실행하고 그 문자열 표현만 조판합니다 |
\pyc | pycode | 실행하지만 조판하지 않습니다. print 출력은 자동으로 들어옵니다 |
\pyv | pyverbatim | 실행하지 않고 코드를 그대로 조판합니다 |
\pyb | pyblock | 실행도 조판도 합니다. print 출력은 자동으로 들어오지 않습니다 |
\pys | pysub | !{expr}를 평가 결과로 바꾼 뒤 그 결과를 LaTeX으로 해석합니다 |
\pycon | pyconsole | 대화형 콘솔을 재현해 >>>와 입력・출력을 함께 조판합니다 |
인라인 명령의 인자는 \verb와 같아서 꼭 중괄호일 필요가 없습니다. 같은 문자를 한 쌍 맞추기만 하면 되므로 \py{2**10}, \py#2**10#, \py@2**10@은 모두 같은 뜻입니다. 코드 자체에 중괄호가 들어갈 때 이 우회로가 요긴합니다. 지켜야 할 제약은 딱 하나, \py는 값을 끼워 넣는 명령이라 대입을 쓸 수 없습니다. 설명서는 \py{a=1}을 명시적으로 무효로 규정하며, 이유는 대입에는 문자열 표현이 없기 때문입니다. 변수를 만드는 일은 pycode 쪽 몫이고, \py{a}는 그것을 불러오기만 합니다.
print의 처리 방식은 표가 암시하듯 코드를 보여 주는지 여부에 따라 뒤집힙니다. 코드를 감추는 쪽인 pycode, \pyc에서는 패키지 옵션 autoprint(기본 켜짐)가 작동해 출력이 그 자리에 흘러듭니다. 반대로 코드를 보여 주는 pyblock, \pyb에서는 자동 삽입이 멈추는데, 코드 바로 아래에 출력이 붙는 모양을 원하는 경우가 드물기 때문입니다. 출력을 내고 싶은 자리에 \printpythontex(또는 \stdoutpythontex)를 두어 명시적으로 가져오세요. \saveprintpythontex{name}으로 이름을 붙여 저장했다가 멀리 떨어진 곳에서 \useprintpythontex{name}으로 꺼내 쓸 수도 있습니다.
교재나 기술 문서에서 늘 필요한 것이 대화형 세션의 재현 입니다. pyconsole 환경은 내용을 인터프리터에 직접 입력한 것처럼 다루며, Python 자체의 code 모듈을 써서 입력과 출력을 번갈아 배치합니다. 아래 예는 >>> a = 1, >>> a + 3, 4의 세 줄로 조판되는데, 그 4는 여러분이 쓴 것이 아니라 빌드 중에 계산된 값입니다. 함수 정의처럼 여러 줄에 걸친 입력을 넣을 때는 마지막 줄 뒤에 빈 줄이 필요할 수 있습니다. 같은 계열에는 붙여 넣은 세션을 실행하지 않고 조판만 하는 \pyconv・pyconverbatim, 실행만 하고 조판하지 않는 \pyconc・pyconcode도 있습니다.
\begin{pyconsole}
a = 1
a + 3
\end{pyconsole}
% typeset result:
% >>> a = 1
% >>> a + 3
% 43단계 빌드와 -shell-escape가 필요 없는 이유
PythonTeX 문서는 LaTeX → pythontex → LaTeX 세 번 을 돌려야 완성됩니다. 첫 번째 LaTeX 실행은 본문의 코드를 실행하지 않고 <jobname>.pytxcode라는 외부 파일로 뽑아내기만 합니다. 이어서 pythontex 프로그램이 그 코드를 실행해 결과를 저장하고, 두 번째 LaTeX 실행이 저장된 결과를 주워 담아 PDF를 만듭니다. 한 번만 돌리면 애써 써 놓은 값이 어디에도 나오지 않는데, 이것이 누구나 처음 겪는 전형적인 실수입니다.
pdflatex document.tex # 1) LaTeX extracts the code to document.pytxcode
pythontex document.tex # 2) a separate program runs it and caches the results
pdflatex document.tex # 3) LaTeX pulls the results back into the documentminted를 아는 사람일수록 뜻밖으로 느낄 사실이 있습니다. PythonTeX에는 -shell-escape가 필요 없습니다. minted는 조판 도중에 LaTeX 자신이 외부 프로그램을 실행하므로 권한이 없으면 ! Package minted Error: You must invoke LaTeX with the -shell-escape flag.에서 멈춥니다(“소스 코드 목록” 참조). 반면 PythonTeX에서 코드를 실행하는 것은 LaTeX이 아니라 두 번의 LaTeX 실행 사이에 끼어드는 독립된 프로그램 입니다. LaTeX은 .pytxcode를 쓰고 나중에 결과를 읽어 올 뿐입니다. 실제로 pythontex.sty에는 \write18을 쓰는 곳이 한 군데도 없습니다.
이 “사이에 끼어드는” 설계에는 기분 좋은 부수 효과가 하나 더 있습니다. .pytxcode 파일에는 코드 조각뿐 아니라 그 조각이 .tex 파일의 몇 번째 줄에 있었는지 도 함께 기록됩니다. 덕분에 Python 쪽에서 문제가 생기면 pythontex는 생성된 .py의 줄 번호가 아니라 원고의 줄 번호로 알려 줍니다. pycode 블록 안에서 정의되지 않은 이름을 쓰면 * PythonTeX stderr - error on line 8:이 뜨고 이어서 NameError: name 'nosuchname' is not defined가 나옵니다. 여기서 8은 .tex의 8행입니다. 생성된 파일을 열어 줄을 세어 볼 일이 없습니다.
매번 세 명령을 손으로 치는 것은 현실적이지 않으므로 실무에서는 latexmk에 맡깁니다. 설명서가 제시하는 구성은 추출된 코드 파일 .pytxcode를 의존 관계로 등록하고, 그것이 바뀌면 pythontex를 돌리는 방식입니다. pythontex가 결과 파일을 다시 쓰면 latexmk가 이를 감지해 알아서 재컴파일합니다. 여기서도 셸 이스케이프는 등장하지 않습니다. latexmk는 pythontex를 평범한 외부 명령으로 호출할 뿐이기 때문입니다.
# run pythontex whenever the extracted code changes
add_cus_dep('pytxcode', 'tex', 0, 'pythontex');
sub pythontex { return system("pythontex \"$_[0]\""); }엔진은 가리지 않습니다. pdflatex를 lualatex나 xelatex로, 일본어 문서라면 platex로 바꾸어도 3단계라는 형태는 그대로입니다. 다만 코드에 비 ASCII 문자가 들어간다면 문서 쪽 설정이 필요하고, 설명서는 이를 명확히 정해 두었습니다. pdfLaTeX에서는 \usepackage[T1]{fontenc}와 \usepackage[utf8]{inputenc}, LuaLaTeX에서는 \usepackage{fontspec}, XeLaTeX에서는 거기에 더해 \defaultfontfeatures{Ligatures=TeX}입니다. XeLaTeX에만 있는 함정이 하나 있는데, 코드에 탭이 들어가면 -8bit를 붙여 컴파일해야 하며 그러지 않으면 탭이 ^^I라는 문자열로 쓰여 나갑니다.
다시 빌드해도 빠른 이유: 캐시, 세션, --rerun
바뀌지 않은 코드는 실행되지 않습니다. 무거운 계산을 문서에 심는다는 무모해 보이는 발상을 실용적으로 만들어 주는 핵심이 바로 이것입니다. pythontex는 결과를 pythontex-files-<jobname>/ 아래에 저장하고(캐시 본체는 pythontex_data.pkl), 다음 실행에서는 변경된 조각만 돌립니다. 한 문단의 오타를 고쳤다고 30초짜리 시뮬레이션이 다시 돌아가는 일은 없습니다.
무엇을 “바뀐 것”으로 볼지는 --rerun으로 조절할 수 있고, 같은 뜻의 패키지 옵션 \usepackage[rerun=…]{pythontex}도 있습니다. 기본값은 errors로, 수정된 조각에 더해 지난번에 오류를 낸 조각도 실행합니다. 디버깅 중에 손대지 않아도 실패한 블록이 다시 시도되는 이유가 이것입니다. 문턱값은 단계적으로 고를 수 있습니다.
never— 아무것도 실행하지 않습니다. 변경된 코드가 있으면 경고만 냅니다.modified— 변경된 조각(그리고 의존 대상이 바뀐 조각)만 실행합니다.errors— 기본값. 변경된 것에 더해 지난번에 오류가 난 것도 실행합니다.warnings— 여기에 더해 지난번에 경고를 낸 것도 다시 실행합니다.always— 언제나 전부 실행합니다. 명령줄의--runall과 거의 같습니다.
캐시의 약점은 코드는 그대로인데 그 코드가 읽는 데이터가 바뀐 경우입니다. Python 쪽에서 pytex.add_dependencies('data.csv')로 선언해 두면 그 파일이 갱신될 때만 해당 블록이 자동으로 다시 실행됩니다(기본은 수정 시각으로 판정하며, --hashdependencies로 해시 방식으로 바꿀 수 있습니다). 반대로 만들어 낸 파일은 pytex.add_created()로 등록해 두면 뒷정리 대상이 됩니다. 또한 세션은 병렬로 실행됩니다. \begin{pycode}[sessionname]으로 나눈 세션은 별도 프로세스가 되고, 동시 실행 수는 기본적으로 CPU 코어 수입니다(--jobs로 변경). 그래도 앞뒤가 맞지 않으면 설명서가 권하는 최후의 수단은 pythontex-files-<jobname>/를 통째로 지우고 다시 빌드하는 것입니다.
matplotlib 그림과 SymPy 수식을 문서에 흘려 넣기
그림을 만드는 방법은 아주 직설적입니다. pycode 안에서 matplotlib에 savefig를 시키고, 그 파일을 \includegraphics로 넣으면 끝입니다. 기본 저장 위치가 .tex 옆이므로 경로를 고민할 필요가 없습니다(바꾸고 싶다면 \setpythontexworkingdir가 있습니다). 재미있는 부분은 그다음입니다. \setpythontexcontext{textwidth=\the\textwidth}라고 써 두면 LaTeX 쪽 치수가 Python 쪽으로 건너가 pytex.context.textwidth로 읽히고, pytex.pt_to_in()으로 인치로 바꾸면 판면 폭에 정확히 들어맞는 그림 을 만들 수 있습니다. 나중에 확대·축소하지 않으므로 그림 속 글자가 본문 글자와 같은 크기로 나옵니다.
\documentclass{article}
\usepackage{graphicx}
\usepackage{pythontex}
\setpythontexcontext{textwidth=\the\textwidth}
\begin{document}
\begin{pycode}
import matplotlib
matplotlib.use('pgf')
import matplotlib.pyplot as plt
import numpy as np
width = pytex.pt_to_in(pytex.context.textwidth)
x = np.linspace(0, 2*np.pi, 200)
fig, ax = plt.subplots(figsize=(width, 0.4*width))
ax.plot(x, np.sin(x))
fig.savefig('wave.pdf', bbox_inches='tight')
\end{pycode}
\includegraphics{wave.pdf}
\end{document}여기에 첫 빌드에서 거의 반드시 마주치는 함정이 있습니다. 첫 번째 LaTeX 실행 시점에는 wave.pdf가 아직 없으므로 ! Package pdftex.def Error: File 'wave.pdf' not found: using draft setting.라는 말을 듣게 됩니다. 망가진 것이 아닙니다. 그림을 만드는 것은 두 번째 단계인 pythontex이므로, 3단계를 끝까지 돌리면 두 번째 LaTeX 실행에서 제대로 들어갑니다. 이 한 줄을 보고 설정을 잘못했다고 여겨 되돌아가지 않는 것이 처음 익힐 요령입니다.
수식 쪽에는 전용 가족이 따로 마련되어 있습니다. 기본 이름 py를 바꾸기만 하면 똑같은 구성이 다시 손에 들어옵니다. \sympy, sympycode, sympyblock과 \pylab, pylabcode, pylabblock입니다. 다른 것은 맨 처음의 import와 결과를 보여 주는 방식뿐입니다.
- sympy 계열 — 기호 대수 라이브러리 SymPy를
from sympy import *로 불러옵니다.\sympy로 삽입한 식은 SymPy의LatexPrinter를 거쳐 인라인인지 별행인지에 따라 문맥에 맞는 LaTeX 표기로 정리됩니다. 도함수와 적분 표를 통째로 자동 생성하는 묘기가 여기서 나옵니다. - pylab 계열 —
from pylab import *로 matplotlib의pylab모듈을 불러와 작도와 NumPy를 한 이름공간에 모읍니다. 위 예처럼 import를 직접 쓰는 편이 좋다면 평범한py계열로 충분합니다.
투고처가 빌드할 수 없을 때: depythontex와 보안 문제
PythonTeX의 진짜 제약은 여기에 있습니다. LaTeX 엔진만 돌리는 처리 계통에서는 이 문서가 결코 완성되지 않습니다. 부족한 것은 셸 이스케이프 권한이 아니라 중간에 끼어야 할 pythontex 실행 그 자체입니다. 설명서 스스로도 PythonTeX를 쓴 문서는 투고, 공유, 다른 형식으로의 변환에서 순수 LaTeX 문서보다 다루기 어렵다고 인정합니다. 바로 그것을 위해 depythontex 가 있습니다. \usepackage[depythontex]{pythontex}로 빌드하면 보조 파일 <jobname>.depytx가 생기고, depythontex 스크립트가 이것과 원본 원고를 대조해 모든 PythonTeX 명령과 환경을 조판된 코드와 그 출력으로 바꾼 또 하나의 .tex를 써냅니다. 결과가 구워 넣어진, PythonTeX에 전혀 의존하지 않는 평범한 LaTeX입니다.
# 1) run the usual three steps, with the depythontex package option on
pdflatex document.tex
pythontex document.tex
pdflatex document.tex
# 2) write the static, PythonTeX-free copy
depythontex -o document-plain.tex document.tex
# code display in the output can be switched to another package
depythontex --listing minted -o document-plain.tex document.tex--listing은 눈에 띄지 않지만 요긴합니다. 정적 버전에서 코드를 어떻게 보여 줄지를 verbatim, fancyvrb, listings, minted, pythontex 중에서 고를 수 있으므로 “투고 규정이 listings를 요구한다” 같은 사정에도 그대로 맞출 수 있습니다(“소스 코드 목록” 참조). 더 가벼운 방법도 있습니다. 설명서는 그저 공저자에게 넘기기만 하면 되는 경우라면 pythontex.sty와 출력 디렉터리를 함께 건네면 된다고 적고 있습니다. 받은 쪽은 Python을 한 번도 돌리지 않고 Python 이외의 부분을 평범한 LaTeX 문서처럼 편집할 수 있습니다.
마지막으로 설명서가 경고 상자에 따로 적어 둔 한 가지입니다. PythonTeX를 쓴 문서를 컴파일한다는 것은 여러분의 컴퓨터에서 Python을(경우에 따라 다른 프로그램까지) 실제로 실행한다 는 뜻입니다. 따라서 컴파일해도 되는 것은 출처를 신뢰할 수 있는 문서뿐 입니다. -shell-escape가 필요 없다는 사실이 더 안전하다는 뜻은 아닙니다. 코드가 실행되는 것은 똑같고, 실행되는 자리가 LaTeX 바깥일 뿐입니다.