Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

alkaline-ml / numpy   python

Repository URL to install this package:

Version: 1.19.1 

/ doc / subclassing.py

"""=============================
Subclassing ndarray in python
=============================

Introduction
------------

Subclassing ndarray is relatively simple, but it has some complications
compared to other Python objects.  On this page we explain the machinery
that allows you to subclass ndarray, and the implications for
implementing a subclass.

ndarrays and object creation
============================

Subclassing ndarray is complicated by the fact that new instances of
ndarray classes can come about in three different ways.  These are:

#. Explicit constructor call - as in ``MySubClass(params)``.  This is
   the usual route to Python instance creation.
#. View casting - casting an existing ndarray as a given subclass
#. New from template - creating a new instance from a template
   instance. Examples include returning slices from a subclassed array,
   creating return types from ufuncs, and copying arrays.  See
   :ref:`new-from-template` for more details

The last two are characteristics of ndarrays - in order to support
things like array slicing.  The complications of subclassing ndarray are
due to the mechanisms numpy has to support these latter two routes of
instance creation.

.. _view-casting:

View casting
------------

*View casting* is the standard ndarray mechanism by which you take an
ndarray of any subclass, and return a view of the array as another
(specified) subclass:

>>> 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'>

.. _new-from-template:

Creating new from template
--------------------------

New instances of an ndarray subclass can also come about by a very
similar mechanism to :ref:`view-casting`, when numpy finds it needs to
create a new instance from a template instance.  The most obvious place
this has to happen is when you are taking slices of subclassed arrays.
For example:

>>> 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

The slice is a *view* onto the original ``c_arr`` data.  So, when we
take a view from the ndarray, we return a new ndarray, of the same
class, that points to the data in the original.

There are other points in the use of ndarrays where we need such views,
such as copying arrays (``c_arr.copy()``), creating ufunc output arrays
(see also :ref:`array-wrap`), and reducing methods (like
``c_arr.mean()``).

Relationship of view casting and new-from-template
--------------------------------------------------

These paths both use the same machinery.  We make the distinction here,
because they result in different input to your methods.  Specifically,
:ref:`view-casting` means you have created a new instance of your array
type from any potential subclass of ndarray.  :ref:`new-from-template`
means you have created a new instance of your class from a pre-existing
instance, allowing you - for example - to copy across attributes that
are particular to your subclass.

Implications for subclassing
----------------------------

If we subclass ndarray, we need to deal not only with explicit
construction of our array type, but also :ref:`view-casting` or
:ref:`new-from-template`.  NumPy has the machinery to do this, and this
machinery that makes subclassing slightly non-standard.

There are two aspects to the machinery that ndarray uses to support
views and new-from-template in subclasses.

The first is the use of the ``ndarray.__new__`` method for the main work
of object initialization, rather then the more usual ``__init__``
method.  The second is the use of the ``__array_finalize__`` method to
allow subclasses to clean up after the creation of views and new
instances from templates.

A brief Python primer on ``__new__`` and ``__init__``
=====================================================

``__new__`` is a standard Python method, and, if present, is called
before ``__init__`` when we create a class instance. See the `python
__new__ documentation
<https://docs.python.org/reference/datamodel.html#object.__new__>`_ for more detail.

For example, consider the following Python code:

.. testcode::

  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)

meaning that we get:

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

When we call ``C('hello')``, the ``__new__`` method gets its own class
as first argument, and the passed argument, which is the string
``'hello'``.  After python calls ``__new__``, it usually (see below)
calls our ``__init__`` method, with the output of ``__new__`` as the
first argument (now a class instance), and the passed arguments
following.

As you can see, the object can be initialized in the ``__new__``
method or the ``__init__`` method, or both, and in fact ndarray does
not have an ``__init__`` method, because all the initialization is
done in the ``__new__`` method.

Why use ``__new__`` rather than just the usual ``__init__``?  Because
in some cases, as for ndarray, we want to be able to return an object
of some other class.  Consider the following:

.. testcode::

  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__')

meaning that:

>>> 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'>

The definition of ``C`` is the same as before, but for ``D``, the
``__new__`` method returns an instance of class ``C`` rather than
``D``.  Note that the ``__init__`` method of ``D`` does not get
called.  In general, when the ``__new__`` method returns an object of
class other than the class in which it is defined, the ``__init__``
method of that class is not called.

This is how subclasses of the ndarray class are able to return views
that preserve the class type.  When taking a view, the standard
ndarray machinery creates the new ndarray object with something
like::

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

where ``subdtype`` is the subclass.  Thus the returned view is of the
same class as the subclass, rather than being of class ``ndarray``.

That solves the problem of returning views of the same type, but now
we have a new problem.  The machinery of ndarray can set the class
this way, in its standard methods for taking views, but the 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__(...`` then?  Because we may not
have a ``__new__`` method with the same call signature).

The role of ``__array_finalize__``
==================================

``__array_finalize__`` is the mechanism that numpy provides to allow
subclasses to handle the various ways that new instances get created.

Remember that subclass instances can come about in these three ways:

#. explicit constructor call (``obj = MySubClass(params)``).  This will
   call the usual sequence of ``MySubClass.__new__`` then (if it exists)
   ``MySubClass.__init__``.
#. :ref:`view-casting`
#. :ref:`new-from-template`

Our ``MySubClass.__new__`` method only gets called in the case of the
explicit constructor call, so we can't rely on ``MySubClass.__new__`` or
``MySubClass.__init__`` to deal with the view casting and
new-from-template.  It turns out that ``MySubClass.__array_finalize__``
*does* get called for all three methods of object creation, so this is
where our object creation housekeeping usually goes.

* For the explicit constructor call, our subclass will need to create a
  new ndarray instance of its own class.  In practice this means that
  we, the authors of the code, will need to make a call to
  ``ndarray.__new__(MySubClass,...)``, a class-hierarchy prepared call to
  ``super(MySubClass, cls).__new__(cls, ...)``, or do view casting of an
  existing array (see below)
* For view casting and new-from-template, the equivalent of
  ``ndarray.__new__(MySubClass,...`` is called, at the C level.

The arguments that ``__array_finalize__`` receives differ for the three
methods of instance creation above.

The following code allows us to look at the call sequences and arguments:

.. testcode::

   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))


Now:

>>> # 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'>

The signature of ``__array_finalize__`` is::

    def __array_finalize__(self, obj):

One sees that the ``super`` call, which goes to
``ndarray.__new__``, passes ``__array_finalize__`` the new object, of our
own class (``self``) as well as the object from which the view has been
taken (``obj``).  As you can see from the output above, the ``self`` is
always a newly created instance of our subclass, and the type of ``obj``
differs for the three instance creation methods:

* When called from the explicit constructor, ``obj`` is ``None``
* When called from view casting, ``obj`` can be an instance of any
  subclass of ndarray, including our own.
* When called in new-from-template, ``obj`` is another instance of our
  own subclass, that we might use to update the new ``self`` instance.

Because ``__array_finalize__`` is the only method that always sees new
instances being created, it is the sensible place to fill in instance
defaults for new object attributes, among other tasks.

This may be clearer with an example.

Simple example - adding an extra attribute to ndarray
-----------------------------------------------------

.. testcode::

  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)
Loading ...