Type Constraints (overload_numpy.constraints
)#
Classes for defining type constraints in __array_function__
.
__array_function__
has an argument types
, which is a
Collection
of unique argument types from the original
NumPy function call that implement __array_function__
. The purpose of types
is
to allow implementations of __array_function__
to check if all arguments of a type
that the overload knows how to handle. Normally this is implemented inside of
__array_function__
, but overload_numpy
gives overloading functions more
flexibility to set constrains on a per-overloaded function basis.
Examples#
First, some imports:
>>> from dataclasses import dataclass
>>> from typing import ClassVar
>>> import numpy as np
>>> from overload_numpy import NumPyOverloader, NPArrayFuncOverloadMixin
Now we can define a NumPyOverloader
instance:
>>> W_FUNCS = NumPyOverloader()
The overloads apply to an array wrapping class. Let’s define one, and a subclass that contains more information:
>>> @dataclass
... class Wrap1D(NPArrayFuncOverloadMixin):
... NP_OVERLOADS: ClassVar[NumPyOverloader] = W_FUNCS
... NP_FUNC_TYPES: ClassVar[None] = None
... x: np.ndarray
Note the NP_FUNC_TYPES
. Normally this would inherit from
NPArrayFuncOverloadMixin
and be an empty frozenset
,
signalling that types are covariant with Wrap1D
(includes subclasses).
Changing NP_FUNC_TYPES
to None
means each overload must explicitly define
the types
argument. See NPArrayFuncOverloadMixin
for further details.
Now numpy
functions can be overloaded and registered for Wrap1D
. Here
is where we introduce the type constraints:
>>> from overload_numpy.constraints import Invariant, Covariant
>>> invariant = Invariant(Wrap1D) # only works for Wrap1D
>>> @W_FUNCS.implements(np.concatenate, Wrap1D, types=invariant)
... def concatenate(w1ds):
... return Wrap1D(np.concatenate(tuple(w.x for w in w1ds)))
>>> w1d = Wrap1D(np.arange(3))
>>> np.concatenate((w1d, w1d))
Wrap1D(x=array([0, 1, 2, 0, 1, 2]))
This implementation of numpy.concatenate
is invariant on the type of
Wrap1D
, so should fail if we use a subclass:
>>> @dataclass
... class Wrap2D(Wrap1D):
... '''A simple 2-array wrapper.'''
... y: np.ndarray
>>> w2d = Wrap2D(np.arange(3), np.arange(3, 6))
>>> try: np.concatenate((w2d, w2d))
>>> except Exception as e: print(e)
there is no implementation
Normally overlaoded functions are made to be
Covariant
. As this is the default, just
passing the type is a convenience short-hand.
>>> @W_FUNCS.implements(np.concatenate, Wrap1D, types=Wrap1D)
... def concatenate(w1ds):
... return Wrap1D(*(np.concatenate(tuple(getattr(w, f.name) for w in w1ds))
for f in fields(wlds[0])))
>>> np.concatenate((w2d, w2d))
Wrap1D(x=array([0, 1, 2, 0, 1, 2]), y=array([3, 4, 5, 3, 4, 5]))
This module offers other types of constraints, so be sure to check them out. Also, if you need something more specific, it’s easy to make your own constraint. There are currently two things you need to do:
define a method
validate_type
As an example, let’s define a constraint where the argument must be one of 2 types:
>>> from overload_numpy.constraints import TypeConstraint
>>> @dataclass(frozen=True)
... class ThisOrThat(TypeConstraint):
... this: type
... that: type
... def validate_type(self, arg_type: type, /) -> bool:
... return arg_type is self.this or arg_type is self.that
Note
TypeConstraint will eventually be converted to a runtime-checkable
typing.Protocol
. When that happens step 1 (subclassing
TypeConstraint) will become optional.
API#
overload_numpy.constraints Module#
Classes#
ABC for constraining an argument type. |
|
|
Type constraint for invariance -- the exact type. |
|
A covariant constraint -- permitting subclasses. |
|
A contravariant constraint -- permitting superclasses. |
|
Type constrained between two types. |