Spec-Zone.ru › NumPy 1.19

Использование Python в качестве связующего языка

Многие говорят, что Python — замечательный связующий язык. Надеюсь, эта глава убедит вас в этом. Первые пользователи Python в научных целях обычно использовали его для связывания больших прикладных кодов, работающих на суперкомпьютерах. Использование Python было намного удобнее, чем использование командной оболочки или Perl, а также возможность легко расширять Python делала относительно простым создание новых классов и типов, специально адаптированных к решаемым задачам. Из взаимодействия этих ранних участников возник Numeric — объект, похожий на массив, который можно использовать для передачи данных между этими приложениями.

По мере созревания и развития Numeric в NumPy люди смогли написать больше кода непосредственно в NumPy. Часто этот код достаточно быстрый для использования в производстве, но иногда все же возникает необходимость доступа к скомпилированному коду. Либо для достижения последней части эффективности алгоритма, либо для более легкого доступа к широко распространенным кодам, написанным на C/C++ или Fortran.

В этой главе будут рассмотрены многие инструменты, доступные для доступа к коду, написанному на других скомпилированных языках. Существует много ресурсов для изучения вызова других скомпилированных библиотек из Python, и цель этой главы — не сделать вас экспертом. Основная цель — познакомить вас с некоторыми возможностями, чтобы вы знали, что «погуглить», чтобы узнать больше.

Вызов других скомпилированных библиотек из Python

Хотя Python — это великолепный язык, и писать на нем приятно, его динамическая природа приводит к накладным расходам, которые могут привести к тому, что некоторый код (например, численные вычисления внутри циклов for) будет выполняться в 10–100 раз медленнее, чем эквивалентный код, написанный на статически скомпилированном языке. Кроме того, это может привести к большему потреблению памяти, так как временные массивы создаются и уничтожаются во время вычислений. Для многих типов вычислительных задач дополнительные замедления и потребление памяти часто недопустимы (по крайней мере, для критически важных по времени или памяти частей вашего кода). Поэтому одна из самых распространенных потребностей — вызов из кода Python быстрой, машинной кодовой рутины (например, скомпилированной с использованием C/C++ или Fortran). Тот факт, что это относительно легко сделать, является большой причиной, почему Python — это превосходный язык высокого уровня для научного и инженерного программирования.

Существует два основных подхода к вызову скомпилированного кода: написание модуля расширения, который затем импортируется в Python с помощью команды import, или прямой вызов подпрограммы общей библиотеки из Python с помощью модуля ctypes. Написание модуля расширения — наиболее распространенный метод.

Предупреждение

Вызов кода C из Python может привести к сбою Python, если вы не будете осторожны. Ни один из подходов в этой главе не застрахован. Вам нужно знать что-то о том, как обрабатываются данные как NumPy, так и используемой сторонней библиотекой.

Самописные обертки

Модули расширения обсуждались в Написание модуля расширения. Наиболее базовый способ взаимодействия со скомпилированным кодом — написание модуля расширения и построение метода модуля, который вызывает скомпилированный код. Для повышения читаемости ваш метод должен использовать PyArg_ParseTuple для преобразования между объектами Python и типами данных C. Для стандартных типов данных C, вероятно, уже есть встроенный преобразователь. Для других вам может потребоваться написать свой преобразователь и использовать формат "O&" , который позволяет указать функцию, которая будет использоваться для преобразования объекта Python в необходимые структуры C.

После преобразования в соответствующие структуры и типы данных C следующий шаг в обертке — вызов базовой функции. Это просто, если базовая функция находится на C или C++. Однако для вызова кода Fortran вам необходимо знать, как вызывать подпрограммы Fortran из C/C++ с помощью вашего компилятора и платформы. Это может несколько отличаться на разных платформах и компиляторах (поэтому f2py делает жизнь намного проще для взаимодействия с кодом Fortran), но, как правило, включает в себя замену подчеркиваний имени и тот факт, что все переменные передаются по ссылке (то есть все аргументы являются указателями).

Преимущество самописной обертки заключается в том, что вы полностью контролируете, как используется и вызывается библиотека C, что может привести к точной и тесной интеграции с минимальными накладными расходами. Недостатком является то, что вам нужно писать, отлаживать и поддерживать код C, хотя большая его часть может быть адаптирована с помощью проверенного временем метода «вырезать-вставить-и-изменить» из других модулей расширения. Поскольку процедура вызова дополнительного кода C довольно регламентирована, были разработаны процедуры генерации кода, чтобы упростить этот процесс. Одна из этих техник генерации кода распространяется с NumPy и позволяет легко интегрировать Fortran и (простой) код C. Этот пакет, f2py, будет кратко рассмотрен в следующем разделе.

f2py

F2py позволяет автоматически создавать модуль расширения, который взаимодействует с подпрограммами в коде Fortran 77/90/95. Он имеет возможность анализировать код Fortran 77/90/95 и автоматически генерировать сигнатуры Python для встречающихся подпрограмм или вы можете направить, как подпрограмма взаимодействует с Python, создав файл определения интерфейса (или изменив сгенерированный f2py).

Создание исходного кода для простого модуля расширения

Вероятно, самый простой способ ввести f2py — предложить простой пример. Вот одна из подпрограмм, содержащихся в файле с именем add.f:

C
      SUBROUTINE ZADD(A,B,C,N)
C
      DOUBLE COMPLEX A(*)
      DOUBLE COMPLEX B(*)
      DOUBLE COMPLEX C(*)
      INTEGER N
      DO 20 J = 1, N
         C(J) = A(J)+B(J)
 20   CONTINUE
      END

Эта рутина просто суммирует элементы в двух смежных массивах и помещает результат в третий. Память для всех трех массивов должна быть предоставлена вызывающей программой. Очень базовый интерфейс с этой рутиной может быть автоматически сгенерирован f2py:

f2py -m add add.f

Вы должны иметь возможность запустить эту команду, предположительно, если ваш путь поиска правильно настроен. Эта команда создаст модуль расширения с именем addmodule.c в текущем каталоге. Этот модуль расширения теперь можно скомпилировать и использовать из Python, как любой другой модуль расширения.

Создание скомпилированного модуля расширения

Вы также можете заставить f2py скомпилировать add.f и также скомпилировать созданный модуль расширения, оставив только файл расширения общей библиотеки, который можно импортировать из Python:

f2py -c -m add add.f

Эта команда оставляет файл с именем add.{ext} в текущем каталоге (где {ext} — соответствующее расширение модуля расширения python на вашей платформе — например, pyd и т. д.). Этот модуль затем можно импортировать из Python. Он будет содержать метод для каждой подпрограммы в add (zadd, cadd, dadd, sadd). Строка документации каждого метода содержит информацию о том, как можно вызвать метод модуля:

>>> import add
>>> print(add.zadd.__doc__)
zadd - Function signature:
  zadd(a,b,c,n)
Required arguments:
  a : input rank-1 array('D') with bounds (*)
  b : input rank-1 array('D') with bounds (*)
  c : input rank-1 array('D') with bounds (*)
  n : input int

Улучшение базового интерфейса

По умолчанию интерфейс — это очень буквальный перевод кода Fortran в Python. Аргументы массива Fortran теперь должны быть массивами NumPy, а целочисленный аргумент должен быть целым числом. Интерфейс будет пытаться преобразовать все аргументы в требуемые типы (и формы) и выдавать ошибку, если преобразование невозможно. Однако, поскольку он ничего не знает о семантике аргументов (например, C — выходной, а n должен действительно соответствовать размерам массива), злоупотреблять этой функцией можно способами, которые могут привести к сбою Python. Например:

>>> add.zadd([1,2,3], [1,2], [3,4], 1000)

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

Для улучшения интерфейса должны быть предоставлены директивы. Это достигается путем создания файла определения интерфейса. Обычно лучше всего начать с файла интерфейса, который может сгенерировать f2py (где он получает свое поведение по умолчанию). Чтобы заставить f2py сгенерировать файл интерфейса, используйте параметр -h:

f2py -h add.pyf -m add add.f

Эта команда оставляет файл add.pyf в текущем каталоге. Раздел этого файла, соответствующий zadd, имеет вид:

subroutine zadd(a,b,c,n) ! in :add:add.f
   double complex dimension(*) :: a
   double complex dimension(*) :: b
   double complex dimension(*) :: c
   integer :: n
end subroutine zadd

Расположение директивы intent и проверка кода позволяют значительно улучшить интерфейс, пока метод модуля Python не станет более удобным и надежным.

subroutine zadd(a,b,c,n) ! in :add:add.f
   double complex dimension(n) :: a
   double complex dimension(n) :: b
   double complex intent(out),dimension(n) :: c
   integer intent(hide),depend(a) :: n=len(a)
end subroutine zadd

Директива intent(out) используется для указания f2py, что c является переменной результата и должна быть создана интерфейсом перед передачей в базовый код. Директива intent(hide) указывает f2py не разрешать пользователю указать переменную n, а вместо этого получить ее из размера a. Директива depend( a ) необходима, чтобы указать f2py, что значение n зависит от входного значения a (чтобы он не пытался создать переменную n до создания переменной a).

После изменения add.pyf, новый файл модуля python можно сгенерировать, скомпилировав add.f и add.pyf:

f2py -c add.pyf add.f

Новый интерфейс имеет строку документации:

>>> import add
>>> print(add.zadd.__doc__)
zadd - Function signature:
  c = zadd(a,b)
Required arguments:
  a : input rank-1 array('D') with bounds (n)
  b : input rank-1 array('D') with bounds (n)
Return objects:
  c : rank-1 array('D') with bounds (n)

Теперь функцию можно вызвать гораздо более надежным способом:

>>> add.zadd([1,2,3],[4,5,6])
array([ 5.+0.j,  7.+0.j,  9.+0.j])

Обратите внимание на автоматическое преобразование в правильный формат.

Вставка директив в исходный код Fortran

При желании, красивый интерфейс также можно автоматически сгенерировать, поместив директивы переменной в виде специальных комментариев в исходный код Fortran. Таким образом, если я изменю исходный код, чтобы он содержал:

C
      SUBROUTINE ZADD(A,B,C,N)
C
CF2PY INTENT(OUT) :: C
CF2PY INTENT(HIDE) :: N
CF2PY DOUBLE COMPLEX :: A(N)
CF2PY DOUBLE COMPLEX :: B(N)
CF2PY DOUBLE COMPLEX :: C(N)
      DOUBLE COMPLEX A(*)
      DOUBLE COMPLEX B(*)
      DOUBLE COMPLEX C(*)
      INTEGER N
      DO 20 J = 1, N
         C(J) = A(J) + B(J)
 20   CONTINUE
      END

Тогда я могу скомпилировать модуль расширения с помощью:

f2py -c -m add add.f

Полученная подпись для функции add.zadd будет точно такой же, как и была создана ранее. Если исходный код содержал A(N) вместо A(*) и так далее с B и C, тогда я мог получить (почти) тот же интерфейс, просто поместив комментарий INTENT(OUT) :: C в исходный код. Единственное отличие состоит в том, что N будет необязательным входом, который будет по умолчанию равен длине A.

Пример фильтрации

Для сравнения с другими методами, обсуждаемыми ниже. Вот другой пример функции, которая фильтрует двумерный массив чисел двойной точности с плавающей запятой с помощью фиксированного фильтра усреднения. Преимущество использования Fortran для индексирования многомерных массивов должно быть ясно из этого примера.

      SUBROUTINE DFILTER2D(A,B,M,N)
C
      DOUBLE PRECISION A(M,N)
      DOUBLE PRECISION B(M,N)
      INTEGER N, M
CF2PY INTENT(OUT) :: B
CF2PY INTENT(HIDE) :: N
CF2PY INTENT(HIDE) :: M
      DO 20 I = 2,M-1
         DO 40 J=2,N-1
            B(I,J) = A(I,J) +
     $           (A(I-1,J)+A(I+1,J) +
     $            A(I,J-1)+A(I,J+1) )*0.5D0 +
     $           (A(I-1,J-1) + A(I-1,J+1) +
     $            A(I+1,J-1) + A(I+1,J+1))*0.25D0
 40      CONTINUE
 20   CONTINUE
      END

Этот код можно скомпилировать и связать в модуль расширения под названием filter с помощью:

f2py -c -m filter filter.f

Это создаст модуль расширения под названием filter.so в текущем каталоге с методом dfilter2d, который возвращает отфильтрованную версию входных данных.

Вызов f2py из Python

Программа f2py написана на Python и может быть запущена из вашего кода для компиляции кода Fortran во время выполнения, как показано ниже:

from numpy import f2py
with open("add.f") as sourcefile:
    sourcecode = sourcefile.read()
f2py.compile(sourcecode, modulename='add')
import add

Исходная строка может быть любым допустимым кодом Fortran. Если вы хотите сохранить исходный код модуля расширения, то подходящее имя файла можно указать с помощью ключевого слова source_fn функции компиляции.

Автоматическое создание модуля расширения

Если вы хотите распространить свой модуль расширения f2py, то вам нужно только включить файл .pyf и код Fortran. Расширения distutils в NumPy позволяют определить модуль расширения полностью в терминах этого файла интерфейса. Валидный файл setup.py, позволяющий распространять модуль add.f (как часть пакета f2py_examples, чтобы он загружался как f2py_examples.add) выглядит так:

def configuration(parent_package='', top_path=None)
    from numpy.distutils.misc_util import Configuration
    config = Configuration('f2py_examples',parent_package, top_path)
    config.add_extension('add', sources=['add.pyf','add.f'])
    return config

if __name__ == '__main__':
    from numpy.distutils.core import setup
    setup(**configuration(top_path='').todict())

Установка нового пакета проста и выполняется с помощью:

pip install .

при условии, что у вас есть соответствующие разрешения для записи в основной каталог site-packages для используемой версии Python. Для работы получившегося пакета необходимо создать файл с именем __init__.py (в той же директории, что и add.pyf). Обратите внимание, что модуль расширения определяется полностью в терминах файлов add.pyf и add.f. Преобразование файла .pyf в файл .c выполняется с помощью numpy.disutils.

Заключение

Файл определения интерфейса (.pyf) позволяет точно настроить интерфейс между Python и Fortran. Документация по f2py есть в каталоге numpy/f2py/docs, где установлен NumPy (обычно в каталоге site-packages). Более подробная информация об использовании f2py (включая использование для обертывания кода C) доступна на странице https://scipy-cookbook.readthedocs.io в разделе «Взаимодействие с другими языками».

Метод f2py для связи скомпилированного кода — это в настоящее время наиболее сложный и интегрированный подход. Он позволяет четко разделить Python и скомпилированный код, при этом позволяя отдельно распространять модуль расширения. Единственный недостаток заключается в том, что для установки кода пользователю требуется компилятор Fortran. Однако, с наличием бесплатных компиляторов g77, gfortran и g95, а также высококачественных коммерческих компиляторов, это ограничение не является существенным. На мой взгляд, Fortran по-прежнему является самым простым способом написания быстрого и понятного кода для научных вычислений. Он обрабатывает комплексные числа и многомерную индексацию наиболее простым способом. Однако имейте в виду, что некоторые компиляторы Fortran не смогут оптимизировать код так же хорошо, как хорошо написанный код на C.

Cython

Cython — это компилятор для диалекта Python, который добавляет (по желанию) статическую типизацию для повышения скорости и позволяет смешивать код C или C++ в модули. Он генерирует расширения на C или C++, которые могут быть скомпилированы и импортированы в код Python.

Если вы пишете модуль расширения, который будет включать значительный объём собственного алгоритмического кода, то Cython — хороший вариант. Среди его возможностей — возможность лёгкой и быстрой работы с многомерными массивами.

Обратите внимание, что Cython — это только генератор модулей расширения. В отличие от f2py, он не включает автоматические средства для компиляции и компоновки модуля расширения (что должно выполняться стандартными методами). Он предоставляет изменённый класс distutils под названием build_ext, который позволяет создать модуль расширения из исходного кода .pyx. Таким образом, вы можете написать в файле setup.py:

from Cython.Distutils import build_ext
from distutils.extension import Extension
from distutils.core import setup
import numpy

setup(name='mine', description='Nothing',
      ext_modules=[Extension('filter', ['filter.pyx'],
                             include_dirs=[numpy.get_include()])],
      cmdclass = {'build_ext':build_ext})

Добавление каталога включения NumPy, конечно, необходимо только если вы используете массивы NumPy в модуле расширения (для чего, по сути, и используется Cython). Расширения distutils в NumPy также включают поддержку автоматического создания модуля расширения и его компоновки из файла .pyx. Она работает так, что если у пользователя нет Cython, то ищется файл с таким же именем, но с расширением .c, который затем используется вместо попытки повторного создания файла .c.

Если вы просто используете Cython для компиляции стандартного модуля Python, то получите модуль расширения на C, который, как правило, работает немного быстрее, чем эквивалентный модуль Python. Дальнейшее повышение скорости можно получить, используя ключевое слово cdef для статической объявления переменных C.

Давайте рассмотрим два ранее рассмотренных примера, чтобы увидеть, как они могут быть реализованы с помощью Cython. Эти примеры были скомпилированы в модули расширения с помощью Cython 0.21.1.

Комплексное сложение в Cython

Вот часть модуля Cython под названием add.pyx, который реализует функции сложения комплексных чисел, которые мы ранее реализовали с помощью f2py:

cimport cython
cimport numpy as np
import numpy as np

# We need to initialize NumPy.
np.import_array()

#@cython.boundscheck(False)
def zadd(in1, in2):
    cdef double complex[:] a = in1.ravel()
    cdef double complex[:] b = in2.ravel()

    out = np.empty(a.shape[0], np.complex64)
    cdef double complex[:] c = out.ravel()

    for i in range(c.shape[0]):
        c[i].real = a[i].real + b[i].real
        c[i].imag = a[i].imag + b[i].imag

    return out

Этот модуль демонстрирует использование оператора cimport для загрузки определений из заголовочного файла numpy.pxd, который поставляется с Cython. Похоже, что NumPy импортируется дважды; cimport предоставляет доступ только к C-API NumPy, в то время как обычный import вызывает импорт в стиле Python во время выполнения и позволяет вызывать знакомый Python-API NumPy.

Пример также демонстрирует «типизированные представления памяти» Cython, которые аналогичны массивам NumPy на уровне C, в том смысле, что они представляют собой массивы с формой и шагом, которые знают свой размер (в отличие от массива C, адресуемого через указатель). Синтаксис double complex[:] обозначает одномерный массив (вектор) с плавающей точкой двойной точности с произвольным шагом. Согласованный массив целых чисел будет int[::1], а матрица чисел с плавающей точкой — float[:, :].

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

Помимо синтаксиса представления, функция сразу понятна программисту Python. Статическая типизация переменной i подразумевается. Вместо синтаксиса представления мы также могли использовать специальный синтаксис массивов NumPy Cython, но синтаксис представления предпочтительнее.

Фильтр изображений в Cython

Двумерный пример, который мы создали с использованием Fortran, так же легко написать на Cython:

cimport numpy as np
import numpy as np

np.import_array()

def filter(img):
    cdef double[:, :] a = np.asarray(img, dtype=np.double)
    out = np.zeros(img.shape, dtype=np.double)
    cdef double[:, ::1] b = out

    cdef np.npy_intp i, j

    for i in range(1, a.shape[0] - 1):
        for j in range(1, a.shape[1] - 1):
            b[i, j] = (a[i, j]
                       + .5 * (  a[i-1, j] + a[i+1, j]
                               + a[i, j-1] + a[i, j+1])
                       + .25 * (  a[i-1, j-1] + a[i-1, j+1]
                                + a[i+1, j-1] + a[i+1, j+1]))

    return out

Этот двумерный фильтр усреднения работает быстро, потому что цикл находится на C, а вычисления указателей выполняются только по мере необходимости. Если код выше скомпилирован как модуль image, то двумерное изображение img может быть отфильтровано с помощью этого кода очень быстро, используя:

import image
out = image.filter(img)

Что касается кода, два момента заслуживают внимания: во-первых, невозможно вернуть представление памяти в Python. Вместо этого сначала создаётся массив NumPy out, а затем используется представление b этого массива для вычислений. Во-вторых, представление b имеет тип double[:, ::1]. Это означает двумерный массив с сопряженными строками, т.е., порядок матрицы C. Явное указание порядка может ускорить некоторые алгоритмы, так как они могут пропустить вычисления шагов.

Заключение

Cython — механизм расширения, используемый в нескольких научных библиотеках Python, включая Scipy, Pandas, SAGE, scikit-image и scikit-learn, а также библиотеке обработки XML LXML. Язык и компилятор хорошо поддерживаются.

Существует несколько недостатков использования Cython:

  1. При написании собственных алгоритмов, а иногда и при оборачивании существующих библиотек C, требуется некоторое знакомство с C. В частности, при использовании управления памятью C (malloc и аналогичные) легко допустить утечки памяти. Однако, простое компилирование модуля Python, переименованного в .pyx, уже может ускорить его работу, а добавление нескольких объявлений типов может значительно ускорить некоторые участки кода.
  2. Легко потерять ясное разделение между Python и C, что затрудняет повторное использование C-кода для других проектов, не связанных с Python.
  3. Код C, сгенерированный Cython, трудно читать и изменять (и обычно компилируется с раздражающими, но не критическими предупреждениями).

Одно из больших преимуществ модулей расширения, сгенерированных Cython, заключается в том, что они легко распространяются. В заключение, Cython — очень мощный инструмент для быстрого соединения кода C или генерации модуля расширения, и его не следует игнорировать. Он особенно полезен для тех, кто не может или не хочет писать код C или Fortran.

ctypes

Ctypes — это модуль расширения Python, включённый в стандартную библиотеку, который позволяет напрямую вызывать произвольную функцию в динамической библиотеке из Python. Этот подход позволяет напрямую взаимодействовать с кодом C из Python. Это открывает огромный спектр библиотек для использования из Python. Однако недостатком является то, что ошибки программирования могут очень легко привести к неприятным аварийным завершениям программы (как и в случае с C), так как проверка типов или границ параметров минимальна. Это особенно актуально, когда данные массива передаются как указатель на сырое место в памяти. Тогда ответственность лежит на вас, чтобы подпрограмма не обращалась к памяти за пределами фактического массива. Но если вы не против немного рисковать, ctypes может быть эффективным инструментом для быстрого использования большой динамической библиотеки (или для написания расширенной функциональности в вашей собственной динамической библиотеке).

Поскольку подход ctypes предоставляет прямой интерфейс к скомпилированному коду, он не всегда устойчив к ошибкам пользователя. Надежное использование модуля ctypes обычно включает дополнительный слой кода Python для проверки типов данных и границ массивов объектов, передаваемых в подпрограмму. Этот дополнительный слой проверки (не говоря уже о преобразовании объектов ctypes в типы данных C, которое выполняет сам ctypes), замедлит интерфейс по сравнению с рукописным интерфейсом модуля расширения. Однако эта накладные расходы должны быть незначительны, если вызываемая C-функция выполняет значительную работу. Если вы отличный программист Python со слабыми навыками C, ctypes — лёгкий способ создать полезный интерфейс к (динамической) библиотеке скомпилированного кода.

Для использования ctypes необходимо

  1. Иметь динамическую библиотеку.
  2. Загрузить динамическую библиотеку.
  3. Преобразовать объекты Python в аргументы, понятные для ctypes.
  4. Вызвать функцию из библиотеки с аргументами ctypes.

Использование динамической библиотеки

Существуют определённые, зависящие от платформы, требования к динамической библиотеке, которая может быть использована с ctypes. Этот гайд предполагает, что у вас есть некоторые знания о создании динамической библиотеки на вашей системе (или у вас просто есть доступная динамическая библиотека). Следует помнить:

  • Динамическая библиотека должна быть скомпилирована специальным образом (например, с использованием флага -shared в gcc).
  • На некоторых платформах (например, Windows) динамическая библиотека требует файл .def, в котором указываются экспортируемые функции. Например, файл mylib.def может содержать:

    LIBRARY mylib.dll
    EXPORTS
    cool_function1
    cool_function2
    

    В качестве альтернативы, вы можете использовать спецификатор класса хранения __declspec(dllexport) в определении функции C, чтобы избежать необходимости в этом файле .def.

В Python distutils нет стандартного способа создания стандартной разделяемой библиотеки (модуль расширения — это «специальная» разделяемая библиотека, которую Python понимает) в кроссплатформенном режиме. Таким образом, существенным недостатком ctypes на момент написания этой книги является то, что трудно распространять кроссплатформенно модуль расширения Python, использующий ctypes и содержащий собственный код, который должен быть скомпилирован как разделяемая библиотека на системе пользователя.

Загрузка разделяемой библиотеки

Простой, но надежный способ загрузки разделяемой библиотеки — получить абсолютный путь к ней и загрузить ее с помощью объекта cdll из ctypes:

lib = ctypes.cdll[<full_path_name>]

Однако в Windows доступ к атрибуту метода cdll загрузит первый DLL с этим именем, найденный в текущем каталоге или на PATH. Загрузка абсолютного пути требует некоторой тонкости для кроссплатформенной работы, так как расширение разделяемых библиотек различно. Доступен утилита ctypes.util.find_library, которая может упростить процесс поиска библиотеки для загрузки, но она не является безупречной. Более того, разные платформы имеют разные значения по умолчанию для расширений, используемых разделяемыми библиотеками (например, .dll — Windows, .so — Linux, .dylib — Mac OS X). Это также необходимо учитывать, если вы используете ctypes для упаковки кода, который должен работать на нескольких платформах.

NumPy предоставляет удобную функцию под названием ctypeslib.load_library (имя, путь). Эта функция принимает имя разделяемой библиотеки (включая любой префикс, например, «lib», но не включая расширение) и путь, где может находиться разделяемая библиотека. Она возвращает объект библиотеки ctypes или вызывает исключение OSError, если библиотека не найдена, или ImportError, если модуль ctypes недоступен. (Пользователи Windows: объект библиотеки ctypes, загруженный с помощью load_library, всегда загружается с вызовом по соглашению cdecl. См. документацию ctypes по ctypes.windll и/или ctypes.oledll для способов загрузки библиотек с другими соглашениями вызова).

Функции в разделяемой библиотеке доступны как атрибуты объекта библиотеки ctypes (возвращаемого из ctypeslib.load_library или как элементы с использованием синтаксиса lib['func_name']. Последний метод получения имени функции особенно полезен, если имя функции содержит символы, недопустимые в именах переменных Python.

Преобразование аргументов

Целые числа/длинные целые, строки и объекты unicode Python автоматически преобразуются в эквивалентные аргументы ctypes по мере необходимости. Объект None также автоматически преобразуется в указатель NULL. Все остальные объекты Python должны быть преобразованы в типы, специфичные для ctypes. Есть два способа обойти это ограничение, позволяющие ctypes интегрироваться с другими объектами.

  1. Не устанавливайте атрибут argtypes объекта функции и определите метод _as_parameter_ для объекта, который вы хотите передать. Метод _as_parameter_ должен возвращать целое число Python, которое будет передано непосредственно в функцию.
  2. Установите атрибут argtypes в список, элементы которого содержат объекты с методом класса from_param, который знает, как преобразовать ваш объект в объект, который понимает ctypes (целое число/длинное целое, строка, unicode или объект с атрибутом _as_parameter_).

NumPy использует оба метода, отдавая предпочтение второму методу, так как он может быть безопаснее. Атрибут ctypes массива ndarray возвращает объект, имеющий атрибут _as_parameter_, который возвращает целое число, представляющее адрес ndarray, к которому он связан. В результате вы можете передавать этот объект атрибута ctypes непосредственно в функцию, ожидающую указатель на данные в вашем массиве ndarray. Вызывающая сторона должна убедиться, что объект ndarray имеет правильный тип, форму и установлены правильные флаги, чтобы избежать неприятных сбоев, если указатель на данные в неподходящих массивах передается.

Для реализации второго метода NumPy предоставляет функцию-фабрику классов ndpointer в модуле numpy.ctypeslib. Эта функция-фабрика создает соответствующий класс, который можно поместить в запись атрибута argtypes функции ctypes. Класс будет содержать метод from_param, который ctypes будет использовать для преобразования любого передаваемого в функцию массива ndarray в объект, распознаваемый ctypes. При этом преобразование выполнит проверку любых свойств массива ndarray, указанных пользователем при вызове ndpointer. Аспекты ndarray, которые могут быть проверены, включают тип данных, количество измерений, форму и/или состояние флагов любого переданного массива. Значение возвращаемого методом from_param объекта — это атрибут ctypes массива, который (поскольку он содержит атрибут _as_parameter_, указывающий на область данных массива) может быть непосредственно использован ctypes.

Атрибут ctypes массива ndarray также наделен дополнительными атрибутами, которые могут оказаться удобными при передаче дополнительной информации об массиве в функцию ctypes. Атрибуты data, shape и strides могут предоставить типы, совместимые с ctypes, соответствующие области данных, форме и шагам массива. Атрибут data возвращает c_void_p, представляющий указатель на область данных. Атрибуты shape и strides каждый возвращают массив целых чисел ctypes (или None, представляющий указатель NULL, если массив 0-мерный). Базовый тип ctype массива — целое число ctype того же размера, что и указатель на платформе. Также существуют методы data_as({ctype}), shape_as(<base ctype>), и strides_as(<base ctype>). Они возвращают данные как объект ctype по вашему выбору и массивы shape/strides, используя базовый тип по вашему выбору. Для удобства модуль ctypeslib также содержит c_intp в качестве целочисленного типа данных ctypes, размер которого равен размеру c_void_p на платформе (его значение равно None, если ctypes не установлен).

Вызов функции

К функции обращаются как к атрибуту или элементу загруженной разделяемой библиотеки. Таким образом, если у ./mylib.so есть функция под названием cool_function1, я мог бы получить доступ к этой функции так:

lib = numpy.ctypeslib.load_library('mylib','.')
func1 = lib.cool_function1  # or equivalently
func1 = lib['cool_function1']

В ctypes значение возвращаемого функцией по умолчанию равно «int». Это поведение можно изменить, установив атрибут restype функции. Используйте None для restype, если функция не имеет возвращаемого значения («void»):

func1.restype = None

Как уже обсуждалось ранее, вы также можете установить атрибут argtypes функции, чтобы ctypes проверял типы входных аргументов при вызове функции. Используйте функцию-фабрику ndpointer для генерации готового класса для проверки типа данных, формы и флагов вашей новой функции. Функция ndpointer имеет сигнатуру

ndpointer(dtype=None, ndim=None, shape=None, flags=None)

Ключевые аргументы со значением None не проверяются. Указание ключевого аргумента обеспечивает проверку этого аспекта ndarray при преобразовании в совместимый с ctypes объект. Ключевой аргумент dtype может быть любым объектом, понимаемым как объект типа данных. Ключевой аргумент ndim должен быть целым числом, а ключевой аргумент shape — целым числом или последовательностью целых чисел. Ключевой аргумент flags указывает минимальные флаги, которые требуются для любого переданного массива. Он может быть задан как строка с перечисленными требованиями, через запятую, целое число, представляющее требование бит, объединенные операцией OR, или объект flags, возвращенный из атрибута flags массива с необходимыми требованиями.

Использование класса ndpointer в методе argtypes может значительно повысить безопасность вызова функции C с использованием ctypes и области данных массива ndarray. Вы все равно можете обернуть функцию дополнительным Python-оберткой, чтобы сделать ее удобной для пользователя (скрыть некоторые очевидные аргументы и сделать некоторые аргументы выходными аргументами). В этом процессе функция requires в NumPy может оказаться полезной для возврата нужного типа массива из заданного ввода.

Полный пример

В этом примере я покажу, как функцию сложения и функцию фильтра, реализованные ранее другими способами, можно реализовать с помощью ctypes. Сначала C-код, реализующий алгоритмы, содержит функции zadd, dadd, sadd, cadd, и dfilter2d. Функция zadd:

/* Add arrays of contiguous data */
typedef struct {double real; double imag;} cdouble;
typedef struct {float real; float imag;} cfloat;
void zadd(cdouble *a, cdouble *b, cdouble *c, long n)
{
    while (n--) {
        c->real = a->real + b->real;
        c->imag = a->imag + b->imag;
        a++; b++; c++;
    }
}

с похожим кодом для cadd, dadd, и sadd, которые обрабатывают комплексные типы данных float, double и float соответственно:

void cadd(cfloat *a, cfloat *b, cfloat *c, long n)
{
        while (n--) {
                c->real = a->real + b->real;
                c->imag = a->imag + b->imag;
                a++; b++; c++;
        }
}
void dadd(double *a, double *b, double *c, long n)
{
        while (n--) {
                *c++ = *a++ + *b++;
        }
}
void sadd(float *a, float *b, float *c, long n)
{
        while (n--) {
                *c++ = *a++ + *b++;
        }
}

Файл code.c также содержит функцию dfilter2d:

/*
 * Assumes b is contiguous and has strides that are multiples of
 * sizeof(double)
 */
void
dfilter2d(double *a, double *b, ssize_t *astrides, ssize_t *dims)
{
    ssize_t i, j, M, N, S0, S1;
    ssize_t r, c, rm1, rp1, cp1, cm1;

    M = dims[0]; N = dims[1];
    S0 = astrides[0]/sizeof(double);
    S1 = astrides[1]/sizeof(double);
    for (i = 1; i < M - 1; i++) {
        r = i*S0;
        rp1 = r + S0;
        rm1 = r - S0;
        for (j = 1; j < N - 1; j++) {
            c = j*S1;
            cp1 = j + S1;
            cm1 = j - S1;
            b[i*N + j] = a[r + c] +
                (a[rp1 + c] + a[rm1 + c] +
                 a[r + cp1] + a[r + cm1])*0.5 +
                (a[rp1 + cp1] + a[rp1 + cm1] +
                 a[rm1 + cp1] + a[rm1 + cp1])*0.25;
        }
    }
}

Одно из возможных преимуществ этого кода перед эквивалентным кодом Fortran заключается в том, что он принимает произвольно шаг (т. е. несмежные массивы) и может также работать быстрее в зависимости от возможностей оптимизации вашего компилятора. Но это очевидным образом сложнее, чем простой код в filter.f. Этот код должен быть скомпилирован в разделяемую библиотеку. На моей системе Linux это выполняется с помощью:

gcc -o code.so -shared code.c

Что создает разделяемую библиотеку с именем code.so в текущем каталоге. В Windows не забудьте добавить __declspec(dllexport) перед void в строке перед каждым определением функции или написать файл code.def, который перечисляет имена экспортируемых функций.

Должен быть создан подходящий Python-интерфейс к этой разделяемой библиотеке. Для этого создайте файл с именем interface.py с приведенными ниже строками вверху:

__all__ = ['add', 'filter2d']

import numpy as np
import os

_path = os.path.dirname('__file__')
lib = np.ctypeslib.load_library('code', _path)
_typedict = {'zadd' : complex, 'sadd' : np.single,
             'cadd' : np.csingle, 'dadd' : float}
for name in _typedict.keys():
    val = getattr(lib, name)
    val.restype = None
    _type = _typedict[name]
    val.argtypes = [np.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous'),
                    np.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous'),
                    np.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous,'\
                            'writeable'),
                    np.ctypeslib.c_intp]

Этот код загружает разделяемую библиотеку под названием code.{ext}, расположенную в том же пути, что и этот файл. Затем он добавляет возвращаемый тип void к функциям, содержащимся в библиотеке. Он также добавляет проверку аргументов в функции библиотеки, чтобы массивы ndarray могли быть переданы в качестве первых трех аргументов вместе с целым числом (достаточно большим, чтобы содержать указатель на платформе) в качестве четвертого аргумента.

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

lib.dfilter2d.restype=None
lib.dfilter2d.argtypes = [np.ctypeslib.ndpointer(float, ndim=2,
                                       flags='aligned'),
                          np.ctypeslib.ndpointer(float, ndim=2,
                                 flags='aligned, contiguous,'\
                                       'writeable'),
                          ctypes.POINTER(np.ctypeslib.c_intp),
                          ctypes.POINTER(np.ctypeslib.c_intp)]

Далее определите простую функцию выбора, которая выбирает, какую функцию сложения вызывать в разделяемой библиотеке, в зависимости от типа данных:

def select(dtype):
    if dtype.char in ['?bBhHf']:
        return lib.sadd, single
    elif dtype.char in ['F']:
        return lib.cadd, csingle
    elif dtype.char in ['DG']:
        return lib.zadd, complex
    else:
        return lib.dadd, float
    return func, ntype

Наконец, две функции, которые должны быть экспортированы библиотекой интерфейса, можно записать просто как:

def add(a, b):
    requires = ['CONTIGUOUS', 'ALIGNED']
    a = np.asanyarray(a)
    func, dtype = select(a.dtype)
    a = np.require(a, dtype, requires)
    b = np.require(b, dtype, requires)
    c = np.empty_like(a)
    func(a,b,c,a.size)
    return c

и:

def filter2d(a):
    a = np.require(a, float, ['ALIGNED'])
    b = np.zeros_like(a)
    lib.dfilter2d(a, b, a.ctypes.strides, a.ctypes.shape)
    return b

Заключение

Использование ctypes — мощный способ подключения Python к произвольному коду C. Преимущества для расширения Python включают

END_OF_DOCUMENT_MARKER
  • чистое разделение кода C от кода Python

    • нет необходимости изучать новый синтаксис, кроме Python и C
    • позволяет повторно использовать код C
    • функциональность в общих библиотеках, написанных для других целей, может быть получена с помощью простого оболочки Python и поиска библиотеки.
  • лёгкая интеграция с NumPy через атрибут ctypes
  • полная проверка аргументов с помощью фабрики класса ndpointer

Его недостатки включают

  • Трудно распространять модуль расширения, созданный с помощью ctypes, из-за отсутствия поддержки построения общих библиотек в distutils (но я подозреваю, что это со временем изменится).
  • Вы должны иметь общие библиотеки своего кода (нет статических библиотек).
  • Очень малая поддержка кода C++ и его различных соглашений о вызове библиотек. Вам, вероятно, понадобится оболочка C вокруг кода C++, чтобы использовать его с ctypes (или просто используйте Boost.Python).

Из-за трудностей с распространением модуля расширения, созданного с помощью ctypes, f2py и Cython по-прежнему являются наиболее простыми способами расширения Python для создания пакетов. Однако ctypes в некоторых случаях является полезной альтернативой. Это должно добавить больше функций в ctypes, которые должны устранить трудности при расширении Python и распространении расширения с помощью ctypes.

Дополнительные инструменты, которые могут быть полезны

Эти инструменты оказались полезными для других пользователей Python, поэтому они включены здесь. Они обсуждаются отдельно, потому что это либо устаревшие способы выполнения задач, сейчас обрабатываемые f2py, Cython или ctypes (SWIG, PyFort), либо потому, что я не знаю о них много (SIP, Boost). Я не добавлял ссылки на эти методы, потому что я знаю, что вы можете быстрее найти соответствующую ссылку с помощью Google или другого поисковика, и любая предоставленная здесь ссылка быстро устареет. Не предполагайте, что просто потому, что он включён в этот список, я не думаю, что пакет заслуживает вашего внимания. Я включаю информацию об этих пакетах, потому что многие люди сочли их полезными, и я хотел бы предоставить вам как можно больше вариантов для решения проблемы лёгкой интеграции вашего кода.

SWIG

Simplified Wrapper and Interface Generator (SWIG) — это старый и довольно стабильный метод для обертывания библиотек C/C++ в большое количество других языков. Он не понимает массивы NumPy напрямую, но может быть сделан пригодным для использования с NumPy с помощью typemaps. В каталоге numpy/tools/swig в numpy есть несколько примеров typemaps вместе с примерным модулем, который их использует. SWIG отлично справляется с обёртыванием больших библиотек C/C++, потому что он может (почти) разобрать их заголовки и автоматически сгенерировать интерфейс. Технически, вам нужно сгенерировать файл .i, который определяет интерфейс. Однако часто этот файл .i может быть частью самого заголовка. Интерфейс обычно требует некоторой настройки, чтобы быть действительно полезным. Эта способность разбирать заголовки C/C++ и автоматически генерировать интерфейс по-прежнему делает SWIG полезным подходом к добавлению функциональности из C/C++ в Python, несмотря на другие появившиеся методы, которые более нацелены на Python. SWIG может на самом деле нацеливать расширения на несколько языков, но typemaps обычно должны быть специфичны для языка. Тем не менее, с изменениями в typemaps, специфичных для Python, SWIG может быть использован для взаимодействия библиотеки с другими языками, такими как Perl, Tcl и Ruby.

Мой опыт работы с SWIG в целом положительный, потому что он относительно прост в использовании и достаточно мощный. Я часто использовал его раньше, прежде чем стал более опытным в написании расширений C. Однако мне было сложно писать пользовательские интерфейсы с SWIG, потому что это нужно делать с использованием концепции typemaps, которые не являются специфичными для Python и написаны на синтаксисе, похожем на C. Поэтому я склонён отдавать предпочтение другим стратегиям склеивания и буду пытаться использовать SWIG только для обёртывания очень больших библиотек C/C++ . Тем не менее, есть и другие, кто успешно использует SWIG.

SIP

SIP — ещё один инструмент для обертывания библиотек C/C++, специфичный для Python, который, по-видимому, имеет очень хорошую поддержку C++. Компания Riverbank Computing разработала SIP для создания Python-связей с библиотекой QT. Для генерации связей должен быть написан файл интерфейса, но файл интерфейса очень похож на файл заголовка C/C++. Хотя SIP не является полным анализатором C++, он понимает значительную часть синтаксиса C++ а также свои собственные специальные директивы, которые позволяют изменять способ выполнения Python-связи. Он также позволяет пользователю определять сопоставления между типами Python и структурами и классами C/C++.

Boost Python

Boost — это хранилище библиотек C++, и Boost.Python — одна из таких библиотек, которая предоставляет компактный интерфейс для связывания классов и функций C++ с Python. Удивительная часть подхода Boost.Python в том, что он работает полностью в чистом C++ без введения нового синтаксиса. Многие пользователи C++ сообщают, что Boost.Python позволяет сочетать лучшее из обоих миров плавным образом. Я не использовал Boost.Python, потому что я не большой пользователь C++, а использование Boost для обёртывания простых подпрограмм C обычно избыточно. Его основное назначение — предоставление доступности классов C++ в Python. Итак, если у вас есть набор классов C++, которые нужно интегрировать в Python чистым способом, подумайте об изучении и использовании Boost.Python.

PyFort

PyFort — хороший инструмент для обёртывания кода Fortran и похожих на Fortran C-кода в Python с поддержкой массивов Numeric. Он был написан Полом Дюбуа, известным специалистом в области компьютерных наук и первым разработчиком Numeric (сейчас на пенсии). Стоит упомянуть об этом в надежде, что кто-то обновит PyFort, чтобы он работал с массивами NumPy, которые теперь поддерживают массивы с фортрановской или C-стильной непрерывностью.

© 2005–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.19/user/c-info.python-as-glue.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API