# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.15.2
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Introducción a Python: Listas, Iteraciones y Strings
#
# <img style="float: right; margin: 0px 0px 15px 15px;" src="https://www.python.org/static/community_logos/python-logo.png" width="200px" height="200px" />
#
# > Ya conocemos un poco más de la sintaxis de Python, como hacer funciones y como usar condicionales. Es hora que veamos otros tipos de variables (arreglos) y cómo hacer líneas de código que ejecuten operaciones repetitivas.
#
# Referencias:
# - https://www.kaggle.com/learn/python
# ___

# # 1. Listas
#
# Las listas son objetos en Python representan secuencias ordenadas de valores. 
#
# Veamos un par de ejemplos de como crearlas:

# Primeros números primos
primos = [2, 5, 3, 7]
# Planetas del sistema solar
planetas = ['Mercurio', 'Venus', 'Tierra', 'Marte', 
            'Jupiter', 'Saturno', 'Urano', 'Neptuno']

primos

planetas

# Vemos que las listas no son exclusivamente de números. 
#
# Ya vimos listas de números, pero también de strings.
#
# Incluso, se pueden hacer listas de listas:

lista_primos_planetas = [primos, planetas]

lista_primos_planetas

# Aún más, se pueden hacer listas de diferentes tipos de objetos:

lista_diferentes_tipos = [2, 0., 'Hola', help, primos]

lista_diferentes_tipos

# Sin duda, en muchas ocasiones nos será muy útil tener una sola lista guardando varios resultados, que muchos resultados guardados en objetos individuales.
#
# Pero, una vez en la lista, ¿cómo accedemos a los objetos individuales?

# ## 1.1 Indizado
#
# Podemos acceder a los elementos individuales pertenecientes a la lista a través de brackets ([])
#
# Por ejemplo, ¿cuál planeta está más cercano al sol en nuestro sistema solar?
#
# - Acá una nota importante: Python usa índices comenzando en cero (0):

# Planeta más cercano al sol
planetas[0]

# Siguiente planeta
planetas[1]

# Todo bien...
#
# Ahora, ¿cuál es el planeta más alejado del sol?
#
# - Los elementos de una lista pueden tambien ser accedidos de atrás para adelante, utilizando números negativos:

# Planeta más alejado del sol
planetas[-1]

# Segundo planeta más alejado
planetas[-2]

# Muy bien...
#
# Y si quisiéramos averiguar, por ejemplo, ¿cuáles son los tres planetas más cercanos al sol?

# Tres primeros planetas
planetas[0:3]

# Entonces `lista[a:b]` es nuestra manera de preguntar por todos los elementos de la lista con índice comenzando en `a` y continuando hasta `b` sin incluir (es decir, hasta `b-1`).
#
# Los índices de comienzo y de término son opcionales:
# - Si no ponemos el índice de inicio, se asume que es cero (0): `lista[:b] == lista[0:b]`

# Reescribir la expresión anterior
planetas[:3]

planetas[-3:]

# - Equivalentemente, si no ponemos el índice de fin, se asume que este equivale a la longitud de la lista:

# Lista de todos los planetas comenzando desde el planeta tierra
planetas[2:]

# También podemos usar índices negativos cuando accedemos a varios objetos.
#
# Por ejemplo, ¿qué obtenemos con las siguientes expresión?

planetas[-1]

# ```python
# lista[n:n + N] = [lista[n], lista[n + 1], ..., lista[n + N - 1]]
# ```

planetas[1:-1]

planetas[-3:]

planetas[:4]

planetas[5:]

planetas[:4] + planetas[5:]

# Slice:
#
# ```python
# lista[n:n+N:s] = [lista[n], lista[n + s], lista[n + 2 * s], ..., ]
# ```

primos

primos[::2]

# Elementos de la lista en reverso (al revés)
primos[::-1]

# ## 1.2 Modificando listas
#
# Las listas son objetos "mutables", es decir, sus objetos pueden ser modificados directamente en la lista.
#
# Una manera de modificar una lista es asignar a un índice.
#
# Por ejemplo, supongamos que la comunidad científica, con argumentos basados en la composición del planeta, decidió modificar el nombre de "Planeta Tierra" a "Planeta Agua".

planetas

planetas[2] = 'Agua'

planetas

# También podemos cambiar varios elementos de la lista a la vez:

planetas[:3] = ['mer', 'ven', 'tie']

planetas

# ## 1.3 Funciones sobre listas
#
# Python posee varias funciones supremamente útiles para trabajar con listas.
#
# `len()` nos proporciona la longitud (número de elementos) de una lista:

# función len()
len(planetas)

len(primos)

# `sorted()` nos regresa una versión ordenada de una lista:

# Ayuda en la función sorted
help(sorted)

primos

# Llamar la función sorted sobre primos
sorted(primos)

sorted(primos, reverse=True)

planetas = ['Mercurio', 'Venus', 'Tierra', 'Marte', 
            'Jupiter', 'Saturno', 'Urano', 'Neptuno']

# Llamar la función sorted sobre planetas
sorted(planetas)

len('Jupiter')


def long_str(s):
    return len(s)


long_str2 = lambda s: len(s)

long_str("Jupiter"), long_str2("Jupiter")

# **Paréntesis: Funciones anónimas**
#
# Las funciones anónimas comienzan con la palabra clave `lambda` seguidas por el (los) argumento(s) de la función. Después de `:` se escribe lo que retorna la función.

sorted(planetas, key=long_str)

sorted(planetas, key=lambda s: len(s))

# `sum()`, ya se imaginarán que hace:

primos

# Ayuda en la función sum
help(sum)

# sum
sum(primos)

# En la clase pasada utilizamos las funciones `min()` y `max()` sobre varios argumentos.
#
# También le podemos pasar un solo argumento tipo lista.

# min
min(primos)

# max
max(primos)

# ___
# ## Pausa: Objetos
#
# Hasta ahora he venido utilizando la palabra **objeto** sin darle mucha importancia. ¿Qué significa en realidad?
#
# - si han visto algo de Python, pueden haber escuchado que todo en Python es un objeto.
#
# En la siguiente semana estudiaremos a nivel muy básico qué es la programación orientada a objetos.
#
# Por ahora, nos basta con saber que los objetos cargan varias "cosas" con ellos, y podemos acceder a estas "cosas" utilizando la "sintaxis punto (.)" de Python.
#
# Por ejemplo, los números en Python tienen una variable asociada llamada `imag`, la cual representa su parte imaginaria:

# Atributos real e imag
a = 7
a.imag, a.real

dir(a)

a.denominator, a.numerator

b = (6 + 5j) / 3
b.real, b.imag

dir(b.imag)

c = 5 / 3
c.as_integer_ratio()

7505999378950827 / 4503599627370496

from fractions import Fraction

Fraction(c).limit_denominator(10)

help(Fraction().limit_denominator)

# Entre las "cosas" que los objetos cargan, también pueden haber funciones. 
#
# Una función asociada a un objeto se llama **método**.
#
# Las "cosas" asociadas a los objetos, que no son funciones, son llamados **atributos** (ejemplo: imag).

# Método conjugate()
b.conjugate()

# Y si no sabemos qué hace un método determinado en un objeto, también podemos pasar métodos a la función `help()`, de la misma manera en que le pasamos funciones:

# help(objeto.metodo)
help(b.conjugate)

# Bueno, ¿y esto de que nos sirve?
#
# Pues las listas tienen una infinidad de métodos útiles que estaremos usando...
# ___

# ## 1.4 Métodos de las listas
#
# `list.append()` modifica una lista añadiéndole un elemento en el final:

planetas = ['Mercurio',
            'Venus',
            'Tierra',
            'Marte',
            'Jupiter',
            'Saturno',
            'Urano',
            'Neptuno']

# Plutón también es un planeta
variable = planetas.append("Pluton")

print(variable)

planetas

# ¿Porqué no obtuvumos una salida en la celda de arriba?
#
# Verifiquemos la documentación del método append:

help(planetas.append)

help(list.append)

help(append)

# **Comentario:** append es un método de todos los objetos tipo `list`, de manera que habríamos podido llamar `help(list.append)`. Sin embargo, si intentamos llamar `help(append)`, Python nos dirá que no existe nada con el nombre "append", pues `append` solo existe en el contexto de listas.

# `list.pop()` remueve y devuelve el último elemento de una lista:

# Que Plutón siempre no es un planeta
planetas.pop()

planetas

help(planetas.pop)

planetas.pop(1)

planetas

# ### 1.4.1 Buscando en listas
#
# ¿En qué lugar de los planetas se encuentra la Tierra? Podemos obtener su índice usando el método `list.index()`:

planetas = ['Mercurio',
            'Venus',
            'Tierra',
            'Marte',
            'Jupiter',
            'Saturno',
            'Urano',
            'Neptuno']

planetas

# índice del planeta tierra
planetas.index("Tierra")

planetas[2]

planetas[planetas.index('Tierra'):]

# Está en el tercer lugar (recordar que el indizado en Python comienza en cero)
#
# ¿En qué lugar está Plutón?

# índice del planeta plutón
planetas.index('Pluton')

# <font color=red> Error ... </font> ¡como debe ser!
#
# Para evitar este tipo de errores, existe el operador `in` para determinar si un elemento particular pertenece a una a una lista:

planetas

# ¿Es la Tierra un planeta?
'Tierra' in planetas

# ¿Es Plutón un planeta?
'Pluton' in planetas

# Usar esto para evitar el error de arriba
if 'Pluton' in planetas:
    planetas.index("Pluton")

# Hay otros métodos interesantes de las listas que no veremos. Si quieren aprende más acerca de todos los métodos y atributos de un objeto particular, podemos llamar la función `help()` sobre el objeto.
#
# Por ejemplo:

dir(list)

help(list)

primos

primos.extend([11, 13])

primos

# ## 1.5 Tuplas
#
# También son arreglos de objetos similares a las listas. Se diferencian en dos maneras:
#
# - La sintaxis para crear tuplas usa paréntesis (o nada) en vez de brackets:

t = (1, 2, 3)
t

# O equivalentemente
t = 1, 2, 3
t

t[1:]

# - Las tuplas, a diferencia de las listas, no pueden ser modificadas (son objetos inmutables):

# Intentar modificar una tupla
t[1] = 5

# Las tuplas son usadas comúnmente para funciones que devuelven más de un valor.
#
# Por ejemplo, el método `as_integer_ratio()` de los objetos `float`, devuelve el numerador y el denominador en la forma de una tupla:

# as_integer_ratio
0.25.as_integer_ratio()

num, den = 0.25.as_integer_ratio()

num

den

# Ayuda en el método float.as_integer_ratio
help(float.as_integer_ratio)

# También pueden ser usadas como un atajo:

a = (1, 2)
b = (0, 'A')
a, b = b, a
print(a, b)

# # 2. Ciclos o iteraciones
#
# ## 2.1 Ciclos `for`
#
# Las iteraciones son una manera de ejecutar cierto bloque de código repetidamente:

# Planetas, de nuevo
planetas = ['Mercurio', 'Venus', 'Tierra', 'Marte', 'Jupiter', 'Saturno', 'Urano', 'Neptuno']

# Imprimir todos los planetas en la misma línea
for planeta in planetas:
    print(planeta, end=', ')

# Para construir un ciclo `for`, se debe especificar:
#
# - el nombre de la variable que va a iterar (planeta),
#
# - el conjunto de valores sobre los que va a iterar la variable (planetas).
#
# Se usa la palabra `in`, en este caso, para hacerle entender a Python que *planeta* va a iterar sobre *planetas*.
#
# El objeto a la derecha de la palabra `in` puede ser cualquier objeto **iterable**. Básicamente, un iterable es cualquier arreglo (listas, tuplas, conjuntos, arreglos de numpy, series de pandas...).
#
# Por ejemplo, queremos hallar la multiplicación de todos los elementos de la siguiente tupla.

multiplicandos = (2, 2, 2, 3, 3, 5)

# +
# Multiplicación como ciclo
producto = 1
for number in multiplicandos:
    producto *= number
    
producto
# -

# Incluso, podemos iterar sobre los caracteres de un string:

s = 'steganograpHy is the practicE of conceaLing a file, message, image, or video within another fiLe, message, image, Or video.'
# Imprimir solo los caracteres en mayúscula, sin espacios, uno seguido de otro
for char in s:
    print(char if char.isupper() else '', end='')

# ### 2.1.1 Función `range()`
#
# La función `range()` es una función que devuelve una secuencia de números. Es extremadamente útil para escribir ciclos for.
#
# Por ejemplo, si queremos repetir una acción 5 veces:

# For de 5 iteraciones
for i in range(5):
    print('Hola, ¡Mundo!')

help(range)

range(4, 8)

list(range(4, 8)), list(range(4, 8, 2))


# **Ejercicio:**
#
# 1. Escribir una función que devuelva los primeros $n$ elementos de la sucesión de Fibonacci, usando un ciclo `for`.

def fibonacci_for(n):
    if n == 1:
        fibonacci = [0]
    elif n == 2:
        fibonacci = [0, 1]
    elif n >= 3:
        fibonacci = [0, 1]
        for i in range(n - 2):
            fibonacci.append(fibonacci[-2] + fibonacci[-1])
        
    return fibonacci


fibonacci_for(10)

# ## 2.2 Ciclos `while`
#
# Son otro tipo de ciclos en Python, los cuales iteran hasta que cierta condición deje de cumplirse.
#
# Por ejemplo:

i = 0
while i >= 0:
    print(i, end=' ')
    # i = i + 1 es equivalente a i += 1
    i += 1


# El argumento de un ciclo `while` se evalúa como una condición lógica, y el ciclo se ejecuta hasta que dicha condición sea **False**.

# **Ejercicio:**
#
# 1. Escribir una función que devuelva los primeros $n$ elementos de la sucesión de Fibonacci, usando un ciclo `while`.
#
# 2. Escribir una función que devuelva los elementos menores a cierto número $x$ de la sucesión de Fibonacci, usando un ciclo `while`.

def fibonacci_while(n):
    if n == 1:
        fibonacci = [0]
    elif n == 2:
        fibonacci = [0, 1]
    elif n >= 3:
        i = 2
        fibonacci = [0, 1]
        while i < n:
            fibonacci.append(fibonacci[-2] + fibonacci[-1])
            i += 1
        
    return fibonacci


fibonacci_while(10)



# ## Pausa: Recursión
#
# Una manera adicional de ejecutar iteraciones se conoce como *recursión*, y sucede cuando definimos una función en términos de sí misma.
#
# Por ejemplo, el $n$-ésimo número de la secuencia de Fibonacci, recursivamente sería:

def fibonacci_recursive(n):
    if n == 1:
        return 0
    elif n == 2:
        return 1
    else:
        return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)


fibonacci_recursive(10)

# ## 2.3 List comprehensions (no encuentro una traducción suficientemente buena de esto)
#
# List comprehension son una de las características más chidas de Python. La manera más fácil de entenderla, como muchas cosas, es revisando ejemplos:

# Primero, con ciclo for: listar los cuadrados de los 10 dígitos


# Ahora con una list comprehension


# Podemos agregar, incluso, condicionales:

planetas

# Ejemplo con los planetas
[planeta for planeta in planetas if len(planeta) < 7]

# Se puede usar para dar formato:

# str.upper()


# Es supremamente importante aprender esto, ya que es ampliamente utilizado y ayuda a reducir muchísimas líneas de código.
#
# Ejemplo: escribir la siguiente función usando un ciclo for.

def cuantos_negativos(iterable):
    """
    Devuelve el número de números negativos en el iterable dado.
    
    >>> cuantos_negativos([5, -1, -2, 0, 3])
    2
    """
    pass


cuantos_negativos([5, -1, -2, 0, 3])


# Ahora, con list comprehensions:

def cuantos_negativos(iterable):
    """
    Devuelve el número de números negativos en el iterable dado.
    
    >>> cuantos_negativos([5, -1, -2, 0, 3])
    2
    """
    pass


# Probar la función
cuantos_negativos([5, -1, -2, 0, 3])

# # 3. Strings y diccionarios
#
# ## 3.1 Strings
#
# Si hay algo en lo que Python es la ley es manipulando Strings. En esta sección veremos algunos de los métodos de los objetos tipo string, y operaciones de formateo (muy útiles en la limpieza de bases de datos, por cierto).

# ### 3.1.1 Sintaxis string
#
# Ya hemos visto varios ejemplos involucrando strings anteriormente. Solo para recordar:

x = 'Pluton es un planeta'
y = "Pluton es un planeta"
x == y

# Hay casos particulares para preferir una u otra:
#
# - Las comillas dobles son convenientes si tu string contiene un apóstrofe.
#
# - De manera similar, se puede crear fácilmente un string que contiene comillas dobles englobándolo en comillas simples.
#
# Ejemplos:

print("Pluto's a planet!")
print('My dog is named "Pluto"')

print('Pluto\'s a planet!')

print("My dog is named \"Pluto\"")

# ### 3.1.2 Los strings son iterables
#
# Los objetos tipo strings son cadenas de caracteres. Casi todo lo que vimos que le podíamos aplicar a una lista, se lo podemos aplicar a un string.

# string de ejemplo


# Indexado


# Indexado multiple


# ¿Cuántos caracteres tiene?


# También podemos iterar sobre ellos


# Sin embargo, una diferencia principal con las listas, es que son inmutables (no los podemos modificar).



# ### 3.1.3 Métodos de los strings
#
# Como las listas, los objetos tipo `str` tienen una gran cantidad de métodos útiles.
#
# Veamos algunos:

# string de ejemplo


# EN MAYÚSCULAS


# en minúsculas


# pregunta: comienza con?


# pregunta: termina con?


# #### Entre listas y strings: métodos `split()` y `join()`
#
# El método `str.split()` convierte un string en una lista de strings más pequeños.
#
# Esto es supremamente útil para tomar de un string cada una de sus palabras:

# Palabras de una frase






# O para obtener cierta información:



# Año, mes y día de una fecha especificada como string






# `str.join()` nos sirve para devolver los pasos. 
#
# Teniendo una lista de pequeños strings, la podemos convertir en un solo string usando el string sobre el que se llama como separador:

# Con la fecha...


# ### 3.1.4 Concatenación de strings
#
# Python nos permite concatenar strings con el operador `+`:

# Ejemplo


# Sin embargo, hay que tener cuidado:



# Concatenar un string con un número


# ¿cómo concatenar lo anterior?


# ## 3.2 Diccionarios
#
# Los diccionarios son otros objetos de Python que mapean llaves a elementos:

numeros = {'uno': 1, 'dos': 2, 'tres': 3}

# En este caso, los strings "uno", "dos", y "tres" son las llaves, y los números 1, 2 y 3 son sus valores correspondientes.
#
# Los valores son accesados con brackets, similarmente a las listas:

numeros['uno']

# Usamos una sintaxis similar para añadir otro par llave, valor

numeros['cuatro'] = 4

numeros

# O cambiar el valor asociado a una llave existente

numeros['uno'] = '1'

numeros

# ### Navegando entre listas, tuplas, diccionarios: `zip`

# Supongamos que tenemos dos listas que se corresponden:

key_list = ['name', 'age', 'height', 'weight', 'hair', 'eyes', 'has dog']
value_list = ['Esteban', 30, 1.81, 75, 'black', 'brown', True]

# ¿Cómo puedo asociar estos valores en un diccionario? Con `zip`:

# Primero, obtener la lista de pares


# Después obtener diccionario de relaciones


# Al ser los diccionarios iterables, puedo iterar sobre ellos (valga la redundancia)

# Iterar sobre diccionario


# Iterar sobre valores


# Iterar sobre pares llave-valor


# ___
# - Quiz 1 al comenzar la siguiente clase. Comprende clases 1 y 2.

# <script>
#   $(document).ready(function(){
#     $('div.prompt').hide();
#     $('div.back-to-top').hide();
#     $('nav#menubar').hide();
#     $('.breadcrumb').hide();
#     $('.hidden-print').hide();
#   });
# </script>
#
# <footer id="attribution" style="float:right; color:#808080; background:#fff;">
# Created with Jupyter by Esteban Jiménez Rodríguez.
# </footer>
