Skip to content

Creating multiple constructors & shorter bindings (similar to pybind11) #4356

@phil-zxx

Description

@phil-zxx

Say I have the following C++ Date class. Using pybind11 I can easily wrap it to be used in Python:

/* date.hpp */
#include <string>

class Date
{
public:
    int year, month, day;
    
    Date(int year, int month, int day);
    Date(const std::string& yyyymmdd);

    Date add_days(int N) const;  // Returns new date with N days added
    bool is_weekend() const;     // Returns true if date is Sat or Sun
};
/* date_bindings.cpp */
#include <pybind11/pybind11.h>
#include <date.hpp>

namespace py = pybind11;

PYBIND11_MODULE(date_py, m) {
    py::class_<Date>(m, "Date")
        .def(py::init<int, int, int>(),      py::arg("year"), py::arg("month"), py::arg("day"))
        .def(py::init<const std::string&>(), py::arg("yyyymmdd"))
        .def("add_days",        &Date::add_days, py::arg("N"))
        .def("is_weekend",      &Date::is_weekend)
        .def_readwrite("year",  &Date::year)
        .def_readwrite("month", &Date::month)
        .def_readwrite("day",   &Date::day);
}

That's 15 lines of code for my bindings using pybind11.

Now, I want to do the same in Rust. So I implemented my Date class and then used PyO3 to wrap it:

/* date.rs */

#[derive(Debug)]
pub struct Date {
    pub year: u16,
    pub month: u8,
    pub day: u8
}

impl Date {
    pub fn from_ymd(year: u16, month: u8, day: u8) -> Date {
        // ...
    }

    pub fn from_str(s: &str) -> Date {
        // ...
    }

    pub const fn add_days(&self, N: u8) -> Self {
        // ...
    }
    pub const fn is_weekend(&self) -> bool {
        // ...
    }
}
/* date_bindings.rs */

use pyo3::prelude::*;
use my_library::Date;

#[pyclass(name = "Date")]
struct DatePy {
    inner: Date
}

#[pymethods]
impl DatePy {
    #[new]
    fn new(year: u16, month: u8, day: u8) -> Self {
        DatePy{inner: Date::from_ymd(year, month, day)}
    }
    fn add_days(&self, N: u8) -> PyResult<Self> {
        Ok(DatePy{inner: self._date.add_days(N)})
    }
    fn is_weekend(&self) -> PyResult<bool> {
        Ok(self.inner.is_weekend())
    }
    #[getter]
    fn get_year(&self) -> PyResult<u16> {
        Ok(self.inner.year)
    }
    #[getter]
    fn get_month(&self) -> PyResult<u8> {
        Ok(self.inner.month)
    }
    #[getter]
    fn get_day(&self) -> PyResult<u8> {
        Ok(self.inner.day)
    }
    #[setter]
    fn set_year(&mut self, year: u16) -> PyResult<()> {
        self.inner.year = year;
        Ok(())
    }
    #[setter]
    fn set_month(&mut self, month: u8) -> PyResult<()> {
        self.inner.month = month;
        Ok(())
    }
    #[setter]
    fn set_day(&mut self, day: u8) -> PyResult<()> {
        self.inner.day = day;
        Ok(())
    }
}

#[pymodule]
fn date_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<DatePy>()?;
    Ok(())
}

I've noticed that the PyO3 bindings contain a lot of boilerplate, and hence they are much longer compared to pybind11:

  • 15 lines with pybind11
  • 54 lines with PyO3

Questions:

  1. Am I using PyO3 correctly? Are there ways to shorten the code to achieve a similar level of "conciseness" as is the case for pybind11?
  2. Is there is a way to create PyO3 bindings without the DatePy helper struct? Again, ideally similar to pybind where I don't need to manually create any helper structs.
  3. How can I add two (or more) constructors such as #[new] fn new(year: u16, month: u8, day: u8) and #[new] fn new(yyyymmdd: &str) similar to pybind11? RIght now, I am only able to expose 1 constructor with PyO3.

Question originates from Correct & concise way to use PyO3 Bindings (similar to pybind11), where a commenter suggested to raise an issue on the PyO3 GitHub.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions