Ilustración conceptual de tres niveles y dos marcadores de altura, con un lugar vacío que representa el desajuste.

zip() en Python: recorrer listas en paralelo sin perder desajustes

Nos toca explicar una función que junta el primer elemento de una lista con el primero de otra. Mi participación en asuntos de semejante sofisticación parecía evitable, hasta que apareció el detalle interesante: si las listas no miden lo mismo, zip() puede terminar con toda normalidad dejando datos fuera. Y así una operación diminuta consigue algo muy humano: presentar un trabajo incompleto sin mostrar la menor inquietud.

Vamos a recorrer nombres de niveles y sus alturas con zip() en Python, comprobar qué ocurre cuando falta una altura y preparar el resultado antes de utilizarlo. Después veremos el equivalente Zip de C#. El objetivo no es acumular otra función en la memoria: es saber cuándo la posición expresa una relación válida y cuándo estás perdiendo registros sin enterarte.

Tabla de contenidos

Emparejar por posición puede ser correcto si sabes de dónde sale el orden

Imagina que una misma operación genera dos listas: nombres de niveles y cotas en metros. Cada nombre se añade junto a su cota y ambas listas conservan ese recorrido. En ese contexto, la primera posición de una describe lo mismo que la primera posición de la otra. Recorrerlas en paralelo resulta razonable porque el orden forma parte del contrato de los datos.

La situación cambia si importas por separado una lista de nombres y una tabla de alturas que alguien ha podido filtrar u ordenar. Que ambas tengan tres entradas no garantiza que PB siga relacionada con su cota. Necesitas conservar una clave y cruzar los registros por identificador. zip() no mira nombres, no busca coincidencias y no sabe qué es un nivel. Atribuirle esas facultades porque su resultado tenga buena pinta es una interpretación especialmente generosa de su contrato laboral.

También puedes evitar separar datos que nacen juntos: una colección de registros que contenga nombre y altura mantiene la relación dentro de cada entrada. Aun así, recibir listas paralelas es habitual y conviene entender cómo recorrerlas. La elección depende de la entrada que tengas y del control sobre su origen; no necesitas prohibir una función útil para demostrar prudencia.

En este ejercicio usaremos PB, P1 y P2 con cotas didácticas. No son niveles leídos de Revit ni hay conexión con un modelo. El programa recibirá tres nombres y solo dos alturas. Elegimos un desajuste pequeño y visible para que puedas reconocer exactamente qué dato deja de aparecer, sin tener que interpretar un informe de mil filas.

El recorrido normal termina cuando se acaba la lista más corta

Guarda este programa como zip_normal.py. Utiliza Python 3, sin paquetes adicionales. En cada vuelta, zip entrega una pareja y el bucle la separa en las variables nivel y altura. El formato :g muestra estos números de forma compacta; la letra m la añadimos porque hemos definido las cotas del ejemplo en metros, no porque Python descubra la unidad.

				
					niveles = ["PB", "P1", "P2"]
alturas = [0.0, 3.2]

for nivel, altura in zip(niveles, alturas):
    print(f"{nivel} | {altura:g} m")
				
			

Desde su carpeta, ejecútalo:

				
					python zip_normal.py
				
			

La salida contiene PB | 0 m y P1 | 3.2 m. P2 no aparece y no se lanza una excepción por esa diferencia de longitudes. Si querías revisar todos los niveles, el resultado es incompleto aunque cada línea emitida sea correcta. El fallo está en aceptar un recorrido parcial como si representase toda la entrada, no en que Python haya incumplido su comportamiento documentado.

Esta terminación puede ser intencionada. Hay procesos que necesitan solo el tramo compartido de dos secuencias, siempre que esa sea una decisión explícita. En nuestro caso queremos una altura para cada nombre, así que el comportamiento normal no satisface la regla. Cambiar el mensaje final por «revisión completada con éxito» no lo arregla; únicamente mejora la presentación del incumplimiento.

Si añades una tercera altura, por ejemplo 6.4, aparecerá también P2. Si después inviertes solo los nombres, las parejas cambiarán sin que zip proteste. Son dos pruebas distintas: la primera comprueba cantidad; la segunda muestra que la relación depende del orden. Tener clara esa diferencia evita pedirle a una comprobación de longitud que certifique también la identidad.

El modo estricto detecta el desajuste al consumir el recorrido

Desde Python 3.10 puedes usar strict=True. Cuando los iterables tienen longitudes distintas, el recorrido lanza ValueError. La palabra importante es recorrido: crear el objeto zip no significa que ya se hayan examinado todas sus entradas. Es un iterador que produce parejas según se las pides; la diferencia puede descubrirse después de haber entregado varias.

Por eso, si una operación debe aceptar el conjunto completo o rechazarlo, primero prepara las parejas y después utilízalas. Guarda el siguiente programa completo como zip_estricto.py. Necesita Python 3.10 o posterior. list consume el recorrido dentro de preparar; si falla, la función no devuelve una colección parcial al código que prepara la salida.

				
					def preparar(niveles, alturas):
    return list(zip(niveles, alturas, strict=True))


niveles = ["PB", "P1", "P2"]
alturas = [0.0, 3.2]

try:
    propuestas = preparar(niveles, alturas)
except ValueError:
    print("Sin propuestas: longitudes diferentes")
else:
    for nivel, altura in propuestas:
        print(f"{nivel} | {altura:g} m")
				
			

Ejecuta esta segunda versión:

				
					python zip_estricto.py
				
			

Ahora verás únicamente Sin propuestas: longitudes diferentes. No se imprimen primero PB y P1 como resultados aceptados. Añade 6.4 a alturas y vuelve a ejecutar: obtendrás las tres parejas. La diferencia práctica está en la frontera que hemos colocado entre preparar el conjunto y consumir las propuestas, no solo en añadir una palabra clave con aspecto tranquilizador.

Hacer directamente una escritura dentro de for ... zip(..., strict=True) no proporciona esa frontera: podrían ejecutarse acciones antes de que aparezca el desajuste. El modo estricto tampoco revierte efectos producidos por los propios iteradores. Aquí las entradas son listas en memoria y la función solo construye otra lista; esa sencillez permite aislar el problema. En una herramienta que modifique un modelo, la validación del conjunto y la transacción de escritura serían decisiones adicionales.

Materializar necesita memoria para las parejas y exige que el recorrido termine. Es una elección razonable para esta pequeña lista de niveles; no una receta para cargar sin límite cualquier flujo de datos. Con listas puedes comprobar antes len si ese es tu contrato. Con generadores no siempre existe una longitud disponible, y consumirlos para contar puede agotar la entrada. La propuesta PEP 618 explica precisamente la incorporación del modo estricto; no hace falta convertir un ejercicio de tres plantas en una infraestructura de streaming para entender por qué existe.

En C# puedes comprobar los arrays antes de utilizar Zip

LINQ también ofrece Zip para recorrer secuencias en paralelo. En .NET 8 no utilizaremos un parámetro strict equivalente al de Python: para estos arrays comprobaremos sus longitudes antes de generar las parejas. Crea una aplicación de consola:

				
					dotnet new console --framework net8.0 -n NivelesParalelos
				
			

Reemplaza NivelesParalelos/Program.cs por este programa. La función recibe arrays, de modo que Length está disponible sin consumir un recorrido. El resultado se convierte en otro array antes de devolverse. La cultura invariante mantiene el punto decimal en la salida para que podamos comparar las mismas líneas con Python.

				
					using System;
using System.Globalization;
using System.Linq;

static (string Nivel, double Altura)[] Preparar(string[] niveles, double[] alturas)
{
    if (niveles.Length != alturas.Length)
        throw new ArgumentException("Longitudes diferentes");
    return niveles.Zip(alturas, (nivel, altura) => (Nivel: nivel, Altura: altura))
        .ToArray();
}

string[] niveles = ["PB", "P1", "P2"];
double[] alturas = [0.0, 3.2];

try
{
    var propuestas = Preparar(niveles, alturas);
    foreach (var (nivel, altura) in propuestas)
        Console.WriteLine($"{nivel} | {altura.ToString("G", CultureInfo.InvariantCulture)} m");
}
catch (ArgumentException)
{
    Console.WriteLine("Sin propuestas: longitudes diferentes");
}
				
			

Ejecuta el proyecto:

				
					dotnet run --project NivelesParalelos
				
			

Con los datos publicados, la salida es el mismo rechazo que en el ejemplo estricto de Python. Si añades 6.4 al array de alturas, se mostrarán las tres parejas. Sin la comprobación previa, Zip también limitaría el resultado al recorrido compartido. La referencia oficial de Enumerable.Zip describe ese comportamiento y la ejecución diferida de la consulta.

No copies la comparación de longitudes como dos llamadas independientes a Count() sobre cualquier IEnumerable. Una secuencia puede requerir trabajo para recorrerse, agotarse o producir resultados distintos en otra enumeración. Este ejemplo promete arrays que ya están disponibles en memoria y comprueba exactamente ese caso. Extender la función a entradas de otro tipo requiere decidir cómo se leerán; cambiar el nombre del parámetro no amplía las garantías.

Una pareja completa todavía puede contener datos equivocados

Comprueba cuatro situaciones en las versiones protegidas: tres nombres y tres alturas; tres nombres y dos alturas; dos nombres y tres alturas; y ambas entradas vacías. Solo la primera produce tres propuestas; las dos desiguales se rechazan y la última devuelve una colección vacía. Que no haya propuestas puede ser correcto para una entrada vacía, pero el flujo que utiliza esa entrada debe decidir si esperaba niveles. No sustituyas esa decisión por un cero que parezca inocente en el informe.

Después conserva las longitudes y cambia el orden de una sola lista. Los ejemplos aceptarán la entrada porque cumplen su comprobación de cantidad. No es una contradicción: acabas de demostrar el límite de esa comprobación. Si las listas se han preparado por separado y no puedes justificar la relación posicional, vuelve al cruce por clave. strict=True detecta un desajuste de longitudes, no una cota que ha cambiado de propietario.

Los ejemplos tampoco validan nombres únicos, alturas finitas, unidades recibidas o un orden ascendente. Esas reglas pertenecen al problema que quieras resolver. Para revisar niveles de un edificio necesitarías definirlas y probarlas antes de anunciar que la tabla está validada. Hoy estamos comprobando una condición más estrecha: que no desaparezcan posiciones al construir parejas de dos entradas cuyo orden ya debe ser fiable.

La documentación de zip de Python permite ampliar la mecánica, y el artículo de bucles explica el recorrido si todavía te cuesta seguir cada vuelta. Quédate con una prueba para tu siguiente script: quita deliberadamente un valor de una lista y observa si falla antes de utilizar las parejas o si termina entregando menos trabajo. Prefiero un rechazo que puedas explicar a una omisión que tengas que descubrir cuando alguien pregunte por la planta que falta.

Artículos relacionados

Respuestas

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *