воскресенье, 30 июня 2013 г.

Python: примеры и тесты, часть 3 - SWIG

Показанная раньше техника создания модулей Python на C/C++ понятна и достаточно хорошо описана в документации в документации: передача объектов от исполняющей системы Python (параметры), преобразование значений переменных C/C++ в объекты Python (возврат значений)... Недостающие детали можно подсмотреть в заголовочном файле <python*/modsupport.h>. Но всё-таки такая ручная техника создания модулей на C/C++ достаточно громоздкая. Для её упрощения создано несколько инструментальных пакетов, автоматизирующих и упрощающих эту деятельность (Cython, SWIG и другие). Мной опробовано (пока?) только:

Использование SWIG.

В качестве упражнения я выбрал для разбирательства весьма простую задачу: подсчитать частоту вхождения в произвольный текст символов (256 символов из основной таблицы ASCII, только 1-байтовых, для упрощения). Реализующий C-код имеет вид (файл freq.c):

#include <stdlib.h>
 
int* frequency( char s[] ) {
    int *freq;
    char *ptr;
    freq = (int*)( calloc( 256, sizeof( int ) ) );
    if( freq != NULL )
        for( ptr = s; *ptr; ptr++ )
            freq[ *ptr ] += 1;
    return freq;
}


Теперь мы не пишем в коде программную реализацию интерфейса к этой  функции C, как в предыдущем рассмотрении, а составляем интерфейсный файл SWIG  (freq.i, расширение i):

%module freq
%typemap(out) int* {
    int i;
    $result = PyTuple_New( 256 );
    for( i = 0; i < 256; i++ )
        PyTuple_SetItem( $result, i, PyLong_FromLong( $1[ i ] ) );
    free( $1 );
}
extern int* frequency( char s[] );


Даже вот такая, несколько усложнённая форма описания, связана только с тем фактом, что С-функция frequency() динамически приобретает память, и после использования её нужно вернуть во избежание утечек памяти. Во многих же практических случаях (см. далее) интерфейсные описания и того проще. После создания мы обрабатываем файл описания вот таким образом:

$ swig -python freq.i

В результате чего генерируются файла исходного кода интерфейса модуля (freq_wrap.c). Из имеющихся файлов уже можно непосредственно собирать динамическую библиотеку, реализующую модуль Python:

$ gcc -c -fpic freq_wrap.c freq.c -DHAVE_CONFIG_H -I/usr/include/python2.7 -I/usr/lib/python2.7/co
$ ld -shared freq_wrap.o freq.o -o _
freq.so

Всё! Нам остаётся только написать тестирующее приложение Python (файл frtest.py) для непосредственного импорта созданного модуля:

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
from sys import argv
import freq
 
sarg = lambda : ( len( argv ) > 1 and argv[ 1 ] ) or str( input( "string?: " ) )
 
F = freq.frequency( sarg() )
s = ""
for i in range( 256 ):
    if F[ i ] != 0: s = s + ( "x%x:%d " % ( i, F[ i ] ) )
print s


В итоге мы получили работоспособный модуль Python (с именем freq), пусть не очень умный, но в достаточной мере иллюстрирующий технику использования SWIG:

$ python frtest.py "1234123121 abc ab a"
x20:3 x31:4 x32:3 x33:2 x34:1 x61:3 x62:2 x63:1


Прямой доступ к библиотекам C/C++.

В этой технике интересно создание модулей прямых интерфейсов к уже  существующим ранее целевым библиотекам (.so) C/C++ (при отсутствии их  аналогов в Python), это вообще практически без написания кода. В порядке иллюстрации сказанного я сделаю интерфейсный модуль к файловым операциям C. Файл интерфейсного описания SWIG (fileio.i) имеет вид:

%module fileio
extern FILE *fopen(char *, char *);
extern int fclose(FILE *);
extern unsigned fread(void *ptr, unsigned size, unsigned nobj, FILE *);
extern unsigned fwrite(void *ptr, unsigned size, unsigned nobj, FILE *);
extern void *malloc(int nbytes);
extern void free(void *);


Больше ничего нам, собственно, и не придётся делать:


$ swig -python fileio.i
$ gcc -c -fpic fileio_wrap.c -DHAVE_CONFIG_H -I/usr/include/python2.7 -I/usr/lib/python2.7/config
$ ld -shared fileio_wrap.o -lc -o _fileio.so


Тестовая задача (fiotest.py) на Python:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from fileio import *
import fileio
from sys import argv
 
def filecopy( source, target ):  # Copy a file
    sum = long( 0 )
    f1 = fopen( source, "r" )
    if f1 == None: return -1
    f2 = fopen( target, "w" )
    buffer = malloc( 8192 )
    nbytes = fread( buffer, 1, 8192, f1 )
    while( nbytes > 0 ):
        fwrite( buffer, 1, nbytes, f2 )
        sum = sum + nbytes
        nbytes = fread( buffer, 1, 8192, f1 )
    free( buffer )
    fclose( f1 )
    fclose( f2 )
    return sum
 
n = filecopy( ( len( argv ) > 1 and argv[ 1 ] ) or "in.txt", \
              ( len( argv ) > 2 and argv[ 2 ] ) or "out.txt" )
print "скопировано %d байт" % n


И вот её функционирование:

$ python fiotest.py in.txt out.txt
скопировано 178 байт
$ ls -l *.txt
-rw-rw-r-- 1 olej olej 178 июня 21 23:48 in.txt
-rw-rw-r-- 1 olej olej 178 июня 22 00:14 out.txt




Python: примеры и тесты, часть 2 - модули C/C++

Написание модулей Python на языках C/C++.


Этот предмет в меру детально описан в оригинальной документации на сайте Python. Тем не менее, хотелось это проверить и проделать автономно этот путь (хотя бы для того, чтобы оценить его трудоёмкость).

Интерес представляла реализация измерителя временных интервалов наносекундного диапазона, построенного на чтении аппаратного счётчика процессорных циклов процессоров x86 (такой счётчик появился начиная с Pentium II, на других архитектурах Linux это не работает). Функция (файл rdtsc.c), читающая значение этого счётчика, на языке C (или, если точнее, на инлайн ассемблерной вставке, допускаемой как компилятором GCC, так и компилятором Clang):

unsigned long long rdtsc( void ) {
    unsigned long long int x;
    asm volatile ( "rdtsc" : "=A" (x) );
    return x;
}


Для более старых версий GCC, не распознающих мнемонику ассемблерной команды rdtsc из расширенного набора команд (для операционных систем Solaris, Minix 3), этот код может выглядеть так:

unsigned long long rdtsc( void ) {
    unsigned long long int x;
    asm volatile ( ".byte 0x0f, 0x31" : "=A" (x) );
    return x;
}


Хотелось бы получить интерфейс для вызова rdtsc() из среды Python. Этот пример крайне прост, чем хорош для экспериментирования, и обладает некоторой практической полезностью. Пишем, пользуясь документацией с сайта Python, интерфейсный модуль (файл rdtsc_wrap.c)

// это будет модуль Python "rdtsc":
#include <Python.h>
extern unsigned long long rdtsc( void );
  
PyObject* rdtsc_wrap( PyObject* self, PyObject* args ) {
    if( self != NULL ) return NULL; // обработка ошибки вызова
    return Py_BuildValue( "L", rdtsc() );

  
// таблица методов
static PyMethodDef rdtscmethods[] = {
    { "rdtsc", rdtsc_wrap, METH_NOARGS },
    { NULL, NULL }
};
  
// функция инициализации модуля
void initrdtsc() {
    Py_InitModule( "rdtsc", rdtscmethods );
}

Собираем всё это командами:

$ gcc -c -fpic rdtsc_wrap.c rdtsc.c -I/usr/include/python2.7
$ ld -shared rdtsc_wrap.o rdtsc.o -lc -o rdtscmodule.so


После чего получим реализующую модуль динамическую библиотеку:

$ ls -l *.so
-rwxrwxr-x 1 olej olej 2616 июня 26 23:23 rdtscmodule.so

Для проверки результата создадим два (для сравнения) тестовых приложения: одно на языке C и одно - на Pyton. Попутно реализуем ещё одну полезную функцию того же сорта (что и rdtsc()) - calibr(), интервал времени (в циклах частоты процессора) для вычисления двух последовательных, следующих друг за другом, вызовов rdtsc() (это можно считать временем выполнения самого вызова rdtsc() при измерении протяжённости очень коротких операций).

Тест на C (файл ctest.c):

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
extern unsigned long long rdtsc( void );
  
#define NUMB 10
static unsigned calibr( int rep ) {
    uint32_t n, m, sum = 0;
    n = m = ( rep <= 0 ? NUMB : rep );
    while( n-- ) {
        uint64_t cf, cs;
        cf = rdtsc();
        cs = rdtsc();
        sum += (uint32_t)( cs - cf );
    }
    return sum / m;

  
int main( int argc, char **argv, char **envp ) {
    printf( "число процессорных тактов = %llu\n", rdtsc() );
    printf( "калибровка последовательных вызовов:" );
    printf( " %lu(0)", calibr( 0 ) );
    int n;
    for( n = 10; n <= 100000; n*=10 )
        printf( " %lu(%d)", calibr( n ), n );
        printf( "\n");
    exit( EXIT_SUCCESS );
};

Выполнение такого теста:

$ ./ctest
число процессорных тактов = 79971042747980
калибровка последовательных вызовов: 117(0) 115(10) 115(100) 147(1000) 139(10000) 115(100000)


Подобный тест, но на этот раз написанный на Python:

  • файл модуля (calibr.py), реализующего calibr():
#!/usr/bin/python
# -*- coding: utf-8 -*-
from rdtsc import rdtsc
 
def calibr( args = 10 ):
    sum = 0L
    if int( args ) <= 0: n = 10
    else: n = int( args )
    m = n
    while n > 0 :
        cf = -( rdtsc() - rdtsc() )
        sum = sum + cf
        n = n - 1
    return sum / m

  • файл (ptest.py) вызывающего теста: 

#!/usr/bin/python -O
# -*- coding: utf-8 -*-
from rdtsc import rdtsc
from calibr import calibr
 
counter = []
for i in range( 5 ):
counter.append( rdtsc() )
print "счётчик процессорных циклов:\n", counter
 
arg = [ 0, 10, 100, 1000, 10000, 100000 ]
msg = "калибровка последовательных вызовов:"
for i in arg:
s = " %s(%i)" % ( str( calibr( i ) ), i )
msg = msg + s
print msg


Выполнение такого теста:

$ python -O ptest.py
счётчик процессорных циклов:
[79971137334370L, 79971137337650L, 79971137338900L, 79971137340040L, 79971137341090L]
калибровка последовательных вызовов: 548(0) 515(10) 947(100) 500(1000) 487(10000) 484(100000)


Всё достаточно предсказуемо!

Этот источник (rdtsc()) временных меток высокого разрешения я буду неоднократно использовать в тестах параллельного выполнение (потоков, процессов) для наблюдения за последовательностью операций.

Python: примеры и тесты, часть 1 - функции

На сегодня, по моему мнению, Python составляет лучший "тендем" разработчику C/C++ в Linux, из числа интерпретирующих языков. Ещё лет 5 назад я бы на должность такого кандидата называл бы Perl. Подобные инструменты крайне необходим в крупных проектах, начиная от написания скриптов и тестов, и до полновесной реализации проекта (в частности, в качестве подобных вспомогательных инструментов в разном качестве интересны, например, JavaScript и Lua).

Но многие стороны использования Python достаточно поверхностно  описаны в документации и публикациях, примеры кода их иллюстрирующие, временами, старые или неработающие ("писано с головы" ... не на клавиатуре проверялось). Поэтому по уточнению отдельных аспектов использования Python мне потребовалось написать (или воссоздать из существующих) примеры кода и тесты его применимости. И мне показалось, что некоторые из них могут быть любопытны и другим пользователям...

P.S. Меня совершенно не интересует использование Python в WEB-разработках, где может использоваться с равным успехом любой язык или технология: Perl, JavaScript, ... CGI-интерфейс, Ajax и другие всякие штучки. Это совершенно особая песня. Речь идёт только о использовании Python в "классическом" программировании.  

Хронологическое развитие темы Python развёрнуто в форумном обсуждении форумном обсуждении, но там оно, как и полагается такому обсуждению "в прямом эфире" скомкано и рвано... Кроме того, в том объёмном обсуждении затронуты другие сопутствующие темы: обсуждение публикаций и описаний языка, учебных курсов, интегрированных сред разработки (IDE), облегчающих разработку в Python. Здесь же всё это, как "рутинные" вопросы, будет отброшено, и  предполагается сделать некоторую упорядоченную подборку по предмету "примеры и тесты", как и сказано в заголовке. И дополнять их по мере накопления новых ... примеров и тестов...

Собственно, для программирования в UNIX/Linux меня интересовало всего несколько аспектов Python, по моему мнению явно недостаточно освещённых в литературе. А именно:

  • "ручное" написание  собственных модулей Python на языках C/C++;
  • использование для этого специализированных инструментальных средств, таких как Swig ... и других полезных инструментов;
  • обработка опций командной строки (модуль getopt) для построения консольных приложений в UNIX-стиле;
  • реализация параллельных потоков (причём как в технике "низкого уровня" - модуль thread, так и технике "высокого уровня" - модуль threading);
  • какие доступные механизмы синхронизаций потоков как в том, так и в другом варианте (поскольку как без синхронизации цена параллелизму - ноль);
  • использование fork() и других механизмов для порождения дочерних процессов;
  • работа с сигналами UNIX (модуль signal) ... 
  • средства метапрограммирования в Python: метаклассы, декораторы...
Ну и, возможно, некоторые другие...

Функциональное программирование. 

О функциональном стиле программирования в Python написано много: практически ни одно руководство по языку не обходится без главы с таким названием... Но всё написанное базируется на очень ограниченном числе публикаций от одного-двух авторов. Представляется так, что ... "слухи о функциональном программировании на Python сильно преувеличены". Функционально программировать лучше, пожалуй, на языках, предназначенных специально для функционального программирования: Lisp, Haskell, Scheme... Но в Python можно с успехом строить фрагменты в стиле функционального программирования, с пользой для итогового результата. Это достаточно сильно напоминает то, как функциональный стиль использовался в языке APL. И, конечно, как всегда в функциональном программировании, с широчайшим применением рекурсии, часто и для замены итерационных вычислений.

Традиционно самый распространённый приём иллюстрации рекурсии - это вычисление факториала (это как программа "Hello world!" в иллюстрации любого языка программирования). Но, не исключено, что факториал и есть обоснованно удачный пример в виду своей простоты. Вот такой пример:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from sys import *
arg = lambda : ( len( argv ) > 1 and int( argv[ 1 ] ) ) or  \
                            int( input( "число?: " ) )
factorial = lambda x: ( ( x == 1 ) and 1 ) or x * factorial( x - 1 )
n = arg()
m = getrecursionlimit()
if n >= m :
    print "глубина рекурсии превышает установленную в системе %d, \ 
               переустановлено в %d" % \
              ( m, n + 1 )
setrecursionlimit( n + 1 )
print "n=%d => n!=%d" % ( n, factorial( n ) )
if getrecursionlimit() > m :
    print "глубина рекурсии восстановлена в %d" % m
setrecursionlimit( m )

Собственно, самой программы и определениями функций здесь являются только две строки: обеспечивающая ввод аргумента вычисления arg, и осуществляющая вычисление факториала от этого аргумента:

$ ./fact2.py 10
n=10 => n!=3628800

$ ./fact2.py
число?: 10
n=10 => n!=3628800


Вся оставшаяся большая часть связана с тем, что я прочитал где-то в интернет-обсуждениях такую глупость: "... а вы попробуйте на Python вычислить факториал больше 1000 с соотвествующей глубиной рекурсии...". Попробуем:

$ ./fact2.py 10000
глубина рекурсии превышает установленную в системе 1000, переустановлено в 10001
n=10000 => n!=28462596809...000
глубина рекурсии восстановлена в 1000

Ну и показанноое число-результат (28462596809...000) будет занимать запись в 163 строки терминала. Но это огромное значение замечательно вычислилось исполняющей системой Python!

Хотя и без рекурсии реализация вычисления факториала в функциональном стиле выглядит красиво, используя встроенные функции высшего порядка, в данном случае reduce (это уже стиль языка APL):

factorial = lambda z: reduce( lambda x, y: x * y, range( 1, z + 1 ), 1 )
if len( sys.argv ) > 1: n = int( sys.argv[ 1 ] )
else: n = int( input( "число?: " ) )
print "n=%d => n!=%d" % ( n, factorial( n ) )


И снова проверяем:

$ ./fact3.py 5
n=5 => n!=120

$ ./fact3.py 50
n=50 => n!=30414093201713378043612608166064768844377641568960512000000000000



Функции высших порядков. 

Функциональное программирование предоставляет возможности, недостижимые в императивном программировании (или достигаемые очень искусственными приёмами). В частности, это связано с возможностью динамического создания новых функциональных объектов "на лету". Вот некоторые из таких приёмов:

  • Замыкание - когда из вызова возвращается объект-функция, конкретизированная параметрами образующего вызова; 
  • Функторы - параметризируемые (параметрами конструктора) объекты класса, который допускает функциональный вызов для экземпляров этого класса;
  • Частичное применение функции - когда функция нескольких переменных фиксируется для частных значений некоторых из этих переменных (уменьшается размерность переменных функции);
  • Карринг (англ. currying, или каррирование) — преобразование функции от многих аргументов в функцию, берущую свои аргументы по одному. 

Проверяем как это работает:

#!/usr/bin/python
# -*- coding: utf-8 -*-
def addClosure( val1 ):         # замыкание - closure
    def closure( val2 ):
        return val1 + val2
    return closure
cl = addClosure( 3 )            # новая функция
   
class addFunctor:               # эквивалентный предыдущему функтор 
    def __init__( self, val1 ):
        self.val1 = val1
    def __call__( self, val2 ):
        return self.val1 + val2
fn = addFunctor( 3 )            # экземпляр класса addFunctor
   
from functools import partial   # частичное применение функции
def addPart( a, b ):            # функция 2-х переменных
    return a + b
pr = partial( addPart, 3 )      # новая функция 1-й переменной
   
print cl( 2 ), fn( 2 ), pr( 2 ) # напечатает: 5 5 5

Каждый из 3-х вызовов в последнем операторе print синтаксически корректен, и возвратит своим значением 5.

Пример карринга (файл curry1.py):

#!/usr/bin/python
# -*- coding: utf-8 -*-
def spam( x, y ) :
    print u"x=%d, y=%d" % ( x, y )

   
spam1 = lambda x : lambda y : spam( x, y ) # новая функция
 
def spam2( x ) :
                          # новая функция
    def new_spam( y ) :
        return spam( x, y )
    return new_spam
 
spam1( 2 )( 3 )
spam2( 2 )( 3 )


$ ./curry1.py
x=2, y=3
x=2, y=3