FUNCIONES exec(), hash() Y zip().

CABECERA DEL BARRANCO DE EL RÍO, COMARCA DE ARICO, CENTRO SUR DE TENERIFE.

      1) exec():

      Es la abreviatura de execute, en inglés, esto es, "ejecutar". Y eso es precisamente lo que hace exec(): ejecutará el contenido de la función que le pasemos com argumento en formato cadena. Sin más.



      2) hash():

      Su traducción del inglés viene a ser algo así como "picar y trocear", lo que no da muchas pistas de lo que realmente hace salvo que parece que convierte algo, lo que hace, en picadillo. Y, bueno, hasta cierto punto, lo que hace es algo parecido: desmenuza lo que le pasemos como argumento.
Dicho esto, vayamos por partes. 
Primero vamos a hacer una introducción genérica (y algo de repaso) sobre el concepto hash desde el punto de vista de la programación en general, y de la programación en Python en particular.
A partir de una lista dada, como ya sabemos, podríamos llamar a sus componentes por medio del índice, en referencia al puesto que ocupa de izquierda a derecha en la secuencia, para lo que debemos pasar un valor entero entre corchetes justo a continuación del nombre de la referencia a la variable que almacena al objeto lista. Pero también podemos almacenar esos mismos elementos de la lista asignándoles una clave específica, decidida, escogida,  por nosotros mismos, con la que podemos igualmente llamarlos. De este modo construiríamos un diccionario, un hash, y obtener los elementos que queramos a través de sus claves respectivas y no por su índice. Es como ponerle un nombre a cada elemento y, en lugar de llamarlo por un número (índice) lo hacemos por un nombre (par key(clave)/value(valor) ¿recordamos ahora que cuando estudiamos el tema de los diccionarios sus elementos pares key(clave)/value(valor) podían estar desordenados a como lo escribimos o esperamos obtenerlo de una función, justo lo opuesto a las listas, por ejemplo, donde una vez construidas, a menos que modifiquemos activamente la posición de sus elementos, un millón de veces que pidiéramos repetirla, un millón de veces que Python nos la devuelve exactamente con el mismo orden y, por eso, podíamos llamar a sus componentes por sus índices dado que siempre serían los mismos?
Fijémonos en el siguiente esquema:

En cierto modo, podemos decir que un mapa, un diccionario y un objeto hash (hasta cierto punto) vienen a ser lo mismo.
Tengamos presente que llamar a un elemento como "naranja" de nuestra lista nos obliga a recurrir a solo dos posibilidades, ambas obligatorias, según la codificación de Python: o como a[0] o como a[-3]. Con los diccionarios, sin embargo, podemos construir un par key(clave)/value(valor) y proporcionarle así un nombre (la clave) que queramos: "F1", "Fruto_del_naranjo", "Cítrico_1", etc. Cualquier nombre que se nos ocurra y extraer el valor "naranja" por ese nombre. Así pues, si se desea tener un mayor y mejor control sobre las llamadas a los elementos de una secuencia, la mejor idea es optar por construir un diccionario.
Y en esta idea de control y manejo es donde subyace la intencionalidad de hash.
La función hash() toma un tipo de dato limitado como argumento y devuelve una secuencia numérica de 19 dígitos que constituye un número entero pudiendo ser éste positivo o negativo. ¿Por qué un tipo de dato limitado? Porque, fijémonos bien, teniendo en cuenta su vinculación directa con los diccionarios, sólo podemos pasar como argumento y, en consecuencia, obtener el mencionado número (lo que se denomina un hashcode)  aquéllos tipos de datos que puedan ser claves (keys) en un diccionario de Python.
Vemos, primero, un esquema de su funcionamiento:



Y ahora, en sendas capturas, veremos cuáles son los tipos de datos que podemos pasar como argumentos de hash(), aquéllos tipos que pueden ser claves (keys) en un tipo de dato diccionario de Python:





Notemos que con un número entero devuelve el mismo entero y que con una función, simplemente, la ejecuta.
Visto lo anterior...¿qué es un objeto hash?
Pues de manera sucinta, un objeto hash es el resultado de la conversión de un tipo de dato asumible como clave en un diccionario Python en un número entero de longitud 19 salvo que éste sea un entero donde devolverá el mismo entero, positivo o negativo. Por eso un objeto hash se conoce también como un hashcode ("código hash", en español).
Ya está.
Vale...¿y qué características tiene?
1.- Para empezar, que un mismo dato "hasheado", pasado como argumento de la función hash(), vamos, devolverá siempre el mismo objeto hash/hashcode.



Podemos re-hashear si queremos el objeto hash devuelto por la función, pero el objeto hash devuelto originalmente continúa siendo el mismo:


2.-  Es único. Relacionado con lo anterior, en el sentido de que cualquier modificación que se efectúe sobre el tipo de dato original generará otro objeto hash diferente.


3.-  Como la cantidad de objetos que podemos pasar como argumento de hash() para obtener nuestro apreciado hashcode es casi infinita, es posible que se produzca una paridad en el sentido de que dos objetos distintos, por ejemplo, una string como "mar" y una tupla como (12, 24, 36) tengan un mismo hashcode. Así, el objeto hash no apunta a un único elemento en exclusiva, como debería ser, sino a dos completamente distintos. Esto es lo que se denomina colisión.
Podemos decir, pues, que una buena aplicación de la función hash() es aquélla en la que tengamos la mayor certeza posible de que no se van a producir colisiones, básicamente, porque no vamos a pasar un gran número de datos y disminuir así las posibilidades de que esto ocurra, bien llamemos a la función en un punto concreto de nuestro código, o bien la llamemos en varios repartidos por nuestro programa.
Por cierto, las colisiones entre objetos hash/hashcode se resuelven con el método especial __eq__() pero esto no lo vamos a ver en este capítulo, ya que los métodos especiales corresponden a la Programación Orientada a Objetos que estudiaremos más adelante.

VIEJOS CASERÍOS RURALES ABANDONADOS EN LAS CAMPAS BAJO EL MONTEVERDE RALO DE LA ESCALONA, EN LA VECINDAD DE VILAFLOR, SUR DE TENERIFE. 

Bueno... ¿y para qué sirve? Fundamentalmente, para tres cosas:
  • ESTRUCTURAR DATOS: Existen fórmulas que utilizan funciones hash en distintos lenguajes de programación, como Python, para almacenar de manera segura sus datos.
  • SEGURIDAD: Como recurso criptográfico. Habida cuenta de que a un tipo de dato dado le asigna un hashcode (démonos cuenta de que se trata de una estructura similar al clásico par clave/valor (key/value) de los diccionarios: "Andrés": -5613983898105625992), ¿por qué no usarlo como un par clave/valor donde el hashcode sea la propia contraseña?
  • DETERMINAR EQUIVALENCIAS: Como un mismo y único elemento genera un mismo y único objeto hash/hashcode, podemos emplear éste para comprobar si un elemento cualquiera (por ejemplo, dos archivos con distinto nombre o dos variables en nuestro código) son exactamente iguales o almacenan la misma información. De hecho, Python los emplea a nivel interno para comparar claves (keys) en los diccionarios (¡Ah!, ¿Nos suena esto?) cuando busca una específica: siempre es más fácil y rápido comparar una secuencia de 19 dígitos (para el código máquina, claro, no para nosotros, pobres y limitados humanos) que comparar los valores (values) asociados. ¡Imaginémonos que un par de claves (keys) en un diccionario almacene como sendos string todo el listado telefónico de una localidad cualquiera de dos mil habitantes! Para determinar que son la misma clave y que por tanto debe eliminar una de ellas de un diccionario dado que, como ya sabemos, no pueden contener claves que almacenen un mismo valor, esto es, pares clave/valor repetidos, igual que sucede con los conjuntos, set, o que una misma clave (key) almacene dos valores (values) distintos, es mucho mejor comparar sus hashcodes respectivos que sus valores (values).
  • TRANSPORTAR INFORMACIÓN: En relación con lo que acabamos de decir a veces, los sistemas operativos de nuestros pcs, los navegadores, algunos programas, etc. generan un objeto hash/hashcode sobre un elemento, un archivo de texto, por ejemplo. Si decidimos descargarnos ese archivo de texto, un paso previo podría ser generar en el momento un nuevo objeto hash/hashcode de ese mismo archivo y compararlo con el que ya tenía almacenado: si coincide exactamente es porque es el mismo archivo de texto exactamente; si no coincide, es porque dicho archivo, o se ha modificado del original o se ha corrompido miserablemente, lanzándonos una advertencia en dicho caso.

Veamos a continuación un ejemplo de cómo apoyarnos en un hashcode para confirmar la identidad de un usuario de login y recuperar su contraseña:






      TAMBIÉN NOSOTROS PODEMOS GENERAR UN CÓDIGO NUMÉRICO, UN PSEUDO-HASHCODE, VAMOS, DE LA MANERA SIGUIENTE:






      YA QUE ESTAMOS, PODEMOS DECIR QUE LA FUNCIÓN INTEGRADA ord() RECIBE COMO ÚNICO ARGUMENTO UN CARÁCTER LITERAL UNICODE, ES DECIR, UNA STRING DE LONGITUD 1, Y NOS DEVUELVE EL NÚMERO ENTERO QUE LE CORRESPONDE EN EL CHARSET UNICODE. COMO SUELE OCURRIR, ESTE TIPO DE FUNCIONES SUELEN TENER SU INVERSA, EN ESTE CASO, chr(), QUE RECIBE UN NÚMERO ENTERO Y DEVUELVE EL CARÁCTER UNICODE CORRESPONDIENTE. VEMOS UN EJEMPLO:




CASTILLETE FORMADO POR EXTRUSIONES LÁVICAS DENSAS ACUMULADAS CAPA POR CAPA, CABECERA DEL BARRANCO DE ICOR, SUR DE TENERIFE.


      3) zip():

      La traducción de 'zip' del inglés es "cremallera". Y esto ya nos debe dar una idea más o menos aproximada a lo que hace esta función. Básicamente, esta función trabaja con iterables asociando sus ítems respectivos de uno en uno y de izquierda a derecha, como se encuentran frente a sí los dientes de una cremallera, que sólo encaja o desencaja con el que tiene en frente. Así, el primer ítem de un iterable 'a' se asociará con el primer ítem de un iterable 'b', y a su vez, si lo hay, con el primer ítem de un iterable 'c', y así sucesivamente con cuantos iterables queramos pasar como argumentos.
Estos iterables pueden ser del mismo tipo, todos listas, por ejemplo, o de tipos distintos, combinando listas, tuplas, conjuntos, cadenas y/o diccionarios. Pero eso sí, del mismo modo que una cremallera sólo cierra mientras cada diente encuentre su par y se detiene cuando no es así, la función zip() sólo devolverá aquéllos ítems amorosamente emparejados mientras las longitudes de los iterables que pasemos como argumentos coincidan.
¿Y cómo devuelve la función sus resultados? Pues primero debemos envolverla (convertirla) a su vez en un iterable para que su resultado sea visible. Y lo que nos devuelve, como ítems de ese mismo iterable en que hemos convertido la función zip(), son tantas n-tuplas como la longitud de cualquiera del menor de los iterables que le hayamos pasado como argumento: en concordancia con lo antedicho, si pasamos tres iterables, 'a', 'b' y 'c' donde len(a) = 5, len(b) = 5 y len(c) = 4, nos devolverá 4 3-tuplas (3 porque se corresponde al número de ítems que toma de cada iterable), ya que los dos últimos elementos de 'a' y 'b', es decir a[4] y b[4] (recordemos que los índices empiezan a contar de izquierda a derecha a partir de 0) no encuentran "pareja" en 'c', ya que el último elemento de 'c' es c[3], que ya se "emparejó" con los correspondientes elementos a[3] y b[3].
Por otra parte, si no se pasan iterables a la función ésta nos devolverá un iterador vacío. Y si se nos ocurre pasarle como argumento un único iterable, nos devolverá un iterable de 1-tupla, con tantas tuplas como ítems contenga el iterable.
Veamos un ejemplo para aclararnos con todo esto:




      NOTEMOS QUE CUANDO CONVERTIMOS LA FUNCIÓN zip() EN UN DICCIONARIO, NO SE NOS DEVUELVEN n-tuplas COMO ÍTEMS DE NUESTRO DICCIONARIO, SINO UN OBJETO DICT DE PYTHON CON TODAS LAS DE LA LEY, PERFECTAMENTE FUNCIONAL, TAL Y COMO PODEMOS COMPROBAR AL CABO DEL EJEMPLO ANTERIOR. 
AQUÍ PODEMOS ESTABLECER CONCOMITANCIAS CON EL MÉTODO dict.setdefault(a,b) DE LOS DICCIONARIOS.
¿ALGUIEN ME RASCA LA OREJA?




Por otra parte, con la adición del operador * antes del nombre del iterable, lista a ser posible, en que hayamos convertido la función, nos proporcionará un desempaquetado como la copa de un pino:


Para concluir con la función zip(), vamos a ver una comparativa entre esta misma función de "cremallera" (zip, en inglés) y el concepto de combinatoria. En el caso de la función zip(), sólo se combinan los índices coincidentes de cada iterable, como ya sabemos, mientras que en el segundo caso, donde recurriremos a un bucle for/in en línea (v. http://conocepython.blogspot.com/p/bucle-forin.html)  que nos devuelve un  generator (v. http://conocepython.blogspot.com/p/generators.html), que a su vez, nos permitirá la combinación de cada elemento de un iterable con TODOS los demás elementos de un segundo iterable (podemos añadir tantos iterables como queramos). En el ejemplo se subrayan en naranja los pares coincidentes entre la devolución de zip() y la aplicación del bucle:


Tengamos en cuenta las limitaciones de la función zip(), ya que solo combinará elementos entre iterables mientras coincidan los índices de cada elemento en su secuencia respectiva, mientras que la combinatoria que nos permite establecer el bucle for/in, no se ve afectada por esta restricción y devuelve todas las combinaciones posibles entre elementos:





Y como lo prometido es deuda y a pesar de que recogeremos una lista de enlaces interesantes para aprender a programar en Python antes de comenzar con la POO, aquí os dejamos un par de ellos:

    • Documentación de Python 3.6 en Español, traducida por la comunidad de programadores Python de la República de Argentina, con especial atención a los apartados 11 y 12 que nos acompañan por "un pequeño paseo por la Biblioteca Estándar".
    • Python para impacientes, un magnífico e impagable blog donde sus autores,  Alejandro Suárez Lamadrid y Antonio Suárez Jiménez, Andalucía - España, desarrollan un trabajo espectacular, con todo el contenido en general, y con las librerías en particular. 🏆
Listado de funciones integradas de Python. Fuente: Documentación de Python



ALMENDROS EN FLOR EN EL VALLE DE SANTIAGO DEL TEIDE, OESTE DE TENERIFE.


No hay comentarios:

Publicar un comentario