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.
  • Una tabla de deduplicación FNV-1a dinámica (crece en potencias de 2 desde 8 192 entradas iniciales) con 16 384 buckets hash — O(1) amortizado, con colisiones resueltas por lista enlazada.
  • 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 dump, load, dumps, loads, dedup_reset o backend().

Versión actual: 1.0.3 (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'.
  • Resetea la tabla de deduplicación antes de cada serialización (vía _c_dedup_reset() en C o _reset_write_state() en Python).
compickle.dump(mi_objeto, "salida.cpkl")

compickle.dumps(obj) → bytes

Serializa obj y devuelve los bytes resultantes directamente.

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()

Resetea manualmente todas las tablas internas de deduplicación — tanto la del motor C como la del fallback Python — y también limpia el source_cache y exec_cache.

compickle.dedup_reset()

Útil en procesos de larga duración que serializan muchos objetos distintos en sesiones separadas, para evitar que la tabla FNV-1a se llene (capacidad máxima: 8 192 entradas).


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 (59–65535) 0x0F 3 bytes: tag + uint16 big-endian
int (arbitrario) 0x02 signo + longitud variable + bytes big-endian
float 0x03 IEEE 754 doble precisión, 8 bytes big-endian
complex 0x04 Dos float64 big-endian (16 bytes total)
str 0x05 UTF-8 + dedup FNV-1a
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() (Python) / PySequence_List (C)
frozenset 0x0B Ordenado por repr() (Python) / PySequence_List (C)
dict 0x0C Claves y valores serializados recursivamente
function (simple) 0x0D ✅ (source) Nombre UTF-8 + código fuente vía inspect.getsource
function (closure) 0x1E ✅ (source) Fuente + dict de variables capturadas (co_freevars)
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 Prioridad máxima; soporta tuple de 2 o 3 elementos

Prioridad de serialización de instancias (C y Python coinciden):

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

🔬 Cómo funciona internamente

Motor C (compickle.c, ~898 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 cada vez que el espacio disponible es insuficiente. Las funciones primitivas son:

buf_grow  → realloc en potencias de 2
buf_u8    → escribe 1 byte
buf_u16be → escribe uint16 big-endian (2 bytes)
buf_u64be → escribe uint64 big-endian (8 bytes)
buf_raw   → escribe N bytes con memcpy

Tabla de deduplicación

#define DEDUP_CAP    8192
#define HASH_BUCKETS 16384

typedef struct { uint8_t tag; uint8_t *data; Py_ssize_t len; int next; } DEntry;
static DEntry dedup_table[DEDUP_CAP];
static int    hash_buckets[HASH_BUCKETS];

El hash usado es FNV-1a 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. La función dedup_reset() libera la memoria de cada entrada y hace memset(-1) sobre los buckets.

Cuando se registra un dato nuevo:

  • Si len ≤ 63 → se emite 0x0E + longitud_1byte + datos (opcode de string corta, 2 bytes de cabecera).
  • Si len > 63 → se emite 0xFB + type_tag + write_len(n) + datos.

Cuando se detecta duplicado → se emite una referencia compacta:

idx ≤ 0xFF   → 0xFE + 1 byte de índice  (2 bytes total)
idx ≤ 0xFFFF → 0xFD + 2 bytes big-endian (3 bytes total)
idx > 0xFFFF → 0xFC + 4 bytes big-endian (5 bytes total)

Codificación de longitudes variable

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

Caché de fuentes y ejecución

  • source_cache (dict): mapea id(objeto)bytes del source obtenido con inspect.getsource. Evita llamar a inspect múltiples veces para el mismo objeto.
  • exec_cache (dict): mapea src_bytesnamespace dict resultante de PyRun_String. Evita re-ejecutar el mismo source al deserializar.

Detección de __reduce__ personalizado

La función has_custom_reduce() recorre el MRO del tipo del objeto, saltando object (PyBaseObject_Type), y busca __reduce__ directamente en el __dict__ de cada clase base. Esto evita falsos positivos por el __reduce__ heredado de object.

Enteros arbitrarios — compatibilidad 3.13+

La API privada _PyLong_NumBits fue eliminada en Python 3.13. El motor C usa bit_length() vía PyObject_CallMethod para calcular el número de bytes necesarios, manteniendo compatibilidad con Python 3.9–3.13+.


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

Implementa exactamente el mismo protocolo binario en Python puro. Las diferencias internas son:

  • La deduplicación usa un dict Python {(type_tag, raw_bytes): idx} en lugar de la tabla C con FNV-1a.
  • La serialización acumula en un bytearray y retorna bytes(buf).
  • La deserialización usa memoryview para acceso sin copia.
  • Los closures se reconstruyen con ctypes y types.FunctionType, replicando las celdas de cierre.

El fallback Python también expone dumps() y loads() directamente, además de dump() y load() con archivo.


Lazy loading (__init__.py, ~96 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

La función _ensure_loaded() se llama en la primera invocación de cualquier función pública. Esto significa que un import compickle nunca falla aunque la extensión C no esté compilada — el fallback Python se activa transparentemente.

El módulo __init__.py también define __getattr__ para resolver _USE_C, serialize_fast y deserialize_fast bajo demanda.


🏗️ Estructura del proyecto

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

🧪 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 incluyendo su código fuente completo (vía inspect.getsource). Al deserializar, el source se ejecuta en un namespace aislado y se recupera la clase 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 una tupla de 2 elementos (callable, args) o de 3 (callable, args, state). El estado opcional se deserializa y aplica vía __dict__.update() o setattr().


Serialización en memoria (dumps / loads)

import compickle

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

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

Uso de deduplicación manual

import compickle

# Resetear antes de una sesión nueva
compickle.dedup_reset()

# Serializar múltiples objetos que comparten strings
for i in range(100):
    compickle.dumps({"tipo": "evento", "id": i, "fuente": "sensor_A"})

# Las strings "tipo", "evento", "fuente", "sensor_A" se deduplicarán
# en la primera aparición y se referenciarán con 2–5 bytes en las siguientes.

📦 Formato binario — tabla completa de opcodes

Opcode      Tipo / Significado
─────────────────────────────────────────────────────────────────────
0x00        None
0x02        int arbitrario: signo(1) + write_len + bytes big-endian
0x03        float: 8 bytes IEEE 754 big-endian
0x04        complex: 2 × float64 big-endian (16 bytes)
0x05        str: tag + write_dedup(UTF-8)
0x06        bytes: tag + write_dedup(contenido)
0x07        bytearray: tag + write_dedup(contenido)
0x08        list: write_len(n) + n × serialize(item)
0x09        tuple: write_len(n) + n × serialize(item)
0x0A        set: write_len(n) + n × serialize(item)
0x0B        frozenset: write_len(n) + n × serialize(item)
0x0C        dict: write_len(n) + n × (serialize(k) + serialize(v))
0x0D        function: 0x05+nombre + write_dedup(source)
0x0E        dedup short: longitud(1 byte) + datos (≤ 63 bytes, nueva entrada)
0x0F        int 16 bits: 2 bytes uint16 big-endian (rango 59–65535)
0x12        class: 0x05+nombre + 0x05+módulo + write_dedup(source)
0x1C        instancia __dict__: class_header + 0x0C + dict
0x1D        (interno) tag de source en tabla de dedup
0x1E        instancia __slots__ / closure: class_header + 0x0C + dict de slots
0x1F        instancia __reduce__: callable_ref + args(0x09) + flags(1) + [state] + [list_items] + [dict_items]
0x80        False
0x81        True
0xC0–0xFA   int pequeño (valor = opcode − 0xC0, rango 0–58)
0xFB        dedup long: type_tag(1) + write_len + datos (nueva entrada, > 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 ≤ 255)

⚠️ Limitaciones conocidas

  • Funciones y clases requieren source: inspect.getsource() debe poder acceder al código fuente en tiempo de serialización. No funciona con lambdas anónimas, funciones definidas en el REPL interactivo o código generado dinámicamente con exec(). El callable usado en __reduce__ sí tiene fallback a builtins si no se encuentra su fuente.
  • 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__ accesible. Callables arbitrarios sin nombre (por ejemplo, instancias de clases con __call__) no están soportados.

📄 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.7.tar.gz (28.9 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.7-cp313-cp313-android_24_arm64_v8a.whl (64.2 kB view details)

Uploaded Android API level 24+ ARM64 v8aCPython 3.13

File details

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

File metadata

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

File hashes

Hashes for compickle-1.0.7.tar.gz
Algorithm Hash digest
SHA256 8ee2993dabc250e9a6b3620d69130c806b37bed62428541d4b997cb9be1c51c6
MD5 0953bca230596091e1cc9278e2fa2516
BLAKE2b-256 c1ed5e07f7dcb3fc0704a4abef17fdea295066238e9760490f34a4505a97c3b2

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for compickle-1.0.7-cp313-cp313-android_24_arm64_v8a.whl
Algorithm Hash digest
SHA256 20318be8391deaa9fe8bd6e0631b4096608020afc3b1a2db4941b86586cf5681
MD5 f6777f8a85d3c63777f22c7607406f09
BLAKE2b-256 17b57454e7aa18827f9e831a38c530f7d951f0eb1b3572926f14396b041ca589

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