空虛的真

空虛的真

來一個我自己覺得滿有趣的 Python 程式問答題。

在 Python 裡,all() 這個函數可以接一個串列,串列裡的每一顆元素都成立的時候就會回傳 True,反之只要有一顆不成立,就會是 False,例如:

>>> all([True, True, True])
True
>>> all([True, False, True])
False

題目來了:

>>> all([])     # A
>>> all([[]])   # B
>>> all([[[]]]) # C

一堆中括號、小括號,眼睛都快看花了,在正式的專案千萬不要這樣整自己(或是你的同事)!大家猜猜看 A、B、C 的結果分別是什麼?

答案是 True / False / True

首先,我們得先看看 all() 這個函數的設計,當如果裡面所有的元素都是 True 的時候會得到 True,但文件上跟原始碼裡都有一行但書:

If the iterable is empty, return True.

也就是說如果是 all([]) 的話,會得到 True,至於為什麼是這樣的結果我們待會再看。

第二題,因為在 Python 裡空的串列 [] 會被當做 False 看待,所以 all([[]]) 相當於 all([False]),所以答案是 False

最後一題,[[]] 是一個串列,它是一個包含空串列的串列,但它本身不是一個空的串列。有點像是箱子裡面裝了一個箱子,就算內層的箱子是空的,對外層的箱子來說它也是有裝東西的,就是裝了你這個空箱子!

對於非空串列,在 Python 會被當做 True,所以 all([[[]]]) 等於是 all([True]) 的效果,答案會得到 True。繼續延伸下去,all([[[[[[[[]]]]]]]]) 就算裡面包再多層次,答案也是 True

真空?空虛?

這裡比較難理解的可能是為什麼 all([])True。這不是腦筋急轉彎,也不是設計者故意惡搞,其實這是刻意的設計。在數學或邏輯上稱為「Vacuous Truth」。我覺得中文翻譯成「空虛的真」滿酷的,聽起來有點像領域展開之類的中二招式。它的意思是想要表達在這個空間裡沒有任何一個條件是不成立的,用一般路人也聽的懂的話來舉個例子:

如果房間裡一隻手機都沒有,那麼「房間裡所有的手機都已經關機」這句話就是成立的。

事實上,你要說「房間裡所有的手機都已經開機」或是「房間裡每一隻手機都是 iPhone」也都是對的。也就是說,對於一個空的陣列或集合來說,透過 all() 函數問它是否每個元素都成立(或都不成立),基本上沒什麼太大的意義,因為都會成立。

如果用幾個希臘字母來寫的話:

∀𝑥(𝑥∈𝐴 → 𝑃(𝑥))

這幾個字母組成一起的意思是「對所有的 𝑥,如果 𝑥 屬於集合 𝐴,那麼 𝑥 具有性質 𝑃」。在邏輯中,當一個條件語句(若 p 則 q)的前提(p)永遠不成立時,無論結論(q)是真是假,整個條件語句都被認為是成立的。因為「 𝑥 屬於集合 𝐴」這個前提永遠不會成立(因為集合 𝐴 是空集合),所以無論 𝑃(𝑥) 是什麼,整個陳述都成立。

如果有興趣翻 CPython 的原始碼,就會發現它的實作滿簡單的,為了簡化,我把一些判斷錯誤、處理記憶體跟相對比較不重要的語句拿掉,看起來像這樣:

// 檔案:Python/bltinmodule.c

static PyObject *
builtin_all(PyObject *module, PyObject *iterable)
{
    PyObject *it, *item;
    int cmp;

    for (;;) {
        item = iternext();
        if (item == NULL)
            break;
        cmp = PyObject_IsTrue(item);
        if (cmp == 0) {
            Py_RETURN_FALSE;
        }
    }
    Py_RETURN_TRUE;
}

簡單的說,就是用 for 跑個無窮迴圈,把元素一個一個拿出來,如果有任何一個值是 False 就結束迴圈,不然最後就回傳 True。簡化成 Python 的寫法,大概就是:

def all(iterable):
    for element in iterable:
        if not element:
            return False
    return True

在 Python 跟 all() 類似邏輯的還有 any(),大家可想想看,如果把原本題目裡的 all() 換成 any(),答案會變成什麼。

不是只有 Python,Ruby 的.all? 方法也是這樣的設計。如果你是前端工程師,你去看看 JavaScript 陣列的 .every() 方法也一樣:

// 每個元素都大於 1000
> [1450, 9527, 1314].every((x) => x > 1000)
true

// 有一個沒有滿足條件
> [1450, 9527, 520].every((x) => x > 1000)
false

// 但是沒有人不滿足條件!
> [].every((x) => x > 1000)
true

對於空的集合體來說,基努李維也說過(並沒有),就算你說 1 加 1 等於 5,也是對的:

> [].every(() => 1 + 1 == 5)
true

是說,當你跟朋友經過一家餐廳門口,餐廳裡一個客人都沒有,但你朋友突然說「喂,這家餐廳裡的所有客人都在低頭安靜的吃飯耶!」,這句話雖然是符合邏輯,但放在七月份就會覺得有點嚇人了。