From fa0053899fc12744553aeaf3927d7d6b962a314d Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Sun, 24 Nov 2019 19:19:24 +0100 Subject: functions almost done --- src/04_funciones.md | 228 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 207 insertions(+), 21 deletions(-) (limited to 'src/04_funciones.md') diff --git a/src/04_funciones.md b/src/04_funciones.md index 7cee36c..0151bcd 100644 --- a/src/04_funciones.md +++ b/src/04_funciones.md @@ -13,7 +13,7 @@ def inc(a): return b ``` -Si lanzamos: +Si la llamamos: ``` python >>> inc(1) @@ -22,7 +22,8 @@ Si lanzamos: 11 ``` -Si preguntamos por `b`: +Cuidado con las declaraciones internas en las funciones. Si preguntamos por +`b`: ``` python >>> b @@ -93,7 +94,7 @@ Por ejemplo: ... def mayor_que_4(a): ... return a > 4 ... return list( filter(mayor_que_4, lista) ) -... +... >>> filtra_lista( [1,2,3,4,5,6,7] ) [5, 6, 7] ``` @@ -135,7 +136,7 @@ función lambda: ``` python >>> def filtra_lista( lista ): ... return list( filter(lambda x: x > 4, lista) ) -... +... >>> filtra_lista( [1,2,3,4,5,6,7] ) [5, 6, 7] ``` @@ -219,9 +220,9 @@ Ilustrándolo en un ejemplo: ``` python >>> a = 1 >>> def f(): -... a = 2 +... a = 2 ... print(a) -... +... >>> a 1 >>> f() @@ -242,9 +243,9 @@ que se altere, sino el contexto padre que la albergue. >>> a = 1 >>> def f(): ... global a -... a = 2 +... a = 2 ... print(a) -... +... >>> f() 2 >>> a @@ -300,25 +301,210 @@ función que las creaba terminó y luego volvió a iniciarse. Este funcionamiento donde el comportamiento de las funciones depende del lugar donde se crearon y no del contexto donde se ejecutan se conoce como *scope -léxico* y esta forma de implementarlo, haciendo que cada función recuerde el -contexto en el que se creó se denomina *closure*. +léxico*. -Concretamente, las *closures* son una forma de implementar el *scope léxico* en -un lenguaje cuyas funciones sean *first-class citizens*, como es el caso de -python, y su funcionamiento se basa en la construcción de los contextos y su -asociación a una función capaz de recordarlos aunque la función madre haya -terminado. +Las *closures* son una forma de implementar el *scope léxico* en un lenguaje +cuyas funciones sean *first-class citizens*, como es el caso de python, y su +funcionamiento se basa en la construcción de los contextos y su asociación a +una función capaz de recordarlos aunque la función madre haya terminado. -A nivel práctico, las *closures* son útiles. +Python analiza cada función y revisa qué referencias del contexto superior +deben mantenerse en la función. Si encuentra alguna, las asocia a la propia +función creando así lo que se conoce como *closure*, una función que recuerda +una parte del contexto. No todas las funciones necesitan del contexto previo +así que sólo se crean *closures* en función de lo necesario. -TODO +Puedes comprobar si una función es una *closure* analizando su campo +`__closure__`. Si no está vacío (valor `None`), significará que la función es +una *closure* como la que ves a continuación. Una *closure* que recuerda un +*int* del contexto padre: -## Argumentos de entrada +``` python +>>> f.__closure__ +(,) +``` + +Lo que estás viendo lo entenderás mejor cuando llegues al apartado de +programación orientada a objetos. Pero, para empezar, ves que contiene una +tupla con una `cell` de tipo *integer*. + +A nivel práctico, las *closures* son útiles para muchas labores que iremos +desgranando de forma accidental. Si tienes claro el concepto te darás cuenta +dónde aparecen en los futuros ejemplos. + + +## Argumentos de entrada y llamadas + +Los argumentos de entrada se definen en la declaración de la función y se ha +dado por hecho que es evidente que se separan por comas (`,`) y que, a la hora +de llamar a la función, deben introducirse en el orden en el que se han +declarado. Por mucho que esto sea cierto, requiere de una explicación más +profunda. + +### Callable + +En python las funciones son un tipo de *callable*, «cosa que puede ser llamada» +en inglés. Esto significa, de algún modo que hay otras cosas que pueden ser +llamadas que no sean funciones. Y así es. + +Para python cualquier valor que soporte la aplicación de los paréntesis se +considera «llamable». En el apartado de programación orientada a objetos +entenderás esto en detalle. De momento, piensa que, igual que pasa al acceder a +los campos de una colección usando los corchetes, siempre que python se +encuentre unos paréntesis después de un valor tratará de ejecutar el valor. Así +que los paréntesis no son una acción que únicamente pueda aplicarse en nombres +de función[^lambdas-ejemplo] y python no lanzará un fallo de sintaxis cuando +los usemos fuera de lugar, si no que será un fallo de tiempo de ejecución al +darse cuenta de lo que se intenta ejecutar no es ejecutable. + +``` python +>>> 1() +Traceback (most recent call last): + File "", line 1, in +TypeError: 'int' object is not callable +``` + +[^lambdas-ejemplo]: Aunque en realidad esto ya lo has visto en los ejemplos de + las funciones lambda. + +#### Caso de estudio: Switch Case + +Si quieres ver un ejemplo avanzado de esto, te propongo la creación de la +estructura *switch-case*[^switch-case], que puede encontrarse en otros +lenguajes, pero que en lugar de usar una estructura basada en un *if* con +múltiples *elif* uses un diccionario de funciones. + +Las funciones son valores, por lo que pueden ocupar un diccionario como +cualquier otro valor. Construyendo un diccionario en cuyas claves se encuentran +los casos del *switch-case* y en cuyos valores se encuentran sus funciones +asociadas se puede crear una sentencia con el mismo comportamiento. + +En el siguiente ejemplo se plantea una aplicación por comandos. Captura el +tecleo del usuario y ejecuta la función asociada al comando. Las funciones no +están escritas, pero puedes completarlas y analizar su comportamiento. Las +palabras que no entiendas puedes consultarlas en la ayuda. + +``` python +def borrar(*args): + pass +def crear(*args): + pass +def renombrar(*args): + pass + +casos = { + "borrar": borrar, + "crear": crear, + "renombrar": renombrar +} + +comando = input("introduce el comando> ") + +try: + casos[comando]() +except KeyError: + print("comando desconocido") + +``` -### Positional and Keyword Arguments +[^switch-case]: -#### Defaults +### Positional vs Keyword Arguments -> WARNING! MUTABLE DEFAULTS +Las funciones tienen dos tipos de argumentos de entrada, aunque sólo hayamos +mostrado uno de ellos de momento. + +El que ya conoces se denomina *positional argument* y se refiere a que son +argumentos que se definen en función de su posición. Los argumentos +posicionales deben ser situados siempre en el mismo orden, si no, los +resultados de la función serán distintos. Las referencias `source` y `target` +toman el primer argumento y el segundo respectivamente. Darles la vuelta +resulta en el resultado opuesto al que se pretendía. + +``` python +def move_file ( source, target ): + "Mueve archivo de `source` a `target" + pass + +move_file("file.txt", "/home/guido/doc.txt") + # "file.txt" -> "/home/guido/doc.txt" +move_file("/home/guido/doc.txt", "file.txt") + # "/home/guido/doc.txt"-> "file.txt" +``` + +Los *keyword argument* o argumentos con nombre, por otro lado, se comportan +como un diccionario. Su orden no importa pero es necesario marcarlos con su +respectiva clave. Además, son opcionales porque en el momento de la declaración +de la función python te obliga a que les asocies un valor por defecto +(*default*). En el siguiente ejemplo se convierte la función a una basada en +argumentos con nombre. No se han utilizado valores por defecto especiales, pero +pueden usarse otros. + +``` python +def move_file( source=None, target=None): + "Mueve archivo de `source` a `target" + pass + +move_file(source="file.txt", target="/home/guido/doc.txt") + # "file.txt" -> "/home/guido/doc.txt" +move_file(target="/home/guido/doc.txt", source="file.txt") + # "file.txt" -> "/home/guido/doc.txt" +``` + +> NOTA: Si quieres que sean obligatorios, siempre puedes lanzar una excepción. + +Para funciones que acepten ambos tipos de argumento, es obligatorio declarar e +introducir todos los argumentos posicionales primero. Es lógico, porque son +los que requieren de una posición. + +También es posible declarar funciones que acepten cualquier cantidad de +argumentos de un tipo u otro. Ésta es la sintaxis: + +``` python +def argument_catcher( *args, **kwargs ) + "Función ejecutable con cualquier número de argumentos de entrada, tanto + posicionales como con nombre." + print( args ) + print( kwargs ) +``` + +Los nombres `args` y `kwargs` son convenciones que casi todos los programadores +de python utilizan, pero puedes seleccionar los que quieras. Lo importante es +usar `*` para los argumentos posicionales y `**` para los argumentos con +nombre. + +Prueba a ejecutar la función del ejemplo, verás que los argumentos posicionales +se capturan en una tupla y los argumentos con nombre en un diccionario. + +Este tipo de funciones multiargumento se utilizan mucho en los *decorators*, +caso que estudiaremos al final de este capítulo. + +#### Peligro: Mutable Defaults + +Existe un caso en el que tienes que tener mucho cuidado. Los valores por +defecto en los argumentos con nombre se memorizan de una ejecución de la +función a otra. En caso de que sean valores inmutables no tendrás problemas, +porque su valor nunca cambiará, pero si almacenas en ellos valores mutables y +los editas, la próxima vez que ejecutes la función los valores por defecto +habrán cambiado. + +La razón por la que los valores por defecto se recuerdan es que esos valores se +construyen en la creación de la función, no en su llamada. Lógicamente, puesto +que es en la sentencia `def` donde aparecen. + +``` python +>>> def warning(default=[]): +... default.append(1) +... return default +... +>>> warning() +[1] +>>> warning() +[1, 1] +>>> warning() +[1, 1, 1] +>>> warning() +[1, 1, 1, 1] +``` ## Decorators -- cgit v1.2.3