子类化 ndarray#
简介#
子类化 ndarray 相对简单,但与其他 Python 对象相比,它有一些复杂之处.在本页中,我们将解释允许您子类化 ndarray 的机制,以及实现子类的含义.
ndarrays 和对象创建#
子类化 ndarray 变得复杂,因为 ndarray 类的新实例可以通过三种不同的方式产生.这些是:
显式构造函数调用 - 如
MySubClass(params)中所示.这是 Python 实例创建的常用方法.视图转换 - 将现有 ndarray 转换为给定的子类
从模板新建 - 从模板实例创建一个新实例.示例包括从子类数组返回切片,从 ufuncs 创建返回类型,以及复制数组.有关更多详细信息,请参见 从模板创建新的实例
后两个是ndarray的特性 - 为了支持数组切片之类的操作.ndarray子类化的复杂性源于numpy支持后两种实例创建方式的机制.
何时使用子类化#
除了NumPy数组子类化的额外复杂性之外,子类可能会遇到意外的行为,因为某些函数可能会将子类转换为基类,并“忘记”与子类相关的任何附加信息.如果您使用的NumPy方法或函数未经过明确测试,则可能会导致意外行为.
另一方面,与其他互操作方法相比,子类化可能很有用,因为很多东西都可以“直接使用”.
这意味着子类化可能是一种方便的方法,而且在很长一段时间内,它通常也是唯一可用的方法.但是,NumPy现在提供了在”Interoperability with NumPy “中描述的附加互操作性协议.对于许多用例,这些互操作性协议现在可能更适合或补充子类化的使用.
如果出现以下情况,子类化可能是一个不错的选择:
您不太关心可维护性或您自己以外的用户:子类将更快地实现,并且可以“根据需要”添加其他互操作性.并且由于用户很少,可能的意外不是问题.
如果您认为子类信息被忽略或静默丢失没有问题.一个例子是
np.memmap,其中“忘记”数据被内存映射不会导致错误的结果.NumPy的masked arrays是一个有时会使使用者困惑的子类的例子.当它们被引入时,子类化是唯一的实现方法.但是,今天我们可能会尝试避免子类化,而仅依赖于互操作性协议.
请注意,子类作者可能也希望研究 Interoperability with NumPy 以支持更复杂的使用情况或解决令人惊讶的行为.
astropy.units.Quantity 和 xarray 是与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属于同一类,并且指向原始数据.
在使用ndarray时,我们还需要这样的视图,例如复制数组 ( c_arr.copy() ),创建ufunc输出数组(另请参见 用于ufunc和其他函数的 __array_wrap__ ),以及reduce方法(例如 c_arr.mean() ).
视图转换和从模板创建新实例的关系#
这些路径都使用相同的机制. 我们在此处进行区分,因为它们会导致方法的输入不同. 具体来说, 视图转换 意味着您已经从ndarray的任何潜在子类创建了数组类型的新实例. 从模板创建新的实例 意味着您已经从预先存在的实例创建了类的新实例,从而使您能够(例如)复制特定于子类的属性.
子类化的含义#
如果我们子类化 ndarray,我们不仅需要处理数组类型的显式构造,还需要处理 视图转换 或者 从模板创建新的实例 .NumPy 拥有完成此操作的机制,这种机制使得子类化略微不那么标准.
ndarray 使用的机制有两个方面来支持子类中的视图和从模板新建实例.
首先是使用 ndarray.__new__ 方法来完成对象初始化的主要工作,而不是通常的 __init__ 方法.其次是使用 __array_finalize__ 方法,允许子类在从模板创建视图和新实例后进行清理.
关于 __new__ 和 __init__ 的 Python 简要入门#
__new__ 是一个标准的 Python 方法,如果存在,在创建类实例时,它会在 __init__ 之前被调用.有关更多详细信息,请参阅 python __new__ documentation .
例如,考虑以下 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 提供的机制,允许子类处理创建新实例的各种方式.
记住,子类实例可以通过以下三种方式创建:
显式构造函数调用 (
obj = MySubClass(params)).这将调用通常的序列MySubClass.__new__,然后(如果存在)调用MySubClass.__init__.
我们的 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):
可以看出,传递给 ndarray.__new__ 的 super 调用会将新对象(我们自己的类 ( self ))以及从中获取视图的对象 ( obj ) 传递给 __array_finalize__ .从上面的输出中可以看出, self 始终是我们子类的一个新创建的实例,并且 obj 的类型对于三种实例创建方法而言是不同的:
从显式构造函数调用时,
obj为None从视图转换调用时,
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 对象具有相同的构造函数,包括传入缓冲区和形状等等.我们可能更希望构造函数能够从调用 np.array 的常规 numpy 调用中获取已形成的 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 ufunc 时发生的情况.此方法将代替 ufunc 执行,并且应该返回操作结果;如果请求的操作未实现,则返回 NotImplemented .
__array_ufunc__ 的签名是:
def __array_ufunc__(ufunc, method, *inputs, **kwargs):
ufunc 是被调用的 ufunc 对象.
method 是一个字符串,指示 Ufunc 的调用方式,
"__call__"表示直接调用,或是它的 methods 之一:"reduce","accumulate","reduceat","outer"或"at".inputs 是
ufunc的输入参数元组kwargs 包含传递给函数的任何可选或关键字参数.这包括所有
out参数,它们始终包含在元组中.
一个典型的实现会将任何属于自身类的输入或输出实例进行转换,使用 super() 将所有内容传递给超类,最后在可能的回溯转换后返回结果.一个例子,取自测试用例 test_ufunc_override_with_super ,位于 _core/tests/test_umath.py 中,如下所示.
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__ ,从而 允许 A 和 B 协同工作.
用于ufunc和其他函数的 __array_wrap__#
在numpy 1.13之前,ufunc的行为只能使用 __array_wrap__ 和 __array_prepare__ 进行调整(后者现在已被删除). 这两个允许人们更改ufunc的输出类型,但是,与 __array_ufunc__ 相比,不允许任何人更改输入. 希望最终弃用这些,但是 squeeze 等其他 numpy 函数和方法也使用 __array_wrap__ ,因此目前仍然需要 __array_wrap__ 才能实现全部功能.
从概念上讲, __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 )已使用参数 self 作为参数调用了 __array_wrap__ 方法 obj ,并使用 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__ 方法,我们可以调整来自ufunc的输出. __array_wrap__ 方法需要 self ,然后是一个参数 - 它是ufunc的结果或另一个NumPy函数 - 以及一个可选的参数上下文.此参数由ufunc作为3元素元组传递:(ufunc的名称,ufunc的参数,ufunc的域),但不会被其他numpy函数传递. 虽然,如上所示,可以以其他方式进行,但 __array_wrap__ 应该返回其包含类的实例. 有关实现,请参见masked array子类. __array_wrap__ 总是传递一个NumPy数组,它可能是也可能不是一个子类(通常是调用者的子类).
额外的注意事项 - 自定义 __del__ 方法和 ndarray.base#
ndarray解决的问题之一是跟踪ndarray及其视图的内存所有权. 考虑这样一种情况,我们创建了一个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 book.
base 属性对于能够判断我们拥有视图还是原始数组非常有用. 反过来,如果我们想知道在删除子类数组时是否需要进行一些特定的清理,这可能很有用. 例如,我们可能只想在删除原始数组时才进行清理,而不是视图. 有关如何工作的示例,请查看 numpy._core 中的 memmap 类.
子类化和下游兼容性#
当子类化 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 参数中.