Python
Tuplas, diccionarios y conjuntos
Las otras tres colecciones del lenguaje — cuándo conviene una tupla, cómo buscar por clave en un diccionario y para qué sirve un conjunto.
Tuplas: valores que no cambian
En Listas viste que una lista es mutable: puedes agregarle elementos, quitarlos o reemplazarlos en cualquier momento. Una tupla es lo contrario: una secuencia ordenada de valores que, una vez creada, no se puede modificar.
Se escribe entre paréntesis, con los elementos separados por comas:
punto = (3, 4)
print(punto)
print(punto[0])
print(punto[1])
print(len(punto))
print(type(punto))Guárdalo como tuplas.py y ejecútalo:
python tuplas.py(3, 4)
3
4
2
<class 'tuple'>Todo lo que sabes de índices y de slicing funciona igual sobre una tupla, porque también es una secuencia. Lo único que cambia es que no puedes escribir en ella:
punto = (3, 4)
print(punto[0]) # leer sí se puede
# punto[0] = 99 # esto falla: TypeError
nuevo = (99, punto[1]) # para "cambiarla" hay que construir otra
print(nuevo)3
(99, 4)Si descomentas la línea del medio, el programa se detiene con TypeError: 'tuple' object does not support item assignment, exactamente el mismo error que da intentar modificar una cadena de texto. No es casualidad: las cadenas y las tuplas comparten esa característica de ser inmutables.
Para qué sirve una tupla si ya existen las listas
La pregunta es razonable: si una lista hace todo lo que hace una tupla y además se puede modificar, ¿por qué molestarse? La respuesta no es técnica, es de intención.
Usa una tupla cuando se cumplan estas dos cosas:
- La cantidad y el orden de los elementos son fijos. Un punto en el plano siempre tiene dos números, y el primero es la coordenada horizontal. Un registro de “nombre, edad, ciudad” siempre tiene esos tres campos, en ese orden.
- Conceptualmente es un solo valor compuesto, no una colección de cosas parecidas.
[6.5, 3.0, 4.5]son tres notas del mismo tipo, y mañana pueden ser cuatro: eso es una lista.(3, 4)no son “dos números”, es un punto: eso es una tupla.
Una lista de notas crece y se ordena; un punto no se “ordena” ni se le “agrega un elemento”. Elegir la tupla comunica esa diferencia a quien lea el código, y además impide modificarla por accidente.
Hay un beneficio práctico extra: como una tupla no cambia, se puede usar como clave de un diccionario, cosa que una lista no puede. Eso lo verás más abajo en esta misma página.
Desempaquetar una tupla
Si una tupla es un valor compuesto, lo natural es querer repartir sus partes en variables sueltas. Eso se llama desempaquetado y se escribe poniendo varios nombres a la izquierda del =:
punto = (3, 4)
x, y = punto
print("x =", x)
print("y =", y)
# El mismo mecanismo sirve para intercambiar dos variables
a = 1
b = 2
a, b = b, a
print(a, b)x = 3
y = 4
2 1Ese a, b = b, a es un modismo muy típico de Python: intercambia el contenido de dos variables sin necesitar una tercera variable auxiliar. Funciona porque el lado derecho se evalúa entero y primero, construyendo la tupla (2, 1), y recién después se reparte sobre los nombres de la izquierda.
El desempaquetado también explica cómo una función puede devolver varios valores a la vez, algo que quedó pendiente en Devolver un resultado con return:
def dividir(a, b):
return a // b, a % b
cociente, resto = dividir(17, 5)
print("cociente:", cociente)
print("resto:", resto)
print(dividir(17, 5))cociente: 3
resto: 2
(3, 2)La última línea revela el truco: la función no devuelve “dos cosas”, devuelve una tupla (3, 2). Es el desempaquetado del lado izquierdo el que la reparte en dos variables. Los operadores // y % son la división entera y el resto que ya conoces.
Lo que crea una tupla es la coma, no el paréntesis
Aquí hay una sutileza que conviene fijar bien, porque explica errores que de otro modo parecen magia negra: lo que convierte a 3, 4 en una tupla es la coma, no los paréntesis. Los paréntesis solo agrupan, y se escriben por claridad.
con_parentesis = (3, 4)
sin_parentesis = 3, 4
un_solo_elemento = (5,) # la coma final es obligatoria
no_es_tupla = (5) # esto es solo el número 5 entre paréntesis
print(con_parentesis, type(con_parentesis))
print(sin_parentesis, type(sin_parentesis))
print(un_solo_elemento, type(un_solo_elemento))
print(no_es_tupla, type(no_es_tupla))Guárdalo como coma.py y ejecútalo:
python coma.py(3, 4) <class 'tuple'>
(3, 4) <class 'tuple'>
(5,) <class 'tuple'>
5 <class 'int'>Las dos primeras líneas son idénticas: con y sin paréntesis, el resultado es la misma tupla. Las dos últimas son las que sorprenden: (5,) con coma es una tupla de un elemento, mientras que (5) sin coma es simplemente el número 5, porque ahí los paréntesis están agrupando una expresión, no creando nada.
Esto explica un error silencioso que ya te habían advertido: escribir un decimal con coma en vez de punto, como altura = 1,75, no da error porque Python ve una coma y construye la tupla (1, 75). El programa sigue tan campante y falla mucho más adelante, cuando intentas hacer cuentas con esa variable. Una coma de más, en cualquier asignación, convierte el valor en una tupla sin avisar.
Vale la pena poner los tres usos de () uno al lado del otro, porque son distintos y ya los viste todos:
| Lo que escribes | Qué hacen los paréntesis ahí | Dónde apareció |
|---|---|---|
print("Hola") | Llamar a una función | Qué es una función |
(a + b) / 2 | Agrupar una expresión aritmética | Operadores aritméticos |
(3, 4) | Nada: la tupla la crea la coma | Esta sección |
Diccionarios: buscar por clave, no por posición
Una lista y una tupla se acceden por posición: notas[0], punto[1]. Eso funciona mientras la posición signifique algo. Pero si quieres guardar los datos de una persona, datos[0] no dice nada: hay que recordar de memoria que la posición 0 era el nombre y la 1 la edad.
Un diccionario resuelve eso: guarda pares de clave y valor, y se accede por la clave. Se escribe entre llaves {}, con dos puntos separando cada clave de su valor:
persona = {"nombre": "Ana", "edad": 30, "ciudad": "Santiago"}
print(persona)
print(persona["nombre"])
print(persona["edad"])
print(len(persona))Guárdalo como diccionario.py y ejecútalo:
python diccionario.py{'nombre': 'Ana', 'edad': 30, 'ciudad': 'Santiago'}
Ana
30
3Fíjate en que se accede con corchetes, igual que una lista, pero con la clave adentro en vez de un número: persona["nombre"]. Y que len() devuelve la cantidad de pares, no la de caracteres ni la de valores sueltos.
Las claves suelen ser cadenas de texto, y por eso van entre comillas. persona["nombre"] usa la clave "nombre", que no tiene ninguna relación con una variable llamada nombre. Olvidar las comillas dentro de los corchetes es un error frecuente: Python buscaría entonces una variable con ese nombre y respondería NameError.
Leer un valor: corchetes o .get()
Pedir una clave que no existe con corchetes detiene el programa. El método .get() hace lo mismo pero sin romper nada:
persona = {"nombre": "Ana", "edad": 30}
print(persona.get("nombre"))
print(persona.get("telefono"))
print(persona.get("telefono", "sin teléfono"))
# print(persona["telefono"]) # esto falla: KeyErrorAna
None
sin teléfonoTres comportamientos distintos en tres líneas:
.get(clave)devuelve el valor si la clave existe.- Si no existe, devuelve
None—el mismo valor “nada” que devuelve una función sinreturn— en vez de lanzar un error. .get(clave, valor_por_defecto)devuelve ese segundo argumento cuando la clave falta. Es la forma de decir “si no está, usa esto”.
Si descomentas la última línea, el programa se detiene con KeyError: 'telefono'. Es el equivalente en diccionarios del IndexError de las listas.
Usa corchetes cuando la clave tiene que estar (si falta, es un bug y quieres enterarte de inmediato) y .get() cuando la ausencia es un caso normal y previsto. No es que uno sea mejor que el otro: expresan intenciones distintas.
Agregar, modificar y eliminar claves
Un diccionario es mutable, como una lista. Y hay un solo gesto para agregar y para modificar: asignar sobre la clave.
persona = {"nombre": "Ana", "edad": 30}
persona["ciudad"] = "Santiago" # la clave no existía: se agrega
persona["edad"] = 31 # la clave ya existía: se reemplaza
print(persona)
del persona["ciudad"] # elimina el par completo
print(persona)
print("nombre" in persona)
print("telefono" in persona)Guárdalo como modificar_dict.py y ejecútalo:
python modificar_dict.py{'nombre': 'Ana', 'edad': 31, 'ciudad': 'Santiago'}
{'nombre': 'Ana', 'edad': 31}
True
FalseDos cosas nuevas en ese ejemplo:
deles una palabra clave que elimina algo. Aplicada apersona["ciudad"], borra la clave y su valor de una vez.- El operador
inpregunta “¿está esto dentro?” y devuelve unbool. Sobre un diccionario busca en las claves, nunca en los valores:"Ana" in personaseríaFalseaunque"Ana"sea uno de los valores.
No hay ninguna diferencia de escritura entre agregar una clave nueva y sobrescribir una existente: persona["edad"] = 31 hace lo primero o lo segundo según lo que hubiera antes, sin avisar. Si escribes mal el nombre de una clave (persona["edda"] = 31), no obtienes ningún error: obtienes un diccionario con una clave de más y el dato original intacto. Este es el error típico de los diccionarios, y es silencioso.
Recorrer un diccionario con .keys(), .values() y .items()
Recorrer un diccionario es el mismo for de siempre; lo único que hay que decidir es qué quieres recorrer: las claves, los valores o ambos a la vez.
persona = {"nombre": "Ana", "edad": 30, "ciudad": "Santiago"}
for clave in persona.keys():
print(clave)
for valor in persona.values():
print(valor)
for clave, valor in persona.items():
print(clave, "->", valor)Guárdalo como recorrer_dict.py y ejecútalo:
python recorrer_dict.pynombre
edad
ciudad
Ana
30
Santiago
nombre -> Ana
edad -> 30
ciudad -> Santiago.keys()entrega solo las claves..values()entrega solo los valores..items()entrega cada par como una tupla(clave, valor). Por eso elforpuede escribir dos variables (for clave, valor in ...): es exactamente el desempaquetado del principio de esta página, aplicado una vez por vuelta. Es también lo mismo que hacíaenumerate()en las listas.
for clave in persona: sin .keys() hace exactamente lo mismo que con él: recorrer un diccionario a secas recorre sus claves. Escribir .keys() es opcional y se usa cuando quieres dejarlo explícito para quien lee.
Las claves tienen que ser inmutables
No cualquier valor puede ser clave. La regla práctica es: una clave tiene que ser de un tipo inmutable. Cadenas, números y tuplas sirven; listas, diccionarios y conjuntos no.
agenda = {
"lunes": "gimnasio",
3: "tercer día",
(0, 0): "el origen",
}
print(agenda["lunes"])
print(agenda[3])
print(agenda[(0, 0)])
# agenda[[1, 2]] = "no vale" # esto falla: TypeErrorgimnasio
tercer día
el origenLos valores, en cambio, pueden ser cualquier cosa: una lista, otro diccionario, lo que sea. La restricción es solo para las claves.
Si intentas usar una lista como clave, el error es TypeError: unhashable type: 'list'. El motivo de fondo tiene que ver con cómo Python calcula internamente dónde guardar cada par, y no hace falta entenderlo para trabajar con diccionarios: alcanza con la regla de arriba. Y es justamente aquí donde una tupla se vuelve insustituible, porque es la única forma de usar un valor compuesto (unas coordenadas, un par “año y mes”) como clave.
Las claves de un diccionario son únicas: si escribes la misma clave dos veces al crearlo, la última gana silenciosamente. {"a": 1, "a": 2} es simplemente {'a': 2}.
Conjuntos: valores únicos y sin orden
Un conjunto (set) es una colección de valores sin duplicados y sin un orden garantizado. Se escribe entre llaves, igual que un diccionario, pero con valores sueltos en vez de pares clave: valor:
numeros = {1, 2, 3}
repetidos = {1, 2, 2, 3, 3, 3}
print(numeros)
print(repetidos)
print(len(repetidos))
print(2 in numeros)Guárdalo como conjuntos.py y ejecútalo:
python conjuntos.py{1, 2, 3}
{1, 2, 3}
3
TrueLos seis valores de repetidos quedaron en tres. No hubo que filtrar nada: el conjunto simplemente no admite repetidos, así que los duplicados se descartan al crearlo.
Comparado con una lista, un conjunto pierde dos cosas y gana otras dos:
- Pierde el orden: no puedes hacer
numeros[0], porque no hay una “posición 0”. Intentarlo daTypeError. - Pierde los duplicados, que a veces son información válida (tres votos iguales son tres votos).
- Gana la garantía de unicidad sin escribir código para conseguirla.
- Gana velocidad al comprobar pertenencia con
in: en una lista larga,x in listarecorre elemento por elemento; en un conjunto la respuesta es prácticamente inmediata sin importar cuántos elementos haya.
En el ejemplo los números salieron ordenados, pero eso es una coincidencia de cómo Python guarda los enteros pequeños, no una promesa. Nunca escribas código que dependa del orden en que se imprime un conjunto. Si necesitas un orden concreto, conviértelo a lista ordenada con sorted().
Quitar duplicados y comprobar pertenencia
Los dos usos que justifican el 90 % de los conjuntos que verás en código real:
votos = [3, 1, 2, 3, 1, 1, 5]
unicos = set(votos)
print(unicos)
ordenados = sorted(set(votos))
print(ordenados)
print(len(votos), "votos,", len(unicos), "opciones distintas"){1, 2, 3, 5}
[1, 2, 3, 5]
7 votos, 4 opciones distintasset(lista) construye un conjunto a partir de una lista y, de paso, elimina los repetidos. Combinado con sorted() —la función que ya viste en los métodos de lista— tienes el modismo estándar para “dame los valores distintos, ordenados”: sorted(set(lista)), que devuelve una lista nueva.
Y las operaciones clásicas de conjuntos, que Python escribe con símbolos:
a = {1, 2, 3}
b = {3, 4, 5}
a.add(4)
a.add(4) # ya estaba: no pasa nada, no da error
print(a)
print(a | b) # unión: lo que está en cualquiera de los dos
print(a & b) # intersección: lo que está en ambos
print(a - b) # diferencia: lo que está en a y no en bGuárdalo como operaciones_set.py y ejecútalo:
python operaciones_set.py{1, 2, 3, 4}
{1, 2, 3, 4, 5}
{3, 4}
{1, 2}.add() es el .append() de los conjuntos, con una diferencia importante: agregar algo que ya estaba no hace nada ni da error. Esa tolerancia es justamente lo que los hace cómodos para acumular valores únicos dentro de un bucle.
El conjunto vacío se escribe set()
{} no es un conjunto vacío: es un diccionario vacío. Para crear un conjunto vacío hay que escribir set(). No hay ninguna otra forma.
Esta es una de las trampas más citadas del lenguaje, y la razón es histórica y simple: los diccionarios existían en Python antes que los conjuntos, así que se quedaron con las llaves vacías. Cuando llegaron los conjuntos, {} ya estaba ocupado.
vacio_llaves = {}
vacio_set = set()
print(type(vacio_llaves))
print(type(vacio_set))
vacio_set.add(1)
print(vacio_set)
# vacio_llaves.add(1) # esto falla: AttributeError<class 'dict'>
<class 'set'>
{1}El error que aparece si descomentas la última línea es AttributeError: 'dict' object has no attribute 'add', y es un mensaje engañoso: parece decir que .add() no existe, cuando el problema real es que tu “conjunto” nunca fue un conjunto. Cada vez que veas ese error, revisa cómo inicializaste la variable.
El patrón general: en Python, {} significa cosas distintas según lo que tenga adentro. Con pares clave: valor es un diccionario; con valores sueltos separados por comas es un conjunto; y vacío es un diccionario por convención histórica. Es la misma idea que ya viste con : en el slicing: el contexto determina el significado, no el símbolo. Todos estos símbolos se recopilan ordenados, uno por uno, en la página de repaso de sintaxis que cierra esta sección.
Cuál de las cuatro colecciones usar
Con esta página ya conoces las cuatro colecciones básicas de Python. Esta tabla las resume:
| Colección | Se escribe | ¿Ordenada? | ¿Mutable? | ¿Admite duplicados? | Se accede por |
|---|---|---|---|---|---|
| Lista | [1, 2, 3] | Sí | Sí | Sí | Índice |
| Tupla | (1, 2, 3) | Sí | No | Sí | Índice |
| Diccionario | {"a": 1} | Sí (orden de inserción) | Sí | Claves no, valores sí | Clave |
| Conjunto | {1, 2, 3} | No | Sí | No | Solo con in |
Para elegir, tres preguntas en este orden:
- ¿Necesito buscar los datos por un nombre o identificador? → diccionario.
- ¿Solo me importa si algo está o no está, sin repetidos? → conjunto.
- Si no, ¿esto va a cambiar de tamaño o de contenido? → lista si sí, tupla si no.
Ejemplo completo: contar palabras
Este programa usa las tres estructuras nuevas a la vez, junto con el for y las listas que ya conocías:
frase = "el perro y el gato y el pez"
palabras = frase.split()
print(palabras)
conteo = {}
for palabra in palabras:
conteo[palabra] = conteo.get(palabra, 0) + 1
for palabra, veces in conteo.items():
print(palabra, "->", veces)
print("Palabras distintas:", len(set(palabras)))Guárdalo como contar_palabras.py y ejecútalo:
python contar_palabras.py['el', 'perro', 'y', 'el', 'gato', 'y', 'el', 'pez']
el -> 3
perro -> 1
y -> 2
gato -> 1
pez -> 1
Palabras distintas: 5Lo que hace funcionar este programa:
.split()es un método de las cadenas que las parte por los espacios y devuelve una lista de palabras. Es la forma habitual de pasar de texto suelto a datos con los que se puede trabajar.conteo.get(palabra, 0) + 1es el corazón del conteo, y es donde.get()brilla: si la palabra ya se vio, devuelve su cuenta actual; si es la primera vez, devuelve0. En ambos casos se le suma 1 y se guarda. Con corchetes habría que escribir unifpara distinguir el primer caso, o el programa fallaría conKeyErroren la primera palabra.- El diccionario
conteoempieza vacío ({}, que aquí sí es lo que queremos: un diccionario vacío) y crece con una clave por palabra distinta. len(set(palabras))cuenta cuántas palabras distintas hay, sin escribir ni un bucle: el conjunto descarta los repetidos ylen()cuenta lo que queda.
Siguiente paso
Ya sabes agrupar datos en cuatro estructuras distintas, pero todas ellas guardan solo datos. Un diccionario como {"titular": "Ana", "saldo": 1000} describe una cuenta bancaria, pero no sabe depositar ni retirar: esas operaciones viven sueltas en funciones, lejos de los datos sobre los que trabajan.
Las clases resuelven eso, permitiéndote crear tus propios tipos que agrupan los datos y el comportamiento que va con ellos: continúa con Clases: programación orientada a objetos en Python.