Перевод статьи подготовлен специально для студентов курса «Разработчик Python».




Внимание: код в этой статье лицензирован под GNU AGPLv3.

Я написал это руководство, поскольку не смог найти такого, которое будет объединять в себе все полезное о ctypes. Надеюсь, эта статья сделает чью-то жизнь намного легче.

Содержание:

  1. Базовые оптимизации
  2. сtypes
  3. Компиляция под Python
  4. Структуры в Python
  5. Вызов вашего кода на С
  6. 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 отобразит все предупреждения;
  • -Werror обернет все предупреждения в ошибки;
  • -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).

На этом все. До встречи на курсе!