子類別化 ndarray#

簡介#

子類別化 ndarray 相對簡單,但與其他 Python 物件相比,它有一些複雜性。在此頁面中,我們將解釋允許您子類別化 ndarray 的機制,以及實作子類別的含義。

ndarrays 和物件建立#

子類別化 ndarray 的複雜性在於,ndarray 類別的新實例可以透過三種不同的方式產生。這些方式是

  1. 顯式建構子呼叫 - 如 MySubClass(params) 中所示。這是 Python 實例建立的常用途徑。

  2. 視圖轉換 - 將現有的 ndarray 轉換為給定的子類別

  3. 從範本建立新實例 - 從範本實例建立新實例。範例包括從子類別化陣列傳回切片、從 ufuncs 建立傳回類型,以及複製陣列。請參閱 從範本建立新實例 以取得更多詳細資訊

後兩種是 ndarray 的特性 - 為了支援陣列切片之類的功能。子類別化 ndarray 的複雜性是由於 numpy 必須支援後兩種實例建立途徑的機制。

何時使用子類別化#

除了子類別化 NumPy 陣列的額外複雜性之外,子類別可能會遇到意外的行為,因為某些函式可能會將子類別轉換為基底類別,並「忘記」與子類別相關聯的任何額外資訊。如果您使用您尚未明確測試過的 NumPy 方法或函式,這可能會導致令人驚訝的行為。

另一方面,與其他互通性方法相比,子類別化可能很有用,因為許多事情會「直接運作」。

這表示子類別化可能是一種方便的方法,而且長期以來它也經常是唯一可用的方法。然而,NumPy 現在提供了「與 NumPy 的互通性」中描述的其他互通性協定。對於許多用例,這些互通性協定現在可能更適合或補充子類別化的使用。

如果符合以下條件,子類別化可能很適合

  • 您不太擔心可維護性或您自己以外的使用者:子類別將更快實作,並且可以「按需」新增額外的互通性。而且由於使用者很少,可能的意外不是問題。

  • 您不認為子類別資訊被忽略或靜默遺失是有問題的。一個範例是 np.memmap,其中「忘記」資料被記憶體映射不會導致錯誤的結果。NumPy 遮罩陣列是一個有時會讓使用者感到困惑的子類別範例。當它們被引入時,子類別化是實作的唯一方法。然而,今天我們可能會嘗試避免子類別化,而僅依賴互通性協定。

請注意,子類別作者也可能希望研究 與 NumPy 的互通性,以支援更複雜的用例或解決令人驚訝的行為。

astropy.units.Quantityxarray 是與 NumPy 良好互通的類陣列物件範例。Astropy 的 Quantity 是一個同時使用子類別化和互通性協定的雙重方法的範例。

視圖轉換#

視圖轉換是標準的 ndarray 機制,您可以使用它來取得任何子類別的 ndarray,並將陣列的視圖傳回為另一個(指定的)子類別

>>> import numpy as np
>>> # create a completely useless ndarray subclass
>>> class C(np.ndarray): pass
>>> # create a standard ndarray
>>> arr = np.zeros((3,))
>>> # take a view of it, as our useless subclass
>>> c_arr = arr.view(C)
>>> type(c_arr)
<class '__main__.C'>

從範本建立新實例#

ndarray 子類別的新實例也可以透過與 視圖轉換 非常相似的機制產生,當 numpy 發現它需要從範本實例建立新實例時。最明顯的發生地點是當您取得子類別化陣列的切片時。例如

>>> v = c_arr[1:]
>>> type(v) # the view is of type 'C'
<class '__main__.C'>
>>> v is c_arr # but it's a new instance
False

切片是原始 c_arr 資料的視圖。因此,當我們從 ndarray 取得視圖時,我們會傳回一個新的 ndarray,其類別相同,並指向原始資料中的資料。

在使用 ndarray 的其他地方,我們也需要這樣的視圖,例如複製陣列 (c_arr.copy())、建立 ufunc 輸出陣列(另請參閱 用於 ufuncs 和其他函式的 __array_wrap__)以及縮減方法(如 c_arr.mean())。

視圖轉換和從範本建立新實例的關係#

這些路徑都使用相同的機制。我們在此區分它們,因為它們會導致您方法的不同輸入。具體而言,視圖轉換 表示您已從 ndarray 的任何潛在子類別建立陣列類型的新實例。從範本建立新實例 表示您已從預先存在的實例建立類別的新實例,讓您可以 - 例如 - 複製特定於子類別的屬性。

子類別化的含義#

如果我們子類別化 ndarray,我們不僅需要處理陣列類型的顯式建構,還需要處理 視圖轉換從範本建立新實例。NumPy 具有執行此操作的機制,正是這種機制使子類別化稍微不標準。

ndarray 用於支援子類別中的視圖和從範本建立新實例的機制有兩個方面。

第一個是使用 ndarray.__new__ 方法來完成物件初始化的主要工作,而不是更常用的 __init__ 方法。第二個是使用 __array_finalize__ 方法,讓子類別可以在建立視圖和從範本建立新實例後進行清理。

關於 __new____init__ 的 Python 簡要入門#

__new__ 是一種標準的 Python 方法,如果存在,則在我們建立類別實例時在 __init__ 之前呼叫。有關更多詳細資訊,請參閱 python __new__ 文件

例如,考慮以下 Python 程式碼

>>> class C:
...     def __new__(cls, *args):
...         print('Cls in __new__:', cls)
...         print('Args in __new__:', args)
...         # The `object` type __new__ method takes a single argument.
...         return object.__new__(cls)
...     def __init__(self, *args):
...         print('type(self) in __init__:', type(self))
...         print('Args in __init__:', args)

這表示我們會得到

>>> c = C('hello')
Cls in __new__: <class '__main__.C'>
Args in __new__: ('hello',)
type(self) in __init__: <class '__main__.C'>
Args in __init__: ('hello',)

當我們呼叫 C('hello') 時,__new__ 方法會將其自身的類別作為第一個引數,並傳遞引數,即字串 'hello'。在 python 呼叫 __new__ 之後,它通常(見下文)會呼叫我們的 __init__ 方法,其中 __new__ 的輸出作為第一個引數(現在是類別實例),而後跟著傳遞的引數。

如您所見,物件可以在 __new__ 方法或 __init__ 方法或兩者中初始化,實際上 ndarray 沒有 __init__ 方法,因為所有初始化都在 __new__ 方法中完成。

為什麼使用 __new__ 而不是僅使用常用的 __init__?因為在某些情況下,例如 ndarray,我們希望能夠傳回一些其他類別的物件。考慮以下情況

class D(C):
    def __new__(cls, *args):
        print('D cls is:', cls)
        print('D args in __new__:', args)
        return C.__new__(C, *args)

    def __init__(self, *args):
        # we never get here
        print('In D __init__')

這表示

>>> obj = D('hello')
D cls is: <class 'D'>
D args in __new__: ('hello',)
Cls in __new__: <class 'C'>
Args in __new__: ('hello',)
>>> type(obj)
<class 'C'>

C 的定義與之前相同,但對於 D__new__ 方法傳回類別 C 的實例,而不是 D 的實例。請注意,D__init__ 方法不會被呼叫。一般而言,當 __new__ 方法傳回的物件的類別與定義它的類別不同時,該類別的 __init__ 方法不會被呼叫。

ndarray 類別的子類別能夠傳回保留類別類型的視圖的方式就是這樣。當取得視圖時,標準的 ndarray 機制會使用類似以下的方式建立新的 ndarray 物件

obj = ndarray.__new__(subtype, shape, ...

其中 subtype 是子類別。因此,傳回的視圖與子類別的類別相同,而不是類別 ndarray

這解決了傳回相同類型視圖的問題,但現在我們有一個新問題。ndarray 的機制可以透過這種方式設定類別,在其用於取得視圖的標準方法中,但 ndarray __new__ 方法不知道我們在自己的 __new__ 方法中為了設定屬性等等所做的事情。(題外話 - 為什麼不呼叫 obj = subdtype.__new__(... 呢?因為我們可能沒有具有相同呼叫簽名的 __new__ 方法)。

__array_finalize__ 的作用#

__array_finalize__ 是 numpy 提供的機制,允許子類別處理建立新實例的各種方式。

請記住,子類別實例可以透過以下三種方式產生

  1. 顯式建構子呼叫 (obj = MySubClass(params))。這將呼叫通常的 MySubClass.__new__ 序列,然後(如果存在)呼叫 MySubClass.__init__

  2. 視圖轉換

  3. 從範本建立新實例

我們的 MySubClass.__new__ 方法僅在顯式建構子呼叫的情況下被呼叫,因此我們不能依賴 MySubClass.__new__MySubClass.__init__ 來處理視圖轉換和從範本建立新實例。事實證明,對於所有三種物件建立方法,都會呼叫 MySubClass.__array_finalize__,因此這是我們物件建立內務管理通常所在的位置。

  • 對於顯式建構子呼叫,我們的子類別需要建立其自身類別的新 ndarray 實例。實際上,這表示我們(程式碼的作者)需要呼叫 ndarray.__new__(MySubClass,...)、類別階層準備好的 super().__new__(cls, ...) 呼叫,或轉換現有陣列的視圖(見下文)

  • 對於視圖轉換和從範本建立新實例,會在 C 層級呼叫相當於 ndarray.__new__(MySubClass,... 的程式碼。

__array_finalize__ 接收的引數對於上述三種實例建立方法有所不同。

以下程式碼允許我們查看呼叫序列和引數

import numpy as np

class C(np.ndarray):
    def __new__(cls, *args, **kwargs):
        print('In __new__ with class %s' % cls)
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        # in practice you probably will not need or want an __init__
        # method for your subclass
        print('In __init__ with class %s' % self.__class__)

    def __array_finalize__(self, obj):
        print('In array_finalize:')
        print('   self type is %s' % type(self))
        print('   obj type is %s' % type(obj))

現在

>>> # Explicit constructor
>>> c = C((10,))
In __new__ with class <class 'C'>
In array_finalize:
   self type is <class 'C'>
   obj type is <type 'NoneType'>
In __init__ with class <class 'C'>
>>> # View casting
>>> a = np.arange(10)
>>> cast_a = a.view(C)
In array_finalize:
   self type is <class 'C'>
   obj type is <type 'numpy.ndarray'>
>>> # Slicing (example of new-from-template)
>>> cv = c[:1]
In array_finalize:
   self type is <class 'C'>
   obj type is <class 'C'>

__array_finalize__ 的簽名是

def __array_finalize__(self, obj):

可以看到,super 呼叫(前往 ndarray.__new__)將新物件(我們自己的類別 (self))以及從中取得視圖的物件 (obj) 傳遞給 __array_finalize__。從上面的輸出中可以看到,self 始終是我們子類別的新建立實例,而 obj 的類型對於三種實例建立方法有所不同

  • 從顯式建構子呼叫時,objNone

  • 從視圖轉換呼叫時,obj 可以是 ndarray 的任何子類別的實例,包括我們自己的子類別。

  • 在從範本建立新實例中呼叫時,obj 是我們自己的子類別的另一個實例,我們可以使用它來更新新的 self 實例。

由於 __array_finalize__ 是唯一始終看到正在建立新實例的方法,因此它是填寫新物件屬性的實例預設值以及其他任務的合理位置。

透過範例可能會更清楚。

簡單範例 - 將額外屬性新增至 ndarray#

import numpy as np

class InfoArray(np.ndarray):

    def __new__(subtype, shape, dtype=float, buffer=None, offset=0,
                strides=None, order=None, info=None):
        # Create the ndarray instance of our type, given the usual
        # ndarray input arguments.  This will call the standard
        # ndarray constructor, but return an object of our type.
        # It also triggers a call to InfoArray.__array_finalize__
        obj = super().__new__(subtype, shape, dtype,
                              buffer, offset, strides, order)
        # set the new 'info' attribute to the value passed
        obj.info = info
        # Finally, we must return the newly created object:
        return obj

    def __array_finalize__(self, obj):
        # ``self`` is a new object resulting from
        # ndarray.__new__(InfoArray, ...), therefore it only has
        # attributes that the ndarray.__new__ constructor gave it -
        # i.e. those of a standard ndarray.
        #
        # We could have got to the ndarray.__new__ call in 3 ways:
        # From an explicit constructor - e.g. InfoArray():
        #    obj is None
        #    (we're in the middle of the InfoArray.__new__
        #    constructor, and self.info will be set when we return to
        #    InfoArray.__new__)
        if obj is None: return
        # From view casting - e.g arr.view(InfoArray):
        #    obj is arr
        #    (type(obj) can be InfoArray)
        # From new-from-template - e.g infoarr[:3]
        #    type(obj) is InfoArray
        #
        # Note that it is here, rather than in the __new__ method,
        # that we set the default value for 'info', because this
        # method sees all creation of default objects - with the
        # InfoArray.__new__ constructor, but also with
        # arr.view(InfoArray).
        self.info = getattr(obj, 'info', None)
        # We do not need to return anything

使用物件看起來像這樣

>>> obj = InfoArray(shape=(3,)) # explicit constructor
>>> type(obj)
<class 'InfoArray'>
>>> obj.info is None
True
>>> obj = InfoArray(shape=(3,), info='information')
>>> obj.info
'information'
>>> v = obj[1:] # new-from-template - here - slicing
>>> type(v)
<class 'InfoArray'>
>>> v.info
'information'
>>> arr = np.arange(10)
>>> cast_arr = arr.view(InfoArray) # view casting
>>> type(cast_arr)
<class 'InfoArray'>
>>> cast_arr.info is None
True

此類別不是很有用,因為它具有與裸 ndarray 物件相同的建構子,包括傳入緩衝區和形狀等等。我們可能更希望建構子能夠從常用的 numpy 呼叫 np.array 取得已形成的 ndarray 並傳回物件。

稍微更實際的範例 - 新增至現有陣列的屬性#

這是一個類別,它接受已存在的標準 ndarray,將其轉換為我們的類型,並新增一個額外屬性。

import numpy as np

class RealisticInfoArray(np.ndarray):

    def __new__(cls, input_array, info=None):
        # Input array is an already formed ndarray instance
        # We first cast to be our class type
        obj = np.asarray(input_array).view(cls)
        # add the new attribute to the created instance
        obj.info = info
        # Finally, we must return the newly created object:
        return obj

    def __array_finalize__(self, obj):
        # see InfoArray.__array_finalize__ for comments
        if obj is None: return
        self.info = getattr(obj, 'info', None)

所以

>>> arr = np.arange(5)
>>> obj = RealisticInfoArray(arr, info='information')
>>> type(obj)
<class 'RealisticInfoArray'>
>>> obj.info
'information'
>>> v = obj[1:]
>>> type(v)
<class 'RealisticInfoArray'>
>>> v.info
'information'

用於 ufuncs 的 __array_ufunc__#

子類別可以透過覆寫預設的 ndarray.__array_ufunc__ 方法來覆寫在它上面執行 numpy ufuncs 時發生的情況。此方法會取代 ufunc 執行,並且應該傳回運算的結果,或者如果請求的運算未實作,則傳回 NotImplemented

__array_ufunc__ 的簽名是

def __array_ufunc__(ufunc, method, *inputs, **kwargs):
  • ufunc 是被呼叫的 ufunc 物件。

  • method 是一個字串,指示 Ufunc 的呼叫方式,可以是 "__call__" 以指示它是直接呼叫的,或者是其 方法 之一:"reduce""accumulate""reduceat""outer""at"

  • inputsufunc 的輸入引數的元組

  • kwargs 包含傳遞給函式的任何選用或關鍵字引數。這包括任何 out 引數,這些引數始終包含在元組中。

典型的實作會轉換任何作為自身類別實例的輸入或輸出,使用 super() 將所有內容傳遞給超類別,最後在可能的回溯轉換後傳回結果。以下範例取自 _core/tests/test_umath.py 中的測試案例 test_ufunc_override_with_super

input numpy as np

class A(np.ndarray):
    def __array_ufunc__(self, ufunc, method, *inputs, out=None, **kwargs):
        args = []
        in_no = []
        for i, input_ in enumerate(inputs):
            if isinstance(input_, A):
                in_no.append(i)
                args.append(input_.view(np.ndarray))
            else:
                args.append(input_)

        outputs = out
        out_no = []
        if outputs:
            out_args = []
            for j, output in enumerate(outputs):
                if isinstance(output, A):
                    out_no.append(j)
                    out_args.append(output.view(np.ndarray))
                else:
                    out_args.append(output)
            kwargs['out'] = tuple(out_args)
        else:
            outputs = (None,) * ufunc.nout

        info = {}
        if in_no:
            info['inputs'] = in_no
        if out_no:
            info['outputs'] = out_no

        results = super().__array_ufunc__(ufunc, method, *args, **kwargs)
        if results is NotImplemented:
            return NotImplemented

        if method == 'at':
            if isinstance(inputs[0], A):
                inputs[0].info = info
            return

        if ufunc.nout == 1:
            results = (results,)

        results = tuple((np.asarray(result).view(A)
                         if output is None else output)
                        for result, output in zip(results, outputs))
        if results and isinstance(results[0], A):
            results[0].info = info

        return results[0] if len(results) == 1 else results

因此,此類別實際上沒有執行任何有趣的事情:它只是將其自身的所有實例轉換為常規 ndarray(否則,我們會得到無限遞迴!),並新增一個 info 字典,指示它轉換了哪些輸入和輸出。因此,例如,

>>> a = np.arange(5.).view(A)
>>> b = np.sin(a)
>>> b.info
{'inputs': [0]}
>>> b = np.sin(np.arange(5.), out=(a,))
>>> b.info
{'outputs': [0]}
>>> a = np.arange(5.).view(A)
>>> b = np.ones(1).view(A)
>>> c = a + b
>>> c.info
{'inputs': [0, 1]}
>>> a += b
>>> a.info
{'inputs': [0, 1], 'outputs': [0]}

請注意,另一種方法是使用 getattr(ufunc, methods)(*inputs, **kwargs) 而不是 super 呼叫。對於此範例,結果將相同,但如果另一個運算元也定義了 __array_ufunc__,則會有所不同。例如,假設我們評估 np.add(a, b),其中 b 是另一個類別 B 的實例,該類別具有覆寫。如果您像範例中一樣使用 super,則 ndarray.__array_ufunc__ 會注意到 b 具有覆寫,這表示它本身無法評估結果。因此,它會傳回 NotImplemented,我們的類別 A 也是如此。然後,控制權將傳遞給 b,它要么知道如何處理我們並產生結果,要么不知道並傳回 NotImplemented,引發 TypeError

相反,如果我們將 super 呼叫替換為 getattr(ufunc, method),我們實際上會執行 np.add(a.view(np.ndarray), b)。同樣,將會呼叫 B.__array_ufunc__,但現在它將 ndarray 視為另一個引數。它很可能知道如何處理此問題,並向我們傳回 B 類別的新實例。我們的範例類別未設定為處理此問題,但如果例如要使用 __array_ufunc__ 重新實作 MaskedArray,則這可能仍然是最佳方法。

最後請注意:如果 super 路徑適合給定的類別,則使用它的優點是它有助於建構類別階層。例如,假設我們的另一個類別 B 也在其 __array_ufunc__ 實作中使用 super,並且我們建立了一個依賴兩者的類別 C,即 class C(A, B)(為了簡單起見,沒有另一個 __array_ufunc__ 覆寫)。然後,對 C 的實例執行的任何 ufunc 都會傳遞給 A.__array_ufunc__A 中的 super 呼叫將前往 B.__array_ufunc__,而 B 中的 super 呼叫將前往 ndarray.__array_ufunc__,從而允許 AB 協作。

用於 ufuncs 和其他函式的 __array_wrap__#

在 numpy 1.13 之前,ufuncs 的行為只能使用 __array_wrap____array_prepare__ 來調整(後者現在已移除)。這兩者允許您變更 ufunc 的輸出類型,但與 __array_ufunc__ 不同,不允許您對輸入進行任何變更。希望最終棄用這些方法,但 __array_wrap__ 也被其他 numpy 函式和方法使用,例如 squeeze,因此目前仍需要它才能實現完整功能。

從概念上講,__array_wrap__ 「封裝了動作」,從而允許子類別設定傳回值的類型並更新屬性和元資料。讓我們透過範例展示其運作方式。首先,我們回到更簡單的範例子類別,但使用不同的名稱和一些列印語句

import numpy as np

class MySubClass(np.ndarray):

    def __new__(cls, input_array, info=None):
        obj = np.asarray(input_array).view(cls)
        obj.info = info
        return obj

    def __array_finalize__(self, obj):
        print('In __array_finalize__:')
        print('   self is %s' % repr(self))
        print('   obj is %s' % repr(obj))
        if obj is None: return
        self.info = getattr(obj, 'info', None)

    def __array_wrap__(self, out_arr, context=None, return_scalar=False):
        print('In __array_wrap__:')
        print('   self is %s' % repr(self))
        print('   arr is %s' % repr(out_arr))
        # then just call the parent
        return super().__array_wrap__(self, out_arr, context, return_scalar)

我們在新陣列的實例上執行 ufunc

>>> obj = MySubClass(np.arange(5), info='spam')
In __array_finalize__:
   self is MySubClass([0, 1, 2, 3, 4])
   obj is array([0, 1, 2, 3, 4])
>>> arr2 = np.arange(5)+1
>>> ret = np.add(arr2, obj)
In __array_wrap__:
   self is MySubClass([0, 1, 2, 3, 4])
   arr is array([1, 3, 5, 7, 9])
In __array_finalize__:
   self is MySubClass([1, 3, 5, 7, 9])
   obj is MySubClass([0, 1, 2, 3, 4])
>>> ret
MySubClass([1, 3, 5, 7, 9])
>>> ret.info
'spam'

請注意,ufunc (np.add) 已呼叫 __array_wrap__ 方法,其中引數 selfobj,而 out_arr 為加法的 (ndarray) 結果。反過來,預設的 __array_wrap__ (ndarray.__array_wrap__) 已將結果轉換為類別 MySubClass,並呼叫 __array_finalize__ - 因此複製了 info 屬性。這一切都在 C 層級發生。

但是,我們可以做任何我們想做的事情

class SillySubClass(np.ndarray):

    def __array_wrap__(self, arr, context=None, return_scalar=False):
        return 'I lost your data'
>>> arr1 = np.arange(5)
>>> obj = arr1.view(SillySubClass)
>>> arr2 = np.arange(5)
>>> ret = np.multiply(obj, arr2)
>>> ret
'I lost your data'

因此,透過為我們的子類別定義特定的 __array_wrap__ 方法,我們可以調整 ufuncs 的輸出。__array_wrap__ 方法需要 self,然後是一個引數 - 它是 ufunc 或另一個 NumPy 函式的結果 - 以及一個選用參數 context。此參數由 ufuncs 作為 3 個元素的元組傳遞:(ufunc 的名稱、ufunc 的引數、ufunc 的域),但不被其他 numpy 函式傳遞。雖然如上所示,可以做其他事情,但 __array_wrap__ 應傳回其包含類別的實例。有關實作,請參閱遮罩陣列子類別。__array_wrap__ 始終傳遞一個 NumPy 陣列,該陣列可能是也可能不是子類別(通常是呼叫者的子類別)。

額外注意事項 - 自訂 __del__ 方法和 ndarray.base#

ndarray 解決的問題之一是追蹤 ndarrays 及其視圖的記憶體所有權。考慮以下情況:我們建立了一個 ndarray arr,並使用 v = arr[1:] 取得了一個切片。這兩個物件正在查看相同的記憶體。NumPy 使用 base 屬性追蹤特定陣列或視圖的資料來源

>>> # A normal ndarray, that owns its own data
>>> arr = np.zeros((4,))
>>> # In this case, base is None
>>> arr.base is None
True
>>> # We take a view
>>> v1 = arr[1:]
>>> # base now points to the array that it derived from
>>> v1.base is arr
True
>>> # Take a view of a view
>>> v2 = v1[1:]
>>> # base points to the original array that it was derived from
>>> v2.base is arr
True

一般而言,如果陣列擁有自己的記憶體(如本例中的 arr),則 arr.base 將為 None - 有一些例外情況 - 有關更多詳細資訊,請參閱 numpy 書籍。

base 屬性可用於判斷我們是否擁有視圖或原始陣列。如果我們需要知道在刪除子類別化陣列時是否需要執行某些特定的清理,這反過來可能很有用。例如,我們可能只想在刪除原始陣列時執行清理,而不是在刪除視圖時執行清理。有關這如何運作的範例,請查看 numpy._core 中的 memmap 類別。

子類別化和下游相容性#

當子類別化 ndarray 或建立模仿 ndarray 介面的 duck-type 時,您有責任決定您的 API 與 numpy 的 API 的對齊程度。為了方便起見,許多具有對應 ndarray 方法的 numpy 函式(例如,summeantakereshape)透過檢查函式的第一個引數是否具有相同名稱的方法來運作。如果存在,則會呼叫該方法,而不是將引數強制轉換為 numpy 陣列。

例如,如果您希望您的子類別或 duck-type 與 numpy 的 sum 函式相容,則此物件的 sum 方法的方法簽名應如下所示

def sum(self, axis=None, dtype=None, out=None, keepdims=False):
...

這與 np.sum 的方法簽名完全相同,因此現在如果使用者在此物件上呼叫 np.sum,numpy 將呼叫物件自己的 sum 方法並傳入簽名中上面列舉的這些引數,並且不會引發任何錯誤,因為簽名彼此完全相容。

但是,如果您決定偏離此簽名並執行類似以下的操作

def sum(self, axis=None, dtype=None):
...

此物件不再與 np.sum 相容,因為如果您呼叫 np.sum,它將傳入意外的引數 outkeepdims,從而導致引發 TypeError。

如果您希望保持與 numpy 及其後續版本(可能會新增新的關鍵字引數)的相容性,但不希望公開 numpy 的所有引數,則函式的簽名應接受 **kwargs。例如

def sum(self, axis=None, dtype=None, **unused_kwargs):
...

這個物件現在相容於 np.sum 了,因為任何多餘的參數 (也就是不是 axisdtype 的關鍵字) 都會被隱藏在 **unused_kwargs 參數中。