Наследование от ndarray
Введение
Наследование от ndarray относительно просто, но имеет некоторые сложности по сравнению с другими объектами Python. На этой странице мы объясняем механизмы, которые позволяют вам наследоваться от ndarray, и последствия для реализации подкласса.
ndarray и создание объектов
Наследование от ndarray усложняется тем, что новые экземпляры классов ndarray могут появляться тремя различными способами. Это:
- Явное вызов конструктора — как в
MySubClass(params). Это обычный способ создания экземпляров в Python. - Преобразование представления — приведение существующего ndarray к заданному подклассу.
- Создание из шаблона — создание нового экземпляра на основе шаблона экземпляра. Примеры включают возвращение срезов из подклассированного массива, создание типов возвращаемых значений от ufunc и копирование массивов. Более подробную информацию см. в разделе Создание нового из шаблона.
Последние два способа являются особенностями ndarray — для поддержки таких операций, как срез массива. Сложности при наследовании от ndarray обусловлены механизмами NumPy для поддержки этих последних двух способов создания экземпляров.
Когда использовать наследование
Помимо дополнительных сложностей при наследовании от массива NumPy, подклассы могут демонстрировать неожиданное поведение, потому что некоторые функции могут преобразовывать подкласс в базовый класс и «забывать» любую дополнительную информацию, связанную с подклассом. Это может привести к неожиданному поведению, если вы используете методы или функции NumPy, которые не были явно протестированы.
С другой стороны, по сравнению с другими подходами к взаимодействию, наследование может быть полезным, потому что многие вещи будут «работать».
Это означает, что наследование может быть удобным подходом, и в течение длительного времени это часто был единственный доступный подход. Однако сейчас NumPy предоставляет дополнительные протоколы взаимодействия, описанные в разделе «Взаимодействие с NumPy». Для многих случаев использования эти протоколы взаимодействия могут быть более подходящим вариантом или дополнением к использованию наследования.
Наследование может подойти, если:
- вы меньше беспокоитесь о поддерживаемости или пользователях, помимо вас: Наследование будет быстрее реализовываться, и дополнительное взаимодействие можно добавить «по мере необходимости». А при небольшом количестве пользователей возможные неожиданности не являются проблемой.
- вы не считаете проблематичным, если информация о подклассе игнорируется или теряется незаметно. Примером является
np.memmap, где «забывание» о том, что данные отображаются в памяти, не может привести к неправильному результату. Примером подкласса, который иногда вызывает у пользователей трудности, являются маскированные массивы NumPy. Когда они были введены, наследование было единственным доступным подходом к реализации. Однако сегодня мы, возможно, попытаемся избежать наследования и будем полагаться только на протоколы взаимодействия.
Обратите внимание, что авторы подклассов могут изучить Взаимодействие с NumPy, чтобы поддерживать более сложные случаи использования или устранять неожиданное поведение.
astropy.units.Quantity и xarray являются примерами объектов, похожих на массивы, которые хорошо взаимодействуют с NumPy. Quantity из Astropy — пример, который использует двойной подход, сочетающий как наследование, так и протоколы взаимодействия.
Преобразование представления
Преобразование представления — это стандартный механизм 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, где нам нужны такие представления, такие как копирование массивов (c_arr.copy()), создание выходных массивов ufunc (см. также __array_wrap__ для ufunc и других функций), и методы сведения (например, 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__: <class 'C'>
Args in __new__: ('hello',)
type(self) in __init__: <class '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. Обратите внимание, что метод __init__ класса D не вызывается. В общем случае, когда метод __new__ возвращает объект другого класса, чем тот, в котором он определен, метод __init__ этого класса не вызывается.
Таким образом, подклассы класса ndarray могут возвращать представления, сохраняя тип класса. При получении представления стандартный механизм ndarray создает новый объект ndarray примерно так:
obj = ndarray.__new__(subtype, shape, ...
где subdtype — подкласс. Таким образом, возвращаемое представление имеет тот же класс, что и подкласс, а не класс ndarray.
Это решает проблему возвращения представлений того же типа, но теперь у нас появляется новая проблема. Механизм ndarray может устанавливать класс таким образом в своих стандартных методах получения представлений, но метод __new__ ndarray ничего не знает о том, что мы сделали в собственном методе __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, ...)или выполнить преобразование представления существующего массива (см. ниже) - Для преобразования представлений и создания новых объектов по шаблону эквивалент
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 <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 отличается для трёх способов создания экземпляров:
- При вызове из явного конструктора,
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, включая передачу буферов, форм и так далее. Мы, вероятно, предпочли бы, чтобы конструктор мог принимать уже сформированный 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) <class 'RealisticInfoArray'> >>> obj.info 'information' >>> v = obj[1:] >>> type(v) <class 'RealisticInfoArray'> >>> v.info 'information'
__array_ufunc__ для ufuncs
Новое в версии 1.13.
Подкласс может переопределить поведение при выполнении numpy ufuncs, переопределив метод по умолчанию ndarray.__array_ufunc__. Этот метод выполняется вместо ufunc и должен вернуть либо результат операции, либо NotImplemented, если запрошенная операция не реализована.
Подпись __array_ufunc__:
def __array_ufunc__(ufunc, method, *inputs, **kwargs):
- ufunc — объект ufunc, который был вызван.
-
method — строка, указывающая, как был вызван Ufunc, либо
"__call__"для прямого вызова, либо одно из его методов:"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. Наш пример класса не настроен на обработку этого, но это может быть лучшим подходом, если, например, кто-то хотел бы повторно реализовать 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__, а вызов 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'
Обратите внимание, что ufunc (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 — и необязательного параметра context. Этот параметр передаётся ufuncs в виде 3-элементного кортежа: (имя ufunc, аргументы ufunc, область ufunc), но не передаётся другими функциями numpy. Хотя, как показано выше, можно поступить иначе, __array_wrap__ должен возвращать экземпляр своего содержащего класса. Обратитесь к подклассу массива с маской для реализации.
Дополнительные нюансы — настраиваемые методы __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 для более подробной информации.
Атрибут base полезен для определения, имеем ли мы представление или исходный массив. Это, в свою очередь, может быть полезно, если нам нужно знать, нужно ли выполнять определённую очистку при удалении подкласса массива. Например, мы можем выполнять очистку только в случае удаления исходного массива, но не представлений. Чтобы увидеть пример того, как это может работать, взгляните на класс memmap в numpy._core.
Наследование и совместимость с последующими версиями
При наследовании от ndarray или создании псевдотипов, имитирующих интерфейс ndarray, вы несёте ответственность за выбор степени соответствия ваших API API numpy. Для удобства многие функции numpy, имеющие соответствующий метод ndarray (например, 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.
© 2005–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/user/basics.subclassing.html