Datos anidados

# Diccionarios con listas
alumno={
    'nombre':'Ana',
    'email':'ana@ana.com',
    'notas':[6,7,5,8]
}
print(f"El alumno {alumno['nombre']} tiene las siguientes notas:")
for nota in alumno['notas']:
    print(nota)

cliente={
    'nombre':'Ana',
    'email':'ana@gmail.com',
    'direccion':{
        'calle':'Agla',
        'numero':6,
        'cp':'08001',
        'ciudad':'Barcelona'
    }
}

print(f"El cliente {cliente['nombre']} vive en {cliente['direccion']['calle']} número {cliente['direccion']['numero']}")

alumnos=[
    {
        'nombre':'Ana',
        'email':'ana@gmail.com',
    },
    {
        'nombre':'Pep',
        'email':'pep@pepe.com',
    },
    {
        'nombre':'Iu',
        'email':'iu@iu.com',
    }
]

for alumno in alumnos:
    print(alumno['nombre'])

Diccionarios


# ─────────────────────────────────────────────
# DICCIONARIOS EN PYTHON
# ─────────────────────────────────────────────
# Un diccionario guarda pares  clave: valor
# · Las claves son únicas (no puede haber dos iguales)
# · Los valores pueden ser de cualquier tipo
# · Se definen con llaves  { }
# · A diferencia de las listas, NO se accede por posición
#   sino por el nombre de la clave

producto = {
    'referencia': 'SK789',
    'precio': 200
}
print(producto)   # → {'referencia': 'SK789', 'precio': 200}

clase = {'nombre': 'WEB', 'capacidad': 20}
print(clase)   # → {'nombre': 'WEB', 'capacidad': 20}


# ─────────────────────────────────────────────
# LEER Y MODIFICAR VALORES
# ─────────────────────────────────────────────

# Acceder a un valor con su clave entre corchetes
print(clase['nombre'])      # → WEB
print(clase['capacidad'])   # → 20

# Modificar un valor existente (misma sintaxis que al leer)
clase['capacidad'] = 15
print(clase)   # → {'nombre': 'WEB', 'capacidad': 15}


camisa = {
    'talla':   'L',
    'color':   'rojo',
    'precio':  20,
    'moneda':  '€'
}

# f-string con acceso al diccionario dentro de las llaves { }
print(f"La talla de la camisa es {camisa['talla']}")   # → La talla de la camisa es L


# ─────────────────────────────────────────────
# ACCESO SEGURO: get()
# ─────────────────────────────────────────────

# Si accedemos a una clave que NO existe con corchetes → ERROR (KeyError)
# print(camisa['stock'])   ← esto lanzaría un KeyError y detendría el programa

# get(clave) devuelve el valor si existe, o None si no existe (sin error)
print(camisa.get('stock'))      # → None

# get(clave, valor_por_defecto) devuelve el valor por defecto si no existe
print(camisa.get('stock', 0))   # → 0   ← mucho más útil en la práctica

# 💡 ALTERNATIVA: comprobar antes con 'in'
if 'stock' in camisa:
    print(camisa['stock'])
else:
    print("La clave 'stock' no existe")


# ─────────────────────────────────────────────
# AÑADIR CLAVES NUEVAS
# ─────────────────────────────────────────────

# Asignar a una clave que no existe → la crea automáticamente
camisa['stock'] = 20
print(camisa)
# → {'talla':'L','color':'rojo','precio':20,'moneda':'€','stock':20}


# ─────────────────────────────────────────────
# UPDATE: añadir o modificar varias claves a la vez
# ─────────────────────────────────────────────

# update() acepta argumentos con nombre (clave=valor)
camisa.update(almacen='Central', activo=True)
print(camisa)
# → {..., 'almacen': 'Central', 'activo': True}

# 💡 ALTERNATIVA: pasarle otro diccionario
# camisa.update({'almacen': 'Central', 'activo': True})

# 💡 ALTERNATIVA desde Python 3.9: operador |=
# camisa |= {'almacen': 'Central', 'activo': True}


# ─────────────────────────────────────────────
# VISTAS: keys(), values(), items()
# ─────────────────────────────────────────────
# Estas tres funciones devuelven "vistas" del diccionario.
# No son listas normales, pero se pueden recorrer con for
# y se actualizan automáticamente si el diccionario cambia.

claves   = camisa.keys()     # Todas las claves
valores  = camisa.values()   # Todos los valores
elementos = camisa.items()   # Pares (clave, valor) como tuplas

print(claves)     # → dict_keys(['talla', 'color', 'precio', ...])
print(valores)    # → dict_values(['L', 'rojo', 20, ...])
print(elementos)  # → dict_items([('talla','L'), ('color','rojo'), ...])

# 💡 Si necesitas una lista real puedes convertirlas:
# list(camisa.keys())    → ['talla', 'color', 'precio', ...]
# list(camisa.values())  → ['L', 'rojo', 20, ...]


# ─────────────────────────────────────────────
# ACCEDER CON UNA VARIABLE COMO CLAVE
# ─────────────────────────────────────────────

# La clave puede estar guardada en una variable, no tiene que ser literal
miclave = "stock"
print(camisa[miclave])      # → 20

miclave = "almacen"
print(camisa[miclave])      # → Central

# Esto es muy útil cuando la clave viene de una entrada del usuario
# o se calcula en tiempo de ejecución


# ─────────────────────────────────────────────
# RECORRER UN DICCIONARIO CON FOR
# ─────────────────────────────────────────────

# OPCIÓN 1: iterar solo por las claves y acceder al valor manualmente
for clave in camisa.keys():
    print(clave, camisa[clave])

# OPCIÓN 2 (más elegante): iterar por clave y valor a la vez con items()
for clave, valor in camisa.items():
    print(clave, valor)

# 💡 ALTERNATIVA: iterar directamente (sin .keys()) hace lo mismo que la opción 1
# for clave in camisa:
#     print(clave, camisa[clave])

# 💡 ALTERNATIVA: crear una lista de pares formateados con comprensión de lista
# pares = [f"{k} → {v}" for k, v in camisa.items()]


# ─────────────────────────────────────────────
# ELIMINAR ELEMENTOS: pop() y popitem()
# ─────────────────────────────────────────────

# pop(clave): elimina la clave indicada y DEVUELVE su valor
elemento = camisa.pop('almacen')
print(elemento)   # → Central          (el valor que tenía 'almacen')
print(camisa)     # → {...} sin 'almacen'

# 💡 pop() también acepta valor por defecto para evitar errores:
# camisa.pop('clave_inexistente', None)   → devuelve None sin error

# popitem(): elimina y devuelve el ÚLTIMO par (clave, valor) insertado
# Devuelve una tupla  →  ('clave', valor)
elemento = camisa.popitem()
print(elemento)   # → ('activo', True)   (el último que se añadió)
print(camisa)     # → {...} sin 'activo'

# 💡 ALTERNATIVA para eliminar sin recuperar el valor:
# del camisa['precio']   → borra la clave directamente

Listas y slicing

# ─────────────────────────────────────────────
# MODIFICAR LISTAS: ÍNDICES Y SLICES
# ─────────────────────────────────────────────

# Importación de un módulo propio (archivo iterar_cadenas.py)
# Por ahora no te preocupes por esto, lo veremos más adelante
from iterar_cadenas import posicion

lista = [1, 2, 3, 4, 5, 6, 7]

# Cambiar UN elemento por su índice (posición)
# El índice 3 corresponde al 4º elemento (se empieza a contar desde 0)
lista[3] = 11
print(lista)   # → [1, 2, 3, 11, 5, 6, 7]

# SLICE (rebanada): modificar VARIOS elementos a la vez
# lista[4:7] selecciona desde el índice 4 hasta el 6 (el 7 no se incluye)
# Los reemplazamos por tres nuevos valores
lista[4:7] = [91, 92, 93]
print(lista)   # → [1, 2, 3, 11, 91, 92, 93]

# Un slice puede reemplazarse por MENOS elementos de los que había
# Aquí sustituimos 2 elementos (índices 0 y 1) por solo 1
lista[0:2] = [1]
print(lista)   # → [1, 3, 11, 91, 92, 93]  ← la lista se encoge

# También puede reemplazarse por MÁS elementos de los que había
# Aquí sustituimos 2 elementos por 9 → la lista crece
lista[0:2] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(lista)   # → [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 91, 92, 93]

# ─────────────────────────────────────────────
# EXTEND: añadir elementos de otro iterable
# ─────────────────────────────────────────────

listilla = [1, 2, 3]

otralista = [7, 77, 777]
tupla     = (8, 88, 888)
cadena    = "abc"

# extend() añade cada elemento del iterable uno a uno al final de la lista
listilla.extend(otralista)   # Añade los elementos de una lista
print(listilla)              # → [1, 2, 3, 7, 77, 777]

listilla.extend(tupla)       # También funciona con tuplas
print(listilla)              # → [1, 2, 3, 7, 77, 777, 8, 88, 888]

listilla.extend(cadena)      # Con una cadena añade CADA LETRA por separado
print(listilla)              # → [1, 2, 3, 7, 77, 777, 8, 88, 888, 'a', 'b', 'c']


# ─────────────────────────────────────────────
# EXTEND vs APPEND: diferencia importante
# ─────────────────────────────────────────────

lista = [1, 2, 3]
lista.extend([4, 5])   # extend: desglosa la lista y añade cada elemento
print(lista)           # → [1, 2, 3, 4, 5]

lista = [1, 2, 3]
lista.append([4, 5])   # append: añade el objeto ENTERO como un único elemento
print(lista)           # → [1, 2, 3, [4, 5]]  ← ¡el último elemento es una lista!


# ─────────────────────────────────────────────
# LISTAS ANIDADAS (listas dentro de listas)
# ─────────────────────────────────────────────

# Cada alumno es una lista con su nombre y una tupla de notas
alumnos = [
    ["Ana", (1, 2, 3)],
    ["Pep", (4, 5, 6)],
    ["Iu",  (7, 8)]
]
# Para acceder: alumnos[0] → ["Ana", (1,2,3)]
#               alumnos[0][1] → (1, 2, 3)
#               alumnos[0][1][0] → 1


# ─────────────────────────────────────────────
# MÉTODOS PARA ELIMINAR ELEMENTOS
# ─────────────────────────────────────────────

frutas = ["pera", "manzana", "kiwi", "pera"]

# 'in' comprueba si un elemento existe en la lista
if "pera" in frutas:
    frutas.remove("pera")   # remove() elimina la PRIMERA ocurrencia del valor
print(frutas)               # → ['manzana', 'kiwi', 'pera']  ← queda una "pera"

# pop() elimina y DEVUELVE el último elemento (sin argumentos)
ultima_fruta = frutas.pop()
print(ultima_fruta)   # → 'pera'
print(frutas)         # → ['manzana', 'kiwi']

# pop(índice) elimina y devuelve el elemento en esa posición
primera_fruta = frutas.pop(0)
print(primera_fruta)  # → 'manzana'
print(frutas)         # → ['kiwi']


# ─────────────────────────────────────────────
# BUSCAR ELEMENTOS: index()
# ─────────────────────────────────────────────

lista = [2, 3, 4, 5, 4, 3, 2, 7, 8, 2]

# index(valor) devuelve la posición de la PRIMERA aparición del valor
posicion = lista.index(2)
print(posicion)   # → 0

# index(valor, inicio) busca a partir de una posición concreta
# Así podemos encontrar la 2ª, 3ª... aparición del mismo valor
posicion = lista.index(2, posicion + 1)   # Busca desde la posición 1 en adelante
print(posicion)   # → 6

posicion = lista.index(2, posicion + 1)   # Busca desde la posición 7 en adelante
print(posicion)   # → 9


# ─────────────────────────────────────────────
# ORDENAR LISTAS: sort()
# ─────────────────────────────────────────────

lista = [2, 3, 4, 5, 4, 3, 2, 7, 8, 2]

lista.sort()                  # Orden ascendente (de menor a mayor)
print(lista)                  # → [2, 2, 2, 3, 3, 4, 4, 5, 7, 8]

lista.sort(reverse=True)      # Orden descendente (de mayor a menor)
print(lista)                  # → [8, 7, 5, 4, 4, 3, 3, 2, 2, 2]

# sort(key=función): ordena según el valor que devuelve la función
# para cada elemento. Aquí los pares se quedan igual y los impares
# se "pesan" el doble, por lo que los pares van primero.
def mifuncion(num):
    if num % 2 == 0:
        return num        # Par: se usa su valor real
    else:
        return num * 2    # Impar: se usa el doble (ocupa más "peso" al ordenar)

lista.sort(key=mifuncion)
print(lista)   # → [2, 2, 2, 3, 4, 4, 3, 5, 7, 8]  (pares primero)

# Ordenar cadenas por su longitud usando len como función clave
lista = ["Ana", "Iu", "Eva", "Pep", "Rosa", "Juan", "Roc"]
lista.sort(key=len)    # len devuelve el número de caracteres de cada nombre
print(lista)           # → ['Iu', 'Ana', 'Eva', 'Pep', 'Roc', 'Rosa', 'Juan']


# ─────────────────────────────────────────────
# COPIA DE LISTAS: referencia vs copia real
# ─────────────────────────────────────────────

notas = [6, 7, 3, 5]

# ⚠️ ASIGNACIÓN DIRECTA: NO crea una copia, ambas variables apuntan
# al MISMO objeto en memoria. Cambiar una cambia la otra.
copia = notas

copia[1] = 9
print(copia)    # → [6, 9, 3, 5]
print(notas)    # → [6, 9, 3, 5]  ← ¡notas también cambió! Cuidado con esto.


notas = [6, 7, 3, 5]

# ✅ COPIA REAL con .copy(): crea un objeto nuevo e independiente
# Modificar 'copia' NO afecta a 'notas'
copia = notas.copy()

copia[1] = 9
print(copia)    # → [6, 9, 3, 5]
print(notas)    # → [6, 7, 3, 5]  ← notas no ha cambiado ✓

# ─────────────────────────────────────────────
# SLICES (REBANADAS): acceder a partes de una cadena o lista
# ─────────────────────────────────────────────
# Sintaxis general: coleccion[inicio:fin:paso]
#   · inicio: índice donde empieza la rebanada (se incluye)
#   · fin:    índice donde termina (NO se incluye)
#   · paso:   de cuánto en cuánto avanza (por defecto 1)

cadena = "Mi mamá me mima"
#índices:  0123456789...

# [inicio:fin] → caracteres desde el índice 2 hasta el 5 (el 6 no se incluye)
print(cadena[2:6])    # → ' mam'

# [:fin] → desde el principio hasta el índice 1 (el 2 no se incluye)
print(cadena[:2])     # → 'Mi'

# [inicio:] → desde el índice 5 hasta el final
print(cadena[5:])     # → 'má me mima'

# Índices NEGATIVOS: cuentan desde el final hacia atrás
# -1 es el último carácter, -2 el penúltimo, etc.
# [-3:] → los últimos 3 caracteres
print(cadena[-3:])    # → 'ima'


# ─────────────────────────────────────────────
# LOS MISMOS SLICES FUNCIONAN IGUAL CON LISTAS
# ─────────────────────────────────────────────

lista = ["Ana", "Iu", "Eva", "Pep", "Rosa", "Juan", "Roc"]
#índices:   0      1     2      3      4       5      6

print(lista[2:6])   # → ['Eva', 'Pep', 'Rosa', 'Juan']  (del índice 2 al 5)
print(lista[:2])    # → ['Ana', 'Iu']                   (los 2 primeros)
print(lista[5:])    # → ['Juan', 'Roc']                  (desde el índice 5 hasta el final)
print(lista[-3:])   # → ['Juan', 'Roc', ... ]            (los 3 últimos)


# ─────────────────────────────────────────────
# EL TERCER PARÁMETRO: el PASO
# ─────────────────────────────────────────────

# [::] → sin inicio, sin fin, paso por defecto (1) → copia entera
print(lista[::])    # → ['Ana', 'Iu', 'Eva', 'Pep', 'Rosa', 'Juan', 'Roc']

# [::2] → coge un elemento sí, uno no (paso 2)
#          índices 0, 2, 4, 6 → "Ana", "Eva", "Rosa", "Roc"
print(lista[::2])   # → ['Ana', 'Eva', 'Rosa', 'Roc']

# [::-1] → paso NEGATIVO: recorre la lista al revés
#           sin inicio ni fin → la lista completa invertida
print(lista[::-1])  # → ['Roc', 'Juan', 'Rosa', 'Pep', 'Eva', 'Iu', 'Ana']

# [::-2] → al revés de dos en dos
#           índices 6, 4, 2, 0 → "Roc", "Rosa", "Eva", "Ana"
print(lista[::-2])  # → ['Roc', 'Rosa', 'Eva', 'Ana']

# [6:2:-1] → hacia atrás desde el índice 6 hasta el 3 (el 2 no se incluye)
#             índices 6, 5, 4, 3 → "Roc", "Juan", "Rosa", "Pep"
print(lista[6:2:-1])  # → ['Roc', 'Juan', 'Rosa', 'Pep']

Tuplas

# ─────────────────────────────────────────────
# TUPLAS Y LISTAS EN PYTHON
# ─────────────────────────────────────────────

# TUPLA: colección ordenada que NO se puede modificar (inmutable)
# Se define con paréntesis ( )
numeros = (1, 2, 3, 4)

# LISTA: colección ordenada que SÍ se puede modificar (mutable)
# Se define con corchetes [ ]
numeros_lista = [1, 2, 3, 4]

# También puedes crear una tupla SIN paréntesis, solo con comas
# Python la reconoce igualmente como tupla
masnumeros = 6, 7, 8, 9

print(numeros)       # → (1, 2, 3, 4)
print(masnumeros)    # → (6, 7, 8, 9)

# Recorrer una tupla con un bucle for (igual que con una lista)
for numero in numeros:
    print(numero)    # Imprime cada número en una línea distinta

# Las listas SÍ permiten cambiar sus elementos por índice
# El índice 0 es el primer elemento
numeros_lista[0] = 7  # Cambia el 1 por el 7 → [7, 2, 3, 4]
# numeros[0] = 7  ← Esto daría ERROR porque las tuplas son inmutables


# ─────────────────────────────────────────────
# FUNCIONES QUE DEVUELVEN MÚLTIPLES VALORES
# ─────────────────────────────────────────────

# Una función puede devolver varios valores separados por comas.
# En realidad, Python los empaqueta automáticamente en una tupla.
def estadistica(lista):
    mayor = max(lista)          # Valor más alto de la lista
    menor = min(lista)          # Valor más bajo de la lista
    media = sum(lista) / len(lista)  # Suma total dividida entre cantidad de elementos
    return mayor, menor, media  # Devuelve los tres valores como una tupla


# Llamamos a la función y guardamos el resultado (una tupla) en 'info'
info = estadistica([1, 2, 3, 4, 5])

print(info)           # → (5, 1, 3.0)  — imprime la tupla completa

# Podemos acceder a cada valor por su posición (índice)
print("mayor", info[0])   # → mayor 5
print("menor", info[1])   # → menor 1
print("media", info[2])   # → media 3.0

# DESEMPAQUETADO: asignar cada valor de la tupla a una variable distinta
# en una sola línea. El número de variables debe coincidir con los valores.
a, b, c = estadistica([1, 2, 3, 4, 5])
print(a)   # → 5   (mayor)
print(b)   # → 1   (menor)
print(c)   # → 3.0 (media)


# ─────────────────────────────────────────────
# LISTAS DE TUPLAS
# ─────────────────────────────────────────────

# Una lista puede contener tuplas como elementos.
# Aquí cada tupla representa un punto con coordenadas (x, y).
puntos = [(1, 2), (3, 4), (5, 6)]

# OPCIÓN 1: recibir cada tupla completa y desempaquetarla dentro del bucle
for punto in puntos:
    print(punto)        # Imprime la tupla: (1, 2), (3, 4)...
    x, y = punto        # Desempaquetamos la tupla en dos variables
    print(f"x vale {x} y vale {y}")

# OPCIÓN 2: desempaquetar directamente en la cabecera del for
# Es más compacto y hace exactamente lo mismo que la opción 1
for x, y in puntos:
    print(f"x vale {x} y vale {y}")

Ejercicio cadena

# Nos piden una función que nos diga si una cadena
# tiene letras repetidas consecutivas.
#
# Ejemplos:
# letras_repetidas("hola") -> False
# letras_repetidas("sevilla") -> True
#
# En "sevilla" hay dos letras "l" seguidas.


cadena = "hola"

# Así NO funciona:
#
# for letra in cadena:
#
# Porque con este tipo de recorrido solo tenemos
# la letra actual, pero no sabemos fácilmente
# cuál es la siguiente letra para compararla.


# ---------------------------------------------------
# PRIMERA SOLUCIÓN
# ---------------------------------------------------
# Recorremos la cadena usando posiciones (índices)
# para poder comparar una letra con la siguiente.


def letras_repetidas(cadena):

    # len(cadena) nos dice cuántas letras tiene la cadena.
    #
    # range(len(cadena)-1) genera números desde 0
    # hasta la penúltima posición.
    #
    # Restamos 1 porque vamos a usar i+1 y no queremos
    # salirnos del tamaño de la cadena.

    for i in range(len(cadena) - 1):

        # cadena[i]      -> letra actual
        # cadena[i + 1]  -> letra siguiente

        if cadena[i] == cadena[i + 1]:

            # Si encontramos dos letras iguales seguidas,
            # devolvemos True inmediatamente.
            return True

    # Si termina el bucle y no encontramos letras repetidas,
    # devolvemos False.
    return False


print(letras_repetidas("hola"))      # False
print(letras_repetidas("sevilla"))   # True
print(letras_repetidas("bliss"))     # True


# ---------------------------------------------------
# SEGUNDA SOLUCIÓN
# ---------------------------------------------------
# En vez de usar posiciones, guardamos la letra anterior
# y la comparamos con la actual.


def letras_repetidas2(cadena):

    # Variable donde guardaremos la letra anterior.
    # Al principio está vacía porque todavía no hemos
    # recorrido ninguna letra.
    anterior = ""

    # Recorremos la cadena letra por letra.
    for letra in cadena:

        # Comparamos la letra actual con la anterior.
        if letra == anterior:

            # Si son iguales, hay letras repetidas consecutivas.
            return True

        # Guardamos la letra actual como "anterior"
        # para la siguiente vuelta del bucle.
        anterior = letra

    # Si terminamos el recorrido sin encontrar repeticiones,
    # devolvemos False.
    return False


print(letras_repetidas2("hola"))      # False
print(letras_repetidas2("sevilla"))   # True
print(letras_repetidas2("bliss"))     # True

Ejercicios funciones predefinidas

# -----------------------------------------------------------------------------
# EJERCICIO 1: rango_lista(numeros)
# -----------------------------------------------------------------------------
# Crea una función llamada rango_lista que reciba una lista de números
# y devuelva el RANGO, es decir, la diferencia entre el número más grande
# y el número más pequeño.
#
# PISTAS:
#   - Para encontrar el mayor usa:   max(lista)
#   - Para encontrar el menor usa:   min(lista)
#   - El rango es simplemente:       mayor - menor
#
# Ejemplos:
#   rango_lista([3, 7, 2, 9, 1])    →  8    (9 - 1 = 8)
#   rango_lista([10, 10, 10])        →  0    (10 - 10 = 0, todos iguales)
#   rango_lista([5])                 →  0    (un solo elemento)
#   rango_lista([-3, 0, 3])          →  6    (3 - (-3) = 6)
#   rango_lista([100, 1])            →  99   (100 - 1 = 99)

def rango_lista(numeros):
    # Escribe tu solución aquí
    


# -----------------------------------------------------------------------------
# EJERCICIO 2: resumen_lista(numeros)
# -----------------------------------------------------------------------------
# Crea una función llamada resumen_lista que reciba una lista de números
# y devuelva una cadena de texto con un resumen de sus datos principales:
# cuántos elementos tiene, el menor, el mayor y la media.
#
# PISTAS:
#   - Para contar los elementos usa:      len(lista)
#   - Para el menor usa:                  min(lista)
#   - Para el mayor usa:                  max(lista)
#   - Para la media: sum(lista) / len(lista)
#   - Para redondear a 2 decimales usa:   round(numero, 2)
#   - Para construir el texto usa:        f"..."
#
# Ejemplos:
#   resumen_lista([3, 7, 2, 9, 1])
#       → "Elementos: 5 | Mínimo: 1 | Máximo: 9 | Media: 4.4"
#
#   resumen_lista([10, 20, 30])
#       → "Elementos: 3 | Mínimo: 10 | Máximo: 30 | Media: 20.0"
#
#   resumen_lista([7])
#       → "Elementos: 1 | Mínimo: 7 | Máximo: 7 | Media: 7.0"

def resumen_lista(numeros):
    # Escribe tu solución aquí
    


# -----------------------------------------------------------------------------
# EJERCICIO 3: ordenar_y_mostrar(numeros)
# -----------------------------------------------------------------------------
# Crea una función llamada ordenar_y_mostrar que reciba una lista de números
# y devuelva una cadena de texto con la lista ordenada de menor a mayor
# y también de mayor a menor, separadas por un salto de línea.
#
# PISTAS:
#   - Para ordenar de menor a mayor usa:  sorted(lista)
#   - Para ordenar de mayor a menor usa:  sorted(lista, reverse=True)
#   - sorted() NO modifica la lista original, devuelve una nueva lista
#   - Para convertir una lista en texto usa: str(lista)
#
# Ejemplos:
#   ordenar_y_mostrar([3, 7, 2, 9, 1])
#       → "Ascendente:  [1, 2, 3, 7, 9]
#          Descendente: [9, 7, 3, 2, 1]"
#
#   ordenar_y_mostrar([5, 5, 1, 3])
#       → "Ascendente:  [1, 3, 5, 5]
#          Descendente: [5, 5, 3, 1]"

def ordenar_y_mostrar(numeros):
    # Escribe tu solución aquí
    

Ejercicios listas

# =============================================================================
# RECORRER LISTAS EN PYTHON — PATRONES HABITUALES
# =============================================================================
# En este programa veremos tres patrones muy comunes cuando trabajamos con listas:
#
#   1. Buscar el elemento "mejor" de una lista (el más largo, el mayor, el menor...)
#   2. Filtrar una lista: quedarse solo con los elementos que cumplan una condición
#   3. Filtrar con condiciones sobre cadenas (primera letra, última letra...)
#
# Estos tres patrones aparecen constantemente en programas reales.
# =============================================================================


# =============================================================================
# PATRÓN 1: BUSCAR EL NOMBRE MÁS LARGO DE UNA LISTA
# =============================================================================
# Problema: dada una lista de nombres, ¿cuál es el más largo?
#
# Estrategia: "candidato provisional"
#   Asumimos que el primero es el más largo.
#   Recorremos el resto y si encontramos uno más largo, lo sustituimos.
#   Al terminar, el candidato es el ganador real.
# =============================================================================

alumnos = ["Ana", "Rosa", "Pep", "Iu", "Felicia", "Juan"]
# Lista de nombres con la que vamos a trabajar.
# ALTERNATIVA: pedir los nombres al usuario con un bucle while.

nombre_mas_largo = alumnos[0]
# Tomamos el PRIMER elemento como candidato provisional al más largo.
# alumnos[0] → "Ana"
# No lo inicializamos a "" porque "" tiene longitud 0 y cualquier nombre
# sería más largo, lo que también funcionaría, pero esta forma es más clara.
# IMPORTANTE: hacerlo ANTES del bucle, no dentro.

for alumno in alumnos:
    # Recorremos TODOS los alumnos, incluido "Ana" (el primero).
    # Comparar "Ana" consigo mismo no causa ningún problema: len("Ana") > len("Ana")
    # es False, así que simplemente no lo sustituimos. Todo correcto.

    if len(alumno) > len(nombre_mas_largo):
        # len() devuelve el número de caracteres de una cadena.
        # len("Ana")     → 3
        # len("Felicia")  → 7
        # Si el alumno actual tiene MÁS letras que el candidato actual...

        nombre_mas_largo = alumno
        # ...actualizamos el candidato. Ahora este es el más largo provisional.
        # Vuelta a vuelta:
        #   "Ana"     → candidato="Ana"     (inicial)
        #   "Rosa"    → 4 > 3 → candidato="Rosa"
        #   "Pep"     → 3 > 4 → NO cambia
        #   "Iu"      → 2 > 4 → NO cambia
        #   "Felicia" → 7 > 4 → candidato="Felicia"
        #   "Juan"    → 4 > 7 → NO cambia

print(nombre_mas_largo)
# → "Felicia"
#
# RETO: modifica el código para encontrar también el nombre MÁS CORTO.
# Pista: solo tienes que cambiar > por < y el nombre de la variable.


# =============================================================================
# PATRÓN 2: FILTRAR NÚMEROS DE UNA LISTA
# =============================================================================

notas = [3, 6, 7, 8, 2, 4, 9, 10]
# Lista de notas con la que trabajaremos.
# Queremos saber cuántos alumnos han aprobado y cuántos han suspendido.


# -----------------------------------------------------------------------------
# FUNCIÓN lista_mayores_que(lista, umbral)
# -----------------------------------------------------------------------------
# Recibe una lista de números y un umbral.
# Devuelve una nueva lista con solo los números ESTRICTAMENTE mayores que el umbral.
# La lista original no se modifica.
#
# Ejemplos:
#   lista_mayores_que([1, 5, 3, 8, 2, 9], 4)  →  [5, 8, 9]
#   lista_mayores_que([7, 7, 7], 7)            →  []  (7 no es mayor QUE 7)

def lista_mayores_que(lista, umbral):
    # Dos parámetros:
    #   "lista"   → la lista de números que queremos filtrar
    #   "umbral"  → el valor límite. Solo pasarán los números que lo superen.

    resultado = []
    # Lista vacía donde iremos guardando los números que cumplan la condición.
    # Debe crearse DENTRO de la función para que empiece vacía en cada llamada.
    # Si estuviera fuera, las llamadas anteriores dejarían números sobrantes.

    for numero in lista:
        # Recorremos cada número de la lista que nos han pasado.

        if numero > umbral:
            # Comprobamos si supera el umbral.
            # OJO: es > (estrictamente mayor), no >= (mayor o igual).
            # Por eso lista_mayores_que([7,7,7], 7) devuelve [] y no [7,7,7].
            # Si quisiéramos incluir el umbral cambiaríamos > por >=.

            resultado.append(numero)
            # .append() añade el número AL FINAL de la lista resultado.
            # Solo llegamos aquí si el número supera el umbral.

    return resultado
    # Devolvemos la lista con los números seleccionados.
    # Si ningún número superó el umbral, devolvemos [] (lista vacía).

# --- Casos de prueba ---
mayores = lista_mayores_que([1, 5, 3, 8, 2, 9], 4)
print(mayores)
# → [5, 8, 9]
# Guardamos el resultado en "mayores" para poder usarlo después si queremos.

print(lista_mayores_que([2, 10, 7, 40, 8, 3], 7))   # → [10, 40, 8]
print(lista_mayores_que([10, 20, 30], 15))            # → [20, 30]
print(lista_mayores_que([1, 2, 3], 10))               # → []
print(lista_mayores_que([7, 7, 7], 7))                # → []

# --- Uso práctico con las notas ---
# Aprobados: notas mayores que 4 (es decir, 5 o más)
# Suspensos: notas menores o iguales a 4
aprobados = lista_mayores_que(notas, 4)
# → [6, 7, 8, 9, 10]
# len(aprobados) nos diría cuántos hay: len([6,7,8,9,10]) = 5


# =============================================================================
# PATRÓN 3: FILTRAR NOMBRES SEGÚN SUS LETRAS
# =============================================================================
# Ahora filtramos una lista de cadenas comprobando la primera y última letra.
# Veremos dos herramientas nuevas:
#   nombre[0]   → primera letra  ("Ana"[0]  → "A")
#   nombre[-1]  → última letra   ("Ana"[-1] → "a")
#   .lower()    → convierte a minúscula para ignorar mayúsculas/minúsculas


# -----------------------------------------------------------------------------
# FUNCIÓN nombres_con_a(nombres)
# -----------------------------------------------------------------------------
# Recibe una lista de nombres.
# Devuelve una nueva lista con los nombres que empiecen O acaben por 'a' (o 'A').
#
# Ejemplos:
#   nombres_con_a(["Ana", "Pedro", "María", "Jordi", "Almudena"])
#       → ["Ana", "María", "Almudena"]

def nombres_con_a(nombres):

    resultado = []
    # Lista vacía donde iremos guardando los nombres que cumplan la condición.

    for nombre in nombres:
        # Recorremos cada nombre de la lista.

        letra_inicial = nombre[0].lower()
        # nombre[0]  → primera letra del nombre. "Ana"[0] → "A"
        # .lower()   → la convertimos a minúscula. "A".lower() → "a"
        # Así "Ana", "ANA" y "ana" funcionan igual: siempre comparamos con "a".
        # Sin .lower(), "Ana" empezaría por "A" y no pasaría el if letra=="a".

        letra_final = nombre[-1].lower()
        # nombre[-1] → última letra del nombre.
        # Los índices negativos en Python cuentan desde el final:
        #   nombre[-1] → última letra
        #   nombre[-2] → penúltima letra
        # "María"[-1] → "a"   "Pedro"[-1] → "o"
        # .lower() por el mismo motivo que antes.

        if letra_inicial == "a" or letra_final == "a":
            # "or" → basta con que UNA de las dos condiciones sea True.
            # Si el nombre empieza por 'a' → entra.
            # Si el nombre acaba por 'a'   → entra.
            # Si cumple las DOS           → también entra, pero solo se añade UNA vez.
            # Si no cumple ninguna        → no entra.
            #
            # Repaso con cada nombre de la prueba:
            #   "Ana"      → inicial="a" ✓ → entra
            #   "Pedro"    → inicial="p", final="o" → NO entra
            #   "María"    → inicial="m", final="a" ✓ → entra
            #   "Jordi"    → inicial="j", final="i" → NO entra
            #   "Almudena" → inicial="a" ✓, final="a" ✓ → entra (una sola vez)

            resultado.append(nombre)
            # Añadimos el nombre original (con sus mayúsculas/minúsculas originales)
            # no la versión .lower(). Queremos devolver "Ana", no "ana".

    return resultado
    # Devolvemos la lista con los nombres seleccionados.

# --- Casos de prueba ---
con_a = nombres_con_a(["Ana", "Pedro", "María", "Jordi", "Almudena"])
print(con_a)
# → ["Ana", "María", "Almudena"]

for con rangos

# =============================================================================
# BUCLES FOR CON RANGE() EN PYTHON
# =============================================================================
# Hasta ahora hemos usado el bucle "for" para recorrer listas y cadenas.
# En este programa aprenderemos a usarlo con range(), que nos permite repetir
# algo un número exacto de veces o recorrer rangos de números.
#
# range() genera una secuencia de números. Sus formas principales son:
#
#   range(n)          → genera: 0, 1, 2, ..., n-1        (n números desde 0)
#   range(a, b)       → genera: a, a+1, ..., b-1         (sin incluir b)
#   range(a, b, paso) → genera: a, a+paso, a+paso*2, ... (de a en a saltos)
#
# En este archivo veremos:
#   1. range() básico para contar y sumar
#   2. range() para recorrer cadenas
#   3. Funciones que usan range() para calcular sumas
#   4. Dibujar figuras con emojis usando bucles
#   5. Árboles de Navidad cada vez más elaborados
# =============================================================================

import random
# Importamos el módulo "random" que usaremos al final para generar números
# aleatorios. Un módulo es una colección de funciones ya escritas que podemos
# usar directamente. "random" viene incluido con Python, no hay que instalarlo.


# =============================================================================
# EJEMPLO 1: range() BÁSICO — CONTAR DEL 0 AL 6
# =============================================================================

for i in range(7):
    # range(7) genera los números: 0, 1, 2, 3, 4, 5, 6
    # OJO: genera 7 números pero el último es 6, no 7. Nunca incluye el límite.
    # En cada vuelta, "i" toma el siguiente valor de la secuencia.
    # "i" es el nombre más habitual para un contador en programación.

    print(i)
    # Imprime el valor actual de i.
    # Salida:
    #   0
    #   1
    #   2
    #   3
    #   4
    #   5
    #   6


# =============================================================================
# EJEMPLO 2: SUMAR TODOS LOS NÚMEROS DEL 0 AL 100
# =============================================================================

suma = 0
# Acumulador: empezamos en 0 y vamos añadiendo números.
# Siempre se inicializa a 0 ANTES del bucle.

for i in range(101):
    # range(101) genera: 0, 1, 2, 3, ..., 100
    # Necesitamos 101 (no 100) porque range() no incluye el número final.
    # PREGUNTA FRECUENTE: ¿por qué no range(100)?
    #   range(100) → llega hasta 99. range(101) → llega hasta 100. ✓

    suma = suma + i
    # Añadimos el valor actual de i a la suma acumulada.
    # ALTERNATIVA más corta: suma += i
    # Paso a paso: suma=0 → 0+0=0 → 0+1=1 → 1+2=3 → 3+3=6 → ... → 5050

print(suma)
# Imprime 5050. Es la suma de los números del 0 al 100.
# Existe una fórmula matemática directa: n*(n+1)/2 → 100*101/2 = 5050


# =============================================================================
# EJEMPLO 3: range() PARA RECORRER UNA CADENA POR POSICIÓN
# =============================================================================

cadena = "hola"

for i in range(len(cadena)):
    # len(cadena) → devuelve 4 (la cadena "hola" tiene 4 caracteres)
    # range(4)    → genera: 0, 1, 2, 3
    # Así recorremos los índices válidos de la cadena.

    print(i, cadena[i])
    # cadena[i] → accede a la letra en la posición i
    # cadena[0] → "h"
    # cadena[1] → "o"
    # cadena[2] → "l"
    # cadena[3] → "a"
    #
    # Salida:
    #   0 h
    #   1 o
    #   2 l
    #   3 a
    #
    # RECUERDA: esta es la "Manera 2" del tema anterior. La manera más pythónica
    # es con enumerate(), pero esta es útil cuando necesitas el índice para
    # acceder a posiciones vecinas (cadena[i-1], cadena[i+1]...).


# =============================================================================
# FUNCIÓN suma_hasta() — SUMAR DEL 0 HASTA UN LÍMITE
# =============================================================================
# Generalizamos el ejemplo 2: en lugar de sumar siempre hasta 100,
# dejamos que el usuario elija el límite.
#
# suma_hasta(10)  → 0+1+2+...+10  = 55
# suma_hasta(100) → 0+1+2+...+100 = 5050
# =============================================================================

def suma_hasta(limite):
    # Parámetro "limite": el número hasta el que queremos sumar.

    suma = 0
    # Acumulador LOCAL a la función. Se reinicia a 0 en cada llamada.

    for i in range(limite + 1):
        # limite + 1 porque range() no incluye el último número.
        # Si limite=10 → range(11) → genera 0, 1, 2, ..., 10 ✓
        # Sin el +1 → range(10) → llegaríamos solo hasta 9 ✗

        suma = suma + i
        # Acumulamos cada número en la suma.

    return suma
    # Devolvemos el resultado al lugar donde se llamó la función.

print(suma_hasta(10))   # → 55
print(suma_hasta(100))  # → 5050


# =============================================================================
# FUNCIÓN suma_entre() — SUMAR ENTRE DOS NÚMEROS
# =============================================================================
# Ahora también elegimos desde dónde empezamos a sumar.
# Usamos la forma range(inicio, final) que empieza en "inicio".
#
# suma_entre(10, 20) → 10+11+12+...+20 = 165
# suma_entre(2, 5)   → 2+3+4+5         = 14
# =============================================================================

def suma_entre(inicio, final):
    # Dos parámetros: "inicio" (desde dónde) y "final" (hasta dónde).

    suma = 0

    for i in range(inicio, final + 1):
        # range(inicio, final+1) genera: inicio, inicio+1, ..., final
        # El +1 sigue siendo necesario para incluir "final".
        # Ejemplo: range(10, 21) → 10, 11, 12, ..., 20 ✓
        # Sin +1: range(10, 20) → 10, 11, ..., 19 — nos falta el 20 ✗

        suma = suma + i

    return suma

print(suma_entre(10, 20))  # → 165
print(suma_entre(2, 5))    # → 14


# =============================================================================
# TRIÁNGULO DE EMOJIS
# =============================================================================
# Usamos range() para controlar cuántos emojis imprimimos en cada línea.
# En la línea 0 → 0 emojis, línea 1 → 1 emoji, ..., línea n → n emojis.
# =============================================================================

tamanyo = 6

for i in range(tamanyo + 1):
    # range(tamanyo+1) → range(7) → genera 0, 1, 2, 3, 4, 5, 6
    # Con tamanyo+1 incluimos la última fila completa.

    print("🟢" * i)
    # "🟢" * i repite el emoji i veces.
    # "🟢" * 0 → ""      (línea vacía)
    # "🟢" * 1 → "🟢"
    # "🟢" * 2 → "🟢🟢"
    # "🟢" * 6 → "🟢🟢🟢🟢🟢🟢"
    # Salida:
    #
    # 🟢
    # 🟢🟢
    # 🟢🟢🟢
    # 🟢🟢🟢🟢
    # 🟢🟢🟢🟢🟢
    # 🟢🟢🟢🟢🟢🟢


# =============================================================================
# FUNCIÓN arbolito() — TRIÁNGULO COMO FUNCIÓN REUTILIZABLE
# =============================================================================

def arbolito(tamanyo):
    # Exactamente el mismo código que arriba, pero dentro de una función.
    # Así podemos llamarla con cualquier tamaño sin reescribir el bucle.

    for i in range(tamanyo + 1):
        print("🟢" * i)

arbolito(10)   # Dibuja un triángulo de 10 filas
# RETO: ¿qué pasa si llamas a arbolito(0)? ¿Y a arbolito(1)?


# =============================================================================
# FUNCIÓN arbolito_puro() — DEVUELVE EL ÁRBOL COMO TEXTO (SIN IMPRIMIR)
# =============================================================================
# Diferencia clave respecto a arbolito():
#   arbolito()      → imprime directamente (no devuelve nada útil)
#   arbolito_puro() → construye el texto y lo devuelve con return
#
# ¿Por qué es mejor devolver el texto?
# Porque podemos guardarlo, modificarlo, enviarlo por email, guardarlo
# en un archivo... En cambio, lo que se imprime con print() se pierde.
# =============================================================================

def arbolito_puro(tamanyo):
    resultado = ""
    # Cadena vacía donde iremos construyendo el árbol línea a línea.

    for i in range(tamanyo + 1):
        resultado += "🟢" * i + "\n"
        # Añadimos los emojis de esta fila MÁS "\n" (salto de línea).
        # "\n" es el carácter especial que representa "pulsar Enter".
        # Sin "\n" todo saldría en una sola línea.
        # ALTERNATIVA: resultado = resultado + "🟢" * i + "\n"

    return resultado
    # Devolvemos todo el árbol como una sola cadena con saltos de línea.

print(arbolito_puro(4))
# Al hacer print() de una cadena con "\n", cada "\n" se convierte en
# un salto de línea visible en pantalla.
# Salida:
#
# 🟢
# 🟢🟢
# 🟢🟢🟢
# 🟢🟢🟢🟢


# =============================================================================
# FUNCIÓN arbol_guay() — ÁRBOL CENTRADO CON FORMA DE TRIÁNGULO INVERTIDO
# =============================================================================
# Ahora mejoramos el árbol:
#   - Empieza en 1 (no en 0) para que la primera fila tenga 1 bola
#   - La primera fila tiene una bola roja (la estrella de la cima)
#   - Cada fila tiene un número IMPAR de bolas: 1, 3, 5, 7...
#   - Añadimos espacios a la izquierda para que quede centrado
#
# Con tamanyo=4, fila por fila:
#   i=1: espacios=3, bolas=1  →  "      🔴"
#   i=2: espacios=2, bolas=3  →  "    🟢🟢🟢"
#   i=3: espacios=1, bolas=5  →  "  🟢🟢🟢🟢🟢"
#   i=4: espacios=0, bolas=7  →  "🟢🟢🟢🟢🟢🟢🟢"
# =============================================================================

def arbol_guay(tamanyo):

    for i in range(1, tamanyo + 1):
        # range(1, tamanyo+1) → empieza en 1, no en 0.
        # Con tamanyo=6 → genera: 1, 2, 3, 4, 5, 6
        # Empezamos en 1 para que la primera fila tenga al menos 1 bola.

        bola = "🟢"
        # Por defecto, todas las bolas son verdes.

        if i == 1:
            bola = "🔴"
            # La primera fila (i=1) es la cima del árbol → bola roja (estrella).

        espacios = tamanyo - i
        # Cuántos espacios dobles ponemos a la izquierda para centrar.
        # Cuanto más arriba (i pequeño), más espacios → la cima queda centrada.
        # i=1 → espacios=5 (muchos, está en la cima)
        # i=6 → espacios=0 (ninguno, es la base)

        bolas = 2 * i - 1
        # Fórmula para obtener números impares: 1, 3, 5, 7, 9, 11...
        # i=1 → 2*1-1=1   i=2 → 2*2-1=3   i=3 → 2*3-1=5
        # Los árboles de Navidad tienen filas con número impar de bolas
        # para que queden simétricas.

        print("  " * espacios + bola * bolas)
        # "  " * espacios → espacios dobles para el centrado (2 espacios cada uno)
        # bola * bolas    → repite la bola el número de veces calculado
        # Los concatenamos con + para formar la línea completa.


# =============================================================================
# FUNCIÓN arbol_decorado() — ÁRBOL CON DECORACIONES ALEATORIAS
# =============================================================================
# La versión más completa. Añade decoraciones aleatorias entre las bolas verdes.
# Novedad: usa un BUCLE ANIDADO (un for dentro de otro for) para pintar
# cada bola de la fila una a una, pudiendo cambiar el emoji en cada posición.
# =============================================================================

def arbol_decorado(tamanyo):
    decoracion = "🟠🟡🔵🎄🎅🤶"
    # Cadena con los emojis de decoración disponibles.
    # decoracion[0] → "🟠"
    # decoracion[1] → "🟡"
    # decoracion[5] → "🤶"
    # len(decoracion) → 6

    for i in range(1, tamanyo + 1):
        # Bucle EXTERIOR: recorre las filas del árbol (igual que arbol_guay).

        bola = "🟢"
        # Bola por defecto para esta fila.

        if i == 1:
            bola = "🔴"
            # La cima sigue siendo roja.

        espacios = tamanyo - i
        bolas = 2 * i - 1
        # Mismas fórmulas de centrado que en arbol_guay().

        print("  " * espacios, end="")
        # Imprimimos los espacios de centrado.
        # end="" evita el salto de línea automático del print().
        # Sin end="" cada print() saltaría de línea y el árbol quedaría roto.

        for j in range(bolas):
            # Bucle INTERIOR: recorre cada posición de bola en esta fila.
            # "j" va de 0 a bolas-1. En cada posición decidimos qué emoji poner.

            bola = "🟢"
            # Reiniciamos la bola a verde para cada posición.

            if random.randint(0, 3) == 0:
                # random.randint(0, 3) genera un número aleatorio: 0, 1, 2 o 3.
                # Solo si sale 0 (probabilidad 1 de cada 4 = 25%), ponemos decoración.
                # Así la mayoría de bolas son verdes y las decoraciones son escasas.

                bola = decoracion[random.randint(0, len(decoracion) - 1)]
                # Elegimos un emoji aleatorio de la cadena "decoracion".
                # random.randint(0, 5) → número entre 0 y 5 (los 6 índices válidos).
                # len(decoracion)-1 = 5, así nunca nos salimos de rango.
                # ALTERNATIVA más corta: bola = random.choice(decoracion)

            print(bola, end="")
            # Imprimimos la bola (verde o decoración) SIN salto de línea,
            # para que todas las bolas de la misma fila queden juntas.

        print()
        # print() sin argumentos imprime solo un salto de línea.
        # Lo necesitamos al final de cada fila para pasar a la siguiente.
        # Es el "Enter" que falta porque usamos end="" en los print anteriores.

arbol_guay(6)
# Llamamos a arbol_guay para probarlo con tamaño 6.
# RETO: llama también a arbol_decorado(10) y observa que cada ejecución
# produce un árbol diferente gracias a random.

Repaso iteración de listas

# =============================================================================
# RECORRIDO DE CADENAS EN PYTHON
# =============================================================================
# Una cadena (string) es una secuencia de caracteres: letras, espacios, comas...
# Python nos permite recorrer esa secuencia carácter a carácter de varias formas.
#
# En este programa veremos:
#   1. Tres maneras distintas de recorrer una cadena con su posición
#   2. Cómo usar esa posición para hacer cosas distintas según sea par o impar
#   3. Cómo meter todo eso en una función reutilizable
#   4. Cómo aplicar esa función a una lista de textos
#
# ¿Qué es la posición (índice)?
#   Cada carácter de una cadena tiene un número de posición que empieza en 0.
#
#   Cadena:    H  o  l  a     q  u  e ...
#   Posición:  0  1  2  3  4  5  6  7 ...
#
#   IMPORTANTE: en Python los índices empiezan en 0, no en 1.
# =============================================================================


cadena = "Hola que tal, Python es genial"
# Declaramos la cadena con la que vamos a trabajar en los tres primeros ejemplos.
# Podría ser cualquier texto: cadena = input("Escribe algo: ")

resultado = ""
# Variable donde iremos construyendo la cadena transformada.
# Empieza vacía y le iremos añadiendo letras una a una.
# NUNCA la inicialices con otro valor o aparecerá texto extra al inicio.


# =============================================================================
# MANERA 1: CONTADOR MANUAL
# =============================================================================
# La forma más intuitiva para un principiante.
# Usamos una variable "posicion" que aumentamos nosotros a mano en cada vuelta.
# =============================================================================

posicion = 1
# Empezamos en 1 porque queremos mostrar posiciones "humanas" (del 1 en adelante).
# Nota: internamente Python usa 0, pero aquí lo mostramos desde 1 para el usuario.

for letra in cadena:
    # El bucle for recorre la cadena carácter a carácter.
    # En cada vuelta, "letra" toma el valor del siguiente carácter.
    # Ejemplo: vuelta 1 → letra="H", vuelta 2 → letra="o", etc.

    print(posicion, letra)
    # Imprime la posición actual y la letra correspondiente.
    # Salida ejemplo:
    #   1 H
    #   2 o
    #   3 l
    #   4 a
    #   5   (el espacio también es un carácter)

    posicion = posicion + 1
    # Incrementamos el contador manualmente para que en la próxima vuelta
    # indique la siguiente posición.
    # ALTERNATIVA más corta: posicion += 1
    # SIN esta línea, "posicion" siempre valdría 1 → mostraría siempre "1"


# =============================================================================
# MANERA 2: CON range() Y len()
# =============================================================================
# En lugar de recorrer las letras directamente, recorremos los números
# de posición (0, 1, 2, ...) y accedemos a cada letra por su índice.
#
# len(cadena) → devuelve el número total de caracteres de la cadena
# range(n)    → genera los números 0, 1, 2, ..., n-1
# cadena[i]   → accede a la letra que está en la posición i
# =============================================================================

for posicion in range(len(cadena)):
    # range(len(cadena)) con nuestra cadena genera: 0, 1, 2, ..., 29
    # (porque "Hola que tal, Python es genial" tiene 30 caracteres)
    # En cada vuelta, "posicion" vale el índice actual (empieza en 0).

    print(posicion + 1, cadena[posicion])
    # cadena[posicion] → accede a la letra en esa posición.
    #   cadena[0]  →  "H"
    #   cadena[1]  →  "o"
    #   cadena[4]  →  " " (espacio)
    # Sumamos +1 al mostrar para que el usuario vea 1, 2, 3... en lugar de 0, 1, 2...
    #
    # VENTAJA de esta manera sobre la 1: con "posicion" podemos acceder a
    # la letra anterior (cadena[posicion-1]) o siguiente (cadena[posicion+1])


# =============================================================================
# MANERA 3: CON enumerate() ← LA MÁS USADA EN PYTHON
# =============================================================================
# enumerate() nos da automáticamente DOS cosas en cada vuelta:
#   - La posición (índice, empezando en 0)
#   - El carácter en esa posición
# Es la manera más "pythónica" (la preferida por los programadores Python).
# =============================================================================

for posicion, letra in enumerate(cadena):
    # enumerate() devuelve pares (posición, carácter) en cada vuelta.
    # Desglosamos ese par en dos variables: "posicion" y "letra".
    # Equivale a hacer las maneras 1 y 2 a la vez, sin código extra.
    # Ejemplo: vuelta 1 → posicion=0, letra="H"
    #          vuelta 2 → posicion=1, letra="o"

    print(posicion + 1, letra)
    # Mostramos posición (desde 1) y letra, igual que en las maneras anteriores.

    if posicion % 2 == 0:
        # El operador % calcula el RESTO de la división.
        # posicion % 2 → si el resto es 0, la posición es PAR; si es 1, es IMPAR.
        # Posiciones pares:   0, 2, 4, 6, 8, ...  (H, l, ' ', u, ' ', t, ...)
        # Posiciones impares: 1, 3, 5, 7, 9, ...  (o, a, q, e, a, ...)
        # RECUERDA: la posición 0 (la primera) es PAR en Python.

        resultado = resultado + letra.lower()
        # .lower() convierte la letra a minúscula.
        # Si ya era minúscula, no cambia nada.
        # La añadimos al final de "resultado".
        # ALTERNATIVA más corta: resultado += letra.lower()

    else:
        # Si la posición es impar (resto 1)

        resultado = resultado + letra.upper()
        # .upper() convierte la letra a mayúscula.
        # Los espacios y comas no cambian con upper() ni lower().

print(resultado)
# Muestra la cadena transformada al estilo "camello alternado":
# "Hola que tal, Python es genial"
#  ↓
# "hOlA QuE TaL, pYtHoN Es gEnIaL"
# (posiciones pares en minúscula, impares en mayúscula)


# =============================================================================
# FUNCIÓN texto_camello()
# =============================================================================
# Metemos todo el proceso en una función para poder reutilizarlo con
# cualquier texto sin tener que reescribir el código.
#
# Entrada:  una cadena de texto cualquiera
# Salida:   la misma cadena con letras alternadas mayúscula/minúscula
#
# Ejemplo: texto_camello("hola") → "hOlA"
# =============================================================================

def texto_camello(cadena):
    # "def" define la función. "cadena" es el parámetro:
    # el texto que recibirá la función cuando la llamemos.
    # Este "cadena" es LOCAL a la función, no tiene nada que ver
    # con la variable "cadena" que declaramos arriba.

    resultado = ""
    # IMPORTANTE: declaramos resultado DENTRO de la función.
    # Cada vez que llamemos a la función, resultado empieza vacío desde cero.
    # Si estuviera fuera, las llamadas anteriores acumularían texto sobrante.

    for posicion, letra in enumerate(cadena):
        # Recorremos la cadena que nos han pasado como parámetro,
        # obteniendo posición y letra en cada vuelta.

        print(posicion + 1, letra)
        # Muestra el progreso por pantalla mientras trabaja.
        # En un programa real probablemente quitaríamos este print,
        # ya que solo nos interesa el resultado final.

        if posicion % 2 == 0:
            resultado = resultado + letra.lower()
            # Posición par → minúscula
        else:
            resultado = resultado + letra.upper()
            # Posición impar → mayúscula

    return resultado
    # Devuelve la cadena transformada al lugar donde se llamó la función.
    # Sin "return" la función haría todo el trabajo pero no compartiría el resultado.


# =============================================================================
# LLAMADAS A LA FUNCIÓN
# =============================================================================

cadena = "Hola que tal, Python es genial"
# Reasignamos la variable cadena (la anterior también valía, pero así
# queda claro con qué texto trabajamos en esta sección).

print(texto_camello("hola que tal"))
# Llamada con texto directo (literal de cadena).
# La función recibe "hola que tal" como parámetro.
# Imprime: "hOlA QuE TaL"

print(texto_camello(cadena))
# Llamada pasando una variable como argumento.
# La función recibe el contenido de "cadena": "Hola que tal, Python es genial"
# Imprime: "hOlA QuE TaL, pYtHoN Es gEnIaL"

versos = ["Vi un gato muerto", "espanzurrado en la carretera", "una gaviota acechaba", "y la tormenta tronaba"]
# Lista de cadenas. Cada elemento es una línea de un poema.
# Podría ser cualquier colección de textos: nombres, frases, párrafos...

for verso in versos:
    # Recorremos la lista. En cada vuelta "verso" toma el valor de la siguiente línea.
    # Vuelta 1: verso = "Vi un gato muerto"
    # Vuelta 2: verso = "espanzurrado en la carretera"
    # ...

    print(texto_camello(verso))
    # Aplicamos la función a cada verso y mostramos el resultado.
    # Salida:
    #   "vI Un gAtO MuErTo"
    #   "eSpAnZuRrAdO En lA CaRrEtErA"
    #   "uNa gAvIoTa aCeChAbA"
    #   "y lA ToRmEnTa tRoNaBa"
    #
    # ALTERNATIVA con comprensión de listas:
    #   transformados = [texto_camello(v) for v in versos]
    #   print('\n'.join(transformados))

Más repaso while

# =============================================================================
# CALCULAR LA MEDIA DE UNA SERIE DE NÚMEROS
# =============================================================================
# Este programa pide números al usuario uno a uno y los va sumando.
# Cuando el usuario introduce un 0, el programa para y calcula la media.
#
# La media es la suma de todos los números dividida entre cuántos hay.
# Ejemplo: si introduces 4, 8 y 6 → media = (4+8+6) / 3 = 6.0
#
# Interacción esperada:
#   Digite un numero: 4
#   Digite un numero: 8
#   Digite un numero: 6
#   Digite un numero: 0   ← el 0 para el bucle, no se suma
#   6.0
# =============================================================================


def calcular_media():
    # Definimos la función. No recibe parámetros porque los datos
    # los pedirá ella misma al usuario con input().
    # Devolverá un número decimal (float) con la media calculada.

    numero = float(input("Digite un numero: "))
    # Pedimos el PRIMER número antes del bucle.
    # Necesitamos hacerlo aquí para poder comprobar la condición del while.
    # float() convierte el texto que escribe el usuario a número decimal.
    # Usamos float en lugar de int para aceptar números como 3.5 o 7.8.
    # PRUEBA: cambia float() por int() e intenta introducir 3.5 para ver el error.

    suma = 0
    # Variable acumuladora: irá sumando todos los números que introduzca el usuario.
    # Siempre se inicializa a 0 antes del bucle.
    # Ejemplo paso a paso con 4, 8, 6:
    #   Antes del bucle: suma = 0
    #   Tras el 4:       suma = 4
    #   Tras el 8:       suma = 12
    #   Tras el 6:       suma = 18

    contador = 0
    # Variable contadora: cuenta cuántos números ha introducido el usuario.
    # También se inicializa a 0. La necesitamos para dividir al calcular la media.
    # Ejemplo: con 4, 8 y 6 → contador llegará a 3.
    # ¡IMPORTANTE! El 0 final NO se cuenta porque para el bucle antes de sumarlo.

    while numero != 0:
        # El bucle se repite MIENTRAS el número introducido sea distinto de 0.
        # En cuanto el usuario escribe 0, el bucle termina sin sumar ni contar ese 0.
        # "!=" significa "distinto de".
        # ALTERNATIVA equivalente: while not numero == 0:

        # ↓ Aquí estaba el comentario "????" en el código original.
        # Lo que hace este bloque es acumular el número en la suma
        # y aumentar el contador en 1 por cada número válido introducido.

        suma = suma + numero
        # Añadimos el número actual a la suma acumulada.
        # Es lo mismo que escribir: suma += numero
        # Solo llegamos aquí si numero != 0, así que el 0 nunca se suma.

        contador = contador + 1
        # Contamos este número como válido.
        # Es lo mismo que escribir: contador += 1

        numero = float(input("Digite un numero: "))
        # Pedimos el SIGUIENTE número al final del bucle.
        # Esto sobreescribe el valor anterior de "numero".
        # En la próxima comprobación del while se usará este nuevo valor.
        # Si el usuario escribe 0 aquí, el while parará en su próxima vuelta.

    # Cuando llegamos aquí, el bucle ya ha terminado.
    # En este punto sabemos que:
    #   - "suma" tiene la suma de todos los números introducidos (sin el 0)
    #   - "contador" tiene cuántos números se introdujeron (sin el 0)

    return suma / contador
    # Calculamos y devolvemos la media: suma total dividida entre cuántos números hay.
    # CUIDADO: si el usuario introduce 0 como primer número, contador valdrá 0
    # y dividir entre 0 causará un error (ZeroDivisionError).
    # MEJORA para evitar ese error:
    #   if contador == 0:
    #       return 0
    #   return suma / contador


# =============================================================================
# PROGRAMA PRINCIPAL
# =============================================================================

media = calcular_media()
# Llamamos a la función. Esta ejecuta todo el proceso de pedir números
# y calcular la media. El resultado que devuelve "return" se guarda aquí.

print(media)
# Mostramos la media por pantalla.
# MEJORA para que quede más claro:
#   print(f"La media de los números introducidos es: {media:.2f}")
#   El :.2f muestra solo 2 decimales. Ejemplo: 6.333333 → 6.33