Search code examples
pythoncythoncythonize

How do I create a constructor that would receive different types of parameters?


I have this Point class. I want it to be able to recieve double and SomeType parameters.

Point.pxd:

from libcpp.memory cimport shared_ptr, weak_ptr, make_shared
from SomeType cimport _SomeType, SomeType

cdef extern from "Point.h":
    cdef cppclass _Point:
        _Point(shared_ptr[double] x, shared_ptr[double] y)
        _Point(shared_ptr[double] x, shared_ptr[double] y, shared_ptr[double] z)
        _Point(shared_ptr[_SomeType] x, shared_ptr[_SomeType] y)
        _Point(shared_ptr[_SomeType] x, shared_ptr[_SomeType] y, shared_ptr[_SomeType] z)

        shared_ptr[_SomeType] get_x()
        shared_ptr[_SomeType] get_y()
        shared_ptr[_SomeType] get_z()


cdef class Point:
    cdef shared_ptr[_Point] c_point

Point.pyx:

from Point cimport *

cdef class Point:
    def __cinit__(self, SomeType x=SomeType("0", None), SomeType y=SomeType("0", None), SomeType z=SomeType("0", None)):
        self.c_point = make_shared[_Point](x.thisptr, y.thisptr, z.thisptr)

    def __dealloc(self):
        self.c_point.reset()

    def get_x(self) -> SomeType:
        cdef shared_ptr[_SomeType] result = self.c_point.get().get_x()
        cdef SomeType coord = SomeType("", None, make_with_pointer = True)
        coord.thisptr = result
        return coord

    def get_y(self) -> SomeType:
        cdef shared_ptr[_SomeType] result = self.c_point.get().get_y()
        cdef SomeType coord = SomeType("", None, make_with_pointer = True)
        coord.thisptr = result
        return coord

    def get_z(self) -> SomeType:
        cdef shared_ptr[_SomeType] result = self.c_point.get().get_z()
        cdef SomeType coord = SomeType("", None, make_with_pointer = True)
        coord.thisptr = result
        return coord

    property x:
        def __get__(self):
            return self.get_x()

    property y:
        def __get__(self):
            return self.get_y()

    property z:
        def __get__(self):
            return self.get_z()

How should I write my .pxd and .pyx files so that my Point constructor can receive different type of parameters?


Solution

  • In Cython, you cannot directly overload constructors (or any methods) as you might in C++ or other languages that support method overloading. However, you can achieve similar functionality by using factory methods or by using Python's flexibility with arguments.

    Given your scenario where the Point class needs to accept different types of parameters (either double or SomeType objects), you can implement this flexibility using Python's *args and **kwargs in combination with type checking and processing logic inside the constructor. Additionally, you can define class methods that act as alternative constructors, which is a common Pythonic approach to solve this issue.

    Here’s how you might adjust your .pxd and .pyx files to accommodate these requirements:

    • Point.pxd

    This file remains largely the same but ensure it correctly declares everything you need:

    # Point.pxd
    from libcpp.memory cimport shared_ptr, make_shared
    from SomeType cimport _SomeType, SomeType
    
    cdef extern from "Point.h":
        cdef cppclass _Point:
            _Point(shared_ptr[double] x, shared_ptr[double] y)
            _Point(shared_ptr[double] x, shared_ptr[double] y, shared_ptr[double] z)
            _Point(shared_ptr[_SomeType] x, shared_ptr[_SomeType] y)
            _Point(shared_ptr[_SomeType] x, shared_ptr[_SomeType] y, shared_ptr[_SomeType] z)
    
    cdef class Point:
        cdef shared_ptr[_Point] c_point
    
    • Point.pyx

    Modify this file to include a flexible constructor and additional class methods for different initialisations:

    # Point.pyx
    from Point cimport *
    from libc.stdlib cimport atof
    
    cdef class Point:
        def __cinit__(self, *args):
            if len(args) == 2 or len(args) == 3:
                if isinstance(args[0], SomeType):
                    ptrs = [arg.thisptr for arg in args]
                else:
                    ptrs = [make_shared[double](atof(arg)) for arg in args]
                if len(args) == 2:
                    self.c_point = make_shared[_Point](ptrs[0], ptrs[1])
                elif len(args) == 3:
                    self.c_point = make_shared[_Point](ptrs[0], ptrs[1], ptrs[2])
            else:
                raise ValueError("Invalid number of arguments")
    
        def __dealloc__(self):
            self.c_point.reset()
    
        @staticmethod
        def from_doubles(x, y, z=None):
            cdef shared_ptr[double] px = make_shared[double](x)
            cdef shared_ptr[double] py = make_shared[double](y)
            cdef shared_ptr[double] pz = make_shared[double](z) if z is not None else None
            if z is None:
                return Point(px, py)
            return Point(px, py, pz)
    
        # ... rest of the methods ...
    

    Here, __cinit__ accepts variable arguments (*args). It determines the type of each argument and constructs the c_point appropriately, based on the number of arguments and their types. The from_doubles static method provides a clearer, type-specific way to create instances from double values.

    This approach gives you the flexibility to initialise Point objects with different types while maintaining clean, readable code. Make sure that the make_shared[double](x) conversions handle input correctly, and adjust the type-checking and conversions as necessary for your specific needs and types.