Shock and Awe 1

A concise overview of libpy. For an introduction to extending Python with C or C++ please see the Python documentation or Joe Jevnik’s C Extension Tutorial.

Simple Scalar Functions

We start by building simple scalar functions in C++ which we can call from Python.

In [1]: from libpy_tutorial import scalar_functions

A simple scalar function:

bool bool_scalar(bool a) {
    return !a;
}
In [2]: scalar_functions.bool_scalar(False)
Out[2]: True

A great way to use libpy is to write the code that needs to be fast in C++ and expose that code via Python. Let’s estimate pi using a monte carlo simulation:

double monte_carlo_pi(int n_samples) {
    int accumulator = 0;

    std::random_device rd;   // Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd());  // Standard mersenne_twister_engine seeded with rd()
    std::uniform_real_distribution<> dis(0, 1);

    for (int i = 0; i < n_samples; ++i) {
        auto x = dis(gen);
        auto y = dis(gen);
        if ((std::pow(x, 2) + std::pow(y, 2)) < 1.0) {
            accumulator += 1;
        }
    }
    return 4.0 * accumulator / n_samples;
}
In [3]: scalar_functions.monte_carlo_pi(10000000)
Out[3]: 3.1416408

Of course, we can build C++ functions that support all the features of regular Python functions.

libpy supports optional args:

std::string optional_arg(py::arg::optional<std::string> opt_arg) {
    return opt_arg.get().value_or("default value");
}
In [4]: scalar_functions.optional_arg(b"An argument was passed")
Out[4]: b'An argument was passed'

In [5]: scalar_functions.optional_arg()
Out[5]: b'default value'

and keyword/optional keyword arguments:

py::owned_ref<>
keyword_args(py::arg::kwd<decltype("kw_arg_kwd"_cs), int> kw_arg_kwd,
             py::arg::opt_kwd<decltype("opt_kw_arg_kwd"_cs), int> opt_kw_arg_kwd) {

    return py::build_tuple(kw_arg_kwd.get(), opt_kw_arg_kwd.get());
}
In [6]: scalar_functions.keyword_args(kw_arg_kwd=1)
Out[6]: (1, None)

In [7]: scalar_functions.keyword_args(kw_arg_kwd=1, opt_kw_arg_kwd=55)
Out[7]: (1, 55)

Working With Arrays

In order to write performant code it is often useful to write vectorized functions that act on arrays. Thus, libpy has extenstive support for numpy arrays.

In [8]: from libpy_tutorial import arrays

In [9]: import numpy as np

We can take numpy arrays as input:

    std::int64_t out = 0;
    for (auto value : values) {
        out += value;
    }
    return out;
}
In [10]: some_numbers = np.arange(20000)

In [11]: arrays.simple_sum(some_numbers)
Out[11]: 199990000

and return them as output:

    std::transform(values.cbegin(),
                   values.cend(),
                   values.begin(),
                   [](std::int64_t v) { return -v; });
}

bool check_prime(std::int64_t n) {
    if (n <= 3) {
        return n > 1;
    }
    else if (n % 2 == 0 || n % 3 == 0) {
        return false;
    }
    for (auto i = 5; std::pow(i, 2) < n; i += 6) {
        if (n % i == 0 || n % (i + 2) == 0) {
            return false;
        }
    }
    return true;
}
In [12]: prime_mask = arrays.is_prime(some_numbers)

In [13]: some_numbers[prime_mask][:100]
Out[13]: 
array([  2,   3,   5,   7,  11,  13,  17,  19,  23,  25,  29,  31,  37,
        41,  43,  47,  53,  59,  61,  67,  71,  73,  79,  83,  89,  97,
       101, 103, 107, 109, 113, 121, 127, 131, 137, 139, 149, 151, 157,
       163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
       233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 289, 293,
       307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379,
       383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457,
       461, 463, 467, 479, 487, 491, 499, 503, 509])

Note

numpy arrays passed to C++ are ranges.

    return std::accumulate(values.begin(), values.end(), 0);
}
In [14]: arrays.simple_sum_iterator(some_numbers)
Out[14]: 199990000

N Dimensional Arrays

We can also work with n-dimensional arrays. As a motivating example, let’s sharpen an image. Specifically - we will sharpen:

In [15]: from PIL import Image

In [16]: import matplotlib.pyplot as plt # to show the image in documenation

In [17]: import numpy as np

In [18]: import pkg_resources

In [19]: img_file = pkg_resources.resource_stream("libpy_tutorial", "data/original.png")

In [20]: img = Image.open(img_file)

In [21]: plt.imshow(img)
Out[21]: <matplotlib.image.AxesImage at 0x7f60f88909d0>
_images/original.png
py::owned_ref<> apply_kernel(py::ndarray_view<const std::uint8_t, 3> pixels,
                             py::ndarray_view<const std::int64_t, 2> kernel) {

    auto n_dimensions = pixels.shape()[2];
    auto n_rows = pixels.shape()[0];
    auto n_columns = pixels.shape()[1];

    auto k_rows = kernel.shape()[0];
    auto k_columns = kernel.shape()[1];
    std::vector<std::uint8_t> out(n_dimensions * n_rows * n_columns, 0);
    py::ndarray_view out_view(out.data(),
                              pixels.shape(),
                              {static_cast<int>(n_dimensions * n_rows),
                               static_cast<int>(n_dimensions),
                               1});

    for (std::size_t dim = 0; dim < n_dimensions; ++dim) {
        for (std::size_t row = 0; row < n_rows; ++row) {
            for (std::size_t column = 0; column < n_columns; ++column) {

                auto accumulated_sum = 0.0;

                for (std::size_t k_row = 0; k_row < k_rows; ++k_row) {
                    for (std::size_t k_column = 0; k_column < k_columns; ++k_column) {

                        auto input_row_idx = row + 1 - k_row;
                        auto input_column_idx = column + 1 - k_column;

                        if (input_row_idx < n_rows && input_column_idx < n_columns) {
                            accumulated_sum +=
                                pixels(input_row_idx, input_column_idx, dim) *
                                kernel(k_row, k_column);
                        }
                    }
                }
                if (accumulated_sum < 0) {
                    accumulated_sum = 0;
                }
                else if (accumulated_sum > 255) {
                    accumulated_sum = 255;
                }
                out_view(row, column, dim) = accumulated_sum;
            }
        }
    }
    return py::move_to_numpy_array(std::move(out),
In [22]: pixels = np.array(img)

In [23]: kernel = np.array([
   ....:     [0, -1, 0],
   ....:     [-1, 5, -1],
   ....:     [0, -1, 0]
   ....: ]) # already normalized
   ....: 

In [24]: from libpy_tutorial import ndarrays

In [25]: res = ndarrays.apply_kernel(pixels, kernel)

In [26]: plt.imshow(res)
Out[26]: <matplotlib.image.AxesImage at 0x7f60f8886460>
_images/sharpened.png

Note

We are able to pass a shaped n-dimensional array as input and return one as output.

Creating Classes

libpy also allows you to construct C++ classes and then easily expose them as if they are regular Python classes.

In [27]: from libpy_tutorial.classes import Vec3d

C++ classes are able to emulate all the features of Python classes:

namespace libpy_tutorial {
class vec3d {
private:
    std::array<double, 3> m_values;

public:
    vec3d(double x, double y, double z) : m_values({x, y, z}) {}

    double x() const {
        return m_values[0];
    }

    double y() const {
        return m_values[1];
    }

    double z() const {
        return m_values[2];
    }

    vec3d operator+(const vec3d& other) const {
        return {x() + other.x(), y() + other.y(), z() + other.z()};
    }

    vec3d operator-(const vec3d& other) const {
        return {x() - other.x(), y() - other.y(), z() - other.z()};
    }

    double operator*(const vec3d& other) const {
        return std::inner_product(m_values.begin(),
                                  m_values.end(),
                                  other.m_values.begin(),
                                  0.0);
    }

    double magnitude() const {
        return std::sqrt(*this * *this);
    }
};

std::ostream& operator<<(std::ostream& s, const vec3d& v) {
    return s << '{' << v.x() << ", " << v.y() << ", " << v.z() << '}';
}

// `repr` could also be a member function, but free functions are useful for adding
// a Python repr without modifying the methods of the type.
std::string repr(const vec3d& v) {
    std::stringstream ss;
    ss << "Vec3d(" << v.x() << ", " << v.y() << ", " << v.z() << ')';
    return ss.str();
}
}  // namespace libpy_tutorial

namespace py::dispatch {
// Make it possible to convert a `vec3d` into a Python object.
template<>
struct LIBPY_NO_EXPORT to_object<libpy_tutorial::vec3d>
    : public py::autoclass<libpy_tutorial::vec3d>::to_object {};
}  // namespace py::dispatch
In [28]: Vec3d.__doc__
Out[28]: 'An efficient 3-vector.'

In [29]: v = Vec3d(1, 2, 3)

In [30]: v
Out[30]: Vec3d(1, 2, 3)

In [31]: str(v)
Out[31]: '{1, 2, 3}'

In [32]: v.x(), v.y(), v.z()
Out[32]: (1.0, 2.0, 3.0)

In [33]: w = Vec3d(4, 5, 6); w
Out[33]: Vec3d(4, 5, 6)

In [34]: v + w
Out[34]: Vec3d(5, 7, 9)

In [35]: v * w
Out[35]: 32.0

In [36]: v.magnitude()
Out[36]: 3.7416573867739413

Exceptions

Working with exceptions is also important.

In [37]: from libpy_tutorial import exceptions

We can throw exceptions in C++ that will then be dealt with in Python. Two patterns:

  1. Throw your own exception: throw py::exception(type, msg...), maybe in response to an exception from a C-API function.

  2. Throw a C++ exception directly.

void throw_value_error(int a) {
    throw py::exception(PyExc_ValueError, "You passed ", a, " and this is the exception");
}

void raise_from_cxx() {
    throw std::invalid_argument("Supposedly a bad argument was used");
}
In [40]: exceptions.throw_value_error(4)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-6cfdcf9a1ea9> in <module>
----> 1 exceptions.throw_value_error(4)

ValueError: You passed 4 and this is the exception

In [41]: exceptions.raise_from_cxx()
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-41-ee2345413222> in <module>
----> 1 exceptions.raise_from_cxx()

RuntimeError: a C++ exception was raised: Supposedly a bad argument was used

Footnotes

1

With naming credit to the intorduction of Q for Mortals.