Внимание: код в этой статье лицензирован под GNU AGPLv3.
Я написал это руководство, поскольку не смог найти такого, которое будет объединять в себе все полезное о ctypes. Надеюсь, эта статья сделает чью-то жизнь намного легче.
Содержание:
- Базовые оптимизации
- сtypes
- Компиляция под Python
- Структуры в Python
- Вызов вашего кода на С
- PyPy
Базовые оптимизации
Перед переписыванием исходного кода Python на С, рассмотрим базовые методы оптимизации в Python.
Встроенные структуры данных
Встроенные структуры данных в Python, такие как set и dict, написаны на С. Они работают гораздо быстрее, чем ваши собственные структуры данных, написанные как классы Python. Другие структуры данных, помимо стандартных set, dict, list и tuple описаны в документации модуля collections.
Списочные выражения
Вместо того, чтобы добавлять элементы к списку стандартным методом, воспользуйтесь списочными выражениями.
# Slow mapped = [] for value in originallist: mapped.append(myfunc(value)) # Faster mapped = [myfunc(value) in originallist]
ctypes
Модуль ctypes позволяет вам взаимодействовать с кодом на С из Python без использования модуля subprocess или другого подобного модуля для запуска других процессов из CLI.
Тут всего две части: компиляция кода на С для загрузки в качестве shared object и настройка структур данных в коде на Python для сопоставления их с типами С.
В этой статье я буду соединять свой код на Python с lcs.c, которая находит самую длинную подпоследовательность в двух списках строк. Я хочу, чтобы в Python работало следующее:
list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!'] list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.'] common = lcs(list1, list2) print(common) # ['My', 'name', 'is', 'Stevens']
Одна из проблем заключается в том, что эта конкретная функция на С — это сигнатура функции, которая принимает списки строк в качестве типов аргументов, и возвращает тип, у которого нет фиксированной длины. Я решаю эту задачу с помощью структуры последовательности, содержащей указатели и длину.
Компиляция кода на С под Python
Сначала, исходный код на С (lcs.c) компилируется в lcs.so, чтобы загружаться в Python.
gcc -c -Wall -Werror -fpic -O3 lcs.c -o lcs.o gcc -shared -o lcs.so lcs.o
- —Wall отобразит все предупреждения;
- —Werrow обернет все предупреждения в ошибки;
- —fpic сгенерирует независимые от положения инструкции, которые понадобятся, если вы захотите использовать эту библиотеку в Python;
- —O3 максимизирует оптимизацию;
А теперь мы начнем писать код на Python, используя полученный файл shared object.
Структуры в Python
Ниже представлены две структуры данных, которые используются в моем коде на С.
struct Sequence { char **items; int length; }; struct Cell { int index; int length; struct Cell *prev; };
А вот перевод этих структур на Python.
import ctypes class SEQUENCE(ctypes.Structure): _fields_ = [('items', ctypes.POINTER(ctypes.c_char_p)), ('length', ctypes.c_int)] class CELL(ctypes.Structure): pass CELL._fields_ = [('index', ctypes.c_int), ('length', ctypes.c_int), ('prev', ctypes.POINTER(CELL))]
Несколько заметок:
- Все структуры – это классы, наследуемые от
ctypes.Structure. - Единственное поле
_fields_представляет из себя список кортежей. Каждый кортеж это (<variable-name>,<ctypes.TYPE>). - В
ctypesесть похожие типы c_char (char) и c_char_p (*char). - В
ctypesтакже естьPOINTER(), который создает указатель типа из каждого типа ему переданного. - Если у вас есть рекурсивное определение, как в
CELL, вы должны передать начальное объявление, а затем добавить поля_fields_, чтобы позже получить ссылку на себя же. - Поскольку я не использовал
CELLв своем коде на Python, мне не нужно было писать эту структуру, но у нее есть интересное свойство в рекурсивном поле.
Вызов вашего кода на С
Кроме того, мне нужен был какой-нибудь код для преобразования типов Python в новые структуры на С. Теперь вы можете использовать свою новую функцию на С, чтобы ускорить работу кода на Python.
def list_to_SEQUENCE(strlist: List[str]) -> SEQUENCE: bytelist = [bytes(s, 'utf-8') for s in strlist] arr = (ctypes.c_char_p * len(bytelist))() arr[:] = bytelist return SEQUENCE(arr, len(bytelist)) def lcs(s1: List[str], s2: List[str]) -> List[str]: seq1 = list_to_SEQUENCE(s1) seq2 = list_to_SEQUENCE(s2) # struct Sequence *lcs(struct Sequence *s1, struct Sequence *s2) common = lcsmodule.lcs(ctypes.byref(seq1), ctypes.byref(seq2))[0] ret = [] for i in range(common.length): ret.append(common.items[i].decode('utf-8')) lcsmodule.freeSequence(common) return ret lcsmodule = ctypes.cdll.LoadLibrary('lcsmodule/lcs.so') lcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE) list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!'] list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.'] common = lcs(list1, list2) print(common) # ['My', 'name', 'is', 'Stevens']
Немного заметок:
**char(список строк) проводит сопоставление напрямую в список байт в Python.- В
lcs.cесть функцияlcs()с сигнатурой struct Sequence *lcs(struct Sequence *s1, struct Sequence *s2). Чтобы настроить возвращаемый тип, я используюlcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE). - Чтобы сделать вызов с ссылкой на структуру Sequence, я использую
ctypes.byref(), который возвращает «легкий указатель» на ваш объект (быстрее, чемctypes.POINTER()). common.items– это список байт, они могут быть декодированы для полученияretв виде спискаstr.- lcsmodule.freeSequence(common) просто освобождает память, ассоциированную с common. Это важно, поскольку сборчик мусора (AFAIK) его не соберет автоматически.
Оптимизированный код на Python – это код, который вы написали на С и обернули в Python.
Кое-что еще: PyPy
Внимание: Сам я никогда PyPy не пользовался.
Одна из самых простых оптимизаций заключается в запуске ваших программ в среде выполнения PyPy, которая содержит в себе JIT-компилятор (just-in-time), который ускоряет работу циклов, компилируя их в машинный код при многократном выполнении.
Если у вас появятся комментарии или вы захотите что-то обсудить, напишите мне (samuel.robert.stevens@gmail.com).
На этом все. До встречи на курсе!
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/490244/
Добавить комментарий