如何编写一个Quine

Hacker News Top 工具

摘要

解释如何编写一个Quine(一个在不使用自省的情况下打印自身源代码的程序),使用比喻和详细步骤。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/24 23:04

# 如何编写一个 quine 来源: https://czterycztery.pl/slowo/quine-EN.html 主页 (https://czterycztery.pl/#en)这是一篇关于如何编写 quine \(https://en.wikipedia.org/wiki/Quine_%28computing%29\) 的文章。quine 是一个不依赖内省(introspection)就能打印自身源代码的程序。我假设读者之前接触过 quine,甚至尝试编写过,并理解它们为何既困难又有趣。文章由两部分组成:第一部分通过一个寓言解释 quine 如何工作,第二部分详细描述如何编写一个 quine。 ## quine 如何工作 现在我来解释如何构建一台能够制造自身副本的机器。当然,最简单的方法是该机器观察自身,了解自己的构造,然后制造一个相同的副本。但能否制造一台不借助内省就能创建自身副本的机器? 首先,我会制造一台机器,只要给它一份计划,它就能制造任何东西。可以说,这样的机器在插入计划之前并非真正的机器,因为如果直接打开它,它什么也不做——但这点不重要。它仍然很酷:只需插入一份皮划艇的计划,你就得到一台制造皮划艇的机器;插入一份手推车的计划,你就得到一台制造手推车的机器,等等。那么,如果我们插入一份关于它自身的计划,能不能得到一台制造自身副本的机器呢?有一个问题:在计划中,当描绘计划持有者时,该画什么?持有者会是空的吗?如果是空的,那么根据这份计划制造的机器将无法制造任何东西(所以它并不是父机器的真正副本,因为父机器能制造子机器,但子机器却不能)。或者,计划中会显示这台机器精确的计划?这更好,但计划中再嵌套计划时,描绘持有者又该画什么?不幸的是,有两种可能:要么某个计划中的持有者是空的,意味着机器无法制造出完美的副本;要么我们将永远无法画完这份计划。 *我鼓励你在此暂停片刻。想象这一切,直到你感到适应。想象一台机器,只要在它的计划持有器中插入某物的计划,它就能制造该物。想象插入一张书桌(汽车、煎锅、耳机、手机)的计划,按下按钮,它便制造出来。现在想象绘制这台机器的计划,将这个计划插入计划持有器,然后启动机器。父机器产生了一个子机器;我们启动子机器,然后会发生什么?再来一个思想实验:我们绘制这台机器的计划,但这次计划持有器里放着一张书桌的计划,然后启动机器,子机器出来了,我们启动子机器,现在会发生什么?再做一个思想实验:我们绘制这台机器的计划,但这次计划持有器里显示的是这台机器的计划(带有一个空计划持有器)。然后启动机器,子机器出来了,我们启动子机器……你脑海中看到了什么?* 但你可以修改这台机器,让它工作方式略有不同:它先造出计划上画的东西,然后复制计划,在造出的物品上找到标有“这里”的位置,把副本插入进去。而在计划上,我们画上这台机器的精确计划,但计划持有器是空的,并标有“这里”。现在看看当我们启动机器时会发生什么:它会制造自己的副本(没有计划),复制计划,并将其插入计划持有器中。这样一来,它就创造了自身的精确副本。 如果你觉得我在作弊,因为复制计划是一种内省,如果你因为我没有明确定义哪些动作是禁止的内省、而哪些(如复制计划)不是而心烦意乱,那就先别管这些想法。这个关于机器的整个故事并不是一个独立的谜题。它甚至不必完全合理。它只是帮你更好地理解我稍后将写的 quine 为何那样构建以及如何工作。这个与机器的类比对我理解 quine(我将很快写出的那个)帮助很大,所以我把这个故事分享给你。 让我们注意一个细节,它对机器来说无关紧要,但对我实际编写 quine 时很重要:这台机器在造出子机器后,不能立即把它交给按下按钮的人——它必须先短暂持有它,以便插入一份计划的副本。 ### 机器工作方式总结 以下是这台机器工作方式的总结: 1. 有人按下按钮,机器启动 2. 根据计划,它制造出一台能够根据计划制造任何东西的机器 3. 它复制一份计划 4. 它将这份计划拷贝插入到它制造的机器中 5. 它将装备了计划的机器交给按下按钮的人 *我鼓励你在此暂停片刻。想象这一切,直到你感到适应。精确地、一步一步地、在脑海中形象化这台机器如何工作:它如何制造子机器,如何复制计划,如何将副本插入子机器,我们如何启动子机器,这个子机器做什么,我们如何启动孙机器……如此往复。* ## 编写一个 quine ### Python 中的简易 quine 现在我将编写一个 quine——一个打印自身(不使用内省)的程序。首先,我会用 Python 写一个简单的 quine 来展示如何编写 quine,然后我会写几个变体(Python 和其他语言),以展示可能出现的问题及解决方法。 首先,请注意,打印自身的程序是打印某物的程序的一个特例。所以我先写一个打印某物的程序。在 Python 中,最简单的方式是: `` print() `` 这是一个可以打印任何东西的程序——你只需将那个东西插入到括号中。在不插入任何东西时,这个程序是无用的(像没有计划的机器),但例如当我把词*Poland*插入括号中,我就得到了一个打印词*Poland*的程序: `` print('Poland') `` 其次,注意,打印自身的程序是一个打印一个打印自身的程序的程序。而这又是打印一个打印某物的程序的程序的一个特例。所以我们先写一个打印一个打印某物的程序的程序。在 Python 中,最简单的方式是: `` print('print()') `` 这是一个创建可以打印任何东西的程序的程序——你只需将那个东西插入到括号中。它就像一台制造能根据计划制造任何东西的机器的机器,但计划持有器是空的。只要括号中没有插入任何东西,这个程序就是无用的(像一台制造未装备计划的机器的机器),但例如当我将词*Poland*插入括号中,我就得到了一个打印一个打印词*Poland*的程序的程序: `` print('print("Poland")') `` 让我们再次与第一部分中提到的机器进行比较。程序`print()`就像一台可以根据任何东西的计划制造该东西的机器。符号`'print()'`就像这台机器的计划。程序`print('print()')`就像一台能够根据计划制造任何东西的机器,其计划持有器里放着一台能够根据计划制造任何东西的机器(计划持有器为空)的计划。而程序`print('print("Poland")')`就像一台能够根据计划制造任何东西的机器,其计划持有器里放着一台能够根据计划制造任何东西的机器(计划持有器里放着词*Poland*的计划)的计划。注意,这不是打印自身源代码的程序——虽然它打印的是与自身相似的程序的源代码,所以我们已经很接近了。 现在我们需要执行类似于第一部分对机器执行的操作。还记得吗?在子机器的计划中,我在计划持有器里写了*这里*。现在我将做类似的事情。看: `` print('print(here)') `` 还记得我如何修改机器,让它在返回子机器之前,将制造它所依据的计划的一份副本放入它的容器吗?现在我将做类似的事情:我会修改这个程序,使得在创建字符串之后、但显示之前,将这份字符串的副本放置到标有词*here*的位置。目前这做起来不容易,因为创建字符串和显示是在同一个命令中完成的,所以没有时间进行替换。因此,我将在这个程序中将字符串的创建与显示分开: `` string='print(here)'; print(string) `` 但是等等——既然我改变了程序,我也应该改变字符串字面量(程序源代码),使父程序创建一个与自身相似的孩子(你明白吗?类比机器:当我们改变机器时,也必须改变机器的计划,以使父机器创建与自身相似的孩子): `` string='string=here; print(string)'; print(string) `` 正如你所见,我用蓝色标记了修改过的片段。在本文的其余部分,我将不时这样做。 现在我在程序中添加一个命令,在显示之前将这份字符串的副本插入到词*here*的位置: `` string='string=here; print(string)'; string=string[:7] + string + string[11:]; print(string) `` 注意,我不是通过查找字符串*here*(即通过*查找并替换*方法,也不是通过 Python 的*replace*方法)进行替换,而是通过将字符串在指定位置切片。我这么做是因为这是最简单的。稍后我将展示若尝试其他方法会发生什么。 我运行这个程序,看它是否工作正常。它的输出如下: `` string=string=here; print(string); print(string) `` 还不错,但还有两件事需要修复。首先,插入到*here*位置的字符串应该用引号括起来。Python 中有一个内建函数*repr*,用于此目的。所以我使用它: `` string='string=here; print(string)'; string=string[:7] + repr(string) + string[11:]; print(string) `` 我运行这个程序,看它是否工作。我看到现在它的输出看起来像这样: `` string='string=here; print(string)'; print(string) `` 很好,引号出现了。现在第二件事。既然我修改了程序(因为添加了将所需内容插入*here*位置的代码),我也需要修改字符串字面量(程序源代码)。所以我把程序改为: `` string='string=here; string=string[:7] + repr(string) + string[11:]; print(string)'; string=string[:7] + repr(string) + string[11:]; print(string) `` 我运行这个程序,看它是否工作。我看到它的输出是: `` string='string=here; string=string[:7] + repr(string) + string[11:]; print(string)'; string=string[:7] + repr(string) + string[11:]; print(string) `` 成功!quine 工作了! 当你思考这个 quine 如何工作时,值得将其与第一部分中写到的制造机器的机器进行比较。只有一个困难:在机器的例子中,机器与机器计划之间的区别是鲜明的。在程序的例子中,文本与文本字面量之间的区别是微妙的。 #### Python 简易 quine 总结 所以再次总结我编写这个简单 Python quine 的三个步骤——同样的步骤可以用于其他语言编写 quine: 1. 我编写了一个创建并打印某程序(该程序能打印某物)的源代码的程序 2. 在创建文本的命令和打印文本的命令之间,我添加了代码,将其字面量放置到文本中的适当位置 3. 既然我修改了程序,我也修改了文本字面量,使其包含这个程序的源代码 ### 如果在这个 Python quine 中使用了*查找并替换*会怎样 你还记得我在编写 Python 简易 quine 时必须将其字面量插入到文本中吗?我指的是当我有一个像这样的程序时:`` text='text=here; print(text)'; print(text) `` 然后我把它改成了: `` text='text=here; print(text)'; text=text[:7] + text + text[11:]; print(text) `` 你可能会想,与其将文本分成两部分——第一部分到第七个字符,第二部分从第十一个字符开始,然后在它们之间插入所需内容——不如使用*查找并替换*(即使用*replace*方法)更简单。那么我来试试。我将替换命令同时添加到程序和程序源代码字面量中: `` text='text=here; text=text.replace(\'here\', text); print(text)'; text=text.replace('here', text); print(text) `` 我运行这个程序,发现出错了。输出看起来像这样: `` text=text=here; text=text.replace('here', text); print(text); text=text.replace('text=here; text=text.replace('here', text); print(text)', text); print(text) `` 程序没有像预期那样工作,因为在文本字面量中,词*here*出现了两次:一次是在它标记需要插入内容的位置,另一次是在*查找并替换*命令中我们指定要查找的文本片段。只有第一个*here*应该被替换,而不是第二个。这个问题可以通过几种方法解决。例如,在*查找并替换*命令中,我们可以尝试引用*here*而不使用词*here*。例如,像这样: `` text='text=here; text=text.replace(\'HERE\'.lower(), text); print(text)'; text=text.replace('HERE'.lower(), text); print(text) `` 或者这样: `` text='text=here; text=text.replace(\'ereh\'[::-1], text); print(text)'; text=text.replace('ereh'[::-1], text); print(text) `` 我运行这个程序看看它怎么工作。我看到这个输出: `` text=text=here; text=text.replace('ereh'[::-1], text); print(text); text=text.replace('ereh'[::-1], text); print(text) `` 还不错。现在我们只需要将插入的文本改为文本字面量:用引号括起来并转义任何特殊字符(例如,*'ereh'*周围的引号)。幸运的是,Python 有内建函数*repr*来做这个(但想象一下,如果我们在 JavaScript 中做这个而没有这样的函数,会有多麻烦): `` text='text=here; text=text.replace(\'ereh\'[::-1], repr(text)); print(text)'; text=text.replace('ereh'[::-1], repr(text)); print(text) `` 我运行这个程序。我看到这个输出: `` text="text=here; text=text.replace('ereh'[::-1], repr(text)); print(text)"; text=text.replace('ereh'[::-1], repr(text)); print(text) `` 嗯,几乎完美。几乎相同的结果。只是在*repr*函数将程序源代码文本转换为文本字面量时,它处理*'ereh'*周围的单引号转义问题的方式与我不同:它不是转义它们,而是用双引号将文本括起来。如果我想要程序输出与源代码完全相同(不仅仅是引号相似),我需要要么让*repr*函数像我的方式转义,要么修改程序使其像*repr*那样转义。后者更容易。嗯,其实非常容易。我什么都不用做:上面我的程序打印出的文本已经按照*repr*的方式转义了引号。这正是我想要编写的 quine。 所以使用*查找并替换*的 quine 看起来像这样: `` text="text=here; text=text.replace('ereh'[::-1], repr(text)); print(text)"; text=text.replace('ereh'[::-1], repr(text)); print(text) `` 当我运行它时,得到这样的输出: `` text="text=here; text=text.replace('ereh'[::-1], repr(text)); print(text)"; text=text.replace('ereh'[::-1], repr(text)); print(text) ``

相似文章

Qwen 3.6/3.8 自主编码 C 编译器的探索之旅

Reddit r/LocalLLaMA

作者描述了一个为期六个月的实验,使用 Qwen 3.6/3.8 模型自主构建 C99 编译器,克服了上下文管理和代码生成方面的重大挑战,最终产出了一个可工作的项目。

asciiQuake

Lobsters Hottest

AsciiQuake 是一个工具或项目,它以ASCII艺术形式渲染游戏Quake,可能用于基于文本的游戏或复古计算应用。