
前言
嗨各位!久違了⋯ 最近都在忙着研究 Arch 還有重構一堆幾年前寫下的屎山代碼,所以一直也沒空來打理這裏⋯⋯
這篇可以說是某次重構期間的意外發現,本不打算公開的。但考慮到網絡上的資料比較零散,對初心者而言未必能夠即時理解,所以還是決定把它肝出來了 (
引文
前陣子在重構 discord-py 機器人裏的音樂播放器,先不說裏面 700 多行屎山搞得我快要頭暈與否⋯⋯
平常我們在寫程式時,如果遇到「循環、轉圈圈」的邏輯,最直覺的方法就是狂寫 if-else。舉個例子,當你在寫一個音樂播放器,使用者按「上一首」時,在某些情況下你得判斷:
「如果現在是第一首(Index 是 0),按上一首就要跳到最後一首;否則,就把 Index 減 1。」
傳統寫法大概長這樣:
if index == 0: index = historySize - 1
else: index -= 1好吧當然上面的那個只是例子,筆者自己的那屎山也不是這樣寫的
那還是給一點實質的東西你們看吧 (?

這是機器人裏移除曲目的功能 /remove。其可以直接呼叫,也可以指定所需移除的曲目編號
如果沒有指定曲目編號,會預設為佇列中的最後一首曲目
眼尖的你們可以看到,裏面有用到了類似三元運算子 (Ternary Operator) 的東西,像醬~
index -= 1 if index else player.queue.historySize - 1這種本來就已經夠簡潔了,直到筆者發現了下面的寫法
index = (index - 1) % player.queue.historySize什麼?這到底是何方神聖?
突如其來的回憶殺
這不禁讓筆者憶起了去年的某次評估,當時的題目要求我們做一個單人猜拳遊戲 (石頭剪刀布) 的函數
而我當時是把題目完成了,且得了滿分。但為了判斷誰輸誰贏,寫了九種組合的 if-else 判斷式
結果教授的答案一出,我呆了
def rock_paper_scissors(player, computer): return (player - computer) % 3算出來是 0 👉 平手
1 👉 玩家贏了
2 👉 電腦贏了
這解法搞得當時的筆者頓時毫無頭緒⋯⋯ 所以這是什麼?到底是什麼來的?
原理
其實這一切魔法的起源,要由模除 (Modulo) 以及 Python 對其的處理方法說起
在數學上,欲要計算一 被除數 (Dividend) % 除數 (Divisor) 所得的 餘數 (Remainder) ,公式如下:
商 (Quotient) = 被除數 (Dividend) / 除數 (Divisor)
餘數 (Remainder) = 被除數 (Dividend) - [除數 (Divisor) * 商 (Quotient)]其中 商 (Quotient) 若為正數需先作下捨入 (Round down) 處理,否則需作上捨入 (Round up) 處理
譬如說我們要計算 -1 % 5 的數值
-1 / 5 = -0.2因為得到的商是負數,上捨入後得出 0
-1 - (5 * 0) = -1繼而得出 -1 % 5 = -1
然而在 Python 裏,其計算 a % b 的公式如下:
a % b = a - (b * (a // b))兩者分別在於 // (floor division) 。 而根據 Python 的定義,會一律把裏面所得的值下捨入並趨向至負無限 (negative infinity, -∞)
我們再透過 Python 的算法計算 -1 % 5 的數值
-1 / 5 = -0.2下捨入後得出 -1
-1 - (5 * -1) = 4最終得出 -1 % 5 = 4
相信聰明的人已經明白我在說什麼了。但倘若你還沒明白的,也不要緊~
這又代表了什麼
可能你們剛剛聽完上面的理論反被弄得一頭霧水了⋯⋯ 沒關係,我們可以來換個說法~
時鐘的隱諭
我們可以把整件事想像成為一個12小時制的時鐘
如果現在的時間為凌晨2 時,然後我問你:「6小時前會是幾時了?」
理論上你會很自然的看一眼時鐘,然後回答我:「晚上8 時」
然而在 Python 的角度來看,你剛剛不只是單純看了一眼,其實你的大腦已經在背後做出了
(2 - 6) % 12 = -4 % 12-> -4 % 12 = 8 (in python)這一串動作。
基本上一般 Python 的 % 就好像一部「自動包裝機」,會把數字限制在一個可循環的圓圈裏。
石頭剪刀布
我們可以把三者想像為一個順時針的循環徊圈
假設 0 為石頭、1 為布、2 為剪刀,可以得出下圖:
graph TD 0[0: ROCK] -->|贏| 1[1: PAPER] 1 -->|贏| 2[2: SCISSORS] 2 -->|贏| 0
記住這個圓圈的「遊戲規則」:
只要順時針 (向右轉) 走 1 格,你就能贏過原本那一格。
-
石頭 (0) 順時針走 1 格 -> 抵達 布 (1) -> 布贏石頭。
-
布 (1) 順時針走 1 格 -> 抵達 剪刀 (2) -> 剪刀贏布。
-
剪刀 (2) 順時針走 1 格 -> 繞回起點 抵達 石頭 (0) -> 石頭贏剪刀。
問題來了,我們要怎麼知道「玩家有沒有比電腦順時針領先剛好 1 格」呢?
答案就是靠公式得出的啦:
d = player_choice - computer_choice我們再來舉個例子
就是當玩家出石頭 (0),電腦出剪刀 (2) 的時候,結果會是怎樣的呢?
我們先數字帶進去相減:
d = 0 - 2 = -2這時候你一定會想:「-2 是什麼鬼?怎麼會是負的?」
先別焦急,在圓圈的邏輯裡,減法代表「從電腦的位置出發,要走幾步才會到玩家的位置」
換句話說,負號 (-) 代表往逆時針方向走,而 2 則代表走 2 步。
所以 -2 的意思就是:「從電腦 (剪刀 2) 的位置出發,逆時針往回走 2 步,就會走到玩家 (石頭 0) 的位置。」
這就是 -2 在圓圈上的真正含義。
現在神奇的事情來了,如果我們改成從電腦 (剪刀 2) 往順時針方向走 1 步,在一個只有 3 格的圓圈裡,不也會走到玩家 (石頭 0) 的位置嗎?
既然位置相通,那我們剛剛說過的遊戲規則是什麼?
「只要順時針領先 1 格,就是玩家贏。」因此,雖然數學上算出來的是 -2,但它在這循環圓圈上的靈魂其實就是 1
而 Python 的 % 只是幫你把這個藏在 -2 背後的 1 給算出來而已
-2 % 3 = 1 (in python)
回到原點
現在我們回來看dc機器人音樂播放器的那句代碼
設 player.queue.historySize = 5:
index -= 1 if index else player.queue.historySize - 1假設 index = (1-5) ,其會執行前者:
index -= 1
# for index = 1-> index = 1 - 1-> index = 0
# for index = 2-> index = 2 - 1-> index = 1
# for index = 3-> index = 3 - 1-> index = 2
# for index = 4-> index = 4 - 1-> index = 3
# for index = 5-> index = 5 - 1-> index = 4假設 index == 0 ,其會執行後者:
index = player.queue.historySize - 1-> index = 5 - 1-> index = 4我們再看看那種用 % 的 Python 寫法:
index = (index - 1) % player.queue.historySize假設 index = (0-5):
index = (index - 1) % player.queue.historySize
# for index = 0index = (0 - 1) % 5-> index = -1 % 5-> index = 4
# for index = 1index = (1 - 1) % 5-> index = 0 % 5-> index = 0
# for index = 2index = (2 - 1) % 5-> index = 1 % 5-> index = 1
# for index = 3index = (3 - 1) % 5-> index = 2 % 5-> index = 2
# for index = 4index = (4 - 1) % 5-> index = 3 % 5-> index = 3
# for index = 5index = (5 - 1) % 5-> index = 4 % 5-> index = 4我們可以看到,兩者的邏輯和輸出基本上完全一樣。且後者只需進行數學運算便可以了,無需進行多重的條件式判斷。
考慮到要處理具「循環性」的資料時,這的確為一個比較理想的替代方案。
結論
說實話這個技巧真的超好用,但有以下幾點需要注意:
首先資料必須具有「循環性」,像是播放清單、星期幾、時鐘、猜拳這種會「繞回起點」的邏輯才適用。
如果是判斷年齡、考試分數等那些的,還是乖乖的用 if-else 吧!
其次這可以說是 Python 限定的特異功能。因為如上所述,Python 在處理負數餘數 (e.g. -1 % 5)時,會自動向下捨入從而得到正數的 4。
但在 JavaScript、Java 或 C++ 裡,算出來會是 -1。若強行帶入,程式會直接崩潰喵~
據聞若要在其他語言上使用好像需要另外加上底數。以石頭剪刀布為例,像醬
let result = ((player_choice - computer_choice) + 3) % 3;但這個筆者還沒試過,有興趣的小伙伴歡迎自己來試試
後記
好啦,不說太多了 (?
所謂活學活用,我要繼續透過今天的所學,去優化那個播放器了
各位我們下次見啦~
分享文章
生成精美分享圖或複製連結,與更多人分享本文。
繼續閱讀
換條路線
從其他文章中穩定抽取
最後更新於 ,距今已過 19 天
部分內容可能已過時
評論