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

Preparando el juego de dados de este artículo, quise añadir un aviso al final: «¿ha salido algún doble en la partida?». Escribí una función hay_doble en un minuto, la probé con la partida de ejemplo… y me dijo False. Con un 6 y un 6 en la tercera tirada, delante de mis ojos. Revisé la condición, revisé los datos, añadí prints… y el fallo era una sola palabra mal colocada: un return False dentro de un else, que hacía que la función se rindiera en la primera tirada que no era doble. Lo tienes tal cual en el apartado «¿Qué falla aquí?», porque es de esos errores que no dan ningún mensaje.

Ya viste la teoría de funciones en Python y los tres programas de la práctica. Ahora te toca a ti: cuatro ejercicios en tres niveles (un conversor de notas, un informe, un mini banco y un juego de dados con estadísticas), tres fragmentos con un fallo escondido y, al final, un chuletario para tenerlo todo a mano. El hilo que los une es el error más silencioso del tema: print donde hacía falta return, que te deja trabajando con None sin darte cuenta.

Cómo usar este artículo

  1. Lee el enunciado y la salida esperada. Tu programa tiene que dar exactamente esa salida (en los dos con azar, gracias a una semilla fija).
  2. Intenta resolverlo en VS Code o en Thonny sin mirar nada más. Si llevas más de 10 minutos atascado, lee la pista.
  3. Antes de ver las soluciones, haz el apartado «¿Qué falla aquí?»: predice qué sale y comprueba.
  4. Compara con la solución comentada. No basta con que funcione: mira por qué está hecha así y qué error daría la alternativa obvia.

Ninguno usa input(): los datos van escritos en el programa, como en la práctica. Así puedes comparar tu salida con la del artículo línea a línea.

Nivel básico

Ejercicio 1: conversor de notas entre sistemas

Escribe cuatro funciones que conviertan notas entre tres sistemas: el español (0-10), el americano (0-100) y el de letras (A, B, C, D, F):

  • espanola_a_americana(nota): multiplica por 10.
  • americana_a_espanola(nota): divide entre 10.
  • espanola_a_letra(nota): de 9 a 10, A; de 7 a menos de 9, B; de 5 a menos de 7, C; de 3 a menos de 5, D; por debajo de 3, F.
  • letra_a_espanola(letra): A = 9.5, B = 7.5, C = 5.5, D = 3.5, F = 1.5 (el centro de cada tramo), en mayúsculas o minúsculas, y None si la letra no existe.

Una quinta función, mostrar_conversiones(nota), enseña las tres versiones de una nota. El programa comprueba que la nota está entre 0 y 10, la muestra y termina con dos conversiones de vuelta. Con nota = 7.5, la salida debe ser:

=== CONVERSOR DE NOTAS ===
--- Conversiones ---
Sistema español: 7.5
Sistema americano: 75.0
Sistema de letras: B
De vuelta: 75 → 7.5 · b → 7.5

Cada función de conversión devuelve su resultado con return; solo mostrar_conversiones usa print. Si una conversión hiciera print, americana = espanola_a_americana(7.5) valdría None aunque vieras el 75.0 en la pantalla. Redondea a un decimal con round(…, 1).

Ejercicio 2: generador de informe de notas

Con dos funciones del ejercicio anterior, escribe un programa que reciba las notas de cuatro asignaturas como una lista de parejas (nombre, nota) y genere un informe: cada asignatura en los tres sistemas, la media con su letra y la nota más alta y la más baja con su asignatura.

asignaturas = [("FP1", 8.5), ("Estadística", 7.0), ("Matemáticas", 6.5), ("Bases de datos", 9.0)]

Salida esperada:

=== INFORME DE NOTAS ===
--- Informe completo ---
FP1 → 8.5 | 85.0 | B
Estadística → 7.0 | 70.0 | B
Matemáticas → 6.5 | 65.0 | C
Bases de datos → 9.0 | 90.0 | A
Media: 7.75 → B
Más alta: 9.0 (Bases de datos)
Más baja: 6.5 (Matemáticas)

Crea fila_informe(nombre, nota) que devuelva la línea de una asignatura (no que la imprima) y generar_informe(asignaturas) que la use dentro de un for nombre, nota in asignaturas:. Para la más alta y la más baja, empieza con un valor imposible (−1 y 11) y ve actualizándolo. Ojo: el FP1, con un 8.5, es una B, no una A.

Nivel intermedio

Ejercicio 3: mini banco con funciones

Simula una cuenta bancaria con funciones: consultar_saldo(saldo), depositar(saldo, cantidad), retirar(saldo, cantidad) y mostrar_historial(historial, saldo). La cuenta empieza con 1000.0 € y se hacen, por este orden: un depósito de 500, una retirada de 200, una retirada de 2000 (no hay dinero suficiente) y un depósito de −50 (cantidad no válida). El historial solo apunta los movimientos que salieron bien.

Salida esperada:

=== BANCO SERGIO LEARNS ===
Saldo actual: 1000.0 €
✓ Depósito realizado: +500 €
✓ Retirada realizada: -200 €
Error: saldo insuficiente (tienes 1300.0 €)
Error: la cantidad debe ser positiva
--- Historial de movimientos ---
Saldo inicial: +1000.0 €
Depósito: +500 €
Retirada: -200 €
──────────────────────────────
Saldo actual: 1300.0 €

depositar y retirar reciben el saldo y devuelven el saldo nuevo; si algo falla, devuelven el mismo que recibieron. Ese es el return clave: si lo olvidas, saldo = depositar(saldo, 500) deja el saldo en None. Para saber si hay que apuntar el movimiento, compara el saldo de antes con el de después.

Desafío final

Ejercicio 4: juego de dados con estadísticas

Simula seis lanzamientos de dos dados. Cada lanzamiento se muestra con su suma y un aviso si es doble. Al final, las estadísticas: total de lanzamientos, suma total, media, máximo y mínimo (con el lanzamiento en el que salieron por primera vez), número de dobles con su porcentaje y una distribución con barras de █, de la suma más baja a la más alta, que diga «1 vez» o «2 veces» según toque. Pon random.seed(37) al principio para que tus dados salgan igual que estos:

=== JUEGO DE DADOS ===
Lanzamiento 1: 🎲 6 + 🎲 5 = 11
Lanzamiento 2: 🎲 1 + 🎲 5 = 6
Lanzamiento 3: 🎲 6 + 🎲 6 = 12 ← ¡Doble!
Lanzamiento 4: 🎲 6 + 🎲 5 = 11
Lanzamiento 5: 🎲 1 + 🎲 6 = 7
Lanzamiento 6: 🎲 3 + 🎲 4 = 7
--- Estadísticas ---
Total lanzamientos: 6
Suma total: 54
Media por lanzamiento: 9.0
Máximo obtenido: 12 (lanzamiento 3)
Mínimo obtenido: 6 (lanzamiento 2)
Dobles conseguidos: 1 (16.7 %)
Distribución de resultados:
   6: █ (1 vez)
   7: ██ (2 veces)
  11: ██ (2 veces)
  12: █ (1 vez)

lanzar_dado() devuelve random.randint(1, 6) y lanzar_dos_dados() devuelve dos valores (return lanzar_dado(), lanzar_dado()), que se recogen con d1, d2 = lanzar_dos_dados(). Guarda las sumas en una lista con append, busca el máximo y el mínimo con funciones que devuelvan valor y posición, y para la distribución recorre range(2, 13) contando con sumas.count(valor). El plural también puede ser una función.

¿Qué falla aquí?

Tres fragmentos que parecen correctos. Para cada uno, predice qué sale antes de leer la explicación.

Fragmento 1: la nota que se ve pero no está

def espanola_a_americana(nota):
    print(nota * 10)                # enseña 75.0... y devuelve None


americana = espanola_a_americana(7.5)
print("Con 5 puntos extra:", americana + 5)

¿Qué crees que sale?

Sale esto:

75.0
Traceback (most recent call last):
  File "print_return.py", line 6, in <module>
    print("Con 5 puntos extra:", americana + 5)
                                 ~~~~~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

El 75.0 aparece, así que la conversión «funciona». Pero espanola_a_americana no tiene return, así que devuelve None, y None + 5 es imposible: unsupported operand type(s) for +: 'NoneType' and 'int'. Cuando veas NoneType en un error, lo primero que tienes que pensar es «alguna función no tiene return».

Confiar en la pantalla. Que el número salga no significa que la función lo devuelva: print lo manda a la pantalla y a la variable le llega None.

Arreglado, con return:

def espanola_a_americana(nota):
    return nota * 10                # devuelve el número a quien llama


americana = espanola_a_americana(7.5)
print("Con 5 puntos extra:", americana + 5)
Con 5 puntos extra: 80.0

7.5 × 10 = 75.0 y 75.0 + 5 = 80.0. Fíjate en que ahora el 75.0 no sale suelto: nadie lo imprime, solo se usa.

Las funciones de conversión casi nunca deberían imprimir. Si quieres ver el valor mientras programas, imprímelo fuera: print(espanola_a_americana(7.5)).

Fragmento 2: el saldo que nadie encuentra

saldo = 1000.0


def depositar(cantidad):
    saldo = saldo + cantidad        # ¿la de fuera? Python no lo ve así
    return saldo


print(depositar(500))

¿Qué crees que sale? ¿1500.0?

Sale esto:

Traceback (most recent call last):
  File "saldo_global.py", line 9, in <module>
    print(depositar(500))
          ~~~~~~~~~^^^^^
  File "saldo_global.py", line 5, in depositar
    saldo = saldo + cantidad        # ¿la de fuera? Python no lo ve así
            ^^^^^
UnboundLocalError: cannot access local variable 'saldo' where it is not associated with a value

Como depositar asigna saldo = …, Python decide que saldo es una variable local de la función, en todo su cuerpo. Al calcular saldo + cantidad intenta leer esa local, que todavía no tiene valor: UnboundLocalError. La global de 1000.0 existe, pero la función ya no la mira.

Pensar que una función puede leer una variable de fuera y cambiarla a la vez sin decir nada. Para leerla, sí; para asignarla, Python la convierte en local.

El arreglo no es global saldo, sino hacer que el dato entre y salga a la vista:

def depositar(saldo, cantidad):
    return saldo + cantidad         # entra por parámetro, sale por return


saldo = 1000.0
saldo = depositar(saldo, 500)       # y se guarda fuera, a la vista
print(saldo)
1500.0

Así es exactamente como funciona el mini banco del ejercicio 3: el saldo entra por parámetro y sale por return.

Fragmento 3: el return que se rindió

El de la anécdota. La partida tiene un doble en la tercera tirada:

def hay_doble(tiradas):
    """¿Ha salido algún doble en la partida?"""
    for d1, d2 in tiradas:
        if d1 == d2:
            return True
        else:
            return False            # ← ojo con este return


tiradas = [(6, 5), (1, 5), (6, 6)]
print(hay_doble(tiradas))

¿Qué crees que sale?

Sale esto:

False

En la primera vuelta, la tirada es (6, 5): no es doble, así que entra en el else y hace return False. Y return no termina la vuelta, termina la función. Las tiradas (1, 5) y (6, 6) no se miran nunca. El dibujo compara las dos versiones con la misma partida:

El return que sale en la primera vueltaLas tiradas son (6, 5), (1, 5) y (6, 6). La versión con else devuelve False en la primera tirada y nunca mira las otras dos. La versión arreglada sigue en las dos primeras y devuelve True en la tercera.hay_doble (con else)(6, 5)return False(1, 5)nunca se mira(6, 6)nunca se miraFalsehay_doble (arreglada)(6, 5)no es doble: sigue(1, 5)no es doble: sigue(6, 6)return TrueTruedespués del for: return False
Un return dentro del bucle termina la función entera: el False solo puede ir después del for, cuando ya se han mirado todas las tiradas.

Escribir el «no lo he encontrado» dentro del bucle. Para decir que no hay ningún doble tienes que haber mirado todas las tiradas, y eso solo se sabe cuando el bucle termina.

Arreglado: el return False sale del bucle y del else:

def hay_doble(tiradas):
    """¿Ha salido algún doble en la partida?"""
    for d1, d2 in tiradas:
        if d1 == d2:
            return True             # en cuanto aparece uno, ya sabemos la respuesta
    return False                    # solo después de mirar TODAS las tiradas


tiradas = [(6, 5), (1, 5), (6, 6)]
print(hay_doble(tiradas))
True

En las búsquedas, el patrón es siempre el mismo: return en cuanto encuentras lo que buscas, dentro del bucle; y el return de «no está», después del bucle. Es la misma forma que la búsqueda de las bandejas de la teoría.

Soluciones comentadas

Solución del ejercicio 1

# Nivel básico · Ejercicio 1: conversor de notas entre sistemas
def espanola_a_americana(nota):
    """0-10 → 0-100."""
    return round(nota * 10, 1)


def americana_a_espanola(nota):
    """0-100 → 0-10."""
    return round(nota / 10, 1)


def espanola_a_letra(nota):
    """9-10 A · 7-8,9 B · 5-6,9 C · 3-4,9 D · menos de 3 F."""
    if nota >= 9:
        return "A"
    elif nota >= 7:
        return "B"
    elif nota >= 5:
        return "C"
    elif nota >= 3:
        return "D"
    return "F"


def letra_a_espanola(letra):
    """Cada letra vale el centro de su tramo; None si la letra no existe."""
    letra = letra.upper()
    if letra == "A":
        return 9.5
    if letra == "B":
        return 7.5
    if letra == "C":
        return 5.5
    if letra == "D":
        return 3.5
    if letra == "F":
        return 1.5
    return None


def mostrar_conversiones(nota):
    """La única que imprime: recibe la nota y enseña las tres versiones."""
    print("--- Conversiones ---")
    print("Sistema español:", nota)
    print("Sistema americano:", espanola_a_americana(nota))
    print("Sistema de letras:", espanola_a_letra(nota))


print("=== CONVERSOR DE NOTAS ===")
nota = 7.5                             # la nota que antes se pedía con input()
if nota < 0 or nota > 10:
    print("Error: la nota debe estar entre 0 y 10")
else:
    mostrar_conversiones(nota)
print("De vuelta: 75 →", americana_a_espanola(75), "· b →", letra_a_espanola("b"))

Salida:

=== CONVERSOR DE NOTAS ===
--- Conversiones ---
Sistema español: 7.5
Sistema americano: 75.0
Sistema de letras: B
De vuelta: 75 → 7.5 · b → 7.5

Por qué está hecho así:

  • Cuatro funciones que devuelven y una que imprime. Las conversiones se pueden reutilizar en el ejercicio 2 justo porque no imprimen nada.
  • Los tramos con >= en orden descendente. espanola_a_letra(7.5): 7.5 >= 9 es falso, 7.5 >= 7 es verdadero → "B". Como los elif se miran de arriba abajo, cada uno solo necesita el límite inferior. Si los escribieras de abajo arriba (nota >= 3 primero), un 9.5 saldría como D.
  • letra.upper() antes de comparar, para que "b" y "B" valgan lo mismo; si la letra no existe, se llega al return None del final.
  • Nombres sin ñ. espanola y no española: Python 3 acepta la ñ en los nombres, pero es fácil que un compañero no pueda escribirla en su teclado o que alguna herramienta se queje. Es una costumbre sana.
  • La comprobación del rango, fuera de las conversiones. Las conversiones no deciden qué hacer con una nota imposible; lo decide el programa principal.

Cuentas: 7.5 × 10 = 75.0; 75 / 10 = 7.5; "b" → "B" → 7.5.

Solución del ejercicio 2

# Nivel básico · Ejercicio 2: informe de notas (reutiliza dos funciones del ejercicio 1)
def espanola_a_americana(nota):
    return round(nota * 10, 1)


def espanola_a_letra(nota):
    if nota >= 9:
        return "A"
    elif nota >= 7:
        return "B"
    elif nota >= 5:
        return "C"
    elif nota >= 3:
        return "D"
    return "F"


def fila_informe(nombre, nota):
    """DEVUELVE la línea de una asignatura (no la imprime: así se puede reutilizar)."""
    return f"{nombre} → {nota} | {espanola_a_americana(nota)} | {espanola_a_letra(nota)}"


def generar_informe(asignaturas):
    print("--- Informe completo ---")
    suma = 0
    cuantas = 0
    nota_max, nombre_max = -1, ""     # empezamos por debajo de cualquier nota posible...
    nota_min, nombre_min = 11, ""     # ...y por encima, para que la primera ya gane
    for nombre, nota in asignaturas:
        print(fila_informe(nombre, nota))
        suma += nota
        cuantas += 1
        if nota > nota_max:           # > y no >=: con un empate se queda la primera
            nota_max, nombre_max = nota, nombre
        if nota < nota_min:
            nota_min, nombre_min = nota, nombre
    media = round(suma / cuantas, 2)
    print("Media:", media, "→", espanola_a_letra(media))
    print(f"Más alta: {nota_max} ({nombre_max})")
    print(f"Más baja: {nota_min} ({nombre_min})")


print("=== INFORME DE NOTAS ===")
generar_informe([("FP1", 8.5), ("Estadística", 7.0), ("Matemáticas", 6.5), ("Bases de datos", 9.0)])

Salida:

=== INFORME DE NOTAS ===
--- Informe completo ---
FP1 → 8.5 | 85.0 | B
Estadística → 7.0 | 70.0 | B
Matemáticas → 6.5 | 65.0 | C
Bases de datos → 9.0 | 90.0 | A
Media: 7.75 → B
Más alta: 9.0 (Bases de datos)
Más baja: 6.5 (Matemáticas)

Por qué está hecho así:

  • fila_informe devuelve el texto. generar_informe lo imprime, pero la misma función serviría para escribir el informe en un archivo.
  • Las dos funciones del ejercicio 1, copiadas. Aquí se repiten porque cada ejercicio es un archivo independiente. Copiar funciones entre archivos es justo lo que resuelve el siguiente tema, los módulos en Python: escribirías from conversor_notas import espanola_a_letra y listo.
  • Valores imposibles para empezar. nota_max = -1 garantiza que la primera nota (cualquiera entre 0 y 10) ya sea mayor; nota_min = 11, que sea menor.
  • > y no >=. Con un empate, se queda la primera asignatura que alcanzó esa nota.
  • for nombre, nota in asignaturas desempaqueta cada pareja en dos variables, igual que euros, cents = … en la teoría.

Traza del máximo y el mínimo:

VueltaAsignaturanotasumanota_max (nombre_max)nota_min (nombre_min)
inicio——0−111
1FP18.58.58.5 (FP1)8.5 (FP1)
2Estadística7.015.58.5 (FP1)7.0 (Estadística)
3Matemáticas6.522.08.5 (FP1)6.5 (Matemáticas)
4Bases de datos9.031.09.0 (Bases de datos)6.5 (Matemáticas)

Media: 31.0 / 4 = 7.75, que está entre 7 y 9 → B.

Solución del ejercicio 3

# Nivel intermedio · Ejercicio 3: mini banco con funciones
def consultar_saldo(saldo):
    print(f"Saldo actual: {saldo} €")


def depositar(saldo, cantidad):
    """Devuelve el saldo NUEVO. Si la cantidad no vale, devuelve el mismo saldo."""
    if cantidad <= 0:
        print("Error: la cantidad debe ser positiva")
        return saldo
    print(f"✓ Depósito realizado: +{cantidad} €")
    return round(saldo + cantidad, 2)      # el return clave: sin él, el saldo nunca cambia


def retirar(saldo, cantidad):
    """Devuelve el saldo nuevo, o el mismo si no hay dinero suficiente."""
    if cantidad <= 0:
        print("Error: la cantidad debe ser positiva")
        return saldo
    if cantidad > saldo:
        print(f"Error: saldo insuficiente (tienes {saldo} €)")
        return saldo
    print(f"✓ Retirada realizada: -{cantidad} €")
    return round(saldo - cantidad, 2)


def anotar(historial, texto, saldo_antes, saldo_despues):
    """Apunta el movimiento solo si el saldo ha cambiado (si hubo error, no cambia)."""
    if saldo_despues != saldo_antes:
        historial.append(texto)


def mostrar_historial(historial, saldo):
    print("--- Historial de movimientos ---")
    for movimiento in historial:
        print(movimiento)
    print("─" * 30)
    consultar_saldo(saldo)


print("=== BANCO SERGIO LEARNS ===")
saldo = 1000.0
historial = [f"Saldo inicial: +{saldo} €"]
consultar_saldo(saldo)

antes = saldo
saldo = depositar(saldo, 500)               # se guarda lo que DEVUELVE la función
anotar(historial, "Depósito: +500 €", antes, saldo)

antes = saldo
saldo = retirar(saldo, 200)
anotar(historial, "Retirada: -200 €", antes, saldo)

antes = saldo
saldo = retirar(saldo, 2000)                # no hay tanto: el saldo no cambia
anotar(historial, "Retirada: -2000 €", antes, saldo)

antes = saldo
saldo = depositar(saldo, -50)               # cantidad no válida
anotar(historial, "Depósito: -50 €", antes, saldo)

mostrar_historial(historial, saldo)

Salida:

=== BANCO SERGIO LEARNS ===
Saldo actual: 1000.0 €
✓ Depósito realizado: +500 €
✓ Retirada realizada: -200 €
Error: saldo insuficiente (tienes 1300.0 €)
Error: la cantidad debe ser positiva
--- Historial de movimientos ---
Saldo inicial: +1000.0 €
Depósito: +500 €
Retirada: -200 €
──────────────────────────────
Saldo actual: 1300.0 €

Por qué está hecho así:

  • El saldo nunca es global para las funciones. Entra por parámetro y sale por return, y el programa principal lo reasigna: saldo = depositar(saldo, 500). Es el arreglo del fragmento 2 aplicado a un programa completo.
  • Si algo falla, se devuelve el mismo saldo. Así quien llama no tiene que distinguir casos: siempre recibe un saldo válido.
  • anotar compara antes y después. Si el saldo no cambió, la operación falló y no se apunta. Con la retirada de 2000 y el depósito de −50, el historial se queda en tres líneas.
  • El historial es una lista que anotar modifica con append. Las listas se pueden cambiar «por dentro» desde una función sin devolverlas; los números no. Lo verás a fondo en el tema de mutabilidad; por ahora, fíjate en que el saldo (un número) necesita el return y el historial (una lista) no.
  • round(…, 2) en las operaciones, para que un saldo como 0.1 + 0.2 no acabe con colas de decimales.

El dibujo sigue el saldo de función en función. Cada píldora violeta es lo que sale de un return y entra en la siguiente llamada:

El saldo pasa de función en funciónEl saldo empieza en 1000.0. depositar(saldo, 500) devuelve 1500.0; retirar(saldo, 200) devuelve 1300.0; retirar(saldo, 2000) falla y devuelve 1300.0; depositar(saldo, -50) falla y devuelve 1300.0.saldo = depositar(saldo, 500) …1000.0depositar(saldo, 500)  devuelve el nuevo1500.0retirar(saldo, 200)  devuelve el nuevo1300.01300.0retirar(saldo, 2000)  error: devuelve el mismo1300.0depositar(saldo, -50)  error: devuelve el mismo1300.0
El saldo entra por parámetro y sale por return; si la operación falla, sale el mismo que entró y el historial no lo apunta.

Cuentas: 1000.0 + 500 = 1500.0; 1500.0 − 200 = 1300.0; 2000 > 1300.0, así que la retirada falla; −50 ≤ 0, así que el depósito falla. Saldo final: 1300.0 €.

Solución del ejercicio 4

# Desafío · Ejercicio 4: juego de dados con estadísticas
import random

random.seed(37)    # semilla fija para que la salida sea siempre la misma (quítala para jugar)


def lanzar_dado():
    return random.randint(1, 6)                # return: devuelve el número, no lo imprime


def lanzar_dos_dados():
    return lanzar_dado(), lanzar_dado()        # dos valores a la vez (una tupla)


def es_doble(dado1, dado2):
    return dado1 == dado2


def maximo_y_posicion(sumas):
    """Devuelve el valor más alto y en qué lanzamiento salió (el primero si se repite)."""
    mejor, posicion, i = 0, 0, 0
    for s in sumas:
        i += 1
        if s > mejor:
            mejor, posicion = s, i
    return mejor, posicion


def minimo_y_posicion(sumas):
    peor, posicion, i = 13, 0, 0               # 13: más que la suma máxima posible (6 + 6)
    for s in sumas:
        i += 1
        if s < peor:
            peor, posicion = s, i
    return peor, posicion


def veces(n):
    """«1 vez» o «2 veces»: el plural también es una decisión."""
    if n == 1:
        return "1 vez"
    return f"{n} veces"


def mostrar_distribucion(sumas):
    print("Distribución de resultados:")
    for valor in range(2, 13):                 # todas las sumas posibles, ya en orden
        n = sumas.count(valor)                 # cuántas veces salió ese valor
        if n > 0:
            print(f"  {valor:2d}: {'█' * n} ({veces(n)})")


def jugar(lanzamientos):
    print("=== JUEGO DE DADOS ===")
    sumas = []
    dobles = 0
    for i in range(1, lanzamientos + 1):
        d1, d2 = lanzar_dos_dados()            # desempaquetamos la tupla
        suma = d1 + d2
        aviso = ""
        if es_doble(d1, d2):
            aviso = " ← ¡Doble!"
            dobles += 1
        print(f"Lanzamiento {i}: 🎲 {d1} + 🎲 {d2} = {suma}{aviso}")
        sumas.append(suma)
    maximo, lanz_max = maximo_y_posicion(sumas)
    minimo, lanz_min = minimo_y_posicion(sumas)
    print("--- Estadísticas ---")
    print("Total lanzamientos:", lanzamientos)
    print("Suma total:", sum(sumas))
    print("Media por lanzamiento:", round(sum(sumas) / lanzamientos, 2))
    print(f"Máximo obtenido: {maximo} (lanzamiento {lanz_max})")
    print(f"Mínimo obtenido: {minimo} (lanzamiento {lanz_min})")
    print(f"Dobles conseguidos: {dobles} ({round(dobles / lanzamientos * 100, 1)} %)")
    mostrar_distribucion(sumas)


jugar(6)

Salida:

=== JUEGO DE DADOS ===
Lanzamiento 1: 🎲 6 + 🎲 5 = 11
Lanzamiento 2: 🎲 1 + 🎲 5 = 6
Lanzamiento 3: 🎲 6 + 🎲 6 = 12 ← ¡Doble!
Lanzamiento 4: 🎲 6 + 🎲 5 = 11
Lanzamiento 5: 🎲 1 + 🎲 6 = 7
Lanzamiento 6: 🎲 3 + 🎲 4 = 7
--- Estadísticas ---
Total lanzamientos: 6
Suma total: 54
Media por lanzamiento: 9.0
Máximo obtenido: 12 (lanzamiento 3)
Mínimo obtenido: 6 (lanzamiento 2)
Dobles conseguidos: 1 (16.7 %)
Distribución de resultados:
   6: █ (1 vez)
   7: ██ (2 veces)
  11: ██ (2 veces)
  12: █ (1 vez)

Por qué está hecho así:

  • lanzar_dado devuelve, no imprime. Si imprimiera, d1 + d2 sería None + None.
  • lanzar_dos_dados devuelve una tupla que se desempaqueta en d1, d2. Una función, dos valores.
  • maximo_y_posicion y minimo_y_posicion devuelven dos cosas: el valor y el lanzamiento. Con > (y <) se quedan con la primera vez: el 11 sale en los lanzamientos 1 y 4, pero el máximo es el 12 del 3; y el mínimo, el 6 del 2.
  • range(2, 13) para la distribución. Recorre todas las sumas posibles ya en orden, así que no hace falta ordenar nada; sumas.count(valor) dice cuántas veces salió y el if n > 0 se salta las que no salieron.
  • veces(n), una función diminuta que evita el «(2 vez)» de la versión antigua de este ejercicio.
  • random.seed(37) para que la salida sea siempre la misma. Bórrala para jugar de verdad.

Traza de las estadísticas:

LanzamientoDadosSuma¿Doble?Máximo hasta aquíMínimo hasta aquí
16 + 511no11 (1)11 (1)
21 + 56no11 (1)6 (2)
36 + 612sí12 (3)6 (2)
46 + 511no12 (3)6 (2)
51 + 67no12 (3)6 (2)
63 + 47no12 (3)6 (2)

Suma: 11 + 6 + 12 + 11 + 7 + 7 = 54; media 54 / 6 = 9.0; dobles 1 de 6 = 16.7 %.


Cuestionario tipo examen

Ahora ponte a prueba como en el examen. Los cuestionarios de FP1 son así: 15 preguntas en 20 minutos (16 en 30 en el de Secuencias), casi todas de respuesta única (cada fallo resta un tercio de lo que vale la pregunta) y muchas de «¿qué imprime este código?», con algún hueco, relacionar o verdadero o falso. Aquí tienes 13 preguntas originales sobre funciones. Contesta sin ejecutar nada, pulsa Comprobar y lee la explicación de cada una. Estas preguntas son también el banco del simulacro de teoría de FP1, que las mezclará con las de los demás temas.

Pregunta 1Respuesta corta

¿Qué imprime este programa? Escribe la salida exacta.

def doble_mas(x, y=1):
    return 2 * x + y

print(doble_mas(4) + doble_mas(3, 5))
Solución

Respuesta: 20

doble_mas(4) usa el valor por defecto y = 1: 2 × 4 + 1 = 9. doble_mas(3, 5) da 2 × 3 + 5 = 11. Total: 9 + 11 = 20.

Pregunta 2Única

¿Qué imprime este código?

def saludar(nombre):
    print("Hola", nombre)

r = saludar("Luis")
print(r)
  1. Hola Luis y después None
  2. Hola Luis dos veces
  3. Solo Hola Luis
  4. Un error
Solución

Respuesta: Hola Luis y después None

print muestra y return devuelve. saludar muestra el saludo pero no devuelve nada, así que r vale None. Confundirlos es el error más típico del tema.

Pregunta 3Relacionar

En def area(base, altura): … area(4, 3), relaciona cada término:

base y altura
4 y 3
area
area(4, 3)

Definiciones:

  • Llamada a la función
  • Parámetros formales
  • Argumentos (parámetros reales)
  • Valor por defecto
  • Nombre de la función
Solución
  • base y altura → Parámetros formales
  • 4 y 3 → Argumentos (parámetros reales)
  • area → Nombre de la función
  • area(4, 3) → Llamada a la función

Los parámetros son los nombres que aparecen en la definición; los argumentos, los valores concretos que pasas al llamarla. En la llamada, cada argumento se asigna a su parámetro por orden.

Pregunta 4Única

¿Cómo se llama pasar los argumentos así: area(altura=3, base=4)?

  1. Paso por nombre (argumentos con nombre o keyword arguments)
  2. Paso por posición
  3. Valores por defecto
  4. Recursividad
Solución

Respuesta: Paso por nombre (argumentos con nombre o keyword arguments)

Con nombre=valor asocias cada argumento a su parámetro explícitamente. Es útil cuando una función tiene muchos parámetros o algunos con valor por defecto.

Pregunta 5Respuesta corta

¿Qué imprime este programa? Escribe la salida exacta.

def f(n):
    if n > 10:
        return "grande"
    return "pequeño"
    print("fin")

print(f(15))
Solución

Respuesta: grande

return termina la función en ese mismo instante. Con 15 se cumple el if y devuelve grande. El print("fin") está después de un return, así que no se ejecuta nunca.

Pregunta 6Única

¿Qué imprime este código?

x = 5

def cambiar():
    x = 99
    return x

cambiar()
print(x)
  1. 5
  2. 99
  3. Un error, porque x ya existe
  4. None
Solución

Respuesta: 5

Cada llamada tiene su propio ámbito: x = 99 crea una x local que desaparece al acabar la función. La x global sigue en 5. Para sacar un valor de una función, devuélvelo con return.

Pregunta 7Verdadero o falso

Sobre las funciones:

  1. Una función puede devolver varios valores separados por comas: return a, b.
  2. Una función se ejecuta en cuanto Python lee su def.
  3. Una función puede llamar a otra función.
  4. Los parámetros con valor por defecto pueden ir antes de los que no lo tienen: def f(a=1, b):.
Solución
  • V — Una función puede devolver varios valores separados por comas: return a, b.
  • F — Una función se ejecuta en cuanto Python lee su def.
  • V — Una función puede llamar a otra función.
  • F — Los parámetros con valor por defecto pueden ir antes de los que no lo tienen: def f(a=1, b):.

return a, b devuelve una tupla. El def solo define: el cuerpo se ejecuta al llamarla. Y los parámetros con valor por defecto van siempre al final; def f(a=1, b): es un error de sintaxis.

Pregunta 8Respuesta corta

¿Qué imprime este programa? Escribe la salida exacta.

def minmax(a, b):
    if a < b:
        return a, b
    return b, a

menor, mayor = minmax(8, 3)
print(mayor - menor)
Solución

Respuesta: 5

Con 8 y 3, a < b es falso y devuelve (3, 8). Al desempaquetar, menor = 3 y mayor = 8: 8 − 3 = 5.

Pregunta 9Única

¿Qué ventaja tiene dividir un programa en funciones?

  1. Se puede reutilizar el código y probar cada parte por separado
  2. El programa se ejecuta siempre más rápido
  3. Ya no hacen falta variables
  4. Python lo exige: sin funciones no se puede ejecutar nada
Solución

Respuesta: Se puede reutilizar el código y probar cada parte por separado

Es la idea de la programación estructurada: dividir un problema grande en piezas pequeñas con un nombre claro. Si una pieza falla, sabes dónde mirar.

Pregunta 10Rellenar huecos

Completa la función para que devuelva la media de dos números:

____ media(a, b): y dentro ____ (a + b) / 2

Palabras:

  • function
  • print
  • input
  • return
  • def
Solución

def media(a, b): y dentro return (a + b) / 2

Las funciones se definen con def. Si usaras print en lugar de return, la media se vería en pantalla, pero m = media(4, 6) guardaría None.

Pregunta 11Única

¿Qué imprime este código?

def g(x):
    return x * 2

def h(x):
    return g(x) + 1

print(h(g(3)))
  1. 13
  2. 7
  3. 12
  4. 14
Solución

Respuesta: 13

Se evalúa de dentro hacia fuera: primero g(3) = 6, y luego h(6), que calcula g(6) + 1 = 12 + 1 = 13.

Pregunta 12Única

¿Qué devuelve max(3, 9, 4)?

  1. 9
  2. 3
  3. (3, 9, 4)
  4. Un error: max solo admite listas
Solución

Respuesta: 9

Python trae funciones predefinidas (built-in) como print, len, max, min, abs o round. Se usan igual que las que defines tú.

Pregunta 13Única

¿Qué imprime este código?

def contar(n):
    c = 0
    while n > 0:
        n = n // 10
        c += 1
    return c

print(contar(2026))
  1. 4
  2. 2026
  3. 3
  4. 0
Solución

Respuesta: 4

Cada // 10 quita la última cifra. El bucle da una vuelta por cifra, así que la función cuenta cuántas tiene el número: 4.

Míralo paso a paso en Python Tutor

Python Tutor es la mejor forma de ver el fallo del fragmento 3 en directo. Elige Python 3, pega esto y avanza con Next:

def hay_doble_mal(tiradas):
    for d1, d2 in tiradas:
        if d1 == d2:
            return True
        else:
            return False


def hay_doble_bien(tiradas):
    for d1, d2 in tiradas:
        if d1 == d2:
            return True
    return False


tiradas = [(6, 5), (1, 5), (6, 6)]
print(hay_doble_mal(tiradas))
print(hay_doble_bien(tiradas))

Salida esperada:

False
True

Qué observar paso a paso:

  1. Al entrar en hay_doble_mal, la caja de la función recibe tiradas como una flecha a la misma lista del Global frame: no se copia.
  2. En la primera vuelta, d1 = 6 y d2 = 5. La flecha salta al else y, en el paso siguiente, aparece Return value: False. La caja desaparece con el bucle a medias: las otras dos tiradas no se han mirado.
  3. En hay_doble_bien, las dos primeras vueltas no hacen nada (la condición es falsa y no hay else) y la flecha vuelve al for.
  4. En la tercera vuelta, d1 = 6 y d2 = 6: Return value: True. La línea return False del final no llega a ejecutarse nunca en esta partida.

Chuletario: funciones en Python

Lo esencial en 30 segundos: una función se define con def nombre(parametros): y se ejecuta al llamarla con nombre(argumentos). return devuelve un valor y termina la función; sin return, devuelve None. print enseña, return entrega. Los parámetros y las variables de dentro son locales: nacen con la llamada y mueren al terminar. Entrada por parámetros, salida por return.

Definir y llamar:

Quiero…CódigoResultado
Definir una funcióndef cobrar(total, pago):no ejecuta nada todavía
Documentarla"""Devuelve el cambio.""" en la primera línease lee con cobrar.__doc__
Llamarlacobrar(3.70, 5.00)1.3
Guardar lo que devuelvecambio = cobrar(3.70, 5.00)cambio vale 1.3
Pasar argumentos por nombrecobrar(pago=5.00, total=3.70)1.3
Un parámetro opcionaldef cobrar(total, pago, propina=0):cobrar(3.70, 5.00) usa propina = 0
Anotar tiposdef f(precio: float) -> float:informativo, no se comprueba

Devolver:

Quiero…CódigoResultado
Devolver un valorreturn round(pago - total, 2)la llamada vale ese número
Devolver dos valoresreturn euros, centsuna tupla: e, c = f(...)
Salir sin valorreturndevuelve None
Avisar de que no hay resultadoreturn None y if r is None:quien llama decide el mensaje
Salir de dos buclesreturn dentro del bucle de dentrosale de la función entera
Buscar y decir «no está»return dentro; return None después del forla búsqueda de las bandejas

Plantillas de los ejercicios:

# Conversión: recibe, calcula y DEVUELVE (ejercicio 1)
def espanola_a_americana(nota):
    return round(nota * 10, 1)

# El dato entra por parámetro y sale por return (ejercicio 3)
saldo = depositar(saldo, 500)

# Máximo con su posición, empezando por un valor imposible (ejercicios 2 y 4)
def maximo_y_posicion(sumas):
    mejor, posicion, i = 0, 0, 0
    for s in sumas:
        i += 1
        if s > mejor:
            mejor, posicion = s, i
    return mejor, posicion

# Búsqueda: return dentro si lo encuentras, después del bucle si no (fragmento 3)
def hay_doble(tiradas):
    for d1, d2 in tiradas:
        if d1 == d2:
            return True
    return False

Síntoma y causa probable:

SíntomaCausa probable
TypeError: … 'NoneType' …una función sin return (o con print en su lugar)
La variable vale None y en la pantalla salía el númeroprint donde hacía falta return
NameError: name 'x' is not defined fuera de la funciónx era local: devuélvela con return
UnboundLocalError: cannot access local variableasignas dentro una variable de fuera: recíbela y devuélvela
TypeError: f() missing 1 required positional argumentfalta un argumento en la llamada
SyntaxError: parameter without a default follows…un parámetro opcional delante de uno obligatorio
Resultado con el signo cambiado o absurdoargumentos en el orden equivocado: usa nombres
Una búsqueda dice «no» aunque el dato estáreturn False dentro del bucle (en un else)
La función «no hace nada»la llamaste sin paréntesis o no la llamaste

Antes de entregar, pregúntate:

  1. ¿Cada función que calcula tiene return, y solo las de presentación hacen print?
  2. ¿Guardo lo que devuelven las funciones (saldo = depositar(saldo, 500)) en lugar de llamarlas sueltas?
  3. ¿Mis búsquedas ponen el «no está» después del bucle y no dentro?
  4. ¿Hay alguna variable global que una función cambie? Si la hay, ¿puedo pasarla y devolverla?

Si has llegado hasta aquí con los cuatro ejercicios resueltos, ya tienes funciones de sobra para el siguiente paso: guardarlas en archivos propios y usarlas desde cualquier programa con los módulos en Python.

Sigue aprendiendoTodos los artículos de Python

¿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 *