summaryrefslogtreecommitdiff
path: root/es/04_funciones.md
diff options
context:
space:
mode:
Diffstat (limited to 'es/04_funciones.md')
-rw-r--r--es/04_funciones.md654
1 files changed, 654 insertions, 0 deletions
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 "<stdin>", line 1, in <module>
+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
+<function <lambda> 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
+<function <lambda> 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:
+ <https://en.wikipedia.org/wiki/Deductive_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]: <https://www.python.org/dev/peps/pep-0289/>
+
+
+### 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__
+(<cell at 0x7f04b4ebfa68: int object at 0xa68ac0>,)
+```
+
+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 "<stdin>", line 1, in <module>
+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]: <https://en.wikipedia.org/wiki/Switch_statement>
+
+### 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:
+ <https://docs.python.org/3/library/functools.html#functools.wraps>
+
+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.