x = 5
un_texto = 'Hola mundo!' # Esto es un comentario
una_lista = [1,2,3, "texto"]

"""
Explanation: ¿Qué es programar?
La computadora es una gran gran calculadora que permite hacer cualquier tipo de cuenta de las que necesitemos dentro de nuestras carreras (y de la vida también) mientras sepamos cómo decirle a la máquina qué cómputos hacer.
La idea de esta primera clase, es comprender los siguientes conceptos:
* Cómo definir variables y las verlas en pantalla.
* Cómo hacer cuentas con las variables.
* Trabajar con listas.
* Condicionales (if).
* Reiterar acciones (for loops).
* Declarar funciones

Para hacer estas cuentas la computadora tiene que almacenar los números que necesitemos y luego hacer las operaciones apropiadas con ellos.
Nuestros valores numéricos se guardan en espacios de memoria, y esos espacios tienen un nombre, un rótulo, con el cual los podremos llamar.
La computadora comprende estos nombres y uno puede indicarle que los utilice para operar con ellos, modificarlos, etc.
Ese nombre a cada espacio de memoria se asigna, al menos en Python, con el símbolo =, que suele llamarse operador de asignación.
Pero no solamente guardaremos valores numéricos.
Además, como veremos ahora, podemos guardar otros tipos de datos, como texto (strings) y listas (lists) entre muchos otros.
Todos los tipos de valores que podremos almacenar difieren entre si en el espacio en memoria que ocupan y las operaciones que podremos hacer con ellos.
A estos tipos de datos con los que se puede operar se los llama objetos.
Veamos un ejemplo
End of explanation
"""


print(x)
print(type(x))
print(un_texto, "es del tipo", type(un_texto))

"""
Explanation: Acá asignamos al espacio en memoria x el valor 5 que es un objeto de tipo int (número entero o integer en inglés).
A estos espacios de memoria se los suele llamar variables.
De modo que en este caso tenemos la variable x que guarda el valor 5.
Además, creamos otras dos variables: un_texto y una_lista.
Estas tienen los valores 'Hola mundo!' y [1,2,3] respectivamente.
Los valores entre comillas simples (') o dobles (") son objetos del tipo string, que es la forma en que se le dice a la computadora que se busca almacenar una palabra.
Y los valores entre corchetes son listas, que básicamente son conjuntos de otros valores.
En nuestro ejemplo armamos una lista con los valores numéricos 1, 2, 3 y con un valor del tipo string "texto".
Cualquier texto escrito luego de # Python lo toma como un comentario y no lo trata de ejecutar.
Esto está muy bueno para dejar anotaciones en el código.
¡Úsenlo!
Funciones predefinidas
Ahora que tenemos las variables ya declaradas podemos operar con funciones sobre ellas.
Las funciones son un conjunto de líneas de código que realizan alguna tarea específica.
Por ejemplo veamos las funciones print() y type().
End of explanation
"""


print(type(x, un_texto))

"""
Explanation: Como vemos, la función print() nos permite mostrar en pantalla el valor de una variable específica.
Por otro lado, la función type() nos retorna el tipo de objeto almacenado en una variable.
Donde 'int' hace alusión a los números enteros y 'str' a los strings.
Esta función es muy útil para entender con qué tipo de variables estamos trabajando.
Además, vemos que print(), por ejemplo, puede recibir más de un objeto como argumento.
Pasa lo mismo con type()?
End of explanation
"""


x = 10
y = 8
print('La suma de x e y es igual a', x + y, '. Y el resultado es de tipo', type(x+y))
print('La resta de x e y es igual a', x - y, '. Y el resultado es de tipo', type(x-y))
print('La división entre x e y es igual a', x / y, '. Y el resultado es de tipo', type(x/y))
print('La multiplicación entre x e y es igual a', x * y, '. Y el resultado es de tipo', type(x*y))
print('x elevado a la y es igual a', x ** y, '. Y el resultado es de tipo', type(x**y))

"""
Explanation: No, no pasa lo mismo.
En este caso nos dice que sólo puede recibir 1 ó 3 argumentos.
(Obs: los otros dos argumentos son para cuestiones más específicas que no nos van a interesar por ahora)
Operadores y tipos de números
Para los objetos que son números, los operadores +, -, *, / y ** funcionan exactamente como los operadores matemáticos que conocemos.
(Obs: El operador ** es el "elevado a")
Por ejemplo:
End of explanation
"""


print('Ejemplo 1:') # Solo en versiones >= 3.6
print(f'La suma de x e y es igual a {x} + {y}. Que es de tipo {type(x+y)}')
## la f al principio importa, y mucho
#print('La suma de x e y es igual a {x} + {y}. Que es de tipo {type(x+y)}')

print('\nEjemplo 2:')
print('La suma de x e y es igual a {} + {}. Que es de tipo {}'.format(x, y, type(x+y))) 

print('\nEjemplo 2 bis:')
print('La suma de x e y es igual a {1} + {2}. Que es de tipo {0}'.format(type(x+y), x, y))

print('\nEjemplo 3:')
print('La suma de x e y es igual a %d + %d. Que es de tipo %s' % (x, y, type(x+y)))


"""
Explanation: Tipos de números: enteros, flotantes e imaginarios
La computadora no almacena todos los números de igual forma.
Hay dos tipos de datos fundamentales: los números enteros (int) y los números de punto flotante float.
Hay números que no pueden ser representados por enteros, como los decimales, o que sería muy ineficiente representarlos como enteros, los números muy grandes (más de $\sim 10^{19}$).
Y para eso existen los floats, que guardan a los números en notación científica:
$$a \,.\, b^c$$
Por ejemplo,
$$200 \rightarrow 2\,.\,10^2$$
$$0.002 \rightarrow 2\,.\,10^{-3}$$
No es importante saber el detalle de cómo se representan estos números, pero sí que existen dos tipos de números.
Es importante porque, en ciertos lugares, Python necesita que usemos especïficamente un número entero.
En las operaciones que hicimos arriba, todas devolvieron un int, a excepción de la división, que devolvió un float.
En general, el resultado de una división no puede ser representado por un entero y, como Python a priori no sabe el resultado, se convierten los números a flotantes antes de realizar la división$^{1}$.
Esto sucede en general para cualquier operación que pueda dar como resultado un número no entero: raíces, logarítmos, etc.
Si sabemos que el resultado es un entero, podemos convertirlo con la función int:
python
int(4 / 2)
En caso de que queramos realizar la división entera (10 "dividido" 8 = 1) podemos utilizar el operador //. Y en caso que queramos declarar directamente un número con coma podemos poner x = 10.0
Por completitud, el último tipo de número que falta es el imaginario. Que se declaran de la siguiente manera:
python
num_complejo = 24 + 10j
i = 0 + 1j
De forma nativa Python utiliza j en vez de i porque es la letra más usual en ingeniería en las universidades.
$\scriptsize\text{1. En realidad, esto es así a partir de la versión 3 de Python.}$
Inciso: Cómo dar formato al texto
Antes de continuar vamos a ver un poco cómo dar formato a los strings.
Esto nos interesa para poder imprimir cosas en pantalla de una forma más ordenada y elegante, pero además es muy útil para manejar instrumentos en laboratorio, ya que estos reciben como input una cadena de texto.
En el ejemplo anterior para imprimir un texto junto a los valores de las variables, realizamos líneas de este estilo
python
print("La suma de x e y es igual a", x + y, ". Que es de tipo", type(x+y))
que en principio no tienen nada de malo, pero son difíciles de leer en el código y pueden escalar muy feo si hay que poner más variables.
Dicho esto, veamos algunas formas de reescribir estas líneas de forma más elegante:
End of explanation
"""


lista1 = [1,2,3, "texto"]
print(lista1, type(lista1))
print(lista1[0], type(lista1[1]))
print(lista1[2], type(lista1[2]))
print(lista1[-1], type(lista1[-1]))
print(lista1[1:3], type(lista1[1:3]))

lista2 = [15, 10]
lista3 = [12, 17]
print(lista2 + lista3)
print(lista2[1] + lista3[0])

a = 10
b = a**2
c = a**3
lista4 = [a, b, c]
print(lista4)

"""
Explanation: ...
En el ejemplo 1 vemos una forma muy elegante y fácil de leer que se conoce como f-string (format string).
Esta forma está muy buena, pero sólo es compatible con las versiones de Python a partir de la 3.6, y muchas computadoras (si no están actualizadas) pueden no tener esta versión.
En el ejemplo 2 y 2 bis vemos otra forma, también bastante concisa y elegante.
Esta es la recomendada para programas que van a compartir con otra gente, ya que es compatible con todas las versiones de Python.
La idea en general es poner {} donde van las variables y luego listarlas al final como argumentos del .format().
En el bis vemos que también es posible listarlas desordenadas y utilizar números entre las llaves para elegir el orden deseado.
En el ejemplo 3 vemos una forma un tanto distinta, donde hay que aclarar el tipo de variable con la que estamos trabajando (%d hace alusión a digit y %s a string).
Este formato es bastante molesto de usar pero tiene la ventaja de que es común a otros muchos lenguajes de programación como C, C++ y Java.
La idea de declarar el tipo de variable viene de lenguajes que necesitan compilar.
Acá un link si alguien quiere adentrarse más en la diferencia entre lenguajes interpretados (Python, Javascript, ...) y lenguajes compilados (C, Fortran, ...).
Aclaración: vemos un \n en algunos de los textos.
Estos simplemente lo que hace es poner una linea en blanco en el medio.
Si lo probás por cuenta propia rápidamente vas a entender cómo funciona.

Ejercicio 1
Encontrar las raíces de el polinomio
$$x^2 + \frac{3}{2}x -1$$
Recordar que la fórmula para las raíces de un polinomio del estilo $a\,x^2 + b\,x + c$ es
$$x_{1,2} = \frac{-b\pm(b^2 -4ac)^{\frac{1}{2}}}{2a}$$
Objetivos

Declarar las variables $a$, $b$, $c$ para los coeficientes
Guardar los resultados en las variables $x1$ y $x2$.
Imprimir los valores de $x1$ y $x2$ aclarando cuál es cada uno usando alguna de las formas que vimos para formatear texto.

¿Los resultados son de tipo int o float?
Ejercicio 1 bis: Reemplazar los valores del Ejercicio 1 para encontrar las raíces de 
$$4x^2 - x$$

Listas
Las listas son grupos de datos de cualquier tipo a los que llamamos por el mismo nombre de variable.
Estos datos están enumerados dentro de la lista, de forma de poder acceder a cualquiera de ellos.
Cosas a conocer sobre las listas

En Python, las listas se enumeran desde el 0 en adelante. De modo que el primer elemento es el lista[0].
Para pedir, por ejemplo, el cuarto elemento ponemos lista[3], por lo mismo que el punto anterior.
Para pedir el último elemento de una lista, una sintaxis válida es lista[-1], para el penúltimo es lista[-2], y así...
Para obtener todos los elementos de una lista entre el i-ésimo y el j-ésimo valor, la sintaxis es lista[i-1:j] (probando va a quedar más claro!).
Se puede guardar el valor de una variable dentro de una lista sin ningún problema.
El operador de suma (+) entre listas las concatena. No suma elemento a elemento como si fuera un vector (eso lo veremos más adelante en el curso).

Veamos algunos ejemplos de todo esto:
End of explanation
"""


lista = [1, 4, 7]
print(lista)
lista.append(9)
print(lista)

"""
Explanation: Agregar elementos a una lista
En la sección de strings vimos el .format(), que es una función propia de los tipos de variables de texto.
Otra función de esta índole es el .append().
Esta función se puede utilizar sobre una lista para agregar un elemento a ella.
Por ejemplo:
End of explanation
"""


obj_range = range(10)
lista_range = list(obj_range)

print(obj_range, type(obj_range))
print(lista_range, type(lista_range))

"""
Explanation: Función range()
Para hacer listas largas sin escribir los elementos uno por uno existen algunas funciones.
Una de ellas es range().
Esta función nos devuelve un objeto que podemos convertir a una lista mediante la función list().
Veamos cómo funciona mediante un ejemplo:
End of explanation
"""


print(list(range(10, 15)))
print(list(range(0, 10, 2)))

"""
Explanation: La fución range() nos da un objeto del tipo range que define una sucesión de números en abtracto, y podemos convertirlos a una lista de números explícitos, si lo necesitamos, utilizando list() (esto de convertir un objeto en otro vamos a poder hacerlo casi siempre entre objetos similares y durante el curso vamos a ver otros ejemplos).
Tener en cuenta que el objeto range(0, 10), por ejemplo, nos da la succesión de elemenentos del 0 incluído hasta el 10 excluído.
Esto es así para que la función range(10) contenga 10 elementos y no 11.

Documentación de una función
Siempre que se quiera conocer más de de una función, o de cualquier otro objeto en Python, se puede utilizar el comando help(lo_que_quiero_conocer). 
Como ejemplo, en este caso sería help(range).
No lo ponemos en la presentación porque el texto de salida es largo.
Pero si lo prueban en sus computadoras van a ver que en una de las primeras lineas les dice:
|  range(stop) -&gt; range object
 |  range(start, stop[, step]) -&gt; range object
lo que significa que no sólo podemos darle el argumento stop como valor, sino que, opcionalmente, también podemos darle un inicio y un paso.
Veamos dos ejemplos:
End of explanation
"""


print(5 > 4)

"""
Explanation: Condicionales y booleans
En esta sección vamos a introducir un nuevo tipo de variables: los bool.
Esta variable solo puede tomar dos valores posibles: 1 y 0, o True y 
False.
Son especialmente útiles para verificar si una condición se cumple (True) o no (False).
Estas variables se comportan bajo el álgebra de Bool, por lo que permiten hacer operaciones lógicas si se está familiarizado.
Lo que nos es relevante para este momento del curso es que nos permiten verificar (des)igualdades.
Por ejemplo, si le preguntamos a Python si 5 es mayor que 4 nos va a devolver el valor True
End of explanation
"""


print(5 > 4)
print(5 < 4)
print(5 == 4)
print(5 != 4)
print(type(4 > 5))

"""
Explanation: El símbolo &gt; es lo que se llama un operador lógico.
Acá una lista de los más usados:
* a&gt;b Verifica si a es mayor que b
* a&lt;b Verifica si a es menor que b
* a&gt;=b Verifica si a es mayor o igual que b
* a&lt;=b Verifica si a es menor o igual que b
* a==b Verifica si a es igual que b
* a!=b Verifica si a es distinto que b
Tener en cuenta que para verificar la igualdad se usa == y no = ya que este último está reservado para asignar un valor.
Ejemplo
End of explanation
"""


num1 = -3
num2 = 4

if num1 > 0:
    print('Este número es positivo:')
    print(num1)
    
if num2 > 0:
    print('Este número es positivo:')
    print(num2)

"""
Explanation: Condicionales (if, elif y else)
Veamos los condicionales con un ejemplo sencillo.
Supongamos que queremos saber si un número es positivo, y si lo es, queremos printearlo.
Para poder armar este tipo de condiciones en el código se usa el if.
Ejemplo:
End of explanation
"""


if num1 > 0:
    print('Este número es positivo:')
    print(num1)
else:
    print('Este número no es positivo')
    print(num1)

"""
Explanation: Vemos que la estructura de cada uno de losif es algo así:
```Python
esto se ejecuta sí o sí
if BOOL:
    |esto se ejecuta
    |si BOOL es True
esto se ejecuta sí o sí
```
Las barritas marcan que esa parte de código está identada (es la forma anglosajona decír que tiene una sangría), lo que implica que es una parte que se va a ejecutar únicamente si la condición que está arriba (y sin identar) se cumple.
En el código de arriba se están leyendo ambos if, pero el primero tiene una condición que retorna False por lo que no ejecuta el código que tiene adentro.
En cambio, la condición del segundo es True, por lo que sí se ejecutan sus print().
Pero qué pasa si también quiero printear los números que no son positivos?
Para eso existe else, que te permite ejecutar un bloque distinto de código cuando una dada condición no se cumple, por ejemplo:
End of explanation
"""


num = -3

if num > 0:
    print('El número es positivo:')
    print(num1)
    
elif num < 0:
    print('El número es negativo')
    print(num1)
    
else:
    print('El número es cero')

"""
Explanation: Bien, pero qué pasa si el número no es ni positivo ni negativo sino que es 0?
Para resolver este problema existe elif que es una abreviación de else if que nos permite agregar más de una condición.
En este caso:
End of explanation
"""


lista = [1, 6, 8, 9]
for elemento in lista:
    print(elemento + 0.5)

"""
Explanation: Por lo que la estructura completa queda:
```Python
esto se ejecuta sí o sí
if BOOL_1:
    |esto se ejecuta
    |si BOOL_1 es True
elif BOOL_2:
    |esto se ejecuta
    |si BOOL_1 es False
    |pero BOOL_2 es True
elif BOOL_3:
    |esto se ejecuta
    |si BOOL_1 es False,
    |si BOOL_2 es False
    |pero BOOL_3 es True
...(elif BOOL_4 etc)
else:
    |esto se ejecuta
    |si todas las BOOL_n
    |son False
esto se ejecuta sí o sí
```

Ejercicio 2
Realizar un pequeño programa en el que verifique si un número es par.
Para esto, pueden verificar si el resto al dividirlo por 2 es cero.
Luego, si el número es par, que lo imprima en pantalla.
Si no lo es que imprima el siguiente número par.
La idea es que tengan un resultado similar al siguiente:
```
input: 4
output: El número es par.
input: 5
output: El número es impar.
        El siguiente número par es 6
```
Ayuda: El operador a%b da el resto de dividir a entre b

For loops
Supongamos ahora que queremos realizar este proceso que hicimos antes para los para los primeros 10 números enteros positivos.
Vemos que es muy trabajoso hacerlo a mano.
Y en general los programas los vamos a realizar sobre muchísimos más elementos que 10.
Para solucionar este problema podemos utilizar los cíclos de Python, que nos permiten ejecutar un código muchas veces sobre distintos elementos.
Veamos un ejemplo del ciclo for.
Para esto, vamos a crear una lista con algunos números, vamos a recorrer sobre ella, vamos a sumarle 0.5 a cada uno de los elementos y vamos a printearlos.
End of explanation
"""


for i in range(3):
    print(i)

"""
Explanation: Al proceso de recorrer los elementos de un objeto se lo llama iterar.
En este caso estamos iterando sobre una lista, pero el for nos permite iterar sobre varios objetos, como por ejemplo el range.
End of explanation
"""


for i in range(len(lista)):
    print(lista[i] + 0.5)

"""
Explanation: De este modo podemos reproducir el resultado anterior usando el range.
End of explanation
"""


lista = [1, 6, 8, 9]
for i in range(len(lista)):
    print(f'El elemento {i} de las lista es {lista[i]}')

"""
Explanation: Que parece más engorroso pero es útil si te interesa conocer el número del elemento con el que estás trabajando, ya que el i guarda ese valor, por ejemplo
End of explanation
"""


def promedio(num1, num2):
    suma_de_numeros = num1 + num2
    resultado = suma_de_numeros / 2
    return resultado

"""
Explanation: OBS: Esta no es la forma usual (ni recomendada) de recorrer una lista los elementos e índices de una lista.
Python ofrece una función enumerate() que solucióna el problema y facilita mucho la escritura.
No la vamos a usar en este curso, pero pueden buscarla por su cuenta.

Ejercicio 3
Realizar el Ejercicio 2 para todos lo números enteros positivos hasta el 10 utilizando un ciclo for.
Ayuda:
Se pueden unir los ciclos for e if. Osea, esto vale:
Python
for ALGUNA_ITERACIÓN:
    |if BOOL:
    |    ...
    |else:
    |    ...
y esto también:
Python
if BOOL:
    |for ALGUNA_ITERACIÓN:
    |    ...

Funciones
Ya vimos algunas funciones, como print(), range(), type() o help().
Pero supongamos que quiero una función que haga otra cosa, como calcularme el promedio entre los dos números.
En este caso no conocemos ninguna función que haga eso, así que vamos a crearla.
Para esto utilizamos las palabra def para definir una nueva función y return para indicar que devuelva un cierto valor.
End of explanation
"""


print(promedio(2, 6))
print(promedio(5, 5))
print(promedio(3, 4))

"""
Explanation: de este modo declaramos la función promedio. La estructura en general es
Python
def nombre_de_la_funcion(par_1, par_2, par_3, ...):
    |bloque de código
    |return algun_valor
donde par_1, par_2, ... son los parámetros de la función.
Ahora podemos llamar a esta función de el mismo modo que hacíamos con las demás:
End of explanation
"""


