Otimização de código Python com ctypes

Uma tradução do artigo foi preparada especificamente para os alunos do curso Python Developer .




Nota : o código neste artigo está licenciado sob GNU AGPLv3 .

Escrevi este guia porque não consegui encontrar um que combine todas as coisas úteis sobre os tipos. Espero que este artigo facilite muito a vida de alguém.

Conteúdo:

  1. Otimizações básicas
  2. estilos
  3. Compilação Python
  4. Estruturas em Python
  5. Ligue para seu código em C
  6. Pypy

Otimizações básicas


Antes de reescrever o código fonte do Python em C, considere os métodos básicos de otimização no Python.

Estruturas de dados incorporadas


As estruturas de dados internas do Python, como set e dict, são escritas em C. Eles funcionam muito mais rápido que as suas próprias estruturas de dados escritas como classes Python. Outras estruturas de dados além do conjunto padrão, ditado, lista e tupla são descritas na documentação do módulo de coleções .

Listar expressões


Em vez de adicionar itens à lista usando o método padrão, use expressões de lista.

# Slow
      mapped = []
      for value in originallist:
          mapped.append(myfunc(value))
      
      # Faster
      mapped = [myfunc(value) in originallist]

ctypes


O módulo ctypes permite que você interaja com o código C do Python sem usar um módulo subprocessou outro módulo semelhante para iniciar outros processos a partir da CLI.

Existem apenas duas partes: compilar o código C para carregar qualidade shared objecte configurar estruturas de dados no código Python para mapeá-las para os tipos C.

Neste artigo, conectarei meu código Python ao lcs.c , que encontra a subsequência mais longa em dois listas de strings. Quero que o seguinte funcione em Python:

list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!']
      list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.']
      
      common = lcs(list1, list2)
      
      print(common)
      # ['My', 'name', 'is', 'Stevens']

Um problema é que essa função C específica é a assinatura de uma função que recebe listas de cadeias como tipos de argumento e retorna um tipo que não possui um comprimento fixo. Eu resolvo esse problema com uma estrutura de sequência contendo ponteiros e comprimento.

Compilando código C em Python


Primeiro, o código-fonte C ( lcs.c ) é compilado lcs.sopara carregar no Python.

gcc -c -Wall -Werror -fpic -O3 lcs.c -o lcs.o
      gcc -shared -o lcs.so lcs.o

  • - Wall exibirá todos os avisos;
  • - Werror irá quebrar todos os avisos em erros;
  • - o fpic irá gerar instruções independentes da posição que você precisará se quiser usar esta biblioteca em Python;
  • - O3 maximiza a otimização;

E agora começaremos a escrever código Python usando o arquivo de objeto compartilhado resultante .

Estruturas em Python


Abaixo estão duas estruturas de dados que são usadas no meu código C.

struct Sequence
      {
          char **items;
          int length;
      };
      
      struct Cell
      {
          int index;
          int length;
          struct Cell *prev;
      };

E aqui está a tradução dessas estruturas em 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))]

Algumas notas:

  • Todas as estruturas são classes que herdam ctypes.Structure.
  • O único campo _fields_é uma lista de tuplas. Cada tupla é ( <variable-name>, <ctypes.TYPE>).
  • Existem ctypestipos semelhantes em c_char (char) e c_char_p (* char) .
  • Também existe ctypesum POINTER()que cria um ponteiro de tipo de cada tipo passado para ele.
  • Se você tem uma definição recursiva como em CELL, deve passar a declaração inicial e adicionar os campos _fields_para obter um link para si mesmo posteriormente.
  • Como não usei CELLPython no meu código, não precisei escrever essa estrutura, mas ela possui uma propriedade interessante no campo recursivo.

Ligue para seu código em C


Além disso, eu precisava de algum código para converter tipos Python em novas estruturas em C. Agora você pode usar sua nova função C para acelerar o código 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']

Algumas notas:

  • **char (lista de strings) corresponde diretamente a uma lista de bytes em Python.
  • Existe lcs.cuma função lcs()com a assinatura struct Sequence * lcs (struct Sequence * s1, struct Sequence * s2) . Para configurar o tipo de retorno, eu uso lcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE).
  • Para fazer uma chamada com uma referência à estrutura Sequence , eu uso ctypes.byref()uma que retorna um "ponteiro leve" para o seu objeto (mais rápido que ctypes.POINTER()).
  • common.items- esta é uma lista de bytes, eles podem ser decodificados para entrar retna forma de uma lista str.
  • lcsmodule.freeSequence (common) libera apenas a memória associada ao common. Isso é importante porque o coletor de lixo (AFAIK) não o coleta automaticamente.

Código Python otimizado é o código que você escreveu em C e envolveu em Python.

Algo mais: PyPy


Atenção: eu mesmo nunca usei o PyPy.
Uma das otimizações mais simples é executar seus programas no tempo de execução do PyPy , que contém um compilador JIT (just-in-time) que acelera o trabalho de loops, compilando-os em código de máquina para execução repetida.

Se você tiver comentários ou quiser discutir algo, escreva para mim (samuel.robert.stevens@gmail.com).

Isso é tudo. Vejo você no curso !

All Articles