Skip to content
This page has been auto-translated and may contain errors.View in English

Tuplas y conjuntos ​

Ya conoces las listas. Python tiene otros dos tipos de colección que resuelven problemas que las listas no pueden. Las tuplas guardan un grupo fijo de valores que nunca van a cambiar. Los conjuntos guardan solo valores únicos y te permiten comprobar si algo pertenece a la colección de forma instantánea, sin importar cuán grande sea.

El set de herramientas de colecciones de Python tiene cuatro tipos. Las listas y los diccionarios cubren la mayoría de los casos generales. Las tuplas y los conjuntos resuelven los casos específicos: registros fijos donde la inmutabilidad es una ventaja, y colecciones de valores únicos donde la prioridad es probar membresía en O(1).

Además de list y dict, Python te da tuple (una secuencia de longitud fija que no puedes cambiar después de crearla) y set (una colección sin orden de valores únicos, construida sobre una tabla hash, la misma estructura de búsqueda por contenido que hace rápido a dict). La diferencia que guía cada decisión de aquí en adelante es esta: una tupla es hashable (su contenido se puede reducir a un solo número, así que puede actuar como clave de un dict o miembro de un set), una lista no lo es. Elige según lo que necesitas que la colección haga, no según cuál usas por costumbre.

Tuplas ​

Una tupla es un grupo ordenado de valores que no se puede cambiar después de crearlo. Los paréntesis definen una tupla, pero son opcionales. Lo que realmente la convierte en tupla es la coma. Una tupla de un solo elemento necesita una coma al final.

Las tuplas son secuencias inmutables. Lo que crea una tupla es la coma, no los paréntesis. La inmutabilidad las hace hashable cuando todos sus elementos también lo son, lo que abre casos de uso que las listas no pueden cubrir: claves de dict, miembros de set y registros de estructura fija.

Una tuple es una secuencia de longitud fija que no puedes cambiar una vez que existe. Como nada dentro de ella se mueve, Python puede calcular un único hash (un número derivado de su contenido) siempre que cada elemento sea a su vez hashable, y eso es lo que permite que una tupla sirva como clave de dict o miembro de set, algo que una lista no puede hacer. La indexación y el slicing funcionan exactamente igual que en una lista; no hay asignación por elemento, así que point[0] = 99 lanza TypeError en el momento en que se ejecuta esa línea. La forma de un solo elemento (42,) necesita la coma al final. Sin ella, los paréntesis solo agrupan y obtienes de vuelta el valor puro 42.

python
point = (10, 20)
rgb = (255, 128, 0)
dimensions = (1920, 1080)
single = (42,)            # se requiere coma al final para una tupla de un solo elemento
also_tuple = 42, 99       # los paréntesis son opcionales; la coma es lo que crea la tupla

El acceso por índice funciona exactamente igual que en una lista. Intentar cambiar un elemento lanza un TypeError:

La indexación, el slicing y los índices negativos funcionan igual que en las listas. Cualquier intento de asignar mediante índice lanza TypeError; esto es intencional, no una limitación.

Leer de una tupla, por índice, índice negativo o slice, se comporta exactamente igual que con una lista. Escribir es lo que cambia: no hay asignación por elemento, así que point[0] = 99 lanza TypeError en el instante en que se ejecuta esa línea. Ese es justamente el punto de elegir una tupla en lugar de una lista: es un valor que tú y cualquier persona que lea el código después pueden confiar en que no va a cambiar por debajo de ustedes.

python
point = (10, 20)
point[0]    # 10
point[1]    # 20
point[-1]   # 20

point[0] = 99    # TypeError: 'tuple' object does not support item assignment
JunoTuplas Una tupla es un grupo ordenado de valores que no puedes cambiar después de crearlo. Lo que la crea es la coma, no los paréntesis, por eso (42,) necesita esa coma solitaria al final. Si intentas reasignar un elemento, obtienes un TypeError, y esa sensación de "esto está fijo" es justamente la razón para elegir una tupla.
JunoTuplas Las tuplas son secuencias inmutables, y lo que las crea es la coma, no los paréntesis. Esa inmutabilidad es una ventaja: hace que una tupla sea hashable cuando su contenido también lo es, así que puede ser clave de un dict o miembro de un set, algo que una lista no puede lograr. La asignación por elemento lanza TypeError a propósito.
JunoTuplas Leer una tupla es igual que leer una lista; lo que falta es la escritura, así que point[0] = 99 lanza TypeError en tiempo de ejecución. Apóyate en eso: una tupla es el valor que pasas de un lado a otro cuando necesitas la garantía de que nada más adelante lo va a reescribir. La trampa de (42,) con un solo elemento le pasa a todo el mundo alguna vez.

Cuándo usar una tupla ​

Usa una tupla cuando tienes un grupo pequeño de valores relacionados que van juntos y que no van a cambiar. Coordenadas (x, y), un color (r, g, b), un par nombre-puntaje ("Ana", 87). La estructura fija le indica a cualquiera que lea el código que ese grupo se trata como una sola unidad.

Las tuplas comunican estructura fija: un grupo de valores donde la posición tiene un significado y el grupo se trata como una unidad. Su capacidad de ser hashable las hace válidas como claves de diccionario, algo que las listas no pueden ser. El contrato que señala una tupla es: estos valores van juntos y no se espera que cambien.

Recurre a una tupla cuando tienes un registro de forma fija: un número conocido de campos donde cada posición significa algo específico. Como una tupla es hashable (su contenido se reduce a un solo número), encaja en cualquier lugar donde se requiera un valor hashable: una clave de dict, un miembro de set, o los argumentos que se guardan en caché detrás de functools.lru_cache, que recuerda los resultados de una función según los argumentos con los que la llamaste. La señal semántica también es distinta a la de una lista. Una tupla dice "estos campos van juntos y cada posición tiene un significado" (una coordenada, un color), mientras que una lista dice "una serie de cosas similares cuya longitud puede cambiar".

JunoCuándo usar una tupla Usa una tupla cuando tienes un grupito fijo donde cada lugar significa algo: una coordenada, un color (r, g, b), un par nombre-puntaje. Como las tuplas son hashable, incluso puedes usar una como clave de dict. Una lista no puede hacer eso, y eso confunde a la gente la primera vez que lo intenta.
JunoCuándo usar una tupla Una tupla señala estructura fija: la posición tiene significado y el grupo viaja como una sola unidad. Esa es la diferencia con una lista, que dice "una secuencia cuya longitud puede variar". Y como las tuplas son hashable, funcionan como claves de dict donde las listas lanzan TypeError.
JunoCuándo usar una tupla Tupla para un registro de forma fija, lista para una serie de longitud variable de cosas similares. La recompensa es la capacidad de ser hashable: una tupla entra como clave de dict, como miembro de set, o como clave de lru_cache, todos lugares donde una lista no puede ir. Deja que el tipo lleve ese contrato en lugar de un comentario.
python
locations = {}
locations[(40, -74)] = "Nueva York"   # tupla como clave de dict, funciona
locations[[40, -74]] = "Nueva York"   # lista como clave de dict, TypeError

Desempaquetado ​

El desempaquetado extrae valores de una tupla y asigna cada uno a su propio nombre en una sola línea. La cantidad de nombres debe coincidir con la cantidad de valores. Usa * para capturar el resto de los elementos en una lista.

El desempaquetado funciona en cualquier iterable: tuplas, listas, strings. La cantidad de nombres destino debe coincidir con la longitud del iterable, a menos que un destino con asterisco capture una porción de longitud variable. Si no coinciden, se lanza ValueError. El desempaquetado es la forma idiomática de recibir múltiples valores de retorno de una función.

El desempaquetado funciona con cualquier cosa iterable (cualquier cosa sobre la que puedas iterar con un bucle), no solo tuplas: recorre el lado derecho y asocia cada valor a su nombre destino en orden. Un destino con asterisco (*resto) absorbe los elementos restantes en una list, así que la cantidad ya no tiene que coincidir exactamente. Un desajuste simple en la cantidad lanza ValueError en tiempo de ejecución. La forma a la que más recurrirás es en el encabezado de un for: for nombre, puntaje in pares desempaqueta cada elemento a medida que itera, lo cual se lee mucho más limpio que indexar cada par a mano.

JunoDesempaquetado El desempaquetado extrae cada valor de una tupla o lista y le da su propio nombre en una sola línea, como x, y = point. La cantidad de nombres tiene que coincidir con la cantidad de valores, a menos que agregues un *resto para recoger lo que quede. A mí me hizo clic el día que dejé de escribir point[0] y point[1] por todos lados.
JunoDesempaquetado El desempaquetado asigna cada valor a su propio nombre en una línea, y un destino con asterisco como *resto captura una porción de longitud variable. Si las cantidades no coinciden, se lanza ValueError. Es la forma limpia de recibir múltiples valores de retorno de una función en lugar de indexar el resultado.
JunoDesempaquetado El desempaquetado funciona con cualquier iterable y asocia de izquierda a derecha; *resto absorbe lo que sobra, y un desajuste simple lanza ValueError. Donde más rinde es en el encabezado de un for, for nombre, puntaje in pares, que le gana por goleada a acceder a cada par por índice.
python
point = (10, 20)
x, y = point

print(x)   # 10
print(y)   # 20

first, *rest = [1, 2, 3, 4, 5]
# first = 1, rest = [2, 3, 4, 5]

head, *middle, tail = [1, 2, 3, 4, 5]
# head = 1, middle = [2, 3, 4], tail = 5

Tuplas con nombre ​

Una tupla con nombre es una tupla donde cada posición tiene un nombre. En lugar de recordar que point[0] es la coordenada x, escribes point.x. Los valores siguen siendo inmutables; lo que ganas son nombres de atributo legibles en lugar de posiciones numéricas.

namedtuple genera una clase que se comporta exactamente como una tupla pero agrega acceso por nombre a los atributos. Es más liviana que una clase completa, inmutable y autodocumentada. Úsala cuando el acceso posicional de una tupla común necesitaría un comentario para entenderse.

collections.namedtuple es una fábrica de clases (una función que construye y te entrega una clase nueva para que la uses). La clase que devuelve es una tuple con campos con nombre, así que cuesta lo mismo en memoria que una tupla común pero se lee mucho mejor. Obtienes gratis _asdict() (la convierte en un dict), _replace() (crea una copia con un campo cambiado, ya que no puedes mutarla) y _fields. Cuando necesitas más, valores por defecto o anotaciones de tipo, typing.NamedTuple cubre el caso anotado, y dataclasses.dataclass es la opción moderna una vez que quieres métodos o mutabilidad opcional.

JunoTuplas con nombre Una tupla con nombre le da un nombre a cada posición, así que escribes point.x en lugar de acordarte de que point[0] es la x. Sigue siendo completamente inmutable y se comporta como una tupla normal en todo lo demás. Tu yo del futuro, leyendo el código, te va a agradecer por los nombres.
JunoTuplas con nombrenamedtuple construye una clase parecida a una tupla con acceso por nombre a los atributos, inmutable y autodocumentada, con menos peso que una clase completa. Recurre a ella en el momento en que las posiciones de una tupla común necesitarían un comentario para explicarse. Más allá de eso, dataclass es el paso moderno siguiente.
JunoTuplas con nombrenamedtuple te da una subclase de tupla con campos con nombre al mismo costo en memoria, más _replace() y _asdict() gratis. En cuanto quieras valores por defecto, métodos o anotaciones de tipo reales, salta a typing.NamedTuple o a dataclass en lugar de forzar la fábrica.

namedtuple vive en la biblioteca estándar, así que primero necesita un import: from collections import namedtuple. Los imports se explican a fondo en el capítulo de Módulos.

python
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
Player = namedtuple("Player", ["name", "score", "level"])

p = Point(10, 20)
p.x    # 10
p.y    # 20

alice = Player("Ana", 87, 5)
alice.name    # "Ana"
alice.score   # 87

Conjuntos ​

Un conjunto (set) es una colección de valores únicos sin un orden garantizado. Agregar el mismo valor dos veces no hace nada: un conjunto conserva solo una copia de cada elemento. Usa llaves para un conjunto con elementos, o set() para crear uno vacío.

set es una colección sin orden que rechaza automáticamente los duplicados. La prueba de membresía es O(1) sin importar el tamaño, lo que la convierte en la herramienta correcta cada vez que necesitas comprobar si un valor existe dentro de una colección grande. Nota: {} crea un dict vacío, no un set vacío; para eso usa set().

Un set guarda valores únicos y hashable, y está construido sobre una tabla hash, la misma estructura de búsqueda por contenido que un dict. Eso te da búsqueda, inserción y eliminación en O(1) en promedio (el costo se mantiene plano a medida que el conjunto crece, en lugar de subir con su tamaño). Dos consecuencias a tener en cuenta: solo pueden entrar valores hashable, así que int, str y tuple están bien, pero list, dict y set no; y el orden de iteración sigue las posiciones internas del hash, así que nunca confíes en que sea estable. Una trampa de sintaxis: {} es un dict vacío, no un set vacío; usa set().

JunoConjuntos Un conjunto conserva solo valores únicos, así que agregar algo que ya está ahí no hace nada en absoluto, ni error ni duplicado. Créalo con llaves y elementos, pero para uno vacío usa set(), porque {} en secreto es un dict vacío. Ese último detalle atrapa a casi todo el mundo.
JunoConjuntos Un conjunto elimina duplicados automáticamente y prueba membresía en O(1) sin importar cuán grande sea, lo que lo convierte en la herramienta correcta para "¿esto está aquí?" en una colección grande. Cuidado con la trampa del conjunto vacío: {} es un dict, así que usa set().
JunoConjuntos Un set es una tabla hash de valores únicos y hashable: membresía, inserción y eliminación en O(1), al costo de que solo entran elementos hashable y el orden de iteración nunca es estable. No te apoyes en ese orden, y recuerda que {} es un dict, así que usa set() cuando quieras un conjunto vacío.
python
tags = {"python", "principiante", "tutorial"}
numbers = {1, 2, 3, 4, 5}
empty = set()    # NO {} (eso es un dict vacío)

Agregar el mismo valor dos veces no cambia el conjunto:

python
tags.add("python")   # tags no cambia, "python" ya estaba en el conjunto

Cuándo usar un conjunto ​

Los conjuntos son la herramienta indicada para tres cosas: eliminar duplicados de una lista, comprobar rápidamente si algo está en una colección grande, y comparar dos grupos para ver qué comparten o qué los diferencia.

Tres casos de uso distintos motivan el uso de conjuntos: deduplicación (automática al insertar), prueba de membresía en O(1) (frente a O(n) para list), y álgebra de conjuntos (|, &, -, ^). Cuando la colección es grande y comprobás membresía con frecuencia, la diferencia de rendimiento es considerable.

Tres casos de uso salen directo de la tabla hash: unicidad (los duplicados se rechazan al insertar), membresía en O(1) (el costo de in se mantiene plano a medida que el conjunto crece), y álgebra de conjuntos (|, &, -, ^). La prueba de membresía es la que realmente rinde en código real: in sobre un conjunto de 10.000 elementos es tan rápido como sobre uno de 10, mientras que la misma comprobación en una lista es O(n) y se vuelve más lenta de forma lineal. Cuando te encuentres haciendo repetidamente x in alguna_lista contra una lista grande, convertirla en un conjunto una sola vez suele ser la solución.

JunoCuándo usar un conjunto Tres trabajos en los que los conjuntos son excelentes: quitar duplicados de una lista, comprobar rápido si algo está en una colección grande, y comparar dos grupos para ver qué comparten o qué los diferencia. Si estás haciendo algo de eso, probablemente un conjunto es la herramienta.
JunoCuándo usar un conjunto Tres razones: deduplicación al insertar, membresía en O(1) contra O(n) de una lista, y álgebra de conjuntos con |, &, -, ^. Cuanto más grande la colección y más seguido comprobás in, más se nota la ventaja del conjunto.
JunoCuándo usar un conjunto Unicidad, in en O(1), y álgebra de conjuntos, todo saliendo de la tabla hash. La ganancia práctica es la de membresía: x in lista_grande repetido es O(n) cada vez, así que convertir esa lista en un conjunto una sola vez suele ser la solución.
python
# Eliminar duplicados de una lista
raw = ["gato", "perro", "gato", "pájaro", "perro", "gato"]
unique = list(set(raw))   # ["gato", "perro", "pájaro"] (orden no garantizado)
python
# Comprobación rápida de membresía
valid_codes = {"USD", "EUR", "GBP", "JPY"}
code = "EUR"

if code in valid_codes:    # búsqueda instantánea, incluso con miles de códigos
    print("Válido")

Operaciones de conjuntos ​

Los conjuntos soportan las mismas operaciones que aprendiste en matemáticas: unión (todo lo que está en cualquiera de los dos conjuntos), intersección (solo lo que comparten ambos conjuntos) y diferencia (lo que tiene uno y el otro no). Python usa símbolos de operador para esto, y cada uno tiene un método equivalente.

Los operadores de conjunto en Python reflejan la notación matemática: | para unión, & para intersección, - para diferencia, ^ para diferencia simétrica. Cada operador tiene una forma como método (.union(), .intersection(), etc.) que también acepta cualquier iterable, no solo conjuntos.

Las formas con operador (|, &, -, ^) requieren que ambos lados sean conjuntos y lanzan TypeError si alguno es otra cosa, como una lista. Las formas con método (.union(), .intersection(), y el resto) son más flexibles: aceptan cualquier iterable y lo convierten por ti, así que a.union([1, 2]) funciona donde a | [1, 2] falla. Esa diferencia es la que hay que recordar cuando una operación explota. También existen las formas en el lugar (|=, &=, -=, ^=) que actualizan el conjunto de la izquierda en lugar de devolver uno nuevo, lo mismo que llamar a .update(), .intersection_update(), etc.

JunoOperaciones de conjuntos Cuatro operaciones de la clase de matemáticas: | es unión (en cualquiera), & es intersección (en ambos), - es diferencia (en uno pero no en el otro), y ^ es diferencia simétrica (en uno pero no en ambos). Cada una también tiene un método con nombre completo como .union() si prefieres palabras a símbolos.
JunoOperaciones de conjuntos| unión, & intersección, - diferencia, ^ diferencia simétrica. Las formas con método (.union() y compañía) hacen lo mismo pero aceptan cualquier iterable, no solo un conjunto, cosa que los operadores no permiten.
JunoOperaciones de conjuntos Los operadores exigen que ambos lados sean conjuntos y lanzan TypeError si no; las formas con método aceptan cualquier iterable, así que a.union([1, 2]) funciona donde a | [1, 2] no. Las formas en el lugar (|=, &=) actualizan en el lugar en vez de devolver un conjunto nuevo.
python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b    # {1, 2, 3, 4, 5, 6}   (unión: todo lo que está en cualquiera)
a & b    # {3, 4}               (intersección: solo lo que está en ambos)
a - b    # {1, 2}               (diferencia: en a pero no en b)
b - a    # {5, 6}               (diferencia al revés)
a ^ b    # {1, 2, 5, 6}        (diferencia simétrica: en uno pero no en ambos)

Estas también tienen forma de método: .union(), .intersection(), .difference(), .symmetric_difference().

Modificar conjuntos ​

Los conjuntos son mutables. .add() agrega un elemento. .update() agrega varios a la vez desde cualquier lista u otro iterable. .remove() elimina un elemento pero lanza un error si no está ahí. .discard() elimina en silencio si el elemento existe y no hace nada si no.

.add() es O(1) en promedio. .update() acepta cualquier iterable y equivale a llamar a .add() dentro de un bucle. .remove() lanza KeyError si no lo encuentra, mientras que .discard() es la opción segura cuando no estás seguro de si el elemento está presente. .pop() elimina un elemento arbitrario, no el "último", ya que los conjuntos no tienen orden.

.add(x) inserta un elemento en O(1) en promedio; .update(iterable) agrega varios y es lo mismo que |=. El par que suele confundir a la gente es .remove() frente a .discard(): ambos eliminan un elemento, pero .remove() lanza KeyError cuando el elemento no está presente, mientras que .discard() no hace nada en silencio. Recurre a .discard() cada vez que no estés seguro de si el elemento está presente, así un elemento faltante no se convierte en una excepción que tengas que envolver. .pop() devuelve y elimina algún elemento, pero cuál está indefinido: un conjunto no tiene orden, así que nunca trates a .pop() como si tomara el "último" elemento.

JunoModificar conjuntos.add() mete un solo elemento, .update() agrega un lote entero desde una lista u otro iterable. El par que hay que tener claro es la eliminación: .remove() da error si el elemento no está ahí, mientras que .discard() se hace el desentendido y sigue adelante. Cuando no estés seguro de que algo está en el conjunto, .discard() te ahorra un error inesperado.
JunoModificar conjuntos.add() para uno, .update() para varios desde cualquier iterable. .remove() lanza KeyError si no lo encuentra; .discard() es la versión segura que lo ignora. Y .pop() saca un elemento arbitrario, no un "último", ya que los conjuntos no tienen orden.
JunoModificar conjuntos.add() es O(1), .update() es |=. La trampa es que .remove() lanza KeyError donde .discard() se queda callado, así que usa .discard() por defecto cuando no estés seguro de si el elemento está presente. .pop() devuelve un elemento indefinido; nunca lo leas como el último.
python
tags = {"python", "principiante"}

tags.add("tutorial")          # agregar un elemento
tags.update(["web", "api"])   # agregar varios elementos desde cualquier iterable
tags.remove("principiante")   # eliminar, lanza KeyError si no se encuentra
tags.discard("faltante")      # eliminar, sin error si no se encuentra
tags.pop()                    # eliminar y devolver un elemento arbitrario
tags.clear()                  # eliminar todo

Usa .discard() cuando no estés seguro de si el elemento existe.

Frozen sets ​

Un frozen set es un conjunto que no puedes modificar después de crearlo. La razón principal para usar uno: los frozen sets son hashable, así que se pueden usar como claves de diccionario o guardarse dentro de otros conjuntos.

frozenset es la contraparte inmutable de set. Soporta todas las operaciones de lectura y el álgebra de conjuntos, pero no la mutación. Su inmutabilidad la hace hashable, lo que significa que es válida como clave de dict o como miembro dentro de otro conjunto.

Un frozenset es un set que no puedes cambiar después de crearlo. Como nada dentro de él se mueve, obtiene un hash estable y es a su vez hashable, así que puede ser clave de un dict o miembro de otro conjunto, algo que un set común no puede hacer. Todo lo que lee o devuelve una colección nueva sigue funcionando (membresía, los operadores de álgebra de conjuntos), mientras que los métodos que mutan (add, remove y el resto) desaparecen. El caso donde encaja perfecto: una tabla de búsqueda constante que no debe cambiar mientras el programa corre y que puede necesitar vivir dentro de un dict o un set.

JunoFrozen sets Un frozen set es un conjunto que no puedes cambiar una vez que está creado. Ese estado bloqueado es lo que lo hace hashable, así que, a diferencia de un conjunto normal, puedes usar uno como clave de dict o meterlo dentro de otro conjunto. Recurre a él cuando tengas un grupo fijo de valores permitidos que nunca deberían cambiar.
JunoFrozen setsfrozenset es el set inmutable: todas las lecturas y el álgebra de conjuntos, sin nada de mutación. Su inmutabilidad le da la capacidad de ser hashable, así que funciona como clave de dict o como miembro de otro conjunto, donde un set normal lanzaría un error. Encaja bien para una tabla de búsqueda constante.
JunoFrozen setsfrozenset tiene un hash estable, así que va donde un set no puede: claves de dict, miembros de otro conjunto. Las lecturas y el álgebra de conjuntos se mantienen, los métodos de mutación desaparecen. El uso claro es una tabla de búsqueda constante que no debe cambiar en tiempo de ejecución.
python
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived")    # AttributeError, frozenset es inmutable

Elegir la colección correcta ​

Cuatro tipos, cada uno con un rol claro. Pregúntate qué necesitas hacer con los datos y normalmente la elección correcta se vuelve obvia.

La elección entre tipos de colección tiene que ver con qué operaciones importan y qué restricciones tienen tus datos: mutabilidad, orden, manejo de duplicados y estrategia de búsqueda.

La elección es en parte rendimiento, en parte significado. dict y set dan búsqueda en O(1) en promedio gracias al hashing, así que comprobar membresía se mantiene rápido a medida que crecen. list y tuple dan acceso en O(1) por índice pero membresía en O(n), donde in se vuelve más lento con el tamaño. El otro eje es lo que el tipo señala: una tuple es un registro fijo cuya inmutabilidad le da la capacidad de ser hashable, mientras que una list es una secuencia de longitud variable. Cuando necesitas una colección hashable para anidar dentro de un dict o un set, tuple y frozenset son los dos tipos incorporados que pueden hacerlo.

JunoElegir la colección correcta Cuatro tipos, cuatro trabajos claros: listas para cosas ordenadas que vas a cambiar, tuplas para registros fijos, conjuntos para valores únicos y comprobaciones rápidas de "¿está aquí?", diccionarios para buscar cosas por nombre. Pregúntate qué necesitas hacer con los datos y normalmente el correcto se vuelve evidente.
JunoElegir la colección correcta Elige según lo que necesiten los datos: mutabilidad, orden, si los duplicados importan, y cómo buscas las cosas. Lista para una secuencia ordenada que mutas, tupla para un registro fijo, conjunto para valores únicos y membresía en O(1), diccionario para búsqueda por clave-valor.
JunoElegir la colección correcta Mitad rendimiento, mitad significado: dict y set dan búsqueda en O(1), list y tuple dan O(1) por índice pero O(n) en membresía. Una tuple también señala "registro fijo" y le da capacidad de ser hashable. Cuando necesitas una colección hashable para anidar, tuple y frozenset son tus dos opciones.
listtuplesetdict
OrdenadoSíSíNoSí (orden de inserción)
MutableSíNoSíSí
DuplicadosSíSíNoNo (claves)
Acceso porÍndiceÍndicen/aClave
Úsalo cuandoSecuencia ordenada y modificableRegistro fijoValores únicos, membresía rápidaBúsqueda por clave-valor

Una regla rápida de decisión:

  • ¿Necesitas buscar algo por nombre? → dict
  • ¿Necesitas una colección ordenada que vas a modificar? → list
  • ¿Tienes un grupo fijo de valores relacionados? → tuple
  • ¿Necesitas valores únicos o pruebas de membresía rápidas? → set

En la práctica ​

Usando tuplas para guardar registros fijos y un conjunto para rastrear valores únicos:

python
casa = (51.5074, -0.1278)   # latitud, longitud
oficina = (51.5155, -0.0922)

casa_lat, casa_lon = casa
print(f"Casa: {casa_lat}, {casa_lon}")

# Rastrear visitantes únicos con un conjunto
visitantes = set()
visitantes.add("ana")
visitantes.add("beto")
visitantes.add("ana")    # ya está en el conjunto, se ignora en silencio
visitantes.add("carla")

print(f"Visitantes únicos: {len(visitantes)}")
print(f"ana visitó: {'ana' in visitantes}")
print(f"dario visitó:  {'dario' in visitantes}")

Usando conjuntos para rastrear lo que ya se procesó y calcular el trabajo restante:

python
already_processed = {"reporte_ene.csv", "reporte_feb.csv"}
all_files = {"reporte_ene.csv", "reporte_feb.csv", "reporte_mar.csv", "reporte_abr.csv"}

to_process = all_files - already_processed
print(f"Archivos por procesar: {sorted(to_process)}")

for filename in sorted(to_process):
    print(f"Procesando {filename}...")
    already_processed.add(filename)

print(f"Listo. Total procesados: {len(already_processed)}")

Usando frozenset para tablas de búsqueda constante y demostrando la prueba de membresía en O(1) con álgebra de conjuntos:

python
ALLOWED_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"})
SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})

# El álgebra de conjuntos sobre frozensets devuelve un set normal
unsafe_allowed = ALLOWED_METHODS - SAFE_METHODS
print(f"Métodos permitidos no seguros: {unsafe_allowed}")

# frozenset es hashable, así que se puede guardar dentro de un set (un set normal no puede)
method_groups = {
    frozenset({"GET", "HEAD", "OPTIONS"}),
    frozenset({"POST", "PUT", "PATCH"}),
    frozenset({"DELETE"}),
}
print(f"Grupos de métodos: {len(method_groups)}")

method = "POST"
print(f"Permitido: {method in ALLOWED_METHODS}")
print(f"Seguro:    {method in SAFE_METHODS}")

frozenset conserva búsqueda en O(1) y se puede guardar en cualquier lugar donde se requiera un tipo hashable. El álgebra de conjuntos sobre dos objetos frozenset devuelve un set normal; envuelve el resultado en frozenset() para mantenerlo inmutable.