<jupyter_start><jupyter_text>Repaso de Python 3Este curso asumo una competencia intermedia/avanzada de Python. Esta lección autocontenida es sólo un refresco de memoría de las cosas más importantes que Python ofreceEl zen de Python:<jupyter_code>import this<jupyter_output><empty_output><jupyter_text>Dynamic typingNo es necesario declarar el tipo de una variable, Python lo hace automaticamente<jupyter_code>x = 1
type(x)
x = 1.
type(x)
x = '1'
type(x)<jupyter_output><empty_output><jupyter_text>FuncionesArgumentos posicionales (mandatorios), keywords, lista de argumentos y lista con nombre especificado<jupyter_code>def foo(x,  # Este es un positional argument
        y=100,  # Este es un keyword argument
        *args, # Esta es una lista de argumentos
        **kwargs): # Este es un diccionario de argumentos
    print(x)
    print(y)
    print(args)
    print(kwargs)
    return x


foo(10, 20, 'asd', 21, 54, bar=12)
def foo(x, y=None):
    y = 1. if y is None else y
    return x + y

print(foo(1))
print(foo(1, 2))<jupyter_output><empty_output><jupyter_text>ClasesEjemplo de herencia de clases en Python<jupyter_code>class Fruta:
    def __init__(self, nombre, color):
        self.nombre = nombre # Atributos públicos
        self.color = color
        self.__sabor =  'asd' # Este es un atributo privado
        
class Manzana(Fruta):
    def __init__(self):
        super().__init__("Manzana", "Rojo") # Llamamos al constructor de Fruta con super
    
mi_manzana = Manzana()
print(mi_manzana.color)
#mi_manzana.__atributo_privado<jupyter_output><empty_output><jupyter_text>ImprimiendoEn Python 3 `print` es una función, podemos imprimir texto formateado como<jupyter_code>nombre = 'pablo'
apellido = 'huijse'
edad = 33
peso = 80.2

print("%s %s\t edad: %d peso: %0.4f" %(nombre, apellido, edad, peso))
print("{1} {0}\t edad: {2} peso: {3:0.4f}".format(apellido, nombre, edad, peso))
# Mi favorito: f-strings:
print(f"{nombre} {apellido}\t edad: {edad} peso: {peso:0.4f}")
# Demostración del argumento sep de print
print(nombre, apellido, edad, peso, sep=' ')<jupyter_output><empty_output><jupyter_text>Manejadores de contextoUsando la palabra clave `with` no es necesario preocuparse de cerrar el archivo file.txt<jupyter_code>!echo "asdasdasdasd" > file.txt

with open('file.txt') as f:
    contents = f.read()

print(contents)<jupyter_output><empty_output><jupyter_text>DecoradoresFunciones o clases que pueden modificar el funcionamiento de otra función<jupyter_code>def decorator(func):    
    def new_func(x):
        return func(x) + 10
    return new_func

@decorator
def foo(x):
    return x +1

foo(10)<jupyter_output><empty_output><jupyter_text>Listas, tuplas y rangosSon tipos de datos secuenciales, es decir pueden ser iterados<jupyter_code>lista_vacia = [] # Creación de una lista vacía
print(lista_vacia.append(1)) # Agregando un elemento
lista_vacia.append('hola')
print(lista_vacia) # Tiene dos elementos
print(lista_vacia.pop()) # Eliminando el primer elemento
print(lista_vacia) # Ahora tiene uno
lista_vacia[0] = 'chao' # Modificando el primer elemento
print(lista_vacia)
una_lista = ['a', 'b', 'c', 1, 2, 3, 2.4, 'asd']

for elemento in una_lista:
    print(elemento, end=' ')<jupyter_output><empty_output><jupyter_text>"Desempacando" (unpacking) una lista<jupyter_code>primero, *medio, ultimo = una_lista
print(primero)
print(medio)
print(ultimo)
primero, ultimo = ultimo, primero
print(primero, ultimo)<jupyter_output><empty_output><jupyter_text>Imprimiendo una lista completa<jupyter_code>print(una_lista)
print(*una_lista)
print(*una_lista, sep='-', end=' fin!')<jupyter_output><empty_output><jupyter_text>Obteniendo el largo de una lista<jupyter_code>len(una_lista)<jupyter_output><empty_output><jupyter_text>Tomando slices (trozos) de una lista<jupyter_code>print(una_lista[0])
print(una_lista[1:4])
print(una_lista[-1])
print(una_lista[::2])
print(una_lista[::-1])<jupyter_output><empty_output><jupyter_text>Las tuplas son similares a las listas, en el sentido de que sus elementos pueden tener tipos distintos<jupyter_code>tupla = (0, 10, 51243, 'asd')
print(tupla)
print(tupla[0])
for elemento in tupla:
    print(elemento, end=' ')<jupyter_output><empty_output><jupyter_text>Pero a diferencia de las listas son inmutables, es decir no se pueden modificar<jupyter_code>tupla[0] = 'hola'<jupyter_output><empty_output><jupyter_text>Podemos usar `range` para crear un iterador de números enteros<jupyter_code>for element in range(0, 20, 2):
    print(element, end=' ')<jupyter_output><empty_output><jupyter_text>Adicionalmente podemos usar `enumerate`, para crear un índice entero a partir de una lista<jupyter_code>for i in range(len(una_lista)):
    print("{0}, {1}".format(i, una_lista[i]))

for i, element in enumerate(una_lista):
    print("{0}, {1}".format(i, element))<jupyter_output><empty_output><jupyter_text>Comprensiones de listas (list comprehensions)Son una forma consisa de crear listas<jupyter_code>[x for x in range(20)]
texto_en_minuscula = ['Un', 'día', 'vi', 'una', 'vaca', 'vestida', 'de', 'uniforme']

texto_en_mayuscula = []
for palabra in texto_en_minuscula:
    texto_en_mayuscula.append(palabra.upper())
print(texto_en_mayuscula)

print([palabra.upper() for palabra in texto_en_minuscula])<jupyter_output><empty_output><jupyter_text>Se puede hacer una doble iteración<jupyter_code>print([(x, y) for x in range(5) for y in range(5)])<jupyter_output><empty_output><jupyter_text>También se pueden aplicar condicionales en el iterador y/o en los valores<jupyter_code># condicional en el iterador
print([x for x in range(10) if x % 2 == 0])
# conditional en el valor
print([x**2 if x < 5 else x for x in range(10)])<jupyter_output><empty_output><jupyter_text>Puede usarse `zip` parar iterar sobre más de una lista<jupyter_code>lista1 = [x for x in range(20)]
lista2 = [10]*20
lista3 = texto_en_minuscula

for elemento1, elemento2, elemento3 in zip(lista1, lista2, lista3):
    print(elemento1, elemento2, elemento3, sep=', ')<jupyter_output><empty_output><jupyter_text>SetsLos `set` son una colección desordenada de objetos sin duplicadosEs posible iterar en un set, agregar/remover elementos y aplicar operaciones lógicas entre sets (intersección, union, diferencia)Tiene complejidad O(1) de búsqueda (muy eficientes). Se deben preferir antes que las listas cuando- La colección es de gran tamaño- No hay elementos repetidos- Se realizarán múltiples búsquedas en la colección<jupyter_code>with open("zen.txt") as archivo:
    zen = [palabra.strip(".,-*!").lower() for linea in archivo for palabra in linea.split()]

print(zen)<jupyter_output><empty_output><jupyter_text>Para ver si un elemento es parte de una lista, set, diccionario usamos `in`Comparemos el tiempo que demora esta operación en una lista y en un set<jupyter_code>s = set(zen) # Esto construye un set a partir de una lista

%timeit -n20 'Although' in s

%timeit -n20 'Although' in zen<jupyter_output><empty_output><jupyter_text>DiccionarioEs una secuencia indexada por llaves (*keys*). - Al igual que el set tienen complejidad de búsqueda O(1)- A diferencia del set se puede buscar un elemento usando su llave<jupyter_code>d = {'nombre': 'pablo', 'apellido': 'huijse', 'edad': 33, 'peso': 80.1}

# Leyendo el valor asociado a una llave
print(d['apellido'])
# Buscando si un elemento existe
print('edad' in d)
# Retorna las llaves:
print(list(d))
# Se puede iterar en llaves y valores
for llave, valor in d.items():
    print(llave, valor, sep=': ')<jupyter_output><empty_output><jupyter_text>IteradoresPodemos usar `iter` para crear un iterador a partir de un objeto iterable (lista, tupla, rango, string, diccionario)El iterador se evalua con `next` para escupir el próximo elemento, el cual sale del iterador (lazy, single-use)Son ventajosos en términos de uso de memoría (la lista completa no se mapea en memoria)<jupyter_code>iterador = iter(texto_en_minuscula)

print(next(iterador))
print(next(iterador))
print(list(iterador))
# El iterador arroja una excepción StopIteration cuando se termina
print(next(iterador))
iterador = iter(texto_en_minuscula)
for elemento in iterador:
    print(elemento)

# El iterador queda vacio luego de usarse    
print(list(iterador))<jupyter_output><empty_output><jupyter_text>Escribiendo un iterador<jupyter_code>class Logrange:
    
    def __init__(self, start=-6, end=6):
        self.num = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        a = self.num
        if a > self.end:
            raise StopIteration
        self.num += 1
        return 10**a
        
for elemento in Logrange():
    print(elemento, end=' ')<jupyter_output><empty_output><jupyter_text>Generadores (generator function)Funciones que retornan un iteradorSe usa el keyword reservado `yield`<jupyter_code>def gen():
    for i in range(10):
        yield i**2
        
for x in gen():
    print(x, end=' ')
#print(*gen())<jupyter_output><empty_output><jupyter_text>Expresión generadora (generator expression)- Se construye como una comprensión de lista pero usando () en vez de []- Produce una "receta" en lugar de una lista- Se consume una vez y muere<jupyter_code>gen = (palabra for palabra in texto_en_minuscula)
print(gen)
print(next(gen))
print(next(gen))
print(next(gen))
for palabra in gen:
    print(palabra, end=' ')<jupyter_output><empty_output><jupyter_text>[Contenedores especializados](https://docs.python.org/3.7/library/collections.html)Por ejemplo el contenedor Counter permite hacer histogramas<jupyter_code>from collections import Counter

Counter(zen)<jupyter_output><empty_output><jupyter_text>Manejo de excepcionesBloque `try` en Python<jupyter_code>def foo(x):
    try:
        return x/10
    except TypeError: 
        print("Esta excepcion se captura")    
    else:
        print("Estas excepciones se propagan")
    finally:
        print("Esto corre al final de cualquier camino (cleanup)")
        
foo('asd')<jupyter_output><empty_output><jupyter_text>Levantar excepciones con `raise` y `assert`<jupyter_code>raise TypeError("Algo no está bien aquí")
def foo(x):
    assert type(x) == int, "El argumento no es un entero"
    return x + 1

foo('a')<jupyter_output><empty_output><jupyter_text>Expresión lambdaSon funciones de una linea con la estructura        foo = lambda argumentos: expresión    Una lambda puede tener zero o más argumentos y siempre solo **una** expresión En general se usan para definir funciones anónimas, funciones que se ocupan sólo una vez en el código`lambda` + `map` = comprensión de lista<jupyter_code>foo = lambda x, y : x+1/y

foo(1, 2)
list(map(lambda x : x**2, range(10)))
list(filter(lambda x : x % 2 == 0, range(10)))
parejas = [(1, 'uno'), (2, 'dos'), (3, 'tres'), (4, 'cuatro')]
#pairs.sort(key=lambda pair: pair[1])
print(sorted(parejas, key=lambda p: p[0]))
print(sorted(parejas, key=lambda p: p[1]))
print(sorted(parejas, key=lambda p: len(p[1])))<jupyter_output><empty_output><jupyter_text>Debugging con ipdbDos maneras para encontrar *bugs* con IPython usando el debugger `ipdb` Necesitas tener instalado `ipdb`  **1) Debugeo Paso-a-paso:** Insertar breakpoints manualmente<jupyter_code>def foo(x, y):
    import ipdb; ipdb.set_trace(context=10) # Esto inserta un breakpoint en la función
    z = x/2.
    z += 2/y
    return z<jupyter_output><empty_output><jupyter_text>Si ejecutamos `foo(1, 0)` veremos algo como lo siguiente  Comandos en ipdb- a: Muestra los valores de los argumentos- l: Muestra la linea de código en que estamos posicionados- u/d: Sube y baja en el stack- q: Salir del modo debug **Debugeo Post-mortem:** Entra a modo debug con ipdb cuando ocurre una excepción<jupyter_code># Esto activa el modo post-mortem
%pdb on 
def foo(x, y):
    z = x/2.
    z += 2/y    
    return z<jupyter_output><empty_output><jupyter_text>Si ejecutamos `foo(1, 0)` veremos algo como:   [pathlib](https://docs.python.org/3/library/pathlib.html)Modulo regular de Python3 para leer y manipular directorios<jupyter_code>from pathlib import Path
p = Path('.')
print(sorted(p.glob('*.py')))
print([x for x in p.iterdir() if x.is_dir()])
print([x for x in p.iterdir() if x.is_file() and x])
p = Path('/usr/bin/python3')
print(p.parts)<jupyter_output><empty_output><jupyter_text>[Lista de módulos de Python 3](https://docs.python.org/3/py-modindex.html)Siempre antes de implementar un módulo hay que revisar el link de arribaAprovecha la extensa lista de módulos estándar de Python!  Resumen de buenas prácticas- Prefiere variables keyword antes que posicionales (auto-documentación)- Siempre comenta con docstring cada función o clase- Prefiere los tipos nativos de Python- Trata de seguir el [PEP8](https://www.python.org/dev/peps/pep-0008/)[Python Enhancement Proposal (PEP)](https://www.python.org/dev/peps/pep-0008/) es una especificación técnica que busca mejorar el lenguaje PythonPEP 0 es el índice de todos los PEP existentes. > El PEP 8 es una guia de buenas prácticas para dar formato a nuestros códigos en PythonEl objetivo de PEP 8 es mantener un estándar que facilite la lectura de código escrito en Python<jupyter_code>def saludo(nombre: "Esto debería ser un string"=None):
    """Esta es una función que saluda a quien la llama
    Args:
        nombre: un string
    Returns:
        None
    """
    if nombre is None:
        print("Hola, ¿Cómo te llamas?")
    else:
        print("Hola {0}".format(nombre))

saludo("Pablo")
help(saludo)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11158160612138772, 'fraction_numerical': 0.012926231334967684, 'mean_word_length': 5.296538821328344, 'pattern_counts': {'":': 0, '<': 142, '<?xml version=': 0, '>': 144, 'https://': 5, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '35', 'dir': 'jupyter-structured-clean-dedup', 'id': '129597', 'license': 'Unlicense', 'n_tokens_mistral': 5043, 'n_tokens_neox': 4974, 'n_words': 1387, 'path': 'clases/unidad1/00_repaso_python3.ipynb', 'repo_name': 'magister-informatica-uach/INFO147', 'size': '13461'}