"Hello world!" 混亂程式碼比賽第一名作品解析

atupal發表於2014-11-21

幾個月前,我在這屆的 Code Golf 比賽中獲得了第一名,這個比賽的主題是寫出最怪異最混亂的「Hello world! 」列印程式。我決定寫一篇文章解釋它到底是怎麼執行的。下面是我的程式碼,語言是 Python2.7:

字串文字是不允許使用的,但為了讓它更有趣,我還加了一些其他的限制。那就是它 必須得是一個使用盡可能少的內建函式和不使用整數常量的單獨的表示式(所以沒有print語句)

開始

因為我們不能使用print,我們可以將字串寫入stdout檔案物件:

但是讓我們使用一些更底層的:os.write()。我們需要知道 stdout 的檔案描述符, 也就是 1(你可以通過sys.stdout.fileno()來驗證)

我們想要一個單獨的表示式,所以我們可以使用__import__():

我們還想把write()變得更迷惑一點,所以我們可以 丟出getattr()

這只是開始,從現在開始所有的事情都是在混亂這三個字串和這個整數。

用字串組成字串

“os” 和 “write” 非常簡單,所以我們可以通過把一些內建的類的名字的一些部分 連線起來得到。有許多的方法來做這個,我的選擇的方法如下:

  • “o” 來自 bool 的第二個字母:True.__class__.__name__[1]
  • “s” 來自 list 的第三個字母:[].__class__.__name__[2]
  • “wr” 來自 wrapper_descriptor 的頭兩個字母,你可以在一個 CPython 的實現細節中找到這些內建類的方法(詳情戳):().__class__.__eq__.__class__.__name__[:2]
  • “ite” 來自 tupleiterator 的第六到八個字母,物件的型別通過呼叫 Python 元組的iter()方法得到:().__iter__().__class__.__name__[5:8]

我們已經完成一些了!

“Hello world!n” 更復雜一些。我們準備將它編碼為一個大整數,通過把每個 字元的 ASCII 碼乘以一個 256 的冪,冪指數為字母在 “Hello worldn” 中的索引 。換句話說,下面的和式:

QQ截圖20141115134551

其中 L 是字串的長度,cn 是字串中第 n 個字元的 ASCII 碼。如何生成這個數:

然後現在我們得把這個數轉換回字串。我們使用了一個簡單的遞迴演算法:

使用 lambda 表示式把它重寫為一行:

現在我們使用匿名遞迴把它轉變成一個單獨的表示式。這需要 使用一個組合運算元。如下:

現在我們只需要把函式定義替換到表示式中就得到了我們要的函式:

現在我們可以把它貼上到我們之前的程式碼中了,然後做一些變數替換:f ->, n -> _

函式內部

在我們之前轉換後的函式中還有一個 “” (記住:不允許使用字串字面量), 和一個我們得用某種方法藏起來的整數。我們先從空字串開始。我們可以 通過檢查一些隨機的函式的內件過程中製造一個:

我們這裡真正做的事情是查詢函式中程式碼物件的行號表。因為函式是匿名 的,所以沒有行號,得到的字串就是空的。通過把 0 替換為 _ 使得它 更具有迷惑性(這不影響,因為這個函式並沒有被呼叫),然後把程式碼貼上 到之前的程式碼中。我們還把 256 重構了一些,把它作為一個引數傳遞給我們的 混淆後的convert()。這隻需要在組合運算元中新增一個引數就可以了:

迂迴

讓我們來處理一下另外一個不同的問題吧,。我們想在我們的程式碼中混淆數字, 但每次要用的時候都重新混淆一遍很麻煩(而且一點都沒意思)。如果我們能 實現,例如range(1, 9) == [1, 2, 3, 4, 5, 6, 7, 8],然後我們就 可以把我們現在做的包裝到一個函式中,然後這個函式包含 1-8 這幾個數字做 為函式引數,於是我們只需把程式碼體中相應的數字字面量替換成這些變數就可以了:

雖然我們還需要組成 256 和 802616035175250124568770929992, 但他們可以通過 這 8 個“基本的”的數字通過四則運算創造出來。1 到 8 的選擇是任意的,這算一個 折中的方法。

我們可以通過一個函式的程式碼物件得到它的引數個數:

構建一個元組的引數個數為 1 到 8 的函式:

使用遞迴演算法,我們就能把這個轉變成range(1, 9)的輸出:

像前面一樣,我們把這個變成 lambda 表示式:

然後,變成匿名遞迴形式:

為了更有趣,我們將把我們計算引數個數的操作變成一個額外的函式引數,然後混淆一些變數的名字:

現在新的問題來了: 我們得用某種方法來隱藏 0 和 1。我們可以通過 檢查任意函式的區域性變數的個數來得到 0 和 1:

即使這兩個函式的函式體是一樣的,但前面一個函式中 _ 不是一個引數,它也不是在 函式中定義的,所以 Python 把它解釋為一個全域性變數:

這不管 _ 在全域性域中被定義為啥都沒關係。

把這些應用一下:

現在我們可以把funcs替換進去了,然後使用*來傳遞 最後得到的整數列表,也即被分割為了 8 個變數:

移位

快要完成了!我們將把n{1..8}這幾個變數替換成, _,, _等等。 這會使得我們內部函式使用的變數更具有迷惑性。這不會有問題,因為作用域規則 保證了我們每次使用的都是正確的變數。這也是我們為啥把 256 移出到 _ 等於 1 的地方而不是在convert()函式內部的一個原因。程式碼更長了,所以我只貼了 第一部分:

現在只剩兩件事了。我們先從容易的一個開始: 256 = 2^8, 所以我們可以通過把它 重寫為1 << 8得到(使用左移運算),或者使用我們混淆後的變數:_ << ________

我們對 802616035175250124568770929992 也使用同樣的方法。一個簡單的分治法就能 把它拆成一些數的和,這些數本身也是一起位移的一些數的和。舉個例子,如果我們有 112,我們可以把它拆成 96 + 16 即 (3 << 5) + (2 << 3)。我喜歡使用位移是因為 << 讓我想起 C++ 中的std::cout << "foo"和 Python 中的 print chevron(print >>)。 它們都是帶有誤導性的另類 I/O 方法。

這個數字可以有多種分解方法,沒有標準答案(畢竟我們也可以把它 分解為 (1<<0) + (1<<0) + …, 但是太沒意思了)。我們應該有一些 大量的巢狀,但是我們仍會使用大部分我們的數字變數。顯然,人工來做 是沒意思的,所以我們想出一個演算法,虛擬碼如下:

我們的基本思路是測試在一個確定區間的不同的數字組合,直到我們找到 一個組合使得以一個為基數,一個為移位長度,然後是最接近 num 的(也就是 他們差的絕對值最小)。我們使用我們的分治法來分解成最好的基數和移位長度, 然後迴圈這個過程直到等於零,最後在每一步中加上得到數就可以了。

其中range()的引數:span,代表我們搜尋的空間的寬度。這個數不能太大, 不然我們會得到 num 為基數,0 為移位長度的結果(因為 diff 是 0),並且因為 基數不可能是一個單獨的變數,它會一直迴圈,無限遞迴。如果太小了,我們有可能 最後得到的是像上面說的(1<<0) + (1<<0) + ...這樣的結果。實際上,我們想 隨著遞迴深度的增加 span 隨之變小。通過試錯,我發現下面這個方程工作得很好:

QQ截圖20141115135029

把虛擬碼轉換成 Python 程式碼,然後做一些調整(支援 depth 引數以及呼叫負數的警告),如下:

然後我們呼叫convert(802616035175250124568770929992),我們得到一個很好的分解:

用這個替換 802616035175250124568770929992, 把所有部分組合起來:

然後大功告成。

相關文章