Programar Juegos Arcade con Python y Pygame

Programar Juegos Arcade
con Python y Pygame

Chapter 9: Funciones

9.1 Introducción a Funciones

Vídeo: Por qué Usar Funciones?

Usamos funciones por dos razones. Primero, hacen que el código sea más fácil de leer y comprender. Segundo, permiten utilizar el código más de una vez.

Imagina un conjunto de código para dibujar el árbol mostrado en la Figura 9.1. Para conseguirlo, el programador ejecuta los siguientes comandos:

pygame.dibujar.rect(pantalla, MARRON, [60, 400, 30, 45])
pygame.dibujar.polygon(pantalla, VERDE, [[150, 400], [75, 250], [0, 400]])
pygame.dibujar.polygon(pantalla, VERDE, [[140, 350], [75, 230], [10, 350]])
fig.simple_tree
Figure 9.1: Simple Tree

Realmente no parece tan obvio que esas tres líneas de código vayan a dibujar un árbol! Si tenemos varios árboles u objetos complejos, empieza a ser difícil comprender qué es lo que se está dibujando.

Cuando definimos una función hacemos que el programa sea más fácil de leer. Para definir una función empezamos usando el comando def. A continuación del comando def viene el nombre la función. En este caso la vamos a llamar dibuja_arbol. Empleamos las mismas reglas para los nombres de las funciones que para los nombres de las variables.

A continuación del nombre de la función vienen unos paréntesis y dos puntos. Todos los comandos de la función estarán indentados dentro de ella. Observa el ejemplo siguiente:

def dibuja_arbol():
    pygame.dibujar.rect(pantalla, MARRON, [60, 400, 30, 45])
    pygame.dibujar.polygon(pantalla, VERDE, [[150, 400], [75, 250], [0, 400]])
    pygame.dibujar.polygon(pantalla, VERDE, [[140, 350], [75, 230], [10, 350]])

Por sí solo este código no conseguirá dibujar el árbol. Tan sólo le dice al ordenador cómo tiene que dibujar_arbol. Tú tienes que llamar a la función para que realmente se ejecute el código contenido en ella y conseguir que el árbol se dibuje:

dibujar_arbol()

Con una biblioteca completa de funciones que definan cómo dibujar diferentes cosas, el programa final podría parecerse a éste:

dibujar_arbol()
dibujar_casa()
dibujar_coche()
dibujar_conejito_asesino()

Recuerda que dibujar_arbol tiene tres líneas de código. Cada una de las otras funciones, como dibujar_casa, tienen múltiples líneas de código. Cuando usamos funciones, podemos repetir los comandos sin necesidad de repetir todo el código contenido en ellas, consiguiendo un programa mucho más pequeño.

Los nombres de las funciones son muy importantes. Si los nombre son descriptivos, incluso una persona no programadora podría ser capaz de leer un conjunto de código y obtener una idea de qué es lo que está pasando. Los nombres de las funciones siguen las mismas reglas que los de las variables, debiendo empezar siempre por una minúscula.

9.2 Definiendo una Función

Define todas las funciones al principio de tu programa. Hazlo a continuación de las importaciones y de cualquier variable global. No definas ninguna función después del inicio de tu programa principal.

Para definir la función usa la palabra def seguida por el nombre de la función. Las funciones deberían empezar por una letra minúscula. Siempre cierra el nombre de la función con un par de paréntesis y dos puntos. Mira el siguiente ejemplo:

def imprimir_instrucciones():
    print("Bienvenido a la Batalla del Barro! El objetivo es darle al otro jugado con una bola de barro.")
    print("Introduce el ángulo (en grados) y la cantidad de PSI para cargar tu arma")

Cuando necesites que se impriman las instrucciones, bastaría con llamar a la función de ésta manera:

imprimir_instrucciones()

9.3 Parámetros de la Función

fig.function

Las funciones pueden tomar parámetros. Estos pueden usarse para aumentar la flexibilidad de la función, alterando lo que puede hacer basándose en los parámetros que se le pasen. Por ejemplo, nuestra función dibujar_arbol(), dibuja el árbol en una ubicación específica. Pero podríamos modificar la función para que acepte parámetros que especifiquen dónde dibujar el árbol. Por ejemplo, dibujar_arbol(pantalla, 0, 230), dibujaría el árbol en unas coordenadas (x, y) con valores (0, 230).

Este ajuste de la función podría tener el siguiente aspecto:

def dibujar_arbol(pantalla, x, y):
    pygame.draw.rect(pantalla, MARRON, [60+x, 170+y, 30, 45])
    pygame.draw.polygon(pantalla, VERDE, [[150+x,170+y],[75+x,20+y], [x,170+y]])
    pygame.draw.polygon(pantalla, VERDE, [[140+x,120+y], [75+x,y], [10+x,120+y]])

Esto nos permitiría dibujar múltiples árboles dondequiera que quisiéramos:

dibujar_arbol(pantalla, 0, 230)
dibujar_arbol(pantalla, 200, 230)
dibujar_arbol(pantalla, 400, 230)

La siguiente es una función que puede ejecutarse sin usar gráficos. Esta función calcula e imprime el volumen de una esfera:

def volumen_esfera(radio):
    pi = 3.141592653589
    volumen = (4 / 3) * pi * radio ** 3
    print("El volumen es ", volumen)
Vídeo: Crear Una Función
Los parámetros son asignados cuando se llama a la función, no cuando se la define.

El nombre de la función es volumen_esfera. Los datos que recogerá la función se almacenarán en una nueva variable llamada radio. El volumen resultante se imprime por pantalla. La variable radio no obtiene su valor dentro de la función. Frecuentemente, los programadores principiantes se confunden en esto. A las variables de los parámetros no se les asigna un valor en el momento de definir la función. A los parámetros se les asigna un valor cuando la función es llamada.

Llamamos a la función de ésta manera:

volumen_esfera(22)

La variable radio en la función es creada e inicializada con un valor de 22. El código de la función se ejecuta una vez que la compilación alcanza la llamada de la función.

¿Pero qué sucede si necesitamos pasar más de un valor? Pues podemos pasar múltiples parámetros a la función, cada uno separado por una coma:

def volumen_cilindro(radio, altura):
    pi = 3.141592653589
    volumen = pi * radio ** 2 * altura
    print("El volumen es ", volumen)

Podemos llamar a la función de esta forma:

volumen_cilindro(12, 3)

Los parámetros se escriben en orden, por lo que a radio le corresponde el 12 y a la altura el 3.

9.4 Devolviendo y capturando valores

Lamentablemente, este ejemplo de función es limitado. ¿Por qué? Pues si una persona quisiera usar la función volumen_cilindro para calcular el volumen de un pack de seis, no serviría. Sólo imprime el volumen de un solo cilindro. No podemos usar el resultado de la función para un cilindro, meterlo luego en una ecuación, multiplicarlo por seis y así obtener el volumen del pack.

9.4.1 Devolviendo valores

Esto se puede resolver usando la declaración return. Por ejemplo:

# Suma dos números y devuelve el resultado
def suma_dos_numeros(a, b):
    resultado = a + b
    return resultado

Return no es una función, y no usa paréntesis. No vayas a escribir return(resultado).

Bien, esto sólo resuelve la mitad del problema, ya que si llamásemos ahora a la función, no sucedería gran cosa. Los números se suman. Nos son devueltos. Pero no hacemos nada con el resultado.

# Con esto no adelantamos mucho, ya que no capturamos el resultado
suma_dos_numeros(22, 15)

9.4.2 Capturando los valores devueltos

Necesitamos capturar el resultado. Esto lo conseguimos estableciendo una variable que sea igual al valor devuelto por la función:

# Almacenamos el resultado de la función en un variable
mi_resultado = suma_dos_numeros(22, 15)
print(mi_resultado)

Ahora ya no hemos desperdiciado el resultado. Está almacenado en mi_resultado, el cual lo podemos imprimir o utilizar para alguna otra cosa.

def volumen_cilindro(radio, altura):
    pi = 3.141592653589
    volumen = pi * radio ** 2 * altura
    return volumen
Debido al return, la función puede usarse más tarde como parte de una ecuación que calcule el volumen para el pack de seis:
volumen_pack_seis = volumen_cilindro(2.5, 5) * 6

El valor devuelto por volumen_cilindro va a la ecuación y es multiplicado por seis.

Hay una enorme diferencia entre una función que imprime (print) y otra que devuelve (return) un valor. Observa el código siguiente, luego ejecútalo:

# Función que imprime el resultado
def imprime_suma(a, b):
    resultado = a + b
    print(resultado)

# Función que devuelve el resultado
def devuelve_suma(a, b):
    resultado = a + b
    return resultado

# Esto imprime en pantalla la suma de 4+4
imprime_suma(4, 4)

# Este no
devuelve_suma(4, 4)

# Esto no introducirá el resultado de la suma en x1
# Obtiene realmente un valor de 'None'
x1 = imprime_suma(4, 4)

# Esto sí lo hará
x2 = devuelve_suma(4, 4)

Es normal que cuando empezamos a trabajar con funciones, nos quedemos atascados al observar códigos de este estilo:

def calcular_promedio(a, b):
    """ Calcula el promedio de dos números """
    resultado = (a + b) / 2
    return resultado

# Supongamos que tenemos algún código aquí
x = 45
y = 56

# Espera, ¿cómo imprimo el resultado de esto?
calcular_promedio(x, y)

¿Cómo imprimimos el resultado para calcular_promedio? El programa no puede imprimir resultado porque la variable solo existe dentro de la función. En su lugar, utilizamos una variable para capturar el resultado:

def calcular_promedio(a, b):
    """ Calcula el promedio de dos números """
    resultado = (a * b) / 2
    return resultado

# Imagina que aquí hay algo de código
x = 45
y = 56

promedio = calcular_promedio(x, y)
print(promedio)

9.5 Documentar Las Funciones

En Python, las funciones suelen tener un comentario al principio del todo. Este comentario está delimitado por triples comillas y se le conoce como docstring. Una función podría tener este aspecto:

def volumen_cilindro(radio, altura):
    """Devuelve el volumen de un cilindro, conocidos su radio y altura."""
    pi = 3.141592653589
    volumen = pi * radio ** 2 * altura
    return volumen

Una gran cosa al usar docstrings en funciones, es que el comentario puede ser extraído y colocado en una website que documente tu código, usando una herramienta como Sphinx. La mayoría de lenguajes tienen herramientas similares que ayudan a documentar tu código en un suspiro. Esto te puede ayudar a ahorrar tiempo a medida que comiences a trabajar en programas mucho más largos.

9.6 Ámbito de la Variable

Vídeo: Ámbito de la Variable

El uso de funciones nos introduce en el concepto de ámbito (scope). El ámbito es el lugar del código donde la variable está “viva” y podemos acceder a ella. Observa por ejemplo el código siguiente:

# Define una función elemental que estable que
# x es igual a 22
def f():
    x = 22

# Llamada a la función
f()
# Falla, ya que x existe solo en f()
print(x)

La última línea generará un error, porque x solo existe dentro de la función f(). La variable es creada cuando llamamos a f(), y el espacio de memoria que ocupa es liberado tan pronto como f()termina.

Aquí es donde las cosas se complican.

Una regla áun más confusa es el acceder a variables creadas fuera de la función f(). En el código siguiente, x se crea antes de la función f(), y así, se puede leer desde el interior de la función f().

# Crea la variable x y le asigna 44
x = 44

# Definimos una sencilla función que imprime x
def f():
    print(x)

# Llamamos a la función
f()

Las variables creadas antes de una función, pueden leerse dentro de la función sólo si la función no cambia el valor. Éste código, muy parecido al anterior, fallará. El ordenador reclamará que desconoce qué es x.

# Crea la variable x y le asigna 44
x = 44

# Definimos una sencilla función que imprime x
def f():
    x += 1
    print(x)

# Llamamos a la función
f()

Otros lenguajes tienen reglas más complejas en cuanto a la creación de variables y su ámbito que Python. Debido a que Python es un lenguaje sencillo, es un buen lenguaje introductorio.

9.7 Pass-by-copy

Las funciones pasan sus valores, creando copias del original. Por ejemplo:

# Define una función elemental que imprime x
def f(x):
    x += 1
    print(x)

# Establece y
y = 10
# Llamada a la función
f(y)
# Imprime y para ver si ha cambiado
print(y)

El valor de y no cambia a pesar de que la función f() incrementa el valor que le pasan. Cada una de las variables listada como parámetro en una función, es una variable totalmente nueva. El valor de la variable es copiado desde donde se le ha llamado.

En el primer ejemplo esto se ve razonablemente sencillo. Donde todo empieza a volverse confuso es cuando, tanto el código que llama a la función, como la propia función, tienen variables con el mismo nombre. El siguiente código es idéntico al anterior, pero en lugar de usar y usa x.

#  Define una función elemental que imprime x
def f(x):
    x += 1
    print(x)

# Establece x
x = 10
# Llamada a la función
f(x)
# Imprime x para ver si ha cambiado
print(x)

La salida es igual a la del programa que usa y. A pesar de que ambos, la función y el código que la rodea, usan el nombre de x para la variable, realmente existen dos variables diferentes. Está la variable x que existe dentro de la función, y una variable x, diferente, que existe fuera de la función.

9.8 Funciones Que Llaman Funciones

Es muy posible que una función llame a otra función. Por ejemplo, digamos que hemos definido funciones como las siguientes:

def brazo_fuera(queBrazo, palmasArribaOAbajo):
	# el código vendría aquí
	
def agarrar_mano(mano, brazo):
	# el código vendría aquí

Luego podríamos tener otra función que llamara a las dos anteriores:

def macarena():
	brazo_fuera("derecho", "abajo")
	brazo_fuera("izquierdo", "abajo")
	brazo_fuera("derecho", "arriba")
	brazo_fuera("izquierdo", "arriba")
	agarrar_mano("derecha", "brazo izquierdo")
	agarrar_mano("izquierda", "brazo derecho")
	# etc

9.9 Funciones Principales y Globales

Las variables globales son, sencillamente, diabólicas.

A medida que los programas se hacen más grandes, es importante mantener organizado el código en funciones. Python nos permite escribir el código en un “nivel 0 de indentación.” Esto describe la mayoría del código que hemos escrito hasta ahora. Nuestro código está alineado a la izquierda y no se ha colocado dentro de funciones.

Esta clase de filosofía es similar a la amontonar toda nuestra ropa en el centro de la habitación, o guardar todas nuestras herramientas sobre el banco de trabajo. Solo funciona cuando son pocos los objetos. Pero aun cuando tengas pocos de ellos, todo resulta bastante desordenado.

Deberías poner todo tu código y todas tus variables dentro de funciones. Esto mantendrá organizado el programa. También te ayudará en el momento que tengas la necesidad de buscar un fallo en el programa. Las variables creadas al “nivel 0 de indentación”, se llaman variables globales. Estas variables globales son una una muy mala idea. ¿Por qué? Porque cualquier trozo de código, en cualquier lugar, puede cambiar su valor. Si tienes 50,000 líneas de programa, cada línea de código puede cambiar el valor de esa variable global. En cambio, si mantienes la variable dentro de la función, entonces, solamente ese código de la función puede cambiarla. Así, si tienes un valor inesperado en una variable, solo tienes que buscar a través de, probablemente, 50 líneas de código. De la otra forma tendrías que revisar cada línea de código en el programa entero!

Una buena forma de escribir un programa en Python sería seguir este patrón:

def main():
	print("Hola mundo.")
	
main()

En este caso, todo el código que normalmente se hubiera ejecutado en un nivel 0 de indentación, se coloca dentro de la función main. La última línea del archivo llama a main.

Pero espera! Tenemos otro problema que arreglar. En el Capítulo 14 hablaremos de cómo partir nuestro programa en varios archivos. Podemos usar el comando import para traer funciones desde otros módulos que hayamos creado anteriormente. Si usamos el comando import en este módulo, automáticamente ejecutaría la función main. Pero no queremos esto. Queremos que el programa que lo importa, controle cuándo debe llamarse a la función.

Para solucionarlo, podemos hacer que nuestro programa compruebe una variable global definida automáticamente por Python. (Lo sé, acabo de decir que las variables globales son un mala idea, ¿no es cierto?) La variable es llamada __name__, con dos guiones bajos, antes y después. Podemos usarlo para probar si éste código está siendo importado o ejecutado. Si el código está siendo ejecutado, Python establecerá automáticamente el valor de esta variable como __main__. Usando una declaración if llamaremos a la función main solo si el código está siendo ejecutado. En caso contrario, el código solo definirá la función main. El código que lo haya importado podrá llamar a la función cuando quiera.

Esta es la forma cómo todo tu código Python debería ejecutarse:

def main():
	print("Hola Mundo.")
	
if __name__ == "__main__":
    main()

Una de las razones por las que me encanta Python como primer lenguaje, es que no estás obligado a usar toda esta complejidad hasta que realmente la necesitas. Otros programas, por ejemplo Java, te obligan a toda esta complejidad sin importar lo pequeño que sea tu programa.

Para hacer las cosas fáciles en este libro no te mostraremos ejemplos que sigan este patrón. Pero después de este libro, tus programas serán, probablemente, lo suficientemente complejos, que tu vida será más fácil si no “tiras toda tu ropa sobre un montón”, por decirlo así.

Si eres un super entusiasta de la programación, intenta empezar ahora a escribir tus programas de esta forma. Aunque suponga un poco más de desafío comenzar así, más adelante te será más fácil escribir tus programas. También es una buena forma de aprender cómo manejar adecuadamente tus datos y su ámbito.

Este es un ejemplo que muestra cómo usar la plantilla Pygame con este patrón:
pygame_base_template_proper.py

No es necesario usar esta plantilla, al menos mientras sea yo el que dicte el curso. Para mí está bien que durante tu primer semestre apiles tu ropa en medio de la habitación. Me contento con que puedas vestirte. (friquis de pro: podemos 'limpiar' aún más este programa cuando lleguemos al Capítulo de “clases.”)

9.10 Ejemplos

En cada uno de los siguientes ejemplos, piensa qué es lo que imprimirán. Comprueba si has acertado. Si no lo has conseguido, dedícale un poco de tiempo a entender el por qué no ha sido así.

# Ejemplo 1
def a():
    print("A")
    
def b():
    print("B")
    
def c():
    print("C")
    
a()
# Ejemplo 2
def a():
    b()
    print("A")
    
def b():
    c()
    print("B")
    
def c():
    print("C")
    
a()    
# Ejemplo 3
def a():
    print("A")
    b()
    
def b():
    print("B")
    c()
    
def c():
    print("C")
      
a()
# Ejemplo 4
def a():
    print("A empieza con")
    b()
    print("A termina con")
    
def b():
    print("B empieza con")
    c()
    print("C termina con")
    
def c():
    print("C empieza y termina con")
    
a()
# Ejemplo 5
def a(x):
    print("A empieza con, x = ",x)
    b(x + 1)
    print("A termina con, x = ",x)
    
def b(x):
    print("B empieza con, x = ",x)
    c(x + 1)
    print("C termina con, x = ",x)
    
def c(x):
    print("C empieza y termina con, x = ",x)
    
a(5)
# Ejemplo 6
def a(x):
    x = x + 1

x = 3    
a(x)

print(x)
# Ejemplo 7
def a(x):
    x = x + 1
    return x

x = 3    
a(x)

print(x)
# Ejemplo 8
def a(x):
    x = x + 1
    return x

x = 3    
x = a(x)

print(x)
# Ejemplo 9
def a(x, y):
    x = x + 1
    y = y + 1
    print(x, y)

x = 10
y = 20
a(y, x)

print(z)
# Ejemplo 10
def a(x, y):
    x = x + 1
    y = y + 1
    return x
    return y

x = 10
y = 20
z = a(x, y)

print(z)
# Ejemplo 11
def a(x, y):
    x = x + 1
    y = y + 1
    return x, y

x = 10
y = 20
z = a(x, y)

print(z)
# Ejemplo 12
def a(x, y):
    x = x + 1
    y = y + 1
    return x, y

x = 10
y = 20
x2, y2 = a(x, y) # La mayoría de lenguajes de programación no admiten esto

print(x2)
print(y2)
# Ejemplo 13
def a(mis_datos):
    print("función a, mis_datos =  ", mis_datos)
    mis_datos = 20
    print("función a, mis_datos =  ", mis_datos)

mis_datos = 10

print("entorno global, mis_datos = ", mis_datos)
a(mis_datos)
print("entorno global, mis_datos = ", mis_datos)
# Ejemplo 14
def a(mi_lista):
    print("función a, lista =  ", mi_lista)
    mi_lista = [10, 20, 30]
    print("función a, lista =  ", mi_lista)

mi_lista = [5, 2, 4]

print("entorno global, lista = ", mi_lista)
a(mi_lista)
print("entorno global, lista = ", mi_lista)
# Ejemplo 15
# Concepto nuevo!
# Se describe con más detalle en el Capítulo 12
def a(mi_lista):
    print("función a, lista =  ", mi_lista)
    mi_lista[0] = 1000
    print("función a, lista =  ", mi_lista)

mi_lista = [5, 2, 4]

print("entorno global, lista = ", mi_lista)
a(mi_lista)
print("entorno global, lista = ", mi_lista)
#--
"""
Este es un simple juego de texto para mostrar el uso de funciones.
El juego se llama "BolaDeBarro", donde los jugadores, por turnos, se
lanzan bolas de barro unos contra otros, hasta que alguien es alcanzado.
"""

import math
import random

def imprimir_instrucciones():
    """ Esta función imprimirá las instrucciones. """
    
    # En una declaración print, puedes usar comillas triples para
    # imprimir varias líneas.
    print("""
Bienvenido a Bolas de Barro! El objetivo es darle al otro jugador con una bola de barro.
Introduce el ángulo (en grados) y la presión en PSI para cargar tu arma.
        """)

def calcular_distancia(psi, angulo_en_grados):
    """ Calcula la distancia que vuela la bola de barro. """
    angulo_en_radianes = math.radians(angulo_en_grados)
    distancia = psi * math.sin(angulo_en_radianes) * 15
    return distancia

def obtener_datosdel_usuario(nombre):
    """ Obtiene del usuario los valores para la presión y el ángulo. Lo devuelve como una lista con dos 
    números. """
    # Más adelante, en el capítulo sobre 'excepciones', aprenderemos como
    # modificar este código para que no se cuelgue cuando el usuario escriba
    # algo que no sea un número válido.
    
    psi = float(input(nombre + " ¿con cuántos psi cargamos el arma? "))
    angulo = float(input(nombre + " ¿con qué ángulo quieres apuntar el arma? "))
    return psi, angulo

def obtener_nombres_jugadores():
    """ Obtenemos una lista con los nombres de los jugadores. """
    print("Introduce los nombres de los jugadores. Puedes introducir cuantos quieras.")
    hecho = False
    jugadores = []
    while not hecho:
        jugador = input("Introducir jugador (presiona intro para salir): ")
        if len(jugador) > 0:
            jugadores.append(jugador)
        else:
            hecho = True
            
    print()    
    return jugadores
            
def procesa_turno_jugador(jugador_nombre, distancia_aparte):
    """ El código ejecuta el turno para cada jugador.
    Si devuelve False, continuamos con el juego.
    Si devuelve True, alguien ha ganado así que paramos. """
    psi, angulo = entrada_usuario = obtener_datosdel_usuario(jugador_nombre)            

    distancia_boladebarro = calcular_distancia(psi, angulo)
    diferencia = distancia_boladebarro - distancia_aparte
    
    # Si echamos un vistazo al capítulo de formatos de impresión, estas líneas
    # podrían imprimir números en un bonito formato.
    
    if diferencia > 1:
        print("Ha caído", diferencia, "metros muy lejos!")
    elif diferencia < -1:
        print("Te has quedado", diferencia * -1, "metros corto!")
    else:
        print("Bingo!", jugador_nombre, "gana!")
        return True
    
    print()
    return False

def main():
    """ Programa Principal. """
    
    # Comenzamos el juego.
    imprimir_instrucciones()
    jugador_nombres = obtener_nombres_jugadores()
    distancia_aparte = random.randrange(50, 150)
    
    # Se mantiene alerta hasta que alguien gana
    hecho = False
    while not hecho:
        # Iteramos para cada jugador
        for jugador_nombre in jugador_nombres:
            # Procesamos sus turnos
            hecho = procesa_turno_jugador(jugador_nombre, distancia_aparte)
            # Si alguien gana, 'rompemos' el bucle y finalizamos el juego.
            if hecho:
                break
            
if __name__ == "__main__":
    main()

9.11 Repaso

9.11.1 Test

Haz click para ir al Test.

9.11.2 Ejercicios

Haz click para ir a los Ejercicios.

9.11.3 Taller

Haz click para ir al Taller.


You are not logged in. Log in here and track your progress.