PEP 207 – 豐富比較
- 作者:
- Guido van Rossum <guido at python.org>, David Ascher <DavidA at ActiveState.com>
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 建立日期:
- 2000 年 7 月 25 日
- Python 版本:
- 2.1
- 公告歷史:
摘要
本 PEP 針對比較運算提出了幾項新功能
- 允許在類別與 C 擴充模組中,個別地對 <, >, <=, >=, ==, != 進行重載。
- 允許上述任何一個被重載的運算子回傳布林值以外的結果。
動機
主要動機來自 NumPy,其使用者認為 A<B 應回傳一個包含元素級比較結果的陣列;他們目前必須寫成 less(A,B),因為 A<B 只能回傳布林結果或引發例外。
另一個動機是,類型通常沒有自然的順序,但仍需要進行相等性比較。目前這類類型必須實作比較並定義一個任意的排序,僅為了能夠測試相等性。
此外,對於某些物件類型,相等性測試的實作效率遠高於排序測試;例如,長度不同的列表和字典是不相等的,但排序則需要檢查部分(或全部)項目。
先前的工作
豐富比較先前已被提出;特別是 David Ascher 在獲得 Numerical Python 的經驗後所提出。
本文件下方亦附錄了該內容。本 PEP 中的大部分資料皆源自 David 的提案。
疑慮
- 向後相容性:無論是在 Python 層級(使用
__cmp__的類別無需更動),還是在 C 層級(定義tp_compare的擴充模組無需更動,使用PyObject_Compare()的程式碼即使在比較物件使用新的豐富比較方案時也必須運作正常)。 - 當 A<B 回傳一個元素級比較的矩陣時,若在布林環境中使用此表達式是一個容易犯的錯誤。若無特別預防措施,它將永遠被視為真。此類使用方式應引發例外。
- 如果一個類別重載了 x==y 但沒有重載其他運算子,x!=y 應該計算為 not(x==y) 還是失敗?類似於 < 與 >=,或是 > 與 <= 之間的關係該如何處理?
- 同樣地,我們是否應允許由 y>x 計算出 x<y?由 not(x>y) 計算出 x<=y?由 y==x 計算出 x==y,或由 y!=x 計算出 x!=y?
- 當比較運算子回傳元素級比較時,對於如 A<B<C、
A<B and C<D、A<B or C<D等捷徑運算子該如何處理? - 對於
min()和max()、'in' 和 'not in' 運算子、list.sort()、字典鍵比較,以及其他內建操作對比較的使用該如何處理?
提議的解決方案
- 透過以下方式可達成完整的向後相容性:當物件定義了
tp_compare()但未定義tp_richcompare(),且請求豐富比較時,tp_compare()的結果將以顯而易見的方式使用。例如,若請求 "<",若tp_compare()引發例外則跟著引發例外;若tp_compare()回傳負值則結果為 1,若為零或正值則為 0,以此類推。透過以下方式可達成完整的前向相容性:當對實作了
tp_richcompare()的物件請求傳統比較時,最多會使用三次比較:首先嘗試 ==,若回傳 true 則回傳 0;接著嘗試 <,若回傳 true 則回傳 -1;接著嘗試 >,若回傳 true 則回傳 +1。若嘗試的任何運算子回傳非布林值(見下文),則轉換為布林值所引發的例外將會被透傳。若所有嘗試的運算子均未回傳 true,則嘗試傳統比較的回退機制。(我深入思考過這三種比較應嘗試的順序。曾幾何時,基於循環資料結構的比較行為,我有過一個令人信服的論點支持採取此順序。但既然該程式碼又有了變動,我不確定這是否還有區別。)
- 任何回傳布林集合而非單一布林值的類型,都應定義
nb_nonzero()以引發例外。此類類型被視為非布林值。 - == 和 != 運算子不被假設為彼此的補數(例如 IEEE 754 浮點數並不滿足此點)。若有需要,由類型自行決定是否實作。對於 < 和 >=,或 > 和 <= 亦同;有很多例子證明這些假設並不成立(例如 tabnanny)。
- Python 確實假設了自反性規則。因此,直譯器可能會將 y>x 與 x<y 對調,將 y>=x 與 x<=y 對調,並可能交換 x==y 和 x!=y 的參數。(註:Python 目前假設 x==x 永遠為真且 x!=x 永遠不為真;這不應被預設。)
- 在目前的提案中,當 A<B 回傳元素級比較的陣列時,該結果被視為非布林值,且捷徑運算子將其解釋為布林值時會引發例外。David Ascher 的提案試圖解決此問題;我認為這不值得在程式碼產生器中增加額外的複雜性。與其寫 A<B<C,你可以寫 (A<B)&(B<C)。
min()和list.sort()操作將僅使用 < 運算子;max() 將僅使用 > 運算子。'in' 和 'not in' 運算子以及字典查詢將僅使用 == 運算子。
實作提案
這與 David Ascher 的提案非常吻合。
C API
- 新函式
PyObject *PyObject_RichCompare(PyObject *, PyObject *, int)
這會執行請求的豐富比較,並回傳一個 Python 物件或引發例外。第三個參數必須是 Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT 或 Py_GE 之一。
int PyObject_RichCompareBool(PyObject *, PyObject *, int)
這會執行請求的豐富比較,回傳一個布林值:-1 代表例外,0 代表 false,1 代表 true。第三個參數必須是 Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT 或 Py_GE 之一。請注意,當
PyObject_RichCompare()回傳非布林物件時,PyObject_RichCompareBool()將會引發例外。 - 新的 typedef
typedef PyObject *(*richcmpfunc) (PyObject *, PyObject *, int);
- 類型物件中的新槽 (slot),取代備用的 tp_xxx7
richcmpfunc tp_richcompare;
這應該是一個與
PyObject_RichCompare()具有相同簽章並執行相同比較的函式。至少有一個參數是使用該 tp_richcompare 槽的類型,但另一個參數可能是不同的類型。如果函式無法比較特定組合的物件,它應回傳一個指向Py_NotImplemented的新參照。 PyObject_Compare()已修改為:若定義了豐富比較,則會嘗試使用它(但僅限於未定義傳統比較時)。
對直譯器的更動
- 每當呼叫
PyObject_Compare()旨在取得特定比較的結果時(例如在list.sort()中,當然還有 ceval.c 中的比較運算子),程式碼已改為改呼叫PyObject_RichCompare()或PyObject_RichCompareBool();如果 C 程式碼需要知道比較結果,則會對結果呼叫PyObject_IsTrue()(這可能會引發例外)。 - 大多數目前定義了比較的內建類型將被修改為定義豐富比較。(這是選擇性的;到目前為止我已轉換了列表、元組、複數和陣列,尚未確定是否會轉換其他類型。)
類別
- 類別可以定義新的特殊方法
__lt__,__le__,__eq__,__ne__,__gt__,__ge__來重載對應的運算子。(即 <, <=, ==, !=, >, >=。你一定會喜歡這種 Fortran 傳統。)如果類別也定義了__cmp__,它僅會在__lt__等方法被嘗試且回傳NotImplemented時才會被使用。
版權
此文件已歸入公有領域 (public domain)。
附錄
以下是 David Ascher 原始提案的大部分內容(版本 0.2.1,日期為 1998 年 7 月 22 日星期三 16:49:28;我省略了「目錄」、「歷史」和「補丁」部分)。它解決了上述幾乎所有疑慮。
摘要
提議一種新的機制,允許 Python 物件的比較回傳 -1, 0, 1 以外的值(或引發例外)。此機制完全向後相容,並且可以在 C PyObject 類型或 Python 類別定義層級進行控制。該提議機制包含三個協作部分
- 使用類型物件結構中的最後一個槽來儲存指向豐富比較函式的指標
- 為類別新增特殊方法
- 為內建
cmp()函式新增選用參數。
動機
目前 Python 物件的比較協定假設任何兩個 Python 物件皆可比較(自 Python 1.5 起,物件比較可能會引發例外),且任何比較的傳回值應為 -1, 0 或 1。-1 表示比較函式的第一個參數小於右側參數,+1 表示反之,0 表示兩個物件相等。雖然此機制允許建立順序關係(例如供列表物件的 sort() 方法使用),但事實證明在 Numeric Python (NumPy) 的情境下受到限制。
具體來說,NumPy 允許建立支援大多數數值運算子的多維陣列。因此
x = array((1,2,3,4)) y = array((2,2,4,4))
是兩個 NumPy 陣列。雖然它們可以進行元素級相加,
z = x + y # z == array((3,4,7,8))
但它們無法在目前的框架下進行比較——已發布版本的 NumPy 會比較指標(因此會產生垃圾資訊),這是近期增加(在 1.5 中)在比較函式中引發例外能力之前的唯一解決方案。
即使有引發例外的能力,目前的協定也使得陣列比較毫無用處。為了解決此事實,NumPy 包含數個執行比較的函式:less(), less_equal(), greater(), greater_equal(), equal(), not_equal()。這些函式回傳與其參數形狀相同(模廣播)的陣列,並根據每個元素對是否滿足比較關係,填入 0 或 1。因此,例如使用上述定義的陣列 x 和 y
less(x,y)
將會是一個包含數字 (1,0,0,0) 的陣列。
目前的提案是修改 Python 物件介面,允許 NumPy 套件使得 x < y 回傳與 less(x,y) 相同的結果。確切的回傳值由 NumPy 套件決定——此提案真正要求的是更改 Python 核心,以便擴充物件在作者選擇時,有能力回傳 -1, 0, 1 以外的結果。
現狀
在 C 層級上,目前的協定是每個物件類型定義一個 tp_compare 槽,這是一個指向接收兩個 PyObject* 參照並回傳 -1, 0 或 1 的函式指標。此函式由 C API 中定義的 PyObject_Compare() 函式呼叫。PyObject_Compare() 也由接收兩個參數的內建函式 cmp() 呼叫。
提議的機制
- 對類型物件 C 結構的更動
PyTypeObject中最後一個可用槽(迄今為止保留給未來擴充使用)被用來選擇性地儲存指向新比較函式的指標,該函式的類型為 richcmpfunc,定義如下typedef PyObject *(*richcmpfunc) Py_PROTO((PyObject *, PyObject *, int));
此函式接收三個參數。前兩個是要比較的物件,第三個是代表運算碼的整數(LT, LE, EQ, NE, GT, GE 之一)。如果此槽留空 (NULL),則不支援該物件類型的豐富比較(類別實例除外,其類別提供了下述的特殊方法)。
上述運算碼需要新增至已發布的 Python/C API 中(可能命名為 Py_LT, Py_LE 等)。
- 新增類別的特殊方法
希望支援豐富比較機制的類別必須新增下列一個或多個新特殊方法
def __lt__(self, other): ... def __le__(self, other): ... def __gt__(self, other): ... def __ge__(self, other): ... def __eq__(self, other): ... def __ne__(self, other): ...
當類別實例處於對應運算子(<, <=, >, >=, ==, 以及 != 或 <>)的左側時,會呼叫這些方法。參數 other 被設定為運算子右側的物件。這些方法的回傳值由類別實作者決定(畢竟這就是本提案的全部重點)。
如果運算子左側的物件未定義適當的豐富比較運算子(無論是在 C 層級還是透過特殊方法),則比較會被反轉,並呼叫右側物件的相反運算子,同時交換兩個物件。這假設 a < b 等同於 b > a,a <= b 等同於 b >= a,且 == 與 != 是可交換的(例如 a == b 當且僅當 b == a)。
例如,如果 obj1 是支援豐富比較協定的物件,而 x 和 y 是不支援豐富比較協定的物件,則 obj1 < x 將呼叫 obj1 的
__lt__方法,並以 x 作為第二個參數。x < obj1 將呼叫 obj1 的__gt__方法,並以 x 作為第二個參數,而 x < y 將僅使用既有的(非豐富)比較機制。上述機制使得類別即使不實作
__lt__和__le__,或是__gt__和__ge__也能運作。比較機制本可以加入更多智慧,但選擇這組有限的「交換」方式是因為它不需要基礎設施來處理回傳值的否定(否定運算)。選擇六個特殊方法而非單一(如__richcmp__)方法,是為了允許在 C 實作層級進行運算碼分派,而非在使用者定義的方法中執行。 - 為內建
cmp()新增選用參數內建
cmp()仍用於簡單比較。對於豐富比較,它會以第三個參數呼叫,該參數為 "<", "<=", ">", ">=", "==", "!=", "<>" 之一(後兩者意義相同)。當以這些字串之一作為第三個參數呼叫時,cmp()可以回傳任何 Python 物件。否則,它只能如往常一樣回傳 -1, 0 或 1。
鏈式比較 (Chained Comparisons)
問題
如果能讓比較回傳 -1, 0, 1 以外結果的物件也能用於鏈式比較(如以下範例),將會是很棒的事
x < y < z
目前,Python 將其解釋為
temp1 = x < y
if temp1:
return y < z
else:
return temp1
請注意,這需要測試比較結果的真值,並可能對右側的比較測試進行「捷徑」跳過。換句話說,比較結果的真值決定了鏈式操作的結果。這在陣列的情況下是有問題的,因為如果 x, y 和 z 是三個陣列,使用者期望
x < y < z
是一個由 0 和 1 組成的陣列,其中 1 的位置對應於 y 的元素介於 x 和 z 對應元素之間的陣列。換句話說,無論 x < y 的結果如何,右側都必須進行評估,這與解析器目前使用的機制不相容。
解決方案
Guido 提到一種可能的解決方式是更改鏈式比較所產生的程式碼,以允許陣列進行智慧化的鏈式比較。以下是我與他建議的結合。x < y < z 所產生的程式碼將等同於
temp1 = x < y
if temp1:
temp2 = y < z
return boolean_combine(temp1, temp2)
else:
return temp1
其中 boolean_combine 是一個新的函式,執行類似以下操作
def boolean_combine(a, b):
if hasattr(a, '__boolean_and__') or \
hasattr(b, '__boolean_and__'):
try:
return a.__boolean_and__(b)
except:
return b.__boolean_and__(a)
else: # standard behavior
if a:
return b
else:
return 0
其中 __boolean_and__ 特殊方法由 richcmp 函式的第三個參數的另一個值,為 C 層級類型進行實作。此方法將執行陣列的布林比較(目前在 umath 模組中實作為 logical_and ufunc)。
因此,豐富比較回傳的物件應始終測試為真,但應定義另一個特殊方法,用以建立它們與其參數的布林組合。
此解決方案的優點是允許陣列使用鏈式比較,缺點是要求比較陣列始終回傳真(在理想情況下,我會讓它們在測試真值時始終引發例外,因為測試「if a>b:」的含義有極大的歧義)。
目前處理整數比較時所使用的內聯 (inlining) 機制仍會適用,因此在最常見的情況下不會有效能損耗。
來源:https://github.com/python/peps/blob/main/peps/pep-0207.rst