Оборачивание библиотеки C в Python: C, Cython или ctypes?
Я хочу вызвать библиотеку C из приложения Python. Я не хочу оборачивать весь API, только функции и типы данных, которые имеют отношение к моему случаю. На мой взгляд, у меня есть три варианта:
- Создайте фактический модуль расширения на C. Вероятно, это излишне, и я также хотел бы избежать накладных расходов при обучении написанию расширений.
- Используйте Cython для предоставления соответствующих частей из библиотеки C в Python.
- Сделайте все это в Python, используя
ctypes
общаться с внешней библиотекой.
Я не уверен, что 2) или 3) - лучший выбор. Преимущество 3) заключается в том, что ctypes
является частью стандартной библиотеки, и полученный код будет представлять собой чистый Python - хотя я не уверен, насколько велико это преимущество на самом деле.
Есть ли больше преимуществ / недостатков с любым выбором? Какой подход вы рекомендуете?
Изменить: Спасибо за все ваши ответы, они предоставляют хороший ресурс для тех, кто хочет сделать что-то подобное. Решение, конечно, все еще должно быть принято для единственного случая - нет единственного ответа "Это правильно". Для моего собственного случая я, вероятно, пойду с ctypes, но я также с нетерпением жду возможности попробовать Cython в каком-то другом проекте.
Поскольку единого истинного ответа не существует, принятие его является несколько произвольным; Я выбрал ответ FogleBird, так как он дает хорошее представление о ctypes, и в настоящее время он также является самым популярным. Тем не менее, я предлагаю прочитать все ответы, чтобы получить хороший обзор.
Еще раз спасибо.
11 ответов
ctypes
это ваш лучший выбор для быстрого выполнения работы, и с вами приятно работать, так как вы все еще пишете на Python!
Недавно я обернул драйвер FTDI для связи с USB-чипом с помощью ctypes, и это было здорово. Я сделал все это и работал менее чем за один рабочий день. (Я реализовал только те функции, которые нам нужны, около 15 функций).
Ранее мы использовали сторонний модуль PyUSB для той же цели. PyUSB - это модуль расширения C/Python. Но PyUSB не выпускал GIL при блокировке чтения / записи, что вызывало у нас проблемы. Поэтому я написал наш собственный модуль с использованием ctypes, который высвобождает GIL при вызове нативных функций.
Стоит отметить, что ctypes не будет знать о #define
константы и прочее в используемой вами библиотеке, только функции, так что вам придется переопределять эти константы в вашем собственном коде.
Вот пример того, как код в итоге выглядел (множество вырвано, просто пытаюсь показать вам суть):
from ctypes import *
d2xx = WinDLL('ftd2xx')
OK = 0
INVALID_HANDLE = 1
DEVICE_NOT_FOUND = 2
DEVICE_NOT_OPENED = 3
...
def openEx(serial):
serial = create_string_buffer(serial)
handle = c_int()
if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK:
return Handle(handle.value)
raise D2XXException
class Handle(object):
def __init__(self, handle):
self.handle = handle
...
def read(self, bytes):
buffer = create_string_buffer(bytes)
count = c_int()
if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK:
return buffer.raw[:count.value]
raise D2XXException
def write(self, data):
buffer = create_string_buffer(data)
count = c_int()
bytes = len(data)
if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK:
return count.value
raise D2XXException
Кто-то сделал несколько тестов на различные варианты.
Я мог бы быть более нерешительным, если бы мне пришлось обернуть библиотеку C++ большим количеством классов / шаблонов / и т. Д. Но ctypes хорошо работает со структурами и может даже вызвать обратный вызов в Python.
Warning: a Cython core developer's opinion ahead.
I almost always recommend Cython over ctypes. The reason is that it has a much smoother upgrade path. If you use ctypes, many things will be simple at first, and it's certainly cool to write your FFI code in plain Python, without compilation, build dependencies and all that. However, at some point, you will almost certainly find that you have to call into your C library a lot, either in a loop or in a longer series of interdependent calls, and you would like to speed that up. That's the point where you'll notice that you can't do that with ctypes. Or, when you need callback functions and you find that your Python callback code becomes a bottleneck, you'd like to speed it up and/or move it down into C as well. Again, you cannot do that with ctypes. So you have to switch languages at that point and start rewriting parts of your code, potentially reverse engineering your Python/ctypes code into plain C, thus spoiling the whole benefit of writing your code in plain Python in the first place.
С помощью Cython, OTOH, вы можете совершенно свободно делать код для переноса и вызова таким тонким или толстым, как вам нужно. Вы можете начать с простых вызовов в ваш C-код из обычного Python-кода, и Cython переведет их в нативные C-вызовы без каких-либо дополнительных затрат на вызовы и с чрезвычайно низкими издержками преобразования для параметров Python. Когда вы заметите, что вам нужно еще больше производительности в какой-то момент, когда вы делаете слишком много дорогих вызовов в вашу библиотеку C, вы можете начать аннотировать окружающий код Python статическими типами и позволить Cython оптимизировать его непосредственно для C для вас. Или вы можете начать переписывать части своего C-кода на Cython, чтобы избежать вызовов, а также специализировать и алгоритмически затягивать ваши циклы. А если вам нужен быстрый обратный вызов, просто напишите функцию с соответствующей подписью и передайте ее непосредственно в реестр обратных вызовов C. Опять же, никаких накладных расходов, и это дает вам простую производительность C-вызовов. И в гораздо менее вероятном случае, когда вы действительно не можете получить свой код достаточно быстро в Cython, вы все равно можете рассмотреть возможность переписать действительно важные его части в C (или C++ или Fortran) и вызывать его из своего кода Cython естественным и естественным образом. Но тогда это действительно становится последним средством вместо единственного варианта.
Итак, ctypes хорош для простых вещей и быстрого запуска. Однако, как только дела начнут расти, вы, скорее всего, придете к тому, что заметите, что лучше использовать Cython с самого начала.
Cython - довольно интересный инструмент, который стоит изучить, и он удивительно близок к синтаксису Python. Если вы выполняете какие-либо научные вычисления с Numpy, то Cython - это то, что нужно, потому что он интегрируется с Numpy для быстрых операций с матрицами.
Cython - это расширенная версия языка Python. Вы можете выбросить в него любой допустимый файл Python, и он выплюнет действительную программу на Си. В этом случае Cython просто сопоставит вызовы Python с базовым API CPython. Это может привести к ускорению на 50%, поскольку ваш код больше не интерпретируется.
Чтобы получить некоторые оптимизации, вы должны начать рассказывать Cython дополнительные факты о вашем коде, такие как объявления типов. Если вы скажете это достаточно, он может свести код до чистого C. То есть цикл for в Python становится циклом for в C. Здесь вы увидите значительное увеличение скорости. Вы также можете сослаться на внешние программы на C здесь.
Использование кода Cython также невероятно просто. Я думал, что руководство делает это звучит сложно. Вы буквально просто делаете:
$ cython mymodule.pyx
$ gcc [some arguments here] mymodule.c -o mymodule.so
и тогда ты сможешь import mymodule
в вашем коде Python и полностью забыть, что он компилируется до C.
В любом случае, поскольку Cython очень прост в настройке и использовании, я советую попробовать, чтобы он соответствовал вашим потребностям. Это не будет пустой тратой, если окажется, что это не тот инструмент, который вы ищете.
Для вызова библиотеки C из приложения Python есть также cffi, который является новой альтернативой для ctypes. Это приносит свежий взгляд на FFI:
- это решает проблему захватывающим, чистым способом (в отличие от ctypes)
- не требует написания кода не на Python (как в SWIG, Cython,...)
Я добавлю еще один: SWIG
Он прост в изучении, делает много правильных вещей и поддерживает множество других языков, поэтому время, потраченное на изучение, может быть довольно полезным.
Если вы используете SWIG, вы создаете новый модуль расширения Python, но SWIG делает большую часть тяжелой работы за вас.
Лично я написал бы модуль расширения на C. Не пугайтесь расширений Python C - их совсем нетрудно написать. Документация очень понятная и полезная. Когда я впервые написал расширение C на Python, я подумал, что мне понадобилось около часа, чтобы понять, как его написать - совсем немного времени.
Если у вас уже есть библиотека с определенным API, я думаю, ctypes
это лучший вариант, так как вам нужно только немного инициализировать, а затем более или менее вызвать библиотеку так, как вы привыкли.
Я думаю, что Cython или создание модуля расширения в C (что не очень сложно) более полезны, когда вам нужен новый код, например, вызов этой библиотеки и выполнение некоторых сложных, трудоемких задач, а затем передача результата в Python.
Другой подход для простых программ - это напрямую выполнить другой процесс (скомпилированный извне), выводить результат в стандартный вывод и вызывать его с помощью модуля подпроцесса. Иногда это самый простой подход.
Например, если вы делаете консольную программу C, которая работает более или менее таким образом
$miCcode 10
Result: 12345678
Вы можете позвонить из Python
>>> import subprocess
>>> p = subprocess.Popen(['miCcode', '10'], shell=True, stdout=subprocess.PIPE)
>>> std_out, std_err = p.communicate()
>>> print std_out
Result: 12345678
С небольшим форматированием строки, вы можете получить результат любым удобным для вас способом. Вы также можете зафиксировать стандартный вывод ошибок, так что он достаточно гибкий.
ctypes отлично подходит, когда у вас уже есть скомпилированный большой двоичный объект библиотеки (например, библиотеки ОС). Затраты на вызовы являются серьезными, однако, если вы будете делать много вызовов в библиотеку, и вы все равно будете писать код C (или, по крайней мере, его компилировать), я бы сказал, чтобы цитон. Это не намного больше работы, и будет гораздо быстрее и более питонно использовать полученный pyd-файл.
Лично я склонен использовать cython для быстрого ускорения кода на python (циклы и целочисленные сравнения - это две области, в которых особенно ярко проявляется cython), и когда будет задействован какой-то другой код / перенос других библиотек, я перейду к Boost.Python. Boost.Python может быть сложен в настройке, но как только он заработает, он упрощает перенос кода C/C++.
cython также хорош в упаковке numpy (что я узнал из материалов SciPy 2009), но я не использовал numpy, поэтому я не могу это комментировать.
Я знаю, что это старый вопрос, но эта вещь возникает в Google, когда вы ищете такие вещи, как ctypes vs cython
, и большинство ответов здесь написано теми, кто уже владеет cython
или c
что может не отражать фактическое время, которое вам нужно было потратить на изучение тех, кто внедрил ваше решение. Я полный новичок в обоих. Я никогда не прикасалсяcython
раньше, и у меня очень мало опыта c/c++
.
Последние два дня я искал способ делегировать часть моего кода, требующую высокой производительности, на что-то более низкое, чем python. Я реализовал свой код как вctypes
а также Cython
, который состоял в основном из двух простых функций.
У меня был огромный список строк, которые нужно было обработать. Уведомлениеlist
а также string
. Оба типа не полностью соответствуют типам вc
, потому что строки Python по умолчанию являются Unicode и c
струны нет. Списки в python - это просто НЕ массивы c.
Вот мой вердикт. Использоватьcython
. Он более плавно интегрируется с python, и в целом с ним проще работать. Когда что-то идет не такctypes
просто бросает вам segfault, по крайней мере cython
будет выдавать вам предупреждения компиляции с трассировкой стека, когда это возможно, и вы можете легко вернуть действительный объект python с помощью cython
.
Вот подробный отчет о том, сколько времени мне потребовалось вложить в оба приложения, чтобы реализовать одну и ту же функцию. Кстати, я очень мало программировал на C/C++:
Типы:
- Примерно 2 часа на исследование того, как преобразовать мой список строк Unicode в тип, совместимый с ac.
- Примерно час о том, как правильно вернуть строку из функции ac. Здесь я фактически предоставил свое собственное решение SO после того, как написал функции.
- Примерно полчаса, чтобы написать код на c, скомпилировать его в динамическую библиотеку.
- 10 минут на написание тестового кода на Python, чтобы проверить,
c
код работает. - Примерно час выполнения некоторых тестов и перестановки
c
код. - Затем я подключил
c
код в реальную базу кода, и увидел, чтоctypes
не очень хорошо сочетается сmultiprocessing
модуль как его обработчик по умолчанию не выбирается. - Около 20 минут я изменил свой код, чтобы не использовать
multiprocessing
модуль и повторил попытку. - Затем вторая функция в моем
c
код сгенерировал ошибки в моей кодовой базе, хотя он прошел мой тестовый код. Что ж, вероятно, это моя вина, что я плохо проверил крайние случаи, я искал быстрое решение. - Около 40 минут я пытался определить возможные причины этих сбоев.
- Я разделил свои функции на две библиотеки и попробовал снова. У моей второй функции все еще были ошибки.
- Я решил отказаться от второй функции и использовать только первую функцию
c
код, и на второй или третьей итерации цикла python, который его использует, у меня былUnicodeError
о том, чтобы не декодировать байт в некоторой позиции, хотя я все явно кодировал и декодировал.
На этом этапе я решил поискать альтернативу и решил изучить cython
:
- Cython
- 10 минут чтения cython hello world.
- 15 минут проверки SO о том, как использовать Cython с
setuptools
вместо тогоdistutils
. - 10 минут чтения о типах cython и python. Я узнал, что могу использовать большинство встроенных типов Python для статической типизации.
- 15 минут повторной аннотации моего кода Python с помощью типов Cython.
- 10 минут модификации моего
setup.py
использовать скомпилированный модуль в моей кодовой базе. - Подключается модуль прямо к
multiprocessing
версия кодовой базы. Оно работает.
Для протокола, я, конечно, не измерил точные сроки моих инвестиций. Вполне может быть, что мое восприятие времени было слишком внимательным из-за умственных усилий, необходимых, когда я имел дело с типами. Но он должен передавать ощущение работы сcython
а также ctypes
Есть одна проблема, которая заставила меня использовать ctypes, а не cython и которая не упоминается в других ответах.
При использовании ctypes результат не зависит от используемого компилятора. Вы можете написать библиотеку, используя более или менее любой язык, который может быть скомпилирован в собственную общую библиотеку. Неважно, какая система, какой язык и какой компилятор. Однако Cython ограничен инфраструктурой. Например, если вы хотите использовать компилятор Intel для Windows, гораздо сложнее заставить Cython работать: вы должны "объяснить" компилятор на Cython, перекомпилировать что-то с помощью этого точного компилятора и т. Д., Что существенно ограничивает переносимость.
Если вы ориентируетесь на Windows и решили обернуть некоторые проприетарные библиотеки C++, то вскоре вы обнаружите, что разные версии msvcrt***.dll
(Visual C++ Runtime) немного несовместимы.
Это означает, что вы не сможете использовать Cython
так как в результате wrapper.pyd
связано с msvcr90.dll
(Python 2.7) или msvcr100.dll
(Python 3.x). Если библиотека, которую вы упаковываете, связана с другой версией среды выполнения, то вам не повезло.
Затем, чтобы все заработало, вам нужно создать оболочки C для библиотек C++, связать эту оболочку dll с той же версией msvcrt***.dll
как ваша библиотека C++. А потом использовать ctypes
динамически загружать вашу dll-оболочку вручную.
Итак, есть много мелких деталей, которые подробно описаны в следующей статье:
"Красивые родные библиотеки (на Python) ": http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/
Есть также одна возможность использовать http://live.gnome.org/GObjectIntrospection для библиотек, которые используют GLib.