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.
¿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=nativeaplicada automáticamente ensetup.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 adump,load,dumps,loads,dedup_resetobackend().
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):
__reduce__personalizado en la clase → tag0x1F__dict__disponible → tag0x1C__slots__sin__dict__→ tag0x1E
🔬 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 emite0x0E + longitud_1byte + datos(opcode de string corta, 2 bytes de cabecera). - Si
len > 63→ se emite0xFB + 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): mapeaid(objeto)→bytesdel source obtenido coninspect.getsource. Evita llamar ainspectmúltiples veces para el mismo objeto.exec_cache(dict): mapeasrc_bytes→namespace dictresultante dePyRun_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
dictPython{(type_tag, raw_bytes): idx}en lugar de la tabla C con FNV-1a. - La serialización acumula en un
bytearrayy retornabytes(buf). - La deserialización usa
memoryviewpara acceso sin copia. - Los closures se reconstruyen con
ctypesytypes.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 conexec(). 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 conpickle,marshalu 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file compickle-1.0.6.tar.gz.
File metadata
- Download URL: compickle-1.0.6.tar.gz
- Upload date:
- Size: 26.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.33.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f2dee5e303b59bdd2eaa971fba560179ea0dfdba542dbd184dc81c413dd4e88
|
|
| MD5 |
1d1c7a4727287fcd0776a648b4f20d6f
|
|
| BLAKE2b-256 |
187e23b8abf268500c8b721fed53d6743a0910fd035b60b8897fb73d6a18d16d
|
File details
Details for the file compickle-1.0.6-cp313-cp313-android_24_arm64_v8a.whl.
File metadata
- Download URL: compickle-1.0.6-cp313-cp313-android_24_arm64_v8a.whl
- Upload date:
- Size: 59.4 kB
- Tags: Android API level 24+ ARM64 v8a, CPython 3.13
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.33.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0733cb6527df2541c300db62c732ad9f792e18860b2e9ac777828844d1d8e035
|
|
| MD5 |
45578f64d08c9b032647e11bb50282e1
|
|
| BLAKE2b-256 |
9a9af618290d4fff89b41bf1d15ebe78a95c06581e6f4a460205bf6d81f0a6c0
|