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 dos tipos más de colecciones que resuelven problemas que las listas no pueden. Las tuplas guardan un grupo fijo de valores que nunca cambiarán. Los conjuntos guardan solo valores únicos y te permiten comprobar la pertenencia instantáneamente, sin importar lo grande que sea la colección.

El conjunto 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 verificación de pertenencia O(1) es la prioridad.

Más allá de list y dict, Python provee tuple (secuencia inmutable de longitud fija) y set (colección no ordenada respaldada por una tabla hash, compuesta por objetos hashables únicos). Cada uno tiene un modelo de memoria, una característica de hashabilidad y un perfil de rendimiento distintivo que conviene conocer antes de elegir.

Tuplas

Una tupla es un grupo ordenado de valores que no puede cambiarse 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 requiere una coma final.

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

tuple es una secuencia inmutable respaldada por un arreglo de C de tamaño fijo. __hash__ se calcula a partir de los hashes de los elementos cuando todos son hashables, lo que hace válidas las tuplas como claves de diccionario y miembros de conjunto. __getitem__ admite enteros y slices; __setitem__ no está implementado, así que cualquier intento de mutación lanza TypeError. La forma de un solo elemento (42,) requiere la coma final; sin ella, los paréntesis son solo agrupación.

python
point      = (10, 20)
rgb        = (255, 128, 0)
dimensions = (1920, 1080)
single     = (42,)            # se requiere una coma final para una tupla de un solo elemento
also_tuple = 42, 99           # los paréntesis son opcionales; la coma la convierte en 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 de forma idéntica a las listas. Cualquier intento de asignar por índice lanza TypeError; esto es intencional, no una limitación.

__getitem__ con enteros y objetos slice sigue las mismas reglas de clamping que list. No hay __setitem__: el tipo tuple no lo registra, así que TypeError se lanza en tiempo de ejecución, no en tiempo de análisis.

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

point[0] = 99    # TypeError: 'tuple' object does not support item assignment

Cuándo usar una tupla

Usa una tupla cuando tengas un grupo pequeño de valores relacionados que vayan juntos y no vayan a cambiar. Coordenadas (x, y), un color (r, g, b), un par nombre-puntuación ("Sofía", 87). La estructura fija le indica a cualquier persona que lea el código que este grupo se trata como una sola unidad.

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

Las tuplas son el tipo idiomático para registros de aridad fija. Su hashabilidad las hace utilizables dondequiera que se requiera Hashable: claves de diccionario, miembros de conjunto, firmas de llamada de functools.lru_cache. El contrato semántico difiere del de una lista: las tuplas representan un registro heterogéneo donde la posición tiene significado, las listas representan una secuencia homogénea donde la longitud y el orden pueden variar.

python
locations = {}
locations[(40, -74)] = "Ciudad de México"   # tupla como clave de diccionario, funciona
locations[[40, -74]] = "Ciudad de México"   # lista como clave de diccionario, 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 los elementos restantes en una lista.

El desempaquetado funciona en cualquier iterable: tuplas, listas, cadenas. La cantidad de nombres destino debe coincidir con la longitud del iterable, a menos que un destino con estrella capture una porción de longitud variable. Una discrepancia lanza ValueError. El desempaquetado es la forma idiomática de consumir múltiples valores de retorno de una función.

El desempaquetado llama a __iter__ en el lado derecho y vincula cada valor producido al nombre destino correspondiente. Un destino con estrella (*rest) recopila los elementos restantes en una list. Una discrepancia lanza ValueError en tiempo de ejecución. El desempaquetado extendido también funciona dentro de encabezados for: for x, y in list_of_pairs desempaqueta cada elemento de la iteración.

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; solo obtienes nombres de atributos legibles en vez de posiciones numéricas.

namedtuple genera una clase que se comporta exactamente como una tupla pero añade acceso por atributos con nombre. Es más ligera que una clase completa, inmutable y autodescriptiva. Úsala cuando el acceso posicional de una tupla simple requeriría un comentario para entenderse.

collections.namedtuple es una fábrica de clases: genera una subclase de tuple en tiempo de ejecución con acceso por atributos con nombre compilado. La clase generada incluye _asdict(), _replace() y _fields. El consumo de memoria es idéntico al de una tupla simple. Para más control (valores por defecto, anotaciones de tipo, mutabilidad opcional), dataclasses.dataclass es la alternativa moderna; para tuplas con anotaciones de tipo, typing.NamedTuple es idiomático.

Importación de named tuple

namedtuple está en la biblioteca estándar de Python, pero hay que importarla. La línea from collections import namedtuple es la primera importación de este curso. Las importaciones se cubren a fondo en el capítulo 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

sofia = Player("Sofía", 87, 5)
sofia.name    # "Sofía"
sofia.score   # 87

Conjuntos

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

set es una colección no ordenada que rechaza automáticamente los duplicados. La verificación de pertenencia es O(1) sin importar el tamaño, lo que la convierte en la herramienta adecuada siempre que necesites comprobar si un valor existe dentro de una colección grande. Nota: {} crea un diccionario vacío, no un conjunto vacío; usa set() para eso.

set es una colección respaldada por una tabla hash de objetos hashables únicos. La verificación de pertenencia, la inserción y la eliminación son todas O(1) en promedio. El orden de iteración refleja las posiciones internas del hash y no es estable entre ejecuciones. Solo los objetos hashables pueden ser miembros: int, str, tuple sí; list, dict, set no. {} se analiza como un literal de diccionario vacío, no como un conjunto.

python
tags     = {"python", "beginner", "tutorial"}
numbers  = {1, 2, 3, 4, 5}
empty    = set()    # NO {} (eso es un diccionario vacío)

Añadir el mismo valor dos veces no cambia el conjunto:

python
tags.add("python")   # tags no cambia, "python" ya está en él

Cuándo usar un conjunto

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

Tres casos de uso distintos impulsan el uso de conjuntos: deduplicación (automática al insertar), verificación de pertenencia O(1) (frente a O(n) para list) y álgebra de conjuntos (|, &, -, ^). Cuando la colección es grande y compruebas la pertenencia con frecuencia, la diferencia de rendimiento es sustancial.

Los tres casos de uso canónicos de los conjuntos se corresponden directamente con propiedades de la tabla hash: unicidad (rechazo de duplicados al insertar), __contains__ promedio O(1) (búsqueda hash) y álgebra de conjuntos (operadores estilo bitwise que invocan métodos dunder). La prueba de pertenencia O(1) es la más importante en la práctica: in sobre un conjunto de 10.000 elementos es tan rápido como sobre uno de 10.

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 pertenencia
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 con conjuntos

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

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

Los operadores de conjuntos llaman a métodos dunder: | llama a __or__, & llama a __and__, - llama a __sub__, ^ llama a __xor__. Los operadores requieren que ambos operandos sean conjuntos y lanzan TypeError si no. Las formas de método aceptan cualquier iterable y lo convierten internamente. Las formas in-place (|=, &=, -=, ^=) mutan el operando izquierdo, equivalentes a .update(), .intersection_update(), etc.

python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b    # {1, 2, 3, 4, 5, 6}   (unión: todo en cualquiera)
a & b    # {3, 4}               (intersección: solo en ambos)
a - b    # {1, 2}               (diferencia: en a pero no en b)
b - a    # {5, 6}               (diferencia en el otro sentido)
a ^ b    # {1, 2, 5, 6}        (diferencia simétrica: en uno pero no en ambos)

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

Modificación de conjuntos

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

.add() es O(1) en promedio. .update() acepta cualquier iterable y equivale a llamar a .add() en un bucle. .remove() lanza KeyError si no encuentra el elemento, reflejando dict.__delitem__. .discard() es la opción segura cuando la presencia es incierta. .pop() elimina un elemento arbitrario, no el "último", ya que los conjuntos no tienen orden.

.add(x) calcula el hash de x y lo inserta en la tabla: O(1) en promedio. .update(iterable) es equivalente a |=. .remove() lanza KeyError si no encuentra el elemento. .discard() realiza primero una búsqueda hash y omite la eliminación si no lo encuentra. .pop() elimina un elemento arbitrario determinado por el estado interno de la tabla hash, no por el orden de inserción.

python
tags = {"python", "beginner"}

tags.add("tutorial")          # añadir un elemento
tags.update(["web", "api"])   # añadir múltiples elementos desde cualquier iterable
tags.remove("beginner")       # eliminar, lanza KeyError si no se encuentra
tags.discard("missing")       # 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.

Conjuntos congelados

Un conjunto congelado es un conjunto que no puedes modificar después de crearlo. La razón principal para usarlo: los conjuntos congelados son hashables, por lo que se pueden usar como claves de diccionario o almacenarse dentro de otros conjuntos.

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

frozenset implementa __hash__ calculado a partir de una reducción ordenada de los hashes de sus elementos, dándole un valor hash estable. Todos los operadores y métodos de álgebra de conjuntos que devuelven nuevas colecciones están soportados; los métodos de mutación (add, remove, etc.) no están definidos. frozenset es el tipo adecuado para una tabla de búsqueda constante que no debe cambiar en tiempo de ejecución y que puede necesitar usarse como clave de diccionario.

python
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived")    # AttributeError, frozenset es inmutable

Elegir la colección adecuada

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

La elección entre tipos de colección depende de qué operaciones importan y qué restricciones tienen tus datos: mutabilidad, ordenamiento, manejo de duplicados y estrategia de búsqueda.

La elección de la colección es una decisión de rendimiento y semántica. dict y set ofrecen búsqueda promedio O(1) mediante hashing. list y tuple ofrecen acceso indexado O(1) pero verificación de pertenencia O(n). La inmutabilidad de tuple le otorga hashabilidad. frozenset y tuple son los dos tipos compuestos hashables de la biblioteca estándar.

listtuplesetdict
OrdenadoNoSí (orden de inserción)
MutableNo
DuplicadosNoNo (claves)
Acceso porÍndiceÍndicen/aClave
Usar cuandoSecuencia ordenada y modificableRegistro fijoValores únicos, pertenencia rápidaBúsqueda 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 pertenencia rápidas? → set

En la práctica

Usando tuplas para almacenar registros fijos y un conjunto para llevar un control de valores únicos:

python
home   = (51.5074, -0.1278)   # latitud, longitud
office = (51.5155, -0.0922)

home_lat, home_lon = home
print(f"Casa: {home_lat}, {home_lon}")

# Llevar un control de visitantes únicos con un conjunto
visitors = set()
visitors.add("sofia")
visitors.add("mateo")
visitors.add("sofia")    # ya está en el conjunto, se ignora silenciosamente
visitors.add("camila")

print(f"Visitantes únicos: {len(visitors)}")
print(f"sofia visitó: {'sofia' in visitors}")
print(f"diego visitó:  {'diego' in visitors}")

Usando conjuntos para llevar un registro de lo ya procesado y calcular el trabajo restante:

python
already_processed = {"report_jan.csv", "report_feb.csv"}
all_files         = {"report_jan.csv", "report_feb.csv", "report_mar.csv", "report_apr.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 procesado: {len(already_processed)}")

Usando frozenset para tablas de búsqueda constantes y demostrando la verificación de pertenencia 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 regular
unsafe_allowed = ALLOWED_METHODS - SAFE_METHODS
print(f"Métodos permitidos no seguros: {unsafe_allowed}")

# frozenset es hashable, así que puede almacenarse en 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 mantiene la búsqueda O(1) y puede almacenarse 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.