1. 程式人生 > 實用技巧 >Python 為什麼會有個奇怪的“...”物件?

Python 為什麼會有個奇怪的“...”物件?

本文出自“Python為什麼”系列,請檢視全部文章

在寫上一篇《Python 為什麼要有 pass 語句?》時,我想到一種特別的寫法,很多人會把它當成 pass 語句的替代。在文章釋出後,果然有三條留言提及了它。

所謂特別的寫法就是下面這個:

# 用 ... 替代 pass
def foo():
	...

它是中文標點符號中的半個省略號,也即由英文的 3 個點組成。如果你是第一次看到,很可能會覺得奇怪:這玩意是怎麼回事?(PS:如果你知道它,仔細看過本文後,你同樣可能會覺得奇怪!)

1、認識一下“...”內建常量

事實上,它是 Python 3 中的一個內建物件,有個正式的名字叫作——Ellipsis,翻譯成中文就是“省略號”。

更準確地說,它是一個內建常量(Built-in Constant),是 6 大內建常量之一(另外幾個是 None、False、True、NotImplemented、__debug__)。

關於這個物件的基礎性質,下面給出了一張截圖,你們應該能明白我的意思:

“...“並不神祕,它只是一個可能不多見的符號型物件而已。用它替換 pass,在語法上並不會報錯,因為 Python 允許一個物件不被賦值引用。

嚴格來說, 這是旁門左道,在語義上站不住腳——把“...”或其它常量或已被賦值的變數放在一個空的縮排程式碼塊中,它們是與動作無關的,只能表達出“這有個沒用的物件,不用管它”。

Python 允許這些不被實際使用的物件存在,然而聰明的 IDE 應該會有所提示(我用的是 Pycharm),比如告訴你:Statement seems to have no effect

但是“...”這個常量似乎受到了特殊對待,我的 IDE 上沒有作提示。

很多人已經習慣上把它當成 pass 那樣的空操作來用了(在最早引入它的郵件組討論中,就是舉了這種用法的例子)。但我本人還是傾向於使用 pass,不知道你是怎麼想的呢?

2、奇怪的 Ellipsis 和 ...

... 在 PEP-3100 中被引入,最早合入在 Python 3.0 版本,而 Ellipsis 則在更早的版本中就已包含。

雖然官方說它們是同一個物件的兩種寫法,而且說成是單例的(singleton),但我還發現一個非常奇怪的現象,與文件的描述是衝突的:

如你所見,賦值給 ... 時會報錯SyntaxError: cannot assign to Ellipsis

,然而 Ellipsis 卻可以被賦值,它們的行為根本就不同嘛!被賦值之後,Ellipsis 的記憶體地址以及型別屬性都改變了,它成了一個“變數”,不再是常量。

作為對比,給 True 或 None 之類的常量賦值時,會報錯SyntaxError: cannot assign to XXX,但是給 NotImplemented 常量賦值時不會報錯。

眾所周知,在 Python 2 中也可以給布林物件(True/False)賦值,然而 Python 3 已經把它們改造成不可修改的。

所以有一種可能的解釋:Ellipsis 和 NotImplemented 是 Python 2 時代的遺留產物,為了相容性或者只是因為核心開發者遺漏了,所以它們在當前版本(3.8)中還可以被賦值修改。

... 出生在 Python 3 的時代,或許在將來會完全取代 Ellipsis。目前兩者共存,它們不一致的行為值得我們注意。我的建議:只使用"..."吧,就當 Ellipsis 已經被淘汰了。

3、為什麼要使用“...”物件?

接下來,讓我們回到標題的問題:Python 為什麼要使用“...”物件?

這裡就只聚焦於 Python 3 的“...”了,不去追溯 Ellipsis 的歷史和現狀。

之所以會問這個問題,我的意圖是想知道:它有什麼用處,能夠解決什麼問題?從而窺探到 Python 語言設計中的更多細節。

大概有如下的幾種答案:

(1)擴充套件切片語法

官方文件中給出了這樣的說明:

Special value used mostly in conjunction with extended slicing syntax for user-defined container data types.

這是個特殊的值,通常跟擴充套件的切片語法相結合,用在自定義的資料型別容器上。

文件中沒有給出具體實現的例子,但用它結合__getitem__() 和 slice() 內建函式,可以實現類似於 [1, ..., 7] 取出 7 個數字的切片片段的效果。

由於它主要用在資料操作上,可能大部分人很少接觸。聽說 Numpy 把它用在了一些語法糖用法上,如果你在用 Numpy 的話,可以探索一下都有哪些玩法?

(2)表達“未完成的程式碼”語義

... 可以被用作佔位符,也就是我在《Python 為什麼要有 pass 語句?》中提到 pass 的作用。前文中對此已有部分分析。

有人覺得這樣很 cute,這種想法獲得了 Python 之父 Guido 的支援

(3)Type Hint 用法

Python 3.5 引入的 Type Hint 是“...”的主要使用場合。

它可以表示不定長的引數,比如Tuple[int, ...] 表示一個元組,其元素是 int 型別,但數量不限。

它還可以表示不確定的變數型別,比如文件中給出的這個例子:

from typing import TypeVar, Generic

T = TypeVar('T')

def fun_1(x: T) -> T: ...  # T here
def fun_2(x: T) -> T: ...  # and here could be different

fun_1(1)                   # This is OK, T is inferred to be int
fun_2('a')                 # This is also OK, now T is str

T 在函式定義時無法確定,當函式被呼叫時,T 的實際型別才被確定。

在 .pyi 格式的檔案中,... 隨處可見。這是一種存根檔案(stub file),主要用於存放 Python 模組的型別提示資訊,給 mypy、pytype 之類的型別檢查工具 以及 IDE 來作靜態程式碼檢查。

(4)表示無限迴圈

最後,我認為有一個非常終極的原因,除了引入“...”來表示,沒有更好的方法。

先看看兩個例子:

兩個例子的結果中都出現了“...”,它表示的是什麼東西呢?

對於列表和字典這樣的容器,如果其內部元素是可變物件的話,則儲存的是對可變物件的引用。那麼,當其內部元素又引用容器自身時,就會遞迴地出現無限迴圈引用。

無限迴圈是無法窮盡地表示出來的,Python 中用 ... 來表示,比較形象易懂,除了它,恐怕沒有更好的選擇。

最後,我們來總結一下本文的內容:

  • ... 是 Python 3 中的一個內建常量,它是一個單例物件,雖然是 Python 2 中就有的 Ellipsis 的別稱,但它的性質已經跟舊物件分道揚鑣
  • ... 可以替代 pass 語句作為佔位符使用,但是它作為一個常量物件,在佔位符語義上並不嚴謹。很多人已經在習慣上接受它了,不妨一用
  • ... 在 Python 中不少的使用場景,除了佔位符用法,還可以支援擴充套件切片語法、豐富 Type Hint 型別檢查,以及表示容器物件的無限迴圈
  • ... 對大多數人來說,可能並不多見(有人還可能因為它是一種符號特例而排斥它),但它的存在,有些時候能夠帶來便利。希望本文能讓更多人認識它,那麼文章的目的也就達成了~

如果你覺得本文分析得不錯,那你應該會喜歡這些文章:

1、Python為什麼使用縮排來劃分程式碼塊?

2、Python 的縮排是不是反人類的設計?

3、Python 為什麼不用分號作語句終止符?

4、Python 為什麼沒有 main 函式?為什麼我不推薦寫 main 函式?

5、Python 為什麼推薦蛇形命名法?

6、Python 為什麼不支援 i++ 自增語法,不提供 ++ 操作符?

7、Python 為什麼只需一條語句“a,b=b,a”,就能直接交換兩個變數?

8、Python 為什麼用 # 號作註釋符?

9、Python 為什麼要有 pass 語句?

本文屬於“Python為什麼”系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試著展現 Python 的迷人魅力。所有文章將會歸檔在 Github 上,專案地址:https://github.com/chinesehuazhou/python-whydo