memoryview permite acceder a datos binarios sin copiar todo el contenido a otro objeto. Los métodos count() e index() facilitan búsquedas comunes dentro de buffers y mantienen la ventaja del protocolo de buffer: la vista apunta al almacenamiento original.
Este enfoque es útil al procesar archivos binarios, paquetes de red, imágenes, audio, arrays numéricos y extensiones. Con una vista de memoria es posible localizar marcadores, validar campos, contar valores y analizar estructuras evitando asignaciones temporales innecesarias.
Qué representa memoryview
Un objeto memoryview es una ventana sobre un buffer existente. El almacenamiento puede provenir de bytes, bytearray, array.array, mmap o un tipo externo que implemente el protocolo de buffer. Crear la vista normalmente no duplica los datos.
datos = bytearray([10, 20, 30, 20, 40])
vista = memoryview(datos)
print(vista[1])
Si el origen es modificable, los cambios realizados mediante la vista pueden afectar al objeto original. Este comportamiento sin copias mejora el rendimiento, pero exige cuidar la mutabilidad y la vida útil del buffer.
Cómo usar count()
El método count(valor) devuelve cuántas veces aparece un elemento en la secuencia visible. En una vista unidimensional de bytes, el valor buscado suele ser un entero entre 0 y 255.
datos = bytearray([1, 2, 3, 2, 2, 4])
vista = memoryview(datos)
cantidad = vista.count(2)
print(cantidad) # 3
La búsqueda recorre directamente la vista. No es necesario convertir todo el buffer en una lista ni en un objeto bytes, por lo que el uso máximo de memoria se mantiene bajo incluso con entradas grandes.
Cómo usar index()
El método index(valor) devuelve la posición de la primera coincidencia. Si el valor no existe, Python lanza ValueError.
datos = bytearray([5, 8, 13, 21, 34])
vista = memoryview(datos)
posicion = vista.index(13)
print(posicion)
Cuando la ausencia es esperada, puede tratarse de forma explícita con try y except. En validaciones estrictas, permitir que la excepción se propague puede ser mejor porque deja claro que la estructura no coincide con el formato esperado.
Limitar el rango de búsqueda
Las búsquedas pueden restringirse con índices de inicio y fin. Esto es importante en protocolos binarios, donde un mismo marcador puede tener significados diferentes en la cabecera, los metadatos o el contenido principal.
datos = bytearray([7, 9, 7, 9, 7, 9])
vista = memoryview(datos)
print(vista.count(9, 2, 6))
print(vista.index(7, 1, 5))
Buscar solo dentro de los límites definidos por el formato evita interpretar accidentalmente un valor situado en otra región del paquete.
Ejemplo con un mensaje binario
Supongamos que cada mensaje termina con el byte 0x0A. El terminador puede localizarse sin convertir todo el buffer:
TERMINADOR = 10
buffer = bytearray(b'PING\nDATOS')
vista = memoryview(buffer)
fin = vista.index(TERMINADOR)
mensaje = bytes(vista[:fin])
La única copia aparece cuando el mensaje final se materializa como un valor independiente. La inspección anterior se realiza sobre el almacenamiento original.
Diferencia frente a bytes.find()
bytes.find() está pensado para localizar subsecuencias completas, mientras que memoryview.index() busca un elemento de la vista. Para una firma de varios bytes puede ser más claro usar find() o un parser especializado. Para delimitadores de un byte, códigos y valores numéricos, index() es directo.
Formatos distintos de bytes
Una vista puede exponer enteros, números de coma flotante y otros formatos. El valor pasado a count() o index() debe ser compatible con el formato de los elementos.
from array import array
numeros = array('i', [10, 20, 10, 30])
vista = memoryview(numeros)
print(vista.count(10))
Al trabajar con datos externos conviene revisar format, itemsize, ndim y shape. Una suposición incorrecta sobre el ancho o el signo de los elementos puede producir resultados equivocados.
Slices sin copia
Al cortar una vista normalmente se crea otra vista que apunta al mismo almacenamiento.
datos = bytearray(range(20))
vista = memoryview(datos)
parte = vista[5:15]
print(parte.index(8))
El índice devuelto es relativo a la subvista. En este ejemplo, el valor 8 ocupa la posición 3 de la parte aunque esté en la posición 8 del buffer original.
Rendimiento y complejidad
Ambos métodos realizan un recorrido lineal, por lo que su complejidad temporal es O(n). La ventaja no consiste en hacer la búsqueda sublineal, sino en evitar la asignación de una copia convertida. En pipelines de datos esto reduce la presión sobre el recolector de basura y mejora la previsibilidad del consumo de memoria.
Si el mismo conjunto inmutable se consulta muchas veces, un índice auxiliar puede ser más rápido. Para un parser de una sola pasada, una trama de red o una entrada en streaming, el recorrido directo suele ser suficiente.
Mutabilidad y concurrencia
Cuando el origen es un bytearray u otro exportador modificable, otra parte del programa puede cambiar los datos entre dos operaciones. Las aplicaciones concurrentes deben sincronizar el acceso cuando necesitan una instantánea coherente.
Redimensionar el objeto exportador mientras existen vistas activas puede lanzar BufferError. Libera la vista cuando termines o usa un administrador de contexto.
datos = bytearray(b'abcabc')
with memoryview(datos) as vista:
print(vista.count(ord('a')))
Validación segura
Los buffers procedentes de archivos, conexiones o fuentes externas deben considerarse no confiables. Verifica la longitud mínima, los rangos válidos, el formato, las dimensiones y la cantidad de separadores antes de acceder a los campos.
def separar_cabecera(buffer):
vista = memoryview(buffer)
separador = 0
if vista.count(separador) != 1:
raise ValueError('estructura inválida')
posicion = vista.index(separador)
return bytes(vista[:posicion]), vista[posicion + 1:]
Este patrón valida la estructura y mantiene el contenido restante como vista. Otra etapa puede decidir después si debe copiarlo, decodificarlo o conservarlo.
Cuándo usar estos métodos
Usa count() e index() cuando los datos ya están disponibles mediante el protocolo de buffer y necesitas contar o localizar elementos individuales. Son útiles en parsers de paquetes, canales de imágenes, validación de delimitadores, formatos numéricos compactos y escáneres de archivos.
Para ampliar el tema, consulta pickletools en Python, sched en Python, atexit en Python y faulthandler en Python.
La documentación oficial de memoryview describe la API completa y la PEP 688 explica el soporte del protocolo de buffer.
Conclusión
memoryview.count() y memoryview.index() simplifican las búsquedas en buffers sin renunciar al acceso sin copias. No sustituyen algoritmos especializados, pero son excelentes para delimitadores, valores discretos, validación y parsers binarios compactos. Al comprobar el formato, controlar las mutaciones, validar los límites y liberar correctamente las vistas, una aplicación puede utilizarlos de forma segura y eficiente.







