子类数据数组

介绍

子类化ndarray相对简单,但与其他Python对象相比,它有一些复杂的地方。在本页中,我们将解释允许您对ndarray子类的机制,以及实现子类的含义。

日历和对象创建

由于新的ndarray类实例可以以三种不同的方式出现,因此对ndarray进行子类化非常复杂。这些是:

  1. 显式构造函数调用-如中所示 MySubClass(params) . 这是创建Python实例的常用路径。

  2. 视图转换-将现有的ndarray转换为给定的子类

  3. 从模板新建-从模板实例创建新实例。示例包括从子类数组返回切片、从ufunc创建返回类型以及复制数组。见 从模板新建 有关详细信息

最后两个是ndarrays的特性——为了支持数组切片之类的东西。子类数据库的复杂性是由于numpy必须支持后两个实例创建路径的机制造成的。

视图铸造

视图铸造 是标准的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 'C'>

从模板新建

Ndarray子类的新实例也可以通过与 视图铸造 ,当numpy发现需要从模板实例创建新实例时。最明显的情况是,当您对子类数组进行切片时,会发生这种情况。例如:

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

切片是 view 在原版上 c_arr 数据。所以,当我们从ndarray中查看时,我们返回一个新的ndarray,它属于同一类,指向原始数据。

在使用ndarrays的过程中,还有其他一些点需要这样的视图,例如复制数组 (c_arr.copy() ,创建ufunc输出数组(另请参见 __array_wrap__ 对于UFUNC和其他功能 以及减少方法(如 c_arr.mean()

视图投射与新模板的关系

这些路径都使用相同的机器。我们在这里进行区分,因为它们会导致对您的方法的不同输入。明确地, 视图铸造 意味着您已经从ndarray的任何潜在子类创建了数组类型的新实例。 从模板新建 意味着您已经从一个预先存在的实例创建了一个类的新实例,允许您(例如)跨特定于子类的属性进行复制。

子类化的含义

如果我们对ndarray进行子类化,那么不仅需要处理数组类型的显式构造,而且还需要处理 视图铸造从模板新建 . NumPy有这样的机制,正是这种机制使得子类稍微不标准。

Ndarray用于支持视图的机制有两个方面,以及子类中模板的新增功能。

第一个是使用 ndarray.__new__ 方法的主要工作是初始化对象,而不是更常见的 __init__ 方法。第二种是使用 __array_finalize__ 方法来允许子类在从模板创建视图和新实例后进行清理。

关于 __new____init__

__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 'C'>
Args in __new__: ('hello',)
type(self) in __init__: <class 'C'>
Args in __init__: ('hello',)

当我们呼唤 C('hello') , the __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 , the __new__ 方法返回类的实例 C 而不是 D . 请注意 __init__ 方法 D 不会被呼叫。一般来说,当 __new__ 方法返回一个类的对象,而不是在其中定义该对象的类, __init__ 不调用该类的方法。

这就是ndarray类的子类如何返回保留类类型的视图。在查看时,标准的ndarray机械会创建新的ndarray对象,其内容如下:

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

在哪里? subdtype 是子类。因此,返回的视图与子类属于同一类,而不是属于类。 ndarray .

这解决了返回同一类型视图的问题,但现在我们有了一个新问题。ndarray的机器可以这样设置类,在它的标准视图方法中,但是ndarray __new__ method knows nothing of what we have done in our own __new__ method in order to set attributes, and so on. (Aside - why not call 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__ does 调用所有三种对象创建方法,因此这是我们的对象创建内务管理通常要做的事情。

  • 对于显式构造函数调用,我们的子类需要创建一个自己类的新ndarray实例。在实践中,这意味着代码的作者需要调用 ndarray.__new__(MySubClass,...) ,类层次结构准备调用 super(MySubClass, cls).__new__(cls, ...) 或查看现有数组的强制转换(请参见下文)

  • 对于视图铸造和新模板,等效于 ndarray.__new__(MySubClass,... 在C级别调用。

关于 __array_finalize__ 对于上面三种创建实例的方法,接收不同。

下面的代码允许我们查看调用序列和参数:

import numpy as np

class C(np.ndarray):
    def __new__(cls, *args, **kwargs):
        print('In __new__ with class %s' % cls)
        return super(C, cls).__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__ 通过 __array_finalize__ 我们班的新对象 (self )以及从中获取视图的对象 (obj )从上面的输出可以看到, self 始终是子类的新创建实例,并且 obj 三种实例创建方法不同:

  • 当从显式构造函数调用时, objNone

  • 当从视图强制转换调用时, obj 可以是ndarray的任何子类的实例,包括我们自己的。

  • 当从模板调用new时, 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(InfoArray, subtype).__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调用中获取已经形成的ndarray np.array 并返回一个对象。

稍微现实一点的示例-添加到现有数组的属性

这是一个类,它采用已经存在的标准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'

__array_ufunc__ 为UMULSS

1.13 新版功能.

子类可以通过覆盖默认值来覆盖在其上执行numpy ufunc时发生的情况。 ndarray.__array_ufunc__ 方法。执行此方法 相反 并应返回操作的结果,或 NotImplemented 如果请求的操作未实现。

签字 __array_ufunc__ 是::

def __array_ufunc__(ufunc, method, *inputs, **kwargs):

- *ufunc* is the ufunc object that was called.
- *method* is a string indicating how the Ufunc was called, either
  ``"__call__"`` to indicate it was called directly, or one of its
  :ref:`methods<ufuncs.methods>`: ``"reduce"``, ``"accumulate"``,
  ``"reduceat"``, ``"outer"``, or ``"at"``.
- *inputs* is a tuple of the input arguments to the ``ufunc``
- *kwargs* contains any optional or keyword arguments passed to the
  function. This includes any ``out`` arguments, which are always
  contained in a tuple.

典型的实现将转换任何属于自己类的实例的输入或输出,并使用 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(A, self).__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 给我们上课。我们的示例类没有设置为处理这个问题,但是如果(例如)重新实现一个示例类,那么它可能是最好的方法。 MaskedArray 使用 __array_ufunc__ .

最后一点:如果 super 路由适合于给定的类,使用它的一个优点是它有助于构造类层次结构。例如,假设我们的另一个班 B 也使用了 super 在其 __array_ufunc__ 实现,我们创建了一个类 C 这取决于两者,即, class C(A, B) (为了简单起见,不是另一个 __array_ufunc__ 重写)。然后是的实例上的任何ufunc C 会传给 A.__array_ufunc__ , the super 拜访 A 会去 B.__array_ufunc__super 拜访 B 会去 ndarray.__array_ufunc__ 从而允许 AB 合作。

__array_wrap__ 对于UFUNC和其他功能

在numpy 1.13之前,只能使用 __array_wrap____array_prepare__ . 这两个允许一个更改ufunc的输出类型,但与 __array_ufunc__ ,不允许对输入进行任何更改。人们希望最终会贬低这些,但是 __array_wrap__ 也用于其他numpy函数和方法,例如 squeeze 因此,目前仍然需要完整的功能。

概念上, __array_wrap__ “包装操作”是指允许子类设置返回值的类型并更新属性和元数据。让我们用一个例子来说明这是如何工作的。首先,我们返回到更简单的示例子类,但使用不同的名称和一些print语句:

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):
        print('In __array_wrap__:')
        print('   self is %s' % repr(self))
        print('   arr is %s' % repr(out_arr))
        # then just call the parent
        return super(MySubClass, self).__array_wrap__(self, out_arr, context)

我们在新数组的一个实例上运行一个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__ 带参数的方法 self 作为 objout_arr 作为添加的(ndarray)结果。反过来,默认 __array_wrap__ (ndarray.__array_wrap__ )已将结果强制转换为类 MySubClass 并称之为 __array_finalize__ -因此,复制 info 属性。这一切都发生在C级。

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

class SillySubClass(np.ndarray):

    def __array_wrap__(self, arr, context=None):
        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的结果)和一个可选参数 语境 . 此参数由ufuncs作为3元素元组返回:(ufunc的名称、ufunc的参数、ufunc的域),但不由其他numpy函数设置。不过,如上所述,也有可能采取其他措施, __array_wrap__ 应返回其包含类的实例。有关实现,请参见屏蔽数组子类。

除了 __array_wrap__ 在离开UFunc的路上,也有一个 __array_prepare__ 方法,在创建输出数组之后,但在执行任何计算之前,在进入ufunc的过程中调用。默认实现只通过数组进行传递。 __array_prepare__ 不应尝试访问数组数据或调整数组的大小,它用于设置输出数组类型、更新属性和元数据,以及根据计算开始前可能需要的输入执行任何检查。喜欢 __array_wrap____array_prepare__ 必须返回ndarray或其子类或引发错误。

额外gotchas-自定义 __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 将没有-有一些例外,这-见 NumPy 的书,以获得更多的细节。

这个 base 属性在判断是否有视图或原始数组时很有用。如果我们需要知道在删除子类数组时是否要进行某些特定的清理,那么这反过来会很有用。例如,我们可能只希望在删除原始数组的情况下进行清理,而不希望删除视图。有关这项工作的示例,请查看 memmap 班在 numpy.core .

子类化和下游兼容性

分类时 ndarray 或者创造出模仿 ndarray 接口,您有责任决定API与numpy的API的一致性。为了方便起见,许多numpy函数 ndarray 方法(例如, summeantakereshape )检查函数的第一个参数是否具有相同名称的方法。如果该方法存在,则调用该方法,而不是将参数强制为numpy数组。

例如,如果您希望子类或duck类型与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 ,导致引发类型错误。

如果您希望保持与numpy及其后续版本(可能会添加新的关键字参数)的兼容性,但不希望显示numpy的所有参数,那么您的函数的签名应该接受 **kwargs . 例如:

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

此对象现在与兼容 np.sum 同样,因为任何无关的参数(即关键字不是 axisdtype )将被隐藏在 **unused_kwargs 参数。