Multi-languages Interactions with Python

by Gabriel von Winckler

Why?

Possible project workflow

  1. Prototype
  2. Profile
  • Bottleneck
  • memory footprint
  1. Optimize within python (Numpy / Scipy!)
  2. Optimize with external code (C, Fortran)
  • Better if using an existing library!

Example 1

Sum the absolute value of the elements on an array (float)

In [2]:
import random

n_elements = 1000000
values = [random.random() for _ in xrange(n_elements)]

Example 1, take 1 - Python

In [3]:
def abs_sum_python(values):
  total = 0;
  for i in values:
    total += abs(i)
  return total

# abs_sum_python(values)
%timeit -n 100 abs_sum_python(values)
100 loops, best of 3: 82.1 ms per loop

Example 1, take 2 - Thinking in C

In [4]:
def abs_sum_c_in_python(values):
  total = 0;
  for i in range(len(values)):
    total += abs(values[i])
  return total

# abs_sum_c_in_python(values)
%timeit -n 100 abs_sum_c_in_python(values)
100 loops, best of 3: 116 ms per loop

Example 1, take 3 - True python heart

In [5]:
def abs_sum_very_python(values):
  return sum(map(abs,values))

# abs_sum_very_python(values)
%timeit -n 100 abs_sum_very_python(values)
100 loops, best of 3: 60.2 ms per loop

Example 1, take 4 - Numpy

In [6]:
import numpy as np
def abs_sum_numpy(values):
  return np.sum(np.absolute(values))

values_np = np.random.rand(n_elements)

# abs_sum_numpy(values_np)
%timeit -n 100 abs_sum_numpy(values_np)
100 loops, best of 3: 2.66 ms per loop

Recap:

  1. Prototype
  2. Profile
  3. Optimize within python (Numpy / Scipy)
  4. Optimize with external code (C, Fortran)

Extending python with other libraries

Many options: * Python C API * Interface generators (f2py, swig, ...) * ctypes (https://docs.python.org/2/library/ctypes.html)

ctypes

  • Easy to use
  • Flexible
  • No changes required on the shared library
  • Reduces plataform portability

ctypes: overview

ctypes is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python.

#include <stdlib.h>

/*
  gcc -c -Wall -fPIC myabs.c
  gcc -shared -o myabs.so myabs.o
*/

int myabs(int x) {
  return abs(x);
}
In [7]:
!gcc -c -Wall -fPIC myabs.c
!gcc -shared -o myabs.so myabs.o
In [8]:
import ctypes

c_lib = ctypes.cdll.LoadLibrary('./myabs.so')
c_lib.myabs(-11)
Out[8]:
11

1. Load the shared library

2. Call functions

3. Types

ctypes type C type Python type
c_bool Bool | bool (1) cchar char
c_wchar wchar_t 1-character unicode string
c_byte char int/long
c_ubyte unsigned char int/long
c_short short int/long
c_ushort unsigned short int/long
c_int int int/long
c_uint unsigned int int/long
c_long long int/long
c_ulong unsigned long int/long
c_longlong int64 or long long | int/long c_ulonglong | unsigned int64 or unsigned long long int/long
c_float float float
c_double double float
c_longdouble long double float
c_char_p char * (NUL terminated) string or None
c_wchar_p wchar_t * (NUL terminated) unicode or None
c_void_p void * int/long or None

Example 1, take 5 - With a little help from my friends...

#include <stdio.h>
#include <math.h>

/*
  gcc -c -Wall -fPIC sum_abs.c
  gcc -shared -o sum_abs.so sum_abs.o
*/

void sum_abs(float *in, int *num, float *out) {
  int i;
  float sum = 0;
  for (i=0; i < *num; ++i) { 
    sum += fabs(in[i]);
  }
  *out = sum;
  return;
}
In [9]:
!gcc -O3 -c -Wall -fPIC sum_abs.c
!gcc -shared -o sum_abs.so sum_abs.o

from ctypes import cdll, c_float, c_int, byref

c_lib = ctypes.cdll.LoadLibrary('./sum_abs.so')

# define a C array of floats
NFloatType = ctypes.c_float * n_elements
values_c = NFloatType()

# initialize
for i in xrange(len(values)):
  values_c[i] = random.random()

# other params
n = ctypes.c_int(len(values))
s = ctypes.c_float()

# call
# c_lib.sum_abs(ctypes.byref(values_c),
#                ctypes.byref(n),
#                ctypes.byref(s))

%timeit -n 100 c_lib.sum_abs(ctypes.byref(values_c), ctypes.byref(n), ctypes.byref(s))
100 loops, best of 3: 1.28 ms per loop

Example 1, take 6 - ... and an old colleague

subroutine sum_abs(in, num, out)
    integer, intent(in) :: num
    real (kind=4), intent(in) :: in(num)
    real (kind=4), intent(out) :: out
    real (kind=4) :: sum
    sum = 0
    do i=1,num
      sum = sum + ABS(in(i))
    end do
    out = sum
end subroutine sum_abs
In [10]:
!gfortran -shared -fPIC -g -o sum_abs_f.so sum_abs.f90

f_lib = ctypes.cdll.LoadLibrary('./sum_abs_f.so')

# f_lib.sum_abs_(ctypes.byref(values), ctypes.byref(n), ctypes.byref(s))
%timeit -n 100 f_lib.sum_abs_(ctypes.byref(values_c), ctypes.byref(n), ctypes.byref(s))
100 loops, best of 3: 4.2 ms per loop

Example 2

  • Already working (and tested) code
  • New features (more input formats, GUI, scripting, ...)

F2PY - Fortran to Python interface generator

http://docs.scipy.org/doc/numpy-dev/f2py/

In [11]:
! f2py -c -m sum_abs_f2py sum_abs.f90
running build
running config_cc
unifing config_cc, config, build_clib, build_ext, build commands --compiler options
running config_fc
unifing config_fc, config, build_clib, build_ext, build commands --fcompiler options
running build_src
build_src
building extension "sum_abs_f2py" sources
f2py options: []
f2py:> /tmp/tmph2XGi6/src.linux-x86_64-2.7/sum_abs_f2pymodule.c
creating /tmp/tmph2XGi6/src.linux-x86_64-2.7
Reading fortran codes...
	Reading file 'sum_abs.f90' (format:free)
Post-processing...
	Block: sum_abs_f2py
			Block: sum_abs
Post-processing (stage 2)...
Building modules...
	Building module "sum_abs_f2py"...
		Constructing wrapper function "sum_abs"...
		  out = sum_abs(in,[num])
	Wrote C/API module "sum_abs_f2py" to file "/tmp/tmph2XGi6/src.linux-x86_64-2.7/sum_abs_f2pymodule.c"
  adding '/tmp/tmph2XGi6/src.linux-x86_64-2.7/fortranobject.c' to sources.
  adding '/tmp/tmph2XGi6/src.linux-x86_64-2.7' to include_dirs.
copying /usr/lib/python2.7/dist-packages/numpy/f2py/src/fortranobject.c -> /tmp/tmph2XGi6/src.linux-x86_64-2.7
copying /usr/lib/python2.7/dist-packages/numpy/f2py/src/fortranobject.h -> /tmp/tmph2XGi6/src.linux-x86_64-2.7
build_src: building npy-pkg config files
running build_ext
customize UnixCCompiler
customize UnixCCompiler using build_ext
customize Gnu95FCompiler
Found executable /usr/bin/gfortran
customize Gnu95FCompiler
customize Gnu95FCompiler using build_ext
building 'sum_abs_f2py' extension
compiling C sources
C compiler: x86_64-linux-gnu-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC

creating /tmp/tmph2XGi6/tmp
creating /tmp/tmph2XGi6/tmp/tmph2XGi6
creating /tmp/tmph2XGi6/tmp/tmph2XGi6/src.linux-x86_64-2.7
compile options: '-I/tmp/tmph2XGi6/src.linux-x86_64-2.7 -I/usr/lib/python2.7/dist-packages/numpy/core/include -I/usr/include/python2.7 -c'
x86_64-linux-gnu-gcc: /tmp/tmph2XGi6/src.linux-x86_64-2.7/fortranobject.c
In file included from /usr/lib/python2.7/dist-packages/numpy/core/include/numpy/ndarraytypes.h:1761:0,
                 from /usr/lib/python2.7/dist-packages/numpy/core/include/numpy/ndarrayobject.h:17,
                 from /usr/lib/python2.7/dist-packages/numpy/core/include/numpy/arrayobject.h:4,
                 from /tmp/tmph2XGi6/src.linux-x86_64-2.7/fortranobject.h:13,
                 from /tmp/tmph2XGi6/src.linux-x86_64-2.7/fortranobject.c:2:
/usr/lib/python2.7/dist-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:15:2: warning: #warning "Using deprecated NumPy API, disable it by " "#defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
 #warning "Using deprecated NumPy API, disable it by " \
  ^
x86_64-linux-gnu-gcc: /tmp/tmph2XGi6/src.linux-x86_64-2.7/sum_abs_f2pymodule.c
In file included from /usr/lib/python2.7/dist-packages/numpy/core/include/numpy/ndarraytypes.h:1761:0,
                 from /usr/lib/python2.7/dist-packages/numpy/core/include/numpy/ndarrayobject.h:17,
                 from /usr/lib/python2.7/dist-packages/numpy/core/include/numpy/arrayobject.h:4,
                 from /tmp/tmph2XGi6/src.linux-x86_64-2.7/fortranobject.h:13,
                 from /tmp/tmph2XGi6/src.linux-x86_64-2.7/sum_abs_f2pymodule.c:18:
/usr/lib/python2.7/dist-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:15:2: warning: #warning "Using deprecated NumPy API, disable it by " "#defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
 #warning "Using deprecated NumPy API, disable it by " \
  ^
/tmp/tmph2XGi6/src.linux-x86_64-2.7/sum_abs_f2pymodule.c:111:12: warning: ‘f2py_size’ defined but not used [-Wunused-function]
 static int f2py_size(PyArrayObject* var, ...)
            ^
compiling Fortran sources
Fortran f77 compiler: /usr/bin/gfortran -Wall -ffixed-form -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran f90 compiler: /usr/bin/gfortran -Wall -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran fix compiler: /usr/bin/gfortran -Wall -ffixed-form -fno-second-underscore -Wall -fno-second-underscore -fPIC -O3 -funroll-loops
compile options: '-I/tmp/tmph2XGi6/src.linux-x86_64-2.7 -I/usr/lib/python2.7/dist-packages/numpy/core/include -I/usr/include/python2.7 -c'
gfortran:f90: sum_abs.f90
/usr/bin/gfortran -Wall -Wall -shared /tmp/tmph2XGi6/tmp/tmph2XGi6/src.linux-x86_64-2.7/sum_abs_f2pymodule.o /tmp/tmph2XGi6/tmp/tmph2XGi6/src.linux-x86_64-2.7/fortranobject.o /tmp/tmph2XGi6/sum_abs.o -lgfortran -o ./sum_abs_f2py.so
Removing build directory /tmp/tmph2XGi6

In [12]:
import sum_abs_f2py

print sum_abs_f2py.sum_abs.__doc__

#sum_abs_f2py.sum_abs(values, len(values))
%timeit -n 100 sum_abs_f2py.sum_abs(values)
out = sum_abs(in,[num])

Wrapper for ``sum_abs``.

Parameters
----------
in : input rank-1 array('f') with bounds (num)

Other Parameters
----------------
num : input int, optional
    Default: len(in)

Returns
-------
out : float

100 loops, best of 3: 73.4 ms per loop

Slow?

This is because it uses python list (linked lists).

Let's try again with ctypes array:

In [21]:
import sum_abs_f2py

#sum_abs_f2py.sum_abs(values, len(values_c))
%timeit -n 100 sum_abs_f2py.sum_abs(values_c)
100 loops, best of 3: 3.82 ms per loop

Done! Questions?

Still wants more?

ctype: Structures

In [13]:
from ctypes import Structure
class POINT(Structure):
    _fields_ = [("x", c_int),
                ("y", c_int)]

point = POINT(10, 20)
print point.x, point.y
10 20

In [14]:
point = POINT(y=5)
print point.x, point.y
0 5

In [15]:
class RECT(Structure):
    _fields_ = [("upperleft", POINT),
                ("lowerright", POINT)]

rc = RECT(point)
print rc.upperleft.x, rc.upperleft.y
print rc.lowerright.x, rc.lowerright.y

r = RECT(POINT(1, 2), POINT(3, 4))
r = RECT((1, 2), (3, 4))
0 5
0 0

ctype: Passing pointers (or: passing parameters by reference)

In [6]:
from ctypes import cdll, create_string_buffer, byref, c_char_p, c_double, c_char, c_int, c_float

libc = cdll.LoadLibrary('libc.so.6')

i = c_int()
f = c_float()
s = create_string_buffer('\000' * 32)
print i.value, f.value, repr(s.value)

libc.sscanf("1 3.14 Hello", "%d %f %s",
            byref(i), byref(f), s)

print i.value, f.value, repr(s.value)
0 0.0 ''
1 3.1400001049 'Hello'

ctype: Specifying the required argument types (function prototypes)

In [8]:
printf = libc.printf

printf.argtypes = [c_char_p, c_char_p, c_int, c_double]

# works
printf("String '%s', Int %d, Double %f\n", "Hi", 10, 2.2)

# The output is in ipython console
# String 'Hi', Int 10, Double 2.200000

# raises an error
printf("String '%s', Int %d, Double %f\n", "Hi", 10, "a")
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
<ipython-input-8-9e40bf26f47c> in <module>()
     10 
     11 # raises an error
---> 12 printf("String '%s', Int %d, Double %f\n", "Hi", 10, "a")
     13 

ArgumentError: argument 4: <type 'exceptions.TypeError'>: wrong type

ctype: Return types

In [9]:
# By default functions are assumed to return the C int type. 
#  Other return types can be specified by setting the restype attribute of the function object.

strchr = libc.strchr

# fix argtype
strchr.argtypes = [c_char_p, c_char]

print strchr("abcdef", "d")
-833834537

In [10]:
# c_char_p is a pointer to a string
strchr.restype = c_char_p
print strchr("abcdef", "d")
def

All material on this presentation is covered by CC BY-SA 4.0 and GPLv3 licenses.