The starting point for our example is the following pure Python class:

classInterval(object):""" A half-open interval on the real number line. """def__init__(self,lo,hi):self.lo=loself.hi=hidef__repr__(self):return'Interval(%f, %f)'%(self.lo,self.hi)@propertydefwidth(self):returnself.hi-self.lo

As the Interval class is not known to Numba, we must create a new Numba
type to represent instances of it. Numba does not deal with Python types
directly: it has its own type system that allows a different level of
granularity as well as various meta-information not available with regular
Python types.

We first create a type class IntervalType and, since we don’t need the
type to be parametric, we instantiate a single type instance interval_type:

In itself, creating a Numba type doesn’t do anything. We must teach Numba
how to infer some Python values as instances of that type. In this example,
it is trivial: any instance of the Interval class should be treated as
belonging to the type interval_type:

The type_callable() decorator specifies that the decorated function
should be invoked when running type inference for the given callable object
(here the Interval class itself). The decorated function must simply
return a typer function that will be called with the argument types. The
reason for this seemingly convoluted setup is for the typer function to have
exactly the same signature as the typed callable. This allows handling
keyword arguments correctly.

The context argument received by the decorated function is useful in
more sophisticated cases where computing the callable’s return type
requires resolving other types.

As a general rule, nopython mode does not work on Python objects
as they are generated by the CPython interpreter. The representations
used by the interpreter are far too inefficient for fast native code.
Each type supported in nopython mode therefore has to define
a tailored native representation, also called a data model.

A common case of data model is an immutable struct-like data model, that
is akin to a C struct. Our interval datatype conveniently falls in
that category, and here is a possible data model for it:

This instructs Numba that values of type IntervalType (or any instance
thereof) are represented as a structure of two fields lo and hi,
each of them a double-precision floating-point number (types.float64).

Note

Mutable types need more sophisticated data models to be able to
persist their values after modification. They typically cannot be
stored and passed on the stack or in registers like immutable types do.

You might ask why we didn’t need to expose a type inference hook for this
attribute? The answer is that @overload_attribute is part of the
high-level API: it combines type inference and code generation in a
single API.

There is a bit more going on here. @lower_builtin decorates the
implementation of the given callable or operation (here the Interval
constructor) for some specific argument types. This allows defining
type-specific implementations of a given operation, which is important
for heavily overloaded functions such as len().

types.Float is the class of all floating-point types (types.float64
is an instance of types.Float). It is generally more future-proof
to match argument types on their class rather than on specific instances
(however, when returning a type – chiefly during the type inference
phase –, you must usually return a type instance).

cgutils.create_struct_proxy() and interval._getvalue() are a bit
of boilerplate due to how Numba passes values around. Values are passed
as instances of llvmlite.ir.Value, which can be too limited:
LLVM structure values especially are quite low-level. A struct proxy
is a temporary wrapper around a LLVM structure value allowing to easily
get or set members of the structure. The _getvalue() call simply
gets the LLVM value out of the wrapper.

If you try to use an Interval instance at this point, you’ll certainly
get the error “cannot convert Interval to native value”. This is because
Numba doesn’t yet know how to make a native interval value from a Python
Interval instance. Let’s teach it how to do it:

Unbox is the other name for “convert a Python object to a native value”
(it fits the idea of a Python object as a sophisticated box containing
a simple native value). The function returns a NativeValue object
which gives its caller access to the computed native value, the error bit
and possibly other information.

The snippet above makes abundant use of the c.pyapi object, which
gives access to a subset of the
Python interpreter’s C API.
Note the use of c.pyapi.err_occurred() to detect any errors that
may have happened when unboxing the object (try passing Interval('a','b')
for example).

We also want to do the reverse operation, called boxing, so as to return
interval values from Numba functions: