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

  1. Build directory
  2. Writing C++ files
  3. Write a Pyx file
  4. Compile directly
  5. 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:

  1. The annotation specifies the corresponding CPP file.
  2. Import C symbols from CPython: PyObject,Py_INCREF,Py_DECREF.
  3. Import C symbol: c_stack and its interface from “c_stack.h”.
  4. Wrap it as a Python object.

Note:

  1. 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.
  2. PyObject* is not the same as Object and requires type conversion.
  3. 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.