domingo, 18 de septiembre de 2016

Clausuras y Decoradores en Python


Uno de los conceptos de programación más difíciles de comprender y manejar correctamente para quien los ve por primera vez, o sencillamente para quien lleva mucho tiempo sin tocarlo, es el de las clausuras o "closures" en inglés. De hecho, si uno pretende de verdad dominar un lenguaje como Javascript, tan sumamente extendido y de moda hoy día, especialmente gracias al auge de la nube y los frameworks para crear aplicaciones móviles multiplataforma como PhoneGap, está obligado a saber manejarse mínimamente con las clausuras.

En este artículo, el cual es una traducción que he realizado (junto con algunos toques personales) del original escrito por Simeon Franklin que dejo en las referencias al final de esta página, trataré de explicar con ejemplos y lo más claramente posible no solo las clausuras, sino una de sus aplicaciones más habituales: los decoradores. Todo esto lo realizaré en Python 3, un lenguaje sencillísimo que cualquiera que sepa programar en otros lenguajes no debería tardar mucho en dominar si se lo propone.

A pesar de las concreciones en Python que voy a hacer, todos los conceptos aquí expuestos son exportables a otros lenguajes de programación (en Java los decoradores se pueden implementar como anotaciones), así que no perdáis detalle y vamos allá.

Aunque el primer paso lógico para entender los decoradores y las clausuras sería definirlos, creo que es mejor dejar esto para más adelante, ya que son ideas que se asimilan mejor a través de ejemplos acompañando la definición, y para ver los ejemplos antes tenemos que entender unas cuantas cosas.

Lo cierto es que utilizar decoradores es bastante sencillo. Es en escribirlos donde reside la dificultad, ya que requiere entender varios conceptos de programación funcional, entre los que se encuentran las clausuras que ahora veremos. No obstante, una vez dominados, se convierten en una muy poderosa herramienta para el que sabe utilizarlos correctamente. Y puesto que escribirlos no es sencillo, vamos a dividir los pasos intermedios que componen este gran puzle en trece partes, con el fin de integrar el aprendizaje poco a poco y, de paso, repasar algunos conceptos esenciales de Python para los menos acostumbrados a este lenguaje.

Quiero señalar que el código que voy a mostrar está escrito de manera que parezca una sesión de consola interactiva de Python, de modo que el símbolo >>> indicará declaraciones o ejecuciones de código, mientras que las salidas de dichas ejecuciones se mostrarán en una línea individual sin estar precedidas de dicho símbolo.


1. Funciones


Las funciones en Python se crean utilizando la palabra reservada def y requieren de un nombre y de una lista opcional de parámetros. Pueden devolver valores utilizando la palabra reservada return.

Veamos un ejemplo de definición y posterior llamada de una función muy sencilla:

Como todas las funciones y clases en Python, el indentado es obligatorio para la correcta interpretación de nuestro programa. Para ejecutar funciones, basta con añadir un paréntesis al nombre de la misma y, en su caso, pasarle los argumentos necesarios.


2. Ámbito


En Python las funciones crean un nuevo ámbito o contexto, o lo que es lo mismo, las funciones poseen su propio espacio de nombres (namespace). Esto significa que cuando el intérprete de Python encuentra una función que utiliza variables en su interior, busca primero si esas variables han sido declaradas dentro del cuerpo de la función y, en caso de no hallar resultados, busca en las variables instanciadas en un contexto superior, que normalmente será el ámbito global. Python incluye un par de funciones, locals() y globals(), que nos permiten echar un vistazo a nuestros espacios de nombres.

Veamos un sencillo ejemplo que nos aclare la diferencia entre ámbito local y global:

Ambas funciones devuelven un diccionario conteniendo todos los nombres de variables que Python conoce, ya sea en un ámbito o en otro (para mayor claridad, he omitido en la salida de la ejecución de globals() todas las variables que Python crea automáticamente). Podemos ver cómo la salida de la función foo() imprime las variables del espacio de nombres local de dicha función, que en este caso incluye únicamente la variable l_string.


3. Reglas de resolución de variables


Obviamente, las variables globales, como su propio nombre indica, son accesibles desde el interior de nuestra función:

No obstante no son modificables en principio, sino que si intentamos hacer una asignación a una variable que existe en el ámbito global desde el interior de una función, Python creará una nueva variable local en el interior de la función y ambas variables existirán independientemente la una de la otra:

Como vemos, las variables globales pueden ser accedidas e incluso modificadas si son tipos mutables, pero no reasignadas (por defecto). El método para poder modificar una variable global desde una función consiste en utilizar primero la palabra reservada global para declarar la variable, y posteriormente ya reasignarla, tal y como se muestra en el siguiente ejemplo:

Un detalle importante que puede provocar errores inesperados si se desconoce es que el intérprete de Python primero hace una recolección de todas las variables que se utilizan en una función, antes de pasar a ejecutar la función. Eso explica el error que vemos en el siguiente ejemplo:

El error viene dado porque el intérprete de Python detecta que a_string es una variable local dentro del cuerpo de la función foo() y, en estas condiciones, hemos intentado imprimir la variable por pantalla antes siquiera de asignarle valor alguno, motivo por el cual se ha lanzado este UnboundLocalError.


4. Tiempo de vida de las variables


Por obvio que parezca lo que voy a decir, no está de más señalar que las variables, además de existir en un espacio de nombres concreto, tienen un tiempo de vida. Consideremos lo siguiente:

No son únicamente las reglas del contexto las que impiden que se imprima la variable x (a pesar de que esa es la razón por la que obtenemos un NameError), sino también la manera en la que las llamadas a las funciones están implementadas en Python y en otros muchos lenguajes. No existe forma alguna de obtener el valor de x en la línea donde estamos tratando de imprimirla, ya que, literalmente, la variable no existe. El espacio de nombres creado para la función foo se crea desde cero cada vez que la función es llamada, y se destruye al finalizar la ejecución de ésta.


5. Argumentos y parámetros de una función


Python nos permite pasar argumentos a las funciones. Los nombres de los parámetros se convierten en variables locales en nuestra función.

De hecho, en Python podemos definir los parámetros de las funciones de varias maneras, y pasarles argumentos. Para no perder detalle lo más indicado es consultar la documentación de Python para definir funciones. Un pequeño resumen rápido de lo más relevante sería: los parámetros de las funciones se pueden utilizar bien como parámetros posicionales, bien como parámetros con nombre. Además, se pueden asignar valores por defecto a los parámetros que nos interesen.

Para no crear confusiones con la nomenclatura, voy a llamar parámetros posicionales a aquellos que no tienen un valor por defecto asignado, y llamaré parámetros con nombre a los que sí lo tengan.

Existen algunas reglas fáciles para recordar la prioridad y el comportamiento de cada parámetro al definir la función:
  1. Todos los parámetros se pueden comportar como parámetros posicionales o con nombre, en función de cómo realicemos la llamada a la función cada vez.
  2. Los parámetros con nombre siempre se colocarán los últimos al definir la función. Nunca se puede colocar un parámetro con nombre por delante de un parámetro posicional.
  3. Al realizar la llamada a una función, debe proveerse obligatoriamente un argumento para cada parámetro posicional.
  4. Al realizar la llamada a una función, se pueden proveer todos los parámetros con nombre que queramos, sin necesidad de que sean todos ni de que estén ordenados de manera alguna.
Veamos un ejemplo que aclare todo esto:

En el punto #1 definimos una función resta que tiene un único parámetro posicional x y otro parámetro con nombre y, y que cumple la regla nº2 anterior.

Tal y como vemos en el punto #2 podemos llamar a esta función pasando los argumentos de manera normal (los valores se pasan posicionalmente a pesar de estar definida la función con un parámetro con nombre y uno posicional, cumpliendo la regla nº1).

También podemos llamar a la función sin pasar ningún argumento al parámetro con nombre, como se observa en el punto #3 donde Python utiliza el valor por defecto 0 que declaramos si el parámetro y no recibía ningún valor.

Obviamente, no podemos dejar en blanco el primer parámetro posicional y por tanto obligatorio según la regla nº3. El punto #4 demuestra que hacer esto resulta en una excepción.

El punto #5 nos muestra una mezcla de las reglas 3 y 4. Aquí vemos la llamada a la función con dos argumentos con nombre, a pesar de que estaba definida con un parámetro con nombre y uno posicional. Dado que tenemos nombres para nuestros parámetros, el orden en que los pasamos es irrelevante. Este caso es exactamente el opuesto del caso #2 donde ambos valores los pasábamos posicionalmente.

Aunque he escrito un montón de palabras, en realidad el concepto es bastante simple: los parámetros de las funciones pueden tener nombre o posición. Esto puede significar cosas ligeramente distintas dependiendo de si estamos definiendo la función o llamándola, y podemos utilizar argumentos con nombre para funciones definidas únicamente con parámetros posicionales y viceversa. De nuevo, si esto es demasiado lioso, es recomendable revisar la documentación al respecto.


6. Funciones anidadas


Empezamos a meternos en materia de verdad, y en la clave principal que permite la creación de clausuras y decoradores en Python: la creación de funciones anidadas. Esto significa que podemos declarar funciones dentro de otras funciones, y todas las reglas del ámbito y de tiempo de vida de las variables se aplican de manera normal.

Aunque esto pueda parecer complicado, en realidad se está comportando de una forma muy lógica. Consideremos lo que ocurre en el punto #1: Python busca una variable local llamada x y no la encuentra, así que busca en un contexto superior que la englobe, ¡que resulta ser otra función!

La variable x es una variable local de la función externa pero, al igual que antes, nuestra función interna tiene acceso al contexto que la envuelve (por defecto tiene acceso para leer y, si trabajamos con tipos mutables, para modificar, pero no para reasignar variables, tal y como ocurría antes con las variables globales tratadas en el interior de una función).

En el punto #2 realizamos la llamada a nuestra función interna. Es importante saber que interna no es más que el nombre de una variable que sigue las reglas de Python a la hora de buscar variables: Python cataloga las variables del ámbito de externa primero y allí encuentra una variable local llamada interna (esto sirve de pie al siguiente punto de este artículo).

Para solventar el problema de la reasignación de una variable local de una función dentro de otra función definida en su interior, utilizaremos la palabra reservada nonlocal exactamente del mismo modo en que utilizábamos antes global, tal y como vemos en este ejemplo:



7. Las funciones también son objetos en Python


Esto no es más que la constatación de que, en Python y otros lenguajes como Javascript, las funciones son objetos iguales que todos los demás. Esto implica que pueden ser pasadas como argumentos de otras funciones, asignadas a una variable o devueltas a través de una sentencia return.

Tal vez nunca os hayáis imaginado que las funciones podían tener atributos, pero las funciones en Python se comportan como objetos, al igual que todo lo demás (¡y las clases también!). Veamos un ejemplo de una función tratada como un parámetro y pasada como argumento:

Este ejemplo es bastante sencillo, ya que suma y resta son dos funciones bastante corrientes que reciben dos parámetros y devuelven el valor calculado. En #1 se puede ver que la variable que va a recibir una función es normal y corriente. En #2 se realiza la llamada a la función pasada en el parámetro func (los paréntesis en Python son el operador de llamada y tratan al objeto en cuestión como a una función que puede recibir parámetros). Y en el punto #3 se puede ver que pasar funciones como argumentos no requiere ninguna sintaxis especial para su tratamiento.

Es posible que ya hayáis visto este tipo de comportamiento antes, dado que Python utiliza funciones como argumentos para operaciones bastante frecuentes como personalizar la función de ordenamiento de listas sorted, pasándole el parámetro key que se encarga del criterio que se debe seguir para ordenar los objetos dentro de la lista.

Pero, ¿y qué hay de devolver funciones usando return? Consideremos lo siguiente:

Esto puede parecer algo más extraño. En el punto #1 hemos devuelto la variable interna, la cual parece ser un objeto cualquiera. No hay ninguna sintaxis especial aquí: nuestra función externa está devolviendo la función que hay definida en su interior ya que, en caso contrario, no podría ser llamada más adelante. ¿Recordáis el tiempo de vida de las variables? La función interna se redefine desde cero cada vez que externa es ejecutada; y si interna no fuera devuelta y guardada en la variable foo como hacemos en #2, sencillamente dejaría de existir en cuanto saliese de su ámbito.

Vemos que si evaluamos la variable foo, efectivamente ésta contiene a la función interna, y que podemos llamarla utilizando el operador de llamada (los paréntesis, como hemos dicho). Aunque esto pueda parecer un poco raro, realmente no es difícil de comprender. ¡Así que vamos, ahora sí, a meternos de lleno en las clausuras!


8. Clausuras


Empecemos con un ejemplo en lugar de una definición. Vamos a modificar ligeramente el código anterior para plantear una situación algo controvertida:

En nuestro último ejemplo pudimos ver que interna es una función devuelta por externa, almacenada en una variable llamada foo y que podíamos llamarla utilizando foo(). Pero, ¿funcionará tras la modificación que le hemos hecho? Vamos a considerar primero las reglas de ámbito.

Desde el punto de vista de las reglas de contexto de Python, todo debería funcionar correctamente: x es una variable local en nuestra función externa; y cuando interna imprime x en el punto #1 Python busca una variable local de la función interna con ese nombre y, al no encontrarla, se va al contexto superior, que pertenece a la función externa, encontrándola allí.

¿Pero y si lo vemos desde el punto de vista del tiempo de vida de una variable? Nuestra variable x es local para la función externa, lo que significa que solo existe mientras dicha función existe, mientras se está ejecutando. Por otro lado, no podemos hacer una llamada a interna hasta después de haberla devuelto en una ejecución de externa y haberla asignado a la variable foo. Por tanto, x no debería existir ya para cuando hagamos la llamada a interna en #2 y se debería lanzar alguna excepción del tipo RuntimeError.

Bien, pues resulta que, en contra de lo esperado, todo funciona correctamente. Esto es debido a que Python tiene soporte para una característica llamada clausuras, que implica que las funciones internas definidas en un ámbito no global son capaces de recordar los espacios de nombres que las englobaban en el momento de ser definidas. Esto se puede observar mirando el atributo __closure__ de nuestra función interna, el cual contiene las variables del ámbito que la envolvían (que era el ámbito de externa) y que han sido utilizadas (obviamente, Python hace una gestión adecuada de la memoria y las variables de ámbito que no son utilizadas no se almacenan en la clausura).

Recordad: La función interna se está definiendo de nuevo cada vez que la función externa es ejecutada. Ahora mismo, el valor de x no cambia, por lo que cada función interna que obtenemos hace lo mismo que las anteriores. Pero, ¿y si lo modificamos un poco?

En este ejemplo se puede ver que las clausuras — el hecho de que las funciones recuerden el ámbito que las engloba — pueden ser utilizadas para construir funciones personalizadas que tienen, básicamente, argumentos fijos no modificables (hard coded arguments en inglés). No le estamos pasando los números 1 y 2 a nuestra función interna, sino que estamos construyendo versiones personalizadas de la función interna que "recuerdan" qué numero debería imprimirse por pantalla.

Esto ya de por sí es una técnica poderosa — podríamos incluso pensar en ello como si se tratase de una especie de técnica orientada a objetos: externa es un constructor de interna con x actuando como si fuera una variable privada — y los usos son numerosos. Si estáis familiarizados con el parámetro key de la función sorted de Python que antes mencionamos, probablemente habréis escrito alguna que otra función lambda para ordenar una lista de listas por el segundo elemento de cada lista en lugar del primero. Ahora podréis ser capaces de escribir una función que acepte el índice que indique qué elemento de cada lista deba ser utilizado en la comparación para ordenar, y devuelva una función que pueda ser pasada como argumento para el parámetro key.

¡Pero las clausuras no sirven solamente para algo tan estúpido! Vamos a hacer algo realmente interesante con ellas.


9. Decoradores


Un decorador es simplemente una función que toma otra función como argumento y devuelve una función de reemplazo. Vamos a empezar con algo sencillo para ir abriéndonos camino hacia decoradores que resulten útiles de verdad.

Observad atentamente el decorador del ejemplo.
Hemos definido una función llamada externa que posee un único parámetro: func.
Dentro de externa hemos definido una función anidada llamada interna.
La función interna imprime una cadena y posteriormente hace una llamada a func(), capturando su valor de retorno en el punto #1.
El valor de func puede cambiar cada vez que externa es llamada, pero cualquiera que sea la función func, ésta será invocada.
Finalmente, interna devuelve el valor de retorno de func y le suma 1, y podemos ver que cuando lamamos a la función devuelta almacenada en decorada en el punto #2 obtenemos como resultado la cadena "Antes de func" y el valor 2, en lugar del valor original esperado de retorno de la función foo, que era 1.

Podríamos decir que la variable decorada es una versión decorada de foo (es foo y algo más). De hecho podría darse el escenario en el cual quisiéramos reemplazar la versión de foo que tenemos al principio por la versión decorada de la misma, sin cambiar el nombre de la función.

Para lograr esto basta con reasignar la variable foo (recordad que las funciones son objetos también y pueden tratarse como variables) a su versión decorada:

A partir de ahora, cualquier llamada a foo() no devolverá lo que devolvía la función original, sino su versión decorada. ¿Lo vais pillando? Bien, pues vamos a escribir ahora un decorador que resulte más práctico e interesante.

Imaginemos que tenemos una clase, llamada Coordenadas, que almacena coordenadas en dos dimensiones (básicamente dos números enteros que llamaremos x e y).

Por desgracia, las instancias de estos objetos Coordenadas no pueden operarse matemáticamente entre sí para obtener nuevas coordenadas, y tampoco podemos modificar el código fuente, por lo que no podemos añadir esta característica por nuestra cuenta. Vamos a necesitar hacer muchos cálculos sin embargo, así que queremos crear dos funciones suma y resta que cojan dos objetos Coordenadas y realicen la operación correspondiente entre ellos, devolviéndonos otro objeto Coordenadas.

Veamos cómo sería esta clase Coordenadas y las funciones suma y resta que hemos propuesto:

Pero, ¿qué pasaría si las funciones suma y resta también tuvieran que hacer algún tipo de comprobación respecto a los límites de las coordenadas?  Por ejemplo, pongamos que sólo se pueden sumar o restar coordenadas positivas, y que cualquier resultado debe ser también entregado en coordenadas positivas. Por tanto, con el código actualmente escrito, tendríamos lo siguiente:

Pero ese no es el resultado que queremos. Lo que buscamos es que cuando una coordenada sea negativa, ésta pase a ser 0 automáticamente. De tal forma que la diferencia entre uno y dos sería {x:0, y:0} y la suma de uno y tres sería {x:100, y:200} sin tener que modificar las variables unodos y tres.

Ahora bien, en lugar de añadir chequeos de los argumentos de entrada para cada función que realice operaciones, suma y resta en este caso, que resultaría en una redundancia de código (tendríamos exactamente el mismo código de comprobación en ambas funciones, lo que sería un copiar y pegar en toda regla) y dificultaría la legibilidad de las mismas, vamos a escribir un decorador que realice dichas comprobaciones.

Este decorador funciona exactamente igual que el anterior: devuelve una versión modificada de una función; aunque en este caso sí que realiza una tarea de utilidad chequeando y normalizando los parámetros de entrada y el valor de retorno, sustituyendo cualquier valor negativo de x e y por 0.

Un detalle muy importante a recordar es que la función interna que definamos, checker en este caso, debe tener los mismos parámetros de entrada que las funciones que se pretenden decorar, suma y resta, ya que éstos serán pasados automáticamente cuando apliquemos el decorador.

A partir de aquí, es una decisión personal de cada uno utilizar decoradores para hacer nuestro código más limpio y elegante. Sin duda las posibilidades de una herramienta como ésta son infinitas.


10. El símbolo @ le aplica un decorador a una función


En Python, al igual que en la mayoría de lenguajes, existe lo que comúnmente se conoce como azúcar sintáctico. En Wikipedia podemos ver una definición de esta expresión bastante precisa, la cual indica es que este término se utiliza para referirse a los añadidos a la sintaxis de un lenguaje de programación que no afectan a su funcionalidad, pero que facilitan expresar algunas construcciones de una forma más clara o concisa, o en un estilo alternativo.

En los ejemplos que hemos ido viendo, hemos estado decorando nuestra función y posteriormente reemplazando la original por la decorada haciendo una asignación de la versión decorada a la variable que contenía la función original. De esta forma:

Esto mismo que acabamos de ver puede hacerse utilizando la siguiente sintaxis:

Es importante recalcar que no existe ninguna diferencia entre los dos trozos de código que acabamos de mostrar.

Como dije al principio, ¡utilizar decoradores es muy fácil, lo complicado es escribirlos!


11. *args y **kwargs


Ya hemos escrito con éxito un decorador que funciona y es útil. El único inconveniente que tiene es que está escrito de manera que va a funcionar solo con un tipo muy concreto de función: una que recibe dos argumentos. Anteriormente dije que la función interna que definiéramos debía tener los mismos parámetros de entrada que la función que quisiéramos decorar, ya que éstos se pasaban automáticamente al aplicar el decorador. Pero, ¿y si no sabemos cuántos parámetros de entrada tendrá la función? Para estos casos, Python tiene los muy útiles parámetros genéricos *args y **kwargs.

Supongamos que queremos hacer un decorador que realice una tarea independientemente de la función que vaya a decorar y del número de parámetros que vaya a tener. Por ejemplo, vamos a escribir un decorador que se dedique a incrementar un contador cada vez que una función decorada sea invocada.

Empezando por lo básico, aunque siempre teniendo cerca la documentación oficial, os diré que utilizar el operador * cuando se define una función implica que cualquier argumento posicional extra pasado a la función se añade a una tupla con el nombre de la variable precedida por *.

Un ejemplo sencillo:

La función uno simplemente imprime cualquier argumento posicional que le pasemos. Como se puede ver en #1, nos referimos a la variable args dentro de la función, ya que *args solamente se utiliza en la definición de la función para indicar que los argumentos posicionales deberían ser almacenados en la variable args que, como hemos dicho, es una tupla.

Python también nos permite especificar algunas variables y capturar cualquier posible parámetro adicional en args, como podemos ver en el punto #2.

El operador * también puede ser utilizado al invocar funciones, y aquí significa algo relacionado, pero no idéntico. Una variable precedida por * cuando llamamos a una función significa que los contenidos de la variable deben ser extraídos y utilizados como argumentos posicionales (lo cual implica, evidentemente, que esta variable debe ser algún tipo de iterable).

De nuevo, un ejemplo:

El código en el punto #1 hace exactamente lo mismo que el código en #2, ya que Python está haciendo automáticamente por nosotros lo que podríamos hacer manualmente.

En resumen, *args implica o bien extraer variables posicionales de un iterable si estamos invocando una función, o bien aceptar cualquier tipo de variable posicional extra si estamos definiendo una función.

Podemos darle una pequeña vuelta de tuerca a esto introduciendo el operador ** el cual realiza exactamente la misma función para diccionarios y parámetros con nombre que lo que * hacía para los iterables y parámetros posicionales. Simple, ¿verdad?

Cuando definimos una función podemos utilizar **kwargs para indicar que todos los argumentos con nombre deben ser almacenados en un diccionario llamado kwargs. Quiero señalar que ni args ni kwargs forman parte de la sintaxis de Python, podemos darles el nombre que queramos a estas dos variables, pero por convención se suelen usar estos nombres cuando se declaran funciones.

Al igual que *, podemos usar también ** al invocar una función para que utilice las claves y valores de un diccionario dado como argumentos con nombre:



12. Decoradores más genéricos


Con nuestros nuevos poderes, vamos a escribir un decorador que imprima los argumentos de las funciones que se van invocando.

Observad que nuestra función interna coge un número indeterminado de parámetros en #1 y los pasa como argumentos a la función envuelta en el punto #2. Esto nos permite envolver o decorar cualquier función, sin importar su cabecera o prototipado.

Las invocaciones a nuestras funciones imprimen por pantalla una línea extra conteniendo los argumentos de la invocación, además del resultado esperado de la llamada a cada función.


13. El decorador @wraps


Un último detalle que me gustaría destacar antes de dar esto por concluido es el decorador @wraps. Aunque usar decoradores puede ser realmente útil, también puede ser complicado depurar errores en el código. Concretamente, uno de los inconvenientes a los que nos podemos enfrentar se basa en la naturaleza de los decoradores.

Dado que un decorador es sencillamente una función que envuelve a la original, éste reemplazará el atributo funcion.__name__ de la original por el suyo propio, y esto puede causar confusiones mientras se depura el código. Además, al aplicar un decorador a una función, también perdemos cualquier tipo de documentación que ésta pudiera contener en el atributo funcion.__doc__.

Pero, afortunadamente, Python posee un mecanismo muy sencillo para evitar que toda esta información se pierda, y creo que este ejemplo lo aclarará del todo:


¡Y ya está! Si habéis llegado hasta aquí sin perderos, ¡enhorabuena! Ya controláis los decoradores en Python (y, por consiguiente, las clausuras). Voy a dejar algunos enlaces en las referencias que amplían un poco más los decoradores para los más interesados, como la adición de parámetros a los mismos o su uso con objetos.

Ya sabéis que para cualquier duda, problema o sugerencia, tenéis los comentarios. ¡Nos vemos en el siguiente!


Referencias:
  • Decorators I: Introduction to Python Decorators [Link]
  • Documentación Oficial de Python 3 [Link]
  • Python Decorators II: Decorator Arguments [Link]
  • Python: Why to use @wraps with decorators? [Link]
  • Understanding Python Decorators in 12 Easy Steps! [Link]

1 comentario: