这篇文章上次修改于 434 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

什么是 Quine 程序?

「Quine 程序」与一个有趣的编程问题有关:

如何编写一个程序,让它打印出自己的源代码?

刚刚学习编程时,直觉上我觉得这似乎不太可能。我首先能想到的解决思路是:

打印这段话(
    "打印这段话"
)

输出如下:

打印这段话

显然不对。那么把打印函数也打印出来呢?

打印这段话(
    "打印这段话(

    )"
)

停在这个括号里,我就没再写下去了,因为问题已经出现了:即使再怎么递归下去,这样打印出的东西永远会少一层「打印这段话」。因此我武断地认为:这种问题正如同那些不幸涉及自指性的数学系统一样陷入了悖论的泥沼,是无解的问题。

但 Quine 程序向我指出了问题所在:我的实现思路不正确。Quine 的思路非常简单:

将下面这段话打印两遍,其中第二遍要打印在一对引号里:
「将下面这段话打印两遍,其中第二遍要打印在一对引号里:」

Quine 的关键在于使用一个短语作代指:「下面这段话」。这个代指短语巧妙地解决了递归的问题——我无需把已经说过的话再说一遍。

正如我们在一个话题中第二次提及某人时会用「他/她」这样的代词来代指,编程语言也有许多实现代指的方式。下面我将用 Python3 举例,写一个 Quine 程序。

实现思路

首先要知道,Python 这样的编程语言要求变量「先定义再使用」,因此我们把上面的描述调转一下:

str = 「将字符串 str 打印两遍,其中第一遍要打印成一个字符串定义。」
将字符串 str 打印两遍,其中第一遍要打印成一个字符串定义。

现在我们可以把这段代码翻译成 Python 了。代码还没写完,我们暂时不知道str的内容,因此先留空:

str = ""
print(str)

已有的代码当然要抄进去。

str = "str = ""\nprint(str)"
print(str)

换行符有点烦人,我们最后会用分号代替:Python 允许两句代码写在同一行,中间用分号隔开。字符串中间很明显出现了双引号,这会导致字符串提前被闭合,但我们最后再来解决这个问题。

「代词」在 Python 中是什么?你可能想到了.format()或者%,配合字符串中的%d%s就可以实现代指与替换。我们现在就来试试:

str = "str = "%s"\nprint(str % str)"
print(str % str)

在修改第二行时,记得把字符串里的对应部分一并修改。现在有点像是那么回事了,我们解决一下引号的问题,然后把代码放到同一行上去:

str = "str = \"%s\"; print(str % str)"; print(str % str)

转义符\"就可以避免提前结束字符串的问题了。好像写完了!我们运行一下试试:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: not enough arguments for format string

呃,报错了。我发现原因在于字符串里的str % str那里还有一个%,它似乎被当做格式化符处理了。把它改成%%就可以原样输出:

str = "str = \"%s\"; print(str %% str)"; print(str % str)

现在再来试试。程序输出:

str = "str = "%s"; print(str %% str)"; print(str % str)

成功了——不对!仔细一看,用来转义双引号\"的转义符\没有输出。我想到了 Python 也可以用单引号来表示字符串,但一番尝试后,输出总是和源代码不一样。

既然如此,干脆双引号也用%来替换掉好了:

str = "str = %c%s%c; print(str %% str)"; print(str % ('"', str, '"'))

%c表示输出一个字符。等等,我们还要把外面的代码抄进字符串里,如果还用str % ('"', str, '"')这样的表示法,双引号就又出现了。还是换成 ASCII 码来代替吧:

str = "str = %c%s%c; print(str %% (chr(34), str, chr(34)))"; print(str % (chr(34), str, chr(34)))

这样就可以消除掉外面代码里的引号了。运行试试:

str = "str = %c%s%c; print(str %% (chr(34), str, chr(34)))"; print(str % (chr(34), str, chr(34)))

成功了,输出结果和源代码一模一样!

更简单的解法

但还有更短的写法。什么,这是怎么做到的?

在 Python 中有一种表示方式:repr(),可以将一个表达式的「表达式」形式表达出来。这句话很拗口,我还是举个例子吧:

>>> repr('a')
"'a'"

repr()'a'本身包括两边的引号,当做一个字符串返回了。这也许可以解决我们遇到的引号问题!

%替换格式的字符串中,%r表示表达式的repr()形式。也就是说,在我们的代码中,字符串中写着%r的地方,会自动加上两边的双引号。这下方便多了!

str = 'str = %r; print(str %% str)'; print(str % str)

我把两边的引号换成了单引号,因为%r认为字符串的表达式默认用单引号括起来。输出结果如下:

str = 'str = %r; print(str %% str)'; print(str % str)

你可以把代码写得更短,比如去掉空格,再比如给str取个短点的名字。我决定取名_,因为一个下划线也是合法的变量名,但看起来很有极客感。

_='_=%r;print(_%%_)';print(_%_)

输出如下:

_='_=%r;print(_%%_)';print(_%_)

更——简单的解法

……什么?还有更简单的解法?

没错。代码如下:

输出如下:

空代码!既然在 Python 中空代码也算是合法的代码文件,那自然这也算是 Quine 的解。

我知道你现在的心情。这算是作弊了吧?但这段——呃——「代码」确实符合要求。它的作者 Szymon Rusinkiewicz 在当年的 IOCCC 大赛上因此获得了 Worst Abuse of the Rules 奖项。这段「代码」令我深感无力,仿佛我刚才的全部思考都不堪一击。这完全是另一个次元的思维方式。但我想这也正是研究这种问题的魅力所在:这不会让你的生产力提高,也不会让你的下一个程序运行更快,但它就是很酷,因为你永远也不知道将要面对什么令人兴奋的东西。