In two of our previous posts, we briefly understand a general Python and a C function. Now In this post, I’ll try to explain How a Python function works in Cython.
Cython supports regular Python functions defined with the def keyword, and they work as we would expect.
So, to get a better understanding, let’s define a simple python function named factorial, which recursively computes the factorial of its argument. This simple Python function is valid Cython code. In Cython, this argument is a dynamic Python variable, and the factorial function must be passed a Python object when called and it is used the same way regardless of whether it is defined in pure Python or defined in Cython and imported from an extension module.
Before moving further, just notice that I have placed my function inside the cy_func.pyx file which is placed inside the Cython directory. We can compile this example using any of the methods I have described in our previous posts. But for that, you must have installed a C compiler and Cython on your system, if you didn’t watch that posts, check out the posts appearing on the top right corner right now. So, let’s run this example using the interactive shell IPython.
I will open the terminal and log onto the directory where we have placed our code, which is Cython. Now, first, you need to install IPython if you didn’t already, you can achieve this by using the command pip install ipython.
Now, we have to activate that for an interactive shell, so type the command as IPython, just notice that it is with the capital I & P. It will open an interactive shell, so, inside that first, we need to import the pyximport, then we have to call its install() method, now at this stage, we can import our own module which is cy_func, so I will say import cy_func. Then we will check our function by saying cy_func.factorial and place a “?” at the end. It will provide us the details about our function, and finally, we can use our function as cy_func.factorial and pass a number for example “10”. You can see the result here.
Great, you might be thinking that what’s the point to run this function by using pyximport even though it is a traditional python function. To get an answer to this question, Let’s define a pure-Python version of the factorial function in the interpreter for comparison:
So, I will define it as in_factorial() and pass an argument, then the rest of the function will be similar to the previous one.
Now, We can compare their runtimes with the handy IPython %timeit magic command. So, I will say %timeit in_factorial and pass (20), you can see it runs 100000 loops and took few microseconds per loop.
Now in the same way we will run cython version of this function which is factorial. You can see that this function runs approximately two times faster with Cython for small input values on this system, although the speedup depends on several factors. The source of the speedup is the removal of interpretation overhead and the reduced function call overhead in Cython.
But as you can notice that, With respect to usage, in_factorial and the Cython-compiled factorial are identical. With respect to implementation, these two functions have some important differences.
The Python version has a type function, while the Cython version has type builtin_function_or_method. The Python version has several attributes available to it such as __name__ that are modifiable, while the Cython version is not modifiable. The Python version, when called, executes bytecodes with the Python interpreter, while the Cython version runs compiled C code that calls into the Python/C API, bypassing bytecode interpretation entirely.
One another scenario might be interesting here, Factorials grow very quickly. One nice feature of Python integers is that they can represent arbitrarily large values, and can therefore represent values that C integral types cannot. These large integers are very convenient, but that convenience comes at the cost of performance.
We can tell Cython to type our argument as a C integral type for example "long" and possibly gain a performance improvement, with the understanding that we are now working with limited-precision integers that may overflow.
Great, we have explored that how can we improve the performance of python functions with identical usage by using Cython.