NEP 16 — 用於識別「鴨子陣列」的抽象基底類別#
- 作者:
Nathaniel J. Smith <njs@pobox.com>
- 狀態:
已撤回
- 類型:
標準追蹤
- 建立日期:
2018-03-06
- 決議:
注意
此 NEP 已撤回,以支持 NEP 22 — NumPy 陣列的鴨子型別 — 高階概述 中描述的基於協定的方法
摘要#
我們提議新增一個抽象基底類別 AbstractArray
,以便第三方類別可以宣告它們能夠「像 ndarray 一樣呱呱叫」,以及一個 asabstractarray
函數,其功能與 asarray
類似,但會讓 AbstractArray
實例保持不變。
詳細描述#
NumPy 和第三方套件中的許多函數,都以類似以下的程式碼開始
def myfunc(a, b):
a = np.asarray(a)
b = np.asarray(b)
...
這確保了 a
和 b
是 np.ndarray
物件,因此 myfunc
可以繼續假設它們在語義上(在 Python 層級)和記憶體儲存方式(在 C 層級)都像 ndarray 一樣運作。但是,許多這些函數僅在 Python 層級與陣列一起運作,這表示它們實際上不需要 ndarray
物件本身:它們可以與任何「像 ndarray 一樣呱呱叫」的 Python 物件(例如稀疏陣列、dask 的惰性陣列或 xarray 的標籤陣列)一樣良好地運作。
但是,目前,這些函式庫沒有辦法表達它們的物件可以像 ndarray 一樣呱呱叫,也沒有辦法讓像 myfunc
這樣的函數表達它們很樂意接受任何像 ndarray 一樣呱呱叫的東西。此 NEP 的目的是提供這兩個功能。
有時人們建議使用 np.asanyarray
來達到此目的,但不幸的是,它的語義正好相反:它保證它傳回的物件使用與 ndarray
相同的記憶體佈局,但完全沒有告訴你它的語義,這使得它在實務上幾乎不可能安全使用。實際上,與 NumPy 一起發行的兩個 ndarray
子類別 – np.matrix
和 np.ma.masked_array
– 確實具有不相容的語義,如果它們被傳遞到像 myfunc
這樣的函數,而該函數沒有將它們作為特殊情況檢查,那麼它可能會靜默地傳回不正確的結果。
宣告物件可以像陣列一樣呱呱叫#
我們可以使用兩種基本方法來檢查物件是否像陣列一樣呱呱叫。我們可以檢查類別上是否有特殊屬性
def quacks_like_array(obj):
return bool(getattr(type(obj), "__quacks_like_array__", False))
或者,我們可以定義一個 抽象基底類別 (ABC)
def quacks_like_array(obj):
return isinstance(obj, AbstractArray)
如果你查看 ABC 的運作方式,這基本上等同於保留一組已宣告實作 AbstractArray
介面的全域類型,然後檢查其成員資格。
在這些方法之間,ABC 方法似乎有許多優點
這是 Python 的標準、「一種顯而易見的方式」來做到這一點。
可以內省 ABC(例如,
help(np.AbstractArray)
會做一些有用的事情)。ABC 可以提供有用的混入方法。
ABC 與其他功能整合,例如 mypy 類型檢查、
functools.singledispatch
等。
一個明顯需要檢查的事情是,此選擇是否會影響速度。在使用附加的基準腳本在 CPython 3.7 預發行版(修訂版 c4d77a661138d,自行編譯,無 PGO)上,在執行 Linux 的 Thinkpad T450s 上,我們發現
np.asarray(ndarray_obj) 330 ns
np.asarray([]) 1400 ns
Attribute check, success 80 ns
Attribute check, failure 80 ns
ABC, success via subclass 340 ns
ABC, success via register() 700 ns
ABC, failure 370 ns
注意
包含前兩行是為了將其他行放在上下文中。
此處使用 3.7 是因為
getattr
和 ABC 都在此版本中獲得了實質性的優化,並且更具代表 Python 的長期未來。(失敗的getattr
不再一定會建構例外物件,並且 ABC 在 C 中重新實作。)「成功」行是指
quacks_like_array
將傳回 True 的情況。「失敗」行是指它將傳回 False 的情況。ABC 的第一個測量是像這樣定義的子類別
class MyArray(AbstractArray): ...
第二個測量是像這樣定義的子類別
class MyArray: ... AbstractArray.register(MyArray)
我不知道為什麼這些之間存在如此大的差異。
在實務上,無論哪種方式,我們都只會在首先檢查眾所周知的類型(如 ndarray
、list
等)之後才執行完整測試。這是 NumPy 目前檢查其他雙底線屬性的方式,並且相同的概念適用於此處的任一種方法。因此,這些數字不會影響常見情況,只會影響我們實際擁有 AbstractArray
,或者最終將通過 __array__
或 __array_interface__
或最終成為物件陣列的另一個第三方物件的情況。
因此,總之,使用 ABC 會比使用屬性稍慢,但這不會影響最常見的路徑,並且減速的幅度相當小(在已經花費更長時間的操作中約為 250 奈秒)。此外,我們可以潛在地進一步優化此操作(例如,通過保留一個小的 LRU 快取,其中包含已知是 AbstractArray 子類別的類型,假設大多數程式碼一次只會使用這些類型中的一兩種),並且非常不清楚這是否重要 – 如果 asarray
無操作傳遞的速度是出現在效能分析中的瓶頸,那麼可能我們已經讓它們更快了!(快速路徑處理這個問題是微不足道的,但我們沒有。)
鑑於 ABC 的語義和可用性優勢,這似乎是可以接受的權衡。
asabstractarray
的規格#
給定 AbstractArray
,asabstractarray
的定義很簡單
def asabstractarray(a, dtype=None):
if isinstance(a, AbstractArray):
if dtype is not None and dtype != a.dtype:
return a.astype(dtype)
return a
return asarray(a, dtype=dtype)
需要注意的事項
asarray
也接受order=
參數,但我們在此處不包含它,因為它與記憶體表示的詳細資訊有關,而此函數的重點是您使用它來宣告您不關心記憶體表示的詳細資訊。使用
astype
方法允許a
物件決定如何為其特定類型實作轉換。為了與
asarray
嚴格相容,當 dtype 已經正確時,我們跳過呼叫astype
。比較>>> a = np.arange(10) # astype() always returns a view: >>> a.astype(a.dtype) is a False # asarray() returns the original object if possible: >>> np.asarray(a, dtype=a.dtype) is a True
如果您繼承自 AbstractArray
,您究竟在承諾什麼?#
這可能會隨著時間推移而完善。當然,理想情況是您的類別應該與真正的 ndarray
沒有區別,但除了使用者的期望之外,沒有任何東西強制執行這一點。在實務上,宣告您的類別實作 AbstractArray
介面僅表示它將開始通過 asabstractarray
,因此通過子類別化它,您表示如果某些程式碼適用於 ndarray
但對於您的類別中斷,那麼您願意接受有關該問題的錯誤報告。
首先,我們應該宣告 __array_ufunc__
為抽象方法,並新增 NDArrayOperatorsMixin
方法作為混入方法。
將 astype
宣告為 @abstractmethod
可能也很有意義,因為 asabstractarray
會使用它。我們可能還想繼續新增一些基本屬性,例如 ndim
、shape
、dtype
。
新增新的抽象方法會有點棘手,因為 ABC 會在子類別化時強制執行這些方法;因此,僅僅新增一個 @abstractmethod 將會是向後相容性中斷。如果這成為一個問題,那麼我們可以使用一些技巧來實作一個 @upcoming_abstractmethod 裝飾器,該裝飾器僅在缺少方法時發出警告,並將其視為常規棄用週期。(在這種情況下,我們將棄用的東西是「支援缺少功能 X 的抽象陣列」。)
命名#
ABC 的名稱並不重要,因為它只會在極少數情況下被引用,並且在相對專業的情況下被引用。函數的名稱非常重要,因為大多數現有的 asarray
實例都應該替換為此函數,並且在未來,除非他們有特定原因要使用 asarray
,否則每個人都應該預設使用它。這表示它的名稱確實應該比 asarray
更短且更容易記住... 這很困難。我在這個草稿中使用了 asabstractarray
,但我對此並不真正滿意,因為它太長了,如果沒有無休止的勸誡,人們不太可能養成習慣使用它。
一種選擇實際上是變更 asarray
的語義,以便它讓 AbstractArray
物件保持不變。但我擔心可能會有很多程式碼呼叫 asarray
,然後將結果傳遞到一些 C 函數中,這些函數不會執行任何進一步的類型檢查(因為它知道其呼叫者已經使用了 asarray
)。如果我們允許 asarray
傳回 AbstractArray
物件,然後有人呼叫其中一個 C 包裝函式並將 AbstractArray
物件(例如稀疏陣列)傳遞給它,那麼他們將會收到 segmentation fault。現在,在相同的情況下,asarray
將改為調用物件的 __array__
方法,或使用緩衝區介面建立視圖,或傳遞具有物件 dtype 的陣列,或引發錯誤,或類似情況。可能在大多數情況下,這些結果實際上都不是理想的,所以也許讓它成為 segmentation fault 會是可以接受的?但鑑於我們不知道此類程式碼有多常見,這很危險。另一方面,如果我們從頭開始,那麼這可能是理想的解決方案。
我們不能使用 asanyarray
或 array
,因為這些名稱已被佔用。
還有其他想法嗎? np.cast
、np.coerce
?
實作#
將
NDArrayOperatorsMixin
重新命名為AbstractArray
(留下別名以實現向後相容性)並使其成為 ABC。新增
asabstractarray
(或我們最終稱呼它的任何名稱),以及可能的 C API 等效項。開始將 NumPy 內部函數遷移到在適當的地方使用
asabstractarray
。
向後相容性#
這純粹是一個新功能,因此沒有相容性問題。(除非我們決定變更 asarray
本身的語義。)
已拒絕的替代方案#
已經提出的一個建議是為陣列介面的不同子集定義多個抽象類別。本提案中的任何內容都沒有阻止 NumPy 或第三方在未來執行此操作,但是很難提前猜測哪些子集會有用。此外,「完整的 ndarray 介面」是現有函式庫被編寫為期望的東西(因為它們適用於實際的 ndarray)和測試的東西(因為它們使用實際的 ndarray 進行測試),因此它絕對是最容易開始的地方。
討論連結#
附錄:基準腳本#
import perf
import abc
import numpy as np
class NotArray:
pass
class AttrArray:
__array_implementer__ = True
class ArrayBase(abc.ABC):
pass
class ABCArray1(ArrayBase):
pass
class ABCArray2:
pass
ArrayBase.register(ABCArray2)
not_array = NotArray()
attr_array = AttrArray()
abc_array_1 = ABCArray1()
abc_array_2 = ABCArray2()
# Make sure ABC cache is primed
isinstance(not_array, ArrayBase)
isinstance(abc_array_1, ArrayBase)
isinstance(abc_array_2, ArrayBase)
runner = perf.Runner()
def t(name, statement):
runner.timeit(name, statement, globals=globals())
t("np.asarray([])", "np.asarray([])")
arrobj = np.array([])
t("np.asarray(arrobj)", "np.asarray(arrobj)")
t("attr, False",
"getattr(not_array, '__array_implementer__', False)")
t("attr, True",
"getattr(attr_array, '__array_implementer__', False)")
t("ABC, False", "isinstance(not_array, ArrayBase)")
t("ABC, True, via inheritance", "isinstance(abc_array_1, ArrayBase)")
t("ABC, True, via register", "isinstance(abc_array_2, ArrayBase)")
著作權#
本文檔已置於公共領域。