B: Más python: Funciones, módulos y paquetes, clases…

Funciones

A veces programaremos una funcionalidad que queremos reutilizar en varios sitios de nuestro programa (para evitar repetir código). Esto lo podemos hacer con las llamadas funciones:

[1]:
def imprimeTexto():
    print("Esto es un texto muy interesante")
    print("Que va a ser impreso varias veces en mi programa")
    print("Por eso creo una función para no repetir las mismas sentencias varias veces\n")

imprimeTexto()
imprimeTexto()
imprimeTexto()
Esto es un texto muy interesante
Que va a ser impreso varias veces en mi programa
Por eso creo una función para no repetir las mismas sentencias varias veces

Esto es un texto muy interesante
Que va a ser impreso varias veces en mi programa
Por eso creo una función para no repetir las mismas sentencias varias veces

Esto es un texto muy interesante
Que va a ser impreso varias veces en mi programa
Por eso creo una función para no repetir las mismas sentencias varias veces

  • def es una palabra reservada que indica que estamos definiendo una función.

  • imprimeTexto es el nombre que le damos a nuestra nueva función.

  • () permite especificar parámetros que tendrá nuestra función (en nuestro caso ninguno)

  • : delimita el comienzo del bloque de instrucciones que formará la función.

Parámetros (argumentos) en las funciones

En nuestro caso no hemos utilizado ningún parámetro en la función, pero usualmente podemos definir parámetros para condicionar la ejecución de la función. Los parámetros de una función se especifican en la definición de la misma entre los paréntesis de después del nombre de la función y serparados por comas si hay más de uno.

Asímismo las funciones pueden devolver un valor resultado de ejecutar dicha función con la instrucción return.

[2]:
def esPar(numero):
    return numero % 2 == 0

print(esPar(7))

print(esPar(10))
False
True

Otro ejemplo:

[3]:
def esPrimo(numero):
    for n in range(2, numero):
        if (numero % n == 0):
            return False

    return True

print(esPrimo(29))

print(esPrimo(42))
True
False

Ámbito de las variables (scope)

Las variables definidas dentro de una función tienen ámbito local, es decir, solo pueden ser accedidas desde dentro de esa misma función. Dejan de tener valor una vez que la función finaliza su ejecución:

[4]:
def miFuncion():
    contador = 10

    return contador

# print(contador)     # Dará NameError, porque no podemos acceder a la variable local desde fuera

Las variables declaradas fuera de cualquier función tienen ámbito global y puede ser leida desde cualquier función (o incluso otros módulos que veremos más adelante):

[5]:
contador = 7

def miFuncion():
    print(contador)

miFuncion()
7

En este caso SÍ podemos acceder a la variable contador desde una función porque tiene ámbito global. Para modificar una variable global desde una función hay que usar la palabra reservada global. Si no, al asignar el valor dentro de la función se creará una variable local con el mismo nombre (pero distinta):

[6]:
contador = 7

def miFuncion1():
    contador = 3

    print(f"Desde dentro de miFuncion1 contador vale {contador}")

def miFuncion2():
    global contador

    contador = 5

    print(f"Desde dentro de miFuncion2 contador vale {contador}")

print(f"Valor inical de contador: {contador}")

miFuncion1()

print(f"La variable contador global no se ha modificado: {contador}")

miFuncion2()

print(f"Al usar dentro de la función global SI se modifica la varable global: {contador}")
Valor inical de contador: 7
Desde dentro de miFuncion1 contador vale 3
La variable contador global no se ha modificado: 7
Desde dentro de miFuncion2 contador vale 5
Al usar dentro de la función global SI se modifica la varable global: 5

Como norma general: Es MUY MALA PRÁCTICA usar variables globales dentro de las funciones. Puede parecer cómodo. Puede parecer práctico, pero en menos tiempo del que parece depurar ese código será un infierno. ¡Advertidos quedáis!


Ejercicio B.7:

En el ejercicio B.1 calculamos el valor de una función polinómica de grado 2. Construyamos una función llamada polinomio que admita como parametros x, a, b y c y devuelva el resultado de la evaluación del polinomio.

[7]:
# Tu código aquí

#polinomio(1, 2, 1, -2)

Ejercicio B.8:

Hagamos una función evaluaPolinomio usando la función polinomio del ejercicio anterior devuelva una lista de tuplas \((x, f(x))\) evaluando el polinomio en diferentes \(x\). Como parámetros debe admitir xMin, xMax, xStep (incremento de evaluación entre xMin y xMax), a, b y c. Luego prueba las última líneas comentadas… ¡a ver que pasa!

Nota: Para que funcione la última parte del ejercicio (que grafica el resultado) hace falta instalar un paquete de Python llamado matplotlib. Para hacerlo abrimos un nuevo Anaconda prompt, activamos el entorno cursoAstronomia y ejecutamos:

> conda install -c conda-forge matplotlib

Es posible que tengamos que reiniciar el Kernel de JupiterLab para que tenga efecto la instalación del nuevo paquete: Kernel \(\rightarrow\) Restart Kernel.... En alguna rara ocasión p

[8]:
#def evaluaPolinomio(xMin, xMax, xStep, a, b, c):

# Tu código aquí

#puntos = evaluaPolinomio(-1, 1, .1, 2, 1, -2)

#import matplotlib.pyplot as plt   # Esto aún no hay que entenderlo. Lo veremos más adelante.
#x, y = zip(*puntos)
#plt.plot(x, y)
#plt.show()

Parámetros con nombre (y valores por defecto)

Se pueden pasar los argumentos de una función por nombre. Eso permite cambiar el orden de los parámetros y, además, ganaremos en legibilidad:

[9]:
def graMinSeg2Gra(grados, minutos, segundos):
    return grados + minutos / 60 + segundos / 60 ** 2

print(graMinSeg2Gra(30, 3, 1))

print(graMinSeg2Gra(grados = 30, minutos = 3, segundos = 1))

print(graMinSeg2Gra(segundos = 1, grados = 30, minutos = 3))

print(graMinSeg2Gra(30, 3, segundos = 1))  # Se puede mezclar por posición y por nombre, pero no es muy recomendable
30.05027777777778
30.05027777777778
30.05027777777778
30.05027777777778

Además, podemos dar valores por defecto a algunos parámetros de manera que no sea obligatorio especificarlos siempre:

[10]:
def graMinSeg2Gra(grados, minutos, segundos = 0.0): # No hará falta especificar los segundos
    return grados + minutos / 60 + segundos / 60 ** 2

print(graMinSeg2Gra(30, 3)) # Esto fallaría con la función de antes
30.05

Además podemos tener funciones con un número de parámetros variable:

[11]:
def sumaTodo(*valores):  # Valores será una tupla con todos los parámetros que se le pasen a la función
    suma = 0

    for v in valores:
        suma = suma + v

    return suma

print(sumaTodo(1,2,3,4,5,6))
21

Más información al respeto (español) - Y otra página más (inglés)


Ejercicio B.9:

Hagamos una función similar a la del ejercicio B.7 llamada polinomioN, pero que acepte un argumento x con valor por defecto 0.0 y un número indeterminado de argumentos que serían los coeficientes a, b, c del polinomio de grado igual al número de coeficientes menos uno (si te pasan 3 parámetros será un polinomio de grado 2).

[12]:
# Tu código aquí

# polinomioN(3, 2, 1, -2, x=2.0)

Pasando funciones como parámetro

Las funciones pueden a su ver pasarse como parámetros a otra función (más información):

[13]:
def cuadrado(x):
    return x ** 2

def raiz_cuadrada(x):
    return x ** 0.5

def operar(func, *args):
    for n in args:
        print(func(n))

operar(cuadrado, 3, 4, 5)

operar(raiz_cuadrada, 5, 6, 7)
9
16
25
2.23606797749979
2.449489742783178
2.6457513110645907

Ejercicio B.10:

Creemos una función evaluaF similar a la del ejercicio B.8 que evalue en varios puntos (de xMin a xMax en pasos de xStep). Pasaremos como parámetro una nueva función raizCubica y sierra que viene definida por \(sierra(x) = x \pmod 1\).

[14]:
# Tu código aquí

#puntos1 = evaluaF(raizCubica)
#puntos2 = evaluaF(sierra)

#import matplotlib.pyplot as plt
#x, y = zip(*puntos1)
#plt.plot(x, y)
#x, y = zip(*puntos2)
#plt.plot(x, y)
#plt.show()

Desempaquetado de tuplas (y otros iterables)

Una característica interesante de Python es que es muy fácil y visual tener funciones que devuelvan más de un valor usando el desempaquetado (Más información -se puede complicar mucho-). Por ejemplo si queremos resolver la ecuación de segundo grado y que nos devuelva las 2 raices con la fórmula \(x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\)

[15]:
def segGrado(a, b, c):
    aux = (b**2 - 4*a*c) ** 0.5

    return ((-b + aux) / (2*a), (-b - aux) / (2*a))

sol1, sol2 = segGrado(1, -10, 16)

print(f"Las soluciones de la ecuación son: {sol1} y {sol2}")
Las soluciones de la ecuación son: 8.0 y 2.0

Otras funciones interesantes

range(start, stop, step): Para iterar en una lista de números (en realidad no es una función ni una lista). Más información

[16]:
for n in range(3, 19, 2):
    print(n)
3
5
7
9
11
13
15
17

enumerate: “Devuelve” una lista de tuplas con cada elemento y su posición en la lista (contador):

[17]:
lista = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']

for indice, valor in enumerate(lista): # Usamos el desempaquetado
    print(f"{indice}: {valor}")
0: Enero
1: Febrero
2: Marzo
3: Abril
4: Mayo
5: Junio
6: Julio
7: Agosto
8: Septiembre
9: Octubre
10: Noviembre
11: Diciembre

round(x, d): Redondea x a d cifras decimales:

[18]:
round(-17.728384, 2)
[18]:
-17.73

List comprehension (¿comprensión de listas?)

Existe una manera compacta para operar sobre los elementos de una lista llamada list comprehension. Más información al respecto (inglés). A continuación unos ejemplos:

[19]:
# Inicializar una lista de 30 ceros:
lista = [0 for x in range(30)]
print(lista)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[20]:
# Iniciar una lista con valores de 0 a 29
lista = [x for x in range(30)]
print(lista)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[21]:
# Iniciar lista con valores pares menores que 30
lista = [x for x in range(30) if x%2 == 0]
print(lista)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
[22]:
# Crear un lista con todos los valores de otra lista elevados al cubo
lista2 = [x**3 for x in lista]
print(lista2)
[0, 8, 64, 216, 512, 1000, 1728, 2744, 4096, 5832, 8000, 10648, 13824, 17576, 21952]

Ejercicio B.11:

Si quisieramos listar todos los números primos menores de un valor n utilizar la función anteriormente definida esPrimo() para cada valor posible sería bastante ineficiente. Vamos a implementar una función que haga la criba de Eratóstenes que devuelva una lista de todos los primos menores de n.

[23]:
#def cribaEratostenes(n):

# Tu código aquí


# print(cribaEratostenes(40))

Módulos y Paquetes

Módulos

Un módulo es un archivo de Python (típicamente con extensión .py) cuyos objetos pueden accederse desde nuestro código. Sirven para organizar mejor grandes cantidades de código. Por ejemplo, tenemos el fichero (funciones.py), donde se definen las funciones cuad(x), cubo(x) y raizN(x,n). Podemos usarlas de la siguiente manera:

[24]:
import funciones

print(funciones.cuad(3))
9

Cuidado porque JupyterLab no recarga los módulos aunque cambien. Si necesitas recargarlo porque has modificado el módulo tendrás que reiniciar el Kernel (Kernel \(\rightarrow\) Restart Kernel) o bien usar:

[25]:
import importlib
importlib.reload(funciones)
[25]:
<module 'funciones' from '/home/zerjillo/MEGA/cursoAstronomia/notebooks/funciones.py'>

Si no queremos escribir el nombre del módulo delante de las funciones podemos importarlas de la siguiente manera:

[26]:
from funciones import cuad, raizN

print(raizN(27, 3))
3.0

Y si las queremos todas (aunque habrá que tener cuidado de que no haya funciones con el mismo nombre importadas de distintos módulos):

[27]:
from funciones import *

print(cubo(5))
125

Paquetes

Los paquetes son carpetas con múltiples módulos. Más información al respecto.

Módulos y paquetes básicos de interés

Python viene de serie con una colección grande de paquetes y módulos estándar (para matemáticas, números complejos, números «aleatorios» (El azar es imposible), funciones estadísticas, manejo de ficheros…). Puedes consultar una lista de los mismos en la documentación de la biblioteca estándar de Python.

Módulo math

Documentación

Ejemplos:

[28]:
import math

math.fabs(-3.72)   # Valor absoluto
[28]:
3.72
[29]:
math.sqrt(9)   # Raiz cuadrada
[29]:
3.0
[30]:
math.log(10)   # Logaritmo
[30]:
2.302585092994046
[31]:
math.log10(100)   # Logaritmo base 10
[31]:
2.0
[32]:
math.cos(math.pi / 2)   # Coseno y la constante PI
[32]:
6.123233995736766e-17
[33]:
round(math.cos(math.pi / 2), 1)    # Menos feo
[33]:
0.0

Un buen artículo (en inglés) que habla sobre algunas funciones de Python que puede resultar interesante conocer:

Python built-in functions to know

Programación Dirigida a Objetos (PDO): Clases, Propiedades, Métodos y Objetos

La PDO es un paradigma de programación en el que la información y las operaciones que la manipulan se encapsulan en unas entidades denominadas objetos que pertenecen a determinadas clases. Veamos como lo implementa Python con ejemplos:

[34]:
class Planeta:

    nombre = ""
    diametro = 0.0
    distanciaSol = 0.0

    def __init__(self, nombre, diam, distSol):
        self.nombre = nombre
        self.diam = diam
        self.distanciaSol = distSol

    def getNombre(self):
        return nombre

    def getDistanciaSol(self):
        return self.distanciaSol

    def getDistanciaSolKm(self):
        return self.distanciaSol * 149597870


jupiter = Planeta("Júpiter", 192984, 5.2)
mercurio = Planeta("Mercurio", 4879, 0.39)

print(jupiter.getDistanciaSolKm())
print(mercurio.nombre)
777908924.0
Mercurio

Hemos definido una clase llamada Planeta junto con unas pocas propiedades y métodos. Particularmente tenemos que:

  • class es una palabra reservada para definir una clase

  • Planeta es el nombre de la clase (para los nombres de las clases se suele usar Upper CamelCase

  • nombre, diametro y distanciaSol son las propiedades de la clase (las variables que contiene un objeto de esa clase)

  • getNombre, getDistanciaSol y getDistanciaSolKm son los métodos de la clase (las funciones que permiten operar con las ropiedades de la clase). Es muy importante fijarse en que el primer argumento de cualquier método es siempre self (el propio objeto).

  • self. se us para acceder a cualquiera de los métodos o propiedades del objeto desde dentro del objeto (desde el código de sus propios métodos).

  • __init__ es un método especial llamado constructor que se ejecuta siempre que se instancie (cree) un objeto de dicha clase y sirve normalmente para iniciar las propiedades del objeto. Hay que fijarse que al ser un método como otro cualquiera su primer argumento es self.

En la línea

jupiter = Planeta("Jupiter", 192984, 5.2)

asignamos a una variable llamada jupiter un objeto de la clase Planeta. Se ejecutará el constructor al que le pasamos una serie de parámetros.

La sintaxis para ejecutar uno de los métodos del objeto o para acceder a sus propiedades se hace usando un . que separa el nombre de la variable y el método o propiedad al que queremos acceder:

jupiter.getDistanciaAlSolKm()    # Llamamos a un método del objeto que está
                                 #   en la variable jupiter

mercurio.nombre                  # Accedemos al valor de la propiedad nombre
                                 #   del objeto que está en la variable mercurio

Sobre el paradigma de orientación a objetos hay mucho que se puede decir (herencia, polimorfismo, encapsulamiento…), pero dichos conceptos se salen del ámbito de este curso. Algunos enlaces más interesantes: + Más información sobre la PDO en Python. + ¡No temas a la POO!: Un tutorial sobre programación orientada objetos contando una historia inspirada en el lejano oeste norte-americano. En principio pensado para programadores de Java, pero muchos conceptos como la herencia son perfectamente válidos en Python.


Ejercicio B.12:

Creemos una clase llamada Polinomio que nos permita encapsular la información de un polinomio de grado arbitrario n. Dicha clase debe almacenar la información de sus coeficientes y debe proveer métodos para:

  • obtener los coeficientes del polinomio,

  • para obtener el coeficiente con índice n,

  • para obtener el grado del polinomio,

  • para evaluar el polinomio en cualquier punto x y

  • para obtener una lista de puntos evaluados similar a la del ejercicio B.8.

[35]:
#class Polinomio:

# Tu código aquí

#pol1 = Polinomio(1.25, 2, -1, -2)

#print(f"Los coeficientes del polinomio: {pol1.coeficientes}")

#print(f"El coeficiente de grado 2 es: {pol1.getCoeficiente(2)}")

#print(f"El grado del polinomio es: {pol1.getGrado()}")

#print(f"El polinomio en x=2.0 vale: {pol1.evalua(2.0)}")

#puntos1 = pol1.evaluaF()

#pol2 = Polinomio(2, 0, 0)
#puntos2 = pol2.evaluaF()

#import matplotlib.pyplot as plt
#x, y = zip(*puntos1)
#plt.plot(x, y)
#x, y = zip(*puntos2)
#plt.plot(x, y)
#plt.show()

Ejercicios propuestos (y no resueltos)

A continuación se enuncian unos pocos ejercicios que deberían poderse resolver con los conocimientos adquiridos en este tema de introducción a Python.


Ejercicio B.13 (sin resolver):

Programemos un juego muy sencillo: El juego comienza saludando al jugador y proponiéndole que adivine un número entero entre 1 y 100. El usuario debe entonces introducir un número. Entonces: + Si el número es el correcto el programa felicita al jugador y acaba el programa + Si el número no es el correcto el programa se lo indica al jugador diciéndole mayor o menor y vuelve a permitir que el usuario haga un intento. Este proceso se repetirá hasta que el jugador acierte.


Ejercicio B.14 (sin resolver):

Programa el juego del ahorcado.


Ejercicio B.15 (sin resolver):

Haz un programa que dibuje el fractal de Mandelbrot en el cuadro delimitado por los números complejos (-2.0,-0.9) y (0.5,0.9) a intervalos de 0.05. Como todavía no tenemos mucha experiencia con la representación de gráficos vamos a pintarlo con print y el símbolo *. Como número máximo de iteraciones para determinar si el punto pertenece al conjunto de Mandelbrot podemos usar 100. Deberías obtener un resultado similar a:

                                 *
                               ****
                               ****
                               ****
                             **   *
                         *  **********
                        ******************
                         *****************
                      * *****************
                       *******************
                     ***********************
                      *********************
            * ***    **********************
            *******  **********************
           ********* **********************
           ********* **********************
        ** ********* *********************
*    ************************************
        ** ********* *********************
           ********* **********************
           ********* **********************
            *******  **********************
            * ***    **********************
                      *********************
                     ***********************
                       *******************
                      * *****************
                         *****************
                        ******************
                         *  **********
                             **   *
                               ****
                               ****
                               ****
                                 *