From ef1779490b877405eff362b4e163f5bf64ebe1b1 Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Tue, 19 Nov 2019 16:07:14 +0100 Subject: add chapter 2: data --- src/02_datos.md | 587 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 587 insertions(+) create mode 100644 src/02_datos.md (limited to 'src') diff --git a/src/02_datos.md b/src/02_datos.md new file mode 100644 index 0000000..3a35bea --- /dev/null +++ b/src/02_datos.md @@ -0,0 +1,587 @@ +# 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 + +Aunque no lo hemos mencionado, 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 al programador la +facilidad de poder crear sus propios nombres para no tener que usar las +direcciones propias de la memoria, que normalmente son número sin más +significado que su posición, la mayor parte de las veces permitiendo al +programador 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: + +``` +>>> 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: + +``` +>>> b +Traceback (most recent call last): + File "", line 1, in + b +NameError: name 'b' is not defined +``` + +### 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 referencia es lo que en otros lenguajes se +conoce como un 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. + +> NOTA: 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. + +``` +>>> 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: + +``` +>>> 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[^1], 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. + +[^1]: https://en.wikipedia.org/wiki/Floating-point_arithmetic + +Declarar números de coma flotante es natural porque usa una sintaxis a la que +estamos acostumbrados: + +``` +>>> 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. + +``` +>>> 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). + +``` +>>> "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. + + +``` +>>> "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" +``` + +> NOTA: la contrabarra sirve para introducir caracteres especiales o caracteres +> de escape[^2]: `\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.: `\\`. + +[^2]: https://en.wikipedia.org/wiki/Escape_character + + +### 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 a *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. + +``` +>>> (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 inmutables. Esto significa que no 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. + +``` +>>> [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 el usuario. + +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 son de tipo string. En el caso de los diccionarios, además, se utilizan +llaves para definirlos. 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. + +``` +>>> {"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 +``` + +## 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 como se hace con algunos +ejemplos: + +``` +>>> 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} +``` + +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. + +## 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, el operador `=`, que sirve para nombrar cosas, la suma +(`+`) o la resta (`-`), pero hay otros. + +``` +>>> 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: + +| operator | meaning | +|---|---| +| < | strictly less than | +| <= | less than or equal | +| > | strictly greater than | +| >= | greater than or equal | +| == | equal | +| != | not equal | +| is | object identity | +| is not | negated object identity | +| in | containment test | +| not in | negated containment test | + +``` +>>> 1 > 1 +False +>>> 1 >= 1 +True +>>> 1 not in (0, 2, 3) +True +``` + +### Operadores lógicos + +Los operadores lógicos mezclan booleanos y son muy importantes, sobre todo para +combinar las pruebas de verdad. + +| operator | meaning | +|---|---| +| and | logical and, returns True if both True, False otherwise | +| or | logical or, returns True if any True, False otherwise | +| not | logical not, returns True if False, True otherwise | + +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. + +``` +>>> 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. + +| operator | meaning | +|---|---| +| + | addition | +| - | subtraction or negative | +| `*` | multiplication | +| / | division | +| `**` | power | +| % | remainder | + +### 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: + +``` +>>> "a" + "a" +'aa' +``` + +Esto se debe a que la funcionalidad del operador `+` ha sido diseñada para +operar de forma especial en Strings, 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. + +``` +>>> 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: + +``` +>>> 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: + +``` +>>> 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. + +> NOTA: 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: + +``` +>>> 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. + +``` +>>> a = {} +>>> b = dict(a) +>>> a["cambio"] = "hola!" +>>> b +{} +>>> a +{'cambio': 'hola!'} +``` -- cgit v1.2.3