從 Python 樂透小程式看美感

因為最近有需要個類似樂透電腦選號的功能,需要取六個亂數就是了。
當我完成這個小的程式之後,我深深的覺得 python 是個非常具有美感的語言

choiced = [] # 選到的號碼
for i in range(6):
    choiced.append(random.choice([x for x in range(1, 50) if x not in choiced]))

這已經是我拆出來最重要的三行程式碼了,不過當中最重要的是以下這一段

choiced.append(random.choice([x for x in range(1, 50) if x not in choiced]))

這行看起來比較長,我將他拆成幾個部份來看

[x for x in range(1, 50) if x not in choiced]

這個用到的是 python 美麗的部份,一個叫做 List Comprehensions 的東西。
這東西就是要產生一個 List ,然後我將這個範圍設定在 1 ~ 49 。抽出來之後不放回去。

其實會有 List Comprehensions 這個東西主要是因為以往我們在數學當中,有個表示集合的訴求。

S = { x | 0 < x < 10, x ∈ N }

可以把上面的數學式換成下面這樣

S = { 1, 2, 3, 4, 5, 6, 7, 8 , 9 }

簡單來說,我們數學當中已經設計過有關於集合的表示方法,上面是一個表示集合 S 的方式。
這裡所表達的 S 是一個 1 ~ 10 的所有正整數。

而第一種數學的描述方法是說,我的集合 S ,是所有符合條件的 x ,第一點 x 介在 1 ~ 10 ,第二點則是 x 屬於正整數。
那第二種表示方式就是把所有符合結果的 x 跑一遍所得到的結果,當然也比較直觀。

而 python 當中的 List 當然是很適合拿來塞一堆有值的東西,換個概念也能把他當作數學中的集合。
如果搭配著 python 運算中的 and, or, in, not 那不是天下無敵了嗎?
List Comprehensions 就是為了這件事情慢慢衍生而來,如果我們要上面的那個 1 ~ 10 的正整數
可以像下面這樣:

S = [x for x in range(1, 10)]

而 python 就會把這串中括號包起來的東西轉換成一個包含 1 到 10 的 List 給 S
如果換成別的語言絕對需要很長一段程式碼
或者是一個 function 包起來的 API 組合。

List Comprehensions 還可以搭配著判斷式取 List 中的集合。
假設我們想要:

S = {x | x is even, 1 < x < 50 } // 1 ~ 50 之間的偶數

對 Python 的 List Comprehensions 來說,只要在 for 的後面加上個條件判斷這樣就可以了

S = [x for x in range(1, 50) if x % 2 == 0]

如果很直觀的看這一段的程式碼,也能直接解讀成 S 是在 x 介於 1 到 50 之間,而且除以 2 餘數為 0 的所有數字。 這就是一個有美感的程式語言呀!

接著這程式碼剩下的部份我就輕快的帶過, random.choice(List) 就是從 List 中隨機挑選一個值出來。
挑完之後塞到已選數列,下次繼續挑的時候就會被排除了。

接著一個有美感的 Python 例子是當中的三元運算,簡單來說就是一個 if-else 的簡化版。
這東西寫 C 的人很常看見,以下舉例。

int cond = 5;
a = cond > 2 ? 100 : 0;

----- if-else 版 --------

int cond = 5;
if (cond > 2)
    a = 100;
else
    a = 0;

這東西尤其在寫 Verilog 的時候非常常用,這常常應用在訊號判斷,某 cond 發生時,自動把數值切成 100 不然就是 0。

起初,不少人對三元運算很有意見,當然也包括 Python 的開發者。因為 ? 以及 : 是一個比 if-else 難看懂的符號,
你必須經過學習才能知道符號當中的意思。
再者,Python 的哲學是同樣的事情只用一個方法達成。這對 Python 來說只不過是個 if-else 簡化版而已。

但是三元運算有其方便之處,尤其是你的程式碼已經很長的時候,只為了一個變數的值就加上個 if-else 實在是又臭又長。 舉個例子,如果我想把一個數字的絕對值找出來,我就會用上三元運算

int x = -100;
a = x > 0 ? x : -x;

如果用上 if-else 大約會多個三行

int x = -100;
if (x > 0)
    a = x;
else
    a = -x;

這個取絕對值的例子雖然簡潔,但沒學過這個程式語言的人絕對看不懂那個問號,Python 社群中當然也曾經為了這件事情吵過。
最後社群不斷的 argue 語法,終於有個比較好的方案,讓 Python 的作者滿意。

x = -100
a = x if x > 0 else -x # Python 的三元運算

這就是一個非常 care 美感的語言才會發生的事情,小弟曾在某個 fb 社團跟人家論戰過 C++ 語法美感的問題。
但每一個我不喜歡的語法都被說沒問題,以及有其必要性。

我個人是覺得一定有辦法讓美感提昇,只是當初 C++ 受限於相容 C ,導致美感受限。
跟我論戰的人我想可能是因為寫太久的 C++ 深覺無法變更,以至於都會拿效能問題出來說語法一定得這樣設計,不然就會變慢。

小舉個例子

class A {
public:
    int a;
    int b;
    A() a = 0, b = 0 {
    }
};

我什麼我很 care 這個呢?因為這個例子同樣也能寫成

class A {
public:
    int a;
    int b;
    A() {
        a = 0;
        b = 0;
    }
};

會有這兩種語法,當然有其不同點,前者是以一開始就把值塞 0 到 a 跟 b 裡頭,但是後者是以 assign 的方式,建構完之後再把數值 assign 過去。
當初網友就直接告訴我這個就是有效能問題,兩者是不一樣的。

網友講的是沒錯,但多了這個選項讓 C++ 的美感下降,這東西可以交還給編譯器來優化,讓開發者專心於開發上面。

我想當初會選用 C++ 的開發者是因為他是 native 機器碼速度快,同時又有語法支援物件導向。
但既然已經物件導向了,效能勢必下降。而物件導向的優點是有更好的結構,因此伴隨著美麗的語法也很重要。
因此才會有 java 跳出來大改造,把 C++ 會讓人困惑的部份給刪除掉,讓程式感的美感提昇。
雖然 java 最後還為了跨平台這件事情,捨去了一點效能,但 java 的初衷立意良好。

語言只是工具,我們可以多多揣摩當中的美感,但千萬別被工具給限制住了,讓我們看不見當中的美。