Contenido del capítulo

En esta completa guía se explican muchas cosas sobre las funciones en Python, como las definidas con def para reutilizar código, el uso de lambda para crear funciones pequeñas y anónimas, los decoradores que modifican el comportamiento de las funciones y las generadoras, que devuelven valores de forma perezosa usando yield para optimizar el uso de memoria.

Duración estimada de todo el contenido:
Duración del vídeo:
Contiene 28 ejercicios Contiene 15 vídeos.
Contiene 1 mini proyecto.
Tabla de contenidos
Logo

    Las funciones de Python a fondo

    Empieza este capítulo con la base más fundamental sobre las funciones en Python, y ves progresando a lo largo de todo el capítulo.

    Las funciones Python en 5 minutos
    Las funciones Python en 5 minutos

    Espacio publicitario

    ¿Qué es una función?

    Una función es un bloque de código que se ejecuta al ser llamado. En tales llamadas, puede haber una serie de argumentos.

    Además, las funciones pueden ser utilizadas para devolver valores dentro del programa, con el fin de procesarlos y utilizarlos en él.

    Sintaxis de una función

    def nombre_función(parámetros):
        # bloque de código
        expresión de retorno

    Las funciones se declaran con la palabra reservada def, les damos un nombre, y entre sus paréntesis, ponemos opcionalmente el número de parámetros que queramos.

    Después de los dos puntos, indentaremos (tabularemos) un bloque con el código de la función, y finalmente, como última instrucción, la expresión de retorno con la palabra return. Esta expresión, al igual que los parámetros, también es opcional. Si no quieres devolver valores, no la tienes que utilizar.

    Crear una función

    Vamos a crear una función muy simple:

    def funcion():
        print("¿Me has llamado?")

    Llamar a una función

    Para ejecutar el código de la función, realizaremos una llamada con su nombre y unos paréntesis:

    funcion()
    Resultado en la consola
    ¿Me has llamado?

    Función con bloque de código vacío

    Mientras vas desarrollando la estructura completa del código, puede ser que necesites dejar declaradas las funciones, pero todavía sin el código correspondiente.

    Como todo elemento con dos puntos en Python (:), debes tener un bloque obligatorio indentado:

    def funcion():
    Error en la consola
    IndentationError: expected an indented block after function definition on line X
    Error de indentación: se esperaba un bloque indentado después de la definición en la línea X

    En estos casos, utiliza la palabra reservada pass, para dejar la función vacía:

    def funcion():
        pass

    O bien, añade un comentario docstring:

    def funcion():
        """Añadir código más adelante."""

    Espacio publicitario

    Parámetros en las funciones

    En muchas ocasiones utilizaremos parámetros para poder pasar valores a las funciones. A estos valores se les denominan argumentos.

    Veamos un ejemplo:

    def suma(a, b):
        print(a + b)
    
    suma(10,56)
    Resultado en la consola
    66

    Que te quede clara la diferencia:

    • Parámetro es la variable que lleva la declaración de la función. En el ejemplo anterior, a y b son parámetros.
    • Argumento es el valor que se le pasa a la variable de la función. En el ejemplo anterior, 10 y 56 son argumentos.

    Ahora, si intentas utilizar el resultado de la suma anterior en tu programa, no lo conseguirás, ya que está diseñada solo para imprimir valores en la consola:

    def suma(a, b):
        print(a + b)
    
    resultado = suma(10,56)
    
    
    print(f"El resultado es: {resultado}.")
    Resultado en la consola
    None

    El resultado es un valor nulo que indica que la función no está devolviendo ningún valor.

    Espacio publicitario

    Devolver valores

    Para devolver valores, utilizaremos la antes mencionada palabra reservada return.

    La función del ejemplo anterior, puede ser modificada para que cumpla con este propósito:

    def suma(a, b):
        return a + b
    
    resultado = suma(10,56)
    
    print(f"El resultado es: {resultado}.")
    Resultado en la consola
    El resultado es: 66.

    Tipos de argumentos en las funciones de Python

    Analicemos los tipos de argumentos que utiliza Python en las funciones. Conocerlos bien, es necesario para poder aprovecharlas como es debido.

    • Argumentos posicionales
    • Argumentos de clave
    • Argumentos por defecto
    • Argumentos arbitrarios posicionales
    • Argumentos arbitrarios de clave
    • Argumentos con expresión (estos no son un tipo realmente, se pueden aplicar a cualquiera de los otros)

    Veamos ejemplos prácticos con cada uno de ellos.

    Argumentos posicionales

    Tipos de argumentos en Python: posicionales
    Tipos de argumentos en Python: posicionales

    Los argumentos posicionales son los más típicos a la hora de realizar llamadas.

    Argumentos posicionales en funciones de Python

    Este tipo de argumentos, son los que se correlacionan de forma posicional con la declaración de parámetros. Aquí tienes un ejemplo:

    def suma(a, b):
        return a + b
    
    resultado = suma(10,56)

    En el ejemplo, 10 y 56 son argumentos posicionales. Se pasan en orden de posición a la función.

    Por lo tanto, el valor 10 (valor 1) se le pasa al parámetro a (parámetro 1) y el valor 56, al parámetro b (parámetro 2).

    A los argumentos posicionales se les denomina en inglés como positional arguments.

    Espacio publicitario

    Argumentos de clave

    Tipos de argumentos de Python: de clave
    Tipos de argumentos de Python: de clave

    Los argumentos de clave son los que se especifican como pares de clave y valor.

    Argumentos de clave en Python

    En este caso, en la llamada se hace referencia explícitamente a que parámetro (clave), va a ir asociado un valor.

    Aquí tienes un ejemplo:

    def suma(a, b):
        return a + b
    
    resultado = suma(b=56, a=10)
    
    print(resultado)
    Resultado en la consola
    66

    En el ejemplo, se puede comprobar como el orden de especificación de argumentos no importa.

    No hace falta que cruces los argumentos como en la imagen, utiliza el orden que quieras.

    A los argumentos de clave se les denomina en inglés como keyword arguments.

    Espacio publicitario

    Argumentos por defecto

    Tipos de argumentos de Python: por defecto
    Tipos de argumentos de Python: por defecto

    Los argumentos por defecto son valores que se asignan a los parámetros de una función de manera predeterminada. Esto significa que si se llama a la función sin proporcionar un valor para el parámetro, se utilizará el valor predeterminado.

    Argumentos por defecto en Python

    En este caso, los parámetros por defecto, irán después de los normales, en la declaración de la función.

    Por ejemplo, no puedes hacer esto:

    def suma(a=10, b):
    Error en la consola
    SyntaxError: parameter without a default follows parameter with a default
    Error de sintaxis: un parámetro sin un valor por defecto, sigue a un parámetro con un valor por defecto

    Lo que el error indica, es que no se puede poner un parámetro sin valor por defecto, después de cualquier otro que sí lo tenga. El orden correcto será primero los parámetros normales, y luego los que tienen valores por defecto.

    Aquí tienes un ejemplo de uso correcto:

    def suma(a, b=56):
        return a + b
    
    resultado = suma(10)
    
    print(f"El resultado es: {resultado}.")
    Resultado en la consola
    El resultado es: 66.

    Si quieres pasar otro valor que no sea el que viene por defecto, para el parámetro por defecto, hazlo indicándolo posicionalmente:

    resultado = suma(10,103)
    Resultado en la consola
    El resultado es: 113.

    O también, con argumentos de clave en el orden que quieras:

    resultado = suma(b=10,a=103)

    Python es muy flexible con las funciones.

    A los argumentos por defecto se les denomina en inglés como default arguments.

    Espacio publicitario

    Argumentos arbitrarios posicionales: *args

    Argumentos arbitrarios posicionales *args en Python
    Argumentos arbitrarios posicionales *args en Python

    Gracias a este tipo de argumentos, podremos crear funciones que sirvan para obtener tantos argumentos posicionales como queramos en una llamada a una función. Para aplicar esta funcionalidad, solo tienes que declarar como argumento esta palabra: *args.

    def crear_lista(*args):
        # Creamos una lista vacía.
        lista = []
    
        # Añadimos los datos a la lista.
        for dato in args:
            lista.append(dato)
    
        return lista
    
    # Llamamos a la función.
    llamada = crear_lista(7,45,32,134,563,23)
    
    # Imprimimos la lista.
    print("Los valores en la lista son:", llamada)
    Resultado en la consola
    Los valores en la lista son: [7, 45, 32, 134, 563, 23]

    En este código, gracias al parámetro especial *args, he podido pasar tantos valores como he querido en la llamada de la función.

    Aquí puedes ver lo que ocurre con varias llamadas de diferente número de argumentos:

    · · · Resto código · · ·
    
    # Llamamos a la función.
    llamada_1 = crear_lista(7,45,32,134,563,23)
    llamada_2 = crear_lista(65,67)
    llamada_3 = crear_lista(6)
    
    # Imprimimos la lista.
    print(llamada_1)
    print(llamada_2)
    print(llamada_3)
    Resultado en la consola
    [7, 45, 32, 134, 563, 23]
    [65, 67]
    [6]

    Gracias a *args, podemos crear funciones mucho más flexibles y adaptables a diversas circunstancias.

    Del código anterior, fíjate en el bucle for, en él estoy iterando un elemento llamado args:

        # Añadimos los datos a la lista.
        for dato in args:
            lista.append(dato)

    Esto quiere decir que el parámetro args es un iterable y tiene posiciones de índice.

    Vamos a hacer una prueba de tipo de dato:

    def funcion(*args):
        # Imprimimos la tupla generada
        print(args)
        # Comprobamos el tipo de dato que es
        print(type(args))
    
    funcion(10,20)
    Resultado en la consola
    (10, 20)
    <class 'tuple'>

    Efectivamente, se trata de un iterable. En concreto de una tupla.

    Espacio publicitario

    Esta tupla almacena todos los argumentos que le pasamos en la llamada, así que en realidad este parámetro especial no es mágico, sino que en realidad, le pasamos un solo argumento, la tupla entera.

    En este ejemplo, estoy emulando el comportamiento de *args, sin utilizarlo, claro:

    def funcion(mi_args):
        # Imprimimos la tupla generada
        print(mi_args)
        # Comprobamos el tipo de dato que es
        print(type(mi_args))
    
    tupla = (10,20)
    
    funcion(tupla)
    Resultado en la consola
    (10, 20)
    <class 'tuple'>

    En su lugar, he conseguido emularlo utilizando una tupla.

    Esto es solo una emulación para que veas como funciona internamente *args. ¿Quiere decir que *args entonces es prescindible? No. El parámetro especial *args emplea un tupla con cualquier valor que pasemos como argumento, por ejemplo, que pasas un solo int, *args lo gestiona y crea una tupla con un solo int. Si pasas 10 valores de cualquier tipo, hace lo mismo, en cada llamada.

    En conclusión, *args te evita de tener que estar creando tuplas y añadiendo los valores fuera de las funciones.

    *args como convención

    *args realmente es un nombre de convención. Si pones cualquier otro nombre, funcionará. Lo que le importa al intérprete, es que le pongas el asterisco antes que el nombre.

    Mira el siguiente ejemplo:

    def funcion(*objetos):
        # Imprimimos la tupla generada
        print(*objetos)
    
    funcion("hola", 34.6, 19, True)
    Resultado en la consola
    hola 34.6 19 True

    Si decides no seguir la convención de llamarlo *args, especifícalo muy bien en la documentación, aunque realmente, quien sabe programar en Python, ya conoce de inmediato que *objetos es lo mismo que *args, por el asterisco.

    Espacio publicitario

    La función predefinida print() de Python

    A continuación tienes un fragmento resumido de algunos parámetros que tiene la función print() de Python en su código interno:

    def print(*values: object, sep:" ", end:"\n")

    Fíjate en el primer parámetro (*values).

    Efectivamente, se trata del parámetro especial *args, solo que en esta función le han dado el nombre de *values, ya que eso es lo que espera, una serie de valores. Podemos decir que es un nombre semántico, que trata de describir con más precisión lo que espera el parámetro.

    Gracias a este primer parámetro, podemos pasar todos los objetos y expresiones como queramos a la llamada de la función print(). Por ejemplo:

    print("hola", 34.6, 19, True)
    Resultado en la consola
    hola 34.6 19 True

    *args siempre como último parámetro

    Junto con el parámetro especial *args, puedes utilizar perfectamente argumentos de otros tipos. Puedes apreciarlo en el siguiente ejemplo:

    def funcion(a, b, *args):
        print(a, b, args)
    
    funcion(1, 4, 5, 23, 45)
    Resultado en la consola
    1 4 (5, 23, 45)

    El valor 1 y 4, se pasan posicionalmente a los parámetros a y b respectivamente. Todos los argumentos que vengan después, pasarán a formar parte de la tupla de *args.

    Args en Python

    Si añades un parámetro después de *args e intentas proporcionar un argumento posicional para este, recibirás un error. Esto es porque todos los argumentos posicionales se pasarán a *args.

    Aquí puedes ver un ejemplo:

    def funcion(a, b, *args, c):
        print(a, b, args, c)
    
    funcion(1, 4, 5, 23, 45, 50)
    Error en la consola
    TypeError: funcion() missing 1 required keyword-only argument: 'c'
    Error de tipo: a la llamada de funcion(), le falta un argumento obligatorio de solo clave: 'c'

    El error indica que en la llamada nos falta un argumento de tipo clave. Ahí nos revela lo que tenemos que hacer para conseguir utilizar la función de esta forma.

    En el siguiente ejemplo, le doy un argumento por defecto (de clave), para el parámetro c.

    def funcion(a, b, *args, c=10):
        print(a, b, args, c)
    
    funcion(1, 4, 5, 23, 45)
    Resultado en la consola
    1 4 (5, 23, 45) 10

    De esta forma, no hace falta pasar ese argumento. No da error.

    Espacio publicitario

    Si quieres pasar un argumento para el parámetro c en la llamada, lo tendrás que hacer como argumento de clave:

    def funcion(a, b, *args, c=10):
        print(a, b, args, c)
    
    funcion(1,4,5,23,45,c=1500)
    Resultado en la consola
    1 4 (5, 23, 45) 1500

    A los argumentos arbitrarios posicionales se les denomina en inglés como arbitrary positional arguments

    Expresiones como argumentos

    Expresiones como argumentos en Python
    Expresiones como argumentos en Python

    Podemos pasar expresiones de todo tipo como argumentos. Estas se resolverán primero, y sus resultados serán pasados como argumentos en las llamadas de las funciones.

    Aquí tienes un ejemplo:

    def suma(a):
        print(f"El resultado es: {a}.")
    
    suma(10+20)
    Resultado en la consola
    El resultado es: 30.

    Aquí lo que ocurre es que se resuelve primero la expresión, y después se pasa el resultado como argumento posicional, que nos deja realmente una llamada como esta:

    suma(30)

    Estas expresiones se pueden utilizar también con los otros tipos de argumentos. Por ejemplo:

    def resolver_expresion(a):
        print(f"El resultado de la expresión es: {a}.")
    
    resolver_expresion(a = 10 == 10)
    Resultado en la consola
    El resultado de la expresión es: True.

    En este ejemplo he utilizado un argumento de clave, y le he asignado una expresión booleana, que es lo mismo que hacer esto:

    resolver_expresion(a = True)

    Espacio publicitario

    Argumentos arbitrarios de clave: **kwargs

    Argumentos arbitrarios de clave **kwargs en Python
    Argumentos arbitrarios de clave **kwargs en Python

    Llegamos al segundo tipo de argumentos arbitrarios, **kwargs.

    **kwargs, es parecido a *args. La diferencia principal es que nos permite pasar un diccionario de argumentos, en lugar de una tupla de valores.

    Esto nos será útil para trabajar con argumentos de clave arbitrarios, en las llamadas de las funciones.

    Por ejemplo, podemos hasta construir un diccionario mediante argumentos de clave, con cuantos pares clave-valor queramos:

    def diccionario(**kwargs):
        print(kwargs)
    
    diccionario(nombre="Gabriela", 
                apellidos="Gómez de la barca", 
                edad="27")
    Resultado en la consola
    {'nombre': 'Gabriela', 'apellidos': 'Gómez de la barca', 'edad': '27'}

    **kwargs como convención

    **kwargs, también es un nombre de convención. Lo importante para que el intérprete de Python entienda que quieres usarlo, es especificar los dos asteriscos (**). El resto puede llamarse como quieras:

    def diccionario(**pares):
        print(pares)
    
    diccionario(nombre="Gabriela", 
                apellidos="Gómez de la barca", 
                edad="27")

    ¿Se puede utilizar *args junto con **kwargs?

    Sí, se puede utilizar. Aquí tienes un ejemplo:

    def datos(*args, **kwargs):
        print(args)
        print(kwargs)
        
    usuario1 = {"nombre":"Gabriela", 
                "apellidos":"Gómez de la barca", 
                "edad":"27"}
        
    datos(10,50,60, **usuario1)
    Resultado en la consola
    (10, 50, 60)
    {'nombre': 'Gabriela', 'apellidos': 'Gómez de la barca', 'edad': '27'}

    Primero, le damos todos los argumentos que necesitemos en la llamada, la parte del *args. Después, se le pasa un diccionario al parámetro **kwargs. El intérprete de Python “entiende” lo que queremos hacer.

    Espacio publicitario

    Debes seguir este orden, primero *args y luego **kwargs. Si lo pones al revés, produces un error:

    def datos(**kwargs, *args):
    Error en la consola
    SyntaxError: arguments cannot follow var-keyword argument
    Error de sintaxis: los argumentos no pueden seguir a un argumento de clave variable (**kwargs)

    Cuando utilices Python para un propósito concreto, encontrarás un montón de usos para **kwargs. Por ejemplo, si trabajas con bases de datos, podrías crear un diccionario que llevara los datos de conexión, como este:

    conexion = {
        "host": "localhost",
        "user": "root",
        "password": "1234",
        "database": "bd",
    }

    Este diccionario se le pasaría con **kwargs como argumento, a una función preparada para conectar con un servidor como el de MySQL.

    Diccionarios y el método items()

    Con los diccionarios tenemos un método muy práctico para recibir las claves y los valores de una forma muy curiosa. Estoy hablando del método items().

    En este ejemplo puedes ver el tipo de dato y el valor almacenado en la variable:

    argumentos = {"Nombre": "Enrique", 
                  "Edad": 32, 
                  "Telefono": "123456789"}
    
    print(type(argumentos.items()))
    print(argumentos.items())
    Resultado en la consola
    <class 'dict_items'>
    dict_items([('Nombre', 'Enrique'), ('Edad', 32), ('Telefono', '123456789')]) 

    Este método nos devuelve un objeto especial que se suele denominar como objeto de vista iterable. Estoy hablando de dict_items. El objeto contiene una lista con tuplas con cada par clave-valor de un diccionario.

    Gracias al uso de dos iteradores, podemos iterar con un solo bucle, cada parte individual de cada par.

    Bucle con doble iterador para **kwargs

    Bucles con múltiples iteradores en Python
    Bucles con múltiples iteradores en Python

    Algo que harás muy a menudo con **kwargs, es la utilización de items() y dos iteradores en un mismo bucle.

    Espacio publicitario

    Por ejemplo:

    def info(**kwargs):
        for clave, valor in kwargs.items():
            print(f"Clave: {clave}, Valor: {valor}")
    
    argumentos = {"Nombre": "Enrique", 
                  "Edad": 32, 
                  "Teléfono": "123456789"}
    
    info(**argumentos)
    Resultado en la consola
    Clave: Nombre, Valor: Enrique
    Clave: Edad, Valor: 32
    Clave: Teléfono, Valor: 123456789
    **kwargs y el método items() de Python
    **kwargs y el método items() de Python

    En el ejemplo que acabas de ver, estoy pasándole a la función el diccionario llamado argumentos.

    En el bucle for tengo los iteradores clave y valor separados por una coma.

    El bucle empieza a iterar el objeto dict_items.

    En la primera ejecución entramos en la primera tupla. Se le pasa la clave "Nombre" al iterador clave, y el valor "Enrique" al iterador valor.

    Hará lo mismo con cada tupla que haya en el objeto dict_items.

    A los argumentos arbitrarios de clave se les denomina en inglés como arbitrary keyword arguments.

    Funciones lambda en Python

    Introducción a las funciones lambda de Python
    Introducción a las funciones lambda de Python

    Una función lambda, también conocida como función anónima, es un tipo de función especial de Python, que permite crear funciones simples con tantos parámetros como quieras, pero con una única expresión.

    El término lambda se escribe igual en español.

    Si hablamos del término anónimo/a, lo podemos traducir como anonymous.

    Espacio publicitario

    Sintaxis de las funciones lambda

    La sintaxis de este tipo de funciones es muy simple. Utilizamos la palabra reservada lambda, le damos una serie de parámetros, dos puntos (:), y luego, la expresión que queramos. Recuerda, solo una.

    lambda parámetros : expresión

    Funciones lambda vs. def

    Las funciones lambda son un tipo de función muy práctica para utilizarse en operaciones sencillas.

    Gracias a ellas, conseguiremos crear ciertas funciones en una sola línea, en lugar de ocupar varias.

    En el siguiente ejemplo tienes dos veces la misma función, una escrita como función lambda y la otra como función normal (def).

    Función lambda:

    lambda x, y: x + y

    Función normal:

    def sumar(x, y):
        return x + y

    Ambas funciones sirven para sumar dos valores. Una tiene nombre y la otra no. Una ocupa dos líneas y la otra una. Además, la función anónima devuelve automáticamente el valor de retorno, sin utilizar la palabra return.

    Bajo mi punto de vista, la función normal es más legible. Vemos más claro a simple vista todos los “pasos” que da.

    Llamar a una función lambda

    Quizás hayas caído, en que una función anónima, no tiene nombre. Por eso se le llama así.

    Espacio publicitario

    Entonces, si no tiene nombre, ¿cómo la vamos a llamar para utilizarla?

    Realmente, podríamos decir que sí que tienen nombre, ya que para utilizarlas, las tendremos que guardar en una variable (no siempre es necesario), para así almacenar un valor de retorno, y poder “llamarlas”.

    En el siguiente ejemplo, se asocia la función lambda a una variable llamada suma:

    suma = lambda x, y: x + y
    
    print(suma(5, 20))
    print(suma(3, 7))
    Resultado en la consola
    25
    10

    La variable suma está sirviendo como nombre para llamar a la función anónima.

    En este caso, solo se están imprimiendo los valores de retorno, pero puedes almacenarlos en otras variables:

    suma = lambda x, y: x + y
    
    operacion_1 = suma(5, 20)
    operacion_2 = suma(3, 7)
    
    print(operacion_1)
    print(operacion_2)
    Resultado en la consola
    25
    10

    ¿Cuándo y por qué utilizar funciones lambda?

    Las funciones anónimas se utilizan normalmente en situaciones donde una función normal sería excesiva o innecesaria.

    También habrá veces que podrás facilitar mucho el código, utilizando una función lambda. Evitando incluso la anidación de elementos.

    Por ejemplo, cuando creamos eventos en ciertas situaciones, como con las interfaces gráficas de la biblioteca Tkinter, nos pueden ser de gran utilidad.

    A medida que utilices propósitos específicos en Python, empezarás a ver donde y como utilizar estas funciones anónimas.

    Al principio pueden parecer algo abstractas y carecer de sentido; hace falta práctica para ir conociéndolas mejor.

    Recuerda: Una función anónima es igual que una normal (def), con la diferencia de que se crea en una sola línea, con una sola expresión, que devuelve los valores de retorno sin return y que no tiene nombre.

    Espacio publicitario

    Ejemplo práctico con lambda

    La siguiente función def, es capaz de obtener un conjunto entero de números y devolver el resultado de la raíz cuadrada de cada número, en una lista:

    # Calcula raíces cuadradas y las devuelve en una lista
    def raices_cuadradas(numeros):
        def raiz_cuadrada(numero):
            return numero ** 2
        
        raices = map(raiz_cuadrada, numeros)
        return list(raices)
    
    # Lista para probar la función
    lista_numeros = [9, 15, 150, 63, 70]
    
    # Se almacena la lista devuelta por la función
    resultado = raices_cuadradas(lista_numeros)
    
    # Comprobamos los valores
    print(resultado)
    Resultado en la consola
    [81, 225, 22500, 3969, 4900]

    Para que entiendas a la perfección como funciona esta función, voy a detallar cada parte.

    Fórmula de cálculo de una raíz cuadrada

    Fórmula de raíz cuadrada

    El cálculo de una raíz cuadrada es sencillo. Se representa frecuentemente con la fórmula de la imagen, no obstante, se puede expresar de forma más entendible:

    numero * numero = x

    Donde x, es el resultado de multiplicar un número al cuadrado.

    Este cálculo se puede también con el operador de potencia de Python (**):

    numero ** 2

    Precisamente, este es el cálculo que se aplica en la función anidada raiz_cuadrada() del ejemplo anterior.

    La función externa raices_cuadradas() se encarga de calcular todas las raíces cuadradas, utilizando para ello, la función interna o anidada, que calcula y devuelve una sola raíz cuadrada cada vez que es llamada.

    La función predefinida map()

    La función predefinida map() de Python, permite llamar a una función de forma automática sobre un iterable. Esta devolverá finalmente un nuevo objeto iterable de tipo map.

    Espacio publicitario

    Con map() tenemos esta sintaxis:

    map(funcion, objeto iterable)

    En la función externa raices_cuadradas(), utilizo map() con la función interna, y el objeto numeros, que es el objeto que se pasa como argumento en la llamada de la función externa.

    raices = map(raiz_cuadrada, numeros)

    En este caso, el objeto pasado al parámetro numeros es la lista lista_numeros:

    resultado = raices_cuadradas(lista_numeros)

    Al ejecutarse la función map(), llama a la función interna, pasándole de argumento para numero, el primer valor iterado en el objeto iterable (lista_numeros en el ejemplo).

    Con esto, vamos confeccionando una lista con los valores calculados de las raíces cuadradas, haciendo tantas llamadas como elementos tenga el iterable pasado.

    Finalmente, tenemos un objeto iterable de tipo map, que podemos transformar a otro iterable, como puede ser una lista. Esto lo hace la función aquí:

    return list(raices)

    Devuelve una lista con cada cálculo.

    Puesto que es un valor devuelto en la llamada, lo capturamos en una variable:

    resultado = raices_cuadradas(lista_numeros)

    Así pues, ya tenemos en la variable resultado una lista con las raíces cuadradas calculadas a partir de los números de la lista inicial (lista_numeros).

    Veamos un ejemplo más simple con map():

    # Calcula la raíz cuadrada
    def raiz_cuadrada(numero):
        return numero ** 2
    
    # Lista con números
    lista_numeros = [9, 15, 150, 63, 70]
    
    # Hacemos los cálculos
    resultado = map(raiz_cuadrada, lista_numeros)
    
    # Comprobamos el tipo de dato que crea map
    print(type(resultado))
    
    # Creamos la lista con los cálculos
    lista_raices = list(resultado)
    
    # Comprobamos el resultado
    print(lista_raices)
    Resultado en la consola
    <class 'map'>
    [81, 225, 22500, 3969, 4900]

    La función raiz_cuadrada() solo es capaz de devolver una raíz cuadrada de un solo número.

    # Calcula la raíz cuadrada
    def raiz_cuadrada(numero):
        return numero ** 2

    La funcion w obtiene la función que queremos utilizar, y el iterable lista_numeros.

    # Hacemos los cálculos
    resultado = map(raiz_cuadrada, lista_numeros)

    Si imprimimos el tipo de objeto que crea map(), en la consola nos aparece clase map:

    # Comprobamos el tipo de dato que crea map
    print(type(resultado))
    Resultado en la consola
    <class 'map'>

    Un objeto de tipo map, es un iterador. Este iterador se puede transformar a lista, conjunto, tupla, etc. En concreto, lo hago aquí:

    # Creamos la lista con los cálculos
    lista_raices = list(resultado)

    Le paso a la función conversora list(), el objeto de tipo map, que lo que hace es añadir cada valor en una posición de la lista.

    Este ejemplo es más fácil de entender, pero no implementa la solución completa en una función reutilizable, por lo tanto, es una solución poco óptima si queremos utilizar esta calculadora de raíces en más módulos o en otras partes de la misma hoja de código.

    Espacio publicitario

    Facilitando el código con una función lambda

    Esta es una de las buenas ocasiones para utilizar una función lambda, en lugar de una función def (la función lambda está marcada en el código):

    # Procesa listas de números y devuelve la raíz cuadrada
    def raices_cuadradas(numeros):
        cuadrados = map(lambda numero: numero ** 2, numeros)
        return list(cuadrados)
    
    # Lista para probar la función
    lista_numeros = [9, 15, 150, 63, 70]
    
    # Se crea la lista con las raices cuadradas calculadas
    raices = raices_cuadradas(lista_numeros)
    
    # Comprobamos los valores
    print(raices)
    Resultado en la consola
    [81, 225, 22500, 3969, 4900]

    El resultado es el mismo, pero ya no necesitamos tanta complicación. Con solo tres líneas de código, estamos haciendo todo lo que hacíamos con el sistema de funciones def anidadas.

    Le pasamos a la función raices_cuadradas() un conjunto de números. En el ejemplo, es la lista lista_numeros:

    raices = raices_cuadradas(lista_numeros)

    La función map() lleva la funcion lambda como argumento de función. Esta lambda lleva la única expresión necesaria para el cálculo, que es el de cálculo de las raíces cuadradas.

    cuadrados = map(lambda numero: numero ** 2, numeros)

    Como segundo argumento para la función map(), tenemos el objeto numeros, que es la lista lista_numeros pasada como argumento a la llamada de la función raices_cuadradas():

    cuadrados = map(lambda numero: numero ** 2, numeros)
    
    . . .
    
    lista_numeros = [9, 15, 150, 63, 70]
    
    raices = raices_cuadradas(lista_numeros)

    Entonces, la función va iterando cada valor de la lista pasada, aplicándole la fórmula y devolviendo el resultado.

    Como podrás comprobar, en este caso concreto, el uso de una función lambda, nos ha ahorrado utilizar la anidación, que siempre que sea posible, es mucho mejor no utilizarla, para evitar añadir complejidad extra al código.

    Espacio publicitario

    Puede que te cueste asimilar este comportamiento al principio, pero ves analizándolo detenidamente. Estamos tratando cosas que pueden considerarse bastante complejas, para quien está aprendiendo todavía la base de Python.

    No te frustres si no entiendes algo, o si crees que no serás capaz de aplicarlo más adelante a tus programas. Todo llega con la práctica.

    El término map en español se traduce como mapa.

    Funciones decoradoras con Python

    Introducción a las funciones decoradoras con Python
    Introducción a las funciones decoradoras con Python

    Las funciones decoradoras de Python son funciones especiales que nos ayudarán a “decorar” el código de ciertas funciones. Se trata de emplear ciertas funcionalidades de una función sobre otras. Esto quedará más claro con los ejemplos que verás a continuación.

    Función decoradora se traduce al inglés como decorator function.

    No obstante, es más común utilizar en inglés la palabra decorators (decoradores) a secas.

    Las funciones son objetos

    En Python, las funciones también son objetos; puedes comprobarlo de esta forma:

    def funcion():
        pass
    
    print(type(funcion))
    Resultado en la consola
    <class 'function'>

    Esto entre muchas posibilidades, es uno de los motivos por los cuales podemos asignar una función a una variable.

    Llamadas como argumentos

    También podemos pasar funciones como argumentos para otras funciones:

    def a():
        print("Función A ejecutada")
    
    def b(funcion):
        print("Función B ejecutada")
        funcion() # Llama a la función que se pasa como argumento
    
    # Llamada a la función b pasando la función a como argumento
    b(a)
    Resultado en la consola
    Función B ejecutada
    Función A ejecutada

    En este ejemplo, la función b() espera un argumento. Este argumento podría ser cualquier objeto. Sin embargo, puesto que dentro de la función se hace uso del argumento con unos paréntesis, se espera un objeto invocable (callable), como una función.

    Espacio publicitario

    Si intento pasar un objeto que no se puede llamar, como puede ser un int, ocurre el siguiente error:

    def a():
        print("Función A ejecutada")
    
    def b(funcion):
        print("Función B ejecutada")
        funcion() # Llama a la función que se pasa como argumento
    
    # Llamada a la función b pasando la función a como argumento
    b(10)
    Error en la consola
    TypeError: 'int' object is not callable
    Error de tipo: el objeto de tipo 'int' no es invocable

    El error ocurre en la llamada que intenta hacer la función b(), ya que está intentando llamar a un int, cosa que no tiene sentido y que hace que la llamada a la función pasada como argumento, origine el error.

    En el siguiente código, podemos observar que dentro de la función b(), es posible controlar perfectamente el flujo de ejecución y hacer lo que queramos. Tanto antes, como después de llamar a la función pasada como argumento. Fíjate bien en la salida en la consola:

    def a():
        print("Función A ejecutada")
    
    def b(funcion):
        print("Función B ejecutada")
        funcion() # Llama a la función que se pasa como argumento
    
    # Llamada a la función b pasando la función a como argumento
    b(a)
    Resultado en la consola
    Función B ejecutada
    Función A ejecutada

    La función b() ejecuta su print() y luego llama a la función parámetro (la que le pasamos al parámetro funcion).

    Este orden no es definitivo, podemos poner código en la posición que queramos. Por ejemplo:

    def a():
        print("Función A ejecutada")
    
    def b(funcion):
        print("Se ha iniciado la función")
        funcion() # Llama a la función que se pasa como argumento
        print("Función B ejecutada")
    
    # Llamada a la función b pasando la función a como argumento
    b(a)
    Resultado en la consola
    Se ha iniciado la función
    Función A ejecutada
    Función B ejecutada

    Gracias a entender el flujo de ejecución, puedo controlar en todo momento que se ejecuta antes y qué se ejecuta después. En las tres líneas de la salida de la consola anterior, la ejecución viene dada de esta forma:

    • print("Se ha iniciado la función") - Código de b().
    • funcion() - Ejecuta el código de a().
    • print("Función B ejecutada") - Código de b().

    Espacio publicitario

    Aplicando decoradores

    Conceptos fundamentales sobre las funciones decoradoras de Python
    Conceptos fundamentales sobre las funciones decoradoras de Python

    Veamos un nuevo ejemplo. Voy a crear cuatro funciones de operaciones aritméticas. Hasta aquí, serán funciones normales y corrientes.

    def sumar():
        print(10 + 10)
    
    def restar():
        print(10 - 20)
    
    def multiplicar():
        print(45 * 2)
    
    def dividir():
        print(4 / 87)

    Soy consciente de que no son funciones útiles, al tener simplemente un resultado fijo, pero quiero simplificar el concepto de las funciones decoradoras al máximo, después, ya habrá tiempo de complicarlo.

    Ahora supón que les quieres agregar más funcionalidad, añadiéndoles alguna frase antes de realizar la operación, y otra después:

    def sumar():
        print("El resultado de la operación es: ")
        print(10 + 10)
        print("Operación realizada con éxito.")
    
    def restar():
        print("El resultado de la operación es: ")
        print(10 - 20)
        print("Operación realizada con éxito.")
    
    def multiplicar():
        print("El resultado de la operación es: ")
        print(45 * 2)
        print("Operación realizada con éxito.")
    
    def dividir():
        print("El resultado de la operación es: ")
        print(4 / 87)
        print("Operación realizada con éxito.")

    El código de estas funciones se está repitiendo mucho, tanto el primer print() como el tercero, son exactamente iguales en las cuatro funciones ¿Y si en lugar de cuatro, tienes cien funciones de este tipo?

    Aquí es donde entran en juego las funciones decoradoras; así que voy a crear una para este propósito:

    def decoradora(funcion_parametro):
        print("El resultado de la operación es: ")
        funcion_parametro()
        print("Operación realizada con éxito.")
    
    @decoradora
    def sumar():
        print(10 + 10)
    
    def restar():
        print(10 - 20)
    
    def multiplicar():
        print(45 * 2)
    
    def dividir():
        print(4 / 87)
    Resultado en la consola
    El resultado de la operación es: 
    20
    Operación realizada con éxito.

    En este caso, estoy decorando solo la función sumar(), con la función decoradora llamada @decoradora.Lo que se consigue con esto, es pasar la función sumar() a la función decoradora, como su parámetro.

    El comportamiento es el mismo que en los ejemplos anteriores, pero ahora, no tenemos que hacer una llamada explícita en el código. De esto se encarga el decorador @decoradora.

    Espacio publicitario

    Por la salida en la consola podemos apreciar que primero aparece el primer print() de la funcion decoradora.

    A continuación se llama a la función pasada como argumento (sumar()), y finalmente se imprime la frase con el segundo print() de la función decoradora.

    Es posible aplicar este código repetitivo a cuantas funciones necesitemos. Tan solo hay que utilizar el nombre de la función decoradora con una @ de prefijo.

    Por ejemplo:

    def decoradora(funcion_parametro):
        print("El resultado de la operación es: ")
        funcion_parametro()
        print("Operación realizada con éxito.")
    
    @decoradora
    def sumar():
        print(10 + 10)
    
    @decoradora
    def restar():
        print(10 - 20)
    
    @decoradora
    def multiplicar():
        print(45 * 2)
        
    @decoradora
    def dividir():
        print(4 / 87)
    Resultado en la consola
    El resultado de la operación es: 
    20
    Operación realizada con éxito.
    El resultado de la operación es:
    -10
    Operación realizada con éxito.
    El resultado de la operación es:
    90
    Operación realizada con éxito.
    El resultado de la operación es:
    0.04597701149425287
    Operación realizada con éxito.

    Está funcionando, nos ahorramos repetir los dos print() con cada función. Sin embargo, tenemos un gran problema. Las funciones decoradoras, como la que estamos construyendo, se ejecutan automáticamente, sin llamarlas en el código.

    Por lo tanto, con una función decoradora como la del ejemplo, estamos implementando mal el sentido de estas funciones. A continuación verás una estructura más funcional.

    Espacio publicitario

    Estructura de una función decoradora de Python

    La estructura típica de una función decoradora de Python se basa en tres funciones, las cuales, con el fin de entendernos de forma simple, llamaré decoradora(), funcion_parametro() e interna() (estos nombres no son obligatorios).

    La función decoradora() va a ser la que reciba como argumento la función funcion_parametro(). A raíz de esto, la función decoradora(), devolverá con un return el valor de llamar a la función interna(). Esto parece una locura, pero verás que es más sencillo de lo que parece.

    Sintaxis de una función decoradora de Python

    Esta es la sintaxis de una función decoradora de Python:

    def decoradora(funcion_parametro):
        def interna():
            #código de la función interna
        return interna

    Piensa, que el return está indentado dentro de la función decoradora(), no en la interna().

    Otra forma de representar esto es la siguiente:

    def a(b) -> c

    Aquí puedes ver que la función a() tiene la función b() como parámetro y devuelve la función c().

    En este ejemplo, estos nombres se corresponden con el otro ejemplo, de la siguiente forma:

    • a(): Función decoradora
    • b(): Función parámetro o decorada
    • c(): Función interna

    Empecemos por ejecutar este código:

    def decoradora(funcion_parametro):
        def interna():
            print("El resultado de la operación es: ")
            funcion_parametro()
            print("Operación realizada con éxito.")
    
    @decoradora
    def sumar():
        print(10 + 10)

    Esta vez no se está llamando a la función decorada directamente. La función interna ha contenido la llamada.

    Sin embargo, si llamas a la función sumar(), te toparás de bruces con un problema:

    sumar()
    Error en la consola
    TypeError: 'NoneType' object is not callable
    Error de tipo: el objeto de tipo 'NoneType' no es invocable

    Al decorar la función, la hemos convertido en un elemento de tipo NoneType que no es invocable ¡No podemos llamar a la función!

    No nos alarmemos y busquemos una solución.

    Esto ocurre, porque la llamada está contenida en la función interna; recuerda que teníamos un alcance encerrado en las funciones anidadas.

    Para solucionarlo, escribimos un return:

    def decoradora(funcion_parametro):
        def interna():
            print("El resultado de la operación es: ")
            funcion_parametro()
            print("Operación realizada con éxito.")
        return interna
    
    @decoradora
    def sumar():
        print(10 + 10)
    
    sumar()
    Resultado en la consola
    El resultado de la operación es: 
    20
    Operación realizada con éxito.

    Ahora sí, problema resuelto.

    Espacio publicitario

    Las funciones decoradoras con parámetros

    Funciones decoradoras para funciones con parámetros en Python
    Funciones decoradoras para funciones con parámetros en Python

    Voy a complicar un poco más todo este asunto, añadiendo parámetros a las funciones decoradoras.

    Como puedes apreciar, las funciones del ejemplo anterior, son más bien inútiles, ya que siempre van a dar los mismos resultados, y para eso, no haría ni falta que fueran funciones.

    Con el fin de hacer estas funciones prácticas, podemos añadir parámetros:

    def sumar(num1, num2):
        print(num1 + num2)

    Si llamamos a esta función sin utilizar decoradores, no hay problema alguno:

    sumar(40,54)
    sumar(100,305)
    sumar(48,52)
    Resultado en la consola
    94
    405
    100

    Pero, ¿y si intentamos decorarla?

    def a(b):
        def c():
            print("El resultado de la operación es: ")
            b()
            print("Operación realizada con éxito.")
        return c
    
    @a
    def sumar(num1, num2):
        print(num1 + num2)
        
    sumar(40,54)
    sumar(100,305)
    sumar(48,52)
    Error en la consola
    TypeError: a.<locals>.c() takes 0 positional arguments but 2 were given
    Error de tipo: La función c() definida en a() toma 0 argumentos posicionales, pero se le pasaron 2.

    El error me está diciendo que en la función interna (la c()) que lleva la función decoradora (la a()), estoy pasando dos argumentos posicionales (num1 y num2), pero espera cero.

    El error se desencadena con la primera llamada, pero las otras dos ocasionarían lo mismo, si el código no fallase antes de ni siquiera poder ejecutarlas.

    Si miramos la función c(), el error no se equivoca; esta función no tiene parámetros para los que pasar esos dos argumentos. Por lo tanto, ya sabemos donde hay que colocar los parámetros; en la función c(), la interna:

    def a(b):
        def c(num1,num2):
            print(f"El resultado de la operación es: ")
            b()
            print("Operación realizada con éxito.")
        return c
    
    
    @a
    def sumar(num1, num2):
        print(num1 + num2)
    
    sumar(40,54)
    sumar(100,305)
    sumar(48,52)
    Error en la consola
    TypeError: sumar() missing 2 required positional arguments: 'num1' and 'num2'
    Error de tipo: La función sumar() requiere 2 argumentos posicionales 'num1' y 'num2'

    Vaya, otro error. Ahora, el error está en la función sumar(), que desencadena el problema en la función parámetro b(). El error indica que faltan 2 argumentos posicionales requeridos.

    Espacio publicitario

    Para solucionar esto, le añadimos los dos parámetros a la llamada de la función b(), y listo:

    def a(b):
        def c(num1,num2):
            print(f"El resultado de la operación es: ")
            b(num1, num2)
            print("Operación realizada con éxito.")
        return c
    @a
    def sumar(num1, num2):
        print(num1 + num2)
        
    sumar(40,54)
    Resultado en la consola
    El resultado de la operación es: 
    94
    Operación realizada con éxito.

    Por fin, ha funcionado.

    Es solo pasar los argumentos a la función interna, y esta se los pasa a los de la llamada de la función parámetro. Si alguna de las dos no tiene parámetros, no se le pueden pasar argumentos.

    *args en las funciones decoradoras

    Funciones decoradoras con *args y **kwargs en Python
    Funciones decoradoras con *args y **kwargs en Python

    El resultado está muy bien, pero ¿y si queremos utilizar el decorador en funciones que tengan un número indeterminado de parámetros?

    Supongamos que en la función de resta, queremos operar con cuatro números y no con dos, pero en la de suma, queremos seguir operando con dos.

    Con el fin de no tener que crear varias funciones decoradoras para utilizar lo mismo (lo que les haría perder todo el sentido), podemos utilizar el anteriormente explicado *args.

    def a(b):
        def c(*args):
            print(f"El resultado de la operación es: ")
            b(*args)
            print("Operación realizada con éxito.")
        return c
    
    
    @a
    def sumar(num1, num2):
        print(num1 + num2)
        
    @a
    def restar(num1, num2, num3, num4):
        print(num1 - num2 - num3 - num4)
        
    sumar(30,50)
    restar(40,54,60,43)
    Resultado en la consola
    El resultado de la operación es: 
    80
    Operación realizada con éxito.
    El resultado de la operación es:
    -117
    Operación realizada con éxito.

    Como puedes apreciar, ahora no importa el número de argumentos que pasemos; la función se adapta a ellos.

    Espacio publicitario

    *kwargs en las funciones decoradoras

    Pasemos a otro ejemplo para ver la utilidad de emplear **kwargs en las funciones decoradoras de Python.

    En el siguiente ejemplo, verás que hay funciones que esperan diferente número de argumentos; las dos primeras funciones, dos argumentos y la última, solo uno.

    Pues bien, partiendo de la base, necesitamos poner *args si queremos decorar estas funciones:

    import math
    
    def a(b):
        def c(*args):
            print("Empieza el cálculo...")
            b(*args)
            print("Operación realizada con éxito.")
        return c
    
    @a
    def area_rectangulo(base, altura): 
        print(f"El área del rectángulo es: {base * altura}.")
    
    
    @a
    def area_triangulo(base, altura):
        print(f"El área del rectángulo es: {base * altura / 2}")
    
    
    @a
    def area_circulo(radio):
        print(f"El área del círculo es {math.pi * radio ** 2}")

    Hagamos una llamada, por ejemplo, con la función area_rectangulo():

    area_rectangulo(10,40)
    Resultado en la consola
    Empieza el cálculo...
    El área del rectángulo es: 400.
    Operación realizada con éxito.

    Funciona correctamente, al igual que lo harán las otras dos.

    Sin embargo, mira lo que ocurre si quieres hacer una llamada con argumentos de clave:

    area_rectangulo(altura=40,base=10)
    Error en la consola
    TypeError: a.<locals>.c() got an unexpected keyword argument 'altura'
    Error de tipo: La función c() definida en a() toma un argumento de clave inesperado, 'altura'.

    Nos suelta un error diciendo que no se esperaba un argumento de clave; el primero que encuentra, que es altura.

    Claro, esto ocurre porque se lo estoy pasando a *args. El parámetro especial *args, como bien expliqué anteriormente, solo recibe argumentos posicionales. Es aquí, donde hay que añadir la funcionalidad extra de **kwargs a nuestra función decoradora. Esto permitirá el uso de argumentos de clave en las funciones decoradas:

    import math
    
    def a(b):
        def c(*args, **kwargs):
            print("Empieza el cálculo...")
            b(*args, **kwargs)
            print("Operación realizada con éxito.\n")
        return c
    
    @a
    def area_rectangulo(base, altura): 
        print(f"El área del rectángulo es: {base * altura}.")
    
    @a
    def area_triangulo(base, altura):
        print(f"El área del rectángulo es: {base * altura / 2}")
    
    @a
    def area_circulo(radio):
        print(f"El área del círculo es {math.pi * radio ** 2}")
        
    area_rectangulo(altura=40,base=10)
    area_rectangulo(base=6,altura=10)
    area_circulo(radio=2)
    Resultado en la consola
    Empieza el cálculo...
    El área del rectángulo es: 400.
    Operación realizada con éxito.
    
    Empieza el cálculo...
    El área del rectángulo es: 60.
    Operación realizada con éxito.
    
    Empieza el cálculo...
    El área del círculo es 12.566370614359172
    Operación realizada con éxito.

    Los espacios en la consola, entre los resultados de las llamadas, vienen de un salto de línea que he añadido dentro de la función decoradora. Te lo he dejado en el código marcado (\n).

    Espacio publicitario

    Funciones generadoras e iteradores

    Funciones generadoras de Python - Iteradores e iterables
    Funciones generadoras de Python - Iteradores e iterables

    Las funciones generadoras son un tipo de función especial de Python, que permite dividir una ejecución de función, en partes más pequeñas, con el fin de aprovechar mejor los recursos del sistema, sobre todo hablando de memoria.

    Esto es gracias a que estas funciones crean un objeto iterador que se puede ejecutar paso a paso, en lugar de todo a la vez, como ocurre con las funciones normales.

    Se dice que estas funciones son de evaluación diferida, porque se van ejecutando de forma gradual, generando un valor cada vez, y no todos de golpe.

    Si creas un generador y no lo utilizas, la función no se ejecutará en absoluto. Solo se ejecutará cuando se solicite el primer valor mediante la función next() o al usar el generador con, por ejemplo, un bucle for.

    La palabra reservada yield

    La palabra reservada yield es parecida a return, pero no exactamente igual.

    Una función con return, o incluso con print(), se ejecuta íntegra en el momento de la llamada, y luego devuelve o imprime un valor, dando por finalizada la ejecución.

    Las funciones normales procesan y almacenan todos los datos de una secuencia de una sola vez. Eso no es importante en funciones con consumos mínimos, pero se convierte en un problema a la hora de trabajar con una gran cantidad de datos.

    Con yield, tenemos una especie de return, ya que devuelve valores, pero es capaz de hacerlo por pasos, como un bucle y sus iteraciones. Veamos un pequeño ejemplo utilizando return:

    def generadora():
        for i in range(0, 100):
            return i

    Tenemos una función con un bucle, y queremos que esta nos devuelva los valores iterando un rango. Sin embargo, si imprimimos el valor de retorno, veremos que después de ejecutar el primer valor del range() (0), finaliza la ejecución; como era de esperar:

    def generadora():
        for i in range(0, 100):
            return i
    
    print(generadora())
    Resultado en la consola
    0

    Entonces, en este caso, si queremos imprimir todo el rango lo tendríamos que hacer mediante un print():

    def generadora():
        for i in range(0, 100):
            print(i)
    
    generadora()
    Resultado en la consola
    0
    1
    2
    . . . 
    99

    O bien, nos las ingeniamos para crear una lista o algo por el estilo, que guarde todos los valores y los pueda devolver de una sola vez:

    def generadora_numeros():
        lista_valores = []
        for i in range(0, 100):
            lista_valores.append(i)
        return lista_valores
    
    valores = generadora_numeros()
    print(valores)
    Resultado en la consola
    [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, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

    Mira lo que pasa utilizando yield, e imprimiendo el objeto producido en la llamada:

    def generadora():
        for i in range(0, 100):
            yield i
    
    print(generadora())
    Resultado en la consola
    <generator object generadora at 0x00000225107FC520>

    Recibimos la referencia del objeto, no un valor de retorno.

    Espacio publicitario

    Desde el momento en el que llamas a la función, esta queda con su objeto creada, esperando a ser iterada.

    Claro, que si generas el objeto en un print(), no lo vas a poder usar en el código.

    Entonces, vamos a guardar el objeto en una variable; así lo podremos manejar:

    def generadora():
        for i in range(0, 100):
            yield i
    
    rango = generadora()

    Ahora, vamos a ir iterando el objeto gracias a la función predefinida next() de Python. Esta hará que se ejecute un paso de la función cada vez:

    print(next(rango))
    print(next(rango))
    print(next(rango))
    Resultado en la consola
    0
    1
    2

    Bien, así hasta que ya no quede rango que iterar.

    La ventaja de esto, es que en funciones que manejen grandes volúmenes de datos, podremos dosificar la ejecución, para evitar hacerlo todo de golpe. Entre las ejecuciones, podríamos hacer cualquier cosa, por ejemplo:

    def generadora():
        for i in range(0, 100):
            yield i
    
    rango = generadora()
    
    print(next(rango))
    
    print("Acciones independientes a la función.")
    
    print(next(rango))
    print(next(rango))
    Resultado en la consola
    0
    Acciones independientes a la función.
    1
    2

    En resumen, gracias a yield, vas pausando y ejecutando una llamada a una función, con muchos fines, pero el principal es un rendimiento más óptimo en operaciones que requieren procesar muchos datos.

    Espacio publicitario

    Utilizar varios yield en la misma generadora

    Veamos otro ejemplo utilizando varios yield en la misma función. Cada uno genera una salida distinta:

    def generadora():
        for i in range(0, 10):
            if i % 2 == 0:
                yield f"{i} - par"
            else:
                yield f"{i} - impar"
    
    
    rango = generadora()
    
    print(next(rango))
    print(next(rango))
    print(next(rango))
    print(next(rango))
    Resultado en la consola
    0 - par
    1 - impar
    2 - par
    3 - impar

    El mismo ejemplo con return, no podría ir ejecutando diferentes resultados. En este código concreto, el resultado será siempre el mismo, se ejecuta el if, y finaliza la ejecución de la función:

    def generadora():
        for i in range(0, 10):
            if i % 2 == 0:
                return f"{i} - par"
            else:
                return f"{i} - impar"
    
    
    rango = generadora()
    
    print(rango)
    print(rango)
    print(rango)
    print(rango)
    Resultado en la consola
    0 - par
    0 - par
    0 - par
    0 - par

    Piensa que con return, no creamos un objeto iterable necesariamente. En el caso del ejemplo, se devuelve un str, que es iterable, pero no es un objeto iterador.

    Con lo de que no es un iterador estoy diciendo también, que no puedes utilizar la función next():

    def generadora():
        for i in range(0, 10):
            if i % 2 == 0:
                return f"{i} - par"
            else:
                return f"{i} - impar"
    
    rango = generadora()
    
    print(type(rango))
    print(next(rango))
    Error en la consola
    <class 'str'>
    TypeError: 'str' object is not an iterator
    Error de tipo: el objeto 'str' no es un iterador

    Antes de aparecer el error en la última línea, se imprime que el tipo de objeto es str, no un generator como ocurre con yield:

    rango = generadora()
    print(type(rango))
    Resultado en la consola
    <class 'generator'>

    Espacio publicitario

    Generar otros objetos iterables a partir de una función generadora

    Es posible generar objetos iterables como una lista a partir de un objeto de tipo generator. Esto se consigue con una de las funciones predefinidas de conversión, como puede ser list():

    def generadora():
        for i in range(0, 10):
            if i % 2 == 0:
                yield f"{i} - par"
            else:
                yield f"{i} - impar"
    
    rango = generadora()
    
    # Se crea la lista a partir del objeto generator
    lista_generador = list(rango)
    
    print(lista_generador)
    Resultado en la consola
    ['0 - par', '1 - impar', '2 - par', '3 - impar', '4 - par', '5 - impar', '6 - par', '7 - impar', '8 - par', '9 - impar']

    Con esto tenemos acceso a cada iteración mediante posiciones de lista. Por ejemplo, que necesitamos el resultado de dos iteraciones concretas, pues podemos hacer algo tan simple como acceder a dos posiciones de lista concretas:

    def generadora():
        for i in range(0, 10):
            if i % 2 == 0:
                yield f"{i} - par"
            else:
                yield f"{i} - impar"
    
    rango = generadora()
    lista_generador = list(rango)
    
    print(lista_generador[7])
    print(lista_generador[2])
    Resultado en la consola
    7 - impar
    2 - par

    Excepción StopIteration

    La función next() produce una excepción, cuando las posibles ejecuciones de la función generadora se han terminado, y se intenta acceder a la siguiente. Aquí tienes un ejemplo:

    def generadora():
        for i in range(0, 3):
            if i % 2 == 0:
                yield i
    
    rango = generadora()
    
    print(next(rango))
    print(next(rango))
    
    print(next(rango))
    Error en la consola
    0
    2
    StopIteration
    Detención de iteración

    Description

    Si no quieres producir esta excepción cuando te salgas del rango, puedes controlarla con los bloques try-except; tema que verás muy pronto. Por el momento, aquí tienes un ejemplo:

    def generadora():
        for i in range(0, 3):
            if i % 2 == 0:
                yield i
    
    rango = generadora()
    
    try: 
        print(next(rango))
        print(next(rango))
        print(next(rango))
    except StopIteration:
        print("La ejecución finalizó.")
    Resultado en la consola
    0
    2
    La ejecución finalizó.

    El último print() hace saltar el bloque except, ya que sale del rango de iteraciones posible.

    Espacio publicitario




    Espacio publicitario


    Ejercicios de Python para resolver

    68. Crea una función que permita introducir un str en la consola, y que cuente el número de caracteres que tiene.

    La función deberá avisar con un print(), con el total de caracteres.

    La función que he creado, tiene una variable que cuenta la longitud del str pasado como argumento, gracias a la función len().

    Además, formatea una frase que indica la longitud de caracteres.

    def longitud_str():
        # Almacena la entrada
        cadena = input("Escriba algo y se lo cuento: ")
    
        # Cuenta los caracteres de la entrada
        numero_caracteres = len(cadena)
    
        # Presenta el resultado en la consola
        print(f"La cadena tiene una longitud de {numero_caracteres} caracteres.")
    
    longitud_str()
    Resultado en la consola
    Escriba algo y se lo cuento: Python: El poder de los objetos.  
    La cadena tiene una longitud de 32 caracteres.

    69. Modifica la función anterior para que solo devuelva el número de caracteres. Después, opcionalmente, puedes hacer lo que quieras con ello.

    Este ejercicio era para comprobar si sabes utilizar los valores de retorno.

    def longitud_str():
        # Almacena la entrada
        cadena = input("Escriba algo y se lo cuento: ")
    
        # Cuenta los caracteres de la entrada
        numero_caracteres = len(cadena)
    
        # Devuelve el resultado
        return numero_caracteres
    
    longitud_cadena = longitud_str()
    
    print(longitud_cadena)
    Resultado en la consola
    Escriba algo y se lo cuento: Python: El poder de los objetos.  
    32

    En los siguientes ejercicios, quiero que crees una función iteradora con un bucle dentro.

    70. Crea una función para iterar un rango pasado en los argumentos. Esta función aceptará dos argumentos. Uno para el número de inicio y otro para el del final.

    Deja la función con pass hasta el siguiente ejercicio.

    Creamos la función con dos argumentos, dejándola de momento con pass:

    def iterar_rango(inicio, fin):
        pass

    71. Escribe un bucle for, que itere el rango pasado. Para ello, deberás utilizar una función predefinida de Python destinada a ello.

    El bucle deberá imprimir los valores del iterador en cada ciclo (i).

    Se le añade el bucle con un range() para iterar el rango de inicio y fin, con un print() dentro, que irá mostrando el valor del iterador en cada ciclo del bucle:

    def iterar_rango(inicio, fin):
        for i in range(inicio, fin):
            print(i)

    72. Realiza la llamada con los valores de inicio y fin que quieras.

    Llamamos al método con un rango cualquiera, por ejemplo, 10 y 15:

    def iterar_rango(inicio, fin):
        for i in range(inicio, fin):
            print(i)
    
    iterar_rango(10, 15)
    Resultado en la consola
    10
    11
    12
    13
    14

    73. El rango que sale (por el funcionamiento de range()), no es exactamente el mismo que le ponemos de inicio y fin. Por ejemplo, pones 10 y 15 respectivamente, y lo que sale en la consola es 10 hasta 14.

    Soluciona el problema, y haz que el rango se imprima exactamente igual que en la llamada.

    Deberás incrementar algo en 1.

    Este ejercicio era para que fueses probando donde poner un incremento en 1, como indicaba la pista. Si le sumas 1 al valor de fin, consigues que si los valores pasados en la llamada son 10 y 15, realmente, serán 10 y 16, por lo que un range(10, 16), irá del 10 al 15, obteniendo el resultado deseado:

    def iterar_rango(inicio, fin):
        for i in range(inicio, fin + 1):
            print(i)
    
    iterar_rango(10, 15)
    Resultado en la consola
    10
    11
    12
    13
    14
    15

    Esta función puede parecer poco práctica, y puede que pienses por qué no utilizar la función range() directamente.

    Piensa que con esta función, no tienes que escribir más el bucle, y que gracias a los argumentos, podemos llamarla en líneas separadas o en módulos diferentes, con valores diferentes, sin tener que escribir un bucle cada vez.

    Las funciones están creadas para poder reutilizar código de forma fácil.

    Espacio publicitario


    74. De la siguiente función, ¿sabrías decir cuál de los dos parámetros corresponde a args, y cuál a kwargs?

    def funcion(*a, **b):

    ¿Por qué lo has sabido en cada caso?

    El parámetro *a se corresponde con *args, y el parámetro **b con **kwargs.

    La forma de saberlo, es que args se representa con un asterisco, y kwargs con dos.

    75. Crea una función que reciba un número indefinido de argumentos. Esta función servirá para introducir varios nombres en una lista.

    Finalmente, la función deberá devolver la lista con todo.

    Para hacer esto, dentro de la función, crea una lista vacía.

    A continuación, un bucle que itere los nombres pasados como argumentos, y los vaya añadiendo a la lista vacía con un método para listas.

    No te olvides de añadir el valor de retorno (return) de la lista, al final de la función.

    Primero, obtenemos un número de argumentos indefinido gracias al uso de *args.

    Dentro de la función, tengo una lista vacía con la que ir incluyendo los diferentes nombres pasados como argumentos.

    Esto lo hace el bucle con el método de list llamado append(), que itera el conjunto de argumentos pasados en la llamada, y los va añadiendo a la lista.

    Finalmente, se devuelve la lista completa con un return.

    def obtener_personas(*nombres):
        personas = []
        for nombre in nombres:
            personas.append(nombre)
        return personas
    
    lista_nombres = obtener_personas("Roberto", "Marga", "Raúl")
    print(lista_nombres)
    Resultado en la consola
    ['Roberto', 'Marga', 'Raúl']

    76. ¿Sabes utilizar esta función?

    def imprimir_valores(**kwargs):
        for clave, valor in kwargs.items():
            print(f"{clave}: {valor}")

    Haz una llamada con las siguientes claves (los valores son libres).

    • Nombre
    • Apellidos
    • Edad
    • Ciudad

    Esta es una posible llamada a la función. Aquí solo tenías que demostrar que sabes utilizar una función con **kwargs.

    def imprimir_valores(**kwargs):
        for clave, valor in kwargs.items():
            print(f"{clave}: {valor}")
    
    imprimir_valores(Nombre = "Enrique", 
                     Apellidos = "Barros Fernández",
                     Edad = 32,
                     Ciudad = None 
                     )
    Resultado en la consola
    Nombre: Enrique
    Apellidos: Barros Fernández
    Edad: 32
    Ciudad: None

    77. Crea una función de multiplicación con dos argumentos por defecto.

    Los parámetros serán a, con un valor de argumento por defecto 50, y b con un valor por defecto de 70.

    Haz que devuelva el resultado, y guarda en variables las llamadas que te detallo a continuación.

    Primero, haz una operación sin argumentos para comprobar que los argumentos por defecto, se están aplicando correctamente. Por ejemplo:

    multiplicacion_1 = multiplicacion()

    Después, haz otra llamada con uno solo. Por ejemplo:

    multiplicacion_2 = multiplicacion(100)

    Finalmente, haz una llamada con dos argumentos. Por ejemplo:

    multiplicacion_3 = multiplicacion(10,700)

    Si te devuelve correctamente los resultados (no hay excepciones ni fallos de lógica), es que la has hecho correctamente.

    def multiplicacion(a=50, b=70):
        return a * b
    
    sin_argumentos = multiplicacion()
    print(sin_argumentos)
    
    un_argumento = multiplicacion(100)
    print(un_argumento)
    
    dos_argumentos = multiplicacion(10,700)
    print(dos_argumentos)
    Resultado en la consola
    3500
    7000
    7000

    Espacio publicitario


    Mini proyecto - Ticket de compra

    Vas a realizar un pequeño proyecto dividido en varios ejercicios. Se trata de crear una función generadora de tickets, utilizando **kwargs.

    78. Crea una función que admita un número indefinido de argumentos de clave, con **kwargs.

    Creamos la función. Puedes dejarle pass, para que no te dé un error por dejar el bloque vacío:

    def generar_ticket(**kwargs):
        pass

    79. Crea dentro de la función, una variable para guardar el total de la compra.

    Por defecto, el valor será 0.

    La variable total, tiene que tener un valor inicial de 0:

    def generar_ticket(**kwargs):
        total = 0

    80. En la función, imprime un título para el ticket. Aquí tienes una idea:

    Ticket de compra:

    Añadimos un título con un print():

    def generar_ticket(**kwargs):
        total = 0
        print("Ticket de compra:")

    81. Crea un bucle for con dos iteradores: producto y precio, como te he mostrado en la parte teórica del capítulo.

    El bucle en cada iteración deberá mostrar con un print(), el producto y el precio que le corresponde.

    Los datos más adelante los pasaremos con claves y valores en la llamada a la función. Estos serán argumentos de clave, que pueden usarse como diccionario, ya que tienen sus dos elementos, clave y valor.

    Entonces, deberás utilizar el método items() dentro de la función, para poder iterarlos.

    Creamos el bucle con los dos iteradores. Así podemos iterar de una sola vez, clave y valor de los argumentos de clave.

    def generar_ticket(**kwargs):
        total = 0
        print("Ticket de compra:")
        for producto, precio in kwargs.items():
            print(f"{producto}: ${precio}")

    Se hace igual que en la parte teórica de este capítulo, pero esta vez, en lugar de pasarle el diccionario, le pasamos los argumentos de clave.

    El reto de este ejercicio era para probar si conseguías adaptar algo conocido, con una variación.

    82. Aprovecha el bucle for para añadirle, después de imprimir, un incremento del precio de cada producto procesado, a la variable total, que está a 0 por defecto. Así irá haciendo la cuenta del precio total.

    Añadimos el incremento de precio a la variable total, así se van sumando los precios de los productos:

    def generar_ticket(**kwargs):
        total = 0
        print("Ticket de compra:")
        for producto, precio in kwargs.items():
            print(f"{producto}: ${precio}")
            total += precio

    83. Finalmente, añade todas las decoraciones que quieras con uno o varios print(). Por ejemplo, líneas separadoras:

    print("----------------------------------")

    Además, deberás imprimir al final del ticket el precio final de toda la compra.

    Se imprime una decoración y el coste total de la compra:

    def generar_ticket(**kwargs):
        total = 0
        print("Ticket de compra:")
        for producto, precio in kwargs.items():
            print(f"{producto}: ${precio}")
            total += precio
        print("----------------------------------")
        print(f"Coste total: ${total}")

    84. Podemos añadir un extra a la función. Un return, para que además de hacer todo lo anterior, cada compra genere un valor de retorno con el precio total. Añádelo.

    Añadimos un simple return fuera del bucle, para tener el valor de retorno del total:

    def generar_ticket(**kwargs):
        total = 0
        print("Ticket de compra:")
        for producto, precio in kwargs.items():
            print(f"{producto}: ${precio}")
            total += precio
        print("----------------------------------")
        print(f"Coste total: ${total}")
        return total

    Espacio publicitario

    Mini proyecto - Fase de pruebas

    85. Crea una variable llamada compra_1. Esta variable contendrá una llamada a la función, con la siguiente compra:

    • Taza: $5
    • Collar: $77.99
    • Televisor: $439.79
    • Tenedores: $3.99

    Recuerda que la llamada debe ser con argumentos de clave, para poder devolver el objeto dict_items, que sea iterado por el bucle.

    Hacemos la siguiente llamada, para probar la función:

    compra_1 = generar_ticket(Taza = 5, 
                              Collar = 77.99, 
                              Televisor = 439.79,
                              Tenedores = 3.99
                              )
    Resultado en la consola
    Ticket de compra:
    Taza: $5
    Collar: $77.99
    Televisor: $439.79
    Tenedores: $3.99
    ----------------------------------
    Coste total: $526.77

    86. Crea otra variable llamada compra_2. Esta variable contendrá una llamada a la función, con la siguiente compra:

    • Camiseta: $19.99
    • Monitor: $199
    • Libreta: $3.49

    Si al juntar varias llamadas de compra, te salen en la consola los tickets de una forma que no te gusta, puedes añadir saltos de línea o cualquier otro elemento de presentación. Deja la salida como más te guste.

    Probamos una llamada más:

    compra_2 = generar_ticket(Camiseta = 19.99, 
                              Monitor = 199, 
                              Libreta = 3.49
                              )
    Resultado en la consola
    Coste total: $526.77
    Ticket de compra:
    Camiseta: $19.99
    Monitor: $199
    Libreta: $3.49
    ----------------------------------
    Coste total: $222.48000000000002

    87. Habrá resultados que te salgan con muchos decimales (como es el caso del segundo ticket). Esto no sirve para cobrar a un cliente. Necesitamos que la función dé un precio final con máximo dos decimales, que se correspondan a los céntimos/centavos.

    Simplemente, redondeamos a dos decimales, con la función round():

    def generar_ticket(**kwargs):
        total = 0
        print("Ticket de compra:")
        for producto, precio in kwargs.items():
            print(f"{producto}: ${precio}")
            total += precio
        print("----------------------------------")
        print(f"Coste total: ${round(total, 2)}")
        return total

    88. Haz una simple suma con las variables compra_1, y compra_2. Debería darte el total de las dos compras. Si es así, te está funcionando lo del valor de retorno.

    Todavía hay muchas pruebas que podríamos realizar, por ejemplo, ¿qué ocurre al poner una letra de precio en lugar de un valor numérico en las llamadas?

    Te reto a que vuelvas al terminar la parte de programación defensiva del curso que viene en el siguiente capítulo, y que manejes las posibles excepciones y validaciones que quieras. Es opcional para este mini proyecto.

    Realizamos una suma de las dos compras, para comprobar si funciona el valor de retorno de la función:

    suma_tickets = compra_1 + compra_2
    print(f"La suma de las compras es: ${suma_tickets}")

    Este programa, cuando hagas cosas con interfaces gráficas de usuario, podrá tener una serie de botones, estilo TPV (Punto de venta), que irán añadiendo los precios a un elemento como un diccionario, y se lo pasarán a la función de forma automática.

    Entonces, el usuario irá pulsando botones con la compra del cliente, y estos se encargarán de formar el ticket mediante una lógica muy parecida a la que has visto en este proyecto.

    89. Convierte esta función para calcular el cuadrado de un número, a función lambda:

    def cuadrado(a):
        return a * a

    No la utilices para nada aún. Solo declárala.

    Esta es la adaptación de la función anterior, a función lambda:

    lambda a : a * a

    90. Guarda la función lambda en una variable llamada cuadrado.

    Guardamos la función lambda anterior en una variable llamada cuadrado:

    cuadrado = lambda a : a * a

    91. Haz un cálculo con la función lambda (con cualquier número) y visualiza el resultado en la consola con un print().

    Finalmente, llamamos a la función con cualquier valor numérico:

    cuadrado = lambda a : a * a
    
    print(cuadrado(10))
    Resultado en la consola
    100

    92. Crea una función normal de saludo. Simplemente, que al llamarla imprima un saludo en la consola.

    Simplemente, creamos una función sencilla, sin parámetros:

    def saludo():
        print("Hola, espero que estés bien.")

    93. Sin borrar la función anterior, crea una función decoradora que tenga un mensaje antes de llamar a la función decorada y otro después. El mensaje puede ser cualquier cosa.

    Se crea una función decoradora sin parámetros de ningún tipo, que simplemente muestra un mensaje antes de ejecutar la función decorada, y otro después:

    def decoradora(funcion_externa):
        def funcion_interna():
            print("Antes...")
            funcion_externa()
            print("Después...")
        return funcion_interna

    94. Decora la función de saludo con la función decoradora. Si te salen los tres mensajes al llamar a la función de saludo, lo has hecho perfecto.

    def decoradora(funcion_externa):
        def funcion_interna():
            print("Antes...")
            funcion_externa()
            print("Después...")
        return funcion_interna
    
    @decoradora
    def saludo():
        print("Hola, espero que estés bien.")
    
    saludo()
    Resultado en la consola
    Antes...
    Hola, espero que estés bien.
    Después...

    95. Ahora modifica la función decoradora, para que tenga la posibilidad de pasarle todos los argumentos posicionales y de clave que se desee.

    Utilizamos *args y **kwargs en la función interna, pero también le tenemos que pasar eso mismo a la función decorada (funcion_externa):

    def decoradora(funcion_externa):
        def funcion_interna(*args, **kwargs):
            print("Antes...")
            funcion_externa(*args, **kwargs)
            print("Después...")
        return funcion_interna

    96. Modifica la función de saludo y dale los siguientes parámetros:

    • Nombre
    • Apellidos
    • Edad
    • Ciudad

    Dentro de la función de saludo, tendrás que sustituir la frase inicial (la del ejercicio 92), para que imprima esta frase con los valores de los argumentos:

    Soy {nombre} {apellidos}. Tengo {edad} años, y vivo en {ciudad}.

    Haz una llamada a la función saludo(), con un nombre y apellidos cualquiera como argumentos posicionales, y edad y ciudad con argumentos de clave.

    Todo el código quedará así. Fíjate en las partes marcadas en el código, que son las que he modificado para este ejercicio:

    def decoradora(funcion_externa):
        def funcion_interna(*args,**kwargs):
            print("Antes...")
            funcion_externa(*args,**kwargs)
            print("Después...")
        return funcion_interna
    
    @decoradora
    def saludo(nombre, apellidos, edad, ciudad):
        print(f"Soy {nombre} {apellidos}. Tengo {edad} años, y vivo en {ciudad}.")
    
    saludo("Amaya", 
           "Vallejo Palacios", 
           edad=28, 
           ciudad="Oporto"
           )


    Espacio publicitario