ALMENDROS EN FLOR Y PINOS EN EL SUR DE TENERIFE. |
En muchos de los ejemplos que ya hemos visto hemos "tangenciado" un poco, visto de soslayo, vamos, el tema que nos traemos ahora entre manos.. Veamos, antes que nada, la diferencia que hay entre "parámetro" y "argumento". Contamos con ésta excelente definición de "Python para principiantes", Eugenia Bahit, librosweb: un parámetro es un valor que la función espera recibir cuando sea llamada o invocada, a fin de ejecutar acciones en base al mismo. Una función puede esperar uno o más parámetros (que irán separados por una coma) o ninguno (funciones sin parámetros).
Tal y como hemos podido comprobar, insistimos, en ejemplos anteriores, tales parámetros son, en realidad, variables (referencias a objetos, tengámoslo siempre en mente) que podemos utilizar posteriormente en nuestras funciones def.
Los argumentos, en cambio, son los datos como tal, o bien como variables siempre y cuando éstas contengan un valor asignado, esto es, previamente declaradas y que, además, deben incluirse dentro de un paréntesis en el orden en que se les espera de acuerdo a los parámetros que hayamos inscrito.
Podemos decir que "pasamos" de algo genérico con lo que no podemos operar (parámetro) a algo concreto (argumento) con el que sí podemos operar en tanto que todo argumento es un dato/valor. Por ejemplo, éstos son parámetros: sujeto, verbo, adjetivo. Y cuando decimos qué es cada cosa, cuando asumen valores, son argumentos: Ana, cantar, verde; perro, ladrar, alto; coche, pintar, rojo;...
Como podemos ver, los parámetros forman parte de un esquema general de la función y, como tal, inoperante. Cuando concretamos (pasamos) los parámetros en datos o en variables que almacenan datos, bien hayamos declarado éstas fuera de la función o dentro de ella, transformamos los parámetros en argumentos, haciendo que la función se vuelva completamente operativa pudiéndonos devolver un resultado.
Ahora ya sabemos la diferencia que hay entre parámetros y argumentos. 👍
En Python, el "paso de parámetros" en un procedimiento por medio del cual un parámetro se transforma en argumento asumiendo un valor (entero, flotante, cadena, lista,...) directamente, o indirectamente, como ya sabemos, a través de una variable a la que se le ha asignado un valor, teniendo en cuenta que los tipos de los datos sean mutables o inmutables.
NO EXISTE UN LÍMITE PARA EL NÚMERO DE PARÁMETROS QUE PODEMOS PONER ENTRE LOS PARÉNTESIS DE UNA FUNCIÓN, PERO SÍ DEBEMOS TENER CLARO QUE A CADA PARÁMETRO LE CORRESPONDE UN ARGUMENTO, Y ADEMÁS, ÉSTOS DEBEN CONSERVAR EL MISMO ORDEN EN QUE INSERTAMOS LOS PARÁMETROS. PODEMOS COMPROBARLO EN EL SIGUIENTE EJEMPLO, TOMADO DE PYTHON 3, MARK SUMMERFIELD, ED. ANAYA MULTIMEDIA, PARA CALCULAR EL ÁREA DE UN TRIÁNGULO MEDIANTE LA FÓRMULA DE HERON (SÍ, SÍ. YO TAMBIÉN ME HE QUEDADO PERRUNAMENTE PASMADITO).
Vamos a estirar el ejemplo para estudiar las repercusiones que tiene la inclusión o no, y si la incluimos, en qué modo la sentencia return como pie de la función.
En el caso 1. obviamos la sentencia return. Al invocar la función no se nos devuelve resultado alguno. Ni tampoco el intérprete de Python nos lanza ninguna excepción. Y por algo será. En el caso 2. sólo incluimos la sentencia, sin "apellidos", sin pedirle qué es lo que nos debe devolver, y del mismo modo que nos ha sucedido antes, tampoco nos devuelve nada. Tampoco el intérprete de Python nos lanza excepción alguna. Y por algo será, repetimos. Sin embargo, en el caso 3. , cuando sí le ponemos "apellidos", cuando sí solicitamos aquéllo que nos debe devolver, en este caso, el nombre de la variable que almacena el resultado de la ejecución del algoritmo, al invocar la función, sí que se nos retorna un resultado, un resultado, además, guardado en memoria para poder hacer uso de él en cualquier situación ulterior.
TENGAMOS EN CUENTA QUE LA VARIABLE QUE ALMACENA EL
RESULTADO DE LA EJECUCIÓN DEL CÓDIGO INTERNO DE UNA FUNCIÓN, VIVE Y MUERE DENTRO DE SU ÁMBITO, POR LO QUE AL INVOCARLA FUERA NO NOS MUESTRA RESULTADO ALGUNO Y SE LANZA UNA EXCEPCIÓN: NameError: name 'c' is not defined.
TENGAMOS EN CUENTA QUE LA VARIABLE QUE ALMACENA EL
RESULTADO DE LA EJECUCIÓN DEL CÓDIGO INTERNO DE UNA FUNCIÓN, VIVE Y MUERE DENTRO DE SU ÁMBITO, POR LO QUE AL INVOCARLA FUERA NO NOS MUESTRA RESULTADO ALGUNO Y SE LANZA UNA EXCEPCIÓN: NameError: name 'c' is not defined.
En ambos casos, ahora sí, cuando pretendemos operar con la función, tal y como hicimos en el caso 3. del ejemplo precedente, se nos lanza una excepción: no es posible multiplicar un Nonetype (Ya sabemos lo que nos devuelven las funciones definidas por nosotros cuando "no nos devuelve nada": un objeto Python de tipo None, es decir, un objeto que carece de tipo de dato alguno, como el 0 en Matemáticas, que no tiene valor alguno pero existe y tiene su utilidad. Por esta razón no nos lanzaba una excepción ya que sí que había resultado, aunque el resultado era...ninguno) por, en nuestro ejemplo, un entero. Las funciones se han construido con una sintaxis impecable... hasta llegar al pie. En ambos casos se ha ejecutado la función dado que, leyendo instrucción a instrucción, línea de código a línea de código, nada, ningún error de codificación le ha impedido a Python ejecutarlas en su flujo de lectura habitual de arriba a abajo, pero no nos muestra el resultado. O sí...un objeto de tipo None, nada, ninguna cosa, el vacío.
Y como ya sabemos que Python impide operar entre objetos/valores de distinto tipo, en nuestro caso, un objeto de tipo None y un entero, ¡chass! Error que te pego. ¡Vaya!
Y como ya sabemos que Python impide operar entre objetos/valores de distinto tipo, en nuestro caso, un objeto de tipo None y un entero, ¡chass! Error que te pego. ¡Vaya!
VISTA DE LA CARA NOROESTE DEL VOLCÁN TEIDE DESDE LAS LANDAS DE SAN JOSÉ DE LOS LLANOS, EN EL MUNICIPIO DE EL TANQUE, NOROESTE INTERIOR DE TENERIFE. |
Veamos a continuación el siguiente ejemplo y comparémoslo con el anterior.
Ya lo tenemos. Cuando invocamos a la función, no sólo se nos muestra el resultado sino que, además, se nos devuelve envuelto entre cálidos y suaves algodones binarios. Observemos, por contra, que hemos empleado el nombre de variable z en 1., para guardar el resultado de la ejecución del algoritmo. Si la llamamos fuera de la función, como en 2., Python nos lanza una excepción. ¿Por qué? Pues porque la variable z está "encerrada" en el cuerpo de la función como variable local, donde vivirá y morirá en el momento de ejecutarse la función (d.e.p.): el resultado de ejecutar la función está ligado a la función misma y no a las variables internas que colocamos en su cuerpo. ¿Necesitamos una evidencia? La tenemos en 3.. Aquí podemos declarar una variable z a la que asignamos como valor un objeto de tipo función (function). Y recordemos que en Python TODO SON OBJETOS. Como tenemos una variable global, fuera del cuerpo de la función, podemos hacer uso de ella en cualquier parte del código.
Finalizamos con este último ejemplo que debería aclararnos más las cosas:
Ya lo tenemos. Cuando invocamos a la función, no sólo se nos muestra el resultado sino que, además, se nos devuelve envuelto entre cálidos y suaves algodones binarios. Observemos, por contra, que hemos empleado el nombre de variable z en 1., para guardar el resultado de la ejecución del algoritmo. Si la llamamos fuera de la función, como en 2., Python nos lanza una excepción. ¿Por qué? Pues porque la variable z está "encerrada" en el cuerpo de la función como variable local, donde vivirá y morirá en el momento de ejecutarse la función (d.e.p.): el resultado de ejecutar la función está ligado a la función misma y no a las variables internas que colocamos en su cuerpo. ¿Necesitamos una evidencia? La tenemos en 3.. Aquí podemos declarar una variable z a la que asignamos como valor un objeto de tipo función (function). Y recordemos que en Python TODO SON OBJETOS. Como tenemos una variable global, fuera del cuerpo de la función, podemos hacer uso de ella en cualquier parte del código.
Finalizamos con este último ejemplo que debería aclararnos más las cosas:
En 1. obtenemos de la función lo que ya conocemos de ejemplos anteriores. De hecho, cuando en 2. pedimos una impresión de z mediante la función integrada print(), se nos devuelve el valor None, que ya esperábamos con sólo leer la excepción que nos ha lanzado el intérprete de Python. Pero... ¿Qué sucede si incluimos la función print() para la variable z dentro de la propia función? Pues que, como encontramos en 3. se nos devuelve un resultado. Incluso podemos declarar una segunda variable (modificamos su contenido → dato/valor pero no el continente → variable) z, 4., a la que asignamos la función heron() que, automáticamente, nos devolverá un resultado idéntico pero que, como nos enseña el sencillo algoritmo que tenemos en 5., no es operativo puesto que no se ha almacenado en memoria, igual que hubiera ocurrido si en lugar de utilizar la función print() como pie de la función finalizáramos con una sentencia return z.
Completamos este apartado con un último ejemplo en el que confrontamos las "habilidades" de la función integrada print() y la sentencia return en una misma función. ¿Son compatibles? ¿Se pueden usar conjuntamente? Sí, claro que sí. Aunque con un resultado algo curioso. Veámoslo:
Completamos este apartado con un último ejemplo en el que confrontamos las "habilidades" de la función integrada print() y la sentencia return en una misma función. ¿Son compatibles? ¿Se pueden usar conjuntamente? Sí, claro que sí. Aunque con un resultado algo curioso. Veámoslo:
Cuando invocamos a la función, como en 1., Python nos devuelve el resultado de la ejecución. Pero cuando, como en 2., solicitamos operar con él, en esta ocasión, dividiéndolo entre 4, Python nos devuelve el mismo resultado, como consecuencia del uso de la función print() pero, a continuación, nos lanza una excepción que nos advierte que no tenemos el resultado guardado en memoria, 3., con el tipo de dato None (NoneType). Probamos en 4. a reconstruir la función asignándole la variable z a la sentencia return. Cuando invocamos a la función, 5., se nos devuelve el resultado por duplicado: el primer resultado se corresponde a la función print() mientras que el segundo se corresponde con la sentencia return. Cuando en 6. pedimos operar con el resultado de la función, obtenemos nuevamente dos resultados: como es de esperar, el primer resultado, como corresponde a una función no operativa como print(), se nos devuelve el resultado tal cual, inmodificado, de la ejecución de la función, ignorando la operación aritmética, mientras que el segundo resultado, que se corresponde con la ejecución del algoritmo, es el que nos ofrece la sentencia return.
Vamos a estudiar un ejemplo de función def con un único parámetro en este caso, para observar su funcionamiento. Imaginemos que queremos construir una función que responda a la pregunta: "El número que pasamos, ¿es perfecto o no?". Lo primero que tenemos que saber es qué es un numero perfecto, ¿no? Un número perfecto es aquél donde la suma de todos sus divisores perfectos, es decir, aquéllos divisores de resto igual a 0 (traducido a Python, dividendo%divisor == 0), más el número 1, dado que el resultado de dividirse cualquier número que se nos ocurra entre sí mismo, siempre tendrá como resultado 1 y resto igual a 0, es igual al propio número. Por ejemplo, el 6: 6 dividido entre 1 es 6 y su resto = 0; si 6, que es el dividendo, lo dividimos entre 2, su divisor, obtenemos como cociente el 3 y resto = 0; y si 6, que es el dividendo, lo dividimos entre 3, su divisor, obtenemos como cociente 2 y el resto = 0. No hace falta dividir entre 4 y 5 porque ya sabemos que sus restos no van a ser iguales a 0.
Consecuentemente, ¿qué divisores nos han dado como resto 0? Pues el 1, el 2 y el 3. Si sumamos 1 más 2 y más 3, obtenemos 6, nuestro número de partida, luego 6 es un número perfecto. ¿Y cómo traducimos esto a Python? Veámoslo:
Consecuentemente, ¿qué divisores nos han dado como resto 0? Pues el 1, el 2 y el 3. Si sumamos 1 más 2 y más 3, obtenemos 6, nuestro número de partida, luego 6 es un número perfecto. ¿Y cómo traducimos esto a Python? Veámoslo:
Lo primero que hacemos, cómo no, , en 1., es declarar la función mediante la creación, en su cabecera, de la correspondiente línea de código con la sentencia def, el nombre que le hemos dado y por el que podrá ser invocada desde el momento en que cerremos con el pie en adelante. y los paréntesis que, en esta ocasión, llevan un parámetro, un único parámetro, que hemos dado en llamar n, que se transformará en argumento cuando en la invocación o llamada a la función convirtamos n en un número cualquiera. Terminamos con los dos punto preceptivos, que señalan el fin de la cabeza de la función y determinan el ámbito de la misma que deberá mostrarse gráficamente indentado, sangrado, con una tabulación de 4 espacios si nos adscribimos a la nomenclatura del PEP8.
En 2. declaramos una variable que hemos denominado smtr en referencia al concepto de 'sumatorio' dado que, tengámoslo en cuenta, nuestro objetivo es sumar todos los divisores cuyo resto fuera igual a 0. Siempre, aunque no es obligatorio, deberíamos inicializar las variables al principio del cuerpo de la función, en el 'pecho', como si dijéramos, para luego utilizarlas más adelante, en la 'barriga'. Observemos que le asignamos como valor el 0. Esto es para que no tenga incidencia alguna a la hora de calcular, pues lo que realmente nos importa es declarar la variable. Lo veremos de nuevo cuando tratemos más adelante las funciones recursivas donde se torna imprescindible.
En 3. ya introducimos una herramienta de control de flujo, en este caso, un iterador, un loop o bucle for/in asociado a un rango ("de tanto a cuanto") mediante la función ad hoc range(), y cuya lectura vendría a ser como sigue: "Para cada elemento i (número) que vaya desde el 1 al número n que pasemos como argumento cuando invoquemos a la función...". Con esta instrucción conseguimos que se tengan en cuenta todos los números que van desde el 1 hasta el número n que hayamos escogido (para el ejemplo que expusimos, si n=6, serían el 1, el 2, el 3, el 4, el 5 y el 6). No hemos empezado por el 0 porque, puntualicemos, cualquier número dividido entre 0 implica automáticamente una excepción de tipo ZeroDivisionError.
En 2. declaramos una variable que hemos denominado smtr en referencia al concepto de 'sumatorio' dado que, tengámoslo en cuenta, nuestro objetivo es sumar todos los divisores cuyo resto fuera igual a 0. Siempre, aunque no es obligatorio, deberíamos inicializar las variables al principio del cuerpo de la función, en el 'pecho', como si dijéramos, para luego utilizarlas más adelante, en la 'barriga'. Observemos que le asignamos como valor el 0. Esto es para que no tenga incidencia alguna a la hora de calcular, pues lo que realmente nos importa es declarar la variable. Lo veremos de nuevo cuando tratemos más adelante las funciones recursivas donde se torna imprescindible.
En 3. ya introducimos una herramienta de control de flujo, en este caso, un iterador, un loop o bucle for/in asociado a un rango ("de tanto a cuanto") mediante la función ad hoc range(), y cuya lectura vendría a ser como sigue: "Para cada elemento i (número) que vaya desde el 1 al número n que pasemos como argumento cuando invoquemos a la función...". Con esta instrucción conseguimos que se tengan en cuenta todos los números que van desde el 1 hasta el número n que hayamos escogido (para el ejemplo que expusimos, si n=6, serían el 1, el 2, el 3, el 4, el 5 y el 6). No hemos empezado por el 0 porque, puntualicemos, cualquier número dividido entre 0 implica automáticamente una excepción de tipo ZeroDivisionError.
Con la inclusión del bucle for/in nos garantizamos un iterado, esto es, que Python divida n entre todos y cada uno de los números que van dese 1 al valor mismo que asuma n cuando pase de parámetro a argumento, es decir, entre todos sus divisores posibles.
Ya en 4,. introducimos el condicional if que comprueba los resultados del cálculo n%i, esto es, el resto de dividir n entre cada uno (iterado ítem por ítem como, recordemos, actúa siempre el bucle for/in) de los números que van desde el 1 al número que asuma n en el paso de parámetros. La cuestión estriba en que si el resto es 0 (n%i==0) entonces sucede 5., esto es, que al primer divisor que cumpla la condición lo sumamos a 0, que es el valor original que le asignamos a la variable smtr, en la primera línea de código, tras el encabezado (en el 'pecho'). Por eso smtr valía 0: además de para inicializar la variable, asignarle un valor, el 0, que no interfiera por su valor nulo en la suma final de los diferentes valores que vayamos obteniendo. Y según nuestro amado condicional vaya encontrando divisores cuyo resto fuera igual a 0, esto es, que cumpla la condición, se los vamos sumando a la variable smtr que actúa como variable-contenedor, hasta completar el rango o, lo que es lo mismo, hasta llegar a n-1 puesto que, como ya sabemos, Python no toma en cuenta el valor final de la función range() (for i in range(0,5):print(i) → 0,1,2,3,4). Esta cualidad por la que la variable puede ir sumando valores progresivamente y, en consecuencia, modificando paulatinamente su valor conforme la condición se vaya cumpliendo (devenga True), de acuerdo a las sucesivas iteraciones que impone el bucle for/in, es lo que identifica a la variable smtr como una variable de valor dinámico, y se produce gracias al operador aritmético +=.
Entonces pasamos a 6.. Es importante hacer notar que ahora, la nueva condición if se sitúa por debajo de for, señalando que está al mismo nivel lógico, contrariamente a la condición inmediatamente anterior, que sí pertenece al bloque de código de for por estar indentada en él. Podemos ver así lo importante que resultan las indentaciones para discernir qué código depende de otro, y cuál o cuáles se sitúan al mismo nivel en el flujo de ejecución. Obviamente, ya se ha completado la iteración para el total de los ítems del rango y tenemos que ver qué vamos a hacer a continuación.
Dicho esto podemos hacer la siguiente lectura del código: "Para cada divisor situado en el rango de números que van del 1 al número n, y una vez se hayan sumado todos aquéllos, y sólo aquéllos, que cumplan la condición n%i==0, si dicho número resultante es igual a n, entonces 7., es decir, que la función deviene True → 'sí, este número es un número perfecto'. Y para cualquier otro caso, 8., else, ocurrirá 9., donde la función devendrá False → 'no, este número no es un número perfecto'".
Podemos observar que, en una misma función, por mor del condicional, tenemos dos sentencias return sin que ello afecte para nada a la operatividad de la función. De hecho, podemos insertar todas las sentencias return que queramos si, por ejemplo, empleamos elif como churros en una estructura condicional. ¡Venga!, ¡Hala...!
Ya en 4,. introducimos el condicional if que comprueba los resultados del cálculo n%i, esto es, el resto de dividir n entre cada uno (iterado ítem por ítem como, recordemos, actúa siempre el bucle for/in) de los números que van desde el 1 al número que asuma n en el paso de parámetros. La cuestión estriba en que si el resto es 0 (n%i==0) entonces sucede 5., esto es, que al primer divisor que cumpla la condición lo sumamos a 0, que es el valor original que le asignamos a la variable smtr, en la primera línea de código, tras el encabezado (en el 'pecho'). Por eso smtr valía 0: además de para inicializar la variable, asignarle un valor, el 0, que no interfiera por su valor nulo en la suma final de los diferentes valores que vayamos obteniendo. Y según nuestro amado condicional vaya encontrando divisores cuyo resto fuera igual a 0, esto es, que cumpla la condición, se los vamos sumando a la variable smtr que actúa como variable-contenedor, hasta completar el rango o, lo que es lo mismo, hasta llegar a n-1 puesto que, como ya sabemos, Python no toma en cuenta el valor final de la función range() (for i in range(0,5):print(i) → 0,1,2,3,4). Esta cualidad por la que la variable puede ir sumando valores progresivamente y, en consecuencia, modificando paulatinamente su valor conforme la condición se vaya cumpliendo (devenga True), de acuerdo a las sucesivas iteraciones que impone el bucle for/in, es lo que identifica a la variable smtr como una variable de valor dinámico, y se produce gracias al operador aritmético +=.
ROQUE DE TABORNO, NORTE DEL MACIZO DE ANAGA, NORESTE DE TENERIFE. |
Entonces pasamos a 6.. Es importante hacer notar que ahora, la nueva condición if se sitúa por debajo de for, señalando que está al mismo nivel lógico, contrariamente a la condición inmediatamente anterior, que sí pertenece al bloque de código de for por estar indentada en él. Podemos ver así lo importante que resultan las indentaciones para discernir qué código depende de otro, y cuál o cuáles se sitúan al mismo nivel en el flujo de ejecución. Obviamente, ya se ha completado la iteración para el total de los ítems del rango y tenemos que ver qué vamos a hacer a continuación.
Dicho esto podemos hacer la siguiente lectura del código: "Para cada divisor situado en el rango de números que van del 1 al número n, y una vez se hayan sumado todos aquéllos, y sólo aquéllos, que cumplan la condición n%i==0, si dicho número resultante es igual a n, entonces 7., es decir, que la función deviene True → 'sí, este número es un número perfecto'. Y para cualquier otro caso, 8., else, ocurrirá 9., donde la función devendrá False → 'no, este número no es un número perfecto'".
Podemos observar que, en una misma función, por mor del condicional, tenemos dos sentencias return sin que ello afecte para nada a la operatividad de la función. De hecho, podemos insertar todas las sentencias return que queramos si, por ejemplo, empleamos elif como churros en una estructura condicional. ¡Venga!, ¡Hala...!
Asumimos, compungidos, que la explicación no sólo haya resultado prolija en exceso sino, incluso, innecesaria, por lo que pedimos disculpas en la medida en que proceda. Sin embargo,hemos entendido que hacerlo puede ayudarnos, sobre todo a los más despitados de entre nosotros, a comprender mejor la mecánica de las funciones def sobre las que, en el fondo, desemboca, confluye todo, absolutamente todo lo que hemos estudiado y aprendido desde el capítulo uno de este manual hasta el momento presente. Aún más, las funciones definidas por el usuario constituyen el centro de gravedad de la Programación Orientada a Objetos que veremos más adelante.
Podemos, finalmente, añadir, por ejemplo, una función print() que nos muestre en pantalla qué divisores han arrojado como resto 0, que, para el caso de los números perfectos, serán el total de los divisores.
Podemos, finalmente, añadir, por ejemplo, una función print() que nos muestre en pantalla qué divisores han arrojado como resto 0, que, para el caso de los números perfectos, serán el total de los divisores.
En una función definida por el usuario, podemos asignar un valor a uno o varios parámetros, como si fueran pares datos/valor, de modo análogo a los diccionarios, de tal modo que si en el momento de llamar a la función no pasamos a ninguno de éstos, la función tomará por defecto los valores que hayamos asignado previamente.
Esta "técnica" resulta muy útil para aplicar reglas con factores fijos (en la primera función, dividir "siempre" entre dos; en la segunda, concatenar "siempre" una misma string: 'ero/a'; en la tercera añadir tres parámetros con un valor fijo, donde el primero se aplica siempre, y los otros dos dependen de la evaluación del condicional para mostrarse, o bien uno o bien el otro). Como podemos ver, todo esto redunda en una programación más elegante, práctica y simple,
Podemos, incluso, introducir como parámetro alguna de las propiedades asociadas a los tipos de datos, como en este caso, un indexado sobre una lista dada, aunque siempre tendremos que invocan la función según vayamos modificando las listas.
Cuando le asignamos un valor a una variable dentro de una zona de parámetros en la cabecera de una función estamos introduciendo un parámetro opcional, un par dato=valor. Estos serán los valores que tenga por defecto la función a ejecutarse si no se sobrescriben, es decir, si en la llamada a la función no modificamos el valor de la variable, del dato. Sin embargo, podemos cambiarlos e introducir otros nuevos siempre y cuando respetemos la sintaxis y la jerarquía pertinente como, por ejemplo, que los parámetros posicionales (obligatorios, y así los llamaremos) anteceden siempre a los opcionales (o por defecto, que también se llaman así). Veamos:
AÚN NOS PODEMOS ATREVER CON LOS MÉTODOS. AHORA, ESO SÍ, CON EL TRAJE DE SUPERMÁN, COMO EN ESTE EJEMPLO CON EL MÉTODO list.sort() DE LAS LISTAS.
Aunque, eso sí, con el mismo condicionante que en el caso anterior: incrustar de nuevo la función bajo la lista con cada modificación que hagamos de ésta.
Como vemos, 1. y 2. arrojan el mismo resultado, por lo que resulta mucho más práctico, dado el caso, recurrir a la opción 2. que a la 1. cuando, simplemente, queramos ordenar una lista. De cajón, ¿no? Por este motivo, su auténtica utilidad radica en lo que podamos hacer con ello dentro de la misma función. En 3. tenemos un buen ejemplo.
AVIFAUNA EN EL CAUCE DEL BARRANCO DEL CERCADO DE SAN ANDRÉS, SUROESTE DE ANAGA. |
Proponemos otro ejemplo más para afianzar los conceptos: construimos una función que nos devuelva una cadena que se le pase como argumento. Estableceremos la propiedad length como delimitador para el número de caracteres, de tal modo que si la cadena no sobrepasa un número concreto de éstos, devuelva una segunda string como finalización para aceptarla.
Tenemos que en 1. declaramos la función texto con tres parámetros: una cadena, una longitud a la que le damos un valor, y una segunda cadena asociada a la variable 'acotacion'. En el paso siguiente, 2., establecemos la condición que deseamos que Python valore: que la longitud de la cadena que le pasemos sea mayor que el valor de la longitud que hemos dado, 25 en este caso. La instrucción 3. es la clave: en el caso de que así fuera, esto es, que la condición devenga True, cambiamos el valor original que tenía asociado la variable 'cadena' por este otro: una slice (rebanada) del texto original que tendrá como índice de inicio el comienzo mismo de la cadena (por eso no hemos puesto nada, aunque también, como en 4. nos hubiera resultado válido asignar 0 como índice de inicio), y como índice final el número que resulte de restar a la longitud, 'length', que hemos elegido nosotros, recordémoslo, 25, la longitud de 'acotacion' que, como podemos observar, es una string. El intérprete de Python mostrará esta rebanada a la que concatenará en virtud del operador + el valor de la variable 'acotacion' que seleccionamos. Igualmente, como en 5., también nos hubiera servido colocar directamente una cifra cualquiera, siempre y cuando permita cumplir con los requisitos, claro, no vaya a ser que nos genere un bucle infinito o algo peor, en lugar de len(acotacion).
Dada la importancia que tienen las funciones definidas por el usuario en la Programación Orientada a Objetos, paradigma de programación actual donde Python muestra sus mayores fortalezas y que estudiaremos en el siguiente manual, querríamos insistir sobre una serie de puntos que habremos de tener muy en cuenta a la hora de construir una o varias funciones de este tipo:
- Las funciones definidas por el usuario pueden no llevar argumentos, llevar uno tan sólo o llevar varios, en este último caso, separados por comas.
- Estos argumentos constituyen la concreción en dato/valor de los parámetros, que se muestran como "guías" o variables genéricas. Hablamos de la asignación de un valor concreto, específico, a los mencionados parámetros, bien de forma directa o indirecta, en este caso, mediante variables que hayamos declarado previamente con sus valores correspondientes. Esto es lo que se conoce como "paso de parámetros".
- Toda función espera igual número de argumentos que parámetros contenga entre los paréntesis, en la llamada "zona de parámetros". y, además, en su mismo orden. En caso contrario, Python lanzará una excepción.
- Toda función definida por el usuario tiene una cabecera, que es donde se coloca la sentencia def; su nombre, lo más representativo posible de lo que se espera que haga (una buena opción es recurrir a verbos); un paréntesis que almacena o no parámetros, como ya hemos dicho en el punto 1.; y los dos puntos que cierran la línea de instrucción que señala la apertura de un bloque de código sangrado o identado. Por sí mismas, las funciones no tienen forma de proporcionarnos información sobre sí mismas. Tan solo podemos colegir su utilidad deduciéndolo de su ubicación en el flujo del programa, su relación con el resto del código y los algoritmos que contiene en su cuerpo. Para ayudarnos con ésto y facilitarle las cosas a otros programadores que contribuyan a mantener o mejorar nuestro código, podemos (y debemos hacerlo de acuerdo a las especificaciones de estilo del PEP8) incluir información al uso de los docstrings. Normalmente, se suele ubicar justo debajo de la cabecera de la función, entre ésta y el cuerpo. Por consenso, se suele escribir una primera línea con una descripción breve de la función. Se deja a continuación una línea en blanco y añadimos una descripción más completa. Es habitual añadir algunos ejemplos de uso a modo de ilustración. Veremos un ejemplo de esto al cabo de esta enumeración.
- Toda función definida por el usuario tiene un cuerpo que puede ser más o menos extenso, desde una sóla línea de código a miles de ellas, en el que se pueden incluir incluso otras funciones def que, a su vez, pueden incorporar otras más, y aún otras, sucesivamente. Estaríamos hablando de funciones anidadas con diferentes niveles de anidación.
- Toda función definida por el usuario tiene un pie conformado por la sentencia return, indicándole al intérprete de Python que el cuerpo de la función ya está completo, que la función ya está terminada (definida) y acaba aquí. La sentencia return es necesaria para guardar en memoria el resultado de la ejecución y retornar un valor: el resultado de la ejecución de la función. En algunos casos podemos recurrir a la función predefinida print(), pero si sólo utilizamos ésta sólo se nos mostrará un resultado en pantalla sin almacenar el resultado en memoria.
- Las variables que están fuera del ámbito de la función, y el ámbito de la función, recordemos, es el espacio que media entre la declaración def que comienza la función y return que finaliza la misma, son variables globales que están disponibles para ser usadas por la función y por cualquier otra función, definida o no, que tengamos implementadas en nuestro programa. Es decir, son visibles para todas. Las variables que están dentro del ámbito de la función, sin embargo, declaradas en el cuerpo, son variables locales, visibles tan sólo para las función que las contiene e invisible para el resto y que 'mueren' al ejecutarse la función.
- Entre los paréntesis de la función, en la "zona de parámetros", es posible asignar un valor a un parámetro mediante el consabido operador = de asignación, de manera que, si cuando vayamos a efectuar el paso de parámetros y en el momento de especificar cada uno de ellos con un valor, a éste en concreto, no le pasamos ninguno, asumirá por defecto el valor que le hayamos asignado previamente en la mencionada zona.
Añadamos otro ejemplo más:
FORMACIONES DE PIEDRA ARENISCA Y EROSIÓN HÍDRICA EN LOS CAUCES DE LOS BARRANCOS DEL SUR DE TENERIFE. |
Veamos otra, igualmente sencilla, sobre la que podemos comparar la utilidad de un parámetro con valor pasado, por defecto, en tanto que hacemos uso obligatorio del mismo, independientemente de cualquier otro/s parámetro/s que concurran a la función:
Existe una diferencia que debemos remarcar entre un parámetro de paso obligatorio (aquél que no tiene asignado a priori ningún valor) y un parámetro con valor por defecto. Y es que éste último tiene un carácter opcional, ya que el intérprete de Python puede usar el valor asignado por defecto y que, en consecuencia, ya conoce mientras que un parámetro que no disponga de ninguna asignación previa, por defecto, le resulta completamente desconocido hasta el momento mismo, mágico e irrepetible, en que lo convertimos en argumento.
Como podemos observar, su uso adecuado simplifica y clarifica el código. Sin embargo, hay un asunto ciertamente sutil al que conviene prestar atención: los valores por defecto se generan en el instante mismo en que se crea la función y no cuando ésta es llamada o invocada. En el caso de aquéllos argumentos que almacenen datos inmutables, como números, tuplas y cadenas, no se producen alteraciones de ningún tipo, aunque en el caso de los mutables, como listas y diccionarios, podemos incurrir inadvertidamente en un error:
Este ejemplo lo tomamos del libro Python 3, de Mark Summerfeld, editorial Anaya Multimedia, pg. 180. En la función que hemos definido, consignamos un parámetro dentro del parámetro con un valor por defecto, en este caso, una lista vacía que, como lista, constituye un objeto mutable. Si nuestra intención es almacenar en ella los diferentes números pares que vayamos pasando, no hay conflicto: todos los números pares que vayamos introduciendo irán engrosando paulatinamente una misma lista. El problema sobreviene si nuestra intención inicial era construir una lista nueva cada vez con cada número par. En este caso, deberíamos haber codificado de la siguiente manera:
Ahora podemos crear listas nuevas cada vez. Es una opción inteligente recurrir a un valor None por defecto., y crear así un objeto adicional con cualquiera de los tipos de datos mutables: diccionarios, listas, conjuntos,...
ENORME ARCO DE PIEDRA NATURAL, SURGIDO COMO EFECTO DE LA EROSIÓN HÍDRICA, CON EL PASO DE AGUA CAUCE ABAJO EN EL BARRANCO DE LERE, FASNIA, SUR DE TENERIFE. |
Vamos a extendernos ahora en un ejemplo de script que nos permitirá diferenciar bien entre variables globales y variables locales. El ejemplo lo tomamos del libro Introducción a Python, de Andrés Marzal e Isabel Gracia, editado por la universidad Jaime I.
Vamos a ver ahora lo que se denomina en programación una traza del programa que acabamos de codificar:
1.- La línea 1 importa de la librería estándar de Python (stdlib) math, que nos permite traer a nuestro código las funciones matemáticas que vamos a utilizaren nuestros algoritmos (sqrt, asin, pi). De acuerdo a las especificidades del PEP 8, dejamos un prompt vacío.
2,- Las líneas 3-5 crean una función def que enseña a Python cómo se realiza un cálculo determinado al que denominamos 'area_triangulo', y que necesita tres datos de entrada. Para ahorrar espacio, hemos colocado el algoritmo junto a a la sentencia return.
3.- las líneas 7-9 crean una segunda función def que que enseña a Python cómo se realiza un cálculo determinado, al que denominamos 'angulo_alfa', y que también necesita tres datos de entrada.
4.- las líneas 11-17 definen una tercera función que denominamos 'menu'. Es una función que construimos sin parámetros cuyo cometido consiste en mostrar un menú al usuario con dos opciones a elegir, esperar a que el usuario escoja una y devolver la opción seleccionada.
5.- Las líneas 19-21 permiten la introducción de datos por parte del usuario, que tendrán un tipo flotante. Para nuestro ejemplo, tal y como se puede ver en el ejemplo, hemos elegido los siguientes valores: lado1 = 15, lado2=20 y lado3=25.
6.- La línea 23 contiene una llamada a la función menu(). En este punto, Python memoriza que se encontraba ejecutando la línea 23 cuando se produjo una llamada a la función y deja su ejecución en "suspenso". Saltará entonces a la línea 12, es decir, al cuerpo de la función menu(). Vamos a seguir ahora el flujo de ejecución en dicho proceso:
- Se ejecuta la línea 12. La variable local de la función menu() almacena el valor 0.
- En la línea siguiente, 13, hay un bucle while. "¿Es 'opcion' distinto de 1 y 2?" Sí. Entramos, pues, en el bloque de código que pertenece a dicho bucle while: la siguiente línea a ejecutar será, pues, la 14.
- En la línea 14 se imprime mediante la llamada a la función integrada print() un texto en la pantalla, que se corresponde con el de la primera opción.
- En la línea 15 se imprime por el mismo procedimiento otro texto en pantalla, que se corresponde con el de la segunda opción.
- Como el bloque del bucle no tiene más líneas, volvemos a la línea 13. Nos volvemos a preguntar, "¿Es 'opcion' distinto de 1 y a la vez distinto de 2?" No: En nuestro ejemplo, hemos escogido la opción 1. Así pues, el bucle while finaliza y saltamos a la línea 17.
- A través de la sentencia return, se devuelve el valor 1 (insistimos, el que hemos elegido para nuestro ejemplo) , y la variable local 'opcion' se destruye.
7.- La ejecución de la llamada a la función menu() ha finalizado, así que Python regresa a la línea 23, que es la línea de código desde la que se produjo la llamada y cuya ejecución había quedado en suspenso. El valor devuelto por la función (el valor 1) se almacena ahora en una variable que hemos llamado 's' → s = menu(), que sabemos que es global en tanto en cuanto no forma parte del ámbito de ninguna de las tres funciones que hemos definido.
8.- La línea 25 introduce un operador de comparación para comparar el valor de 's' con el valor 1 (de la 'opcion') y, como son iguales, salta a la siguiente línea de ejecución, la 26. Las líneas 27 y 28 no se ejecutarán, dado que no hemos escogido la opción 2, en cuyo caso sí lo haría, obviando por ende las líneas 25 y 26.
9.- La línea 26 asigna a la variable 'resultado' el resultado de invocar a la función 'area_triangulo(a, b, c)', con los valores 15, 20 y 25 respectivamente. Al invocar la función, el flujo de ejecución del script "salta" al cuerpo de la función mencionada y la ejecución de la línea 26 queda en suspenso.
- Saltamos, pues, a la línea 4, con la que empieza el cuerpo de la función 'area_triangulo()'.¡Ojo! Los parámetros a, b y c se crean como variables locales, y toman los valores 15, 20 y 25 respectivamente en virtud de la asignación que hemos hecho en la línea 26 gracias a los input de las líneas 19-21. En la línea 4 se asigna a la variable 's' una nueva variable local: el valor que resulte de evaluar (a + b + c) / 2.0, es decir, 30.0.
- En la línea 5 se devuelve el resultado de evaluar 'sqrt(5 * (s-a) * (s-b) * (s-c))', que es 150.0. Tanto 's' como los tres parámetros, una vez ejecutada la función, dejan de existir.
10.- Volvemos a la línea 26, cuya ejecución estaba en suspenso a la espera de conocer el valor de la llamada a la función 'area_triangulo()'. el valor devuelto, 150.0, se asigna a 'resultado'.
11.- La línea 30 muestra por pantalla el valor actual de 's'...¿Y qué valor es éste? Al ejecutar la línea 23 le asignamos a 's' el valor 1, pero al ejecutar la línea 4, le asignamos el valor 30.0 ¿Debe salir en pantalla el valor 30.0? No: la línea 23 asignó el valor 1 a la variable global 's'; el 30.0 de la línea 4 se asignó a la variable local 's' de la función 'area_triangulo()', que ya no existe.
12.- Finalmente, el valor de 'resultado' se muestra en la pantalla en la línea 31.
MONTEVERDE EN LAS ZONAS ALTAS DE ICOD DE LOS VINOS, CENTRO DE TENERIFE. |
En este ejemplo hemos llamado 's' a dos variables diferentes y cada una de ellas recuerda su valor sin interferir con el valor de la otra.. Si accedemos a 's' desde 'area_triangulo()' accedemos a la 's' local; si accedemos a 's' desde fuera de cualquier función, accedemos a la 's' global.
En el caso de que hubiéramos seleccionado la opción 2, a partir de aquí hubiéramos saltado a la línea 8, en la que se nombra a la función y de aquí, inmediatamente a la 9, donde encontramos una llamada a la función 'area_triangulo()'. Python buscará si existe tal función en el programa y, efectivamente, la encuentra en la línea 3. La ejecuta con los argumentos que le hemos pasado, en nuestro ejemplo, 15, 20 y 25 respectivamente, y devuelve un resultado, 30.0, que llevará a la línea 9 almacenándola en la variable local 's'. Ya en la línea 10 ejecuta el algoritmo completo, nos devuelve un resultado, 36.869897..., dejando dichas variables de existir que, de acuerdo a la línea 30 almacenará en la variable 'resultado' que se nos mostrará en pantalla en virtud de la línea 33 del código.
Vamos a apoyarnos ahora en Python para principiantes, de Eugenia Bahit, en Librosweb, para profundizar en el concepto de llamadas de retorno.
Como ya sabemos, en Python podemos llamar o invocar a una función dentro de otra (anidación):
Pero puede suceder que queramos efectuar la llamada dinámica, es decir, sin conocer el nombre de la función que queramos llamar que, en el caso del ejemplo anterior, era sal(), y que pasamos como argumento de la función sal2(). Esto es lo que se denomina llamada sin retorno.
Para conseguirlo, Python dispone de dos funciones nativas: locals() y globals(). En ambos casos retorna un objeto de tipo diccionario:
- locals() ➨ Diccionario de todos los elementos de ámbito local.
- globals() ➨ Diccionario de todos los elementos de ámbito global.
Vamos a ver cómo se aplican en el siguiente ejemplo:
En 1.- definimos una segunda función a la que le pasamos como argumento una variable, fun (de fun-ción), y que sabemos por las comillas vacías que le hemos asignado que recibirá un objeto de tipo string, cadena, como valor para almacenar que, por el momento, desconocemos. En 2.-, junto a la sentencia return que devuelve el resultado de ejecutar toda función definida por el usuario, colocamos la instrucción con una sintaxis, lo podemos ver perfectamente, característica: primero, la llamada a la función nativa globals() de Python, tal cual, sin paños calientes ni ná, con los paréntesis (área o zona de parámetros) vacío; segundo, dos corchetes vacíos, [ ] que envuelven al nombre de la variable fun que declaramos como parámetro de la función llamada_retorno; y por último, dos paréntesis, ( ), vacíos. Esta estructura, [fun]() apunta a la función definida sal que modelizamos más arriba, como concluimos en 3,-, donde fun adquiere como valor la string "sal" que es el nombre de la función anterior. Como el resultado de la ejecución de la función sal es "HOLA", el valor de la función sal es "HOLA" y, como éste se encuentra fuera del ámbito de la función llamada_retorno, ejerce como global, igual que sucede con una variable global. Pero como sal no es una variable sino una función, en lugar de hacer la llamada que ya conocemos para las variables globales global nombre_de_la_variable, tenemos que usar la función nativa globals()[fun]() para conseguir el mismo efecto: traer algo de fuera de una función al interior de la misma. Por eso, el resultado de la ejecución de la función llamada_retorno devuelve "HOLA".
- Traer una variable externa a la función al cuerpo de ésta: global nombre_de_la_variable
- Traer una función externa a la función al cuerpo de ésta: globals()["nombre_de_la_función"]()
Ambas desempeñan una función cuasi similar:
global nombre_de_la_variable ⟺ globals()["nombre_de_la_función"]()
Para tomar un poco de aire y despejar las neuronas vamos a introducir un inciso para mostrar una regla de diseño de programas ajustado en gran medida a las especificaciones del PEP8.
Como norma general, primero son la importaciones, a continuación las funciones y, finalmente, las sentencias que configuran el programa principal.
Debemos tener siempre presente que la legibilidad del código debe ser nuestro principal objetivo como programadores junto a la eficiencia operativa del mismo.
Finalizamos aquí con unos ejercicios sencillos para ayudarnos a reforzar nuestros conocimientos.
T4. BLOQUE DE EJERCICIOS 10. SOLUCIONES.
MONTAÑETA DE ICOD DE LOS VINOS. LADERAS DE LA COMARCA DE ISLA BAJA. NOROESTE DE TENERIFE. |
Drop here!
Hola!
ResponderEliminarGracias por publicar todo el contenido, tu blog me ayuda mucho!
Muchísimas gracias, amig@. Un placer compartir conocimientos y ayudar en la medida de nuestras posibilidades. Ojalá este blog te siga sirviendo de ayuda. Saludos.
ResponderEliminarMuchas gracias por tu blog. Estoy recién aventurandome con Phyton.
ResponderEliminarMuchas gracias por tu comentario, Electrónico de Concepción. Mis mejores deseos para tu aprendizaje. Aquí estamos. Saludos.
ResponderEliminarMuchas gracias! el mejor blog que encontre hasta ahora! sigan a si!
ResponderEliminarMuchísimas gracias por sus palabras. No deje de hacer clic sobre la imagen del libro, en el margen superior derecho de cada página de este blog para acceder a otros recursos, blogs incluidos, que pueden ayudarle, al margen de otras websites, excelentes y muy didácticas, como https://www.hektorprofe.net/, y otras, donde podrá aumentar y mejorar sus conocimientos. Saludos.
ResponderEliminar