Использование Python в качестве связующего языка
Многие говорят, что Python — замечательный связующий язык. Надеемся, эта глава убедит вас в этом. Первые пользователи Python в научных целях, как правило, использовали его для объединения больших приложений, работающих на суперкомпьютерах. Использование Python вместо скриптов оболочки или Perl было гораздо приятнее, а возможность легко расширять Python делала относительно простым создание новых классов и типов, специально адаптированных к решаемым проблемам. В результате взаимодействия этих первых разработчиков появился объект Numeric, похожий на массив, который можно было использовать для передачи данных между этими приложениями.
По мере развития Numeric в NumPy, люди смогли писать больше кода непосредственно в NumPy. Часто этот код достаточно быстрый для использования в производстве, но иногда возникает необходимость доступа к скомпилированному коду. Либо для достижения максимальной эффективности алгоритма, либо для упрощения доступа к широко распространенным кодам, написанным на C/C++ или Fortran.
В этой главе будут рассмотрены многие инструменты, доступные для доступа к коду, написанному на других скомпилированных языках. Существует множество ресурсов для изучения вызова других скомпилированных библиотек из Python, и цель этой главы не сделать вас экспертом. Главная цель — ознакомить вас с некоторыми возможностями, чтобы вы знали, что «поискать в Google», чтобы узнать больше.
Вызов других скомпилированных библиотек из 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(a,b,c,n)
Wrapper for ``zadd``.
Parameters
----------
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__)
c = zadd(a,b)
Wrapper for ``zadd``.
Parameters
----------
a : input rank-1 array('D') with bounds (n)
b : input rank-1 array('D') with bounds (n)
Returns
-------
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 только делает доступным NumPy C-API, в то время как обычный import вызывает импорт в стиле Python во время выполнения и позволяет вызывать знакомый NumPy Python API.
Пример также демонстрирует «типизированные представления памяти» Cython, которые подобны массивам NumPy на уровне C, в том смысле, что они представляют собой массивы с формой и шагом, которые знают свой размер (в отличие от массива C, к которому обращаются через обычный указатель). Синтаксис double complex[:] обозначает одномерный массив (вектор) double, с произвольными шагами. Соседний массив целых чисел был бы 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:
- При написании пользовательских алгоритмов, а иногда и при обертке существующих библиотек C, необходимы некоторые знания C. В частности, при использовании управления памятью C (
mallocи аналогичные), легко допустить утечку памяти. Однако, просто компиляция модуля Python с переименованием в.pyxуже может ускорить его работу, а добавление нескольких объявлений типов может обеспечить значительное ускорение в некоторых кодах. - Легко потерять четкое разделение между Python и C, что затрудняет повторное использование кода C для других проектов, не связанных с Python.
- Код C, сгенерированный Cython, трудно читать и изменять (и, как правило, компилируется с раздражающими, но безобидными предупреждениями).
Одним из больших преимуществ модулей расширения, сгенерированных Cython, является их простота распространения. В заключение, Cython — это очень мощный инструмент для быстрого связывания кода C или генерации модуля расширения, и его не стоит игнорировать. Он особенно полезен для тех, кто не может или не хочет писать код на C или Fortran.
ctypes
Ctypes — это модуль расширения Python, включенный в stdlib, который позволяет вызывать произвольную функцию в общей библиотеке напрямую из Python. Этот подход позволяет напрямую взаимодействовать с кодом C из Python. Это открывает огромный выбор библиотек для использования из Python. Однако недостаток заключается в том, что ошибки программирования могут очень легко привести к некрасивым авариям программы (как и в C), потому что выполняется мало проверок типов или границ параметров. Это особенно верно, когда данные массива передаются как указатель на сырое местоположение памяти. Тогда ответственность ложится на вас, что подпрограмма не будет обращаться к памяти за пределами фактической области массива. Но если вы не против немного рисковать, ctypes может быть эффективным инструментом для быстрого использования большой общей библиотеки (или для написания расширенной функциональности в вашей собственной общей библиотеке).
Поскольку подход ctypes предоставляет прямой интерфейс к скомпилированному коду, он не всегда терпим к ошибкам пользователя. Надежное использование модуля ctypes, как правило, включает дополнительный уровень кода Python для проверки типов данных и границ массивов объектов, передаваемых в подпрограмму. Этот дополнительный уровень проверки (не говоря уже о преобразовании из объектов ctypes в типы данных C, которое выполняет сам ctypes), замедлит интерфейс по сравнению с написанным вручную интерфейсом модуля расширения. Однако это дополнительная нагрузка должна быть незначительной, если вызываемая процедура C выполняет значительную работу. Если вы отличный программист Python со слабыми навыками C, ctypes — это простой способ написать полезный интерфейс к (общей) библиотеке скомпилированного кода.
Для использования ctypes необходимо:
- Наличие общей библиотеки.
- Загрузка общей библиотеки.
- Преобразование объектов Python в аргументы, понятные ctypes.
- Вызов функции из библиотеки с аргументами 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.
Преобразование аргументов
Python целые числа/длинные, строки и объекты unicode автоматически преобразуются в соответствующие аргументы ctypes по мере необходимости. Объект None также автоматически преобразуется в указатель NULL. Все остальные объекты Python должны быть преобразованы в типы, специфичные для ctypes. Существует два способа обойти это ограничение, которые позволяют ctypes взаимодействовать с другими объектами.
- Не устанавливайте атрибут argtypes объекта функции и определите метод
_as_parameter_для объекта, который вы хотите передать. Метод_as_parameter_должен вернуть целое число Python, которое будет передано непосредственно функции. - Установите атрибут 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 включают:
-
чистое разделение кода 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, которые теперь поддерживают массивы, контигуированные по стилю Fortran или C.
© 2005–2022 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.21/user/c-info.python-as-glue.html