NAMEDTUPLES

 


OBSERVATORIOS DE IZAÑA, EN LAS CAÑADAS DEL TEIDE, CON EL VOLCÁN AL FONDO.





      Las namedtuples, las tuplas (v. TUPLAS) con nombre o tuplas nominadas, no dejan de ser tuplas en sí mismas, básicamente, una secuencia de datos inmutable, como ya sabemos, mondas y lirondas aunque, en su caso, como ya veremos, se acrecienta la legibilidad. Resultan muy prácticas cuando no tenemos necesidad de añadir comportamientos a sus ítems, esto es, a los elementos que contenga nuestra secuencia de datos, a la par que contamos con un conocimiento previo de las propiedades que podamos almacenar. Por ejemplo, las namedtuples nos vendrán bien cuando nuestras tuplas se comporten como un simple almacén de constantes (en tanto que elementos que una vez insertados en una tupla ya no se pueden modificar: no podemos modificar ni su valor ni su apariencia), y quizás no tan bien cuando usemos esas mismas tuplas como almacén de valores para completar registros en una base de datos, y evitar con ello "ataques" externos mediante inyección.

En la POO, las namedtuples se utilizan comúnmente para diseñar o modelizar subclases o clases hijas de la superclase o clase padre tuple, lo que nos faculta para construir nuevas tuplas a la medida de las exigencias o requisitos de los códigos que estuviéramos desarrollando.

La razón de la existencia de las tuplas con nombre lo expresa de manera magistral el Sr. Leodanis Pozo  Ramos, ingeniero industrial y amante de Python, en su artículo dedicado a la namedtuples en el sitio web realpython.com (v. página) y que nos permitimos reproducir fielmente:

"Python namedtuple se creó para mejorar la legibilidad del código al proporcionar una forma de acceder a los valores utilizando nombres de campo descriptivos en lugar de índices enteros, que la mayoría de las veces no proporcionan ningún contexto sobre cuáles son los valores. Esta característica también hace que el código sea más limpio y más fácil de mantener."

Las namedtuples se pueden utilizar como sustitutas de cualquier tupla común y corriente, con la ventaja de que nos ofrecen la posibilidad de acceder a sus elementos, a sus campos o ítems, por su NOMBRE (el que le proporcionemos) en lugar de hacerlo por su ÍNDICE (la posición que ocupa cada elemento/campo/ítem en la secuencia, leyendo siempre de izquierda a derecha y comenzando desde 0 para el primer elemento, el más próximo al paréntesis de apertura).

¿A que esto último nos recuerda un poquito a los diccionarios, verdad, donde llamamos a los valores (values) por sus claves (keys)? 😉


      ESTE PARECIDO QUE PODEMOS ENCONTRAR ENTRE UN DICCIONARIO Y UNA TUPLA CON NOMBRE, OBEDECE A LA FORMA PARTICULAR QUE TIENEN AMBOS TIPOS DE DATOS PARA EXTRAER UN DETERMINADO VALOR, CAMPO, ÍTEM, O COMO QUERAMOS LLAMARLO A CUALQUIER ELEMENTO DE AMBAS SECUENCIAS: EN AMBOS CASOS LO HACEMOS POR UN NOMBRE. PERO LA DIFERENCIA ESTRIBA EN QUE EN LOS DICCIONARIOS, LAS CLAVES (KEYS) DEBEN SER HASH, "VALORES ENTEROS QUE SE USAN PARA COMPARAR ESAS MISMAS CLAVES, Y ES EL MÉTODO INTERNO QUE TIENEN LOS DICCIONARIOS PARA HACER LO QUE SE LES PIDE", MIENTRAS QUE LOS VALORES (VALUES), NO LO SON. DADO QUE LAS namedtuples NO TIENE CLAVES, ÉSTAS NO SON HASH (NO SON HASHABLE) POR LO QUE EL ACCESO A SUS ELEMENTOS ES MÁS SENCILLO. POR CONTRA, EL EQUIVALENTE A LAS CLAVES DE LOS DICCIONARIOS EN LAS namedtuples QUE SON LOS field_names, COMO VEREMOS A CONTINUACIÓN, TIENEN QUE SER CADENAS, strings. ESPERO QUE MIS LADRIDOS SE HAYAN ENTENDIDO UN POQUITO...
 





INVOCACIÓN





      Las namedtuples es una función o método que debemos importar desde el módulo collections de la librería estándar de Python, la stdlib, que tenemos a nuestra entera disposición con la mera descarga del programa. En segundo lugar debemos proporcionarle un nombre, esto es, el named- de la tupla, y que deberá pasarse como primer argumento obligatorio en la zona de parámetros de la función: es el typename.

Finalmente, debemos señalar sus propiedades/atributos en el argumento, obligatorio también como el anterior y a continuación de éste, field_names. Veámoslo:



Como vemos, el método namedtuple de collections nos devuelve  una subclase (subclass, por tanto una clase hija) de la clase tuple cuyo nombre será el que hayamos proporcionado, como hemos dicho más arriba, en el parámetro typename.

El argumento opcional rename, por su parte, viene predefinido como False. Se comporta como una estructura de control de tal modo que, si pasamos algún "campo de nombres", field_names, que no fuera válido (los "campo de nombres" están sujetos a las mismas restricciones que los nombres de las variables: pueden contener caracteres alfabéticos, numéricos, algún que otro signo ortográfico que no conlleve un comportamiento de acuerdo a la sintaxis del lenguaje, aspecto o cualidad, como ª o ·, o guiones bajos; pero no pueden ser palabras reservadas (keywords) ni empezar con guiones bajos o números), si cambiamos su asignación inicial a True, lo sustituirá de manera automática por valores posicionales pasados como cadenas o strings: "_1", "_2", "_3", etc. Vamos a verlo:



En 1. declaramos una lista, el "campo de nombres", el field_names, con cinco elementos, campos, valores o ítems: "name1", "pass", "colorname", "_str" y "position". Como podemos ver, los elementos "pass" y "_str" no son elementos válidos. En 2. pasamos a True el valor original del parámetro rename. En 3. comprobamos que, en efecto, nt es ya una subclase de la clase tuple. Y en 4. ¡tacháááán!... podemos ver cómo los valores equivocados se han sustituido por sus posiciones (índices) en la secuencia pasados como cadenas o strings.






      POR LO GENERAL, LAS namedtuples A PESAR DE LO QUE  PUEDA PARECER, CONSUMEN UNA CANTIDAD DE MEMORIA SIMILAR A LA DE UNA tupla CUALQUIERA, CONSERVANDO SUS PROPIAS CUALIDADES, COMO LA INMUTABILIDAD Y EL INDEXADO, PERO QUE A SU VEZ ADOPTA CARACTERÍSTICAS PROPIAS DE LOS DICCIONARIOS, COMO EL ESQUEMA TIPO CLAVE/VALOR (KEY/VALUE) Y AÑADE VARIAS NUEVAS FUNCIONES, QUE VEREMOS MÁS ADELANTE:
                    
                    * _make(iterable)
                    * _asdict()
                    * _fields()
                    * _field_default()
                    * _replace(**kwargs)

ADEMÁS, SON PERFECTAMENTE INTERCAMBIABLES: PODEMOS TOMAR UNA tupla CUALQUIERA DE NUESTRO CÓDIGO Y TRANSFORMARLA EN UNA TUPLA CON NOMBRE, EN UNA namedtuple, SI LO CONSIDERAMOS CONVENIENTE, ESO SÍ, TENIENDO EN CUENTA LAS ESPECIFICACIONES Y SINTAXIS PARTICULAR DE LAS namedtuples.

Como ya hemos dicho, namedtuple nos devuelve una subclase o clase hija (lo vimos en el punto 3. de la captura anterior) de la superclase o clase padre tuple, con lo cual obtendremos un tipo de dato class, a partir del cual podremos instanciar objetos que podremos invocar cuantas veces queramos.

Vamos a verlo:


En 1. tenemos las "claves" del iterable que pasamos como argumento del parámetro field_names, y en 2., mire usted por dónde,... ¡Ah! las funciones propias de las tuplas. Insistimos: una namedtuple es como una tupla normal y corriente disfrazada de diccionario.



El subrayado ya nos indica que entre los requisitos que requiere la función integrada isinstance() (v. LA FUNCION ISINSTANCE()), el segundo parámetro obligatorio puede ser una clase o... una tupla.



Aquí lo vemos: efectivamente, Colores es una subclase o clase hija de la superclase o clase padre tuple y c, un objeto/instancia de la clase Colores.

PAISAJE LUNAR, PRODUCTO DE UNA FORMA PECULIAR DE SOLIDIFICACIÓN Y EROSIÓN DE LA LAVA VOLCÁNICA, MUNICIPIO DE VILAFLOR, CENTRO DE TENERIFE.

                    
Vamos a ver ahora un ejemplo básico de uso de las namedtuples:


En 1. efectuamos la llamada al módulo. Aquí hemos optado por hacer una importación directa o absoluta y recurrir a un alias para llamarlo con más facilidad. Pero igualmente podemos hacer una importación indirecta o relativa centrándonos en la clase namedtuple mediante la sintaxis: from collections import namedtuple.

En 2. modelizamos la subclase namedtuple Colores integrando en su zona de parámetros los dos argumentos obligatorios necesarios: el typename y el iterable que se corresponde con el campo de nombres, el field_names.

En 3. instanciamos un objeto o instancia de la subclase al que llamamos c1 y donde proporcionamos los valores (values) que queremos proporcionarle a cada unos de los elementos del iterable (que funcionan, recordémoslo, como auténticas claves (keys) de un diccionario): en este caso, simples nombres de colores.

En 4. creamos una estructura de tipo cadena o string llamando a los valores por los índices de sus claves correspondientes.

En 5. y 6. repetimos la misma operación instanciando un objeto o instancia nuevo, c2, a la que proporcionamos igualmente valores nuevos.

En 7. probamos a llamar sobre el objeto o instancia c1 uno de los dos métodos exclusivos de las tuplas: el método count(), que cuenta las veces que se repite un mismo elemento dentro de la misma. Funciona correctamente y obviamente, nos devuelve 1.

Finalmente, en 8. llamamos a los valores directamente por el nombre de sus claves correspondientes. En este caso, muy lindamente, hemos mezclado llamadas a los valores que almacenan claves pertenecientes a los objetos/instancias c1 y c2.

Vamos con otro ejemplo más:


En esta ocasión, hemos optado por una importación relativa combinada con un alias. Claro, con esos nombres tan largos... Todo lo demás es más de lo mismo: llamadas a los valores que pasamos como argumentos en el objeto/instancia elem por el nombre de su clave tal cual las inscribimos en los campos de nombres, field_names, y debajo, lo mismo pero por su índice.

Por esta cualidad de las namedtuples éstas resultan muy útiles, por ejemplo, en desarrollos matemáticos (de hecho, la mayoría de los ejemplos que se muestran en la red para ilustrar cómo funcionan recurren a establecer coordenadas para puntos x e y) donde podemos pasar valores distintos cada vez a partir de cada objeto/instancia que instanciemos a partir de la namedtuple de turno para obtener distintos resultados.

Ya que estamos, lo vemos:








      
      HEMOS LADRADO HASTA LA SACIEDAD QUE TANTO LAS TUPLAS COMO LAS TUPLAS CON NOMBRE, namedtuple, QUE ESTUDIAMOS EN ESTE NUEVO ARTÍCULO, CONSTITUYEN SECUENCIAS DE DATOS INMUTABLES, QUE NO SE PUEDEN MODIFICAR, VAMOS. PERO, OJO, EL ITERABLE (LISTA) QUE PASAMOS COMO ARGUMENTO DEL PARÁMETRO field-names QUE LO ES POR LO QUE, EN REALIDAD, SI "JUGAMOS BIEN NUESTRAS CARTAS", PODEMOS MODIFICAR LA DATA ORIGINAL ABAJO OS MUESTRO CÓMO HACERLO. ¡GUAUU!


¿Qué hemos hecho aquí? Veamos.

Tras la importación necesaria, la clave del asunto está en 1.. Fijémonos que cuando creamos nuestra namedtuple Alumno, el segundo parámetro obligatorio, field_names, nuestro campo de nombres,  ya no es una lista. En su lugar pasamos, sí, un iterable, una secuencia de datos modificable, pero en este caso, una cadena o string. Esta cadena o string lleva dos grupos de caracteres separados por un carácter tipo whitespace, espacio en blanco, para separarlos: "Nombre" y "Matemáticas_Lengua_Sociales", en este último caso, usando el guion bajo como carácter de unión (recordemos que no se aceptan caracteres extraños que no sean válidos para declarar una variable: por ejemplo, si en lugar de guion bajo hubiéramos utilizado el carácter "/" Python nos habría lanzado una excepción como un cañonazo). Esto es importante tenerlo en cuenta porque la namedtuple va a tomar ambos elementos, "Nombre" y "Matemáticas_Lengua_Sociales" como variables

En 2. y en 3., instanciamos sendos objetos/instancias, Almn1Almn2, cada uno de los cuales va a contener dos argumentos: una cadena o string, que muestran un nombre propio; y una lista con notas o calificaciones como ítems o elementos.

En 4. y 5., incluimos sendas impresiones en nuestro código que demuestra la viabilidad de nuestra codificación. Incluso, en 5., diferenciamos cada nota por su índice.

En 6. ya establecemos alguna modificación con la que queremos demostrar que es posible cambiar o alterar la data original. Aprovechando que "Matemáticas_Lengua_Sociales", en realidad, es una variableMatemáticas_Lengua_Sociales, lo que hacemos es llamar a este iterable del objeto/instancia Almn2 y, aplicando la función remove() de las listas, eliminamos el elemento "pendiente" (se ve que el chaval se presentó por fin al examen de Lengua. Ojalá tenga suerte).

En 7. aprovechamos la función insert() de las listas para insertar, como su nombre sugiere, un dato (segundo argumento, "8,5") en una posición específica de la lista por medio de su índice (primer argumento, 1).

En 8., finalmente, comprobamos que la modificación ocurre perfectamente (se ve que el chaval le ha echado codos a la asignatura: ¡Un 8.5!).

FRONDA Y LAURISILVA. BOSQUES HÚMEDOS ENDÉMICOS DE CANARIAS. TENERIFE.


MÉTODOS PROPIOS DE LAS NAMEDTUPLES



      1) _make(iterable):


      Este primer método de las namedtuples simplemente instancia un nuevo objeto/instancia de la subclase (nuestra esplendorosa namedtuple) a partir de los elementos en un iterable, una lista, por ejemplo, que le pasamos como argumento del parámetro iterable del método. 

Sencillamente, crea una instancia nueva a partir de los datos que le pasemos.



Como muestra la documentación oficial de Python (ojo, para la versión 3.8.0 del programa, esto hay que tenerlo siempre en cuenta: nuestra versión de Python determina qué métodos existen, se modifican o se eliminan de las distintas clases y subclases de su biblioteca estándar), el método _make(iterable) crea un nuevo objeto/instancia a partir de Elemento (sí, Python reconoce nuestro espacio de nombres, namespaces, y nos muestra el nombre que le proporcionamos a nuestra tupla con "nombre") desde una secuencia o iterable.



A continuación creamos un iterable, newelem, donde incluimos los tres elementos que se corresponden con los tres elementos en field_names. Justo debajo llamamos a nuestra subclase, nuestra namedtuple, elemento_quimico, y sobre ella llamamos al método _make(iterable) pasando el parámetro iterable por el argumento newelem, nuestro flamante iterable.

Como se aprecia, de manera automática, Python nos imprime un resultado donde nuestra tupla con el nombre Elemento tiene asignado a cada uno de los campos del field_names un valor específico establecido por el iterable que pasamos como argumento del métodonewelem



En 1. comprobamos que efectivamente nuestros datos se han guardado en el espacio de memoria que Python asigna al módulo collections. Obviamente, para poder manejar correctamente los datos debemos instanciar el objeto/instancia que asuma el resultado del proceso. Así pues, en 2. instanciamos al objeto n, y en la impresión llamamos sobre él las propiedades/atributos nombre, símbolo y numatomico mediante la sintaxis del punto, el dott method, y... ¡voilà!

      2) _asdict():


      Hemos hecho referencia en varias ocasiones en este capítulo a la similitud que presentan las namedtuples con los diccionarios. ¡Si es que son casi hermanas! Hombre, pues con semejante "afinidad familiar", ya puestos,... ¿Por qué no construir un método de la subclase namedtuple que transforme una tupla con nombre en un diccionario de los de toda la vida? Pues a semejante conclusión debió llegar mr. Python para traernos este método sin argumentos _asdict().

Lo que hace este método es generar un tipo de dato diccionario a partir de un objeto/instancia de una namedtuple donde los nombres de los campos que integran el iterable field_names se convierten en claves/keys y sus datos asignados en valores/values.



La documentación es tan sucinta como clara al respecto: devuelve un diccionario nuevo mapeando los nombres de los campos en el field_names con sus valores asignados.

Vamos a verlo:


En 1. declaramos una variable a la que llamamos d y que será la encargada de almacenar el diccionario resultante de la ejecución del método _asdict(). Llamamos posteriormente a d y, efectivamente, ahí tenemos nuestro flamante diccionario.



      3) _replace(**kwargs):



      Su nombre lo dice todo: reemplaza algo que ya existe por algo nuevo. ¿Y qué reemplaza? Un dato o valor asignado a uno o más campos en el field_names. Y como esto se hace en pares claves/valor, el parámetro del método _replace() es **kwargs.

Este método viene genial cuando cometemos un yerro con los valores, cuando debemos proceder a re-asignaciones de valores nuevos en tiempo de ejecución (runtime); o cuando, por ejemplo, en cálculos matemáticos, debemos ir modificando valores con cada llamada a la instancia para obtener una data amplia para, por ejemplo, efectuar cálculos estadísticos o afinar un determinado valor que queremos que sea muy preciso, entre otras posibilidades.


Lo dice clarito: devuelve una instancia/objeto nuevo de nuestra namedtuple Elemento reemplazando el valor asignado a un campo por otro nuevo, debiendo especificar el campo, claro está, para que Python sepa de qué campo se trata, y su nuevo valor, utilizando para ello el operador = de asignación.


      EN EL CUADRO DE DIÁLOGO DE ARRIBA SE SUSTITUYE **kwargs POR **kwds. ESTO NO TIENE IMPORTANCIA ALGUNA. LA NOMENCLATURA POR CONVENCIÓN EN EL PEP8 ES **kwargs. 




Arriba mostramos un resultado de la ejecución del método _replace(**kwargs). En los iterables e1 y e2 cometemos errores por aquello de escribir deprisa: nos equivocamos en el número atómico y en el símbolo respectivamente. ¡Ay, Dios! ¿Qué hacemos ahora? 🙀 Pues, muy fácil. Recurrimos al método _replace(**kwargs) que aplicamos, como sabemos, sobre el objeto/instancia correspondiente, pasando como argumento el nombre del campo donde se desea efectuar el cambio, el operador = de asignación y el nuevo valor que sustituirá al original.


      

      4) _fields:



      Como vemos, _fields no es un método sino una propiedad ya que, como la propiedad que sigue a continuación de ésta, _fields_defaults, no lleva zona de paréntesis por lo que no podemos pasarle argumento alguno, ni tan siquiera, un argumento vacío. Su función es tan simple como el mecanismo de un chupete: aplicada sobre cualquier instancia de la subclase, en nuestro ejemplo, elemento_quimico, nos devuelve una tupla con los nombres de los campos, el field_names, vamos. Esto nos permitirá construir tuplas con nombre nuevas, donde añadir campos nuevos o restarlos (para eso debemos convertir primero a la tupla, inmutable, en lista, mutable) a partir de una tupla con nombre dada, como la nuestra, elemento_quimico



Vamos a crear un namedtuple nueva aprovechando la propiedad _fields de las tuplas con nombre:



En 1. declaramos una variable, campos, que almacenará los nombres de los campos de la namedtuple original, esto es, de elemento_quimico, y a continuación, en 2. aplicamos la función conversora list() para transformar nuestra tupla campos, inmutable, en una lista, listacampos, mutable, que podemos modificar.

Esto lo hacemos en 3. mediante el método append(elem) de las listas que nos permite añadir elementos, ítems, a una lista. Finalmente, en 4. modelizamos una nueva namedtuple, elemento_quimico_plus, en la que pasamos como field_names. el nombre de nuestro nuevo y flamante iterable: listacampos. Luego instanciamos un objeto/instancia de elemento_quimico_plus, que llamamos e2, y a partir de aquí, pues tal cual hemos aprendido.


      4) _field_deafults:



      Esta propiedad de las namedtuples demanda la introducción del parámetro defaults en la zona de parámetros de la namedtuple. Este parámetro apunta a valores que podemos proporcionar por defecto a uno o más campos del field_names, y esto lo hará por orden: el primer valor que pasemos por defecto se corresponderá con el primer campo del field_names; el segundo valor que pasemos por defecto se corresponderá con el segundo campo del field_names; el tercero con el tercero, y así sucesivamente. todo ellos pasados dentro de una lista.

Esta propiedad nos devuelve un diccionario con el nombre del campo como clave (key) y su valor por defecto como valor (value). Nos puede venir bien para casos en los que vamos a tener muchas instancias que van a tener una resultado similar. En el ejemplo que viene a continuación, proponemos un balance económico para una microempresa en la que podríamos agrupar en varios modelos de resultados, siendo uno de ellos un balance donde los activos estén por encima de la franja de los 1000 euros  mientras que sus pasivos sean de 0 euros:


Como vemos, en 1. pasamos un argumento en modo lista con dos elementos como valor del parámetro defaults

Es importante recalcar que debemos pasar tantos valores por defecto en la lista defaults como campos tengamos en field_names, ya que de lo contrario, Python los irá asignado desde el último campo de la lista hacia atrás, leyendo de derecha a izquierda:


A partir de un diccionario también podemos crear una tupla con nombre. Por ejemplo, podemos hacer una reversión del proceso de la propiedad _field_defaults y, a partir del diccionario que nos devuelve, obtener la namedtuple original. Para ello usamos el nombre de la variable donde almacenamos el diccionario (diccionario, en nuestro ejemplo, para no calentarnos mucho la cabeza) antecedido por un doble asterisco:


INVIERNO EN LAS CAÑADAS DEL TEIDE, CENTRO DE TENERIFE.






No hay comentarios:

Publicar un comentario