[TOC]
-
# Crear definir una Clase class Hotel: pass # Crear una instancia hotel = Hotel()
Todas las clases crean objetos y todos los objetos tienen atributos. Se utiliza el método especial
__init__
para definir el estado inicial de la instancia. Recibe como primer parámetro obligatorioself
(que es simplemente una referencia a la instancia).class Hotel: def __init__(self, numero_maximo_de_huespedes, lugares_de_estacionamiento): self.numero_maximo_de_huespedes = numero_maximo_de_huespedes self.lugares_de_estacionamiento = lugares_de_estacionamiento self.huespedes = 0 hotel = Hotel(numero_maximo_de_huespedes=50, lugares_de_estacionamiento=20) print(hotel.lugares_de_estacionamiento) # 20
Todos reciben
self
como primer argumento.class Hotel: ... def anadir_huespedes(self, cantidad_de_huespedes): self.huespedes += cantidad_de_huespedes def checkout(self, cantidad_de_huespedes): self.huespedes -= cantidad_de_huespedes def ocupacion_total(self): return self.huespedes hotel = Hotel(50, 20) hotel.anadir_huespedes(3) hotel.checkout(1) hotel.ocupacion_total() # 2
-
Son los tipos de datos creados por el desarrollador.
Un objeto tiene las siguientes formas de interactuar con el:
- Creación
- Manipulación
- Destrucción. En algunos lenguajes esto se tiene que hacer manualmente en el caso de Python esto se hace automáticamente cuando cuando un objeto no es usado por ningún elemento del programa.
El uso de objetos tiene las siguientes ventajas:
- De-composición: estructurar objetos más pequeños a partir de un objeto principal.
- Abstracciones
- Encapsulación
🛈 Nota: en Python
__init__
es el método constructor.class Person: def __init__(self, name, age): self.name = name self.name = age # Implementación
En Python no existen el keyword
private
por lo que se usa una convención para definir atributos privados, usar un_
al principio del nombre del atributo/métodoisinstance(obj, ClassName)
→ Comprobar si un objeto es instancia de determinada clase. -
Consiste en dividir un problema en problemas más pequeños.
-
Enfocarse en la información relevante, separando la información central de los detalles secundarios.
-
Son formas sencillas de llamar funciones de orden mayor, es decir, funciones que toman otra función cómo parámetro y/o retornan otra función como resultado. De esta forma un decorador añade capacidades a una función sin modificarla. Ejemplo:
def presentarse(nombre): return f"Me llamo {nombre}" def estudiemos_juntos(nombre): return f"¡Hey {nombre}, aprendamos Python!" def consume_funciones(funcion_entrante): return funcion_entrante("David") # >>> consume_funciones(presentarse) # 'Me llamo David' # >>> consume_funciones(estudiemos_juntos) # '¡Hey David, aprendamos Python!'
def funcion_mayor(): print("Esta es una función mayor y su mensaje de salida.")
def librerias(): print("Algunas librerías de Python son: Scikit-learn, NumPy y TensorFlow.") def frameworks(): print("Algunos frameworks de Python son: Django, Dash y Flask.") frameworks() librerias() # >>> funcion_mayor() # Esta es una función mayor y su mensaje de salida. # Algunos frameworks de Python son: Django, Dash y Flask. # Algunas librerías de Python son: Scikit-learn, NumPy y TensorFlow.
Las funciones anidadas dentro de
funcion_mayor
no se ejecutan sino hasta que se llama esta primera, siendo muestra del scope o alcance de las funciones y si se llaman se obtiene un error. -
Los decoradores se indican usando la sintaxis
@nombre_decorador
. Ejemplo:@funcion_decoradora def zumbido(): print("Buzzzzzz")
Este código es equivalente a escribir
zumbido = funcion_decoradora(zumbido)
.Se usan para acceder y especificar el valor de una variable privada, así como para añadir código de validación al momento de definir y obtener un valor.
class Millas: def __init__(self, distancia = 0): self.distancia = distancia def convertir_a_kilometros(self): return (self.distancia * 1.609344) # Creamos un nuevo objeto # avion = Millas() # Indicamos la distancia # avion.distancia = 200 # Obtenemos el atributo distancia # >>> print(avion.distancia) # 200 # Obtenemos el método convertir_a_kilometros # >>> print(avion.convertir_a_kilometros()) # 321.8688
class Millas: def __init__(self, distancia = 0): self.distancia = distancia def convertir_a_kilometros(self): return (self.distancia * 1.609344) # Método getter def obtener_distancia(self): return self._distancia # Método setter def definir_distancia(self, valor): if valor < 0: raise ValueError("No es posible convertir distancias menores a 0.") self._distancia = valor
El método getter obtendrá el valor de la distancia que y el método setter se encargará de añadir una restricción. También debemos notar cómo
distancia
fue reemplazado por_distancia
, denotando que es una variable privada.Si probamos nuestro código funcionará, la desventaja es que cualquier aplicación que hayamos creado con una base similar deberá ser actualizado. Esto no es nada escalable si tenemos cientos o miles de líneas de código.
Esta función está incluida en Python, en particular crea y retorna la propiedad de un objeto. La propiedad de un objeto posee los métodos
getter()
,setter()
ydel()
.En tanto la función tiene cuatro atributos:
property(fget, fset, fsel, fdoc)
:fget
: trae el valor de un atributo.fset
: define el valor de un atributo.fdel
: elimina el valor de un atributo.fdoc
: crea un docstring por atributo.
Ejemplo:
class Millas: def __init__(self): self._distancia = 0 # Función para obtener el valor de _distancia def obtener_distancia(self): print("Llamada al método getter") return self._distancia # Función para definir el valor de _distancia def definir_distancia(self, recorrido): print("Llamada al método setter") self._distancia = recorrido # Función para eliminar el atributo _distancia def eliminar_distancia(self): del self._distancia distancia = property(obtener_distancia, definir_distancia, eliminar_distancia) # Creamos un nuevo objeto # avion = Millas() # Indicamos la distancia # avion.distancia = 200 # Obtenemos su atributo distancia # >>> print(avion.distancia) # Llamada al método getter # Llamada al método setter # 200
Este decorador es uno de varios con los que ya cuenta Python, el cual nos permite utilizar getters y setters para hacer más fácil la implementación de la programación orientada a objetos en Python cambiando los métodos o atributos de las clases de forma que no modifiquemos el código. Ejemplo:
class Millas: def __init__(self): self._distancia = 0 # Función para obtener el valor de _distancia # Usando el decorador property # Llamamos a este setter haciendo avion.distancia = 20 @property def distancia(self): print("Llamada al método getter") return self._distancia # Función para definir el valor de _distancia # Llamamos a esta función simplemente llamando a avion.distancia @distancia.setter def distancia(self, valor): if valor < 0: raise ValueError("No es posible convertir distancias menores a 0.") print("Llamada al método setter") self._distancia = valor # Función para eliminar el valor de _distancia # Llamamos a esta función llamando a del avion.distancia @distancia.deleter def distancia(self): print("Llamada al método deleter") del self._distancia # Creamos un nuevo objeto # avion = Millas() # Indicamos la distancia # avion.distancia = 20 # Obtenemos su atributo distancia # print(avion.distancia) # Eliminamos el atributo # del avion.distancia # Salida # Llamada al método setter # Llamada al método getter # 20 # Llamada al método deleter
Mas info → https://www.freecodecamp.org/news/python-property-decorator/
-
Para definir getters:
@property def region(self): return self.__region
Para implementar setters:
@region.setter def region(self, region): # Implementación
Se usa
@
seguido del nombre de la propiedad que se va a usar para el setter seguido de.setter
-
class Rectangle: # Implementación class Square(Rectangle): # Define herencia en Python. Rectangle sería la Superclase # Implementación
super()
→ Permite obtener una referencia a la Superclase. Ejemplo:class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height class Square(Rectangle): def __init__(self, side): super().__init__(side, side) if __name__ == '__main__': rectangle = Rectangle(width=3, height=4) print(rectangle.area()) square = Square(side=5) print(square.area())
-
Cambiar el comportamiento de una Superclase para adaptarlo a una subclase.
class Person: def __init__(self, name): self.name = name def move(self): print('Estoy caminando') class Cyclist(Person): def __init__(self, name): super().__init__(name) def move(self): # Sobrescribe el método move() de la Superclase print('Estoy moviéndome en mi bicicleta')
-
Comparar la eficiencia de un algoritmo y tratar de predecir su tiempo de ejecución.
La complejidad puede ser temporal (cuanto tiempo tarda) o espacial (cuanto espacio ocupa).
La complejidad temporal se define como $$ T(n) $$ Se puede contabilizar de varias formas:
- Cronometrar tiempo de ejecución. Método poco eficaz pues esta influido por muchas variables externas.
- Contar los pasos (operaciones matemáticas, comparaciones, etc). Ineficaz pues los pasos pueden variar según la implementación.
- Contar los pasos conforme nos aproximamos al infinito, conforme el dataset crece (medida asintomática).
-
1oo2+x+2x^2
No es una mala aproximación, pero a medida que crece el data set hay términos
de la ecuación que dejan de importar, puesto que por ejemplo mientras más grande sea el input el término que mayor define el número de pasos es x^2 mientras que los demás términos apenas afectan el total. Existe Big O notation que es una notación en la que se excluyen los términos que dejan de ser relevantes cuando el input se acerca al infinito.
-
No importan las variaciones pequeñas. EL enfoque se centra en lo que pasa conforme el tamaño del problema de acerca al infinito.
Se analiza el mejor de los casos, caso promedio y el peor de los casos. Por ejemplo en un algoritmo de búsqueda el mejor de los casos sería que se encuentre el dato que se busque en la primera posición, no obstante lo ideal es trabajar en base al peor escenario. S ólo importa el término de mayor tamaño.
⬆ Función de crecimiento lineal. En otras palabras la función crece en O de n.
⬆ Al ser loops anidados se multiplican.
⬆ Por cada llamada a la función
fibonacci()
se hacen 2 llamadas recursivas. -
Existen distintos tipos de complejidad algorítmica:
- O(1) Constante: no importa la cantidad de input que reciba, siempre demorara el mismo tiempo.
- O(n) Lineal: la complejidad crecerá de forma proporcional a medida que crezca el input.
- O(log n) Logarítmica: nuestra función crecerá de forma logarítmica con respecto al input. Esto significa que en un inicio crecerá rápido, pero luego se estabilizara.
- O(n log n) Log lineal: crecerá de forma logarítmica pero junto con una constante.
- O(n²) Polinomial: crecen de forma cuadrática. No son recomendables a menos que el input de datos en pequeño.
- O(2^n) Exponencial: crecerá de forma exponencial, por lo que la carga es muy alta. Para nada recomendable en ningún caso, solo para análisis conceptual.
- O(n!) Factorial: crece de forma factorial, por lo que al igual que el exponencial su carga es muy alta, por lo que jamas utilizar algoritmos de este tipo.
-
Buscar en todos los elementos de manera secuencial.
Operador ternario
print(f'El elemento {number_to_search} {"esta" if found else "NO esta"} en la lista')
Generar listas de números aleatorios
numbers_list = [random.randint(0, 100) for i in range(list_length)]
-
Divide el problema en dos en cada iteración, necesita elementos ordenados para trabajar.
-
Bubble sort tiene la particular de que es bastante probable que el elemento más grande quede al final en la primera iteración.
-
Es intuitivo y fácil de implementar, pero muy ineficiente para inputs grandes.
Ordena en su lugar, no requiere memoria adicional pues modifica los valores en memoria.
Una lista es dividida entre una sublista ordenada y otra sublista desordenada. Al principio, la sublista ordenada contiene un solo elemento, por lo que por definición se encuentra ordenada.
A continuación se evalua el primer elemento dentro la sublista desordenada para que podamos insertarlo en el lugar correcto dentro de la lista ordenada.
La inserción se realiza al mover todos los elementos mayores al elemento que se está evaluando un lugar a la derecha.
Continua el proceso hasta que la sublista desordenada quede vacia y, por lo tanto, la lista se encontrará ordenada.
Ejemplo:
Imagina que tienes la siguiente lista de números:
7, 3, 2, 9, 8
Primero añadimos 7 a la sublista ordenada:
7, 3, 2, 9, 8
Ahora vemos el primer elemento de la sublista desordenada y lo guardamos en una variable para mantener el valor. A esa variable la llamaremos
valor_actual
. Verificamos que 3 es menor que 7, por lo que movemos 7 un lugar a la derecha.7, 7, 2, 9, 8 (valor_actual=3)
3 es menor que 7, por lo que insertamos el valor en la primera posición.
3, 7, 2, 9, 8
Ahora vemos el número 2. 2 es menor que 7 por lo que lo movemos un espacio a la derecha y hacemos lo mismo con 3.
3, 3, 7, 9, 8 (valor_actual=2)
Ahora insertamos 2 en la primera posición.
2, 3, 7, 9, 8
9 es más grande que el valor más grande de nuestra sublista ordenada por lo que lo insertamos directamente en su posición.
2, 3, 7, 9, 8
El último valor es 8. 9 es más grande que 8 por lo que lo movemos a la derecha:
2, 3, 7, 9, 9 (valor_actual=8)
8 es más grande que 7, por lo que procedemos a insertar nuestro
valor_actual
.2, 3, 7, 8, 9
Ahora la lista se encuentra ordenada y no quedan más elementos en la sublista desordenada.
Ejemplo gráfico:
def ordenamiento_por_insercion(lista): for indice in range(1, len(lista)): valor_actual = lista[indice] posicion_actual = indice while posicion_actual > 0 and lista[posicion_actual - 1] > valor_actual: lista[posicion_actual] = lista[posicion_actual - 1] posicion_actual -= 1 lista[posicion_actual] = valor_actual
-
Es un algotimo bastante eficiente.
Explicación de la recursividad en el ejercicio:
🔗 Recursos:
- Simulador gráfico con todos los algoritmos de ordenamiento ➡ https://visualgo.net/en/sorting
- Video con una explicación gráafica (y un tanto curiosa) del Merge Sort ➡ https://www.youtube.com/watch?v=XaqR3G_NVoo
**🛈 Nota: ** las listas en Python se pasan por referencia. Lo cual quiere decir, que si modificamos la lista dentro de la función, también lo hacemos en la lista original
-
Permiten aislar el ambiente paara poder instalar diversas versiones de paquetes.
A partir de Python 3 se incluye en la librería estándar en el módulo
venv
Níngún ingeniero profesional en Python trabaja sin ellos
Permite descargar e instalar paquetes de terceros, así como compartir con la ccomunidad paquetes propios. Se puede especificar la versión del paquete que se necesita.
python3 -m venv env
➡ - m (módulo que se va a ejecutar) venv (nombre módulo) env (nombre ambiente virtual)source env/bin/activate
➡ Activar ambiente virtualdeactivate
➡ salir del ambiente virtualpip install bokeh
Siempre es usar ambientes virtuales no importa si se usa Python o Anaconda.
Una práctica muy común es crear un archivo
requeriments.txt
y colocar ahí los paquetes que se necesiten uno debajo del otro. Ejemplo:bokeh numpy flask
También se puede especificar la versión de cada paquete. Ejemplo:
flask==1.1.8
pip search <package>
pip show <package>
➡ Muestra los detalles de un paquete instaladopip uninstall <package>
pip list
➡ Retorna la lista de paquetes en el ambiente actualpip freeze
➡ Se usa para congelar los paaquetes y su versión actual
-
Los gráficos permiten:
- Hacer reconociento de patrones.
- Predecir cuál vaa a ser el siguiente elementos de una serie.
- Simplifican la interpretación y las conclusiones acerca de los datos.
-
Bokeh permite construir gráficos ccomplejos de forma sencilla y exportar a formatoss como html, notebooks, imágenes, etc. Además se puede usar en un servidor con Flask y Django.
-
EL concepto de optimización permite resolver muchos problemas de manera computacional.
Para pensar en estos algoritmos se puede pensar en una función que se pueda maximizar (encontrar el input que retorne el output más alto) o minimizar (encontrar el input que retorne el output más bajo) dentro de una función especifica.
En ocasiones se pueden definir algunas limitantes. Por ejemplo encontrar el vuelo más barato dentro de ciertas fechas, que no tenga escalas y que tenga asientos en la salida de emergencia.
🛈 Nota: investigar sobre el problema del vendedor viajero
Problemas del milenio por Un millón de dólares
- P versus NP (https://www.youtube.com/watch?v=UR2oDYZ-Sao)
- La conjetura de Hodge
- La conjetura de Poincaré. Ya fué resuelto.
- La hipótesis de Riemann
- Existencia de Yang-Mills y del salto de masa
- Las ecuaciones de Navier-Stokes
- La conjetura de Birch y Swinnerton-Dyer
-
Algunos sitios donde encontrar retos: