Встановлення бібліотеки С у Python: C, Cython чи ctypes?


284

Я хочу зателефонувати до бібліотеки С із програми Python. Я не хочу обробляти весь API, лише функції та типи даних, які відповідають моєму випадку. Як я бачу, у мене є три варіанти:

  1. Створіть фактичний модуль розширення в C. Можливо, зайвий рівень, і я також хотів би уникнути накладних витрат на навчання розширення.
  2. Використовуйте Cython, щоб відкрити відповідні частини з бібліотеки С Python.
  3. Робіть все в Python, використовуючи ctypesзв'язок із зовнішньою бібліотекою.

Я не впевнений, 2) чи 3) - кращий вибір. Перевага 3) полягає в тому, що ctypesце частина стандартної бібліотеки, і отриманий код був би чистим Python - хоча я не впевнений, наскільки ця перевага насправді велика.

Чи є більше переваг / недоліків у будь-якого вибору? Який підхід ви рекомендуєте?


Редагувати: Дякую за всі ваші відповіді, вони забезпечують хороший ресурс для всіх, хто хоче зробити щось подібне. Рішення, звичайно, все ж має бути прийняте для єдиного випадку - немає жодної відповіді "Це правильна річ". Для мого власного випадку я, мабуть, пітиму з ctypes, але я також з нетерпінням чекаю спробувати Cython в якомусь іншому проекті.

Оскільки немає єдиної правдивої відповіді, прийняття однієї є дещо довільною; Я вибрав відповідь FogleBird, оскільки вона дає хороше уявлення про типи, і на даний момент це також відповідь з найбільш високим голосом. Однак я пропоную прочитати всі відповіді, щоб отримати хороший огляд.

Знову дякую.


3
Певною мірою конкретний додаток (що робить бібліотека) може вплинути на вибір підходу. Ми використовували ctypes досить успішно, щоб поговорити з постачальниками DLL для різних частин хардаре (наприклад, осцилоскопи), але я не обов'язково вибирати в першу чергу типи для розмови з бібліотекою цифрової обробки через додаткові накладні витрати на Cython або SWIG.
Пітер Хансен

1
Тепер у вас є те, що ви шукали. Чотири різні відповіді. (Хтось також знайшов SWIG). Це означає, що тепер у вас є 4 варіанти замість 3.
Лука Ране

@ralu Це те, що я теж думав :-) Але серйозно, я не очікував (або хотів) таблиці про / кон або одну єдину відповідь, що говорить "Ось що вам потрібно зробити". На будь-яке питання щодо прийняття рішень найкраще відповідати "шанувальникам" кожного можливого вибору, вказуючи свої причини. Потім голосування громади виконує свою роль, як і моя власна робота (дивлячись на аргументи, застосовуючи їх до моєї справи, читати надані джерела тощо). Довга коротка історія: Тут є кілька хороших відповідей.
бальфа

Отже, з яким підходом ви збираєтесь піти? :)
FogleBird

1
Наскільки я знаю (будь ласка, виправте мене, якщо я помиляюсь), Cython - це роздріб Pyrex, який має більше розвитку, що робить Pyrex значно застарілим.
бальфа

Відповіді:


115

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.


5
Приєднуйтесь до похвал для типів, але зауважте одне (недокументоване) питання: ctypes не підтримує розгортання. Якщо ви відключитесь від процесу, що використовує ctypes, і як батьківський, так і дочірній процеси продовжують використовувати ctypes, ви натрапите на неприємну помилку, пов’язану з ctypes, використовуючи спільну пам'ять.
Орен Шемеш

1
@OrenShemesh Чи є в цьому питанні додаткове читання, на яке можна вказати мені? Я думаю, що я можу бути в безпеці з проектом, над яким зараз працюю, оскільки я вважаю, що лише батьківський процес використовує ctypes(для pyinotify), але я хотів би розібратися в цій проблемі більш ретельно.
зигг

Цей уривок мені дуже допомагає. One thing to note is that ctypes won't know about #define constants and stuff in the library you're using, only the functions, so you'll have to redefine those constants in your own code.Отже, я маю визначити константи, які знаходяться там у winioctl.h....
swdev

як щодо продуктивності? ctypesнабагато повільніше, ніж c-розширення, оскільки вузьким місцем є інтерфейс від Python до C
TomSawyer

154

Попередження: попереду думка розробника ядра Cython.

Я майже завжди рекомендую Cython над ctypes. Причина в тому, що він має набагато плавніший шлях оновлення. Якщо ви використовуєте ctypes, багато речей буде спочатку простим, і, безумовно, круто писати свій FFI-код на звичайному Python, без компіляції, побудови залежностей і все таке. Однак в якийсь момент ви майже напевно виявите, що вам доведеться багато зателефонувати до своєї бібліотеки С, або в циклі, або в більш тривалій серії взаємозалежних дзвінків, і ви хотіли б прискорити це. Це той момент, коли ви помітите, що ви не можете зробити це з ctypes. Або коли вам потрібні функції зворотного дзвінка, і ви виявите, що ваш код зворотного виклику Python стає вузьким місцем, ви також хотіли б прискорити його та / або перемістити його вниз на C. Знову ж таки, ви не можете це зробити з ctypes.

Завдяки Cython, OTOH, ви можете абсолютно безкоштовно робити коду для обгортки та виклику таким, яким ви хочете. Ви можете почати з простих дзвінків у свій код C із звичайного коду Python, і Cython переведе їх на рідний C-виклик, без додаткових накладних витрат та з надзвичайно низькою накладною витратою на перетворення параметрів Python. Коли ви помітите, що вам потрібна ще більша продуктивність у якийсь момент, коли ви здійснюєте занадто багато дорогих дзвінків у свою бібліотеку C, ви можете почати коментувати ваш оточуючий код Python статичними типами і дозволити Cython оптимізувати його прямо в C для вас. Або ви можете почати перезаписувати частини свого коду С у Cython, щоб уникнути дзвінків та алгоритмічно спеціалізуватися та затягувати петлі. І якщо вам потрібен швидкий зворотний дзвінок, просто напишіть функцію з відповідною підписом та передайте її безпосередньо в реєстр зворотних дзвінків C. Знову ж таки, немає накладних витрат, і це дає вам звичайну ефективність дзвінків на C. І в набагато менш вірогідному випадку, якщо ви дійсно не можете отримати свій код досить швидко в Cython, ви все ще можете розглянути можливість переписати справді критичні частини його на C (або C ++ або Fortran) і викликати його зі свого коду Cython природним шляхом і вродженим. Але тоді це справді стає останнім засобом замість єдиного варіанту.

Отже, в стилі приємно робити прості речі і швидко щось запускати. Однак, як тільки все почне рости, ви, швидше за все, дійдете до того, що ви помітите, що краще використовувати Cython з самого початку.


4
+1 - це хороші бали, велике спасибі! Хоча мені цікаво, чи переїзд лише частин вузьких місць до Cython - це справді велика накладні витрати. Але я погоджуюся, якщо ви очікуєте будь-яких проблем із продуктивністю, ви можете також використати Cython з самого початку.
balpha

Чи все-таки це стосується програмістів, досвідчених як з C, так і з Python? У цьому випадку можна стверджувати, що кращий вибір Python / ctypes, оскільки векторизація циклів C (SIMD) іноді є більш простою. Але, крім цього, я не можу придумати жодних недоліків Cython.
Алекс ван Хоутен

Дякую за відповідь! Одне, з чим у мене виникли проблеми щодо Cython, - це правильний процес збирання (але це також має відношення до того, що я ніколи не писав модуль Python) - чи слід його компілювати раніше, або включати вихідні файли Cython у sdist та подібні питання. Я написав про це в блозі, якщо хтось має подібні проблеми / сумніви: martinsosic.com/development/2016/02/08/…
Martinsos

Дякую за відповідь! Одним із недоліків, коли я використовую Cython, є те, що перевантаження оператора здійснюється не повністю (наприклад __radd__). Це особливо дратує, коли ви плануєте взаємодія вашого класу із вбудованими типами (наприклад, intта float). Крім того, магічні методи в цитоні взагалі просто трохи баггі.
Моноліт

100

Сам по собі Cython є досить класним інструментом, який варто вивчити і на диво близький до синтаксису Python. Якщо ви робите якісь наукові обчислення з Numpy, то Cython - це шлях, оскільки він інтегрується з Numpy для швидких операцій з матрицею.

Cython - це суперсеть мови Python. Ви можете кинути на нього будь-який дійсний файл Python, і він виплюне дійсну програму C. У цьому випадку Cython просто відобразить виклики Python на базовий API CPython. Це призводить, можливо, до 50% прискорення, оскільки ваш код більше не інтерпретується.

Щоб отримати оптимізацію, потрібно почати розповідати Cython додаткові факти про ваш код, наприклад, декларації типу. Якщо ви скажете це достатньо, він може звести код до чистого C. Тобто, цикл for Python стає циклом для C. Тут ви побачите значне збільшення швидкості. Ви також можете посилання на зовнішні програми C тут.

Використовувати код Cython також неймовірно просто. Я подумав, що в цьому посібнику це звучить важко. Ви буквально просто робите:

$ cython mymodule.pyx
$ gcc [some arguments here] mymodule.c -o mymodule.so

і тоді ви можете import mymoduleу своєму Python-коді і повністю забути, що він складається до C.

У будь-якому випадку, оскільки Cython настільки простий у налаштуванні та початку використання, я пропоную спробувати перевірити, чи відповідає він вашим потребам. Це не буде марним, якщо виявиться, що це не той інструмент, який ви шукаєте.


1
Нема проблем. Найприємніше у Cython - це те, що ти можеш навчитися лише тому, що тобі потрібно. Якщо ви хочете лише невеликого вдосконалення, все, що вам потрібно зробити, - це зібрати свої файли Python, і ви закінчите.
Карл

18
"Ви можете кинути на нього будь-який дійсний файл Python, і він виплюне дійсну програму C." <- Не зовсім, є деякі обмеження: docs.cython.org/src/userguide/limitations.html Мабуть, це не проблема для більшості випадків використання, але просто хотіла бути завершеною.
Ренді Сірінг

7
Проблем стає все менше з кожним випуском, до того часу, що на цій сторінці сказано, що "більшість питань було вирішено в 0,15".
Генрі Гомерсалл

3
Додамо, Є НАДІЙ простіший спосіб імпорту цитонного коду: напишіть свій код цитону як mymod.pyxмодуль, а потім зробіть, import pyximport; pyximport.install(); import mymodі компіляція відбувається за кадром.
Kaushik Ghose

3
@kaushik Ще простішим є pypi.python.org/pypi/runcython . Просто використовуйте runcython mymodule.pyx. І на відміну від pyximport, ви можете використовувати його для більш складних завдань, що зв'язують. Тільки застереження полягає в тому, що я є тим, хто написав 20 рядків баш за це і може бути упереджений.
RussellStewart

42

Для виклику бібліотеки С з програми Python також є cffi, яка є новою альтернативою для типів . Це приносить новий вигляд FFI:

  • він вирішує проблему захоплюючим, чистим способом (на відміну від типів )
  • не потрібно писати код, який не належить Python (як у SWIG, Cython , ...)

Однозначно шлях для обгортання , як хотів ОП. cython чудово звучить для написання ними гарячих циклів, але для інтерфейсів, cffi просто є прямим оновленням від ctypes.
літаючі вівці

21

Я викину ще одного: SWIG

Це легко вчитися, робить багато речей правильно, і підтримує багато інших мов, так що час, витрачений на вивчення, може бути дуже корисним.

Якщо ви використовуєте SWIG, ви створюєте новий модуль розширення python, але SWIG робить більшу частину важкого підйому для вас.


18

Особисто я написав би модуль розширення в C. Не залякуйте розширення Python C - їх зовсім не важко написати. Документація дуже чітка і корисна. Коли я вперше написав розширення на C в Python, я думаю, що мені знадобилося близько години, щоб розібратися, як написати це - зовсім не багато часу.


Обгортання бібліотеки С. Дійсно
mipadi

1
@forivall: Код насправді не так корисний, і там є кращі генератори випадкових чисел. У мене є резервна копія на комп’ютері.
mipadi

2
Домовились. C-API Python не так страшно, як виглядає (якщо ви знаєте C). Однак, на відміну від python та його резервуара бібліотек, ресурсів та розробників, коли ви пишете розширення на C, ви в основному самостійно. Напевно, є єдиним його недоліком (крім тих, які зазвичай надходять із написанням на С).
Noob Saibot

1
@mipadi: добре, але вони відрізняються між Python 2.x та 3.x, тому Cython зручніше використовувати для написання розширення, Cython розібрається з усіма деталями, а потім скомпілювати створений код C для Python 2.x або 3.x за потребою.
0xC0000022L

2
@mipadi здається, що посилання github мертве, і воно не здається доступним на archive.org. У вас є резервна копія?
jrh

11

ctypes - це чудово, коли у вас вже є компільована бібліотека, яку ви маєте вирішити (наприклад, бібліотеки ОС). Однак накладні виклики є серйозними, тому, якщо ви будете робити багато дзвінків у бібліотеку, і ви все одно будете писати код C (або принаймні його компілювати), я б сказав, щоб піти на цитон . Це не набагато більше роботи, і використовувати отриманий файл pyd буде набагато швидше і пітонічніше.

Я особисто схильний використовувати cython для швидкого прискорення коду python (зіставлення циклів і цілих чисел - це дві області, де цитон особливо світить), і коли буде ще якийсь задіяний код / ​​обгортання інших задіяних бібліотек, я звернусь до Boost.Python . Boost.Python може бути вибагливим у налаштуванні, але після того, як ви працюєте, він робить обробку коду C / C ++ просто.

cython також чудово підходить для обгортання нумету (про що я дізнався з розгляду SciPy 2009 ), але я не використовував numpy, тому не можу це коментувати.


11

Якщо у вас вже є бібліотека з визначеним 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

З невеликим формуванням рядків, ви можете взяти результат будь-яким способом. Ви також можете фіксувати стандартний вихід помилок, тому він досить гнучкий.


Хоча в цій відповіді немає нічого невірного, люди повинні бути обережними, якщо коду потрібно відкрити для доступу до інших, оскільки виклик підпроцесу shell=Trueможе легко призвести до певного використання, коли користувач дійсно отримує оболонку. Це добре, коли розробник є єдиним користувачем, але у світі є ціла купа дратівливих колючок, які просто чекають на щось подібне.
Бен

7

Є одне питання, яке змусило мене використовувати ctypes, а не cython, і це не згадується в інших відповідях.

Використовуючи ctypes, результат взагалі не залежить від компілятора, який ви використовуєте. Ви можете написати бібліотеку, використовуючи більш-менш будь-яку мову, яка може бути зібрана в рідну спільну бібліотеку. Не має великого значення, яка система, яка мова та який компілятор. Однак Cython обмежений інфраструктурою. Наприклад, якщо ви хочете використовувати компілятор intel у Windows, набагато складніше змусити роботу cython: вам слід "пояснити" компілятор до cython, перекомпілювати щось із цим точним компілятором і т. Д. Що суттєво обмежує портативність.


4

Якщо ви орієнтовані на 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/


Ця стаття не має нічого спільного з проблемами, які виникають із сумісністю компіляторів Microsoft. Отримати розширення Cython для роботи в Windows насправді не дуже складно. Я міг використовувати MinGW майже для всього. Хоча хороший розподіл Python.
ЯнХ

2
+1 для згадування можливої ​​проблеми у Windows (що я зараз маю також ...). @IanH взагалі менше стосується Windows, але це безлад, якщо ви застрягли з певною стороною lib, яка не відповідає вашому розповсюдженню python.
себастьян


2

Я знаю, що це давнє запитання, але ця річ виникає в 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 в сумісний тип змінного струму.
    • Близько години про те, як правильно повернути рядок з функції змінного струму. Тут я фактично запропонував своє власне рішення SO після того, як я написав функції.
    • Близько півгодини, щоб написати код на c, скласти його в динамічну бібліотеку.
    • 10 хвилин, щоб написати тестовий код у python, щоб перевірити, чи cпрацює код.
    • Близько години робити тести і переставляти cкод.
    • Потім я підключив cкод до фактичної бази коду, і побачив, що ctypesвін не дуже добре працює з multiprocessingмодулем, оскільки його обробник не підбирається за замовчуванням.
    • Близько 20 хвилин я переставив код, щоб не використовувати multiprocessingмодуль, і повторився.
    • Тоді друга функція в моєму cкоді генерувала segfault в моїй кодовій базі, хоча вона пройшла мій код тестування. Ну, це, мабуть, моя вина, що я не перевіряв кращі справи, я шукав швидке рішення.
    • Близько 40 хвилин я намагався визначити можливі причини цих segfault.
    • Я розділив свої функції на дві бібліотеки і спробував ще раз. У мене ще були сегменти для моєї другої функції.
    • Я вирішив відпустити другу функцію і використовувати лише першу функцію cкоду, а під час другої або третьої ітерації циклу python, який її використовує, я мав UnicodeErrorпро те, щоб не декодувати байт у деякому положенні, хоча я кодував і декодував усе виразно.

У цей момент я вирішив шукати альтернативу і вирішив розглянути cython:

  • Сітон
    • 10 хв читання світу cython hello .
    • 15 хв перевірки SO про те , як використовувати Cython з setuptoolsзамість distutils.
    • 10 хв читання типів цитонів та типів пітона. Я дізнався, що можу використовувати більшість типів вбудованого пітона для статичного набору тексту.
    • 15 хв reannotiting мій python код з цитонами.
    • 10 хв зміни моїх setup.pyможливостей використовувати скомпільований модуль у моїй кодовій базі.
    • Підключено модуль безпосередньо до multiprocessingверсії кодової бази. Це працює.

Для запису я, звичайно, не вимірював точні терміни моєї інвестиції. Цілком може статися так, що моє сприйняття часу було трохи уважним через розумові зусилля, необхідні, коли я мав справу з типом. Але це повинно передати відчуття стосунків cythonіctypes

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.