Funciones en Python — práctica con un conversor de temperaturas, una calculadora modular y piedra, papel o tijera (y el menú que dejó de pedir datos)

La primera versión de mi calculadora de FP1 tenía un menú precioso y un problema: para comprobar si la división entre cero estaba bien, tenía que ejecutar el programa, elegir la opción 4, teclear 7, teclear 0… y repetirlo cada vez que cambiaba una línea. A la décima vez me equivoqué de tecla, el programa se colgó en otro sitio y ya no sabía qué estaba probando. En la revisión, el profesor señaló la función dividir y me preguntó: «¿y por qué esta función lee del teclado?». No tenía respuesta. La lección que me llevé es la de toda esta práctica: las funciones que calculan no deberían ni pedir datos ni imprimirlos. Reciben, calculan y devuelven. Lo demás va aparte.

En la teoría de funciones en Python viste def, los parámetros, return, la pila de llamadas y el ámbito. Aquí lo usamos en tres programas completos de nivel medio, los mismos de siempre pero rehechos: un conversor de temperaturas donde unas funciones se apoyan en otras, una calculadora modular que separa el cálculo de la presentación y un piedra, papel o tijera que limpia lo que «teclea» el jugador. Ninguno usa input(): los datos van escritos al final de cada programa, como si fueran lo que el usuario tecleó (con mayúsculas, espacios y alguna errata), para que puedas ejecutarlos y obtener exactamente la misma salida que aquí.

Antes de empezar

Conviene tener claro:

  • Cómo se define y se llama una función, y que def no ejecuta el cuerpo.
  • La diferencia entre print (enseñar) y return (devolver), y que sin return se devuelve None.
  • Que una función puede llamar a otras y que cada llamada tiene sus propias variables locales.
  • Los condicionales (if, elif, else) y los bucles for.

Si alguno te suena dudoso, repasa la teoría antes de seguir.

Crea una carpeta practica_funciones con un archivo por programa (conversor.py, calculadora.py, ppt.py) y ábrelos en VS Code o en Thonny. Ejecuta cada uno antes de leer su explicación y compara con la salida del artículo.

Programa 1: conversor de temperaturas modular

Un conversor entre Celsius, Fahrenheit y Kelvin con seis opciones: de °C a °F (1), de °C a K (2), de °F a °C (3), de K a °C (4), una conversión completa desde °C (5) y de °F a K (6). Lo que lo complica: hay que rechazar opciones que no existen y temperaturas imposibles (por debajo del cero absoluto), y queremos que ninguna fórmula esté escrita dos veces.

La idea

Repartimos el trabajo en tres tipos de funciones:

  1. De cálculo: una por fórmula. Reciben un número y devuelven otro. No imprimen.
  2. De comprobación: responden una pregunta (¿de qué escala parte esta opción?, ¿esta temperatura es posible?) y devuelven la respuesta.
  3. De presentación: convertir(opcion, valor) decide qué hacer y devuelve el texto que se va a ver. Solo el programa principal hace print.

Y una idea clave: para pasar de °F a K no escribimos una fórmula nueva. Ya sabemos pasar de °F a °C y de °C a K: encadenamos las dos funciones. A eso se le llama composición.

Antes de programar, piensa en qué cosas haces más de una vez. Aquí, convertir °C a °F aparece en la opción 1 y en la conversión completa: eso es una función.

El código

# Programa 1 · Conversor de temperaturas modular
# Sin input(): las pruebas van al final, cada una con su opción y su temperatura.

# --- Funciones de cálculo: reciben un número y DEVUELVEN otro (no imprimen nada) ---
def celsius_a_fahrenheit(celsius):
    return round(celsius * 9 / 5 + 32, 2)


def celsius_a_kelvin(celsius):
    return round(celsius + 273.15, 2)


def fahrenheit_a_celsius(fahrenheit):
    return round((fahrenheit - 32) * 5 / 9, 2)


def kelvin_a_celsius(kelvin):
    return round(kelvin - 273.15, 2)


def fahrenheit_a_kelvin(fahrenheit):
    # composición: no copiamos fórmulas, encadenamos dos funciones que ya funcionan
    return celsius_a_kelvin(fahrenheit_a_celsius(fahrenheit))


# --- Funciones de comprobación: responden una pregunta y devuelven la respuesta ---
def escala_de_origen(opcion):
    """Opciones 1, 2 y 5 parten de °C; la 3 y la 6, de °F; la 4, de K."""
    if opcion == 3 or opcion == 6:
        return "°F"
    if opcion == 4:
        return "K"
    return "°C"


def es_temperatura_valida(valor, escala):
    """Nada puede estar por debajo del cero absoluto (−273,15 °C = −459,67 °F = 0 K)."""
    if escala == "°C":
        return valor >= -273.15
    if escala == "°F":
        return valor >= -459.67
    return valor >= 0


# --- Funciones de presentación: preparan el texto que se verá ---
def conversion_completa(celsius):
    """Las tres escalas de golpe y una comprobación de ida y vuelta (°C → °F → °C)."""
    fahrenheit = celsius_a_fahrenheit(celsius)
    vuelta = fahrenheit_a_celsius(fahrenheit)    # si todo está bien, vuelve al valor de partida
    return f"{celsius} °C = {fahrenheit} °F = {celsius_a_kelvin(celsius)} K · ida y vuelta: {vuelta} °C"


def convertir(opcion, valor):
    """Devuelve el texto de la conversión elegida (1-6) o un aviso si algo no cuadra."""
    if opcion < 1 or opcion > 6:
        return f"Opción {opcion} no válida: elige de 1 a 6"
    escala = escala_de_origen(opcion)
    if not es_temperatura_valida(valor, escala):
        return f"{valor} {escala} está por debajo del cero absoluto"
    if opcion == 1:
        return f"{valor} °C = {celsius_a_fahrenheit(valor)} °F"
    if opcion == 2:
        return f"{valor} °C = {celsius_a_kelvin(valor)} K"
    if opcion == 3:
        return f"{valor} °F = {fahrenheit_a_celsius(valor)} °C"
    if opcion == 4:
        return f"{valor} K = {kelvin_a_celsius(valor)} °C"
    if opcion == 6:
        return f"{valor} °F = {fahrenheit_a_kelvin(valor)} K"
    return conversion_completa(valor)            # solo queda la opción 5


# --- Programa principal: cada línea es lo que antes tecleaba el usuario ---
print("=== CONVERSOR DE TEMPERATURAS ===")
print(convertir(1, 100))
print(convertir(2, -40))
print(convertir(3, 98.6))
print(convertir(4, 0))
print(convertir(5, 37))
print(convertir(6, 212))
print(convertir(1, -300))
print(convertir(9, 20))

Salida

=== CONVERSOR DE TEMPERATURAS ===
100 °C = 212.0 °F
-40 °C = 233.15 K
98.6 °F = 37.0 °C
0 K = -273.15 °C
37 °C = 98.6 °F = 310.15 K · ida y vuelta: 37.0 °C
212 °F = 373.15 K
-300 °C está por debajo del cero absoluto
Opción 9 no válida: elige de 1 a 6

Comprobamos a mano: 100 × 9 / 5 + 32 = 180 + 32 = 212; −40 + 273.15 = 233.15; (98.6 − 32) × 5 / 9 = 66.6 × 5 / 9 = 37.0; 0 − 273.15 = −273.15. La opción 5 hace las tres de golpe con 37 °C: 37 × 9 / 5 + 32 = 98.6 °F y 37 + 273.15 = 310.15 K, y la vuelta de 98.6 °F a °C da otra vez 37.0, señal de que las dos fórmulas son coherentes entre sí. La opción 6 pasa 212 °F a 100.0 °C y de ahí a 373.15 K. Las dos últimas líneas son los casos incómodos: −300 °C no existe y la opción 9 tampoco.

La función que decide: convertir

Es la más larga, así que la miramos sola:

def convertir(opcion, valor):
    """Devuelve el texto de la conversión elegida (1-6) o un aviso si algo no cuadra."""
    if opcion < 1 or opcion > 6:
        return f"Opción {opcion} no válida: elige de 1 a 6"
    escala = escala_de_origen(opcion)
    if not es_temperatura_valida(valor, escala):
        return f"{valor} {escala} está por debajo del cero absoluto"
    if opcion == 1:
        return f"{valor} °C = {celsius_a_fahrenheit(valor)} °F"
    if opcion == 2:
        return f"{valor} °C = {celsius_a_kelvin(valor)} K"
    if opcion == 3:
        return f"{valor} °F = {fahrenheit_a_celsius(valor)} °C"
    if opcion == 4:
        return f"{valor} K = {kelvin_a_celsius(valor)} °C"
    if opcion == 6:
        return f"{valor} °F = {fahrenheit_a_kelvin(valor)} K"
    return conversion_completa(valor)            # solo queda la opción 5

Va en este orden, y el orden importa:

  1. Primero, la opción. Si no está entre 1 y 6, devuelve el aviso y termina (el return corta la función: no se mira nada más).
  2. Después, la temperatura. Pregunta a escala_de_origen de qué escala parte la opción y a es_temperatura_valida si el valor es posible. Si no, otro aviso y fuera.
  3. Por último, la conversión. Cada if tiene su return, así que no hace falta elif: en cuanto uno se cumple, la función termina. Si no se ha cumplido ninguno, solo puede ser la opción 5.

Traza de convertir(1, -300):

PasoLlamadaDevuelveConsecuencia
1opcion < 1 or opcion > 6 → 1 < 1 or 1 > 6Falsesigue
2escala_de_origen(1)"°C"escala = "°C"
3es_temperatura_valida(-300, "°C") → -300 >= -273.15Falsenot False es True
4return f"{valor} {escala} está por debajo…"el avisola función termina

Y así se reparten el trabajo las funciones cuando eliges la opción 5 con 37 °C. Cada caja es una llamada y el valor coral, lo que devuelve:

Las llamadas de convertir(5, 37)convertir(5, 37) llama a escala_de_origen, que devuelve °C, a es_temperatura_valida, que devuelve True, y a conversion_completa, que a su vez llama a celsius_a_fahrenheit (98.6), fahrenheit_a_celsius (37.0) y celsius_a_kelvin (310.15).convertir(5, 37)escala_de_origen(5)→ "°C"es_temperatura_valida(37, "°C")→ Trueconversion_completa(37)→ "37 °C = 98.6 °F = …"celsius_a_fahrenheit(37)→ 98.6fahrenheit_a_celsius(98.6)→ 37.0celsius_a_kelvin(37)→ 310.15Cada función hace una sola cosa y devuelve su resultado; convertir solo las encadena.
Una opción del menú pone a trabajar a seis funciones; cada flecha coral es un return que vuelve hacia arriba.

Preguntas que te puedes hacer

¿Por qué las funciones de cálculo redondean con round(…, 2)? Porque los decimales en binario no son exactos (en Python, 0.1 + 0.2 da 0.30000000000000004) y una conversión puede arrastrar colas así de feas. Redondear en la función de cálculo hace que todas las conversiones salgan limpias sin acordarse de hacerlo fuera.

¿Por qué convertir devuelve el texto en vez de imprimirlo? Porque así sirve para más cosas: el programa principal lo imprime, pero podrías guardarlo en un archivo, enseñarlo en una ventana o comprobarlo en una prueba (convertir(1, 100) == "100 °C = 212.0 °F"). Una función que imprime solo sirve para imprimir.

¿Por qué escala_de_origen y es_temperatura_valida son funciones aparte? Porque responden preguntas que convertir no tiene por qué saber resolver. Si mañana añades la escala Rankine, cambias esas dos funciones y convertir apenas se toca.

¿Qué gana fahrenheit_a_kelvin llamando a otras dos? Que si alguna vez corriges la fórmula de °F a °C, la de °F a K se corrige sola. Con la fórmula copiada, tendrías dos sitios que arreglar y alguno se te olvidaría.

Comprobar la temperatura dentro de cada if de conversión. Acabas con la misma comprobación copiada seis veces. Compruébalo una vez, antes de convertir, como hace convertir.

Pruébalo tú

  • Añade la opción 7, de K a °F, sin escribir ninguna fórmula nueva: compón kelvin_a_celsius y celsius_a_fahrenheit.
  • Cambia conversion_completa para que también diga si la temperatura es de congelación (≤ 0 °C), normal o de ebullición (≥ 100 °C), con una función nueva que devuelva esa palabra.

Programa 2: mini calculadora modular

Una calculadora con seis operaciones (suma, resta, multiplicación, división, potencia y módulo). El enunciado de siempre, pero con una exigencia nueva: las funciones que calculan nunca imprimen y las que imprimen nunca calculan. Y tiene que aguantar dos casos incómodos: dividir (o hacer el módulo) entre cero y elegir una opción que no existe.

La idea

Tres capas, cada una con su responsabilidad:

  1. Calcular: sumar, restar, multiplicar, dividir, potencia y modulo. Reciben dos números y devuelven el resultado. Si no hay resultado posible (dividir entre cero), devuelven None.
  2. Mostrar: mostrar_menu, simbolo y mostrar_resultado. Son las únicas que saben cómo se ve la calculadora.
  3. Coordinar: calcular elige la función de cálculo según la opción y ejecutar junta una operación completa: comprueba, calcula y muestra.

Antes de programar, piensa en qué devuelve una función cuando «no puede». Aquí, None: no es un número, así que nadie lo confundirá con un resultado válido.

El código

# Programa 2 · Mini calculadora modular
# --- Funciones de cálculo: solo calculan y devuelven; nunca hacen print ---
def sumar(a, b):
    return a + b


def restar(a, b):
    return a - b


def multiplicar(a, b):
    return a * b


def dividir(a, b):
    if b == 0:
        return None          # None = "no hay resultado": quien llame decide qué decir
    return round(a / b, 4)


def potencia(a, b):
    return a ** b


def modulo(a, b):
    if b == 0:
        return None
    return a % b


# --- Funciones de interfaz: solo se ocupan de lo que se ve ---
def mostrar_menu():
    print("=== CALCULADORA MODULAR ===")
    print("1. Suma  2. Resta  3. Multiplicar  4. Dividir  5. Potencia  6. Módulo")


def simbolo(opcion):
    if opcion == 1:
        return "+"
    if opcion == 2:
        return "-"
    if opcion == 3:
        return "×"
    if opcion == 4:
        return "/"
    if opcion == 5:
        return "^"
    return "%"


def mostrar_resultado(a, signo, b, resultado):
    if resultado is None:
        print(f"{a} {signo} {b} → error: no se puede dividir entre cero")
    else:
        print(f"{a} {signo} {b} = {resultado}")


# --- Funciones que coordinan ---
def calcular(opcion, a, b):
    """Elige la función de cálculo según la opción y DEVUELVE su resultado."""
    if opcion == 1:
        return sumar(a, b)
    if opcion == 2:
        return restar(a, b)
    if opcion == 3:
        return multiplicar(a, b)
    if opcion == 4:
        return dividir(a, b)
    if opcion == 5:
        return potencia(a, b)
    return modulo(a, b)


def ejecutar(opcion, a, b):
    """Una operación completa: comprueba, calcula con una función y muestra con otra."""
    if opcion < 1 or opcion > 6:
        print(f"Opción {opcion} no válida")
        return                                   # return sin valor: sale ya de la función
    resultado = calcular(opcion, a, b)
    mostrar_resultado(a, simbolo(opcion), b, resultado)


# --- Programa principal: las operaciones que antes se elegían en el menú ---
mostrar_menu()
ejecutar(1, 7, 2)
ejecutar(4, 7, 2)
ejecutar(4, 7, 0)
ejecutar(5, 2, 10)
ejecutar(6, 17, 5)
ejecutar(8, 1, 1)

Salida

=== CALCULADORA MODULAR ===
1. Suma  2. Resta  3. Multiplicar  4. Dividir  5. Potencia  6. Módulo
7 + 2 = 9
7 / 2 = 3.5
7 / 0 → error: no se puede dividir entre cero
2 ^ 10 = 1024
17 % 5 = 2
Opción 8 no válida

Comprobamos: 7 + 2 = 9; 7 / 2 = 3.5; 2¹⁰ = 1024; 17 = 3 × 5 + 2, así que 17 % 5 = 2. La división entre cero no rompe el programa: dividir devuelve None y mostrar_resultado lo traduce a un mensaje. La opción 8 se rechaza antes de calcular nada.

Zoom: dividir y ejecutar

La pieza que hace que todo funcione es este contrato de dividir:

def dividir(a, b):
    if b == 0:
        return None          # None = "no hay resultado": quien llame decide qué decir
    return round(a / b, 4)

dividir no decide qué decirle al usuario: solo avisa de que no hay resultado devolviendo None. Y quien decide qué texto enseñar es mostrar_resultado, que pregunta if resultado is None. Se usa is None (y no == None) porque None es un valor único y is comprueba justo eso: si es ese objeto.

Y esta es la función que coordina:

def ejecutar(opcion, a, b):
    """Una operación completa: comprueba, calcula con una función y muestra con otra."""
    if opcion < 1 or opcion > 6:
        print(f"Opción {opcion} no válida")
        return                                   # return sin valor: sale ya de la función
    resultado = calcular(opcion, a, b)
    mostrar_resultado(a, simbolo(opcion), b, resultado)

Fíjate en el return solo, sin valor: sirve para salir de la función en cuanto la opción no vale, sin meter el resto del código dentro de un else. Devuelve None, pero aquí nadie usa lo que devuelve ejecutar, así que da igual.

Traza de ejecutar(4, 7, 0):

PasoQuiénQué haceDevuelve
1ejecutar4 < 1 or 4 > 6 es False: sigue—
2calcular(4, 7, 0)la opción 4 es dividir: llama a dividir(7, 0)—
3dividir(7, 0)b == 0None
4calculardevuelve lo que le dio dividirNone
5simbolo(4)—"/"
6mostrar_resultado(7, "/", 0, None)resultado is None: imprime el errorNone

El dibujo enseña el patrón con esa misma llamada: arriba coordina, a la izquierda se calcula (y se devuelve) y a la derecha se muestra (y se imprime):

El patrón de la calculadora: coordinar, calcular y mostrarejecutar(4, 7, 0) llama a calcular, que llama a dividir(7, 0) y devuelve None. Después llama a simbolo(4), que devuelve la barra, y a mostrar_resultado, que es la única que imprime: 7 / 0 → error: no se puede dividir entre cero.Coordina · ejecutar(4, 7, 0)Calcula · devuelve, no imprimecalcular(4, 7, 0)→ dividir(7, 0)→ return NoneMuestra · imprime, no calculasimbolo(4) → "/"mostrar_resultado(7, "/", 0, None)1. pide el resultado2. se lo pasa7 / 0 → error: no se puede dividir entre cero
Las funciones de cálculo devuelven (aquí None, «no hay resultado»); solo las de presentación tocan la pantalla.

Preguntas que te puedes hacer

¿Por qué tantas funciones para algo tan pequeño? Porque cada una se puede probar sola. dividir(7, 0) tiene que devolver None y dividir(7, 2) tiene que devolver 3.5: lo compruebas en dos líneas, sin menú, sin teclado y sin leer la pantalla. Es justo lo que no podía hacer con mi primera versión.

¿Por qué simbolo es una función y no un if dentro de mostrar_resultado? Porque el símbolo es un dato de presentación que también podría usar el menú o un historial. Además, mostrar_resultado queda corta y fácil de leer.

¿No sería más fácil que dividir imprimiera el error ella misma? Más fácil sí, mejor no. Si dividir imprime, ya no puedes usarla en un programa con ventanas o en una prueba sin que te ensucie la pantalla. Además, ¿qué devolvería? Tendrías que devolver algo de todas formas.

¿Y si la potencia es enorme? Python trabaja con enteros de cualquier tamaño: 2 ** 1000 funciona. Con decimales (2.0 ** 10000) sí obtendrías un OverflowError. Para FP1 basta con saber que existe.

Devolver un texto de error desde una función de cálculo (return "Error"). Quien la llama tiene que comparar textos para saber si salió bien, y si alguna vez suma ese «resultado», tendrás un TypeError lejísimos de su causa. Devuelve None (o, cuando lleguemos a ellas, lanza una excepción).

Pruébalo tú

  • Añade la operación 7, división entera (//), con su propia función que devuelva None si el divisor es 0. ¿Cuántas funciones has tenido que tocar?
  • Haz que mostrar_resultado redondee a 2 decimales los resultados que no sean enteros, sin tocar ninguna función de cálculo.

Programa 3: piedra, papel o tijera con funciones

El juego contra la máquina. El jugador escribe su jugada como le da la gana (Piedra, " TIJERA ") y a veces con erratas (tijeras). Hay que limpiar lo que escribe, rechazar lo que no sea una jugada, decidir quién gana cada ronda y llevar el marcador. La máquina elige al azar.

La idea

Una función por responsabilidad, y una función principal, jugar, que solo coordina:

  1. limpiar_jugada(texto): pasa a minúsculas, quita espacios y devuelve la jugada o None.
  2. jugada_maquina(): devuelve una jugada al azar.
  3. determinar_ganador(jugador, maquina): devuelve "empate", "jugador" o "maquina". No imprime.
  4. mostrar_ronda y mostrar_final: las únicas que imprimen.

Para decidir quién gana usamos un diccionario, GANA_A, que dice a qué jugada le gana cada una. Los diccionarios los verás a fondo en contenedores asociativos; aquí basta con saber que GANA_A["piedra"] vale "tijera".

Antes de programar, piensa en qué partes del juego no dependen del azar. Esas (limpiar, decidir el ganador) son las que más fácil se prueban.

El código

# Programa 3 · Piedra, papel o tijera con funciones
import random

random.seed(7)    # semilla fija: la máquina juega siempre lo mismo (quítala para jugar de verdad)

OPCIONES = ("piedra", "papel", "tijera")
GANA_A = {"piedra": "tijera", "papel": "piedra", "tijera": "papel"}    # cada jugada → a cuál le gana
EMOJI = {"piedra": "🪨", "papel": "📄", "tijera": "✂️"}


def jugada_maquina():
    return random.choice(OPCIONES)


def limpiar_jugada(texto):
    """Pasa a minúsculas y quita espacios. Devuelve la jugada, o None si no es válida."""
    jugada = texto.lower().strip()
    if jugada in OPCIONES:
        return jugada
    return None


def determinar_ganador(jugador, maquina):
    """Devuelve "empate", "jugador" o "maquina". No imprime nada."""
    if jugador == maquina:
        return "empate"
    if GANA_A[jugador] == maquina:
        return "jugador"
    return "maquina"


def mostrar_ronda(numero, jugador, maquina, ganador):
    print(f"Ronda {numero}: tú {EMOJI[jugador]} {jugador} · máquina {EMOJI[maquina]} {maquina}")
    if ganador == "empate":
        print("  → Empate")
    elif ganador == "jugador":
        print("  → ¡Ganas tú!")
    else:
        print("  → Gana la máquina")


def mostrar_final(victorias, derrotas, empates):
    print(f"Marcador: tú {victorias} · máquina {derrotas} · empates {empates}")
    if victorias > derrotas:
        print("¡Ganaste la partida! 🏆")
    elif derrotas > victorias:
        print("La máquina gana la partida 🤖")
    else:
        print("Empate perfecto 🤝")


def jugar(entradas):
    """Una ronda por cada entrada válida; lleva el marcador y lo enseña al final."""
    victorias = derrotas = empates = 0
    ronda = 0
    for texto in entradas:
        jugador = limpiar_jugada(texto)
        if jugador is None:
            print(f"«{texto}» no es una jugada válida: no cuenta")
            continue
        ronda += 1
        maquina = jugada_maquina()
        ganador = determinar_ganador(jugador, maquina)
        mostrar_ronda(ronda, jugador, maquina, ganador)
        if ganador == "jugador":
            victorias += 1
        elif ganador == "maquina":
            derrotas += 1
        else:
            empates += 1
    mostrar_final(victorias, derrotas, empates)


# Lo que "teclea" el jugador: con mayúsculas, espacios y una errata, como en la vida real
print("=== PIEDRA, PAPEL O TIJERA ===")
jugar(["Piedra", "tijeras", "papel", " TIJERA ", "tijera"])

Salida

=== PIEDRA, PAPEL O TIJERA ===
Ronda 1: tú 🪨 piedra · máquina 📄 papel
  → Gana la máquina
«tijeras» no es una jugada válida: no cuenta
Ronda 2: tú 📄 papel · máquina 🪨 piedra
  → ¡Ganas tú!
Ronda 3: tú ✂️ tijera · máquina 📄 papel
  → ¡Ganas tú!
Ronda 4: tú ✂️ tijera · máquina ✂️ tijera
  → Empate
Marcador: tú 2 · máquina 1 · empates 1
¡Ganaste la partida! 🏆

Comprobamos ronda a ronda: piedra contra papel, gana el papel (la máquina); "tijeras" no es ninguna de las tres jugadas y no cuenta (fíjate en que no gasta una jugada de la máquina, porque jugada_maquina() se llama después del continue); papel contra piedra, ganas tú; " TIJERA " se limpia a tijera y gana al papel; tijera contra tijera, empate. Marcador 2-1 con un empate.

La línea random.seed(7) fija la «semilla» del azar, así que la máquina juega siempre papel, piedra, papel y tijera, y tu salida será igual que la del artículo. Para jugar de verdad, bórrala.

Zoom: limpiar y decidir

def limpiar_jugada(texto):
    """Pasa a minúsculas y quita espacios. Devuelve la jugada, o None si no es válida."""
    jugada = texto.lower().strip()
    if jugada in OPCIONES:
        return jugada
    return None


def determinar_ganador(jugador, maquina):
    """Devuelve "empate", "jugador" o "maquina". No imprime nada."""
    if jugador == maquina:
        return "empate"
    if GANA_A[jugador] == maquina:
        return "jugador"
    return "maquina"

limpiar_jugada encadena lower() (todo a minúsculas) y strip() (quita espacios al principio y al final). Los métodos de los textos los verás en el tema de strings; aquí los usamos como herramienta. Lo importante es la forma: devuelve la jugada limpia o None, y quien la llama decide qué hacer con un None.

determinar_ganador resuelve el juego en tres líneas gracias al diccionario. El dibujo enseña el ciclo que guarda GANA_A:

Quién le gana a quién en GANA_AUn ciclo de tres jugadas: piedra le gana a tijera, tijera le gana a papel y papel le gana a piedra. A la izquierda, las tres entradas del diccionario GANA_A; a la derecha, la regla que usa determinar_ganador. piedra tijera papelle gana ale gana ale gana aGANA_A["piedra"] → "tijera"GANA_A["tijera"] → "papel"GANA_A["papel"] → "piedra"Si GANA_A[jugador] == maquina,ganas tú. Si son iguales, empate.Si no, gana la máquina.
Cada flecha es una entrada del diccionario: una sola consulta, GANA_A[jugador] == maquina, sustituye a seis comparaciones.
jugadormaquina¿Iguales?GANA_A[jugador]¿== maquina?Devuelve
"piedra""papel"no"tijera"no"maquina"
"papel""piedra"no"piedra"sí"jugador"
"tijera""papel"no"papel"sí"jugador"
"tijera""tijera"sí——"empate"

Preguntas que te puedes hacer

¿Por qué jugar recibe una lista de entradas en lugar de pedirlas con input()? Para que el programa se pueda ejecutar y comprobar siempre igual. Para jugar de verdad, cambiarías el bucle for texto in entradas por un input() dentro de un while, y ninguna otra función tendría que cambiar. Esa es la gracia de separarlo todo.

¿Por qué victorias, derrotas y empates son locales de jugar? Porque solo jugar los necesita mientras dura la partida. Si fueran globales, cualquier función podría tocarlos y una segunda partida empezaría con el marcador sucio.

¿Qué hace victorias = derrotas = empates = 0? Asigna 0 a las tres variables en una línea. Funciona porque los números no se pueden modificar «por dentro»; con listas no lo hagas, que acabarían siendo la misma lista.

¿Por qué continue en una jugada no válida? Porque esa entrada no debe contar como ronda: continue salta a la siguiente vuelta del for sin pasar por el resto. Por eso las rondas van numeradas 1, 2, 3 y 4 aunque haya cinco entradas.

Llamar a jugada_maquina() antes de comprobar si la jugada del jugador es válida. La máquina «gasta» jugadas en rondas que no existen y, con la semilla fija, todas las rondas siguientes cambian.

Pruébalo tú

  • Acepta abreviaturas: que limpiar_jugada convierta "p", "pa" y "t" en "piedra", "papel" y "tijera", sin tocar ninguna otra función.
  • Añade lagarto y Spock (cada jugada gana a dos y pierde contra dos). Pista: el valor de GANA_A puede ser una lista de dos jugadas y la comprobación pasa a ser maquina in GANA_A[jugador].

El patrón que debes llevarte

Los tres programas tienen la misma forma, y es la que vas a usar en todas las prácticas de GCID a partir de ahora:

Tipo de funciónQué haceQué nunca haceEn los programas
De cálculorecibe datos y devuelve un resultado (o None)imprimir o pedir datoscelsius_a_fahrenheit, dividir, determinar_ganador
De comprobaciónresponde una pregunta con True/False o un valorcambiar nadaes_temperatura_valida, limpiar_jugada
De presentaciónrecibe resultados y los enseñacalcularmostrar_resultado, mostrar_ronda
Coordinadorallama a las demás en ordencontener fórmulasconvertir, ejecutar, jugar

Cuando tengas dudas sobre dónde va una línea, pregúntate: ¿esto calcula o esto enseña? Si hace las dos cosas, parte la función en dos.

Errores típicos en esta práctica

  • Funciones de cálculo que imprimen. Funcionan una vez y estorban siempre: no se pueden probar ni reutilizar.
  • Usar el resultado de una función que solo imprime. La variable vale None y el error salta en otra línea.
  • Copiar una fórmula en lugar de componer funciones. Al corregir una, la copia se queda mal.
  • Devolver textos de error desde el cálculo. Mejor None, y que la presentación decida el mensaje.
  • Comprobar con == None. Funciona casi siempre, pero lo correcto es is None.
  • Marcadores o contadores globales. Mételos en la función que los usa y pásalos o devuélvelos cuando haga falta.

Míralo paso a paso en Python Tutor

Python Tutor dibuja cada llamada como una caja con sus variables. Elige Python 3 y pega esta versión mínima del juego, con dos rondas fijas:

GANA_A = {"piedra": "tijera", "papel": "piedra", "tijera": "papel"}


def determinar_ganador(jugador, maquina):
    if jugador == maquina:
        return "empate"
    if GANA_A[jugador] == maquina:
        return "jugador"
    return "maquina"


def sumar_punto(ganador, victorias):
    if ganador == "jugador":
        victorias = victorias + 1
    return victorias


victorias = 0
g = determinar_ganador("papel", "piedra")
victorias = sumar_punto(g, victorias)
g = determinar_ganador("piedra", "papel")
victorias = sumar_punto(g, victorias)
print(g, victorias)

Salida esperada:

maquina 1

Qué observar paso a paso:

  1. Al llamar a determinar_ganador("papel", "piedra") se abre una caja con jugador y maquina. Mira cómo, en la tercera línea de la función, el Return value es "jugador" y la caja se cierra.
  2. En sumar_punto(g, victorias) hay dos variables llamadas victorias: la del Global frame (0) y la local de la caja. Cuando la local pasa a 1, la global sigue en 0 hasta que el return y la asignación victorias = sumar_punto(...) la actualizan.
  3. En la segunda ronda, GANA_A["piedra"] vale "tijera", que no es "papel": la función sale por el último return con "maquina", y sumar_punto devuelve la victorias que recibió, sin cambiarla.
  4. Fíjate en que GANA_A está en el Global frame y las funciones la leen sin pasarla como parámetro: es una constante, nadie la cambia.

Resumen y siguiente paso

  • Las funciones de cálculo reciben, calculan y devuelven: ni input() ni print.
  • Componer funciones (celsius_a_kelvin(fahrenheit_a_celsius(f))) evita copiar fórmulas.
  • Cuando no hay resultado posible, se devuelve None y se comprueba con is None.
  • Un return dentro de un if corta la función: cada rama con su return no necesita elif.
  • Una función coordinadora llama a las demás en orden y es la única que conoce el flujo completo.
  • Si los datos llegan como parámetros, el programa se puede probar siempre igual.

Checklist: ¿lo has practicado?

  • ¿Has ejecutado los tres programas y comprobado las salidas?
  • ¿Sabrías decir qué funciones de los tres programas imprimen y cuáles devuelven?
  • ¿Puedes explicar por qué fahrenheit_a_kelvin no tiene fórmula propia?
  • ¿Entiendes por qué dividir devuelve None y quién decide el mensaje de error?
  • ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
  • ¿Podrías cambiar la entrada del juego a input() sin tocar determinar_ganador?

Si has respondido que sí a casi todo, estás listo para los ejercicios de funciones en Python, donde ya no te doy el código hecho. ¡Nos vemos allí!

Siguiente paso: ejerciciosFunciones en Python — ejercicios con notas, un mini banco y dados con estadísticas (y el return que se rindió en la primera tirada)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *