# Trabajando con datos Programar es, principalmente, tratar con datos y los datos no son más que piezas de información que se estructura de una forma concreta. ## Nombrando datos Los datos se almacenan en la memoria principal de la computadora. Puedes imaginarte la memoria como un conjunto de cajas identificadas con direcciones, como si fuesen los buzones de un portal con muchos pisos. Para utilizar datos, éstos se guardan en los diferentes cajones y se van extrayendo y actualizando. La importancia de nombrar las cosas es evidente, si no guardamos un listado de dónde hemos guardado qué, no podemos recuperar los datos que hemos creado. Al igual que en las matemáticas se utilizan los símbolos para representar posibles valores y simplificar los procesos, en los lenguajes de programación se utilizan símbolos para definir referencias a los datos y así poder referirse a los mismos datos por su nombre sin tener que introducir su contenido constantemente. Los lenguajes de programación aportan a quien programa la facilidad de poder crear sus propios nombres para no tener que usar las direcciones propias de la memoria, que normalmente son números sin más significado que su posición, la mayor parte de las veces permitiendo a quien programa abstraerse de estos detalles internos. En python existe un operador para declarar símbolos que hacen referencia a un valor: el operador `=`. Este operador enlaza el valor de la derecha con el nombre de la izquierda. Con un ejemplo es más fácil de comprender. Probando en la REPL: ``` python >>> a = 10 >>> a 10 >>> a + 1 11 ``` Como ves, en el primer paso se asocia el valor `10` al identificador `a` y más adelante se puede utilizar `a` para hacer referencia al valor enlazado. Las referencias han de ser declaradas antes de usarse, si no, el intérprete no las conoce y lanza una excepción: ``` python >>> b Traceback (most recent call last): File "", line 1, in b NameError: name 'b' is not defined ``` > Los nombres para poder ser interpretados correctamente por python deben > cumplir unas normas estrictas: > > - No pueden tener espacios. > - Sólo pueden estar formados por combinaciones de letras, números y el > símbolo `_`. > - No pueden empezar por un dígito. ### Todo es una referencia El comportamiento de estas referencias puede no ser intuitivo si vienes de otros lenguajes. El operador `=` enlaza, como se dice anteriormente, un nombre a un valor concreto, pero eso no significa que copie su contenido. En python los valores y los nombres son conceptos independientes. Los valores ocupan su lugar en la memoria y los nombres hacen referencia a dónde se encuentran estos valores. Es muy común tener muchas referencias a la misma estructura de datos y transformar su contenido desde todas las referencias. A nivel técnico, lo que en python se conoce como variable y en este documento hemos hecho el esfuerzo de llamar, hasta este momento, referencia es parecido a lo que en otros lenguajes se conoce como puntero y la labor del operador `=` es la de asignar el puntero a la dirección donde se encuentran los datos a los que debe apuntar. Volviendo a la metáfora de los buzones, en lenguajes de más bajo nivel como C estás obligado a seleccionar qué buzón vas a utilizar para introducir cada dato. Por lo que si cambia el tipo de dato a gestionar, puede que el buzón se quede pequeño o que los datos se interpreten de forma incorrecta. Sin embargo, python guarda las referencias de forma independiente a los valores y adecua el número de buzones en uso al tamaño de los datos de los que dispones reordenando, si es necesario, la estructura completa de valores y referencias. A este concepto se le conoce como gestión automática de memoria (*automatic memory management*). En resumen, las referencias en python son únicamente un recordatorio que sirve para poder acceder a un valor. Esto cobra importancia más adelante, y no vamos a rehuir la responsabilidad de enfangarnos en ello. ## Tipos Tal y como aclaraba el texto de la introducción, python tiene un sistema de tipos dinámico (*dynamic type system*). Lo que significa que gestiona los tipos de forma automática, permitiendo a los nombres hacer referencia a diferentes tipos de valor durante la ejecución del programa a diferencia de otros lenguajes como, por ejemplo, C, donde el tipo de las variables debe ser declarado de antemano y no puede cambiarse. Esto es posible debido al fenómeno explicado en el apartado anterior por un lado, y, por el otro, a que los datos de python son un poco más complejos que en otros lenguajes y guardan una pequeña nota que indica cómo deben ser interpretados. Si en algún momento se le pide a python que asigne un valor de un tipo distinto al que una referencia tenía no habrá problemas porque es el propio dato quien guarda la información suficiente para saber cómo entenderlo. Las referencias sólo almacenan dónde se guarda este dato. > Seguramente te habrás dado cuenta de que el funcionamiento de python es > más ineficiente que el de C o lenguajes similares pero mucho más flexible. Y > así es. A la hora de elegir el lenguaje debemos valorar cuál nos interesa > más para la labor que vamos a realizar. ### Tipos simples Hemos denominado tipos simples a los que no empaquetan más de un valor internamente. En otros lenguajes o contextos se les conoce como escalares. #### La nada La nada en python se representa con el valor `None` y es útil en innumerables ocasiones. #### Boolean Los valores booleanos expresan *verdad* (`True`) o *mentira* (`False`) y sirven para gestionar lógica desde esos términos. Mas adelante los veremos en acción. #### Integer Los Integer, números enteros en inglés, ya han aparecido anteriormente. Para usar un número entero puedes introducirlo tal cual. Recuerda que hay enteros positivos y negativos. El símbolo para marcar números negativos en python es el `-`, que coincide con el operador de la resta. ``` python >>> 14 14 >>> -900 -900 >>> ``` Los números enteros también pueden expresarse en otras bases, como en hexadecimal. Dependiendo de la aplicación en la que te encuentres igual necesitas mirarlo más en detalle: ``` python >>> 0x10 16 ``` #### Float Los números Float, o de coma flotante, son números no-enteros. Ten cuidado con ellos porque la coma flotante es peculiar. El nombre coma flotante viene de que la coma no siempre se mantiene con la misma precisión. En realidad estos números se guardan como los números en notación científica (2,997E8 m/s para la velocidad de la luz, por ejemplo). La notación científica siempre implica tener una coma, pero cuya posición se varía con el exponente posterior. El modo de almacenamiento de los números de coma flotante es muy similar a la notación científica: se almacenan dos valores de tamaño fijo, la *mantisa* y el *exponente* para, de este modo, poder ajustar la precisión en función del tamaño del número almacenado. No es lo mismo expresar la velocidad de la luz, 2,997E8 m/s, que la longitud de onda del color rojo, 6,250E-7 m, ambos valores tienen un tamaño muy distinto, pero usando dos *mantisas* de tamaño similar, cuatro dígitos (2997 y 6250), y dos exponentes de tamaño similar, un dígito (8 y -7), hemos expresado valores muy diferentes, ambos con la misma precisión relativa. El problema viene cuando nos apetece mezclarlos, por ejemplo, sumándolos. Imagina que tienes dos valores de esas dimensiones, uno de millones y otro de millonésimas partes de la unidad, y los quieres sumar entre ellos. Si sólo tienes una mantisa limitada para representarlos, la suma resultará en el redondeo de ambos a la precisión que tienes disponible. Es decir: el resultado será el valor grande y el pequeño se perderá en el redondeo. Aunque en este caso la suma es precisa, si tratas con un billón de valores pequeños y uno grande y quieres obtener la suma de todos y los sumas en parejas siempre con el grande, en cada suma se descartará el valor pequeño en el redondeo y el resultado total será el valor grande que tenías. Sin embargo, si sumas los valores de tamaño similar entre ellos primero, obtendrás un valor suficientemente grande como para alterar el resultado de la suma contra el valor grande al final, dando así un resultado distinto. Te recomiendo entonces, que si te encuentras en una situación como ésta tengas cuidado y, por ejemplo, ordenes los números de menor a mayor antes de sumarlos, para obtener una suma de buena precisión. En realidad, el exponente en el caso de python (y en casi todos los demás lenguajes) no está elevando un 10 a la enésima potencia, si no que lo hace con un 2. Por lo que lo expresado anteriormente es un poco distinto. Esto provoca que algunos números de coma flotante no sean tan redondos como deberían (por ejemplo, `2.999999999`, cuando debería ser `3.0`) y compararlos entre ellos puede ser desastroso. Para evitarlo, te recomiendo que *siempre* redondees los valores a una precisión que puedas controlar. Aunque realmente es algo más complejo, lo que sabes ahora te evitará problemas en el futuro, sobre todo cuando analices datos, uno de los sectores donde python se usa de forma extensiva. Si necesitas saber más, debes investigar la artimética de coma flotante, o *floating point arithmetic* en inglés. Declarar números de coma flotante es natural porque usa una sintaxis a la que estamos acostumbrados: ``` python >>> 1E10 10000000000.0 >>> 1.0 1.0 >>> 0.2E10 2000000000.0 >>> ``` #### Complex Python soporta números complejos y los expresa utilizando la letra `j`. Como suponen un caso bastante concreto no los analizaremos en detalle. Pero tienes disponible la documentación de python para lo que quieras. ``` python >>> 1-23j (1-23j) ``` #### String Un String es una cadena de caracteres. Los Strings en python son, a diferencia de en otros lenguajes, un escalar, al contrario de lo que la primera definición que hemos expresado puede hacernos pensar. En python los Strings no son un conjunto de caracteres alfanuméricos sueltos que se comportan como un valor, es al revés. El concepto de carácter no existe y ha de expresarse con un String de longitud 1. Los strings se expresan rodeando texto con comillas dobles, `"`, o simples `'` (el símbolo del apóstrofe). ``` python >>> "Hola" 'Hola' >>> 'Hola' 'Hola' ``` El hecho de que haya dos opciones para delimitar los strings facilita el etiquetado como string de valores que contienen las propias comillas en su contenido. También puede utilizarse la contrabarra `\` para cancelar la acción de las comillas. ``` python >>> "Tiene un apóstrofe: Luke's" "Tiene un apóstrofe: Luke's" >>> 'Tiene un apóstrofe: Luke's' SyntaxError: invalid syntax >>> 'Tiene un apóstrofe: Luke\'s' "Tiene un apóstrofe: Luke's" ``` > la contrabarra sirve para introducir caracteres especiales o caracteres > de escape: `\n` salto de línea, `\t` tabulador, etc. Que son una herencia de > los tiempos de las máquinas de escribir, pero son aún útiles y muy usados. > Para expresar la propia contrabarra ha de escaparse a sí misma con otra > contrabarra para que no evalúe el siguiente caracter como un caracter de > escape.: `\\`. ### Tipos compuestos Hemos denominado tipos compuestos a los que pueden incluir diferentes combinaciones de tipos simples. Estos tipos dejan de ser escalares y se comportan como vectores o conjuntos de datos. Estos tipos de dato pueden contenerse a sí mismos, por lo que pueden crear estructuras anidadas complejas. #### Tuple Las tuplas o *tuple* en inglés son el tipo compuesto más sencillo en python. Las tuplas definen un conjunto de valores de cualquier tipo. Se declaran utilizando paréntesis añadiendo sus elementos separados por comas. Y se accede a sus contenidos utilizando los corchetes e introduciendo el índice del elemento que se quiere extraer. ``` python >>> (2, 3, "HOLA") (2, 3, 'HOLA') >>> (2, 3, "HOLA")[0] 2 ``` En python los índices comienzan en `0`. #### List Las listas o *list* son muy similares a las tuplas, pero son algo más complejas porque pueden alterarse así mismas. A diferencia de todos los tipos que hemos visto hasta ahora, tanto las listas como los diccionarios que veremos a continuación son mutables. Esto significa que puede transformarse su valor. Más adelante trataremos esto en detalle. De momento, recuerda que las listas se construyen de forma similar a las tuplas, pero utilizando corchetes en lugar de paréntesis. La forma de acceder a los índices es idéntica. ``` python >>> [2, 3, "HOLA"] [2, 3, 'HOLA'] >>> [2, 3, "HOLA"][0] 2 ``` #### Dictionary Los diccionarios o *dictionary* son un tipo de dato similar a los dos anteriores, pero que en lugar de utilizar índices basados en la posición de sus elementos usan claves arbitrarias definidas por quien programa. Además, los diccionarios no están ordenados así que no se puede suponer que las claves siempre van a estar en el orden en el que se introducen. Para declarar diccionarios es necesario indicar qué claves se quieren usar. Las claves pueden ser de cualquier tipo que se considere *hasheable*[^hasheable], concepto que se analiza más adelante, aunque normalmente su usan cadenas de caracteres como claves. El acceso a sus valores se realiza con los corchetes, del mismo modo que en las listas, pero es necesario seleccionar la clave para acceder. Los diccionarios, al igual que las listas, son mutables. Como veremos en seguida. ``` python >>> {"nombre": "Guido", "apellido": "Van Rossum", "popularidad": 8.0} {'nombre': 'Guido', 'apellido': 'Van Rossum', 'popularidad': 8.0} >>> {"nombre": "Guido", "apellido": "Van Rossum", "popularidad": 8.0}["popularidad"] 8.0 ``` [^hasheable]: Los diccionarios son una implementación del concepto conocido como *hashmap* o *hash-table*. Su funcionamiento interno requiere que las claves puedan procesarse mediante una función de *hash*. #### Set Los *sets* son muy similares a las listas y tuplas, pero con varias peculiaridades: - Sus valores son únicos. No pueden repetirse. - No están ordenados. - No se puede acceder a ellos mediante los corchetes (`[]`). Estas dos características tan estrictas, lejos de ser limitantes, aportan una mejora radical en su rendimiento. Buscar elementos en un set es extremadamente eficiente y se usan principalmente para esa labor. Si quieres validar en algún momento que un valor pertenece a un conjunto de valores, el set es el tipo de dato que estás buscando. Los sets se declaran también usando las llaves, como un diccionario, pero no usan claves. ``` python >>> {"a", "b", 1} {'a', 1, 'b'} ``` Otro de los usos más habituales de los sets es el de aplicar teoría de conjuntos (*set* significa «conjunto»). Los sets pueden combinarse forma eficiente mediante uniones (*union*), diferencias (*difference*), intersecciones (*intersection*) y otros métodos descritos en esta teoría. ## Conversión Ahora que conoces los valores sé que quieres saber cómo cambiar de uno a otro. Cómo leer un Integer desde un String, etc. Python tiene funciones para construir sus diferentes tipos de datos a partir de los diferentes inputs posibles. Aunque aún no sabes ejecutar funciones te adelanto cómo se hace con algunos ejemplos: ``` python >>> bool(1) True >>> bool(0) False >>> int("hola") Traceback (most recent call last): File "", line 1, in int("hola") ValueError: invalid literal for int() with base 10: 'hola' >>> int("10") 10 >>> float(10) 10.0 >>> complex(19) (19+0j) >>> str(10) '10' >>> tuple([1,2,3]) (1, 2, 3) >>> list((1,2,3)) [1, 2, 3] >>> dict((("a", 1),("b", 2))) {'a': 1, 'b': 2} >>> set([1,2,2,3,4,4,4,4,4]) {1, 2, 3, 4} ``` Los propios nombres de las funciones son bastante representativos de a qué tipo convierten. Si quieres saber más puedes ejecutar `help(nombre)` y ver qué te cuenta la ayuda. > Fíjate que si conviertes una secuencia de valores repetidos a *set* > únicamente almacena los que no se repiten. Es uno de los usos más comunes que > tienen. ## Operadores Ahora que sabes el contexto en el que vas a jugar, necesitas poder alterar los datos. Existen operadores básicos que te permiten transformar los datos. Algunos ya los has visto antes, como el operador `=`, que sirve para nombrar cosas, la suma (`+`) o la resta (`-`), pero hay otros. ``` python >>> 1 + 1 2 >>> 10 - 9 1 >>> 10 ** 2 100 >>> ``` Los siguientes apartados muestran algunos operadores que es interesante memorizar. ### Pruebas de verdad Las pruebas de verdad generan un valor booleano desde una pareja de valores. A continuación una lista de las pruebas de verdad con unos ejemplos: | operador | significado | |----------|-------------------------------------------| | `<` | menor que | | `<=` | menor o igual que | | `>` | mayor que | | `>=` | mayor o igual que | | `==` | igual que | | `!=` | diferente de | | `is` | identidad de objetos: «es» | | `is not` | identidad negada: «no es» | | `in` | comprobación de contenido: «en» | | `not in` | comprobación de contenido negada: «no en» | ``` python >>> 1 > 1 False >>> 1 >= 1 True >>> 1 not in (0, 2, 3) True ``` Aunque en otros lenguajes no es posible, la notación matemática habitual se puede utilizar en python concatenando pruebas de verdad: ``` python >>> x = 1.2 >>> 1 < x < 2 True ``` ### Operadores lógicos Los operadores lógicos mezclan booleanos y son muy importantes, sobre todo para combinar las pruebas de verdad. | operador | significado | |----------|----------------------------------------------------------| | `and` | «Y» lógico, es `True` si todos sus operandos son `True` | | `or` | «O» lógico, es `True` si algún operando es `True` | | `not` | «No» lógico, invierte el operando | La mayor parte de los operadores son binarios (como la suma), necesitan dos valores y devuelven otro, pero existe al menos una excepción que funciona con un único valor. El operador `not` sirve para darle la vuelta a un Booleano. ``` python >>> not True False >>> True and True True >>> False and True False >>> False or True True >>> 1 > 0 and 2 > 1 True ``` ### Matemáticos Los operadores matemáticos transforman números entre ellos. Casi todos son conocidos por su operación matemática. | operador | significado | |----------|----------| | `+` | Suma | | `-` | Negativo o resta| | `*` | Multiplicación | | `/` | División | | `**` | Potencia | | `%` | Resto | ### Operador ternario Existe además un operador que puede usar tres parámetros, el *inline-if* (*if en línea*), o *ternary operator*[^ternary]. El *ternary operator* se comporta así: ``` python >>> 1 if 1 > 9 else 9 9 >>> 1 if 1 < 9 else 9 1 ``` [^ternary]: En realidad, el nombre de operador ternario no indica nada más que el hecho de que use tres argumentos. Históricamente se ha usado este nombre para este operador en concreto, que en otros lenguajes aparece con la forma `condición ? resultado1 : resultado2`, porque no solía existir ningún otro operador que recibiera tres argumentos. ### Operación en función del tipo Python simplifica muchas tareas transformando el comportamiento de los operadores en función del tipo de dato sobre el que trabajan. Los operadores matemáticos están preparados para trabajar sobre números (de cualquier tipo) pero la verdad es que algunos pueden ejecutarse sobre otros formatos. Por ejemplo: ``` python >>> "a" + "a" 'aa' >>> [1,2] + [3,4] [1, 2, 3, 4] ``` Esto se debe a que la funcionalidad del operador `+` ha sido diseñada para operar de forma especial en Strings y en Listas, haciendo una concatenación. En el futuro, cuando aprendas a diseñar tus propios tipos podrás hacer que los operadores les afecten de forma especial, tal y como pasa aquí. ### Precedencia La precedencia en python es muy similar a la matemática y usa las mismas reglas para marcarla de forma explícita. Recuerda, en matemáticas se utilizan los paréntesis para esto. Los operadores siempre trabajan con sus correspondientes valores y python los resuelve de forma ordenada. Si generas una operación muy compleja, python la irá desgranando paso a paso y resolviendo las parejas una a una, cuanto más te acostumbres a hacerlo en tu mente menos errores cometerás. ``` python >>> 8 + 7 * 10 == (8 + 7) * 10 False >>> 8 + 7 * 10 78 >>> (8 + 7) * 10 150 >>> 78 == 150 False ``` ## Mutabilidad Ya adelantamos que el operador `=` sirve para nombrar cosas. Ahora vamos a combinar esa propiedad con la mutabilidad, o la propiedad de las cosas de alterarse a sí mismas. Empecemos con un ejemplo: ``` python >>> a = 10 >>> b = a + 10 >>> b 20 >>> a = b >>> a 20 ``` En este ejemplo hacemos que `a` haga referencia al valor 10, y después creamos `b` a partir de `a` y otro 10. Gracias a la precedencia, primero se resuelve el lado derecho completo obteniendo un 20 y después se asigna la referencia `b` a ese valor. Si queremos, después podemos reasignar el símbolo `a` a otro valor nuevo, en este caso al que hacer referencia `b`, que es 20. En este primer ejemplo, ningún valor está siendo alterado, si te fijas, sólo estamos creando nuevos valores y cambiando las referencias a éstos. Con los datos mutables podemos alterar los valores. Lo vemos con otro ejemplo: ``` python >>> a = {} >>> b = a >>> a["cambio"] = "hola!" >>> b {'cambio': 'hola!'} ``` Primero creamos un diccionario vacío y le asignamos la referencia `a`. Después le asignamos la referencia `b` a quien referenciaba `a`, es decir, al diccionario vacío. Ambas referencias apuntan al mismo dato. Si después usamos alguna alteración en el diccionario `a` como asignarle una nueva clave, `b`, que hace referencia al mismo diccionario, también ve los cambios. Esto mismo podría hacerse si se tratara de listas, ya que tienen la capacidad de alterarse a sí mismas, pero nunca podría hacerse en tuplas, porque son inmutables. > Los diccionarios y las listas soportan un montón de funciones y > alteraciones, no se mencionan en este apartado porque nunca terminaría. Se > dejan para el futuro y para los ejemplos que se verán durante el documento. Si intentásemos un ejemplo similar en tupla, no nos dejaría: ``` python >>> a = () >>> b = a >>> a[0] = 1 Traceback (most recent call last): File "", line 1, in a[0] = 1 TypeError: 'tuple' object does not support item assignment ``` Ten cuidado cuando trates con elementos mutables, sobre todo si tienen muchas referencias, porque puede que estés alterando los valores en lugares que no te interesa. Para evitar este tipo de problemas, puedes generar copias de los objetos, pero el proceso es poco eficiente y tedioso. En este segundo caso, creamos una copia de `a` para que `b` sea independiente de los cambios que ocurran en ésta. Aquí ya no estamos haciendo referencia desde `b` a los datos que había en `a`, sino a una copia de éstos, almacenada en otro lugar. ``` python >>> a = {} >>> b = dict(a) >>> a["cambio"] = "hola!" >>> b {} >>> a {'cambio': 'hola!'} ``` ## Lo que has aprendido En este apartado has conocido los tipos fundamentales de python y cómo convertir de uno a otro. Además, al conocer los operadores y su funcionamiento ya eres más o menos capaz de usar python como una calculadora. Además, has tenido ocasión de entender de forma superficial varios conceptos avanzados como el manejo automático de memoria, el tipado dinámico y los números de coma flotante, que son muy interesantes a la hora de trabajar porque te permiten comprender la realidad que tienes a tu alrededor.