每一篇论文里都有这样的数字:它在别处算出来——一段脚本、一个笔记本、一张电子表格——然后被手工敲进正文。脚本一改,正文里的那个数字就悄悄变成了假话。PythonTeX 正是用来堵上这道缝的宏包:它在排版过程中执行你写在 LaTeX 源码里的 Python,并把返回的结果就地排出来。作者是 Geoffrey M. Poore;虽然名字里只有 Python,它同样能驱动 Ruby、Julia、R、Octave、Bash、Rust、Perl 和 JavaScript。本页从 \usepackage{pythontex} 讲起,依次是 \py 系列命令、几乎人人都要栽一次的三步构建,以及那个「不该用它」的场合。
「展示代码」的 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
% 4三步构建,以及为什么不需要 -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 document这里有一个让熟悉 minted 的人意外的事实: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 会察觉并自动重新编译。这里同样用不上 shell escape——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,三步的形状都不变。但代码里若含非 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),下一次运行时只执行发生变化的片段。改一个段落的错别字,不会让那段要跑三十秒的模拟重新来过。
「变化」的判定标准可以用 --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() 换算成英寸,就能在 Python 里做出 宽度与版心严丝合缝的图。由于事后不再缩放,图中的文字与正文的文字大小一致。
\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 造出来的,只要把三步走完,第二遍 LaTeX 自然会把它放进去。看到这一行别以为自己配错了就掉头,这是入门时值得知道的第一个诀窍。
公式那一侧另有专门的家族。只要把基底名 py 换掉,就能得到阵容完全相同的一组:\sympy、sympycode、sympyblock,以及 \pylab、pylabcode、pylabblock。不同之处只在最初的 import,以及结果的呈现方式。
- sympy 系 — 用
from sympy import *载入符号计算库 SymPy。用\sympy插入的表达式会经过 SymPy 的LatexPrinter,按上下文(行内还是独立成行)整理成合适的 LaTeX 写法。整张导数与积分表可以就此自动生成。 - pylab 系 — 用
from pylab import *载入 matplotlib 的pylab模块,把绘图和 NumPy 收进同一个命名空间。若你更愿意像上面的例子那样自己写 import,用普通的py系就够了。
当投稿系统构建不了时:depythontex 与安全性
PythonTeX 真正的限制在这里。只跑 LaTeX 引擎的处理链路,永远做不完这份文档。 缺的不是 shell escape 的许可,而是本该夹在中间的那一次 pythontex 运行。手册自己也承认:用了 PythonTeX 的文档,在投稿、共享和转换成其他格式这几件事上,都不如素的 LaTeX 文档好办。depythontex 正是为此而设。用 \usepackage[depythontex]{pythontex} 构建,会生成辅助文件 <jobname>.depytx;depythontex 脚本把它与原稿对照,写出另一份 .tex,其中 所有 PythonTeX 命令和环境都被替换成排版好的代码及其输出——一份把结果烘焙进去、完全不依赖 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 之外而已。