Skip to main content

Extensiones y utilidades de alto rendimiento para Python.

Project description

fastcorex

Extensión en C para acelerar loops y estructuras de datos comunes en Python. No es un reemplazo de NumPy ni Pandas — es una capa delgada que elimina las partes tediosas y repetitivas (agrupar, deduplicar, aplanar, contar, filtrar, particionar, fusionar dicts, generar slugs, acceso anidado seguro) que normalmente se reescriben a mano en cada proyecto.

Instalación

pip install fastcorex

Requiere Python 3.9 o superior y un compilador de C (se compila al instalar, como cualquier extensión nativa).

Uso rápido

import fastcorex as fx

fx.fast_sum([1, 2, 3])                      # 6.0
fx.count_freq(["a", "b", "a"])              # {'a': 2, 'b': 1}
fx.unique([3, 1, 2, 1, 3])                  # [3, 1, 2]
fx.filter_gt([1, 5, 10, 3], 4.0)            # [5.0, 10.0]
fx.groupby(lista_de_dicts, "categoria")     # dict agrupado
fx.flatten([1, [2, [3, 4]], 5])             # [1, 2, 3, 4, 5]
fx.unique_by(lista_de_dicts, "id")          # FastList dedup por campo (encadenable)
fx.chunk([1, 2, 3, 4, 5], 2)                # [[1, 2], [3, 4], [5]]
fx.safe_get(d, "a.b.c", default=None)       # acceso anidado sin try/except
fx.clamp(15.0, 0.0, 10.0)                   # 10.0
fx.pick({"a": 1, "b": 2, "c": 3}, ["a"])    # {'a': 1}
fx.omit({"a": 1, "b": 2, "c": 3}, ["a"])    # {'b': 2, 'c': 3}
fx.deep_merge(config_base, config_local)    # dict fusionado recursivamente
fx.slugify("Título con Ñandú")              # "titulo-con-nandu"
fx.partition(numeros, lambda n: n > 0)      # (positivos, no_positivos)
fx.dedupe_consecutive([1, 1, 2, 2, 1])      # [1, 2, 1]
fx.ensure_list(5)                           # [5]
fx.first_or_default(lista, predicado)       # primer match o default

Documentación de funciones

Funciones de la versión 0.1.x

fast_sum(lista) — Suma todos los elementos numéricos de una lista.

fx.fast_sum([1, 2, 3, 4.5])  # 10.5

count_freq(lista) — Cuenta cuántas veces aparece cada elemento. Reemplaza un loop de 4 líneas con dict.get() por una sola llamada.

fx.count_freq(["a", "b", "a", "c", "b", "a"])  # {'a': 3, 'b': 2, 'c': 1}

unique(lista) — Elimina duplicados manteniendo el orden original. Reemplaza el patrón de set() + loop + append.

fx.unique([3, 1, 2, 1, 3, 4])  # [3, 1, 2, 4]

filter_gt(lista, umbral) — Devuelve solo los elementos mayores al umbral dado.

fx.filter_gt([1, 5, 10, 3, 8], 4.0)  # [5.0, 10.0, 8.0]

groupby(lista_de_dicts, clave) — Agrupa una lista de diccionarios según el valor de una clave. Devuelve un dict normal. Reemplaza el patrón de dict + setdefault manual.

fx.groupby(ventas, "categoria")  # {'ropa': [...], 'comida': [...]}

flatten(lista_anidada) — Aplana listas anidadas de cualquier profundidad. Reemplaza una función recursiva escrita a mano.

fx.flatten([1, [2, 3, [4, [5, 6]], 7], 8])  # [1, 2, 3, 4, 5, 6, 7, 8]

unique_by(lista_de_dicts, clave) — Deduplica diccionarios según el valor de un campo específico, no el objeto completo. Devuelve un FastList (ver sección de encadenamiento).

fx.unique_by(ventas, "id")  # FastList sin ids repetidos

chunk(lista, tamaño) — Parte una lista en sublistas de tamaño fijo; el último chunk puede quedar más corto. Reemplaza el slicing manual con range(0, len(lista), tamaño).

fx.chunk([1, 2, 3, 4, 5, 6, 7], 3)  # [[1, 2, 3], [4, 5, 6], [7]]

safe_get(dict, "a.b.c", default=None) — Acceso anidado seguro a diccionarios usando un path con puntos. Reemplaza el try/except (KeyError, TypeError) que normalmente envuelve un acceso encadenado. default puede pasarse posicional o como keyword.

fx.safe_get({"a": {"b": {"c": 42}}}, "a.b.c")            # 42
fx.safe_get({"a": {"b": {}}}, "a.b.c", default="N/A")    # "N/A"

Un path vacío, o con un segmento vacío ("a..b", ".a", "a."), lanza ValueError en vez de devolver silenciosamente el default — un path malformado suele ser un bug en quien llama, no un caso de "no encontrado".

clamp(valor, minimo, maximo) — Acota un número al rango [minimo, maximo]. Reemplaza max(minimo, min(valor, maximo)) o un if/elif/else.

fx.clamp(15.0, 0.0, 10.0)  # 10.0
fx.clamp(-5.0, 0.0, 10.0)  # 0.0

Funciones nuevas (0.2.0)

pick(dict, claves) — Devuelve un nuevo dict con solo las claves indicadas que existan en el original; las ausentes se ignoran sin error. Reemplaza {k: d[k] for k in claves if k in d}.

fx.pick({"nombre": "Ana", "edad": 30, "email": "a@x.com"}, ["nombre", "email"])
# {'nombre': 'Ana', 'email': 'a@x.com'}

Útil para serializar solo un subconjunto de campos hacia una API o un log, sin exponer el resto del objeto.

omit(dict, claves) — Lo inverso de pick: devuelve un nuevo dict sin las claves indicadas. Reemplaza {k: v for k, v in d.items() if k not in claves}.

fx.omit({"nombre": "Ana", "password": "secreta", "edad": 30}, ["password"])
# {'nombre': 'Ana', 'edad': 30}

Útil para el caso contrario a pick: quitar uno o dos campos sensibles de un dict grande sin tener que enumerar todos los que sí quedan.

deep_merge(base, override) — Fusiona override sobre base recursivamente: cuando ambos tienen un dict en la misma clave, se fusionan sus contenidos en vez de que uno reemplace al otro; en cualquier otro caso, override gana. Ninguno de los dos argumentos originales se modifica.

config_base = {"db": {"host": "localhost", "port": 5432}, "debug": False}
config_local = {"db": {"port": 5433}}
fx.deep_merge(config_base, config_local)
# {'db': {'host': 'localhost', 'port': 5433}, 'debug': False}

Pensado para el caso típico de fusionar un archivo de configuración base con overrides de entorno o de usuario, sin perder las claves del base que el override no toca.

slugify(texto) — Normaliza un string a minúsculas, sin acentos (cubre á é í ó ú ü ñ ç y sus mayúsculas), con guiones en vez de espacios o símbolos, sin guiones duplicados ni al inicio/final. Reemplaza normalizar texto a mano con .lower() + reemplazos de acentos + regex de limpieza.

fx.slugify("Título de Sección: ¡Importante!")  # "titulo-de-seccion-importante"
fx.slugify("  Café & Té  ")                     # "cafe-te"

No es un normalizador Unicode completo (para eso está unicodedata en la librería estándar); cubre el caso real de generar slugs legibles para URLs o nombres de archivo a partir de texto en español y similares.

partition(lista, predicado) — Recorre la lista una sola vez y la separa en (cumplen, no_cumplen) según predicado(item), en vez de hacer dos pasadas (filter + filter con la condición negada). Devuelve una tupla de dos FastList.

pares, impares = fx.partition(range(10), lambda n: n % 2 == 0)
# ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])

dedupe_consecutive(lista) — Colapsa elementos repetidos que aparecen uno justo después del otro. A diferencia de unique(), no deduplica globalmente: dedupe_consecutive([1, 2, 1]) deja [1, 2, 1] intacto, porque el segundo 1 no es consecutivo con el primero. Pensado para logs, lecturas de sensores o streams donde importa el valor inmediatamente anterior, no todo el historial.

fx.dedupe_consecutive([1, 1, 2, 2, 2, 1, 3, 3])  # [1, 2, 1, 3]
fx.dedupe_consecutive(["OK", "OK", "ERROR", "OK"])  # ['OK', 'ERROR', 'OK']

Utilidades en Python puro (0.2.0)

Estas dos funciones se implementaron directamente en Python, no en C, porque su costo ya es mínimo en el intérprete (una rama de isinstance en un caso, delegar a next()/iter() en el otro) y una extensión de C no traería ninguna ganancia medible — solo complejidad extra de mantenimiento.

ensure_list(valor) — Envuelve valor en una lista si no es ya una lista o tupla. Reemplaza el patrón de normalizar un parámetro que a veces llega como valor único y a veces como colección.

fx.ensure_list(5)          # [5]
fx.ensure_list([1, 2, 3])  # [1, 2, 3]
fx.ensure_list(None)       # [None]

first_or_default(iterable, predicado=None, default=None) — Devuelve el primer elemento que cumple predicado, o default si ninguno cumple (o el iterable está vacío), sin lanzar StopIteration. Funciona con cualquier iterable, incluidos generadores, y se detiene apenas encuentra la primera coincidencia.

fx.first_or_default([1, 2, 3, 4], lambda x: x > 2)  # 3
fx.first_or_default([], default="vacío")             # "vacío"

FastList: encadenamiento de métodos

unique_by(), partition() y algunas otras funciones devuelven un FastList: un subtipo de list que se comporta como una lista normal (indexable, iterable, con len(), slicing, etc.) pero además expone métodos propios para seguir encadenando sin volver a pasar por funciones sueltas del módulo.

Métodos disponibles en FastList:

  • .groupby(clave) — igual que fx.groupby(), pero devuelve otro FastList (de pares [clave, sublista]) en vez de un dict, para poder seguir encadenando.
  • .sum(campo=None) — sin argumento, suma los elementos como números. Con argumento, asume que el FastList viene de un .groupby() y devuelve un dict {clave: suma_del_campo}.
  • .count() — asume que el FastList viene de un .groupby() y devuelve un dict {clave: cantidad}.
  • .filter(predicado) — devuelve un FastList con los elementos donde predicado(item) es verdadero.
  • .map(función) — devuelve un FastList con función(item) aplicada a cada elemento.
  • .unique_by(clave) — igual que fx.unique_by(), encadenable.
  • .flatten() — igual que fx.flatten(), encadenable.
  • .chunk(tamaño) — igual que fx.chunk(), encadenable.
  • .partition(predicado) — igual que fx.partition(), devuelve una tupla de dos FastList.
  • .dedupe_consecutive() — igual que fx.dedupe_consecutive(), encadenable.
ventas_unicas = fx.unique_by(ventas, "id")
totales = ventas_unicas.groupby("categoria").sum("monto")
# {'ropa': 240.0, 'comida': 30.0, 'tech': 500.0}

conteos = ventas_unicas.groupby("categoria").count()
# {'ropa': 2, 'comida': 1, 'tech': 1}

También se puede construir un FastList directamente para tener esta API encadenada desde el principio, combinando cualquier secuencia de métodos:

resultado = (
    fx.FastList(eventos)
    .filter(lambda e: e["nivel"] == "ERROR")
    .unique_by("id")
    .groupby("servicio")
    .count()
)
# Limpieza de datos: normalizar, quitar vacíos, colapsar repetidos consecutivos
resultado = (
    fx.FastList(lecturas_sensor)
    .map(str.lower)
    .filter(lambda s: s != "")
    .dedupe_consecutive()
)

Nota: .sum(campo) y .count() esperan específicamente el formato que produce .groupby() (una lista de pares [clave, sublista]); si se les pasa otra cosa, lanzan TypeError con un mensaje explicando qué esperaban.

Ejemplo real combinando varias funciones

Sin fastcorex (19 líneas): un loop para deduplicar por id con set() + seen, otro loop para agrupar por categoría con dict + setdefault, y un tercer loop anidado para sumar montos por grupo.

Con fastcorex, dos formas equivalentes:

Con funciones sueltas (4 líneas):

ventas_unicas = fx.unique_by(ventas, "id")
grupos = fx.groupby(ventas_unicas, "categoria")
totales = {cat: fx.fast_sum([i["monto"] for i in items]) for cat, items in grupos.items()}

Con encadenamiento de FastList (2 líneas):

ventas_unicas = fx.unique_by(ventas, "id")
totales = ventas_unicas.groupby("categoria").sum("monto")

Ambas versiones dan el mismo resultado: {'ropa': 240.0, 'comida': 30.0, 'tech': 500.0}

Resultados de benchmark

Medido con listas de 200,000 y 2,000,000 de elementos, mejor tiempo de 5 corridas (ver benchmark.py en el repositorio para reproducirlo). Datos generados aleatoriamente por función (números, strings de un alfabeto de 50 valores, dicts con ~50% de ids duplicados para forzar deduplicación real en unique_by, texto con acentos para slugify, corridas de repetidos para dedupe_consecutive).

Con 200,000 elementos: fast_sum 2.6x más rápido, count_freq 1.5x, unique 1.7x, filter_gt 2.1x, groupby 2.4x, flatten 11.0x, unique_by 1.2x, chunk 1.3x, safe_get 1.1x, clamp 1.9x, pick 1.8x, omit 5.8x, deep_merge 13.9x, slugify 34.6x, dedupe_consecutive 2.2x, pipeline encadenado completo (unique_by → groupby → sum) 1.7x. partition: 0.53x — más lento que Python puro.

Con 2,000,000 elementos: fast_sum 1.8x, count_freq 1.4x, unique 1.5x, filter_gt 1.6x, groupby 1.9x, flatten 8.4x, unique_by 1.3x, chunk 1.1x, safe_get 1.1x, clamp 2.0x, pick 1.6x, omit 5.9x, deep_merge 14.2x, slugify 34.9x, dedupe_consecutive 1.9x, pipeline encadenado 1.7x. partition: 0.56x — más lento que Python puro.

deep_merge y slugify tienen las mejores ganancias entre las funciones nuevas porque hacen trabajo genuinamente pesado en C sin depender de callbacks a Python: deep_merge evita el copy.deepcopy completo de Python (que recorre y copia recursivamente todo el árbol) y solo copia los dicts que realmente cambian; slugify reemplaza un .translate() + comprehension + limpieza de guiones por un único paso sobre los bytes, sin crear objetos string intermedios por cada substitución. omit gana bien porque evita construir un dict.items() view y el filtrado elemento por elemento en bytecode de Python. pick gana menos que omit porque su costo ya está dominado por las búsquedas en el dict origen (PyDict_GetItemWithError), que en ambas versiones usan la misma implementación en C de CPython por debajo.

partition es la excepción real, igual que safe_get lo era en 0.1.1 antes de esta versión: es consistentemente ~45-47% más lento que el equivalente en Python. La razón no es un descuido de implementación sino estructural: partition invoca el predicado de Python una vez por elemento vía PyObject_CallFunctionObjArgs, y ese cruce Python→C→Python en cada iteración cuesta más de lo que se ahorra teniendo el loop externo en C. Se confirmó directamente: usando una función builtin de C como predicado (bool) en vez de una lambda de Python, partition iguala la velocidad de filter_gt (que no usa callback); el cuello de botella está en el lado Python del callback, no en el loop de C. Lo mismo aplica a .filter() y .map() de FastList, y por eso el pipeline nuevo filter → map → dedupe_consecutive también da un ratio menor a 1x (0.73x-0.74x) cuando se lo compara contra su equivalente en Python puro: dos de sus tres pasos pagan ese mismo costo de callback. Si la velocidad de estos métodos importa en un caso puntual, conviene o bien usar filter_gt/dedupe_consecutive solos (que no necesitan callback), o aceptar que la ganancia real de partition/filter/map no es de velocidad sino de expresividad y de mantener todo en una sola cadena legible.

flatten sigue teniendo una de las mayores ganancias porque en Python puro depende de recursión con overhead de llamadas a función, que en C es casi gratis. groupby gana bien porque evita el overhead del bytecode en el loop principal. Las funciones que ya dependen de dict/set de Python (count_freq, unique, unique_by, fast_sum) ganan menos, porque esas estructuras ya están optimizadas en C por debajo del intérprete — el beneficio ahí es sobre todo la reducción de líneas de código, no la velocidad. chunk gana relativamente poco porque el slicing de listas en Python ya es una operación en C bien optimizada.

safe_get ya no es una excepción negativa. En la versión 0.1.1 era ~25% más lento que Python puro porque hacía strdup() + strtok() sobre el path en cada llamada (con su malloc/free correspondiente) y usaba PyDict_GetItemString, que construye un objeto string de Python por cada segmento. La implementación 0.2.0 elimina el strdup inicial y recorre el path original con punteros crudos, sin duplicarlo; sigue creando un PyUnicode por segmento porque PyDict_GetItem necesita un objeto Python como clave, pero ya no hay malloc/free adicional para el path completo. El resultado pasó de 0.75x-0.77x (más lento) a un consistente ~1.1x-1.4x más rápido que Python puro, dependiendo del tamaño del path y de cuánto se repita la llamada.

Licencia

MIT License. Ver el archivo LICENSE para el texto completo.

Estado del proyecto

Versión 0.2.0. Cubre patrones comunes de listas, diccionarios y strings (agrupar, deduplicar, aplanar, contar, filtrar, particionar, fusionar, generar slugs, acceso anidado, acotar rangos) más un tipo FastList que permite encadenar diez operaciones distintas (groupby, sum, count, filter, map, unique_by, flatten, chunk, partition, dedupe_consecutive) sin pasar por dicts intermedios. No pretende reemplazar NumPy para cómputo numérico ni Pandas para análisis de datos tabulares — está enfocado en el trabajo genérico con listas, dicts y strings que esas librerías no cubren directamente.

Cubierto por pruebas automatizadas (tests/, ejecutables con python -m unittest discover -s tests o con pytest tests/): funciones originales de 0.1.1 (regresión), las seis utilidades nuevas de 0.2.0, y encadenamientos largos de FastList combinando filter, map, unique_by, groupby, dedupe_consecutive y flatten en una sola expresión.

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

fastcorex-0.2.0.tar.gz (23.4 kB view details)

Uploaded Source

Built Distribution

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

fastcorex-0.2.0-cp314-cp314-android_24_arm64_v8a.whl (40.2 kB view details)

Uploaded Android API level 24+ ARM64 v8aCPython 3.14

File details

Details for the file fastcorex-0.2.0.tar.gz.

File metadata

  • Download URL: fastcorex-0.2.0.tar.gz
  • Upload date:
  • Size: 23.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.34.2

File hashes

Hashes for fastcorex-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d2449b7e514b18dc5384895f2560bb45711ed33b9fcc64938e98705f82acd1f1
MD5 b115a867e0123b5039790bfca99f546d
BLAKE2b-256 64a7473b86cad308741e2c3fa8b15a464503cd4e497c30ff9dd5ddc7482594df

See more details on using hashes here.

File details

Details for the file fastcorex-0.2.0-cp314-cp314-android_24_arm64_v8a.whl.

File metadata

File hashes

Hashes for fastcorex-0.2.0-cp314-cp314-android_24_arm64_v8a.whl
Algorithm Hash digest
SHA256 e2f3ad642ffc7336b0d7512000ff3ed7f4e2b00a8a4abbc9aa837d3aed5917ae
MD5 344ec6656b0121f196e1d5931d187f59
BLAKE2b-256 4fd6cab7b560bbfa006ed192e560ae3c021040b50730e1ff042d5298ed6bd749

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