</>waridocu

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.

pythontuplasdiccionariosconjuntoscolecciones

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:

Ejemplo: crear una tupla y leerla por índicepython
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:

bashbash
python tuplas.py
texttext
(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:

Ejemplo: una tupla no se puede modificar por dentropython
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)
texttext
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.

Tip

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 =:

Ejemplo: desempaquetar una tupla en variables sueltaspython
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)
texttext
x = 3
y = 4
2 1

Ese 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:

Ejemplo: una función que devuelve dos valores a la vezpython
def dividir(a, b):
    return a // b, a % b


cociente, resto = dividir(17, 5)

print("cociente:", cociente)
print("resto:", resto)
print(dividir(17, 5))
texttext
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.

Ejemplo: lo que crea la tupla es la comapython
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:

bashbash
python coma.py
texttext
(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.

Advertencia

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 escribesQué hacen los paréntesis ahíDónde apareció
print("Hola")Llamar a una funciónQué es una función
(a + b) / 2Agrupar una expresión aritméticaOperadores aritméticos
(3, 4)Nada: la tupla la crea la comaEsta 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:

Ejemplo: crear un diccionario y leer un valor por su clavepython
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:

bashbash
python diccionario.py
texttext
{'nombre': 'Ana', 'edad': 30, 'ciudad': 'Santiago'}
Ana
30
3

Fí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.

Nota

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:

Ejemplo: [] falla si la clave no existe, .get() nopython
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: KeyError
texttext
Ana
None
sin teléfono

Tres 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 sin return— 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.

Tip

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.

Ejemplo: agregar, modificar y eliminar clavespython
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:

bashbash
python modificar_dict.py
texttext
{'nombre': 'Ana', 'edad': 31, 'ciudad': 'Santiago'}
{'nombre': 'Ana', 'edad': 31}
True
False

Dos cosas nuevas en ese ejemplo:

  • del es una palabra clave que elimina algo. Aplicada a persona["ciudad"], borra la clave y su valor de una vez.
  • El operador in pregunta “¿está esto dentro?” y devuelve un bool. Sobre un diccionario busca en las claves, nunca en los valores: "Ana" in persona sería False aunque "Ana" sea uno de los valores.
Advertencia

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.

Ejemplo: recorrer un diccionario de tres formaspython
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:

bashbash
python recorrer_dict.py
texttext
nombre
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 el for puede 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ía enumerate() en las listas.
Nota

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.

Ejemplo: qué puede ser una clave y qué nopython
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: TypeError
texttext
gimnasio
tercer día
el origen

Los 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.

Nota

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:

Ejemplo: un conjunto descarta los duplicados solopython
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:

bashbash
python conjuntos.py
texttext
{1, 2, 3}
{1, 2, 3}
3
True

Los 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 da TypeError.
  • 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 lista recorre elemento por elemento; en un conjunto la respuesta es prácticamente inmediata sin importar cuántos elementos haya.
Nota

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:

Ejemplo: quitar duplicados de una lista y contar valores distintospython
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")
texttext
{1, 2, 3, 5}
[1, 2, 3, 5]
7 votos, 4 opciones distintas

set(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:

Ejemplo: agregar elementos y comparar dos conjuntospython
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 b

Guárdalo como operaciones_set.py y ejecútalo:

bashbash
python operaciones_set.py
texttext
{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()

Advertencia

{} 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.

Ejemplo: {} vacío es un diccionario, no un conjuntopython
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
texttext
<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.

Importante

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ónSe escribe¿Ordenada?¿Mutable?¿Admite duplicados?Se accede por
Lista[1, 2, 3]Índice
Tupla(1, 2, 3)NoÍndice
Diccionario{"a": 1}Sí (orden de inserción)Claves no, valores síClave
Conjunto{1, 2, 3}NoNoSolo con in

Para elegir, tres preguntas en este orden:

  1. ¿Necesito buscar los datos por un nombre o identificador? → diccionario.
  2. ¿Solo me importa si algo está o no está, sin repetidos? → conjunto.
  3. 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:

Ejemplo: contar cuántas veces aparece cada palabra de una frasepython
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:

bashbash
python contar_palabras.py
texttext
['el', 'perro', 'y', 'el', 'gato', 'y', 'el', 'pez']
el -> 3
perro -> 1
y -> 2
gato -> 1
pez -> 1
Palabras distintas: 5

Lo 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) + 1 es 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, devuelve 0. En ambos casos se le suma 1 y se guarda. Con corchetes habría que escribir un if para distinguir el primer caso, o el programa fallaría con KeyError en la primera palabra.
  • El diccionario conteo empieza 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 y len() 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.