From e81137474f102d71fe0b25307bcf88810c1b2d43 Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Wed, 4 Mar 2020 13:41:02 +0100 Subject: New arrangement for multilanguage and metadata support --- es/04_funciones.md | 654 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 es/04_funciones.md (limited to 'es/04_funciones.md') diff --git a/es/04_funciones.md b/es/04_funciones.md new file mode 100644 index 0000000..618ccd6 --- /dev/null +++ b/es/04_funciones.md @@ -0,0 +1,654 @@ +# Funciones + +El objetivo de este capítulo es que te familiarices con el uso de las +funciones. Parece sencillo pero es una tarea un tanto complicada porque, visto +como nos gusta hacer las cosas, tenemos una gran cantidad de complejidad que +abordar. + +Antes de entrar, vamos a definir una función y a usarla un par de veces: + +``` python +def inc(a): + b = a + 1 + return b +``` + +Si la llamamos: + +``` python +>>> inc(1) +2 +>>> inc(10) +11 +``` + +Cuidado con las declaraciones internas en las funciones. Si preguntamos por +`b`: + +``` python +>>> b +Traceback (most recent call last): + File "", line 1, in +NameError: name 'b' is not defined +``` + +Parece que no conoce el nombre `b`. Esto es un tema relacionado con el *scope*. + +## Scope + +Anteriormente se ha dicho que python es un lenguaje de programación con gestión +automática de la memoria. Esto significa que él mismo es capaz de saber cuando +necesita pedir más memoria al sistema operativo y cuando quiere liberarla. +El *scope* es un resultado este sistema. Para que python pueda liberar +memoria, necesita de un proceso conocido como *garbage collector* (recolector +de basura), que se encarga de buscar cuando las referencias ya no van a poder +usarse más para pedir una liberación de esa memoria. Por tanto, las referencias +tienen un tiempo de vida, desde que se crean hasta que el recolector de basura +las elimina. Ese tiempo de vida se conoce como *scope* y, más que en tiempo, se +trata en términos de espacio en el programa. + +El recolector de basura tiene unas normas muy estrictas y conociéndolas es +fácil saber en qué espacio se puede mover una referencia sin ser disuelta. + +Resumiendo mucho, las referencias que crees se mantienen vivas hasta que la +función termine. Como en el caso de arriba la función en la que se había creado +`b` había terminado, `b` había sido limpiada por el recolector de basura. `b` +era una referencia *local*, asociada a la función `inc`. + +Puede haber referencias declaradas fuera de cualquier función, que se llaman +*globales*. Éstas se mantienen accesibles desde cualquier punto del programa, y +se mantienen vivas hasta que éste se cierre. Considera que el propio programa +es una función gigante que engloba todo. + +Python define que cualquier declaración está disponible +en bloques internos, pero no al revés. El siguiente ejemplo lo muestra: + +``` python +c = 100 +def funcion(): + a = 1 + # Se conoce c aquí dentro +# Aquí fuera no se conoce a +``` + +El *scope* es peculiar en algunos casos que veremos ahora, pero mientras tengas +claro que se extiende hacia dentro y no hacia fuera, todo irá bien. + +## First-class citizens + +Antes de seguir jugando con el *scope*, necesitas saber que las funciones en +python son lo que se conoce como *first-class citizens* (ciudadanos de primera +clase). Esto significa que pueden hacer lo mismo que cualquier otro valor. + +Las funciones son un valor más del sistema, como puede ser un string, y su +nombre no es más que una referencia a ellas. + +Por esto mismo, pueden ser enviadas como argumento de entrada a otras +funciones, devueltas con sentencias `return` o incluso ser declaradas dentro de +otras funciones. + +Por ejemplo: + +``` python +>>> def filtra_lista(list): +... 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] +``` + +En este ejemplo, haciendo uso de la función `filter` (usa la ayuda para ver lo +que hace), filtramos todos los elementos mayores que `4` de la lista. Pero para +ello hemos creado una función que sirve para compararlos y se la hemos +entregado a la función `filter`. + +Este ejemplo no tiene más interés que intentar enseñarte que puedes crear +funciones como cualquier otro valor y asignarles un nombre, para después +pasarlas como argumento de entrada a otra función. + +## Lambdas + +Las funciones *lambda*[^lambda] o funciones anónimas son una forma sencilla de +declarar funciones simples sin tener que escribir tanto. La documentación +oficial de python las define como funciones para vagos. + +La sintaxis de una función lambda te la enseño con un ejemplo: + +``` python +>>> lambda x,y: x + y + at 0x7f035b879950> +>>> (lambda x,y: x + y)(1,2) +3 +``` + +En el ejemplo primero se muestra la declaración de una función y después +colocando los paréntesis de precedencia y después de llamada a función se +construye una función a la izquierda y se ejecuta con los valores `1` y `2`. + +Es fácil de entender la sintaxis de la función lambda, básicamente es una +función reducida de sólo una sentencia con un `return` implícito. + +El ejemplo de la función `filtra_lista` puede reducirse mucho usando una +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] +``` + +No necesitábamos una función con nombre en este caso, porque sólo iba a +utilizarse esta vez, así que resumimos y reducimos tecleos. + +De todos modos, podemos asignarlas a una referencia para poder repetir su uso: + +``` python +>>> f = lambda x: x + 1 +>>> f(1) +2 +>>> f(10) +11 +>>> f + at 0x7f02184febf8> +``` + +Las funciones lambda se usan un montón como *closure*, un concepto donde el +*scope* se trabaja más allá de lo que hemos visto. Sigamos visitando el +*scope*, para entender sus usos más en detalle. + +[^lambda]: Toman su nombre del Lambda + Calculus: + + +## Scope avanzado + +Cada vez que se crea una función, python crea un nuevo contexto para ella. +Puedes entender el concepto de contexto como una tabla donde se van guardando +las referencias que se declaran en la función. Cuando la función termina, su +contexto asociado se elimina, y el recolector de basura se encarga de liberar +la memoria de sus variables, tal y como vimos anteriormente. + +Lo que ocurre es que estos contextos son jerárquicos, por lo que, al crear una +función, el padre del contexto que se crea es el contexto de la función madre. +Python utiliza esto como método para encontrar las referencias. Si una +referencia no se encuentra en el contexto actual, python la buscará en el +contexto padre y así sucesivamente hasta encontrarla o lanzar un error diciendo +que no la conoce. Esto explica por qué las variables declaradas en la función +madre pueden encontrarse y accederse y no al revés. + +Aunque hemos explicado el *scope* como un concepto asociado a las funciones, la +realidad es que hay varias estructuras que crean nuevos contextos en python. El +comportamiento sería el mismo del que se ha hablado anteriormente, las +referencias que se creen en ellos no se verán en el *scope* de nivel superior, +pero sí al revés. Los casos son los siguientes: + +- Los módulos. Ver capítulo correspondiente +- Las clases. Ver capítulo de Programación Orientada a Objetos. +- Las funciones, incluidas las funciones anónimas o lambda. +- Las expresiones generadoras[^generator-expression], que normalmente se + encuentran en las *list-comprehension* que ya se han tratado en el capítulo + previo. + +[^generator-expression]: + + +### Scope léxico, Closures + +Hemos dicho que las funciones pueden declararse dentro de funciones, pero no +hemos hablado de qué ocurre con el *scope* cuando la función declarada se +devuelve y tiene una vida más larga que la función en la que se declaró. El +siguiente ejemplo te pone en contexto: + +``` python +def create_incrementer_function(increment): + def incrementer (val): + # Recuerda que esta función puede ver el valor `increment` por + # por haber nacido en un contexto superior. + return val + increment + return incrementer + +increment10 = create_incrementer_function(10) +increment10(10) # Returns 20 +increment1 = create_incrementer_function(1) +increment1(10) # Returns 11 +``` +En este ejemplo hemos creado una función que construye funciones que sirven +para incrementar valores. + +Las funciones devueltas viven durante más tiempo que la función que las +albergaba por lo que saber qué pasa con la variable `increment` es difícil a +simple vista. + +Python no destruirá ninguna variable que todavía pueda ser accedida, si lo +hiciera, las funciones devueltas no funcionarían porque no podrían incrementar +el valor. Habrían olvidado con qué valor debían incrementarlo. + +Para que esto pueda funcionar, las funciones guardan el contexto del momento de +su creación, así que la función `incrementer` recuerda la primera vez que fue +construida en un contexto en el que `increment` valía `10` y la nueva +`incrementer` creada en la segunda ejecución de `create_incrementer_function` +recuerda que cuando se creó `increment` tomó el valor `1`. Ambas funciones son +independientes, aunque se llamen de la misma forma en su concepción, no se +pisaron la una a la otra, porque pertenecían a contextos distintos ya que la +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*. + +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. + +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. + +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: + +``` 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. + +### `global` y `nonlocal` + +Hemos hablado de qué sentencias crean nuevos contextos, pero no hemos hablado +de qué pasa si esos nuevos contextos crean referencias cuyo nombre es idéntico +al de las referencias que aparecen en contextos superiores. + +Partiendo de lo que se acaba de explicar, y antes de adentrarnos en ejemplos, +si se crea una función (o cualquiera de las otras estructuras) python creará un +contexto para ella. Una vez creado, al crear una variable en este nuevo +contexto, python añadirá una nueva entrada en su tabla hija con el nombre de la +variable. Al intentar consultarla, python encontrará que en su tabla hija +existe la variable y tomará el valor con el que la declaramos. Cuando la +función termine, la tabla de contexto asociada a la función será eliminada. +Esto siempre es así, independientemente del nombre de referencia que hayamos +seleccionado. Por tanto, si el nombre ya existía en alguno de los contextos +padre, lo ocultaremos, haciendo que dentro de esta función se encuentre el +nombre recién declarado y no se llegue a buscar más allá. Cuando la función +termine, como el contexto asociado a ésta no está en la zona de búsqueda de la +función madre, en la función madre el valor seguirá siendo el que era. + +Ilustrándolo en un ejemplo: + +``` python +>>> a = 1 +>>> def f(): +... a = 2 +... print(a) +... +>>> f() +2 +>>> a +1 +``` + +Aunque el nombre de la referencia declarada en el interior sea el mismo que el +de una referencia externa su declaración no afecta, lógicamente, al exterior +ya que ocurre en un contexto independiente. + +Para afectar a la referencia global, python dispone de la sentencia `global`. +La sentencia `global` afecta al bloque de código actual, indicando que los +identificadores listados deben interpretarse como globales. De esta manera, si +se reasigna una referencia dentro de la función, no será el contexto propio el +que se altere, sino el contexto global, el padre de todos los contextos. + +``` python +>>> a = 1 +>>> def f(): +... global a +... a = 2 +... print(a) +... +>>> f() +2 +>>> a +2 +``` + +> NOTA: Te recomiendo, de todas formas, que nunca edites valores globales desde +> el cuerpo de funciones. Es más elegante y comprensible si los efectos de las +> funciones sólo se aprecian en los argumentos de entrada y salida. + +Para más detalles sobre limitaciones y excepciones, puedes buscar en la ayuda +ejecutando `help("global")`. + +El caso de `nonlocal` es similar, sin embargo, está diseñado para trabajar en +contextos anidados. Es decir, en lugar de saltar a acceder a una variable +global, `nonlocal` la busca en cualquier contexto que no sea el actual. +`nonlocal` comienza a buscar las referencias en el contexto padre y va saltando +hacia arriba en la jerarquía en busca de la referencia. Para saber más: +`help("nonlocal")`. + +La diferencia principal entre ambas es que `global` puede crear nuevas +referencias, ya que se sabe a qué contexto debe afectar: al global. Sin +embargo, `nonlocal` necesita que la referencia a la que se pretende acceder +esté creada, ya que no es posible saber a qué contexto se pretende acceder. + +Las sentencias `global` y `nonlocal` son tramposas, ya que son capaces de +alterar el comportamiento del *scope léxico* y convertirlo en *scope dinámico* +en casos extraños. El *scope dinámico* es el caso opuesto al léxico, en el que +las funciones acceden a valores definidos en el contexto donde se ejecutan, no +donde se crean. + +## 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") + +``` + +[^switch-case]: + +### Positional vs Keyword Arguments + +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 modificas, 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 + +Los *decorators* son un concepto que, a pesar de ser bastante concreto, nos +permite descubrir todo el potencial de lo que se acaba de tratar en este +apartado. Sirven para dotar a las funciones de características adicionales. + +Por ejemplo, éste es un decorador que permite crear funciones que se ejecutan +en un *thread* independiente. Tiene sentido para realizar acciones de las que +se quiere que se ejecuten por su cuenta sin ralentizar el hilo principal del +programa, como el envío de un email desde un servidor web. + +``` python +import threading + +def run_in_thread(fn): + def run(*args, **kwargs): + t = threading.Thread(target=fn, args=args, kwargs=kwargs) + t.start() + return t + return run + +@run_in_thread +def send_mail(): + """ + Envía un email a un usuario, sin esperar confirmación. + """ + pass +``` + +Hay muchos detalles que te habrán llamado la atención del ejemplo, el uso de +`@run_in_thread` probablemente sea uno de ellos. Éste es, sin embargo, el +detalle menos importante ya que únicamente se trata de un poco de *syntactic +sugar*. + +> NOTA: el *syntactic sugar* son simplificaciones sintácticas que el lenguaje +> define para acortar expresiones muy utilizadas. El ejemplo clásico de +> *syntactic sugar* es: +> `a += b` +> Que es equivalente a: +> `a = a + b` + +Los *decorators* pueden entenderse como un envoltorio para una función. No son +más que una función que devuelve otra. En el caso del decorador del ejemplo, +el *decorator* `run_in_thread` es función que recibe otra función como +argumento de entrada y devuelve la función `run`. Este decorador, al aplicarlo +a una función con `@run_in_thread` está haciendo lo siguiente: + +``` python +send_mail = run_in_thread(send_mail) +``` + +> NOTA: `@decorator` es *syntactic sugar* de `fn = decorator(fn)`. Simplemente, +> es más corto y más bonito. + +Por lo que la función `send_mail` ya no es lo que creíamos, sino la función +`run`. En el ejemplo, la función `run` llama a la función `fn` de la función +madre (`run` es una *closure*), que resulta ser `send_mail`, a modo de thread +independiente. + +Como puedes apreciar, el hecho de capturar todos los posibles argumentos de +entrada en la función `run` permite a `run_in_thread` decorar cualquier +función, sabiendo que funcionará. + +El principal problema que los decoradores generan es que la función que hemos +decorado ya no es la que parecía ser, así que su *docstring*, sus argumentos de +entrada, etc. ya no pueden comprobarse desde la REPL usando la ayuda, ya que la +ayuda buscaría la ayuda de la función devuelta por el decorador (`run` en el +ejemplo). Usando `@functools.wraps`[^functools] podemos resolver este +problema. + +[^functools]: Puedes leer por qué y cómo en la documentación oficial de + python: + + +La realidad es que los *decorators* son una forma muy elegante de añadir +funcionalidades a las funciones sin complicar demasiado el código. Permiten +añadir capacidad de depuración, *profiling* y todo tipo de funcionalidades que +se te ocurran. + +Este apartado se deja varias cosas en el tintero, como los decoradores con +parámetros de entrada, pero no pretende ser una referencia de cómo se usan, +sino una introducción a un concepto útil que resume perfectamente lo tratado +durante todo el capítulo. + +Te animo, como ejercicio, a que analices el decorador `@lru_cache` del módulo +`functools` y comprendas su interés y su funcionamiento. Para leerlo en la +ayuda debes importar el módulo `functools` primero. Como aún no sabes hacerlo, +aquí tienes la receta: + +``` python +>>> import functools +>>> help(functools.lru_cache) +``` + + +## Lo que has aprendido + +Este capítulo puede que sea el más complejo de todos los que te has encontrado +y te encontrarás. En él has aprendido a declarar y a usar funciones, cosa +sencilla, y todos los conceptos importantes relacionados con ellas. Un +conocimiento que es útil en python, pero que puede ser extendido a casi +cualquier lenguaje. + +Tras un sencillo acercamiento al *scope*, has comprendido que las funciones en +python son sólo un valor más, como puede ser un `int`, y que pueden declararse +en cualquier lugar, lo que te abre la puerta a querer declarar funciones +sencillas sin nombre, que se conocen como funciones *lambda*. + +Una vez has aclarado que las funciones son ciudadanos de primera clase +(*first-class citizens*) ya estabas preparado para afrontar la realidad del +*scope* donde has tratado los contextos y cómo funcionan definiendo el concepto +del *scope léxico* que, colateralmente, te ha enseñado lo que es una *closure*, +un método para implementarlo. Pero también has tenido ocasión de aprender que +en python es posible crear casos de *scope dinámico* mediante las sentencias +`global` y `nonlocal`, que pueden ser útiles, pero es mejor no abusar de ellas. + +Pero no había quedado claro en su momento cómo funcionaban los argumentos de +entrada y las llamadas a las funciones, así que has tenido ocasión de ver por +primera vez lo que es un *callable* en python, aunque se te ha prometido +analizarlo en el futuro. Lo que sí que has tenido ocasión de tratar son los +argumentos *positional* y *keyword*, y cómo se utilizan en todas sus posibles +formas. + +Finalmente, para agrupar todo esto en un único concepto, se te han mostrado los +*decorators*, aunque de forma muy general, con el fin de que vieras que todo lo +que se ha tratado en este capítulo aparece en conceptos avanzados y es +necesario entenderlo si quieren llegar a usarse de forma eficiente. -- cgit v1.2.3