cython

cython позволяет писать программы, выглядящие почти как питонские, но с добавлением статических деклараций типов. Эти программы (foo.pyx) транслируются в исходные тексты на C (foo.c) и затем компилируются. Определённые в них функции могут использоваться из программ на чистом питоне. Программа на cython-е может также вызывать функции из библиотек, написанных на C. cython не пытается автоматически сгенерировать интерфейсы к таким библиотекам, читая их .h файлы; для этого можно использовать swig или другие подобные системы.

В ipython можно писать cython фрагменты inline, если загрузить расширение cython.

In [1]:
%load_ext cython

Функции

Это интерпретируемая функция на питоне.

In [2]:
def fib(n):
    if n<=2:
        return 1
    a,b=1,1
    for i in range(n-2):
        a,b=b,a+b
    return b
In [3]:
fib(90)
Out[3]:
2880067194370816120
In [4]:
%timeit fib(90)
100000 loops, best of 3: 7.05 µs per loop

Это такая же функция на cython, типы переменных не объявлены - то есть все они обычные питонские объекты.

In [5]:
%%cython
def dyn_fib(n):
    if n<=2:
        return 1
    a,b=1,1
    for i in range(n-2):
        a,b=b,a+b
    return b
In [6]:
dyn_fib(90)
Out[6]:
2880067194370816120
In [7]:
%timeit dyn_fib(90)
100000 loops, best of 3: 5.52 µs per loop

Получилось чуть быстрее. Скомпилированная программа выполняет всю ту же возню с типами и их преобразованиями, что и интерпретируемая.

Теперь типы декларированы статически.

In [8]:
%%cython
def stat_fib(long n):
    cdef long i,a,b
    if n<=2:
        return 1
    a,b=1,1
    for i in range(n-2):
        a,b=b,a+b
    return b
In [9]:
stat_fib(90)
Out[9]:
2880067194370816120
In [10]:
%timeit stat_fib(90)
The slowest run took 6.68 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 630 ns per loop

Получилось на порядок быстрее.

c_fib - это фактически функция на C, только написанная в cython-ском синтаксисе. Её можно вызывать откуда угодно в той же программе на cython, но не из питонской программы. Поэтому напишем обёртку, которую можно вызывать из питона.

In [11]:
%%cython
cdef long c_fib(long n):
    cdef long i,a,b
    if n<=2:
        return 1
    a,b=1,1
    for i in range(n-2):
        a,b=b,a+b
    return b
def wrap_fib(long n):
    return c_fib(n)
In [12]:
wrap_fib(90)
Out[12]:
2880067194370816120
In [13]:
%timeit wrap_fib(90)
The slowest run took 5.85 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 632 ns per loop

Время то же самое.

cpdef создаёт как C функцию, так и питонскую. Первая вызывается из cython, вторая из питона.

In [14]:
%%cython
cpdef long cp_fib(long n):
    cdef long i,a,b
    if n<=2:
        return 1
    a,b=1,1
    for i in range(n-2):
        a,b=b,a+b
    return b
In [15]:
cp_fib(90)
Out[15]:
2880067194370816120
In [16]:
%timeit cp_fib(90)
The slowest run took 5.04 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 637 ns per loop

Время то же самое.

Интерфейс к библиотеке на C

Пусть у нас есть файл на C.

In [17]:
!cat cfib.c
long cfib(long n)
{   long i,a,b,c;
    if(n<=2) return 1;
    {   a=1; b=1;
        for(i=2;i<n;++i)
        { c=a+b; a=b; b=c; }
        return b;
    }
}
In [18]:
!cat cfib.h
long cfib(long n);

Скомпилируем его.

In [19]:
!gcc -fPIC -c cfib.c

Напишем обёртку на cython.

In [20]:
!cat wrap.pyx
cdef extern from "cfib.h":
    long cfib(long n)

def fib(long n):
    return cfib(n)

Скомпилируем её и соберём в библиотеку.

In [21]:
%%!
cython -3 wrap.pyx
CFLAGS=$(python-config --cflags)
LDFLAGS=$(python-config --ldflags)
gcc $CFLAGS -fPIC -c wrap.c
gcc $LDFLAGS -shared wrap.o cfib.o -o wrap.so
Out[21]:
[]

Эту библиотеку можно импортировать в программу на питоне.

In [22]:
from wrap import fib
In [23]:
fib(90)
Out[23]:
2880067194370816120
In [24]:
%timeit fib(90)
The slowest run took 4.65 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 574 ns per loop

Пулучилось чуть быстрее, чем функция на cython.

Структуры

Структуры можно описывать в cython с помощью ctypedef struct. Поля в них описываются фактически в синтаксисе C. Переменную, описываемую в cdef, можно, если хочется, сразу инициализировать. Имя типа-структуры можно использовать как функцию, аргументы которой - её поля (в порядке описания). print печатает структуру как словарь; на самом деле это не словарь, а структура языка C, не содержащая накладных расходов по памяти и времени, имеющихся у словаря, но и не дающая гибкости словаря. Поля структуры обозначаются z.re; их можно менять.

В cython можно работать с указателями. Импортируем malloc и free из стандартной библиотеки. Результат malloc - адрес, его нужно привести к правильному типу, используя <type>. В C поля структуры, на которую ссылается w, обозначаются w->re; в cython - просто w.re. В C структура, на которую ссылается w, обозначается *w; в cython такой синтаксис не разрешён, вместо этого надо писать w[0] (в C это тоже законная форма записи, но чаще используется *w). При работе с указателями управление памятью производится вручную, а не автоматически, как в питоне, так что не забывайте free.

In [25]:
%%cython
ctypedef struct mycomplex:
    double re
    double im
cdef mycomplex z=mycomplex(1.,2.)
print(z)
print(z.re)
z.re=-1
print(z)
# pointers
from libc.stdlib cimport malloc,free
cdef mycomplex *w=<mycomplex*>malloc(sizeof(mycomplex))
w.re,w.im=2.,1.
print(w[0])
free(w)
{'re': 1.0, 'im': 2.0}
1.0
{'re': -1.0, 'im': 2.0}
{'re': 2.0, 'im': 1.0}

cdef классы

cython позволяет определять классы, объекты которых являются фактически структурами языка C. Их атрибуты нужно статически описывать с помощью cdef; во время выполнения нельзя добавлять новые атрибуты (или уничножать имеющиеся). Вот пример такого класса. Его основной метод atol вызывает функцию atol из стандартной библиотеки C, преобразующую строку в long.

In [26]:
!cat C1.pyx
from libc.stdlib cimport atol

cdef class C1:

    cdef:
        char *s
        long n

    def __init__(self):
        self.s=NULL
        self.n=0

    def set_s(self,bytes s):
        self.s=s

    def get_n(self):
        return self.n

    def atol(self):
        self.n=atol(self.s)

Есть удобный способ импортировать pyx модуль в питон: pyximport, он автоматически произведёт преобразование в C, компиляцию и сборку.

In [27]:
import pyximport
pyximport.install()
Out[27]:
(None, <pyximport.pyximport.PyxImporter at 0x7f2185779780>)
In [28]:
from C1 import C1
In [29]:
o=C1()
s=b"12345"
o.set_s(s)
o.atol()
print(o.get_n())
12345

Тип char* в C соответствует типу bytes в питоне. При совместном использовании питона с его автоматическим управлением памятью и C с указателями нужно соблюдать осторожность. Строка b"12345" доступна в питоне как значение переменной s, поэтому занимаемая ей память не будет освобождена, пока s не будет присвоено другое значение. Мы скопировали её адрес в атрибут o.s типа char*. Если бы мы не присвоили эту строку переменной s, а прямо подставили бы её в качестве аргумента метода o.set_s, то питон не знал бы, что её надо сохранять, и освободил бы занимаемую её память. Указатель o.s указывал бы после этого неведомо куда, с катастрофическими последствиями.

Усовершенствуем немного эту cython программу. По умолчанию cdef атрибуты недоступны ни из питона, ни из cython программы. Но можно описать их как public или readonly, тогда не нужны будут методы get_foo и set_foo. Метод __init__ может быть и не будет вызван (например, другой класс унаследовал текущий, и его __init__ не вызвал __init__ родителя); если в структуре есть указатели, то они могут остаться неинициализированными. Поэтому лучше использовать __cinit__, который обязательно вызывается сразу после выделения память для объекта.

In [30]:
!cat C2.pyx
from libc.stdlib cimport atol

cdef class C2:

    cdef public char *s
    cdef readonly long n

    def __cinit__(self):
        self.s=NULL
        self.n=0

    def atol(self):
        self.n=atol(self.s)
In [31]:
from C2 import C2
In [32]:
o=C2()
o.s=s
o.atol()
print(o.n)
12345

cdef классы поддерживают наследование (только от одного класса, не множественное). Можно написать класс-потомок как cdef класс на cython. Можно и написать класс-потомок на питоне. Пусть мы хотим добавить к нашему классу метод преобразования строки в число с плавающей точкой, но нам лень использовать atof из стандортной библиотеки C. Сделаем это обычными средствами питона. Атрибут x добавляется к объектам класса C3 динамически, описывать его не надо.

In [33]:
class C3(C2):
    
    def atof(self):
        self.x=float(self.s)
In [34]:
o=C3()
s=b"12345.6789"
o.s=s
o.atof()
print(o.x)
12345.6789

Интерфейс к библиотеке на C

Рассмотрим очень упощённый пример того, как можно написать удобный питонский интерфейс к библиотеке на C, используя cython. Если бы мы хотели использовать эту библиотеку из программы на C, достаточно было бы включить #include "foo.h" в эту программу.

In [35]:
!cat cfoo.h
typedef struct { long n; double x; } CFoo;
CFoo *Foo_new(long n,double x);
void Foo_del(CFoo *z);
double Foo_f(CFoo *z,double y);

Здесь описан тип-структура CFoo. Функция Foo_new создаёт и инициализирует такую структуру и возвращает указатель на неё. Функция Foo_del уничтожает эту структуру. Наконец, функция Foo_f делает какое-то вычисление со своим параметром y и данными из структуры. Подобным образом часто выглядят интерфейсы к генераторам случайных чисел: мы можем создать несколько структур с начальными данными и получить несколько независимых потоков случайных чисел.

А вот реализация на C.

In [36]:
!cat cfoo.c
#include <stdlib.h>
#include "cfoo.h"

CFoo *Foo_new(long n,double x)
{   CFoo *r=(CFoo*)malloc(sizeof(CFoo));
    r->n=n;
    r->x=x;
    return r;
}

void Foo_del(CFoo *z)
{ free(z); }

double Foo_f(CFoo *z,double y)
{ return z->n*y+z->x; }

В первую очередь мы напишем файл определений cython. Он почти копирует foo.h с минимальными синтаксическими изменениями.

In [37]:
!cat foo.pxd
cdef extern from "cfoo.h":

    ctypedef struct CFoo:
        pass

    CFoo *Foo_new(long n,double x)
    void Foo_del(CFoo *z)
    double Foo_f(CFoo *z,double y)

Теперь напишем удобную объектно-ориентированную обёртку. Файл определений импортируется при помощи cimport (мы уже использовали эту команду, когда импортировали libc.stdlib; cython содержит ряд стандартных pxd файлов, включая stdlib.pxd, stdio.pxd и т.д.). Теперь определим cdef класс Foo. Метод __dealloc__ вызывается в последний момент перед уничтожением объекта (условие if self.foo!=NULL: написано из перестраховки, в законном объекте класса Foo этот атрибут всегда не NULL, т.к. он инициализируется в __cinit__).

In [38]:
!cat foo.pyx
cimport foo

cdef class Foo:

    cdef foo.CFoo *foo

    def __cinit__(self,long n,double x):
        self.foo=foo.Foo_new(n,x)

    def __dealloc__(self):
        if self.foo!=NULL:
            foo.Foo_del(self.foo)

    def f(self,double y):
        return foo.Foo_f(self.foo,y)

Скомпилируем и соберём.

In [39]:
%%!
gcc -fPIC -c cfoo.c
cython -3 foo.pyx
CFLAGS=$(python-config --cflags)
LDFLAGS=$(python-config --ldflags)
gcc $CFLAGS -fPIC -c foo.c
gcc $LDFLAGS -shared foo.o cfoo.o -o foo.so
Out[39]:
[]
In [40]:
from foo import Foo

Теперь мы можем в питоне создавать объекты класса Foo и вызывать их метод f.

In [41]:
o=Foo(2,0.)
o.f(3.)
Out[41]:
6.0