Наследование от ndarray#

Введение#

Создание подкласса ndarray относительно просто, но имеет некоторые сложности по сравнению с другими объектами Python. На этой странице мы объясняем механизм, который позволяет создавать подклассы ndarray, и последствия для реализации подкласса.

ndarrays и создание объектов#

Наследование от ndarray усложняется тем, что новые экземпляры классов ndarray могут появляться тремя разными способами. Это:

  1. Явный вызов конструктора — как в MySubClass(params). Это обычный путь создания экземпляра Python.

  2. Приведение представления - приведение существующего ndarray к заданному подклассу

  3. Создание из шаблона — создание нового экземпляра из экземпляра-шаблона. Примеры включают возвращение срезов из подкласса массива, создание типов возврата из ufuncs и копирование массивов. См. Создание нового из шаблона для получения дополнительных сведений

Последние два являются характеристиками ndarrays — для поддержки таких вещей, как срезы массивов. Сложности подклассирования ndarray связаны с механизмами, которые NumPy использует для поддержки этих двух путей создания экземпляров.

Когда использовать наследование классов#

Помимо дополнительных сложностей при наследовании от массива NumPy, подклассы могут столкнуться с неожиданным поведением, поскольку некоторые функции могут преобразовывать подкласс в базовый класс и "забывать" любую дополнительную информацию, связанную с подклассом. Это может привести к неожиданному поведению, если вы используете методы NumPy или функции, которые вы не тестировали явно.

С другой стороны, по сравнению с другими подходами к взаимодействию, наследование может быть полезным, поскольку многие вещи будут "просто работать".

Это означает, что наследование может быть удобным подходом, и долгое время оно также часто было единственным доступным подходом. Однако NumPy теперь предоставляет дополнительные протоколы взаимодействия, описанные в «Совместимость с NumPy». Для многих случаев использования эти протоколы взаимодействия теперь могут лучше подходить или дополнять использование подклассов.

Создание подклассов может быть подходящим, если:

  • если вы меньше беспокоитесь о поддерживаемости или пользователях, кроме себя: Подкласс будет быстрее реализовать, и дополнительная совместимость может быть добавлена «по мере необходимости». И с небольшим количеством пользователей возможные сюрпризы не являются проблемой.

  • вы не считаете проблематичным, если информация о подклассе игнорируется или теряется без предупреждения. Примером является np.memmap где "забывание" о том, что данные отображаются в памяти, не может привести к неверному результату. Пример подкласса, который иногда сбивает с толку пользователей, — это маскированные массивы NumPy. Когда они были введены, наследование было единственным подходом для реализации. Однако сегодня мы, возможно, попытались бы избежать наследования и полагаться только на протоколы взаимодействия.

Обратите внимание, что авторам подклассов также может быть полезно изучить Совместимость с NumPy для поддержки более сложных случаев использования или обхода неожиданного поведения.

astropy.units.Quantity и xarray являются примерами объектов, подобных массивам, которые хорошо взаимодействуют с NumPy. Astropy's 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)

Создание нового из шаблона#

Новые экземпляры подкласса ndarray также могут создаваться с помощью очень похожего механизма на Приведение представления, когда numpy обнаруживает необходимость создать новый экземпляр из шаблонного экземпляра. Наиболее очевидное место, где это происходит — при взятии срезов массивов-подклассов. Например:

>>> v = c_arr[1:]
>>> type(v) # the view is of type 'C'

>>> v is c_arr # but it's a new instance
False

Срез представляет собой представление на исходный c_arr данных. Поэтому, когда мы берем представление из ndarray, мы возвращаем новый ndarray того же класса, который указывает на данные в оригинале.

Существуют другие случаи использования ndarrays, где требуются такие представления, например, при копировании массивов (c_arr.copy()), создание выходных массивов ufunc (см. также __array_wrap__ для ufuncs и других функций), и методы редукции (такие как c_arr.mean()).

Связь приведения представления и создания из шаблона#

Оба пути используют один и тот же механизм. Мы делаем это различие здесь, потому что они приводят к разному вводу в ваши методы. В частности, Приведение представления означает, что вы создали новый экземпляр вашего типа массива из любого потенциального подкласса ndarray. Создание нового из шаблона означает, что вы создали новый экземпляр вашего класса из уже существующего экземпляра, позволяя вам - например - скопировать атрибуты, которые характерны для вашего подкласса.

Последствия для наследования классов#

Если мы создаем подкласс ndarray, нам нужно иметь дело не только с явным конструированием нашего типа массива, но также Приведение представления или Создание нового из шаблона. NumPy имеет механизм для этого, и именно этот механизм делает наследование немного нестандартным.

Есть два аспекта механизма, который ndarray использует для поддержки представлений и создания новых из шаблона в подклассах.

Первый — это использование ndarray.__new__ метод для основной работы инициализации объекта, а не более обычный __init__ метод. Второй — использование __array_finalize__ метод для позволения подклассам выполнять очистку после создания представлений и новых экземпляров из шаблонов.

Краткое введение в Python для __new__ и __init__#

__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__: 
Args in __new__: ('hello',)
type(self) in __init__: 
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: 
D args in __new__: ('hello',)
Cls in __new__: 
Args in __new__: ('hello',)
>>> type(obj)

Определение C остаётся таким же, как и раньше, но для D, __new__ метод возвращает экземпляр класса C вместо D. Обратите внимание, что __init__ метод D не вызывается. В общем случае, когда __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, ...), или выполнить приведение представления существующего массива (см. ниже)

  • Для приведения типов представлений и создания новых из шаблона, эквивалент 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().__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 
In array_finalize:
   self type is 
   obj type is 
In __init__ with class 
>>> # View casting
>>> a = np.arange(10)
>>> cast_a = a.view(C)
In array_finalize:
   self type is 
   obj type is 
>>> # Slicing (example of new-from-template)
>>> cv = c[:1]
In array_finalize:
   self type is 
   obj type is 

Сигнатура __array_finalize__ равен:

def __array_finalize__(self, obj):

Можно видеть, что super вызов, который переходит к ndarray.__new__, передает __array_finalize__ новый объект, нашего собственного класса (self), а также объект, из которого было взято представление (obj). Как видно из вывода выше, self всегда является вновь созданным экземпляром нашего подкласса, и тип obj различается для трех методов создания экземпляров:

  • При вызове из явного конструктора, obj является None

  • При вызове из приведения представления, obj может быть экземпляром любого подкласса ndarray, включая наш собственный.

  • При вызове в режиме new-from-template, 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)

>>> obj.info is None
True
>>> obj = InfoArray(shape=(3,), info='information')
>>> obj.info
'information'
>>> v = obj[1:] # new-from-template - here - slicing
>>> type(v)

>>> v.info
'information'
>>> arr = np.arange(10)
>>> cast_arr = arr.view(InfoArray) # view casting
>>> type(cast_arr)

>>> cast_arr.info is None
True

Этот класс не очень полезен, потому что он имеет тот же конструктор, что и базовый объект ndarray, включая передачу буферов, форм и т.д. Мы бы предпочли, чтобы конструктор мог принимать уже сформированный ndarray из обычных вызовов numpy к 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)

>>> obj.info
'information'
>>> v = obj[1:]
>>> type(v)

>>> v.info
'information'

__array_ufunc__ для ufuncs#

Подкласс может переопределить поведение при выполнении numpy ufuncs на нем, переопределив стандартный ndarray.__array_ufunc__ метод. Этот метод выполняется вместо универсальной функции и должен возвращать либо результат операции, либо NotImplemented если запрошенная операция не реализована.

Сигнатура __array_ufunc__ равен:

def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
  • универсальная функция (ufunc) является объектом ufunc, который был вызван.

  • метод является строкой, указывающей, как была вызвана Ufunc, либо "__call__" чтобы указать, что он был вызван напрямую, или один из его методы: "reduce", "accumulate", "reduceat", "outer", или "at".

  • входные данные является кортежем входных аргументов для 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 класс для нас. Наш примерный класс не настроен для обработки этого, но это может быть лучшим подходом, если, например, нужно перереализовать MaskedArray используя __array_ufunc__.

В качестве последнего замечания: если super маршрут подходит для данного класса, преимущество его использования заключается в том, что он помогает в построении иерархий классов. Например, предположим, что наш другой класс B также использовал super в его __array_ufunc__ реализация, и мы создали класс C который зависел от обоих, т.е., class C(A, B) (с, для простоты, не другим __array_ufunc__ переопределить). Затем любая ufunc на экземпляре C передаст A.__array_ufunc__, super вызов в A перейдёт в B.__array_ufunc__должны быть включены в старые категории. Значения, которые были в удаленных категориях, будут установлены в NaN super вызов в B перейдёт в ndarray.__array_ufunc__, таким образом позволяя A и B для сотрудничества.

__array_wrap__ для ufuncs и других функций#

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

Обратите внимание, что уфункция (np.add) вызвал __array_wrap__ метод с аргументами self как 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__ метод для нашего подкласса, мы можем настроить вывод из ufuncs. __array_wrap__ метод требует self, затем аргумент - который является результатом ufunc или другой функции NumPy - и необязательный параметр контекст. Этот параметр передается ufuncs как кортеж из 3 элементов: (имя ufunc, аргументы ufunc, домен ufunc), но не передается другими функциями numpy. Хотя, как видно выше, возможно поступить иначе, __array_wrap__ должен возвращать экземпляр своего содержащего класса. См. подкласс маскированного массива для реализации. __array_wrap__ всегда передаётся массив NumPy, который может быть или не быть подклассом (обычно вызывающего объекта).

Дополнительные подводные камни — пользовательские __del__ методы и ndarray.base#

Одна из проблем, которые решает ndarray, — отслеживание владения памятью ndarray и их представлений. Рассмотрим случай, когда мы создали ndarray, arr и взяли срез с v = arr[1:]. Два объекта смотрят на одну и ту же память. NumPy отслеживает, откуда пришли данные для конкретного массива или представления, с помощью base attribute:

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

The base атрибут полезен для определения, имеем ли мы представление или исходный массив. Это, в свою очередь, может быть полезно, если нам нужно знать, требуется ли выполнить определенную очистку при удалении унаследованного массива. Например, мы можем захотеть выполнить очистку только если удаляется исходный массив, но не представления. Пример того, как это может работать, можно найти в memmap класс в numpy._core.

Создание подклассов и совместимость с нижестоящими проектами#

При наследовании классов ndarray или создание утиных типов, имитирующих ndarray интерфейс, ваша ответственность решить, насколько ваши API будут совместимы с API numpy. Для удобства многие функции numpy, имеющие соответствующие ndarray метод (например, sum, mean, take, reshape) работают, проверяя, есть ли у первого аргумента функции метод с таким же именем. Если он существует, метод вызывается вместо приведения аргументов к массиву 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 параметр.