17. Conjuntos: trabajar con datos únicos y no ordenados
En capítulos anteriores, hemos trabajado con listas (colecciones ordenadas y mutables) y diccionarios (mapeos clave-valor). Ahora exploraremos los conjuntos, el tipo de colección de Python diseñado específicamente para almacenar elementos únicos y realizar operaciones matemáticas de conjuntos de forma eficiente.
Los conjuntos son especialmente potentes cuando necesitas eliminar duplicados, comprobar pertenencia rápidamente o realizar operaciones como encontrar elementos comunes entre colecciones. A diferencia de las listas, los conjuntos no están ordenados y no pueden contener valores duplicados: intentar añadir el mismo elemento dos veces no tiene efecto.
17.1) Crear conjuntos y operaciones básicas
17.1.1) Crear conjuntos con llaves
La forma más común de crear un conjunto es usando llaves {} con valores separados por comas:
# Creación de un conjunto de lenguajes de programación
languages = {"Python", "JavaScript", "Java", "C++"}
print(languages) # Output: {'Python', 'JavaScript', 'Java', 'C++'}
print(type(languages)) # Output: <class 'set'>Importante: El orden de los elementos cuando imprimes un conjunto puede diferir del orden en que los introdujiste. Los conjuntos son colecciones no ordenadas, lo que significa que Python no mantiene ninguna secuencia en particular:
numbers = {5, 2, 8, 1, 9}
print(numbers) # Output might be: {1, 2, 5, 8, 9} or another orderEl orden de salida puede variar entre ejecuciones y versiones de Python. Nunca dependas de que los conjuntos mantengan un orden específico; si el orden importa, usa una lista en su lugar.
17.1.2) Los conjuntos eliminan duplicados automáticamente
Una de las propiedades más útiles de los conjuntos es que eliminan automáticamente los valores duplicados. Si intentas crear un conjunto con elementos duplicados, solo se conserva una copia de cada valor único:
# Creación de un conjunto con valores duplicados
student_ids = {101, 102, 103, 102, 101, 104}
print(student_ids) # Output: {101, 102, 103, 104}
# Esta propiedad hace que los conjuntos sean perfectos para eliminar duplicados
grades = [85, 90, 85, 78, 90, 92, 78, 85]
unique_grades = set(grades)
print(unique_grades) # Output: {78, 85, 90, 92}Esta deduplicación automática ocurre porque los conjuntos usan un modelo matemático de conjuntos donde cada elemento solo puede aparecer una vez. Cuando añades un valor que ya existe, el conjunto simplemente ignora el duplicado.
17.1.3) Crear conjuntos con el constructor set()
Puedes crear conjuntos a partir de otros iterables usando el constructor set(). Esto es especialmente útil para convertir listas, tuplas o cadenas en conjuntos:
# Creación de un conjunto a partir de una lista
colors_list = ["red", "blue", "green", "red", "yellow"]
colors_set = set(colors_list)
print(colors_set) # Output: {'red', 'blue', 'green', 'yellow'}
# Creación de un conjunto a partir de una cadena (cada carácter se convierte en un elemento)
letters = set("programming")
print(letters) # Output: {'p', 'r', 'o', 'g', 'a', 'm', 'i', 'n'}
# Creación de un conjunto a partir de una tupla
coordinates = set((10, 20, 30, 20, 10))
print(coordinates) # Output: {10, 20, 30}Cuando creas un conjunto a partir de una cadena, cada carácter único se convierte en un elemento separado. Esto es útil para encontrar todos los caracteres distintos en un texto:
text = "Mississippi"
unique_chars = set(text.lower())
print(unique_chars) # Output: {'m', 'i', 's', 'p'}
print(f"The word contains {len(unique_chars)} unique letters")
# Output: The word contains 4 unique letters17.1.4) Crear un conjunto vacío
Aquí hay un detalle crítico: no puedes crear un conjunto vacío usando {} porque Python interpreta eso como un diccionario vacío. En su lugar, debes usar set():
# WRONG - This creates an empty dictionary, not a set
empty_dict = {}
print(type(empty_dict)) # Output: <class 'dict'>
# CORRECT - This creates an empty set
empty_set = set()
print(type(empty_set)) # Output: <class 'set'>
print(empty_set) # Output: set()Esta distinción existe porque los diccionarios se añadieron a Python antes que los conjuntos, así que {} ya estaba reservado para los diccionarios vacíos. Cuando imprimes un conjunto vacío, Python lo muestra como set() para evitar confusiones.
Confusión común de principiantes: Cuando creas un conjunto con un solo elemento usando una variable, el conjunto contiene el valor de la variable, no el nombre de la variable:
# Comprender la creación de conjuntos con variables
x = 5
my_set = {x} # Crea {5}, no {'x'}
print(my_set) # Output: {5}
# Si quieres un conjunto que contenga la cadena 'x':
my_set = {'x'}
print(my_set) # Output: {'x'}
# Esto se aplica a cualquier expresión
result = 10 + 5
my_set = {result} # Crea {15}
print(my_set) # Output: {15}17.1.5) Propiedades y operaciones básicas de los conjuntos
Los conjuntos admiten varias operaciones fundamentales que los hacen útiles para el procesamiento de datos:
# Comprobar el número de elementos únicos
website_visitors = {"alice", "bob", "charlie", "alice", "david"}
print(f"Unique visitors: {len(website_visitors)}")
# Output: Unique visitors: 4
# Comprobar pertenencia con 'in' (muy rápido para conjuntos)
if "alice" in website_visitors:
print("Alice visited the website")
# Output: Alice visited the website
# Comprobar no pertenencia
if "eve" not in website_visitors:
print("Eve has not visited yet")
# Output: Eve has not visited yetLas pruebas de pertenencia con in son una de las ventajas clave de los conjuntos. Para colecciones grandes, comprobar si un elemento existe en un conjunto es mucho más rápido que comprobarlo en una lista. Exploraremos por qué esto importa en la Sección 17.5.
17.2) Añadir y eliminar elementos de los conjuntos
A diferencia de las tuplas (que son inmutables), los conjuntos son mutables: puedes añadir y eliminar elementos después de su creación. Sin embargo, los propios elementos deben ser de tipos inmutables (exploraremos esta restricción en la Sección 17.7).
17.2.1) Añadir elementos individuales con add()
Añadir elementos individuales a un conjunto es sencillo con el método add(). Si el elemento ya existe, el conjunto permanece sin cambios: no se lanza ningún error y no se crea ningún duplicado:
# Construir un conjunto de tareas completadas
completed_tasks = {"task1", "task2"}
print(completed_tasks) # Output: {'task1', 'task2'}
# Añadir una tarea nueva
completed_tasks.add("task3")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}
# Añadir un duplicado no tiene efecto
completed_tasks.add("task1")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}Este comportamiento hace que los conjuntos sean ideales para registrar ocurrencias únicas. Puedes llamar a add() con seguridad sin comprobar si el elemento ya existe: el conjunto gestiona los duplicados automáticamente.
17.2.2) Añadir varios elementos con update()
Para añadir varios elementos a la vez, usa update(), que acepta cualquier iterable (lista, tupla, otro conjunto, etc.) y añade todos sus elementos al conjunto:
# Empezar con un conjunto pequeño de habilidades
skills = {"Python", "SQL"}
print(skills) # Output: {'Python', 'SQL'}
# Añadir varias habilidades desde una lista
new_skills = ["JavaScript", "Docker", "Python"]
skills.update(new_skills)
print(skills) # Output: {'Python', 'SQL', 'JavaScript', 'Docker'}Fíjate en que "Python" apareció tanto en el conjunto original como en la lista que se añade, pero el conjunto sigue conteniendo solo una copia. El método update() puede aceptar múltiples iterables como argumentos:
# Combinar habilidades de múltiples fuentes
current_skills = {"Python"}
course_skills = ["JavaScript", "HTML"]
job_requirements = {"SQL", "Python", "Docker"}
current_skills.update(course_skills, job_requirements)
print(current_skills)
# Output: {'Python', 'JavaScript', 'HTML', 'SQL', 'Docker'}17.2.3) Eliminar elementos con remove()
Eliminar elementos requiere cuidado. El método remove() borra un elemento de un conjunto, pero lanza un KeyError si el elemento no existe:
# Gestionar usuarios activos
active_users = {"alice", "bob", "charlie", "david"}
# Eliminar un usuario que cerró sesión
active_users.remove("bob")
print(active_users) # Output: {'alice', 'charlie', 'david'}
# Intentar eliminar un elemento inexistente provoca un error
# active_users.remove("eve") # Raises: KeyError: 'eve'Como remove() lanza un error cuando faltan elementos, es mejor usarlo cuando estás seguro de que el elemento existe, o cuando quieres capturar el error si no existe:
# Eliminación segura con manejo de errores (aprenderemos más sobre try/except en el Capítulo 28)
users = {"alice", "bob", "charlie"}
user_to_remove = "david"
if user_to_remove in users:
users.remove(user_to_remove)
print(f"Removed {user_to_remove}")
else:
print(f"{user_to_remove} was not in the set")
# Output: david was not in the set17.2.4) Eliminar elementos de forma segura con discard()
Para una eliminación más segura que no lance errores, discard() proporciona una alternativa tolerante. Elimina el elemento si está presente, pero no hace nada si el elemento no existe:
# Gestionar un carrito de compras
cart_items = {"apple", "banana", "orange"}
# Eliminar elementos de forma segura (sin error si el elemento no existe)
cart_items.discard("banana")
print(cart_items) # Output: {'apple', 'orange'}
cart_items.discard("grape") # No error, even though grape isn't in the set
print(cart_items) # Output: {'apple', 'orange'}Usa discard() cuando quieras asegurarte de que un elemento no esté en el conjunto, independientemente de si estaba ahí al principio. Usa remove() cuando la ausencia del elemento indique una condición de error que quieras capturar.
17.2.5) Eliminar y devolver un elemento arbitrario con pop()
El método pop() elimina y devuelve un elemento arbitrario del conjunto. Como los conjuntos no están ordenados, no puedes predecir qué elemento se eliminará:
# Procesar una cola de tareas pendientes (el orden no importa)
pending_tasks = {"email", "report", "meeting", "review"}
# Procesar una tarea (no nos importa cuál)
task = pending_tasks.pop()
print(f"Processing: {task}") # Output: Processing: email (or another task)
print(f"Remaining: {pending_tasks}")
# Output: Remaining: {'report', 'meeting', 'review'} (without the popped task)Si llamas a pop() en un conjunto vacío, lanza un KeyError:
empty_set = set()
# empty_set.pop() # Raises: KeyError: 'pop from an empty set'El método pop() es útil cuando necesitas procesar todos los elementos en un conjunto pero no te importa el orden:
# Procesar todos los elementos en un conjunto
items_to_process = {"item1", "item2", "item3"}
while items_to_process:
item = items_to_process.pop()
print(f"Processing {item}")
# Procesar el elemento...
print("All items processed")
# Output:
# Processing item1
# Processing item2
# Processing item3
# All items processed17.2.6) Eliminar todos los elementos con clear()
El método clear() elimina todos los elementos de un conjunto, dejándolo vacío:
# Restablecer los datos de una sesión
session_data = {"user_id", "timestamp", "ip_address"}
print(session_data) # Output: {'user_id', 'timestamp', 'ip_address'}
session_data.clear()
print(session_data) # Output: set()
print(len(session_data)) # Output: 0Esto es más eficiente que crear un conjunto vacío nuevo si quieres reutilizar el mismo objeto set.
17.3) Operaciones de conjuntos: unión, intersección, diferencia y diferencia simétrica
Los conjuntos admiten operaciones matemáticas de conjuntos que te permiten combinar, comparar y analizar colecciones de forma eficiente. Estas operaciones son fundamentales en la teoría de conjuntos y tienen muchas aplicaciones prácticas en el procesamiento de datos.
17.3.1) Unión: combinar conjuntos
Empecemos con un escenario práctico para entender por qué importa la unión. Imagina que estás gestionando matrículas de estudiantes en diferentes cursos:
# Estudiantes matriculados en diferentes cursos
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Encontrar todos los estudiantes que toman cualquiera de los cursos (o ambos)
all_students = python_students | javascript_students
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}La unión de dos conjuntos contiene todos los elementos que aparecen en cualquiera de los conjuntos (o en ambos). Python proporciona dos formas de calcular uniones: el operador | (mostrado arriba) y el método union():
# Mismo resultado usando el método union()
all_students = python_students.union(javascript_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}El método union() puede aceptar varios conjuntos como argumentos, lo que lo hace conveniente para combinar datos de muchas fuentes:
# Estudiantes en tres cursos diferentes
python_students = {"alice", "bob"}
javascript_students = {"bob", "charlie"}
sql_students = {"charlie", "david"}
# Todos los estudiantes en todos los cursos
all_students = python_students.union(javascript_students, sql_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david'}Otro ejemplo de unión es combinar listas de correo electrónico de distintos departamentos:
# Combinar listas de correo electrónico de distintos departamentos
marketing_contacts = {"alice@company.com", "bob@company.com"}
sales_contacts = {"bob@company.com", "charlie@company.com"}
support_contacts = {"david@company.com", "alice@company.com"}
# Todos los contactos únicos entre departamentos
all_contacts = marketing_contacts | sales_contacts | support_contacts
print(f"Total unique contacts: {len(all_contacts)}")
# Output: Total unique contacts: 417.3.2) Intersección: encontrar elementos comunes
Entender qué elementos aparecen en varios conjuntos es crucial para muchas tareas de análisis de datos. La operación de intersección responde a la pregunta: "¿Qué tienen en común estos conjuntos?"
# Encontrar clientes que compraron ambos productos
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Clientes que compraron ambos productos
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}La intersección contiene solo los elementos que aparecen en ambos conjuntos. También puedes usar el método intersection(), que acepta varios conjuntos:
# Encontrar estudiantes matriculados en los tres cursos
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "charlie", "david"}
sql_students = {"charlie", "eve", "bob"}
# Estudiantes que toman los tres cursos
all_three = python_students.intersection(javascript_students, sql_students)
print(all_three) # Output: {'bob', 'charlie'}Aquí tienes un caso de uso práctico para encontrar productos disponibles en múltiples almacenes:
# Encontrar productos disponibles en múltiples almacenes
warehouse_a = {"laptop", "mouse", "keyboard", "monitor"}
warehouse_b = {"mouse", "keyboard", "printer", "scanner"}
warehouse_c = {"keyboard", "monitor", "mouse", "desk"}
# Productos disponibles en todos los almacenes
available_everywhere = warehouse_a & warehouse_b & warehouse_c
print(f"Available in all locations: {available_everywhere}")
# Output: Available in all locations: {'mouse', 'keyboard'}17.3.3) Diferencia: encontrar elementos en un conjunto pero no en otro
A veces necesitas identificar qué es único de una colección. La operación de diferencia encuentra elementos que están en el primer conjunto pero no en el segundo:
# Gestión de inventario: encontrar discrepancias
expected_items = {"item001", "item002", "item003", "item004"}
actual_items = {"item001", "item003", "item005"}
# Elementos que faltan en el inventario
missing = expected_items - actual_items
print(f"Missing items: {missing}")
# Output: Missing items: {'item002', 'item004'}
# Elementos inesperados en el inventario
unexpected = actual_items - expected_items
print(f"Unexpected items: {unexpected}")
# Output: Unexpected items: {'item005'}También puedes usar el método difference():
# Estudiantes solo en el curso de Python (no en JavaScript)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
python_only = python_students.difference(javascript_students)
print(python_only) # Output: {'alice', 'charlie'}Importante: La operación diferencia no es conmutativa: el orden importa:
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Estudiantes en Python pero no en JavaScript
python_only = python_students - javascript_students
print(f"Python only: {python_only}")
# Output: Python only: {'alice', 'charlie'}
# Estudiantes en JavaScript pero no en Python
javascript_only = javascript_students - python_students
print(f"JavaScript only: {javascript_only}")
# Output: JavaScript only: {'david', 'eve'}17.3.4) Diferencia simétrica: elementos en cualquiera de los conjuntos pero no en ambos
La diferencia simétrica encuentra elementos que están en cualquiera de los conjuntos pero no en ambos. Esta operación es especialmente útil para identificar cambios entre dos versiones:
# Comparar dos versiones de una configuración
old_settings = {"debug", "logging", "cache", "compression"}
new_settings = {"logging", "cache", "monitoring", "security"}
# Ajustes que cambiaron (añadidos o eliminados)
changes = old_settings ^ new_settings
print(f"Changed settings: {changes}")
# Output: Changed settings: {'debug', 'compression', 'monitoring', 'security'}
# Para ver específicamente qué se añadió frente a qué se eliminó:
removed = old_settings - new_settings
added = new_settings - old_settings
print(f"Removed: {removed}") # Output: Removed: {'debug', 'compression'}
print(f"Added: {added}") # Output: Added: {'monitoring', 'security'}También puedes usar el método symmetric_difference():
# Estudiantes en exactamente un curso (no en ambos)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
one_course_only = python_students.symmetric_difference(javascript_students)
print(one_course_only)
# Output: {'alice', 'charlie', 'david', 'eve'}A diferencia de la diferencia, la diferencia simétrica es conmutativa: el orden no importa:
result1 = python_students ^ javascript_students
result2 = javascript_students ^ python_students
print(result1 == result2) # Output: True17.4) Relaciones de subconjunto y superconjunto (issubset, issuperset, isdisjoint)
Más allá de combinar conjuntos, a menudo necesitamos entender las relaciones entre ellos. Python proporciona métodos para comprobar si un conjunto está contenido dentro de otro, contiene a otro, o no comparte elementos con otro.
17.4.1) Probar subconjuntos con issubset() y <=
Un conjunto A es un subconjunto del conjunto B si cada elemento de A también está en B. En otras palabras, B contiene todos los elementos de A (y posiblemente más).
# Prerrequisitos de un curso
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Comprobar si las habilidades básicas son un subconjunto de las habilidades intermedias
print(basic_skills.issubset(intermediate_skills)) # Output: True
print(basic_skills <= intermediate_skills) # Output: True (same result)Un conjunto siempre es un subconjunto de sí mismo:
skills = {"Python", "SQL", "JavaScript"}
print(skills.issubset(skills)) # Output: True
print(skills <= skills) # Output: TrueSi quieres comprobar un subconjunto propio (A es subconjunto de B pero no es igual a B), usa el operador <:
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Subconjunto propio: basic es subconjunto de intermediate Y no son iguales
print(basic_skills < intermediate_skills) # Output: True
# No es un subconjunto propio de sí mismo (son iguales)
print(basic_skills < basic_skills) # Output: FalseUn ejemplo práctico de prueba de subconjuntos es comprobar permisos o requisitos:
# Sistema de permisos de usuario
required_permissions = {"read", "write"}
user_permissions = {"read", "write", "delete", "admin"}
# Comprobar si el usuario tiene todos los permisos requeridos
if required_permissions.issubset(user_permissions):
print("Access granted")
else:
print("Access denied - missing permissions")
# Output: Access granted
# Otro usuario con permisos insuficientes
limited_user = {"read"}
if required_permissions.issubset(limited_user):
print("Access granted")
else:
missing = required_permissions - limited_user
print(f"Access denied - missing: {missing}")
# Output: Access denied - missing: {'write'}17.4.2) Probar superconjuntos con issuperset() y >=
Un conjunto A es un superconjunto del conjunto B si A contiene todos los elementos de B. Esta es la relación inversa de subconjunto: si A es subconjunto de B, entonces B es superconjunto de A.
# Niveles de habilidades
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis", "research"}
# Comprobar si advanced_skills es un superconjunto de basic_skills
print(advanced_skills.issuperset(basic_skills)) # Output: True
print(advanced_skills >= basic_skills) # Output: True (same result)Al igual que con los subconjuntos, un conjunto siempre es un superconjunto de sí mismo:
skills = {"Python", "SQL"}
print(skills.issuperset(skills)) # Output: TruePara un superconjunto propio (A es superconjunto de B pero no es igual a B), usa el operador >:
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis"}
# Superconjunto propio: advanced contiene todo lo de basic Y tiene más
print(advanced_skills > basic_skills) # Output: True
# No es un superconjunto propio de sí mismo
print(advanced_skills > advanced_skills) # Output: False17.4.3) Probar conjuntos disjuntos con isdisjoint()
Dos conjuntos son disjuntos si no tienen elementos en común: su intersección es vacía. El método isdisjoint() devuelve True si los conjuntos no comparten elementos:
# Comprobar conflictos en la programación de horarios
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "art", "music"}
# Comprobar si hay conflictos (la misma clase en ambas sesiones)
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: No scheduling conflictsCuando los conjuntos no son disjuntos:
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "math", "music"}
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: Conflicts: {'math'}Los conjuntos vacíos son disjuntos con todos los conjuntos (incluidos otros conjuntos vacíos):
empty = set()
numbers = {1, 2, 3}
print(empty.isdisjoint(numbers)) # Output: True
print(empty.isdisjoint(empty)) # Output: True17.5) Cuándo usar conjuntos en lugar de listas
Comprender cuándo usar conjuntos frente a listas es crucial para escribir código Python eficiente. Aunque ambos almacenan colecciones de elementos, tienen características diferentes que hacen que cada uno sea adecuado para tareas distintas.
17.5.1) Usa conjuntos para pruebas de pertenencia rápidas
Una de las ventajas más significativas de los conjuntos es su velocidad para las pruebas de pertenencia. Comprobar si un elemento existe en un conjunto es mucho más rápido que comprobarlo en una lista, especialmente para colecciones grandes:
# Comprobar si un usuario está en una colección grande
active_users_list = []
for i in range(10000):
active_users_list.append("user" + str(i))
# Con una lista (lento para colecciones grandes)
print("user5000" in active_users_list) # Checks each element until found
active_users_set = set()
for i in range(10000):
active_users_set.add("user" + str(i))
# Con un conjunto (rápido independientemente del tamaño)
print("user5000" in active_users_set) # Direct lookupAunque ambos producen el mismo resultado, la versión con conjuntos es drásticamente más rápida para colecciones grandes. Esto se debe a que los conjuntos usan internamente una tabla hash, lo que permite búsquedas casi instantáneas independientemente del tamaño, mientras que las listas deben comprobar cada elemento de forma secuencial.
17.5.2) Usa conjuntos para eliminar duplicados
Cuando necesitas eliminar duplicados de una colección, convertir a un conjunto es el enfoque más simple:
# Eliminar entradas duplicadas de la entrada del usuario
survey_responses = [
"yes", "no", "yes", "maybe", "yes", "no", "maybe", "yes"
]
# Obtener respuestas únicas
unique_responses = set(survey_responses)
print(unique_responses) # Output: {'yes', 'no', 'maybe'}
# Si necesitas recuperar una lista (con duplicados eliminados)
unique_list = list(unique_responses)
print(unique_list) # Output: ['yes', 'no', 'maybe'] (order may vary)17.5.3) Usa conjuntos para operaciones matemáticas de conjuntos
Cuando necesitas encontrar elementos comunes, diferencias o uniones entre colecciones, los conjuntos proporcionan operaciones claras y eficientes:
# Analizar patrones de compra de clientes
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Clientes que compraron ambos productos
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}
# Clientes que compraron solo el producto A
only_a = customers_product_a - customers_product_b
print(f"Only product A: {only_a}")
# Output: Only product A: {101, 102}
# Todos los clientes que compraron al menos un producto
all_customers = customers_product_a | customers_product_b
print(f"Total customers: {len(all_customers)}")
# Output: Total customers: 717.5.4) Usa listas cuando el orden importa
Los conjuntos no están ordenados, así que si la secuencia de los elementos es importante, debes usar una lista:
# WRONG - Order is not preserved with sets
task_order = {"wake up", "breakfast", "work", "lunch", "work", "dinner"}
print(task_order) # Order is unpredictable and "work" appears only once
# CORRECT - Use a list when order matters
task_order = ["wake up", "breakfast", "work", "lunch", "work", "dinner"]
print(task_order)
# Output: ['wake up', 'breakfast', 'work', 'lunch', 'work', 'dinner']17.5.5) Usa listas cuando los duplicados son significativos
Si los valores duplicados contienen información (como frecuencia o múltiples ocurrencias), usa una lista:
# Registrar puntuaciones de un cuestionario (los duplicados muestran cuántos estudiantes obtuvieron cada puntuación)
quiz_scores = [85, 90, 85, 78, 90, 92, 85, 88]
# Con una lista, podemos contar ocurrencias
score_85_count = quiz_scores.count(85)
print(f"Students who scored 85: {score_85_count}")
# Output: Students who scored 85: 3
# Con un conjunto, perderíamos esta información
unique_scores = set(quiz_scores)
print(unique_scores) # Output: {78, 85, 88, 90, 92}
# No podemos saber cuántos estudiantes obtuvieron cada puntuación17.5.6) Usa listas cuando necesitas indexación
Los conjuntos no admiten indexación porque no están ordenados. Si necesitas acceder a elementos por posición, usa una lista:
# WRONG - Sets don't support indexing
colors = {"red", "blue", "green"}
# first_color = colors[0] # Raises: TypeError: 'set' object is not subscriptable
# CORRECT - Use a list for indexed access
colors = ["red", "blue", "green"]
first_color = colors[0]
print(first_color) # Output: red17.6) Frozensets y conjuntos inmutables
Hasta ahora, hemos trabajado con conjuntos normales, que son mutables: puedes añadir y eliminar elementos después de su creación. Python también proporciona frozensets, que son versiones inmutables de los conjuntos. Una vez creado, un frozenset no se puede modificar.
17.6.1) Crear frozensets
Puedes crear un frozenset usando el constructor frozenset(), de forma similar a como creas un conjunto normal con set():
# Crear un frozenset a partir de una lista
colors = frozenset(["red", "blue", "green"])
print(colors) # Output: frozenset({'red', 'blue', 'green'})
print(type(colors)) # Output: <class 'frozenset'>
# Crear un frozenset a partir de una tupla
numbers = frozenset((1, 2, 3, 4, 5))
print(numbers) # Output: frozenset({1, 2, 3, 4, 5})
# Crear un frozenset vacío
empty = frozenset()
print(empty) # Output: frozenset()Al igual que los conjuntos normales, los frozensets eliminan duplicados automáticamente:
# Se eliminan los duplicados
values = frozenset([1, 2, 2, 3, 3, 3, 4])
print(values) # Output: frozenset({1, 2, 3, 4})17.6.2) Los frozensets son inmutables
Una vez creado, no puedes modificar un frozenset. Métodos como add(), remove(), discard(), pop() y clear() no existen para los frozensets:
# Crear un frozenset
languages = frozenset(["Python", "JavaScript", "Java"])
# Intentar modificarlo lanza un error
# languages.add("C++") # AttributeError: 'frozenset' object has no attribute 'add'
# languages.remove("Java") # AttributeError: 'frozenset' object has no attribute 'remove'Esta inmutabilidad es la característica definitoria de los frozensets. Si necesitas "modificar" un frozenset, debes crear uno nuevo:
# Frozenset original
original = frozenset([1, 2, 3])
# Crear un frozenset nuevo con un elemento adicional
modified = frozenset(list(original) + [4])
print(original) # Output: frozenset({1, 2, 3})
print(modified) # Output: frozenset({1, 2, 3, 4})17.6.3) Las operaciones de conjuntos funcionan con frozensets
Los frozensets soportan las mismas operaciones de conjuntos que los conjuntos normales (unión, intersección, diferencia, etc.):
# Operaciones de conjuntos con frozensets
set_a = frozenset([1, 2, 3, 4])
set_b = frozenset([3, 4, 5, 6])
# Union
print(set_a | set_b) # Output: frozenset({1, 2, 3, 4, 5, 6})
# Intersection
print(set_a & set_b) # Output: frozenset({3, 4})
# Difference
print(set_a - set_b) # Output: frozenset({1, 2})
# Symmetric difference
print(set_a ^ set_b) # Output: frozenset({1, 2, 5, 6})También puedes mezclar conjuntos normales y frozensets en operaciones:
regular_set = {1, 2, 3}
frozen_set = frozenset([3, 4, 5])
# Operaciones entre conjuntos normales y frozensets
result = regular_set | frozen_set
print(result) # Output: {1, 2, 3, 4, 5}
print(type(result)) # Output: <class 'set'> (result is a regular set)17.6.4) ¿Por qué usar frozensets?
La razón principal para usar frozensets es que se pueden usar como claves de diccionario o como elementos en otros conjuntos, cosa que los conjuntos normales no pueden:
# WRONG - Regular sets cannot be dictionary keys
# regular_set = {1, 2, 3}
# my_dict = {regular_set: "value"} # TypeError: unhashable type: 'set'
# CORRECT - Frozensets can be dictionary keys
frozen_set = frozenset([1, 2, 3])
my_dict = {frozen_set: "value"}
print(my_dict) # Output: {frozenset({1, 2, 3}): 'value'}
print(my_dict[frozen_set]) # Output: valueUn ejemplo práctico usando frozensets como claves de diccionario:
# Almacenar información sobre pares de coordenadas
# Cada coordenada es un frozenset de valores (x, y)
location_data = {
frozenset([0, 0]): "origin",
frozenset([1, 0]): "east",
frozenset([1, 1]): "northeast"
}
# Buscar una ubicación
point = frozenset([1, 0])
print(location_data[point]) # Output: eastLos frozensets también pueden ser elementos en otros conjuntos:
# WRONG - Regular sets cannot be elements of sets
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# CORRECT - Frozensets can be elements of sets
set_of_frozensets = {
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
}
print(set_of_frozensets)
# Output: {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}Un ejemplo práctico para representar grupos:
# Representar equipos donde cada equipo es un frozenset de IDs de jugadores
tournament_teams = {
frozenset([101, 102, 103]), # Team A
frozenset([201, 202, 203]), # Team B
frozenset([301, 302, 303]) # Team C
}
# Comprobar si un equipo específico está registrado
team_to_check = frozenset([101, 102, 103])
if team_to_check in tournament_teams:
print("Team is registered")
else:
print("Team not found")
# Output: Team is registered17.6.5) Convertir entre conjuntos y frozensets
Puedes convertir fácilmente entre conjuntos normales y frozensets:
# Convertir un conjunto normal a frozenset
regular = {1, 2, 3, 4}
frozen = frozenset(regular)
print(frozen) # Output: frozenset({1, 2, 3, 4})
# Convertir un frozenset a conjunto normal
frozen = frozenset([5, 6, 7, 8])
regular = set(frozen)
print(regular) # Output: {5, 6, 7, 8}
# Ahora podemos modificar el conjunto normal
regular.add(9)
print(regular) # Output: {5, 6, 7, 8, 9}17.7) Tipos hasheables y no hasheables: qué puede ser clave de diccionario o elemento de un conjunto (y una breve nota sobre el hashing)
A lo largo de este capítulo, hemos visto que los conjuntos pueden contener algunos tipos de objetos pero no otros. Por ejemplo, puedes crear un conjunto de enteros o cadenas, pero no un conjunto de listas. Esta restricción existe porque los elementos de un conjunto (y las claves de diccionario, como aprendimos en el Capítulo 16) deben ser hasheables.
17.7.1) ¿Qué significa "hasheable"?
Un objeto hasheable es aquel que tiene un valor hash que nunca cambia durante su vida. Python calcula este valor hash usando una función incorporada llamada hash():
# Los tipos hasheables tienen un valor hash
print(hash(42)) # Output: 42
print(hash("Python")) # Output: (some large integer)
print(hash((1, 2, 3))) # Output: (some large integer)El valor hash es un entero que Python usa internamente para localizar objetos rápidamente en conjuntos y diccionarios. Piénsalo como una dirección o índice que ayuda a Python a encontrar cosas de forma eficiente.
Propiedad clave: Para que un objeto sea hasheable, su valor hash debe permanecer constante durante toda su vida. Esto significa que el propio objeto debe ser inmutable: si el objeto pudiera cambiar, su valor hash también tendría que cambiar, lo que rompería conjuntos y diccionarios.
17.7.2) Los tipos inmutables son hasheables
Todos los tipos incorporados inmutables de Python son hasheables y pueden usarse como elementos de un conjunto o claves de diccionario:
# Los enteros son hasheables
numbers = {1, 2, 3, 4, 5}
print(numbers) # Output: {1, 2, 3, 4, 5}
# Las cadenas son hasheables
words = {"apple", "banana", "cherry"}
print(words) # Output: {'apple', 'banana', 'cherry'}
# Las tuplas son hasheables (si contienen solo elementos hasheables)
coordinates = {(0, 0), (1, 1), (2, 2)}
print(coordinates) # Output: {(0, 0), (1, 1), (2, 2)}
# Los frozensets son hasheables
frozen_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozen_sets) # Output: {frozenset({1, 2}), frozenset({3, 4})}
# Los booleanos y None son hasheables
mixed = {True, False, None, 42, "text"}
print(mixed) # Output: {False, True, None, 42, 'text'}17.7.3) Los tipos mutables no son hasheables
Los tipos mutables como las listas, los conjuntos normales y los diccionarios no son hasheables porque su contenido puede cambiar:
# Las listas NO son hasheables
# my_set = {[1, 2, 3]} # TypeError: unhashable type: 'list'
# Los conjuntos normales NO son hasheables
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# Los diccionarios NO son hasheables
# my_set = {{"key": "value"}} # TypeError: unhashable type: 'dict'¿Por qué importa la mutabilidad? Considera lo que pasaría si pudiéramos añadir una lista a un conjunto:
# Escenario hipotético (esto no funciona realmente)
# my_list = [1, 2, 3]
# my_set = {my_list} # Suppose this worked
#
# # Python computes hash based on [1, 2, 3]
# # Now we modify the list:
# my_list.append(4) # Now it's [1, 2, 3, 4]
#
# # The hash value would be wrong! The set would be corrupted.Por eso Python impide que objetos mutables estén en conjuntos o se usen como claves de diccionario: rompería la estructura de datos interna.
Confusión común de principiantes: Aunque los propios conjuntos son mutables (puedes añadir y eliminar elementos), los elementos deben ser inmutables. A veces, los principiantes intentan modificar objetos después de añadirlos a conjuntos, sin darse cuenta de esta distinción conceptual:
# Confusión común: el set es mutable, pero los elementos deben ser inmutables
# El set es mutable: puedes cambiar su contenido
fruits = {'apple', 'banana'}
fruits.add('orange') # ✓ Works
fruits.remove('apple') # ✓ Works
# Pero los elementos deben ser inmutables: no se pueden cambiar
my_list = [1, 2, 3]
# my_set = {my_list} # ✗ TypeError: unhashable type: 'list'
# Why? If you could modify my_list after adding it, the set's internal
# structure would be corrupted.
# Esto funciona porque las tuplas son inmutables
my_tuple = (1, 2, 3)
my_set = {my_tuple} # ✓ Works - tuples can't be modified17.7.4) El caso especial de las tuplas
Las tuplas son hasheables solo si todos sus elementos son hasheables. Una tupla que contiene objetos mutables no es hasheable:
# Tupla con solo elementos inmutables: hasheable
good_tuple = (1, 2, "three")
my_set = {good_tuple} # Works: good_tuple is hashable
print(my_set) # Output: {(1, 2, 'three')}
# Tupla que contiene una lista: NO hasheable
bad_tuple = (1, 2, [3, 4])
# my_set = {bad_tuple} # TypeError: unhashable type: 'list'Esto tiene sentido: aunque la tupla en sí es inmutable (no puedes cambiar qué objetos contiene), si uno de esos objetos es mutable, el "valor" global de la tupla puede cambiar:
# Demostrar por qué no se pueden hashear tuplas con elementos mutables
inner_list = [1, 2]
my_tuple = (inner_list, 3)
# La estructura de la tupla es fija, pero la lista interna puede cambiar
inner_list.append(3) # Now inner_list is [1, 2, 3]
# La tupla ahora "contiene" datos diferentes, pero es el mismo objeto tuple17.7.5) Probar la hasheabilidad
Puedes comprobar si un objeto es hasheable intentando calcular su hash:
# Probar la hasheabilidad
def is_hashable(obj):
"""Comprobar si un objeto es hasheable."""
try:
hash(obj)
return True
except TypeError:
return False
# Probar varios tipos
print(is_hashable(42)) # Output: True
print(is_hashable("text")) # Output: True
print(is_hashable((1, 2, 3))) # Output: True
print(is_hashable([1, 2, 3])) # Output: False
print(is_hashable({1, 2, 3})) # Output: False
print(is_hashable({"key": "value"})) # Output: False17.7.6) Resumen de tipos hasheables
Hasheables (pueden ser elementos de set o claves de dict):
- Enteros:
42 - Floats:
3.14 - Cadenas:
"text" - Tuplas (si todos los elementos son hasheables):
(1, 2, "three") - Frozensets:
frozenset([1, 2, 3]) - Booleanos:
True,False - None:
None
No hasheables (no pueden ser elementos de set ni claves de dict):
- Listas:
[1, 2, 3] - Conjuntos normales:
{1, 2, 3} - Diccionarios:
{"key": "value"} - Tuplas que contienen elementos no hasheables:
(1, [2, 3])
Comprender la hasheabilidad te ayuda a elegir las estructuras de datos adecuadas y a evitar errores comunes al trabajar con conjuntos y diccionarios. El principio clave es simple: si un objeto puede cambiar, no se puede hashear; si no se puede hashear, no puede estar en un conjunto ni usarse como clave de diccionario.