In this article, I’m going to show you how to extend Python using Cython.
If you have both C/C++ and Python coding skills, I’m sure you’ll enjoy this.
The wheel we are building is a simplest stack implementation, written in C/C++ that reduces unnecessary overhead and results in significant acceleration.
steps
- Build directory
- Writing C++ files
- Write a Pyx file
- Compile directly
- test
1. Create a directory
First, set up our working catalog.
mkdir pystack
cd pystack
Copy the code
The 32-bit and 64-bit versions present different problems. My C library is 32-bit, so the Python library must also be 32-bit.
Specify the Python version using Pipenv and install Cython.
Pipenv --python P:\Py3.6.5\python.exe pipenv install CythonCopy the code
2. Write C++ files
Extern “C” is used when C++ must be compiled in C.
“c_stack.h”
#include "python.h"
extern "C"{
class C_Stack {
private:
struct Node {
PyObject* val;
Node* prev;
};
Node* tail;
public:
C_Stack();
~C_Stack();
PyObject* peek();
void push(PyObject* val);
PyObject* pop();
};
}
Copy the code
“c_stack.cpp”
extern "C"{ #include "c_stack.h" } C_Stack::C_Stack() { tail = new Node; tail->prev = NULL; tail->val = NULL; }; C_Stack::~C_Stack() { Node *t; while(tail! =NULL){ t=tail; tail=tail->prev; delete t; }}; PyObject* C_Stack::peek() { return tail->val; } void C_Stack::push(PyObject* val) { Node* nt = new Node; nt->prev = tail; nt->val = val; tail = nt; } PyObject* C_Stack::pop() { Node* ot = tail; PyObject* val = tail->val; if (tail->prev ! = NULL) { tail = tail->prev; delete ot; } return val; }Copy the code
The simplest stack implementation, with only push,peek, and POP interfaces, will suffice as examples.
3. Write pyx files
Cython’s C/Python hybrid syntax simplifies the steps of extending Python.
It’s easy to write, if you know the syntax beforehand.
“pystack.pyx”
# distutils: language=c++
# distutils: sources = c_stack.cpp
from cpython.ref cimport PyObject,Py_INCREF,Py_DECREF
cdef extern from 'c_stack.h':
cdef cppclass C_Stack:
PyObject* peek();
void push(PyObject* val);
PyObject* pop();
class StackEmpty(Exception):
pass
cdef class Stack:
cdef C_Stack _c_stack
cpdef object peek(self):
cdef PyObject* val
val=self._c_stack.peek()
if val==NULL:
raise StackEmpty
return <object>val
cpdef object push(self,object val):
Py_INCREF(val);
self._c_stack.push(<PyObject*>val);
return None
cpdef object pop(self):
cdef PyObject* val
val=self._c_stack.pop()
if val==NULL:
raise StackEmpty
cdef object rv=<object>val;
Py_DECREF(rv)
return rv
Copy the code
It is divided into four parts:
- The annotation specifies the corresponding CPP file.
- Import C symbols from CPython: PyObject,Py_INCREF,Py_DECREF.
- Import C symbol: c_stack and its interface from “c_stack.h”.
- Wrap it as a Python object.
Note:
- In the C implementation, a null pointer is returned when the stack is empty. The Python implementation checks for null Pointers and throws the exception StackEmpty.
- PyObject* is not the same as Object and requires type conversion.
- Do reference counting correctly for push and POP, or it will crash the Python interpreter directly. I didn’t know this at first, and I was confused for a long time, until I saw an error related to GC.
4. Direct compilation
pipenv run cythonize -a -i pystack.cpp
Copy the code
Generate three files: pystack. CPP, pystack. HTML, and pystack.cp36-win32.pyd
Pyx is compiled into CPP, which is then compiled into PYD by the C compiler.
HTML is a Cython hint that indicates the degree of interaction with Python in Pyx code.
Pyd is the ultimate Python library.
5. Test it out
“test.py”
from pystack import * st=Stack() print(dir(st)) try: st.pop() except StackEmpty as exc: Print (repr(exc)) print(type(st.pop)) for I in ['1',1,[1.0],1,dict(a=1)]: st.push(I) while True: print(st.pop()) pipenv run python test.py ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__pyx_vtable__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'peek', 'pop', 'push'] <class 'list'> {'a': 1} 1 [1.0] 1 1 Traceback (most recent call last): File "test.py", line 13, in <module> print(st.pop()) File "pystack.pyx", line 32, in pystack.Stack.pop cpdef object pop(self): File "pystack.pyx", line 36, in pystack.Stack.pop raise StackEmpty pystack.StackEmptyCopy the code
Behaves like a normal Python object, perfect!
6. Application
pipenv run python test_polish_notation.py
from operator import add, sub, mul, truediv
from fractions import Fraction
from pystack import Stack
def main():
exp = input('exp: ')
val = eval_exp(exp)
print(f'val: {val}')
op_map = {
'+': add,
'-': sub,
'*': mul,
'/': truediv
}
def convert(exp):
for it in reversed(exp.split(' ')):
if it in op_map:
yield True, op_map[it]
else:
yield False, Fraction(it)
def eval_exp(exp):
stack = Stack()
for is_op, it in convert(exp):
if is_op:
left = stack.pop()
right = stack.pop()
stack.push(it(left, right))
else:
stack.push(it)
return stack.pop()
if __name__ == '__main__':
main()
# exp: + 5 - 2 * 3 / 4 7
# val: 37/7
Copy the code
This article demonstrates the simplest Cython wheel-building techniques, and hopefully provides a stepping stone for those who are going to pit and those who are already in pit. If it helps, please like and bookmark.