2308 字
12 分鐘
Python 代碼優化の心得

前言#

嗨各位!久違了⋯ 最近都在忙着研究 Arch 還有重構一堆幾年前寫下的屎山代碼,所以一直也沒空來打理這裏⋯⋯

這篇可以說是某次重構期間的意外發現,本不打算公開的。但考慮到網絡上的資料比較零散,對初心者而言未必能夠即時理解,所以還是決定把它肝出來了 (

 

引文#

前陣子在重構 discord-py 機器人裏的音樂播放器,先不說裏面 700 多行屎山搞得我快要頭暈與否⋯⋯

平常我們在寫程式時,如果遇到「循環、轉圈圈」的邏輯,最直覺的方法就是狂寫 if-else。舉個例子,當你在寫一個音樂播放器,使用者按「上一首」時,在某些情況下你得判斷:

「如果現在是第一首(Index 是 0),按上一首就要跳到最後一首;否則,就把 Index 減 1。」

傳統寫法大概長這樣:

if index == 0:
    index = historySize - 1
else:
    index -= 1

好吧當然上面的那個只是例子,筆者自己的那屎山也不是這樣寫的

那還是給一點實質的東西你們看吧 (?

Some code from my project

這是機器人裏移除曲目的功能 /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) ,公式如下:

Terminal window
(Quotient) = 被除數 (Dividend) / 除數 (Divisor)

 

Terminal window
餘數 (Remainder) = 被除數 (Dividend) - [除數 (Divisor) * 商 (Quotient)]

其中 商 (Quotient) 若為正數需先作下捨入 (Round down) 處理,否則需作上捨入 (Round up) 處理

譬如說我們要計算 -1 % 5 的數值

Terminal window
-1 / 5 = -0.2

因為得到的商是負數,上捨入後得出 0

Terminal window
-1 - (5 * 0) = -1

繼而得出 -1 % 5 = -1

然而在 Python 裏,其計算 a % b 的公式如下:

Terminal window
a % b = a - (b * (a // b))

兩者分別在於 // (floor division) 。 而根據 Python 的定義,會一律把裏面所得的值下捨入並趨向至負無限 (negative infinity, -∞)

我們再透過 Python 的算法計算 -1 % 5 的數值

Terminal window
-1 / 5 = -0.2

下捨入後得出 -1

Terminal window
-1 - (5 * -1) = 4

最終得出 -1 % 5 = 4

相信聰明的人已經明白我在說什麼了。但倘若你還沒明白的,也不要緊~

 

這又代表了什麼#

可能你們剛剛聽完上面的理論反被弄得一頭霧水了⋯⋯ 沒關係,我們可以來換個說法~

時鐘的隱諭#

我們可以把整件事想像成為一個12小時制的時鐘

如果現在的時間為凌晨2 時,然後我問你:「6小時前會是幾時了?」

理論上你會很自然的看一眼時鐘,然後回答我:「晚上8 時」

然而在 Python 的角度來看,你剛剛不只是單純看了一眼,其實你的大腦已經在背後做出了

Terminal window
(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 格」呢?

答案就是靠公式得出的啦:

Terminal window
d = player_choice - computer_choice

我們再來舉個例子

就是當玩家出石頭 (0),電腦出剪刀 (2) 的時候,結果會是怎樣的呢?

我們先數字帶進去相減:

Terminal window
d = 0 - 2 = -2

這時候你一定會想:「-2 是什麼鬼?怎麼會是負的?」

先別焦急,在圓圈的邏輯裡,減法代表「從電腦的位置出發,要走幾步才會到玩家的位置」

換句話說,負號 (-) 代表往逆時針方向走,而 2 則代表走 2 步。

所以 -2 的意思就是:「從電腦 (剪刀 2) 的位置出發,逆時針往回走 2 步,就會走到玩家 (石頭 0) 的位置。」

這就是 -2 在圓圈上的真正含義。

現在神奇的事情來了,如果我們改成從電腦 (剪刀 2) 往順時針方向走 1 步,在一個只有 3 格的圓圈裡,不也會走到玩家 (石頭 0) 的位置嗎?

既然位置相通,那我們剛剛說過的遊戲規則是什麼?

Terminal window
「只要順時針領先 1 格,就是玩家贏。」

因此,雖然數學上算出來的是 -2,但它在這循環圓圈上的靈魂其實就是 1

而 Python 的 % 只是幫你把這個藏在 -2 背後的 1 給算出來而已

Terminal window
-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 = 0
index = (0 - 1) % 5
-> index = -1 % 5
-> index = 4
# for index = 1
index = (1 - 1) % 5
-> index = 0 % 5
-> index = 0
# for index = 2
index = (2 - 1) % 5
-> index = 1 % 5
-> index = 1
# for index = 3
index = (3 - 1) % 5
-> index = 2 % 5
-> index = 2
# for index = 4
index = (4 - 1) % 5
-> index = 3 % 5
-> index = 3
# for index = 5
index = (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;

但這個筆者還沒試過,有興趣的小伙伴歡迎自己來試試

 

後記#

好啦,不說太多了 (?

所謂活學活用,我要繼續透過今天的所學,去優化那個播放器了

各位我們下次見啦~

Python 代碼優化の心得
https://moe.lolicon.io/posts/tech-posts/python-code-optimization-tips/
作者
ゆき
發佈於
2026-08-21
許可協議
CC BY-NC-SA 4.0

分享文章

生成精美分享圖或複製連結,與更多人分享本文。

繼續閱讀

沿著主題讀

基於共同的標籤與分類

換條路線

從其他文章中穩定抽取

評論

正在載入評論...