- Instructor: Facundo García Martoni
- Link al curso: Curso profesional de Python
Tenemos una clasificación de los lenguajes de programación:
- Compilado: En C++, por ejemplo, el código pasa directamente, mediante el compilador, a lenguaje binario. Se comunica directamente con la computadora a través de la compilación. 🤖
- Interpretados: Python es interpretado, por lo que no se pasa a código maquina, si no que se pasa a un estado intermedio en bytecode, que es un lenguaje de más bajo nivel que puede ser leído por un interprete, por una máquina virtual, que puede funcionar en diferentes SO. 🧠
Algunas preguntas frecuentes:
- Los lenguajes interpretados son normalmente más lentos. "C cuando se debe, Python cuando se puede". 👀 Pero no es realmente importante en la mayoría de las aplicaciones.
- El garbage collector es una sección especial de Python, el cual se encarga de tomar los objetos y variables que no están en uso y las elimina. 🧻
- La carpeta
__pycache__
contiene el bytecode que es leído por la maquina virtual, y funciona como una especie de recuperación.
Es importante comprender dos conceptos:
- Módulo: Es cualquier archivo de Python. Generalmente, contiene código que puedes reutilizar 😃.
- Paquete: Un paquete es una carpeta que contiene un conjunto de módulos. Siempre posee el archivo
__init__.py
(dander init.py). Estos módulos están relacionados.
Un orden común de proyectos:
- exploracion_espacial_proyecto.
- README
- .gitignore
- venv
- exploración espacial
- __init__.py
- nave.py
- destino.py
- tests.py
Sin embargo, en cada proyecto puede ser diferente dependiendo de lo que se esté haciendo, ya que, por ejemplo, podrías usar algún *framework.*🤯
Tenemos tipos de datos primitivos (arreglos, números, strings, booleanos, etc). La manera en que un lenguaje trata a los tipos es el tipado. Tenemos 4 clasificaciones:
- Estático: Son los que levantan los errores de tipo en tiempo de compilación. Esto es, si al estar programando tenemos un error de tipo, entonces el lenguaje nos avisa antes de que se ejecute (mientras compila). 😯 (C/C++)
- Dinámico: Opuesto al estático, levantan los errores de tipo en el tiempo de compilación, es decir, el error sale mientras el programa se ejecuta en esa línea donde está el error. 🛑 (Python)
- Fuerte: Son los que tratan con mas severidad a los datos de diferente tipo. 😠 (Python)
- Débil: Son mas relajados con datos de diferente tipo. 🦜 (Javascript)
-
Podemos convertir a Python a un lenguaje de tipado estático 🤯. Para eso, usamos Static Typing, y es muy sencillo, solo debemos añadir una sintaxis adicional, en la cual declaramos variables con su tipo.
a: int = 5 b: str = 'Hola' c: bool = True print(a, b, c)
Esto funciona desde Python 3.6 👀. Para hacerlo con funciones (con sus variables y con lo que retorna la función):
def suma(a: int, b: int) -> int: return a + b print(suma(1, 2))
Si le pasamos strings a la función
suma
, si funcionará y nos va a regresar las strings sumadas, para evitar esto, debemos añadir un módulo. -
Podemos hacer tipado en estructuras de datos. Para definir que una variable es de un tipo (de una estructura de datos), desde la versión 3.9, podemos hacerlo con las palabras claves de ese tipo. Antes de la versión 3.9, es con:
from typing import Dict, List positives: List[int] = [1, 2, 3, 4, 5] users: Dict[str, int] = { 'argentina': 1, 'mexico': 34, 'colombia': 45 } countries: List[Dict[str, str]] = [ { 'name':'Argentina' 'people':'45000' }, { 'name':'México' 'people':'900000' }, { 'name':'Colombia' 'people':'9999999' } ]
-
Podemos hacer lo mismo con las tuplas:
from typing import Tuple numbers: Tuple[int, float, int] = (1, 0.5, 1)
-
Podemos definir nuestros propios tipos de variable y usarlas:
from typing import Tuple, Dict, List CoordinateType = List[Dict[str, Tuple[int, int]]] coordinates: CoordinateType = [ { 'coord1': (1, 2), 'coord2': (3, 5) }, { 'coord1': (0, 1), 'coord2': (2, 5) } ]
-
Para que Python considere este tipado estático y no se salte, es con el módulo
mypy
que nos detiene cuando hay errores de tipado 👀.
-
Para checar que el código respeta el tipado que especificamos, debemos correrlo con
mypy <nombre>.py --check-untyped-defs
👀. El módulo mypy nos arroja errores de tipado, por ejemplo:palindrome.py:9: error: Argument 1 to "is_palindrome" has incompatible type "int"; expected "str" Found 1 error in 1 file (checked 1 source file)
-
Solución al reto:
def is_prime(number: int) -> bool: for i in range(2, number): if number % i == 0: return False return True def main(): # print(is_prime('Hola')) print(is_prime(5)) if __name__=='__main__': main()
-
Una variable solo está disponible dentro de la región donde fue creada 🧠.
-
Local scope: Región que se corresponde al ámbito de una función:
# Local scope def my_func(): y = 5 print(y) my_func() # 5
En este caso, la variable
y
no puede ser leída fuera de la función. -
Global scope: Variables que tienen alcance en todo nuestro programa. 🌎
# Global scope y = 5 def my_func(): print(y) def my_other_func(): print(y) my_func() my_other_func() # 5 # 5
-
Podemos tener dos variables que se llaman igual, pero dado a que una es global y otra es local, son objetos diferentes. En la función, se le da prioridad a la variable más local. 🤔
-
Nested functions: Las funciones anidadas son simplemente funciones creadas dentro de otra función. Podemos hacer return de una función creada dentro de otra función 😵 y luego guardar esas funciones en variables que podemos utilizar.
def main(): a = 1 def nested(): print(a) return nested my_func = main() my_func() # 1
-
Eso anterior es un closure 🤯 y es básicamente cuando una variable de un scope superior es recordada por una función de scope inferior (aunque luego se elimine la de scope superior).
def main(): a = 1 def nested(): print(a) return nested my_func = main() my_func() # 1 del(main) my_func() # 1
-
Reglas para encontrar un closure: 🔥
- Debemos tener una nested function.
- La nested function debe referenciar un valor de un scope superior.
- La función que envuelve a la nested function debe retornarla también.
-
Ejemplo de closures para crear funciones:
def make_multiplier(x): def multiplier(n): return x*n return multiplier times10 = make_multiplier(10) times4 = make_multiplier(4) print(times10(3)) # 30 print(times4(5)) #20 print(times10(times4(2))) # 80
-
Los closure aparecen en dos casos particulares: cuando tenemos una clase corta (con un solo método), los usamos para que sean elegantes. El segundo caso, es cuando usamos decoradores 👀
-
Solución al reto:
def make_division_by(n: float): assert type(n) == float or type(n) == int, "Debes ingresar un número" def divisor(x: float) -> float: assert type(x) == float or type(x) == int, "Debes ingresar un número" return x/n return divisor def main(): division_3 = make_division_by(3) print(division_3(18)) division_5 = make_division_by(5) print(division_5(100)) division_18 = make_division_by(18) print(division_18(54)) if __name__=='__main__': main()
-
Es el concepto más avanzado de funciones 😵. Un decorador es un closure especial, con una función adicional.
-
Un decorador es una función que recibe como parámetro otra función, le añade cosas, y retorna una función diferente. Le da superpoderes a una función 🦸♀️.
def decorador(func): def envoltura(): print('Esto se añade a mi función original') return envoltura def saludo(): print('Hola!') saludo() # Hola! saludo = decorador(saludo) saludo() # Esto se añade a mi función original. Hola!
-
Esto, a ser un patrón muy común, hay una manera mas pythonica de hacerlo, con azúcar sintáctica. Esto es cuando tenemos un código que está embellecido para que sea más fácil de entender. (Sugar sintax). Sería algo así:
def decorador(func): def envoltura(): print('Esto se añade a mi función original') return envoltura @decorador def saludo(): print('Hola!') saludo() # Esto se añade a mi función original. Hola!
-
Otro ejemplo:
def mayusculas(func): def envoltura(texto): return func(texto).upper() return envoltura @mayusculas def mensaje(nombre): return f'{nombre} recibiste un mensaje' print(mensaje('Cesar'))
-
Un iterator es una estructura de datos para guardar datos infinitos 🤯. Para entenderlo, primero debemos saber que un iterable es todo aquel objeto que puedo recorrer en un ciclo (lista, strings, etc). Un iterable es divisible.
-
Cuando hacemos un ciclo, Python internamente no está recorriendo a ese iterable, si no más bien ese iterable se convierte internamente en un iterador, que si puede ser recorrido. 🤔
-
Para crear un iterador:
# Creando un iterador my_list = [1, 2, 3, 4, 5] my_iter = iter(my_list) # Se recibe un iterable # Iterando un iterador print(next(my_iter)) # Cuando no quedan datos, la excepción StopIteration es elevada
-
Para que no se rompa el código, hacemos manejo de errores: 🧠
# Creando un iterador my_list = [1, 2, 3, 4, 5] my_iter = iter(my_list) # Iterando un iterador while True: try: element = next(my_iter) print(element) except StopIteration: break
Esta es una manera eficiente de extraer todos los elementos de un iterable. De hecho, esta es la manera en la que funciona un ciclo for, es la azúcar sintaxis de este código anterior 😍. El ciclo for en si mismo no existe.
-
¿Cómo construyo un iterador?. Una opción es castear desde un iterable. Para hacerlo desde cero, debemos usar el protocolo de los iteradores que contiene dos clases importantes
__iter__
y__next__
:class EvenNumbers: """Calse que implementa un iterador de todos los números pares, o los números pares hasta un máximo""" def __init__(self, max = None): self.max = max def __iter__(self): self.num = 0 return self def __next__(self): if not self.max or self.num <= self.max: result = self.num self.num += 2 return self else: raise StopIteration
-
Las ventajas de usar iteradores: Nos ahorra recursos computacionales y de memoria, ya que tenemos una función matemática de como obtener los siguientes elementos, sin necesidad de guardarlos todos 👀.
-
Una sucesión matemática famosa es la sucesión de Fibonacci 🧠. Se define cómo:
$$x_0 = 0 \ x_1 =1 \ x_i = x_{i-1} + x_{i-2} \ \ \ \ \ \ i>2$$ -
Solución al reto:
import time class FiboIter(): def __init__(self, max = None): self.max = max def __iter__(self): self.n1 = 0 self.n2 = 1 self.counter = 0 return self def __next__(self): if not self.max or self.counter < self.max: if self.counter == 0: self.counter += 1 return self.n1 elif self.counter == 1: self.counter += 1 return self.n2 else: n1 = self.n2 self.n1, self.n2 = n1, self.n1 + self.n2 self.counter += 1 return self.n2 else: raise StopIteration if __name__=='__main__': fibonacci = FiboIter(max = 10) for element in fibonacci: print(element) time.sleep(0.5)
-
Los iteradores tienen sugar syntax 💖. Son llamados generadores, los cuales son funciones que guardan un estado 🤔.
-
Los generadores son funciones: 🔥
def my_gen(): """Un ejemplo de generadores""" print("Hello world!") n = 0 yield n print("Hello heaven!") n = 1 yield n print("Hello hell!") n = 2 yield n # Lo instanciamos a = my_gen() print(next(a)) # Hello world! print(next(a)) # Hello heaven! print(next(a)) # Hello hell! print(next(a)) # StopIteration
-
yield
este keyword es análoga alreturn
, pero no termina la función, le pone pausa. La siguiente ejecución, corre después de este punto 👀. -
Generator expression. 🤯 Se trae un elemento a la vez, y no ocupa toda la memoria 😯.
my_list = [0, 1, 2, 3, 4, 5, 6] my_second_list = [x*2 for x in my_list] # List comprehension my_second_gen = (x*2 for x in my_list) # Generator expression
-
La ventaja de los generadores es que es más fácil de escribir que un iterador, y tiene las mismas ventajas que este último. 💕
-
Solución al reto:
import time def fibo_gen(max = None): n1, n2 = 0, 1 counter = 0 while not max or counter <= max: if counter == 0: counter += 1 yield n1 elif counter == 1: counter += 1 yield n2 else: aux = n1 + n2 n1, n2 = n2, aux counter += 1 yield n2 if __name__ == '__main__': fibonacci = fibo_gen(10) for element in fibonacci: print(element) time.sleep(0.5)
-
Los sets, o conjuntos, son una colección desordenada de elementos únicos e inmutables. ✨
-
Tuplas, strings, números, True, False, etc son sets. 🤯
-
Para crear un set:
my_set = {3, 4, 5} print(my_set) # {3, 4, 5} my_set2 = {"Hola", 23.3, False, True} print(my_set2) # {False, True, "Hola", 23.3} my_set3 = {3, 3, 2} print(my_set3) # {3, 2} my_set4 = {[1, 2, 3], 4} # ERROR!!! empty_set = {} # Esto es dict por defecto print(type(empty_set)) # <class 'dict'> empty_set = set() print(type(empty_set)) # <class 'set'>
-
Para hacer casting a sets usamos
set()
. Esto elimina los repetidos de una lista o tupla y los hace set. -
Para añadir elementos a un set, usamos
my_set.add(<elemento inmutable>)
. Para añadir varios elementos, hacemosmy_set.update(<lista>, <tupla>)
. -
Para borrar elementos de un set, usamos
my_set.remove(<elemento>)
pero debemos estar seguro de que el set contiene ese elemento. Si no sabemos si existe el elementomy_set.discard(<elemento?>)
. 👀 -
Para borrar un elemento aleatorio, usamos
my_set.pop()
. Para borrar todo usamosmy_set.clear()
.
Podemos operaro con set:
- Unión: La unión de dos conjuntos es el resultado de combinar todos los elementos, sin repetir👀. Para hacer esto, usamos el pipe operator
my_set3 = my_set1 | my_set2
. - Intersección: Nos quedamos solamente con los elementos que ambos sets tienen en común. Para hacer esto, hacemos
my_set3 = my_set1 & my_set2
. 🤯 - Diferencia: Tomar dos set, y de uno quitar todos los elementos que contiene el otro. Para hacer esto, hacemos
my_set3 = my_set1 - my_set2
. Es importante notar quemy_set1 - my_set2 != my_set2 - my_set1
. - Diferencia simétrica: Es lo contrario a la intersección. Nos quedamos con los elementos que no se comparten, esto es hace cómo
my_set3 = my_set1 ^ my_set2
.
También podemos usar los métodos para que sea más explicito, que son:
set1.union(set2)
set1.symmetric_difference(set2)
set1.difference(set2)
set1.intersection(set2)
Ejemplo con sets:
def main():
set1 = {1, 2, 3.5, True, "Juan", "María"}
set2 = {0, 2, 3.1, False, "Juan", "Missael"}
print(f"set1 = {set1}")
print(f"set2 = {set2}")
# union
print(f"set1 + set2 = {set1 | set2}")
# difference
print(f"set2 - set1 = {set2 - set1}")
print(f"set1 - set2 = {set1.difference(set2)}")
# symmetric difference
print(f"set1 ^ set2 = {set1 ^ set2}")
# intersection
print(f"set1 & set2 = {set1 & set2}")
if __name__=='__main__':
main()
El output:
set1 = {1, 2, 3.5, 'María', 'Juan'}
set2 = {0, 2, 3.1, 'Missael', 'Juan'}
set1 + set2 = {0, 1, 2, 3.5, 3.1, 'Missael', 'María', 'Juan'}
set2 - set1 = {0, 'Missael', 3.1}
set1 - set2 = {1, 'María', 3.5}
set1 ^ set2 = {0, 1, 3.1, 'Missael', 3.5, 'María'}
set1 & set2 = {2, 'Juan'}
-
Cuando hagamos aplicaciones o proyectos, es importante conocer como manejar fechas en Python. Podemos hacerlo con Python con el módulo
datetime
que viene ya en el core de Python. -
Ejemplo sencillo 👀
import datetime # Tiempo en días, horas y segundos my_time = datetime.datetime.now() print(my_time) # Siguiendo el estandar utc_time = datetime.datetime.utcnow() print(utc_time) # Para solo la fecha actual my_day = datetime.date.today()
-
Podemos acceder también al día, año y mes con
my_day.year
,my_day.month
, ymy_day.day
-
Debemos considerar el formateo de fechas, ya que no en todas las partes del mundo se escribe de la misma manera. Para especificar el formato de la fecha, tenemos la tabla de código de formato.
-
Ejemplo de formato:
from datetime import datetime my_datetime = datetime.now() print(my_datetime) my_str = my_datetime.strftime('Fecha : %d/%m/%Y') my_str2 = my_datetime.strftime('Estamos en el año %Y')
-
Tabla de formato completa:
datetime - Basic date and time types - Python 3.9.7 documentation
- En cada país, la hora es diferente. Para trabajar distintas zonas horarias, debemos usar un módulo especial
pytz
que debemos instalar porque no viene instalada.