NumPy ndarray 子类化完全指南:从视图转换、__new__机制到__array_ufunc__与下游兼容性
【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy
导读
本指南基于 NumPy 官方文档 doc/source/user/basics.subclassing.rst 编写,系统讲解如何继承np.ndarray创建自定义数组子类。与普通 Python 类不同,ndarray 子类的实例可能通过"显式构造、视图转换(view casting)、从模板新建(new-from-template)"三条路径产生,理解这套机制是写出健壮子类的关键。读完本文,你将掌握__new__与__array_finalize__的分工、如何为子类附加自定义属性、如何用__array_ufunc__拦截 ufunc 运算、__array_wrap__的包装语义,以及如何设计与np.sum等函数兼容的方法签名。
ndarray 子类实例的三种创建途径
子类化 ndarray 相对简单,但与普通 Python 对象相比存在一些特殊之处。根本原因在于:ndarray 子类的新实例可以通过三种不同的方式产生,任何子类实现都必须同时处理这三条路径:
- 显式构造函数调用——例如
MySubClass(params),这是 Python 实例创建的常规路径; - 视图转换(view casting)——把已有的 ndarray 强制转换为某个指定子类;
- 从模板新建(new-from-template)——根据一个模板实例创建新实例,例如对子类数组切片、ufunc 创建返回数组、复制数组(
c_arr.copy())等,详见 new-from-template 一节。
后两条路径是 ndarray 特有的——为了支持数组切片等操作,NumPy 必须能在不经过用户__new__的情况下"再造"出同类实例。子类化 ndarray 的复杂性,正源于 NumPy 为支撑这两条实例创建路径而引入的机制。
何时应该使用子类化
除了额外的复杂性,子类还可能遇到"意外行为":某些 NumPy 函数可能把子类转换为基类 ndarray,从而"遗忘"子类上附带的额外信息。如果使用了未经充分测试的 NumPy 方法或函数,就可能产生出乎意料的结果。
另一方面,与其他互操作方案相比,子类化的好处是很多东西会"自动工作",因此长期以来它既方便,也往往是唯一可行的方案。不过,NumPy 现在提供了更多的互操作协议,详见文档 Interoperability with NumPy。对许多用例而言,这些互操作协议可能更适合,或可以作为子类化的补充。
子类化比较适合以下场景:
- 你不太担心可维护性,或者用户只有你自己:子类实现更快,互操作能力可以"按需"补充;用户少,意外行为就不是大问题;
- 你认为子类信息被静默忽略或丢失是可以接受的。例如
np.memmap,即便"遗忘"了数据是内存映射这一事实,也不会导致错误结果;而 NumPy 的 masked arrays 是另一个例子——它在引入时子类化是唯一实现手段,但今天可能会尽量避免子类化而只依赖互操作协议。
此外,子类作者也应研读 Interoperability with NumPy,以支持更复杂的用例或规避意外行为。astropy.units.Quantity与xarray就是与 NumPy 互操作良好的数组对象范例,其中 Astropy 的Quantity采用了"子类化 + 互操作协议"双管齐下的策略。
视图转换(View casting)
视图转换是 ndarray 的标准机制:取任意子类的 ndarray,返回其作为另一个(指定)子类的视图:
import numpy as np # 创建一个完全无用的 ndarray 子类 class C(np.ndarray): pass # 创建一个标准 ndarray arr = np.zeros((3,)) # 取一个视图,作为我们的无用子类 c_arr = arr.view(C) type(c_arr) # <class '__main__.C'>从模板新建(New-from-template)
子类的新实例还可以通过与视图转换非常相似的机制产生:当 NumPy 需要根据一个模板实例创建新实例时。最典型的场景是对子类数组切片:
v = c_arr[1:] type(v) # 视图的类型是 'C' # <class '__main__.C'> v is c_arr # 但它是新实例 # False切片是原c_arr数据上的视图。也就是说,当从 ndarray 取视图时,会返回一个同类的、指向原数据的新 ndarray。除此之外,还有多处需要这类视图:复制数组(c_arr.copy())、创建 ufunc 输出数组(见下文array_wrap)、以及归约方法(如c_arr.mean())。
视图转换与从模板新建的关系
这两条路径使用同一套底层机制,本文之所以区分它们,是因为它们会向你的方法传入不同的参数。具体而言:
- 视图转换:从 ndarray 的任意潜在子类创建你数组类型的新实例;
- 从模板新建:从既有实例创建你类的新实例——这允许你(例如)把子类特有的属性复制到新实例上。
底层机制:__new__、__init__与__array_finalize__
子类化 ndarray 时,不仅要处理显式构造,还要处理视图转换和从模板新建。NumPy 为此提供的机制,正是子类化"不那么标准"的原因。这套机制有两个方面:
- 使用
ndarray.__new__方法承担对象初始化的主要工作,而不是更常见的__init__方法; - 使用
__array_finalize__方法让子类在视图和从模板新建之后完成"善后"。
Python 中__new__与__init__的速成课
__new__是标准 Python 方法,如果存在,它会在__init__之前被调用(参考 Python 官方文档对object.__new__的说明)。例如:
class C: def __new__(cls, *args): print('Cls in __new__:', cls) print('Args in __new__:', args) # `object` 类型的 __new__ 方法只接受一个参数 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),我们希望__new__能返回另一个类的对象:
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): # 我们永远不会走到这里 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 提供的、让子类处理各种新实例创建方式的机制。回顾子类实例的三种来源:
- 显式构造函数调用(
obj = MySubClass(params))——会走常规的MySubClass.__new__然后是(如果存在)MySubClass.__init__; - 视图转换;
- 从模板新建。
我们的MySubClass.__new__只在显式构造时被调用,因此不能依赖__new__或__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): # 实践中你的子类可能不需要 __init__ 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))观察三种路径:
# 显式构造函数 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'> # 视图转换 a = np.arange(10) cast_a = a.view(C) # In array_finalize: # self type is <class 'C'> # obj type is <type 'numpy.ndarray'> # 切片(从模板新建的示例) 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的类型随三种创建方式不同:
- 显式构造调用时,
obj是None; - 视图转换时,
obj可以是 ndarray 的任何子类实例,包括我们自己的类; - 从模板新建时,
obj是我们子类的另一个实例,可用于更新新的self实例。
由于__array_finalize__是唯一能"看见"所有新实例创建的方法,它理所当然地成为为新对象属性填充默认值的地方。
C 层实现依据
在 C 实现 numpy/_core/src/multiarray/ctors.c 中可以看到这套机制的落地细节:
- 在调用
__array_finalize__之前,C 层会先通过PyArray_SetBaseObject设置好新对象的base,注释明确写道 "Set the base object. It's important to do it here so thatarray_finalizebelow receives it"——这正是__array_finalize__里能用obj.base追踪数据来源的前提; - 只有请求了子类型(
subtype != &PyArray_Type)才会触发调用;当obj为 NULL 时,C 层会用Py_None作为回调参数传入,对应 Python 侧"显式构造时obj is None"的现象; - 出于性能考虑,如果
__array_finalize__是从 ndarray 继承来的空实现,C 层会直接跳过调用("For speed, we skip ifarray_finalizeis inherited from ndarray")。
这解释了为什么子类必须自己实现__array_finalize__才能收到回调,也说明 NumPy 在热路径上对该调用做了专门优化。
简单示例:给 ndarray 附加一个属性
import numpy as np class InfoArray(np.ndarray): def __new__(subtype, shape, dtype=np.float64, buffer=None, offset=0, strides=None, order=None, info=None): # 用常规 ndarray 输入参数创建我们自己类型的 ndarray 实例。 # 这会调用标准的 ndarray 构造器,但返回我们类型的对象。 # 同时也会触发一次 InfoArray.__array_finalize__ 调用。 obj = super().__new__(subtype, shape, dtype, buffer, offset, strides, order) # 把新属性 'info' 设为传入的值 obj.info = info # 最后,必须返回新创建的对象: return obj def __array_finalize__(self, obj): # ``self`` 是 ndarray.__new__(InfoArray, ...) 产生的新对象, # 因此它只有 ndarray.__new__ 构造器赋予的属性——即标准 ndarray 的那些属性。 # # 到达 ndarray.__new__ 调用可能有 3 种方式: # 显式构造函数,如 InfoArray(): # obj is None # (我们正处于 InfoArray.__new__ 构造过程中, # self.info 会在返回 InfoArray.__new__ 后被设置) if obj is None: return # 视图转换,如 arr.view(InfoArray): # obj is arr # (type(obj) 可以是 InfoArray) # 从模板新建,如 infoarr[:3] # type(obj) 是 InfoArray # # 注意:给 'info' 设置默认值要放在这里,而不是 __new__ 方法中, # 因为这个方法能看到所有默认对象的创建——包括 # InfoArray.__new__ 构造器,也包括 arr.view(InfoArray)。 self.info = getattr(obj, 'info', None) # 无需返回任何值使用效果:
obj = InfoArray(shape=(3,)) # 显式构造函数 type(obj) # <class 'InfoArray'> obj.info is None # True obj = InfoArray(shape=(3,), info='information') obj.info # 'information' v = obj[1:] # 从模板新建——这里是切片 type(v) # <class 'InfoArray'> v.info # 'information' arr = np.arange(10) cast_arr = arr.view(InfoArray) # 视图转换 type(cast_arr) # <class 'InfoArray'> cast_arr.info is None # True这个类实用性一般,因为它的构造器与裸 ndarray 相同,要传 buffer、shape 等参数。我们更希望构造器能接受np.array等常规 NumPy 调用产生的已成型 ndarray 并返回对象。
更实际的示例:为既有数组附加属性
下面这个类接受一个已存在的标准 ndarray,转换为我们的类型,并附加一个额外属性:
import numpy as np class RealisticInfoArray(np.ndarray): def __new__(cls, input_array, info=None): # input_array 是一个已成型 ndarray 实例 # 首先转换为我们的类类型 obj = np.asarray(input_array).view(cls) # 给创建出的实例添加新属性 obj.info = info # 最后,必须返回新创建的对象: return obj def __array_finalize__(self, obj): # 参见 InfoArray.__array_finalize__ 的注释 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'注意这里__new__里的np.asarray(input_array).view(cls)先创建标准 ndarray 再转换视图,__array_finalize__则确保后续切片、复制、ufunc 输出等所有从模板新建的实例都能继承info属性。
__array_ufunc__:拦截 ufunc 运算
子类可以通过重写默认的ndarray.__array_ufunc__方法,控制对自身执行 NumPy ufunc 时的行为。该方法代替ufunc 本身执行,应返回运算结果;如果请求的操作未实现,则返回NotImplemented。
签名如下:
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):ufunc:被调用的 ufunc 对象;method:字符串,指示 ufunc 的调用方式,为"__call__"(直接调用)或其一众方法之一:"reduce"、"accumulate"、"reduceat"、"outer"、"at"(参见 basics.ufuncs 中关于 ufunc 方法的说明);inputs:传给 ufunc 的输入参数元组;kwargs:传给函数的所有可选参数和关键字参数,包括out参数——它总是被包含在元组中。
典型实现是:把自身类的输入/输出实例转换为普通 ndarray,用super()把一切转交给超类,最后对结果做可能的反向转换。下面的示例取自测试用例test_ufunc_override_with_super,定义于 numpy/_core/tests/test_umath.py(该测试的注释明确说明此类被本文档使用,修改时需同步更新文档):
import 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]}在 test_umath.py 中,该示例还与另一个定义了__array_ufunc__的类B联合作了验证:np.sin(d, out=(a,))会返回a本身,b.info == {'outputs': [0]};np.sin(a, out=a)则同时记录输入与输出。
super()调用 vsgetattr(ufunc, method)
另一种做法是用getattr(ufunc, methods)(*inputs, **kwargs)取代super调用。对本例而言结果相同,但当另一个操作数也定义了__array_ufunc__时会有差别。假设计算np.add(a, b),其中b是另一个有重写方法的类B的实例:
- 若使用
super(如示例所示),ndarray.__array_ufunc__会注意到b有重写,它无法自行求值,于是返回NotImplemented,我们的类A也随之返回NotImplemented,控制权转交给b——b要么知道如何处理并产生结果,要么也返回NotImplemented,最终抛出TypeError; - 若改用
getattr(ufunc, method),相当于执行np.add(a.view(np.ndarray), b)。B.__array_ufunc__仍会被调用,但此时它看到另一个参数是普通 ndarray,多半能处理并返回一个B类的新实例。示例类并未准备好处理这种情况,但若要用__array_ufunc__重实现MaskedArray,这可能是更好的方案。
最后一点:如果super路线适合某类,它的一个优势是有助于构建类层级。例如假设类B也使用super,而我们创建同时依赖两者的类C,即class C(A, B)(为简单起见不再重写__array_ufunc__)。那么对C实例执行任何 ufunc 都会传给A.__array_ufunc__,A中的super调用进入B.__array_ufunc__,B中的super调用再进入ndarray.__array_ufunc__,从而让A和B协同工作。
__array_wrap__:用于 ufunc 与其他函数
在 NumPy 1.13 之前,ufunc 的行为只能通过__array_wrap__和__array_prepare__(后者现已移除)调节。二者可以改变 ufunc 的输出类型,但与__array_ufunc__不同,不能对输入做任何修改。NumPy 希望最终弃用这些机制,但__array_wrap__仍被squeeze等其他 NumPy 函数和方法使用,所以目前仍为完整功能所必需。
概念上,__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)) # 然后直接调用父类 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)以self为obj、out_arr为加法结果(ndarray)调用__array_wrap__。随后默认的__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__就能调整 ufunc 的输出。__array_wrap__需要self,然后是 ufunc 或其他 NumPy 函数的结果参数,以及可选参数context。context由 ufunc 以三元组传入:(ufunc 名称, ufunc 参数, ufunc 的 domain),其他 NumPy 函数不传。虽然可以像上面那样返回任意值,但__array_wrap__应当返回其所在类的实例(可参考 masked array 子类的实现)。__array_wrap__总是收到一个 NumPy 数组,它可能是也可能不是子类(通常是调用者的子类)。
参考实现:np.memmap
仓库中的 numpy/_core/memmap.py 是这套机制在生产代码中的范例:
__array_finalize__检查obj是否共享内存(np.may_share_memory(self, obj)),若是则继承_mmap、filename、offset、mode,否则全部置空——保证切片、视图不会错误地共享 mmap 生命周期之外的状态;__array_wrap__调用super().__array_wrap__后,仅在self is arr且类型恰为memmap时做特殊处理:return_scalar时返回标量(例如np.sum带axis=None时不返回 0 维 memmap),否则返回普通 ndarray 视图,避免运算结果意外地变成 mmap 输出。
额外注意事项:自定义__del__方法与ndarray.base
ndarray 解决的核心问题之一是跟踪 ndarray 及其视图的内存所有权。考虑创建arr后取切片v = arr[1:],两个对象共享同一块内存。NumPy 用base属性跟踪数组或视图的数据来源:
# 一个拥有自己数据的普通 ndarray arr = np.zeros((4,)) # 这种情况下 base 是 None arr.base is None # True # 取一个视图 v1 = arr[1:] # base 现在指向它派生自的数组 v1.base is arr # True # 再取视图的视图 v2 = v1[1:] # base 指向它最初派生自的原数组 v2.base is arr # True一般来说,如果数组拥有自己的内存(如这里的arr),arr.base为None(有一些例外,可参考 NumPy 相关专著)。base属性可用于判断当前对象是视图还是原数组,进而判断子类数组被删除时是否需要做特定清理——例如只在原数组被删除时清理,而不是视图被删除时。这方面的完整示例可参考numpy._core中的memmap类(见上文 memmap.py 中__new__保存self._mmap、__array_finalize__按共享关系复制的写法,以及flush中通过self.base.flush()逐级刷盘的思路)。
子类化与下游兼容性
子类化ndarray或创建模仿 ndarray 接口的鸭子类型时,你有责任决定自己的 API 与 NumPy API 的对齐程度。为方便起见,许多有对应ndarray方法的 NumPy 函数(如sum、mean、take、reshape)会检查第一个参数是否具有同名方法:如果存在,就调用该方法,而不是把参数强制转换为 NumPy 数组。
例如,若希望子类或鸭子类型与 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时会传入意外的out和keepdims参数,引发TypeError。
如果你希望与 NumPy 及其后续版本(可能会新增关键字参数)保持兼容,又不想暴露 NumPy 的全部参数,则函数签名应接受**kwargs:
def sum(self, axis=None, dtype=None, **unused_kwargs): ...该对象重新变得与np.sum兼容:任何多余的参数(即axis、dtype之外的关键字)都会被收纳进**unused_kwargs。
小结
子类化 ndarray 的核心心智模型可以概括为三句话:
- 三条创建路径都要覆盖:显式构造、视图转换、从模板新建,
__array_finalize__是唯一三者皆达的钩子,属性默认值应放在这里; - 初始化全部交给
__new__:ndarray 没有__init__,通过ndarray.__new__(subtype, ...)才能构造出正确类型的实例,C 层在 ctors.c 中负责设置base并触发__array_finalize__; - ufunc 与函数级兼容按协议走:
__array_ufunc__拦截并转换输入输出(参考 test_umath.py 的权威示例),__array_wrap__调整返回值类型与元数据(参考 memmap.py),而为sum这类函数提供兼容签名时,务必保留**kwargs以应对 NumPy 后续新增参数。
需要进一步了解数组互操作协议的读者,可继续阅读仓库中的 basics.interoperability.rst,以及 basics.ufuncs.rst 中关于 ufunc 方法的完整说明。
【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考