Saltar a contenido

Funciones

Una función es un conjunto de sentencias que se encapsulan para poder ser reutilizadas.

Una función realiza una determinada tarea y, habitualmente, tiene datos de entrada y datos de salida. Algunas de las ventajas de utilizar funciones son:

  • No se repite código innecesariamente.
  • El código es más fácil de interpretar ya que las funciones aportan claridad.
  • El código es más fácil de poner a punto ya que se puede probar cada función de forma independiente.
  • El código es más fácil de depurar ya que se puede acotar que función no realiza correctamente su tarea.
  • El código es más fácil de mantener ya que solo será necesario modificar unas determinadas funciones.

Una función se define en algún lugar del código. Cuando se llama a la función desde otro lugar del código, existe un salto en el flujo del programa al lugar donde la función está definida. Cuando la ejecución de la función termina, existe otro salto a la ubicación original del código desde el cual se llamo a la función.

En python hay dos tipos de funciones:

  • build-in son las funciones definidas en el propio lenguaje como print o input.
  • user-defined son las funciones creadas por el usuario.

La sintaxis básica para definir una función es la siguiente:

1
2
3
def nombre_funcion(lista_parametros):
    """docstring"""
    codigo a ejecutar

Las funciones que llevan nombre se definen con la palabra clave def. A continuación se especifica el nombre de la función. Para nombrar las funciones se utilizan las mismas convenciones que para variables: palabras en minúsculas y si está formado por varias se utiliza el guión bajo _ como separador.

Un función puede tener opcionalmente una lista de parámetros entre paréntesis. Esos parámetros se utilizan para proporcionar datos de entrada a la función.

Los dos puntos : se utilizan para indicar que ha finalizado la cabecera de la función y que comienza su cuerpo (sentencias de código que la conforman). El cuerpo de la función debe de estar indentado por cuatro espacios respecto a la cabecera. La función finaliza cuando se encuentra una sentencia que esté indentada de nuevo al mismo nivel que el def.

Se puede incluir opcionalmente un string que documente lo que hace la función (docstring). Se suele utilizar la triple comilla para que estos comentarios puedan ocupar varias líneas.

Función sin parámetros

Veamos un ejemplo de una función muy simple, sin parámetros y con una sola sentencia en su cuerpo:

1
2
3
4
def saludar():
    print("Hola mundo!")

saludar()

Resultado:

Hola mundo!

En la línea 1 se define la cabecera de la función con la palabra clave def, el nombre de la función saludar, unos paréntesis vacios (sin parámetros, porque no recibe datos) y dos puntos.

En la línea 2 se define el cuerpo de la función compuesto por una única sentencia indentada a la derecha por cuatro espacios. Esta sentencia imprime el mensaje "Hola mundo!".

En la línea 4 se realiza una llamada a la función. Para ello se escribe el nombre de la función seguido de unos paréntesis vacios (sin argumentos, porque la función no recibe datos).

Función con parámetros

Veamos ahora una función que acepta un parámetro para realizar su tarea:

1
2
3
4
5
def imprimir(mensaje):
    print(mensaje)

imprimir("Hola")
imprimir("Como estas?")

Resultado:

Hola mundo!

En la línea 1 vemos que en la cabecera de la función, entre paréntesis, se especifica que la función recibe un parámetro llamado mensaje. Es una variable con datos que la función utilizará para realizar su tarea, que en este caso es tan simple como sacar el mensaje por Consola.

En las líneas 4 y 5 vemos dos llamadas independientes a la misma función. En cada una de ellas se le pasa un argumento a la función entre paréntesis. Ese argumento se asignará al parámetro correspondiente de la función.

Valores de retorno

Una función puede devolver un valor al código que la ha invocado. Esto se realiza usando dentro del cuerpo de la función la palabra clave return seguida del valor que se desea retornar. Cuando en la ejecución de una función se encuentra esta palabra clave, la ejecución de la función termina y se devuelve el valor especificado. El código que ha llamado a la función puede hacer uso de ese valor.

1
2
3
4
5
6
def eleva_al_cubo(n):
    return n * n * n

c = eleva_al_cubo(3) # Guarda el valor de retorno en una variable
print(c)
print(eleva_al_cubo(4)) # Manda el valor de retorno a otra función

Resultado:

27
64

docstring

Es opcional incluir un string al inicio de la función para documentarla (su uso, los parámetros que espera recibir, etc).

Además de clarificar el código, algunas herramientas de desarrollo lo muestran para proporcionar información sobre la función. También podemos acceder a ese comentario desde nuestro código escribiendo el nombre de la función, un punto y el nombre de variable __doc__ (dos guiones bajos al principio y dos al final).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def leer_entero(mensaje):
    """Muestra el mensaje al usuario para pedir un dato entero.
Si el usuario introduce un dato que no es entero
se muestra un mensaje de error y se vuelve a pedir
el dato al usuario. """

    dato = input(mensaje)
    while not dato.isnumeric():
        print("Introduzca un número entero.")
        dato = input(mensaje)
        return int(dato)

print(leer_entero.__doc__)

Resultado:

Muestra el mensaje al usuario para pedir un dato entero.
Si el usuario introduce un dato que no es entero
se muestra un mensaje de error y se vuelve a pedir
el dato al usuario.

Si invocamos a la función este es el resultado:

1
2
n = leer_entero("Introduzca edad:")
print(f"Su edad es {n} años.")

Resultado:

Introduzca edad: veinte
Introduzca un número entero.
Introduzca edad: 20
Su edad es 20 años.

Parámetros

Los parámetros se utilizan para pasar información a una función.

  • Un parámetro es una variable definida en la cabecera de la función y se utiliza para hacer el dato accesible dentro del cuerpo de la función.

  • Un argumento es el valor del dato que se le pasa a una función en el momento de llamarla. Este valor se almacena en el parámetro.

Una función puede tener ninguno, uno o varios parámetros. Si tiene varios parámetros, estos deben ir separados por comas. Cuando invoquemos a la función, será necesario pasar un número equivalente de argumentos en la llamada, también separados por comas. La correspondencia entre los argumentos que proporcionemos y los parámetros de la función será por orden (el primero con el primero, el segundo con el segundo, etc).

1
2
3
4
def alertar_usuario(nombre, mensaje):
    print(f'Atención {nombre}, {mensaje}')

alertar_usuario('Kate', 'se ha detectado un virus')

Resultado:

Atención Kate, se ha detectado un virus

Parámetros por defecto

Podemos especificar valores por defecto para los parámetros en la definición de la función. Cuando un paramétro tiene un valor por defecto, si al llamar la función se especifica un valor para el parámetro se utilizará ese valor, sino se utilizará el valor por defecto.

1
2
3
4
5
def alertar_usuario(nombre, mensaje='algo inusual esta ocurriendo'):
    print(f'Atención {nombre}, {mensaje}')

alertar_usuario('Kate')
alertar_usuario('Kate', 'queda poco espacio de almacenamiento')

Resultado:

Atención Kate, algo inusual esta ocurriendo
Atención Kate, queda poco espacio de almacenamiento

En el ejemplo nombre es un parámetro obligatorio, mientras mensaje es un parámetro opcional porque ya tiene un valor por defecto.

Dentro de la lista de parámetros de una función, cualesquiera de ellos pueden tener un valor por defecto pero, si uno tiene un valor por defecto, todos los que vayan a continuación tienen que tener también un valor por defecto. Esto es porque al hacer la correspondencia por orden, una vez que uno se omita no podrían venir luego otros que fueran obligatorios.

Argumentos con nombre

Normalmente se utiliza la posición para asignar cada valor de un argumento a un parámetro. A este tipo de argumentos les denominaremos argumentos posicionales.

Pero si existen varios parámetros con valores por defecto y se desea asignar un valor a unos y dejar el valor por defecto para otros, puede no ser posible hacerlo basandonos en el orden de los mismos.

Para solucionarlo utilizaremos argumentos con nombre (named arguments) también llamados argumentos con palabra clave (keyword arguments). A la hora de proporcionar estos argumentos indicaremos el nombre del parámetro al que hay que asignar el valor. De esta forma el orden ya no tiene relevancia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def alertar_usuario(nombre,
                    mensaje='algo inusual esta ocurriendo',
                    importancia='Baja',
                    medidas='Ninguna'):
    print(f'Atención {nombre}, {mensaje}')
    print(f'La importancia es {importancia}')
    print(f'Medidas adoptadas: {medidas}')

alertar_usuario(nombre = 'Kate',
                mensaje = 'queda poco espacio de almacenamiento',
                medidas = 'eliminación de archivos temporales')

Resultado:

Atención Kate, queda poco espacio de almacenamiento
La importancia es Baja
Medidas adoptadas: eliminación de archivos temporales

Se pueden mezclar argumentos "posicionales" con argumentos "con nombre". La única regla a seguir es que no puede ir un argumento posicional a continuación de otro por nombre. Los argumentos posicionales iniciales van por orden y cuando aparece el primer argumento con nombre, ya el resto tiene que ser por nombre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def alertar_usuario(nombre,
                    mensaje='algo inusual esta ocurriendo',
                    importancia='Baja',
                    medidas='Ninguna'):
    print(f'Atención {nombre}, {mensaje}')
    print(f'La importancia es {importancia}')
    print(f'Medidas adoptadas: {medidas}')

alertar_usuario('Kate', 'queda poco espacio de almacenamiento',
                medidas = 'eliminación de archivos temporales')

Resultado:

Atención Kate, queda poco espacio de almacenamiento
La importancia es Baja
Medidas adoptadas: eliminación de archivos temporales

Argumentos arbitrarios

Python permite pasar un número arbitrario de argumentos a una función y, dentro de la función, tratarlos como una lista.

Para especificar una lista arbitraria de parámetros, es necesario marcar el parámetro con un asterisco. Habitualmente se le llama al parámetro *args.

1
2
3
4
5
def saluda(*args):
    for nombre in args:
        print('Hola,', nombre)

saluda('Smith', 'Murdock', 'Peck', 'Baracus')

Resultado:

Hola, Smith
Hola, Murdock
Hola, Peck
Hola, Baracus

Además de especificar una lista arbitraria de argumentos por posición, es posible especificar también una lista arbitraria de argumentos por nombre (keyword arguments). Para ello utilizaremos dos asteriscos antes del nombre del parámetro. Habitualmente se le llama al parámetro **kwargs ([k]ey[w]ord[arg]ument[s]).

1
2
3
4
5
6
7
def equipo(*args, **kwargs):
    for arg in args:
        print('argumento:', arg)
    for key in kwargs.keys():
        print('clave:', key, 'valor:', kwargs[key])

equipo('Smith', 'Murdock', teniente='Peck', sargento='Baracus')

Resultado:

argumento: Smith
argumento: Murdock
clave: teniente valor: Peck
clave: sargento valor: Baracus

Podemos utilizar funciones que solo tengan listas arbitrarias de argumentos por posición o bien que solo tengan listas arbitrarias de argumentos por nombre.

Funciones anónimas

Las funciones normalmente tienen un nombre. Mediante este nombre podemos invocar su ejecución tantas veces como sea necesario. Pero en ocasiones queremos utilizar una función una sola vez. Para ello Python proporciona las funciones anónimas (anonymous function).

Una función anónima es aquella que no tiene un nombre y que solamente se puede usar en el lugar donde se define.

Una función anónima se define con la palabra clave lambda. El formato para definirla es el siguiente:

1
lambda arguments: expression

Las funciones anónimas pueden tener cualquier número de argumentos pero solamente una expresión. Cuando la expresión se ejecuta, el valor generado se devuelve como resultado de la función.

1
double = lambda i : i * i

En este ejemplo hemos creado una función anónima que recibe un parámetro i y devuelve el resultado de elevar al cuadrado i. Hemos almacenado esa función anónima en la variable double. A partir de este momento podemos invocar la función anónima de la siguiente forma: double(10).

Veamos otros ejemplos de funciones anónimas:

1
2
3
4
5
6
7
8
9
f0 = lambda: print('sin argumentos')
f1 = lambda x: x * x
f2 = lambda x, y: x * y
f3 = lambda x, y , z: x + y + z

f0()
print(f1(10))
print(f2(4, 3))
print(f3(1, 2, 3))

Resultado:

sin argumentos
100
12
6

Recursividad

La recursividad es una técnica muy efectiva cuando un problema puede descomponerse en problemas más pequeños que tienen la misma forma de resolverse que el problema inicial. Y, a su vez, estos nuevos problemas pueden también descomponerse en otros y así sucesivamente.

Una solución recursiva consiste habitualmente en una función que se llama a sí misma una o más veces para solucionar un problema. En muchas ocasiones esto se combina con el estado actual de los datos en la función para devolver un resultado.

Una función recursiva es una función que se llama a si misma. Para que no resulte en un proceso infinito, debe de existir una condición de finalización para el proceso. Esto supone tener una condición bajo la cual la función ya no se llama a si misma y en su lugar devuelve un valor. La condición de terminación puede ser porque:

  • Se ha encontrado una solución.
  • El problema descompuesto es ya tan sencillo que se puede solucionar directamente, sin recursividad.
  • Se alcanza un nivel de repetición máximo que detiene el proceso aunque no se obtenga una solucion al problema.

Para ententer mejor la recursividad veamos un ejemplo basado en el cálculo del factorial de un número.

Para calcular el factorial del número 4 hay que realizar la multiplicación de 4 * 3 * 2 * 1 = 24.

Una función iterativa para solucionar el cálculo del factorial sería:

1
2
3
4
5
6
7
8
9
def factorial(n):
    f = 1
    for i in range(n, 1, -1):
        f *= i
    return f

n = 4
f = factorial(n)        
print(f"El factorial de {n} es {f}")

Resultado:

El factorial de 4 es 24

La alternativa recursiva a la solución del factorial sería la siguiente:

1
2
3
4
5
6
7
8
9
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n - 1)

n = 4
f = factorial(n)        
print(f"El factorial de {n} es {f}")

Resultado:

El factorial de 4 es 24

Vemos como la solución recursiva es muy elegante. La función resuelve un caso base (el factorial de 1 que se sabe que es 1). Para el resto de los casos, vuelve a invocarse a si misma con un problema más sencillo (calcular el factorial de un número menos).

Quizás el caso del factorial no sea el adecuado para aplicar un método recursivo, pues no simplifica mucho la alternativa iterativa y tampoco ofrece un excesivo aumento de claridad, pero es un buen caso para entender como funciona el concepto.

El beneficio clave de la recursividad es que algunos algoritmos se expresan más elegántemente y con menos código con una aproximación recursiva que con una aproximación iterativa. De esta forma el código es más fácil de escribir y más fácil de leer.

Como principal desventaja hay que destacar que, en ocasiones, la solución recursiva no es tan eficiente como la iterativa. Una llamada a una función es mas costosa que un repetición de un ciclo for. Cada llamada a una función necesita alojar memoria para las variables locales de la función en esa llamada.

Ambito de variables

Las variables que se definen fuera de cualquier función se denoniman variables globales y están disponibles para cualquier parte del código. Las variables que se definen dentro de una función se denominan variables locales y su ámbito de vida y visibilidad está comprendido únicamente al código de la función donde se definen.

1
2
3
4
5
6
7
a = 10          # variable global
def imprimir():
    b = 20      # variable local
    print('funcion:', a)
    print('funcion:', b)
imprimir()
print(a)

Resultado:

funcion: 10
funcion: 20
10

En el ejemplo se define la variable a como una variable global accesible desde cualquier parte del programa (incluso dentro de la función imprime). También se define la variable b como variable local a la función, es decir, no esta disponible fuera de esta.

Cuando definimos una variable local con el mismo nombre que una global, Python lleva cada una de forma independiente y pueden coexistir. Dentro de la función se utiliza la local que tiene prioridad sobre la global.

1
2
3
4
5
6
a = 10          # variable global
def imprimir():
    a = 20      # variable local
    print('funcion:', a)
imprimir()
print(a)

Resultado:

funcion: 20
10

Pero, si dentro de la función, en lugar de querer crear una variable local llamada a lo que queremos es modificar la variable global, necesitamos especificarlo de alguna forma. Para ello utilizaremos la palabra clave global con el nombre de la variable global que queremos utilizar.

1
2
3
4
5
6
7
a = 10          # variable global
def imprimir():
    global a
    a = 20      # variable global
    print('funcion:', a)
imprimir()
print(a)

Resultado:

funcion: 20
20

En Python se pueden definir funciones dentro de otras funciones. En este caso, cada función tiene sus variables locales y puede acceder tanto a las globales como a las de sus funciones superiores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
a = 10          # variable global
def externa():
    b = 20      # variable local en externa
    def interna():
        c = 30  # variable local en interna
        print('interna:', a)
        print('interna:', b)
        print('interna:', c)
    interna()
    print('externa:', a)
    print('externa:', b)
externa()
print(a)

Resultado:

interna: 10
interna: 20
interna: 30
externa: 10
externa: 20
10

En este escenario también se pueden nombrar variables locales en una función igual que las variables locales de la función superior y Python tratará ambas de forma independiente.

1
2
3
4
5
6
7
8
def externa():
    a = 10      # variable local en externa
    def interna():
        a = 20  # variable local en interna
        print('interna:', a)
    interna()
    print('externa:', a)
externa()

Resultado:

interna: 20
externa: 10

Puede darse el caso que queramos modificar una variable de una función superior (sin llegar al ambito global que es externo a todas las funciones). En ese caso utilizaremos la palabra clave nonlocal para señalar que deseamos utilizar una variable ya existente en un ámbito superior (en lugar de definir una nueva variable local). Python se encargará de realizar una busqueda hacia arriba para localizar la variable en una función superior.

1
2
3
4
5
6
7
8
def externa():
    a = 10      # variable local en externa
    def interna():
        a = 20  # variable local en interna
        print('interna:', a)
    interna()
    print('externa:', a)
externa()

Resultado:

interna: 20
externa: 20

Funciones como objetos

Cuando definimos una función, estamos creando un objeto en memoria que representa a la función y asignamos ese objeto a una variable (el nombre de la función). Cuando llamamos a la función estamos obteniendo el objeto almacenado en la variable con el nombre de la función y ejecutando su código (eso lo hacen los paréntesis después del nombre de la función).

1
2
3
4
5
6
7
def saludo():
    return "Hola"

mensaje = saludo()
print(mensaje)
print(type(mensaje))
print(type(saludo))

Resultado:

Hola
<class 'str'>
<class 'function'>

Como cualquier dato almacenado en una varible, podemos asignarselo a otra variable o podemos cambiar su valor por otro. En el caso de una función esto implica definir otra variable que apunte a la misma función o modificar la variable para que apunte a otra función respectivamente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def saludo1():
    return "Hola"

def saludo2():
    return "Hello"

nuevo_saludo = saludo1
print(nuevo_saludo())

saludo1 = saludo2
print(saludo1())
print(saludo2())
print(nuevo_saludo())

Resultado:

Hola
Hello
Hello
Hola

En la línea 7 del ejemplo estamos haciendo que la nueva variable nuevo_saludo apunte a la misma función a la que apunta la variable saludo1 (que es el nombre de la función). A partir de es momento podemos invocar esa función con la variable nuevo_saludo seguida de paréntesis.

En la línea 10 estamos haciendo que la variable saludo1 que antes apuntaba a la primera función apunte ahora a la segunda función (la función a la que apunta saludo2). A partir de ese momento ambas apuntan a la misma función. Esto no afecta a la variable nuevo_saludo que sigue apuntando a la primera función.

Funciones de orden superior

Dado que las funciones no son más que objetos que se almacenan en variables. Es posible enviar esas funciones como parámetros de otras funciones. A esas funciones que reciben y/o devuelven otras funciones como parámetros se las denomina funciones de orden superior (higher order functions).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def cuadrado(n):
    return n * n

def cubo(n):
    return n * n * n

def mas_uno(n):
    return n + 1

def aplicar(n, f):
    return f(n)

print(aplicar(3, cuadrado))
print(aplicar(3, cubo))
print(aplicar(3, mas_uno))

Resultado:

print(aplicar(3, cuadrado))
print(aplicar(3, cubo))
print(aplicar(3, mas_uno))

En el ejemplo, existen varias funciones que reciben un parámetro numérico y devuelven un valor después de realizar ciertas operaciones. Existe también otra función llamada aplicar que es una función de orden superior ya que recibe un parámetro numérico y otro parámetro que es una función. Aplica la función al parámetro numérico y devuelve el resultado.

También es posible crear funciones que devuelvan otras funciones. Veamos un ejemplo en el que se crean varias funciones anónimas en función de un string que indica cual es el objetivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def obtener_validador(tipo):
    if tipo == 'par':
        return lambda n: n % 2 == 0
    elif tipo == 'positivo':
        return lambda n: n >= 0
    else:
        raise ValueError('Tipo no reconocido')

f1 = obtener_validador('par')
f2 = obtener_validador('positivo')

print(f1(-4))
print(f2(-4))

Resultado:

True
False

También es posible devolver una función con nombre creada dentro de otra función. Veamos un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def obtener_validador(tipo):
    def es_primo(n):
        for i in range(2, n):
            if(n % i == 0):
                return False
        return True

    if tipo == 'primo':
        return es_primo

f = obtener_validador('primo')

print(f(4))
print(f(7))

Resultado:

False
True

Currificar funciones

Dada una función existente, una función currificada (curried function) a partir de la misma se obtiene cuando se fija el valor de algunos de los parámetros (de uno o de varios). Es decir, se obtiene una función equivalente pero con menos parámetros que la original. Veamos un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def multiplicar(a, b):
    return a * b

def multiplicar_por(f, n):
    return lambda y: f(n, y)

doblar = multiplicar_por(multiplicar, 2)
triplicar = multiplicar_por(multiplicar, 3)

print(doblar(10))
print(triplicar(10))

Resultado:

20
30

En la línea 1 se define una función multiplicar que devuelve la multiplicación de dos números. En la línea 4 se define una función que recibe una función y un número y devuelve otra nueva función que es igual que la que ha recibido pero fijando ese primer parámetro al número. Para crear esta nueva función utiliza una función anónima.

En las líneas 7 y 8 se obtienen dos nuevas funciones doblary triplicar haciendo uso de la función multiplicar_por que se encarga de fijar el primer parámetro de la función multiplicar.