什麼是Ellipsis

在 Python 中你可能有時候會看到一個奇怪的用法,就像是這樣:

>>> ...  
Ellipsis 

在你輸入了三個點之後,Python 解釋器非但不會報錯,反而還會返回給你「Ellipsis」這麼一個信息。那麼這個有趣的東西是什麼呢?

查閱 Python 官方文檔後可以看到,它是一個**「內置常量」**(Built-in Constant)。經常用於對用戶自定義的容器數據類型進行切片用法的擴展。

這也就意味着它可能是會作爲一個「小衆且另類」的語法糖來使用,但如果你用於 Python 中的容器數據類型(比如列表)進行切片索引時,可能會引發錯誤:

>>> nums = list(range(10))  
>>> nums  
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]  
>>> nums[...]  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
TypeError: list indices must be integers or slices, not ellipsis 

除此之外,如果你使用的是 Python 2 的解釋器,那麼壓根就不支持 Ellipsis 的用法,從一開始輸入時就報錯:

$ python2  
WARNING: Python 2.7 is not recommended.   
This version is included in macOS for compatibility with legacy software.   
Future versions of macOS will not include Python 2.7.   
Instead, it is recommended that you transition to using 'python3' from within Terminal.  
Python 2.7.16 (default, Nov  9 2019, 05:55:08)   
[GCC 4.2.1 Compatible Apple LLVM 11.0.0 (clang-1100.0.32.4) (-macos10.15-objc-s on darwin  
Type "help", "copyright", "credits" or "license" for more information.  
>>> ...  
  File "<stdin>", line 1  
    ...  
    ^  
SyntaxError: invalid syntax 

雖然說在列表中使用 Ellipsis 會報錯,但是碰到這種情況你會發現解釋器返回給你的是這樣的東西:

>>> nums = [1,2,3]  
>>> nums  
[1, 2, 3]  
>>> nums[1] = nums  
>>> nums  
[1, [...], 3] 

可以看到,這裏我們將 nums 中的第二個元素替換成自身,就會形成不斷地遞歸嵌套賦值,而解釋器最後直接給出了頭尾兩個元素之外,其他全部元素都會被 ... 所囊括在內。

根據 Python 官方的另一處文檔,Ellipsis 本身也不支持任何操作,僅僅只是一個單例對象(Singleton)

誰能想到,Guido van Rossum 這麼一位被人稱爲「仁慈的獨裁者」的 Python 之父採納 Ellipsis 的原因竟然是因爲:有人認爲三個省略號的寫法可愛。(原文爲:「Some folks thought it would be cute to be able to write incomplete code like this」)

應用

要說這個看起來「雞肋」的 Ellipsis 類型對象沒有用,這個說法似乎也不正確。因爲它作爲一種奇怪的語法糖也被應用到了某些地方。

Numpy 中的切片

雖然官方說 Ellipsis 主要用於用戶自定義容器類型的切片操作,但是在我搜索了許久之後發現用 Ellipsis 來實現所謂的切片操作的貌似只有 Numpy。

使用 Python 做數據分析、挖掘或機器學習相關的朋友一定對 Numpy 高性能的科學計算庫並不陌生。在 Numpy 中我們真正的使用 Ellipsis 來進行切片索引:

>>> import numpy as np  
>>> arr = np.arange(9).reshape((3,3))  
>>> arr 
 array([[0, 1, 2],  
       [3, 4, 5],  
       [6, 7, 8]]) 

需要注意的是,Ellipsis 主要是對二維以上的數組才起作用:

>>> arr[...,1:2]  
array([[1],  
       [4],  
       [7]])  
>>> arr[2, ...]  
array([6, 7, 8]) 

從結果中我們看到,Ellipsis 三個省略號的寫法其實就等價於 arr[:, 1:2] 冒號的寫法。但是在使用過程中 Ellipsis 只能出現一次:

>>> ndarr = np.arange(24).reshape((2,3,4))  
>>> ndarr  
array([[[ 0,  1,  2,  3],  
        [ 4,  5,  6,  7],  
        [ 8,  9, 10, 11]],  
       [[12, 13, 14, 15],  
        [16, 17, 18, 19],  
        [20, 21, 22, 23]]])  
>>> ndarr[:, :, :]  
array([[[ 0,  1,  2,  3],  
        [ 4,  5,  6,  7],  
        [ 8,  9, 10, 11]],  
       [[12, 13, 14, 15],  
        [16, 17, 18, 19],  
        [20, 21, 22, 23]]])  
>>> ndarr[..., ..., ...]  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
IndexError: an index can only have a single ellipsis ('...') 

Ellipsis 在 Numpy 中出現的意義在於,當你的數組是高維的數組時,那麼可以直接使用它來作爲選取其他維度的等價寫法,以下例子來源於 Numpy 官方文檔:

>>> z = np.arange(81).reshape(3,3,3,3)  
>>> z[1,...,2] # 等價於 z[1, :,:, 2]  
array([[29, 32, 35],  
       [38, 41, 44],  
       [47, 50, 53]]) 

Type Hint 類型註解

自從 PEP 484 之後,Python 解釋器開始支持類型註解。所謂的類型註解無非就是在 Python 實際代碼中能像註釋那樣對當中的一些參數或返回值添加類型註釋,就像是這樣:

def add(x: int, y: int) -> int:  
    return x + y 

如果你是有使用過 Java 或者 Go 這類對類型註解要求較爲嚴格的編譯型語言,那麼相信對此並不陌生,無論是變量還是方法,都要寫上對應的類型以防編譯報錯;但即便沒有接觸過這類編譯型語言也不要緊,將其理解爲註釋即可,這樣的註釋是能被編輯器或 IDE 所支持,在你要查看函數定義或文檔時會給予提示。

但是 Type Hint 僅僅只是一種「協定」,告訴別人你的方法裏參數是如何、最後返回的是什麼僅此而已,無論是加與不加都不會影響最終代碼的效果,影響的僅僅只是代碼的可讀性罷了。

如果你的方法有多個返回值,我們不可能對每個返回值的類型都寫上註解,因此這時 Ellipsis 對象就派上了用場。根據官方文檔給出的說明,我們完全可以像這樣來進行類型註解:

from typing import Tuple  
def get_many_value(  
    a:int, b:int, c:int,   
    d:int, e:int, f:int  
) -> Tuple[int, ...]:  
    return [a+b, c+d, e+f] 

這樣的寫法本質上就是 *args 的作用,表示同類型的可變長度元組。如果你將 Tuple 換成是 List,那麼解釋器會報錯,因爲 *args 在方法中的表現就是元組,那麼作爲註解的 Ellipsis 也應如此。這可能也就說明爲什麼在 Tuple 註解中不報錯了。

FastAPI 中的必選參數

目前正流行開來的高性能 Web 框架 FastAPI 中,也應用了 Ellipsis。它用以表示參數是必填項,這在 Swagger 頁面更能直觀體現。

# pip install fastapi  
# pip install uvicorn  
from fastapi import FastAPI, Query  
app = FastAPI()  
@app.get('/greetWithOutEllipsis')  
async def greet(name: str = None):  
    if name:  
        return {"info": f"Welcome! {name}"}  
    return {"info": f"Welcome to FastAPI!"}  
@app.get('/greetWithEllipsis')  
async def greet(name: str = Query(..., min_length=2)):  
    if name:  
        return {"info": f"Welcome! {name}"}  
    return {"info": f"Welcome to FastAPI!"}  
if __name__ == "__main__":  
    import uvicorn  
    uvicorn.run(app, port = 5000) 

啓動服務之後,在瀏覽器中輸入 http://127.0.0.1:5000/docs 便能進入到服務的 Swagger 頁面中,在上述例子中如果 name 參數並非是個必要的參數時,在 Swagger 頁面中不會看到任何標識,即便我們不帶上 name 參數也能進行請求:

非必要參數

但當我們加上了一個 Query() 方法,並將其 Ellipsis 對象丟到當中時,不僅會給參數加上 required 的標識,同時還對傳入的字符串長度進行了限制。

必要參數

除了參數之外,在 FastAPI 中你還可以在請求體、路徑、字段等多個地方使用 Ellipsis 對象。

「僞」 pass 寫法

Ellipsis 有時候還可以作爲 pass 的一種「僞」寫法,比如這樣:

def greet():  
    ... #等價於 pass 

這其實就和 # 註釋符號與六個引號的長字符串註釋類似。但實際上僅僅只是一種取巧的方法,實際上我們可以將 ... 替換成任何值或對象,如 None、1、True 等,因爲在方法中並沒有顯示聲明返回的對象,所以無論我們寫什麼最後的效果都是一樣的。

但使用 Ellipsis 對象來作爲 pass 關鍵字的替代品從「視覺」上來說或許還有點「意猶未盡」的意思。

當然如果在你和同事協作時,隨手寫下這樣一個省略號,沒準隱含着你對同事 Coding 的無奈,或者是對禿頭的憂愁(逃)

【責任編輯:龐桂玉 TEL:(010)68476606】

相關文章