Conceptos Avanzados de Python 🐍 para ser un profesional en el lenguaje
Tags: spanish, python, beginners
Published: 2022-12-18 16:39:48

#***¿Cómo funciona Python?***

Python es un **lenguaje interpretado** lo que significa que tu código es transformado por el intérprete (máquina virtual de Python) a *bytecode* antes de ser ejecutado por un ordenador con *x* sistema operativo.

![Tipos de lenguaje](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yky7sawt7xlh1gjoleok.png)

### El bytecode
Es un lenguaje de programación de más bajo nivel que puede ser leído por un intérprete.

Aquí un ejemplo de una función pasando a bytecode.

![python-bytecode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r10smxm2joub2lezbdf9.png)

### Interprete

Es la máquina virtual, traduce el bytecode a código máquina (ceros y unos).


![python-maquinavirtual](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eenjy82kep8a44wj3npt.png)

#***¿Los lenguajes interpretados son más lentos?***

La respuesta se puede resumir en la siguiente frase:

> **C** cuando se debe y **Python** cuando se puede.

### Garbage collector

Recuerda que el *garbage collector* toma los objetos y variables que no están en uso y los elimina.

#***¿Qué es la carpeta Pycache?***

**_pycache _** Es el directorio (carpeta) que contiene el *bytecode* (el código intermedio) que crea Python para que lo pueda leer la máquina y no tenga que convertirlo de nuevo a bytecode.

#***¿Cómo organizar las carpetas de tus proyectos?***

Paquetes y módulos

### Módulo

- Es cualquier archivo de **Python.**
- Generalmente, contiene código que se puede reutilizar.

### Paquete

- Es un conjunto de módulos en una carpeta.
- Siempre posee el archivo `__init__.py`

### Ejemplo:
Proyecto de exploración espacial:

![proyecto_exploracion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fgnlb7ds2d5iqp9uno79.png)
**Módulos:** 

`nave.py` → Explica el funcionamiento de la nave. 

`destino.py`→ Calcular las coordenadas del destino.  

`plataforma.py`→ Permite manipular la plataforma.

`lanzamiento.py`→ Cualidades que necesita la nave.

`test.py`→ Contiene funciones de prueba.

`validacion.py`→Verifica identidad de los pasajeros.

### ***¿Cómo se vería esto en una estructura de carpetas?***


![estructura_carpetas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hj88m80m64l0gdxaah7g.png)
**Donde:**

`README` → Como trabajar con este proyecto.

`.gitignore`→ Ignorar archivos en repositorios GIT

`venv`→ Entorno virtual

`exploracion_espacial`→ Paquete que contiene el: `__init__.py`

#***¿Qué son los tipados?***
💻 Los tipados es una clasificación de los lenguajes de programación, tenemos cuatro tipos:

- Estático
- Dinámico
- Débil
- Fuerte

El tipado del lenguaje depende de cómo trata a los tipos de datos.

El tipado estático es el que levanta un error en el tiempo de compilación, ejemplo en JAVA:

```
String str = "Hello" // Variable tipo String
str = 5 
// ERROR: no se puede convertir un tipo de dato en otro de esta forma.
```
El tipado dinámico levantan el error en tiempo de ejecución, ejemplo en Python:

```
str = "Hello" # Variable tipo String
str = 5 # La variable ahora es de tipo Entero, no hay error

## TIPADO FUERTE
x = 1
y = "2"
z = x + y # ERROR: no podemos hacer estas operaciones con tipos de datos distintos entre sí
```
El tipado débil es el que hace un cambio en un tipo de dato para poder operar con él, como lo hace JavaScript y PHP.

> 🐍 Python es un lenguaje de tipado 👾 Dinámico y 💪 Fuerte.

![tipados_notipados](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9zhg7jj0eivdnhdu54op.png)

#***Tipado estático en Python (Static Typing)***
> Asignar tipos a las variables de Python:

![tipos_variables1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j256l85sz9qswsiy0hvk.png)

> Asignar tipos en funciones:

![tipos_variables2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rtfgn2su4hjp3e6tx427.png)

> Trabajando con estructuras de datos: 

![tipos_variables3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwauc39ppz6p2y89sru9.png)

### Trabajar con tipos

Importar de la librería `typing` las clases `Dict` y `List`

```python
from typing import Dict, List
```

Definir una lista de enteros:

```python
positives: List[int] = [1,2,3]
```

Lista de diccionarios con llaves y valores

```python
countries: List[Dict[str, str]]
```
#***Las ventajas de utilizar tipado estático en Python:***
1. Nos aporta claridad del código.
2. Nos da calidad a lo que estamos programando.
3. Nuestro código va a ser más entendible a otros programadores si trabajamos con tipos.
4. Nos va a devolver los errores antes del que programa se ejecute.

#***Scope: alcance de las variables***

- El Scope es el alcance que tienen las variables. 
- Depende de donde declares o inicialices una variable para saber si tienes acceso. 

### Local Scope

Es la región que corresponde el ámbito de una función, donde podremos tener una o más variables, las variables van a ser accesibles únicamente en esta región y no serán visibles para otras regiones

### Global Scope

Al escribir una o más variables en esta región, estas podrán ser accesibles desde cualquier parte del código.

![scope](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/luvg0q3wc9470nusi7hp.png)

#***Closures***
Es una forma de acceder a variables de otros Scopes a través de una nested function. Se retorna la nested function y esta recuerda el valor que imprime, aunque a la hora de ejecutarla no este dentro de su alcance.

### Nested Functions (Funciones anidadas)

Funciones creadas dentro de otra función:

```
def main():
	a = 1
	def nested(): 
		print(a)
	return nested

my_func = main()
my_func()
```

### 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.

#***Decoradores***

Un decorador es una función que recibe como parámetro otra función, le añade cosas y retorna una función diferente. 
Tienen la misma estructura que los `Closures`, pero en vez de variables lo que se envía es una función. 
Ejemplo:

```
def decorador(func):
    def envoltura():
        print("Esto se añade a mi función original.")
        func()
    return envoltura

def saludo():
    print("¡Hola!")

saludo()
# Salida:
# ¡Hola!

saludo = decorador(saludo) # Se guarda la función decorada en la variable saludo
saludo()                   # La función saludo está ahora decorada
# Salida:
# Esto se añade a mi función original.
# ¡Hola!
```

Se puede hacer de manera más sencilla, con azúcar sintáctica (sugar syntax): Cuando tenemos un código que está embellecido para que nosotros lo veamos de una manera más estática, ayudando a entender de manera más sencilla el código. 

De esta manera, tenemos el código anterior:

```
def decorador(func):
    def envoltura():
        print("Esto se añade a mi función original.")
        func()
    return envoltura

def saludo():
    print("¡Hola!")
saludo = decorador(saludo) # Se guarda la función decorada en la variable saludo (se decora)

saludo()                   # La función saludo está ahora decorada

```

```
def decorador(func):
    def envoltura():
        print("Esto se añade a mi función original.")
        func()
    return envoltura

# De esta manera se decora la función saludo (equivale a saludo = decorador(saludo) de la última línea, quedando ahora en la línea inmediata superior ):
@decorador
def saludo():
    print("¡Hola!")

saludo()                   # La función saludo está ahora decorada
```

Esto permite ahorrar código al implementar características (decoradores) comunes a diferentes funciones:

```
def decorator_upper(func):                  # Función decoradora
    def wrapper(text):                      # Función anidada
        return func(text).upper()           # Operación que realiza el decorado a la función (func), inserta el texto a la función original. Convierte todo a mayúsculas.
    return wrapper                          # Devuelve wapper como indica la regla de los Clousures

@decorator_upper                            # Decora la función message
def message(name):
    return f'{name}, recibiste un mensaje'  # Esto es lo que realiza la función message, previo a ser decorada.

@decorator_upper                            # Decora la función warning
def warning(name):
    return f'Usa solo mayúsculas {name}'  # Esto es lo que realiza la función warning, previo a ser decorada.

print(message("Cesar")) # Output: CESAR, RECIBISTE UN MENSAJE
print(warning("Cesar")) # Output: USA SOLO MAYÚSCULAS CESAR
```
#***Iteradores***

Antes de entender qué son los `iteradores`, primero debemos entender a los iterables.

- Son todos aquellos objetos que podemos recorrer en un ciclo. - Son aquellas estructuras de datos divisibles en elementos únicos que yo puedo recorrer en un ciclo.

Pero en Python las cosas no son así. Los iterables se convierten en `iteradores`.

Ejemplo:

```
# Creando un iterador

my_list = [1,2,3,4,5]
my_iter = iter(my_list)

# Iterando un iterador

print(next(my_iter))

# Cuando no quedan datos, la excepción StopIteration es elevada
```

```
# Creando un iterador

my_list = [1,2,3,4,5]
my_iter = iter(my_list)

# Iterando un iterador

while True: #ciclo infinito
  try:
    element = next(my_iter)
    print(element)
  except StopIteration:
    break
```
Momento impactante: El ciclo “`for`” dentro de Python, no existe. Es un `while` con `StopIteration`. 🤯🤯🤯

```
my_list = [1,2,3,4,5]

for element in my_list:
  print(element)

```

> evenNumbers.py

```
class EvenNumbers:
  """Clase que implementa un iterador de todos los números pares,
  o los números pares hasta un máximo
  """

  #* Constructor de la clase
  def __init__(self, max = None): #self hace referencia al objeto futuro que voy a crear con esta clase
    self.max = max


  # Método para tener elementos o atributos que voy a necesitar para que el iterador funcione
  def __iter__(self):
    self.num = 0 #Primer número par
    #* Convertir un iterable en un iterador
    return self

  # Método para tener la función "next" de Python
  def __next__(self):
    if not self.max or self.num <= self.max:
      result = self.num
      self.num += 2
      return result
    else:
      raise StopIteration
```
### Ventajas de usar iteradores:

- Nos ahorra recursos.
- Ocupan poca memoria.

#***Generadores***

Sugar syntax de los iteradores. Los generadores son funciones que guardan un estado. Es un iterador escrito de forma más simple.

```
def my_gen():

  """un ejemplo de generadores"""

  print('Hello world!')
  n = 0
  yield n # es exactamente lo mismo que return pero detiene la función, cuando se vuelva a llamar a la función, seguirá desde donde se quedó

  print('Hello heaven!')
  n = 1
  yield n

  print('Hello hell!')
  n = 2
  yield n


a = my_gen()
print(next(a)) # Hello world!
print(next(a)) # Hello heaven!
print(next(a)) # Hello hell!
print(next(a)) StopIteration
```
Ahora veremos un generator expression (es como list comprehension pero mucho mejor, porque podemos manejar mucha cantidad de información sin tener problemas de rendimiento):

```
#Generator expression

my_list = [0,1,4,7,9,10]

my_second_list = [x*2 for x in my_list] #List comprehension
my_second_gen = ()x*2 for x in my_list]) #Generator expression
```
#***Sets***

Un set es una colección desordenada de elementos únicos e inmutables.

### ¿Cómo se crean los sets?

```
my_set = {1, 2, 3, 4, 5}
print("my_set =", my_set)

my_set2 = {"Hola", 2.4, False, True}
print("my_set2 =", my_set2)

my_set3 = {3,3,2}
print("my_set3 =", my_set3)

my_set4 = {[1,2,3], [4,5,6],4}
print("my_set4 =", my_set4)
## Este último da error porque una lista es un elemento mutable
```
Una llave sin los dos puntos crea un set

Se puede crear sets vacíos…

```
empty_set = {}
print(type(empty_set)) #<class 'dict'>

empty_set = set()
print(type(empty_set)) # <class 'set'>
```

### ***Datos a tener en cuenta***:

- Python trata las llaves como un diccionario
- Se usa la función set() para indicar que es un set

### Casting con set

Convertir un set a otra estructura de datos, o una estructura de datos a un set.

```
my_list = [1,1,2,3,45,324]
my_set = set(my_list)
print(my_set)

my_tulpe = ("hola", "hola", "hola", 1)
my_set2 = set(my_tulpe)
print(my_set2)
# {1, 2, 3, 324, 45}
# {1, 'hola'}
```
### ***Datos a tener en cuenta***:

- Para transformar una lista y una tupla. solo tenemos que usar el set() sobre la variable
- El set() elimina los elementos que se repiten.

### Añadir elementos a un set

```
my_set = {1,2,3}
print(my_set)

#añadir un elemento

my_set.add(4)
print(my_set)

#añadir múltiples elementos
my_set.update([1,2,5])

#añadir múltiples elementos
my_set.update([1,7,2], {6,8})
print(my_set)
# {1, 2, 3}
# {1, 2, 3, 4}
# {1, 2, 3, 4, 5, 6, 7, 8}
```
### Datos a tener en cuenta:

> acá podemos apreciar de que hay métodos que se pueden combinar con los set, por ejemplo .add() y .update(). el .add para agregar un elemento y el update para agregar múltiples elementos

### Borrar elementos de un set

```
my_set = {1,2,3,4,5,6,7}
print(my_set)

#Borrar un elemento existente
my_set.discard(1)

my_set.discard(2)
print(my_set)

#borrar un elemento inexistene
my_set.discard(10)

my_set.remove(12)
print(my_set)
# {1, 2, 3, 4, 5, 6, 7}
# {3, 4, 5, 6, 7}
# Traceback (most recent call last):
#   File "python.py", line 28, in <module>
#     my_set.remove(12)
# KeyError: 12
```
Se usan los métodos **discard** y **remove**
Si el elemento no existe con remove, lanza un error de tipo KeyError. Remove, es más exigene.

Para eliminar elementos aleatorios se usa el **`pop()`** y para eliminar todos los elementos del set se usa **`.clear()`**

### Operaciones con sets


![Operaciones con sets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v01iq8vzsq8g81yksj54.png)

Métodos Set

```
from platform import system
from os import system as console_command


# Utility
def clean_screen() -> None:
  """This function is responsible for cleaning the screen."""

  if system() == 'Windows': console_command('cls')
  else: console_command('clear')


def sets() -> None:
  """Multiple Operations with sets"""
  my_set1 = {'🍎', '🍊', '🍇', '🍓', '🍈'}
  my_set2 = {'🍉', '🍊', '🍒', '🍓', '🍋'}
  print("  →  Set 1:", my_set1)
  print("  →  Set 2:", my_set2)
  print('')
  # Union
  my_set3 = my_set1 | my_set2
  print("     Union                  :", my_set3)
  # Intersection
  my_set4 = my_set1 & my_set2
  print("\n     Intersection           :", my_set4)
  # Difference
  my_set5 = my_set1 - my_set2
  print("\n     Difference set1 - set2 :", my_set5)
  my_set6 = my_set2 - my_set1
  print("\n     Difference set2 - set1 :", my_set6)
  # Symmetric Difference
  my_set7 = my_set1 ^ my_set2
  print("\n     Symmetric Difference   :", my_set7)


if __name__ == '__main__':
  clean_screen()
  print("*** O P E R A T I O N S    W I T H    S E T S ***\n")
  sets()

```

### ***Manejo de fechas***

`datetime` es un módulo de manejo de fechas aquì un ejemplo de su uso:

```
import datetime

my_time = datetime.datetime.now() # hora local de mi PC u hora universal
my_date = datetime.date.today() # fecha actual

my_day = datetime.date.today()

print(my_time)
print(my_date)

print(f'Year: {my_day.year}')
print(f'Month: {my_day.month}')
print(f'Day: {my_day.day}')
```
Tabla de códigos de formato para fechas y horas(los más importantes):

| Formato | Código |
| --- | --- |
| Año | %Y |
| Mes | %m |
| Día | %d |
| Hora | %H |
| Minutos | %M |
| Segundos | %S |

```
from datetime import datetime

my_datetime = datetime.now()
print(my_datetime)

latam = my_datetime.strftime('%d/%m/%Y')
print(f'Formato LATAM: {latam}')

usa = my_datetime.strftime('%m/%d/%Y')
print(f'Formato USA: {usa}')

random_format = my_datetime.strftime('año %Y mes %m día %d')
print(f'Formato random: {random_format}')

formato_utc = datetime.utcnow()
print(f'Formato UTC: {formato_utc}')

```

Este código importa la clase datetime del módulo datetime, que proporciona diversas funciones para trabajar con fechas y horas.

La primera línea crea un objeto datetime que representa la fecha y hora actual. Luego, se imprime en pantalla utilizando la función `print()`.

La siguiente línea utiliza el método `strftime()` para dar formato a la fecha y hora actual en un formato específico. El método `strftime()` toma una cadena de formato como argumento, que indica cómo se deben mostrar los distintos componentes de la fecha y hora (día, mes, año, etc.). En este caso, el formato especificado es `'%d/%m/%Y'`, que indica que se deben mostrar el día, mes y año en ese orden, separados por barras.

La siguiente línea hace lo mismo, pero con un formato distinto: `'%m/%d/%Y'`, que indica que se deben mostrar el mes, día y año en ese orden, separados por barras.

La línea siguiente también utiliza el método `strftime()`, pero con un formato más personalizado: `'año %Y mes %m día %d'`, que muestra el año, mes y día en ese orden, con las palabras "año", "mes" y "día" incluidas en la cadena de formato.

Por último, se crea un nuevo objeto datetime que representa la fecha y hora actual en el formato UTC (Coordinated Universal Time, o Tiempo Universal Coordinado), que es una referencia de tiempo uniforme utilizada a nivel internacional. Luego, se imprime en pantalla utilizando la función `print()`.

> **¡Gracias por leer este post!😊**

> Si te ha parecido útil o interesante, puedes darle un like 💙 para motivarme a seguir compartiendo contenido así. Si tienes la oportunidad y quieres apoyar mi trabajo, puedes hacerlo a través de un pequeño donativo 💸 ¡Un café virtual sería un gran detalle! ☕ Puedes hacerlo a través de [Ko-fi](https://ko-fi.com/geoannycode).

> Además, si te gustaría seguir mis actualizaciones y contenido en redes sociales, puedes encontrarme en [Twitter](https://twitter.com/GeoannyCode) y [YouTube](https://www.youtube.com/channel/UC18KaeuDNfUi9mt9xhNjDyw) como "GeoannyCode" 📱💻 Espero verte por allí! 😊 ¡Gracias por tu apoyo! ❤️"