Skip to main content

Fast binary serializer for Python with optional C acceleration.

Project description

🥒 compickle

Serialización binaria para Python con motor en C — rápido, compacto y sin dependencias.

Python Motor C Versión Licencia Compilación


¿Qué es compickle?

compickle es un serializador binario escrito principalmente en C y expuesto como extensión nativa de Python (_compickle). Está diseñado para ser simple de usar y más rápido que pickle en la mayoría de cargas de trabajo, gracias a:

  • Un buffer de salida dinámico (Buf) que crece en potencias de 2 desde 256 bytes inicial.
  • Un DedupState local por llamada: en v2 el estado de deduplicación vive en el stack de cada serialize_fast() — elimina por completo la necesidad de llamar dedup_reset() antes de cada dumps().
  • Una tabla de deduplicación FNV-1a dinámica que arranca con 256 entradas y 512 buckets, y crece (rehash automático con factor de carga máximo 0.65) sin límite fijo de capacidad.
  • Un id_cache por identidad de puntero Python (uintptr_t): shortcut O(1) antes del hash FNV-1a para objetos que aparecen repetidos en el mismo árbol de serialización.
  • Un arena allocator interno para los datos de dedup: bloque contiguo de 1 MB inicial, reset = mover un puntero, sin malloc/free por cada entrada.
  • Un read_table dinámico en el deserializador: arranca con 256 entradas y crece con realloc, eliminando el límite fijo de 8 192 entradas de v1.
  • Funciones via marshal (opcode 0x20): lambdas, generator functions y funciones normales se serializan por bytecode, sin depender de inspect.getsource. Funciona en REPL y eval().
  • Floats especiales (0.0, -0.0, 1.0, -1.0, NaN, ±inf) en 1 byte cada uno (opcodes 0x820x88).
  • Enteros negativos pequeños -1..-30 en 2 bytes (opcode 0x10).
  • Compact string opcode 0x15 para strings nuevas cortas (≤ 63 bytes UTF-8): ahorra 1 byte frente al camino largo.
  • Compilación nativa con -O3 -march=native -mtune=native aplicada automáticamente en setup.py.
  • Un fallback puro en Python (compickle.py) que implementa exactamente el mismo protocolo binario y se activa si la extensión C no está disponible.
  • Lazy loading en __init__.py: la extensión C se importa solo en la primera llamada real a dumps, loads, dump, load, dedup_reset o backend().

Versión actual: 1.0.8 (definida en __init__.py)


⚙️ Instalación

pip install .

Para desarrollo (compilación en el directorio actual):

python setup.py build_ext --inplace

setup.py detecta la arquitectura vía platform.machine() y añade -march=native -mtune=native automáticamente en GCC/Clang. La extensión nativa se construye como _compickle (con guión bajo) y el módulo público compickle la importa internamente.


🚀 Uso rápido

import compickle

datos = {
    "nombre": "Rex",
    "edad": 5,
    "activo": True,
    "coordenadas": (4.0, 2.0),
    "etiquetas": {"perro", "mascota"},
}

# Serializar a archivo
compickle.dump(datos, "datos.cpkl")

# Deserializar desde archivo
copia = compickle.load("datos.cpkl")

# Serializar/deserializar en memoria (bytes)
raw = compickle.dumps(datos)
copia2 = compickle.loads(raw)

# Ver qué motor está activo
print(compickle.backend())  # → 'c' o 'python'

📖 API completa

compickle.dump(obj, path)

Serializa obj y escribe el resultado binario en path. Internamente llama a dumps(obj) y abre el archivo en modo 'wb'.

compickle.dump(mi_objeto, "salida.cpkl")

compickle.dumps(obj) → bytes

Serializa obj y devuelve los bytes resultantes directamente.

En v2 con el motor C, cada llamada a dumps() crea su propio DedupState en el stack — no es necesario llamar dedup_reset() antes de cada dumps(). En el fallback Python, dumps() llama _reset_write_state() internamente.

raw: bytes = compickle.dumps(mi_objeto)

compickle.load(path) → object

Lee el archivo binario en path y reconstruye el objeto original.

obj = compickle.load("salida.cpkl")

compickle.loads(data: bytes) → object

Deserializa directamente desde un objeto bytes.

obj = compickle.loads(raw_bytes)

compickle.dedup_reset()

Limpia los caches globales de source y ejecución tanto del motor C (source_cache, exec_cache) como del fallback Python (_reset_write_state()).

En v2 esto ya no es necesario antes de cada dumps() — el DedupState del motor C es local por llamada. Úsalo únicamente para liberar memoria en procesos de larga duración que hayan serializado muchas clases o funciones distintas.

compickle.dedup_reset()

compickle.backend() → str

Devuelve el motor activo. Dispara el lazy loading si aún no se había importado la extensión.

compickle.backend()  # → 'c'      (extensión _compickle compilada)
                     # → 'python' (fallback puro Python)

🧩 Tipos soportados

Tipo Python Tag Deduplicado Notas
None 0x00 1 byte
False 0x80 1 byte
True 0x81 1 byte
int (0–58) 0xC0–0xFA 1 byte: opcode directo 0xC0 + valor
int (-1..-30) 0x10 2 bytes: [0x10][magnitud - 1]
int (59–65535) 0x0F 3 bytes: tag + uint16 big-endian
int (arbitrario) 0x02 signo(1) + write_len + bytes big-endian
float (+0.0) 0x82 1 byte
float (-0.0) 0x83 1 byte (distinguido del +0.0 por sign bit)
float (1.0) 0x84 1 byte
float (-1.0) 0x85 1 byte
float (NaN) 0x86 1 byte
float (+inf) 0x87 1 byte
float (-inf) 0x88 1 byte
float (general) 0x03 9 bytes: tag + IEEE 754 doble precisión big-endian
complex 0x04 17 bytes: tag + dos float64 big-endian
str 0x05/0x15 UTF-8 + dedup FNV-1a; 0x15 para strings nuevas ≤ 63 bytes
bytes 0x06 Deduplicado por contenido
bytearray 0x07 Deduplicado por contenido
list 0x08 Elementos serializados recursivamente
tuple 0x09 Elementos serializados recursivamente
set 0x0A Ordenado por repr() para serialización determinista
frozenset 0x0B Ordenado por repr() para serialización determinista
dict 0x0C Claves y valores serializados recursivamente
function / lambda 0x20 ✅ (bytecode) marshal del code object + defaults + freevars
Generador (objeto) 0x08 Se materializa en lista consumiendo el generador
type (clase) 0x12 Nombre + módulo + fuente (inspect.getsource)
Instancia con __dict__ 0x1C ✅ (fuente) Encabezado de clase + __dict__ serializado
Instancia con __slots__ 0x1E ✅ (fuente) Recorre MRO completo para capturar todos los slots
Objeto con __reduce__ 0x1F Soporta tuplas de 2 a 5 elementos; None en pos. 2–4 se ignora

Prioridad de serialización de instancias:

  1. __reduce__ personalizado en el MRO → tag 0x1F
  2. __dict__ disponible → tag 0x1C
  3. __slots__ sin __dict__ → tag 0x1E

🔬 Cómo funciona internamente

Motor C (compickle.c, ~1 476 líneas)

Buffer de salida (Buf)

typedef struct { uint8_t *buf; Py_ssize_t len, cap; } Buf;

El buffer comienza con capacidad 256 bytes y se duplica vía realloc cuando el espacio es insuficiente. Las funciones primitivas son buf_u8, buf_u16be, buf_u64be y buf_raw.

Arena allocator

typedef struct { uint8_t *base; size_t pos; size_t cap; } Arena;

Bloque contiguo de 1 MB inicial para los datos de cada entrada dedup. Reset = mover arena.pos a 0. Sin malloc/free por entrada — elimina la fragmentación de memoria en serializaciones intensivas.

DedupState local por llamada (novedad v2)

typedef struct {
    Arena    arena;
    DEntry  *table;   /* tabla de entradas (dinámica, inicia en 256) */
    int      count, cap;
    int     *buckets; /* tabla hash (inicia en 512 buckets, potencia de 2) */
    int      nbuckets;
    IdEntry *id_tab;  /* tabla hash de punteros Python → índice */
    int      id_nb, id_count;
} DedupState;

En v1, dedup_table era un array estático global de 8 192 entradas. En v2 el DedupState es local al stack de py_serialize_fast() y se destruye al terminar cada llamada. Esto elimina el bug de estado compartido entre llamadas consecutivas sin dedup_reset() explícito.

El rehash ocurre automáticamente cuando el factor de carga supera 0.65. El id_tab (cache por puntero) rehashea cuando supera 0.60.

id_cache (shortcut O(1) por identidad)

Antes de calcular FNV-1a, se busca id(obj) en id_tab por probing lineal sobre uintptr_t. Si hay hit, se emite la referencia directamente sin hash ni memcmp.

Tabla hash FNV-1a

static uint32_t fnv1a(uint8_t tag, const uint8_t *d, Py_ssize_t n);

Hash de 32 bits aplicado sobre el tag de tipo seguido de los bytes del dato. Las colisiones se resuelven con listas enlazadas dentro de DEntry.next.

Codificación de longitudes variables

n ≤ 0x3F    → 1 byte
n ≤ 0x3FFF  → 2 bytes (0x40 | n>>8 , n & 0xFF)
n > 0x3FFF  → 5 bytes (0xFF + uint32 big-endian)

Compact string opcode (0x15)

Para strings nuevas con codificación UTF-8 de longitud ≤ 63 bytes:

[0x15] [len_1byte] [datos...]   → 2 + len bytes

Frente al camino general para strings nuevas largas:

[0x05] [0xFB] [0x05] [len...] [datos...]

Referencias dedup

idx ≤ 0xFE   → [0xFE] [idx_1byte]      (2 bytes total)
idx ≤ 0xFFFF → [0xFD] [idx_2bytes_be]  (3 bytes total)
idx > 0xFFFF → [0xFC] [idx_4bytes_be]  (5 bytes total)

Si hay hit en dedup, no se emite el type tag — solo la referencia. El deserializador reconstruye el tipo desde read_table_types[idx].

Funciones vía marshal (opcode 0x20)

El code object se serializa con marshal.dumps() y se deduplica en la tabla. A continuación se emiten los defaults posicionales y los valores de las variables libres (freevars). Cells vacías se emiten como None (0x00).

Al deserializar se reconstruye la función con types.FunctionType (C: PyFunction_New) usando builtins como globals.

Objetos generador

PyGen_CheckExact / PyCoro_CheckExact → se materializan en lista con PySequence_List (consume el generador) y se emiten con opcode 0x08.

read_table dinámico (novedad v2)

En v1 era un array fijo de 8 192 PyObject*. En v2 arranca con 256 entradas y crece con realloc sin límite. El reset solo decrementa read_table_count y hace Py_XDECREF de las referencias — los buffers se reutilizan entre llamadas.

Caches globales (source_cache, exec_cache)

Son los únicos estados globales que persisten entre llamadas:

  • source_cache: id(obj)bytes del source vía inspect.getsource. Evita I/O repetido.
  • exec_cache: src_bytesnamespace dict de PyRun_String. Evita re-ejecutar el mismo source al deserializar.

dedup_reset() limpia ambos; el DedupState de serialización se destruye automáticamente al terminar cada llamada.

Enteros arbitrarios — compatibilidad 3.9–3.13+

Para Python < 3.13 se usa _PyLong_AsByteArray con firma de 5 argumentos. Para Python ≥ 3.13 (donde cambió la firma) se usa la variante de 6 argumentos con #if PY_VERSION_HEX >= 0x030d0000. El número de bytes se calcula con bit_length() vía PyObject_CallMethod.


Fallback Python (compickle.py, ~794 líneas)

Implementa exactamente el mismo protocolo binario en Python puro. Diferencias internas:

  • La deduplicación usa _dedup_by_content: dict[tuple[int, bytes], int] más _dedup_by_id: dict[int, int] para el shortcut por identidad.
  • Un _ref_bytes_cache: dict[int, bytes] precalcula los bytes de cada referencia.
  • La serialización acumula en un bytearray (append nativo, sin concatenación de bytes).
  • La deserialización usa memoryview para acceso sin copia.
  • Los closures se reconstruyen con types.FunctionType usando la misma técnica de lambda v: (lambda: v).__closure__[0] que el motor C.
  • Dispatch por tipo exacto en _SERIALIZE_DISPATCH: dict[type, Callable] — evita la cascada de isinstance en el ~99 % de los casos.
  • dumps() llama _reset_write_state() internamente — mismo comportamiento que el motor C en v2.

Lazy loading (__init__.py, ~103 líneas)

def _ensure_loaded():
    global _USE_C, _serialize_fast, _deserialize_fast, _c_dedup_reset
    if _USE_C is not None:
        return
    try:
        from ._compickle import serialize_fast, deserialize_fast, dedup_reset
        _USE_C = True
    except ImportError:
        _USE_C = False

_ensure_loaded() se invoca en la primera llamada a cualquier función pública. Un import compickle nunca falla aunque la extensión C no esté compilada.

__getattr__ resuelve _USE_C, serialize_fast y deserialize_fast bajo demanda para compatibilidad con código que acceda a estos nombres directamente en el módulo.


🏗️ Estructura del proyecto

compickle/
├── compickle.c     # Motor principal en C (~1 476 líneas) — extensión CPython _compickle
├── compickle.py    # Implementación de referencia en Python puro (~794 líneas)
├── __init__.py     # API pública con lazy loading (~103 líneas), versión 1.0.8
└── setup.py        # Build con -O3 -march=native -mtune=native

🧪 Ejemplos avanzados

Clase con __dict__

import compickle

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Punto(3.0, 7.5)
compickle.dump(p, "punto.cpkl")
p2 = compickle.load("punto.cpkl")
print(p2.x, p2.y)  # → 3.0  7.5

La clase se serializa con su código fuente completo (vía inspect.getsource). Al deserializar, el source se ejecuta en un namespace aislado y la clase se recupera por nombre.


Clase con __slots__

class Vector:
    __slots__ = ("x", "y", "z")
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z

v = Vector(1, 2, 3)
compickle.dump(v, "vector.cpkl")
v2 = compickle.load("vector.cpkl")
print(v2.x, v2.y, v2.z)  # → 1  2  3

El motor recorre el MRO completo para capturar todos los slots, incluyendo los heredados. Solo se incluyen los slots con valor asignado.


Objeto con __reduce__

class Color:
    def __init__(self, r, g, b):
        self.r, self.g, self.b = r, g, b

    def __reduce__(self):
        return (Color, (self.r, self.g, self.b))

c = Color(255, 128, 0)
compickle.dump(c, "color.cpkl")
c2 = compickle.load("color.cpkl")
print(c2.r, c2.g, c2.b)  # → 255  128  0

__reduce__ puede devolver tuplas de 2 a 5 elementos: (callable, args[, state[, list_items[, dict_items]]]). Los elementos None en posiciones 2–4 se tratan como ausentes. El callable debe tener __name__; si no tiene source accesible, se busca en builtins y módulos ya importados al deserializar.


Lambda y funciones vía marshal

import compickle

fn = lambda x: x * 2
raw = compickle.dumps(fn)
fn2 = compickle.loads(raw)
print(fn2(21))  # → 42

En v2, las funciones se serializan por bytecode (marshal). Esto funciona en el REPL y con lambdas, sin necesidad de archivo fuente en disco.


Serialización en memoria (dumps / loads)

import compickle

datos = [1, "hola", {"a": True}, (3.14,)]
raw = compickle.dumps(datos)
print(type(raw))   # → <class 'bytes'>

restaurado = compickle.loads(raw)
assert restaurado == datos

📦 Formato binario — tabla completa de opcodes

Opcode        Tipo / Significado
──────────────────────────────────────────────────────────────────────────
0x00          None
0x02          int arbitrario: signo(1) + write_len + bytes big-endian
0x03          float general: 8 bytes IEEE 754 big-endian
0x04          complex: 2 × float64 big-endian (16 bytes)
0x05          str: tag + write_dedup(UTF-8) [strings largas o refs]
0x06          bytes: tag + write_dedup(contenido)
0x07          bytearray: tag + write_dedup(contenido)
0x08          list / generador materializado: write_len(n) + n × serialize(item)
0x09          tuple: write_len(n) + n × serialize(item)
0x0A          set: write_len(n) + n × serialize(item ordenado por repr())
0x0B          frozenset: write_len(n) + n × serialize(item ordenado por repr())
0x0C          dict: write_len(n) + n × (serialize(k) + serialize(v))
0x0E          dedup short (nuevo): longitud(1 byte, ≤ 63) + datos
0x0F          int 16 bits: 2 bytes uint16 big-endian (rango 59–65535)
0x10          int negativo pequeño: [0x10][magnitud - 1] (rango -1..-30)
0x12          class: str_nombre + str_módulo + write_dedup(source)
0x15          str corta nueva (≤ 63 bytes UTF-8): [0x15][len][datos] — ahorra 1 byte
0x1C          instancia __dict__: class_header + serialize(__dict__)
0x1D          (interno) tag de source en tabla de dedup
0x1E          instancia __slots__: class_header + serialize(dict de slots)
0x1F          instancia __reduce__: callable_ref + args(0x09) + flags(1) + [state] + [list_items] + [dict_items]
0x20          función/lambda vía marshal: [marshal_dedup][n_def][defaults...][n_free][freevars...]
0x80          False
0x81          True
0x82          float +0.0
0x83          float -0.0
0x84          float 1.0
0x85          float -1.0
0x86          float NaN
0x87          float +inf
0x88          float -inf
0xC0–0xFA     int pequeño positivo (valor = opcode − 0xC0, rango 0–58)
0xFB          dedup long (nuevo): type_tag(1) + write_len + datos (> 63 bytes)
0xFC          referencia dedup: 4 bytes big-endian de índice
0xFD          referencia dedup: 2 bytes big-endian de índice
0xFE          referencia dedup: 1 byte de índice (índice ≤ 254)

⚠️ Limitaciones conocidas

  • Clases y callables de __reduce__ requieren source: inspect.getsource() debe poder acceder al código fuente al serializar. No funciona con clases definidas en el REPL. Las funciones normales y lambdas sí funcionan en el REPL vía marshal (opcode 0x20).
  • Funciones no portables entre versiones de Python: el bytecode serializado con marshal tiene un magic number ligado a la versión de CPython. Un stream serializado con Python 3.11 no puede deserializarse con Python 3.12.
  • Generadores objeto se consumen: serializar un objeto generador (g = gen_fn()) lo materializa en lista y consume el generador. Para serializar la función generadora, pasar gen_fn directamente.
  • No compatible con pickle: el formato binario es propio y no intercambiable con pickle, marshal u otros serializadores estándar de Python.
  • __reduce__ requiere callable con __name__: el callable devuelto por __reduce__ debe tener atributo __name__. Si no tiene source accesible se busca en builtins y módulos ya importados al deserializar; si tampoco se encuentra ahí, la deserialización falla con KeyError.

📄 Licencia

MIT — úsalo como quieras.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compickle-1.0.8.tar.gz (37.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

compickle-1.0.8-cp313-cp313-android_24_arm64_v8a.whl (75.6 kB view details)

Uploaded Android API level 24+ ARM64 v8aCPython 3.13

File details

Details for the file compickle-1.0.8.tar.gz.

File metadata

  • Download URL: compickle-1.0.8.tar.gz
  • Upload date:
  • Size: 37.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.33.1

File hashes

Hashes for compickle-1.0.8.tar.gz
Algorithm Hash digest
SHA256 0f6b04cd4b5230d4d989cba796cc13c7d2cadbf389eb0da2d86aa5a262253f59
MD5 096382cf0e9fe2542d781b3c52017c1e
BLAKE2b-256 e0c69a62d031922c7d8ffaa88e06a646f044d5d32caa08554cf4c2439d52d125

See more details on using hashes here.

File details

Details for the file compickle-1.0.8-cp313-cp313-android_24_arm64_v8a.whl.

File metadata

File hashes

Hashes for compickle-1.0.8-cp313-cp313-android_24_arm64_v8a.whl
Algorithm Hash digest
SHA256 03a19ffab1f057627e570b8e4c42ca801663aae6264e2bbf60de2eb61ce5f14c
MD5 f7965a9826272ff30bcaf882760b72ef
BLAKE2b-256 f82525ad3afd40812a7458a9b331d0550b31cc7ab21842134d865d75d6dbcd49

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page