U1 - Programación en Python

1 Área y perímetro de un círculo

# Área y perímetro de un círculo
def calcular_area(radio):
    pi = 3.14159
    area = pi * radio ** 2
    return area

def calcular_perimetro(radio):
    pi = 3.14159
    perimetro = 2 * pi * radio
    return perimetro

def calcular_area_y_perimetro(radio):
    area = calcular_area(radio)
    perimetro = calcular_perimetro(radio)
    return area, perimetro

calcular_area(radio=2.5)
calcular_perimetro(radio=2.5)

area, perimetro = calcular_area_y_perimetro(radio=2.5)
resultados = calcular_area_y_perimetro(radio=2.5)

print(type(area))
print(type(perimetro))
print(type(resultados))
print(type(resultados[0]))
print(type(resultados[1]))

2 Promociones

# Promociones
def calcular_precio(monto, medio):
    if medio == "efectivo":
        monto_final = monto
    elif medio == "débito":
        monto_final = monto * (1 - 0.1)
    elif medio == "crédito":
        monto_final = monto * (1 + 0.05)
    else:
        print(f"El medio {medio} es desconocido.")
        monto_final = monto

    return monto_final

calcular_precio(80, "efectivo")
calcular_precio(80, "débito")
calcular_precio(80, "crédito")

# Extra:
# Usando valor por defecto 'efectivo'
def calcular_precio(monto, medio="efectivo"):
    if medio == "efectivo":
        monto_final = monto
    elif medio == "débito":
        monto_final = monto * (1 - 0.1)
    elif medio == "crédito":
        monto_final = monto * (1 + 0.05)
    else:
        print(f"El medio {medio} es desconocido.")
        monto_final = monto

    return monto_final

# Usando valor por defecto None
def calcular_precio(monto, medio=None):
    if medio is None:
        medio = "efectivo"

    if medio == "efectivo":
        monto_final = monto
    elif medio == "débito":
        monto_final = monto * (1 - 0.1)
    elif medio == "crédito":
        monto_final = monto * (1 + 0.05)
    else:
        print(f"El medio {medio} es desconocido.")
        monto_final = monto

    return monto_final

3 Etapas de la vida

# Etapas de la vida
def obtener_etapa(edad):
    if edad < 2:
        etapa = "bebé"
    elif edad < 4:
        etapa = "infante"
    elif edad < 13:
        etapa = "niño/a"
    elif edad < 20:
        etapa = "adolescente"
    elif edad < 65:
        etapa = "adulto/a"
    else:
        etapa = "persona mayor"

    mensaje = f"La persona es un/a {etapa}"
    print(mensaje)

    return None

obtener_etapa(67)

# Comentarios:
# 1. Un posible problema con esta implementación es que asume que la edad es positiva.
#    También admite edades irrealmente altas.

4 Conteo de caracteres

# Conteo de caracteres
def contar_caracteres(texto):
    caracteres = {}
    for caracter  in texto:
        # No considerar espacios
        if caracter == " ":
            continue

        # Pasar el caracter a minuscula
        caracter_minuscula = caracter.lower()
        if caracter_minuscula not in caracteres:
            caracteres[caracter_minuscula] = 1
        else:
            caracteres[caracter_minuscula] += 1

    return caracteres

contar_caracteres("Ahora es mejor que nunca")

# Comentarios:
# 1. Implementacion usando 'in' vs 'not in'
# 2. ¿Podrían meter el .lower en otro lado? Si, se podría pasar el texto a minúsculas antes de empezar a iterar.
# 3. ¿En cuál implementación se realizaría menos trabajo?
# 4. ¿Podriamos eliminar el `continue`?

# Moraleja: no hay una única implementación correcta.

5 Orden de mérito

# Orden de mérito
notas = [
    ("Escalada", 9),
    ("Alonso", 7),
    ("Pérez", 8),
    ("Castro", 8),
    ("Rossini", 10),
    ("Martínez", 9),
    ("Pérez", 6),
    ("Riquelme", 5),
]

# Parte 1
notas_dict = {}
for registro in notas:
    # Opcion 1:
     notas_dict[registro[0]] = registro[1]

    # Opcion 2:
    # apellido = registro[0]
    # nota = registro[1]
    # notas_dict[apellido] = nota

    # apellido, nota = registro
    # notas_dict[apellido] = nota

# Pregunta: ¿Que pasa si hay dos personas con el mismo apellido?

# Parte 2
notas_dict = {}
for registro in notas:
    apellido = registro[0]
    nota = registro[1]
    if nota not in notas_dict:
        notas_dict[nota] = [apellido]
    else:
        notas_dict[nota].append(apellido)
        # Otras opciones:
        # notas_dict[nota] = notas_dict[nota] + [apellido]
        # notas_dict[nota] = notas_dict[nota].extend([apellido])

6 Rendimento académico

# Rendimento académico
notas = {
    "Ana": [8, 9, 10],
    "Luis": [6, 7, 8, 3, 9],
    "Carla": [10, 9, 10],
    "Marcos": [5, 6],
    "Sofía": [7, 7, 8],
    "Pedro": [6, 4, 5, 6, 3, 8],
    "Lucía": [9, 8, 10, 9]
}

def resumir_notas(datos, modo):
    resultado = {}

    if modo == "promedio":
        for nombre, notas in datos.items():
            resultado[nombre] = sum(notas) / len(notas)
    elif modo == "proporcion":
        # NOTE: Esto tal vez sea un poco complicado
        for nombre, notas in datos.items():
            n_aprobado = 0
            for nota in notas:
                if nota >= 6:
                    n_aprobado += 1
            resultado[nombre] = n_aprobado / len(notas)

    else:
        print(f"El modo {modo} es desconocido")
        return None

    return resultado

resumir_notas(notas, "promedio")
resumir_notas(notas, "proporcion")

# ¿Cómo sería la implementación sin .items()?
# ¿Podría escribir un único for loop? ¿Qué cambiaría? ¿Pros y contras?
# ¿Hay algún conflicto entre la variable 'notas' del ambiente global y la del ambiente local de
# la función?
# El cálculo de la proporción se podría abstraer en una función.

# Podría usarse un "list comprehension"
# len([nota for nota in notas if nota >= 6])

# Podría usarse un generador:
# sum(nota >= 6 for nota in notas)

7 Índice de precios

ipc_2024 = [20.6, 13.2, 11.0, 8.8, 4.2, 4.6, 4.0, 4.2, 3.5, 2.7, 2.4, 2.7]

# Menor y mayor valor
min(ipc_2024)
max(ipc_2024)

# IPC promedio mensual
sum(ipc_2024) / len(ipc_2024)

# Mes de mayor inflación
ipc_2024.index(max(ipc_2024)) # 0 + 1 -> enero

# Inflación mediana
ipc_ordenado = sorted(ipc_2024)

# Opcion 1:
(ipc_ordenado[5] + ipc_ordenado[6]) * 0.5

# Opcion 2:
sum(ipc_ordenado[5:7]) * 0.5

# Opcion 3:
n_inicio = (len(ipc_ordenado) // 2) - 1 # Resto 1 porque Python indexa desde 0
n_fin = (len(ipc_ordenado) // 2 + 1) -1   # Resto 1 porque Python indexa desde 0

# Sumo 1 porque el slicing no es inclusivo por derecha
sum(ipc_ordenado[n_inicio:(n_fin + 1)]) * 0.5

# Rango de IPC sin min y max
ipc_ordenado[-1] - ipc_ordenado[0]

8 Resúmenes estadísticos

def media(x):
    return sum(x) / len(x)


def rango(x):
    return max(x) - min(x)


# Opcion 1.
def varianza(x):
    x_media = media(x)
    suma = 0
    for x_i in x:
        suma += (x_i - x_media) ** 2
    return suma / len(x)


# Opcion 2. Se podría usar un list-comprehension también.
def varianza2(x):
    x_media = media(x)
    distancias = []
    for x_i in x:
        distancias.append((x_i - x_media) ** 2)
    return media(distancias)

def desvio_estandar(x):
    return varianza(x) ** 0.5


def mediana(x):
    x_ordenado = sorted(x)
    n = len(x)
    mitad = n // 2

    if n % 2 == 1:
        # Cantidad impar: se toma el elemento del medio
        mediana = x_ordenado[mitad]
    else:
        # Cantidad par: se promedian los dos del medio
        mediana = (x_ordenado[   - 1] + x_ordenado[mitad]) * 0.5

    return mediana

9 Validación de DNI

def validar_dni(dni):
    # Si no 'dni' tiene puntos, es DNI válido cuando:
    # - todos los caracteres sean todos dígitos
    # - la longitud esté entre 7 y 8
    if dni.count(".") == 0:
        return dni.isdigit() and 7 <= len(dni) <= 8

    # Si 'dni' tiene puntos, debe tener exactamente 2 puntos.
    partes = dni.split(".")
    if len(partes) != 3:
        return False

    # Si alguna de las partes no contiene todos dígitos, no es un DNI válido.
    # Opcion 1
    for parte in partes:
        if not parte.isdigit():
            return False
    # Opcion 2
    # if not all(parte.isdigit() for parte in partes):
    #     return False

    longitudes_validas = (
        len(partes[0]) in (1, 2) and
        len(partes[1]) == 3 and
        len(partes[2]) == 3
    )

    return longitudes_validas


validar_dni("40.094.127")
validar_dni("19053512")
validar_dni("6.392.780")

validar_dni("40,094,127")
validar_dni("19-053-512")
validar_dni("123456")
validar_dni("40..094127")
validar_dni("40.09412.7")

10 La física del rebote

altura_inicial = 100 # metros
rebote = 3 / 5

# Parte 1 (no es necesario el uso de una variable adicional)
altura_actual = altura_inicial
for i in range(10):
    altura_actual = altura_actual * rebote
    print("Rebote", i + 1, "| Altura:", round(altura_actual, 4))

# Parte 2
def calcular_rebotes(altura_inicial, rebotes_n):
    alturas = []
    altura_actual = altura_inicial
    for _ in range(rebotes_n): # '_' es una convencion cuando la variable de iteración no se usa
        altura_actual = altura_actual * rebote
        alturas.append(altura_actual)

    return alturas

calcular_rebotes(100, 10)


# Parte 3
def calcular_rebotes(altura_inicial, rebotes_n):
    alturas = []
    altura_actual = altura_inicial
    for _ in range(rebotes_n): # '_' es una convencion cuando la variable de iteración no se usa
        altura_actual = altura_actual * rebote
        alturas.append(altura_actual)

        if altura_actual < 0.01:
            break

    return alturas

calcular_rebotes(100, 50)
len(calcular_rebotes(100, 50)) # Solo devuelve 19 rebotes

# Se podria conversar si es necesario crear 'altura_actual'
# Para el caso de las listas una opción, un poco más rebuscada, es:

def calcular_rebotes(altura_inicial, rebotes_n):
    alturas = [altura_inicial * rebote]
    for i in range(rebotes_n):
        alturas.append(alturas[i] * rebote)

        if alturas[-1] < 0.01:
            break

    return alturas

calcular_rebotes(100, 50)
len(calcular_rebotes(100, 50)) # Solo devuelve 19 rebotes

11 Un montón de plata

billete_grosor = 0.11 * 0.001  # grosor de un billete en metros
altura_monumento = 70          # altura en metros

dias = 1
billetes_n = 1
while billetes_n * billete_grosor <= altura_monumento:
    billetes_n = billetes_n * 2
    dias = dias + 1

print("Se necesitan", dias, "dias.")
print("La altura de la pila es", billetes_n * billete_grosor, "metros.")
print("Se colocaron", billetes_n, "billetes.")


# Punto extra
def calcular_dias(altura_objetivo):
    billete_grosor = 0.11 * 0.001
    dias = 1
    billetes_n = 1
    while billetes_n * billete_grosor <= altura_objetivo:
        billetes_n = billetes_n * 2
        dias = dias + 1
    return dias

calcular_dias(0.1)

12 La conjetura de Collatz

def collatz(n):
    # Inicializar lista con solo el valor 'n'
    secuencia = [n]
    while n != 1:
        # Numero par
        if n % 2 == 0:
            n = n // 2 # Notar uso de división entera
        # Numero impar
        else:
            # ¿Puede el resultado ser flotante?
            n = 3 * n + 1
        secuencia.append(n)
    return secuencia


collatz(5)
collatz(20)
collatz(16)
collatz(37)

# Para discutir: ¿Podría implementarse con un 'while True'? ¿Cómo?

13 Adivina el número

# Correr de la terminal con 'python {nombre_de_programa}.py'
import random

print(" ======================== Adivina el número! ======================== ")

numero_secreto = random.randint(0, 100)

mensaje = "Ingresa un numero entero: "
conteo = 0
tope_intentos = 5
while True:
    numero = int(input(mensaje))
    conteo += 1
    if numero < numero_secreto:
        mensaje = f"¡Ups! El numero secreto es mayor a {numero}. Prueba de nuevo: "
    elif numero > numero_secreto:
        mensaje = f"¡Ups! El numero secreto es menor a {numero}. Prueba de nuevo: "
    else:
        print(f"Has ganado, felicitaciones! Te llevó tan solo {conteo} intentos.")
        break

    if conteo == (tope_intentos - 1):
        print("¡Cuidado! Te queda solo un intento.")

    if conteo >= tope_intentos:
        print(f"Has perdido :(. El numero secreto era {numero_secreto}.")
        break

14 Conteo de caracteres Pythonico

def contar_caracteres(texto):
    texto = texto.lower() # Pasa a minuscula al principio
    caracteres = {}
    for caracter  in texto:
        # Solo considerar letras del alfabeto
        if not caracter.isalpha():
            continue

        # Si el caracter no esta en el diccionario, devuelve 0
        # Si está, devuelve su conteo.
        # En ambos casos, le suma 1.
        caracteres[caracter] = caracteres.get(caracter, 0) + 1

    return caracteres

contar_caracteres("Ahora es mejor que nunca")
contar_caracteres("Ahora es mejor que nunca!!   +_")
contar_caracteres("Ahora es mejor que nunca!!   +_") == contar_caracteres("Ahora es mejor que nunca")

15 Validador de contraseñas 😱

CARACTERES_ESPECIALES = "@#$%^&*()"

def contiene_letra(texto):
    # True si al menos un caracter es una letra
    for caracter in texto:
        if caracter.isalpha():
            return True
    return False


def contiene_numero(texto):
    # True si al menos un caracter es un numero (del sistema decimal)
    for caracter in texto:
        if caracter.isdecimal():
            return True
    return False


def contiene_especial(texto):
    # True si al menos un caracter es un numero
    for caracter in texto:
        if caracter in CARACTERES_ESPECIALES:
            return True
    return False

def validar_pwd(pwd):
    errores = []

    if not (8 <= len(pwd) <= 24):
        errores.append("Debe tener entre 8 y 24 caracteres.")

    if not contiene_letra(pwd):
        errores.append("Debe contener al menos una letra.")
    if not contiene_numero(pwd):
        errores.append("Debe contener al menos un número.")
    if not contiene_especial(pwd):
        errores.append("Debe contener al menos un carácter especial '@#$%^&*()'.")

    caracteres_desconocidos = []
    for caracter in pwd:
        # Hablar sobre si es posible simplificar esto
        if (not caracter.isdecimal()) and (not caracter.isalpha()) and (caracter not in CARACTERES_ESPECIALES):
            caracteres_desconocidos.append(caracter)

    # Equivale a len(caracteres_desconocidos) > 0
    if caracteres_desconocidos:
        errores.append(
            f"La contraseña contiene los siguientes caracteres desconocidos {caracteres_desconocidos}"
        )

    if errores == []:
        return True

    return errores


while True:
    password = input("Ingrese una contraseña (o ENTER para salir): ")
    if password == "":
        print("Operación cancelada.")
        break

    resultado = validar_pwd(password)

    # Acá NO se puede usar 'if es_valida'.
    if resultado is True:
        print("Contraseña válida.")
        break

    print("La contraseña no es válida:")
    for error in resultado:
        print("-", error)

16 Conteo de caracteres II 😱

def contar_caracteres(texto, orden="aparicion"):
    texto = texto.lower() # Pasa a minuscula al principio
    caracteres = {}
    for caracter  in texto:
        # Solo considerar letras del alfabeto
        if not caracter.isalpha():
            continue

        # Si el caracter no esta en el diccionario, devuelve 0
        # Si está, devuelve su conteo.
        # En ambos casos, le suma 1.
        caracteres[caracter] = caracteres.get(caracter, 0) + 1

    if orden == "aparicion":
        salida = caracteres
    elif orden == "alfabetico":
        salida = {}
        claves = sorted(caracteres.keys()) # No hace falta el .keys() en realidad
        for clave in claves:
            salida[clave] = caracteres[clave]
    elif orden == "frecuencia":
        salida = {}
        claves = list(caracteres.keys()) # No hace falta el .keys() en realidad
        claves_ordenadas = sorted(claves, key=lambda k: caracteres[k], reverse=True)
        for clave in claves_ordenadas:
            salida[clave] = caracteres[clave]

    return salida

contar_caracteres("Ahora es mejor que nunca", "aparicion")
contar_caracteres("Ahora es mejor que nunca", "alfabetico")
contar_caracteres("Ahora es mejor que nunca", "frecuencia")