There are many ways to address this issue, for instance :
- Optimizing algorithmically the implementation ;
- Using some mature libraries such as NumPy, SciPy or pandas ;
- Calling for Cython help !
- Using Rust and wrap it with your Python code ;
- Etc.
I know.. I know.. I know.. Why bothering with 4. when we can just use 2. and 3.
Give Me A Reason..
We agree that coding in Python is a real joy.. You are equipped with powerful tools which make you highly productive in many use cases.. Despite this fact, there are some restricted environments when speed is crucial.. At MINDIMENSIONS, we started a project of creating a Demand Side Platform (DSP) which uses Artificial Intelligence during the Real-Time Biding (RTB) process. The POC was straightforward thanks to Python ecosystem. But putting the system into production was another story.
As it turns out, before adding any fancy feature to our platform, this one should execute the whole process in no more than the same duration that a website can take in order to be fully loaded. At this stage, we changed our vocabulary and we begun talking about Java, Golang, Scala and Rust.
But, what if we just want to delegate the bottleneck parts to another language and stick with Python for the fancy stuff ?! Yup.. This is when Rust comes in..
Show Me Some Code..
So, how we can put this in practice.. Let's simplify our problem and describe it as follows :
- We have a Python application which is, in our example, a Django API ;
- Our API makes some heavy computation which can be modeled by computing recursively the Fibonacci number. The choice of this algorithm is because Fibonacci sequence is easy to implement and the recursive version has an exponential complexity ;
- Rust code will be wrapped as a package and then be used by our API.
The Python Realm
Let's start by creating a virtual environment just as a good Pythonista citizen should be.. Here I am using conda :
$ conda create -n rust_venv_3-10 python=3.10
$ conda activate rust_venv_3-10
Then installing some requirements :
$ pip install Django==4.2.1 djangorestframework==3.14.0
And starting our Django project:
$ django-admin startproject django_rust_api
We create our API application inside apps folder:
$ cd django_rust_api
$ mkdir -p apps/compute
$ python manage.py startapp compute apps/compute/
Inside the Django settings, we add DRF
and our app named compute
to installed Apps :
INSTALLED_APPS = [
...
# third party apps
"rest_framework",
# local apps
"apps.compute",
...
]
Then we tweak the usual files :
apps/compute/views.py
import time
from rest_framework.response import Response
from rest_framework.views import APIView
class ComputeFibonacci(APIView):
def fibonacci(self, number: int) -> int:
if number <= 0:
raise ValueError("The number should be a non-null positive integer")
if number in range(1, 3):
return 1
return self.fibonacci(number - 1) + self.fibonacci(number - 2)
def get(self, request, *args, **kwargs):
number = int(self.request.query_params.get("number"))
start_time = time.time()
result = self.fibonacci(number)
end_time = time.time()
return Response(dict(
language="Python",
result=result,
computing_time=f"{end_time - start_time:0.4f} s",
))
compute_fibonacci = ComputeFibonacci.as_view()
apps/compute/urls.py
from django.urls import path
from .views import compute_fibonacci
app_name = "compute"
urlpatterns = [
path("fibonacci", view=compute_fibonacci, name="compute_fibonacci"),
]
django_rust_api/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path("api/compute/", include("apps.compute.urls", namespace="compute")),
]
Now we can run our server $ python manage.py runserver
and consume our endpoint http://localhost:8000/api/compute/fibonacci?number=20
which takes number
as a query parameter then returns the Fibonacci value and the computing time.
Computing fibonacci(20)
is very fast (less than a quarter of a second) but fibonacci(40)
takes more than 25 seconds (and depending on the used machine). Then, we rapidly understand that this is a path one should not follow..
So, let's see how we can reduce this time by just changing a few lines of code by making some calls to a Rust implementation. But first, let's create our Rust package then see how we can wrap it inside our Python code.
The Rust Realm
Here we need Rust to be installed and we will use Cargo as a Package Manager. Then inside our apps
folder $ cargo new fibonacci_rust
this will create
- A
Cargo.toml
file which is the manifest of the package src
folder which will be the source of our Rust code. By default, Cargo adds amain.rs
file which is the entry-point of our Rust package.
Inside fibonacci_rust
folder, we can run our Rust program by $ cargo run
; this should display a hello world message. Now let's implement the recursive version of Fibonacci using Rust.
As a mapping between Python and Rust, let's create a package named computing_fibonacci
Inside fibonacci_rust
folder and adding a mod.rs
file which has the same behavior as __
init__
.py
. Then, we add another Rust module named recursive.rs
in which we will implement our Fibonacci function.
Those files should look like :
apps/fibonacci_rust/src/computing_fibonacci/recursive.rs
pub fn fibonacci(number: i32) -> u64 {
if number <= 0 {
panic!("Please provide a positive integer starting with 1");
}
match number {
1 | 2 => 1,
_ => fibonacci(number - 1) + fibonacci(number - 2)
}
}
apps/fibonacci_rust/src/computing_fibonacci/mod.rs
pub mod recursive;
Now we have a Rust implementation of Fibonacci, but how we can call this function from a Python code. In order to do so, we should package our Rust code and install it with pip
The Neutral Realm
Let's create a setup.py
file at the level of apps/fibonacci_rust/
#!/usr/bin/env python
from setuptools import dist
dist.Distribution().fetch_build_eggs(['setuptools_rust'])
from setuptools import setup
from setuptools_rust import Binding, RustExtension
setup(
name="fibonacci_rust",
version="0.1",
rust_extensions=[RustExtension(
".fibonacci_rust.fibonacci_rust",
path="Cargo.toml", binding=Binding.PyO3)],
packages=["fibonacci_rust"],
zip_safe=False,
)
Then we should install the Setuptools plugin with $ pip install setuptools-rust
and modify our Cargo.toml
file :
[package]
name = "fibonacci_rust"
version = "0.1.0"
edition = "2021"
[lib]
name = "fibonacci_rust"
# "cdylib" is necessary to produce a shared library for Python to import from.
#
# Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able
# to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.:
# crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib", "rlib"]
[dependencies]
[dependencies.pyo3]
version = "0.15.1"
features = ["extension-module"]
Then changing the name of our entrypoint to lib.rs
. We should then create a binding between Rust and Python using PyO3. This process should modify two files :
apps/fibonacci_rust/src/computing_fibonacci/recursive.rs
use pyo3::prelude::pyfunction;
#[pyfunction]
pub fn fibonacci(number: i32) -> u64 {
if number <= 0 {
panic!("Please provide a positive integer starting with 1");
}
match number {
1 | 2 => 1,
_ => fibonacci(number - 1) + fibonacci(number - 2)
}
}
apps/fibonacci_rust/src/lib.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
mod computing_fibonacci;
use computing_fibonacci::recursive::__pyo3_get_function_fibonacci;
#[pymodule]
fn fibonacci_rust(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(fibonacci));
Ok(())
}
And in order to create our Python entry-point, we just need to create a new Python package containing just __
init__.py
and import the Rust module :
apps/fibonacci_rust/fibonacci_rust/__init__.py
from .fibonacci_rust import *
Finally, inside our apps
folder, let's install our Rust package using $ pip install fibonacci_rust/
, then we can called it from our Python code.
To do so, we will modify apps/compute/views.py
and enjoy the benchmarking ;)
import time
from rest_framework.response import Response
from rest_framework.views import APIView
from fibonacci_rust import fibonacci_rust
class ComputeFibonacci(APIView):
supported_languages = ["Python", "Rust"]
def fibonacci(self, number: int) -> int:
if number <= 0:
raise ValueError("The number should be a non-null positive integer")
if number in range(1, 3):
return 1
return self.fibonacci(number - 1) + self.fibonacci(number - 2)
def get(self, request, *args, **kwargs):
number = int(self.request.query_params.get("number"))
context = []
for lang in self.supported_languages:
match lang:
case "Python":
start_time = time.time()
result = self.fibonacci(number)
end_time = time.time()
case "Rust":
start_time = time.time()
result = fibonacci_rust.fibonacci(number)
end_time = time.time()
context.append(
dict(
language=lang,
result=result,
computing_time=f"{end_time - start_time:0.4f} s",
)
)
return Response(context)
compute_fibonacci = ComputeFibonacci.as_view()
The entire code can be found at https://github.com/Benakrab/django_rust_api