Python中有趣的Ellipsis對(duì)象
什么是Ellipsis
在 Python 中你可能有時(shí)候會(huì)看到一個(gè)奇怪的用法,就像是這樣:
- >>> ...
- Ellipsis
在你輸入了三個(gè)點(diǎn)之后,Python 解釋器非但不會(huì)報(bào)錯(cuò),反而還會(huì)返回給你「Ellipsis」這么一個(gè)信息。那么這個(gè)有趣的東西是什么呢?
查閱 Python 官方文檔后可以看到,它是一個(gè)**「內(nèi)置常量」**(Built-in Constant)。經(jīng)常用于對(duì)用戶自定義的容器數(shù)據(jù)類(lèi)型進(jìn)行切片用法的擴(kuò)展。
這也就意味著它可能是會(huì)作為一個(gè)「小眾且另類(lèi)」的語(yǔ)法糖來(lái)使用,但如果你用于 Python 中的容器數(shù)據(jù)類(lèi)型(比如列表)進(jìn)行切片索引時(shí),可能會(huì)引發(fā)錯(cuò)誤:
- >>> 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 的用法,從一開(kāi)始輸入時(shí)就報(bào)錯(cuò):
- $ 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
雖然說(shuō)在列表中使用 Ellipsis 會(huì)報(bào)錯(cuò),但是碰到這種情況你會(huì)發(fā)現(xiàn)解釋器返回給你的是這樣的東西:
- >>> nums = [1,2,3]
- >>> nums
- [1, 2, 3]
- >>> nums[1] = nums
- >>> nums
- [1, [...], 3]
可以看到,這里我們將 nums 中的第二個(gè)元素替換成自身,就會(huì)形成不斷地遞歸嵌套賦值,而解釋器最后直接給出了頭尾兩個(gè)元素之外,其他全部元素都會(huì)被 ... 所囊括在內(nèi)。
根據(jù) Python 官方的另一處文檔,Ellipsis 本身也不支持任何操作,僅僅只是一個(gè)單例對(duì)象(Singleton)
誰(shuí)能想到,Guido van Rossum 這么一位被人稱為「仁慈的獨(dú)裁者」的 Python 之父采納 Ellipsis 的原因竟然是因?yàn)椋河腥苏J(rèn)為三個(gè)省略號(hào)的寫(xiě)法可愛(ài)。(原文為:「Some folks thought it would be cute to be able to write incomplete code like this」)
應(yīng)用
要說(shuō)這個(gè)看起來(lái)「雞肋」的 Ellipsis 類(lèi)型對(duì)象沒(méi)有用,這個(gè)說(shuō)法似乎也不正確。因?yàn)樗鳛橐环N奇怪的語(yǔ)法糖也被應(yīng)用到了某些地方。
Numpy 中的切片
雖然官方說(shuō) Ellipsis 主要用于用戶自定義容器類(lèi)型的切片操作,但是在我搜索了許久之后發(fā)現(xiàn)用 Ellipsis 來(lái)實(shí)現(xiàn)所謂的切片操作的貌似只有 Numpy。
使用 Python 做數(shù)據(jù)分析、挖掘或機(jī)器學(xué)習(xí)相關(guān)的朋友一定對(duì) Numpy 高性能的科學(xué)計(jì)算庫(kù)并不陌生。在 Numpy 中我們真正的使用 Ellipsis 來(lái)進(jìn)行切片索引:
- >>> import numpy as np
- >>> arr = np.arange(9).reshape((3,3))
- >>> arr
- array([[0, 1, 2],
- [3, 4, 5],
- [6, 7, 8]])
需要注意的是,Ellipsis 主要是對(duì)二維以上的數(shù)組才起作用:
- >>> arr[...,1:2]
- array([[1],
- [4],
- [7]])
- >>> arr[2, ...]
- array([6, 7, 8])
從結(jié)果中我們看到,Ellipsis 三個(gè)省略號(hào)的寫(xiě)法其實(shí)就等價(jià)于 arr[:, 1:2] 冒號(hào)的寫(xiě)法。但是在使用過(guò)程中 Ellipsis 只能出現(xiàn)一次:
- >>> 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 中出現(xiàn)的意義在于,當(dāng)你的數(shù)組是高維的數(shù)組時(shí),那么可以直接使用它來(lái)作為選取其他維度的等價(jià)寫(xiě)法,以下例子來(lái)源于 Numpy 官方文檔:
- >>> z = np.arange(81).reshape(3,3,3,3)
- >>> z[1,...,2] # 等價(jià)于 z[1, :,:, 2]
- array([[29, 32, 35],
- [38, 41, 44],
- [47, 50, 53]])
Type Hint 類(lèi)型注解
自從 PEP 484 之后,Python 解釋器開(kāi)始支持類(lèi)型注解。所謂的類(lèi)型注解無(wú)非就是在 Python 實(shí)際代碼中能像注釋那樣對(duì)當(dāng)中的一些參數(shù)或返回值添加類(lèi)型注釋,就像是這樣:
- def add(x: int, y: int) -> int:
- return x + y
如果你是有使用過(guò) Java 或者 Go 這類(lèi)對(duì)類(lèi)型注解要求較為嚴(yán)格的編譯型語(yǔ)言,那么相信對(duì)此并不陌生,無(wú)論是變量還是方法,都要寫(xiě)上對(duì)應(yīng)的類(lèi)型以防編譯報(bào)錯(cuò);但即便沒(méi)有接觸過(guò)這類(lèi)編譯型語(yǔ)言也不要緊,將其理解為注釋即可,這樣的注釋是能被編輯器或 IDE 所支持,在你要查看函數(shù)定義或文檔時(shí)會(huì)給予提示。
但是 Type Hint 僅僅只是一種「協(xié)定」,告訴別人你的方法里參數(shù)是如何、最后返回的是什么僅此而已,無(wú)論是加與不加都不會(huì)影響最終代碼的效果,影響的僅僅只是代碼的可讀性罷了。
如果你的方法有多個(gè)返回值,我們不可能對(duì)每個(gè)返回值的類(lèi)型都寫(xiě)上注解,因此這時(shí) Ellipsis 對(duì)象就派上了用場(chǎng)。根據(jù)官方文檔給出的說(shuō)明,我們完全可以像這樣來(lái)進(jìn)行類(lèi)型注解:
- 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]
這樣的寫(xiě)法本質(zhì)上就是 *args 的作用,表示同類(lèi)型的可變長(zhǎng)度元組。如果你將 Tuple 換成是 List,那么解釋器會(huì)報(bào)錯(cuò),因?yàn)?*args 在方法中的表現(xiàn)就是元組,那么作為注解的 Ellipsis 也應(yīng)如此。這可能也就說(shuō)明為什么在 Tuple 注解中不報(bào)錯(cuò)了。
FastAPI 中的必選參數(shù)
目前正流行開(kāi)來(lái)的高性能 Web 框架 FastAPI 中,也應(yīng)用了 Ellipsis。它用以表示參數(shù)是必填項(xiàng),這在 Swagger 頁(yè)面更能直觀體現(xiàn)。
- # 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)
啟動(dòng)服務(wù)之后,在瀏覽器中輸入 http://127.0.0.1:5000/docs 便能進(jìn)入到服務(wù)的 Swagger 頁(yè)面中,在上述例子中如果 name 參數(shù)并非是個(gè)必要的參數(shù)時(shí),在 Swagger 頁(yè)面中不會(huì)看到任何標(biāo)識(shí),即便我們不帶上 name 參數(shù)也能進(jìn)行請(qǐng)求:
非必要參數(shù)
但當(dāng)我們加上了一個(gè) Query() 方法,并將其 Ellipsis 對(duì)象丟到當(dāng)中時(shí),不僅會(huì)給參數(shù)加上 required 的標(biāo)識(shí),同時(shí)還對(duì)傳入的字符串長(zhǎng)度進(jìn)行了限制。
必要參數(shù)
除了參數(shù)之外,在 FastAPI 中你還可以在請(qǐng)求體、路徑、字段等多個(gè)地方使用 Ellipsis 對(duì)象。
「?jìng)巍?pass 寫(xiě)法
Ellipsis 有時(shí)候還可以作為 pass 的一種「?jìng)巍箤?xiě)法,比如這樣:
- def greet():
- ... #等價(jià)于 pass
這其實(shí)就和 # 注釋符號(hào)與六個(gè)引號(hào)的長(zhǎng)字符串注釋類(lèi)似。但實(shí)際上僅僅只是一種取巧的方法,實(shí)際上我們可以將 ... 替換成任何值或?qū)ο?,?None、1、True 等,因?yàn)樵诜椒ㄖ胁](méi)有顯示聲明返回的對(duì)象,所以無(wú)論我們寫(xiě)什么最后的效果都是一樣的。
但使用 Ellipsis 對(duì)象來(lái)作為 pass 關(guān)鍵字的替代品從「視覺(jué)」上來(lái)說(shuō)或許還有點(diǎn)「意猶未盡」的意思。
當(dāng)然如果在你和同事協(xié)作時(shí),隨手寫(xiě)下這樣一個(gè)省略號(hào),沒(méi)準(zhǔn)隱含著你對(duì)同事 Coding 的無(wú)奈,或者是對(duì)禿頭的憂愁(逃)