El Día 009 del reto 100 Días de Python es una Subasta Secreta de línea de comandos. Varios postores ingresan sus nombres y ofertas, y luego el programa determina quién ofertó más una vez que se cierra la subasta.
Este proyecto es la primera introducción real de la serie a los diccionarios, la forma integrada de Python para emparejar dos datos relacionados, como el nombre de un postor y el monto de su oferta. También agrega una validación de datos más completa que en los días anteriores, comprobando tanto que un nombre parezca un nombre como que una oferta sea un número positivo válido.
Esta guía recorre el código fuente completo del Día 009, explica cómo funcionan el diccionario y la lógica para encontrar al ganador, y sigue paso a paso un error real en la forma de mostrar las ofertas empatadas.
Descripción del Proyecto
Las ofertas se guardan en un diccionario con el nombre del postor como clave y el monto de la oferta como valor:
bids[name] = price
Un ciclo recopila postores hasta que el usuario responde n cuando se le pregunta si continúa otro postor. La función find_highest_bidder() recorre entonces el diccionario, registra el monto más alto e imprime una vista ordenada de todas las ofertas enviadas antes de anunciar al ganador.
Código Fuente
El proyecto del Día 009 está en day009/main.py, dentro del repositorio 100daysofpython:
from art import LOGO
print(LOGO)
print("*******************************************")
print("Welcome to The Secret Auction!")
print("*******************************************")
def find_highest_bidder(bidding_dictionary):
winner = ""
highest_bid = 0
for bidder in bidding_dictionary:
bid_amount = bidding_dictionary[bidder]
if bid_amount > highest_bid:
highest_bid = bid_amount
winner = bidder
print("\n************* Auction Results *************")
print(f"Total bids placed: {len(bidding_dictionary)}\n")
print("\nName | Bid Amount")
print("-------------------------------------------")
sorted_amounts = sorted(bidding_dictionary.values(), reverse=True)
for amount in sorted_amounts:
for bidder in bidding_dictionary:
if bidding_dictionary[bidder] == amount:
print(f"{bidder} - ${amount}")
print("\n-------------------------------------------")
print(f"The winner is {winner} with a bid of ${highest_bid}")
print("---------------------------------------------")
def secret_auction():
bids = {}
continue_bidding = True
while continue_bidding:
while True:
name = input("What is your name?: ").strip()
if name == "":
print("Name cannot be empty.")
elif not name.replace(" ", "").isalpha():
print("Name must contain only letters.")
else:
break
while True:
try:
price = int(input("What is your bid?: $"))
if price <= 0:
print("Bid must be greater than 0.")
else:
break
except ValueError:
print("Please enter a valid whole number.")
bids[name] = price
should_continue = input(
"Are there any other bidders? Type 'y' if there other bidders. Otherwise type 'n'.\n"
).lower()
if should_continue == "n":
continue_bidding = False
find_highest_bidder(bids)
elif should_continue == "y":
print("\n")
secret_auction()
Cómo Funciona el Programa
Guardar las Ofertas en un Diccionario
bids[name] = price
Un diccionario empareja el nombre de cada postor (la clave) con el monto de su oferta (el valor). Es una opción natural aquí, ya que el nombre y la oferta de cada postor deben viajar juntos como una sola unidad; una lista simple de nombres y otra lista aparte de ofertas haría mucho más fácil que se desajusten por accidente.
Validación de Datos en Dos Capas
if name == "":
print("Name cannot be empty.")
elif not name.replace(" ", "").isalpha():
print("Name must contain only letters.")
else:
break
La validación del nombre quita los espacios antes de llamar a .isalpha(), de modo que un nombre como "Mary Jane" pasa, mientras que una cadena vacía o una que contiene dígitos o símbolos se rechaza y el ciclo vuelve a preguntar.
try:
price = int(input("What is your bid?: $"))
if price <= 0:
print("Bid must be greater than 0.")
else:
break
except ValueError:
print("Please enter a valid whole number.")
La validación de la oferta combina las dos técnicas vistas en días anteriores: un try/except captura los datos no numéricos, y una verificación if price <= 0: rechaza las ofertas de cero o negativas una vez que se confirma que el dato es un número válido.
Encontrar al Mejor Postor
for bidder in bidding_dictionary:
bid_amount = bidding_dictionary[bidder]
if bid_amount > highest_bid:
highest_bid = bid_amount
winner = bidder
Este ciclo es el clásico patrón de “máximo acumulado”: se parte de un valor base (highest_bid = 0) y se actualiza, junto con el winner asociado, cada vez que se encuentra un valor mayor al recorrer el diccionario.
Un Error Real: Filas Duplicadas en Ofertas Empatadas
Observa con atención cómo se imprimen los resultados:
sorted_amounts = sorted(bidding_dictionary.values(), reverse=True)
for amount in sorted_amounts:
for bidder in bidding_dictionary:
if bidding_dictionary[bidder] == amount:
print(f"{bidder} - ${amount}")
sorted_amounts es una lista ordenada de cada valor de oferta, incluidos los duplicados: si dos postores ofertan $100, el 100 aparece dos veces en esa lista. Por cada aparición del 100, el ciclo interno busca en todo el diccionario e imprime a todos los postores cuya oferta es igual a 100.
Sigue el recorrido con un ejemplo donde bids = {"Alice": 100, "Bob": 100, "Carol": 50}:
sorted_amountses[100, 100, 50].- En el primer
100, el ciclo interno encuentra a Alice y a Bob, e imprime a ambos. - En el segundo
100, el ciclo interno encuentra a Alice y a Bob otra vez, e imprime a ambos por segunda vez. - En el
50, el ciclo interno encuentra a Carol una vez.
El resultado son cinco líneas impresas (Alice y Bob aparecen dos veces cada uno) en lugar de los tres postores reales. Cualquier empate en los montos de las ofertas provoca esta duplicación, y mientras más postores compartan el mismo valor, peor se vuelve.
Qué Enseña Este Proyecto
El Día 009 ofrece práctica con:
- Diccionarios para emparejar dos datos relacionados
- Validación de datos en varias capas que combina verificaciones de cadenas y
try/except - El patrón de “máximo acumulado” para encontrar el valor más grande de una colección
- Por qué un ciclo anidado sobre valores duplicados requiere cuidado extra para evitar salidas duplicadas
El proyecto también mantiene la subasta en memoria, por lo que los resultados desaparecen cuando el programa termina; agregar almacenamiento en archivos o en una base de datos sería un siguiente paso natural para una versión más completa.
Ejecutar el Proyecto
git clone https://github.com/victorhugo81/100daysofpython.git
cd 100daysofpython/day009
python main.py
Posibles Siguientes Pasos
Cuando entiendas el código existente, intenta ampliarlo:
- Corrige la duplicación de ofertas empatadas ordenando directamente los pares
(bidder, amount), por ejemplo consorted(bidding_dictionary.items(), key=lambda item: item[1], reverse=True), en lugar de buscar por separado los montos que coinciden - Permite nombres con guiones o apóstrofos (como “Jean-Luc” u “O’Brien”), que la verificación actual con
.isalpha()rechaza - Evita que un mismo nombre sobrescriba una oferta anterior: como
bidsusa el nombre como clave, ingresar el mismo nombre dos veces reemplaza en silencio la oferta previa en lugar de registrar una segunda - Guarda los resultados de la subasta en un archivo para que persistan después de que el programa termine
Conclusión
El Día 009 del reto 100 Días de Python combina diccionarios, validación de datos por capas y una búsqueda de máximo acumulado en un programa de subasta funcional, y al mismo tiempo sirve como recordatorio de que ordenar los valores por separado de sus claves asociadas, como hace aquí la pantalla de resultados, puede producir salidas duplicadas en silencio cuando hay empates. Detectar y corregir ese tipo de error es justo la habilidad que este reto está diseñado para desarrollar.
Consulta el código fuente del Día 009 en GitHub y sigue explorando el índice de proyectos del repositorio mientras construyes más programas en Python, un día y un proyecto pequeño a la vez.
