Está en la página 1de 61

Capítulo 1: Entornos de trabajo

1.1- Python
1.2- Editores de texto y la consola
1.3- Google Colab
1.4- Formato Markdown
Capítulo 2: Conceptos básicos de Python
2.1- Variables en Python
2.2- Palabras reservadas y asignación simultánea
2.3- Operaciones con números en Python
2.4- Comentarios en Python
2.5- Importando librerías
Capítulo 3: Datos básicos en Python
3.1- Tipos de números y operaciones
3.2- Números complejos en Python
3.3- Introducción a strings
3.4- Operaciones con strings
3.5- Impresión en pantalla
3.6- Casteo y formateo de strings
3.7- Slicing en strings
3.8- Métodos de strings
3.9- Booleanos y operadores lógicos en strings
3.10- Métodos booleanos en strings
Capítulo 4: Condicionales y bucles.
4.1- Estructura condicional (if-elif-else)
4.2- Bucle while
4.3- Bucle for
4.4- Iterador range
4.5- Continue en bucles
Capítulo 5: Estructuras de datos
5.1- Listas
5.2- Operaciones de listas y listas anidadas
5.3- Métodos y listas
5.4- Range y casteo de listas
5.5- Matrices con listas
5.6- Suma y producto de matrices con listas
5.7- Matrices con numpy
5.8- Diccionarios
5.9- Iterando en diccionarios
5.10- Métodos de diccionarios
5.11- Construcción de diccionarios
5.12- List y dict comprehensions
5.13- Conjuntos
5.14- Métodos de conjuntos
5.15- Tuplas
5.16- Forma de guardar variables(el método unpacking)
5.17- Operaciones y sutilezas de las tuplas
5.18- Iteraciones en tuplas
5.19- La función zip()
Capítulo 6: Funciones
6.1- Conceptos básicos de funciones
6.2- Parámetros de una función
6.3- Documentando funciones(Docstring)
6.4- Alcance de variables (scope), paso por copia y paso por referencia
6.5- Estándares en la comunidad de Python
6.6- Funciones recursivas
6.7- Funciones helper
6.8- Funciones lambda
6.9- Funciones de orden superior
6.10- Filtrado de iterables con filter()
6.11- Reducción de listas con reduce()
6.12- Funciones vectoriales de varias variables con map()
6.13- Ordenando listas con sorted()
Capítulo 7: Programación orientada a objetos (POO)
7.1- Conceptos básicos de POO
7.2- Metodo constructor y destructor
7.3- Métodos de una clase
7.4- Propiedades
7.5- Herencia y polimorfismo
7.6- Variables privadas (mangling)
7.7- Forma alternativa para el setter, getter y del
Capitulo 8: Scripts y módulos
8.1- Conceptos básicos de módulos
8.2- Módulos comunes y funciones predeterminadas
8.3- El zen de Python
8.4- Entornos virtuales
8.5- PIP y requerimientos
Capítulo 9: Errores y excepciones
9.1- Errores y excepciones
9.2- Try/except
9.3- Try/finally
9.4- Assert
9.5- Raise
9.6- Pruebas de caja negra
9.7- Pruebas de caja de cristal
Capítulo 10: Funcionamiento avanzado de Python
10.1- Organización de proyectos
10.2- Tipado y tipado estático en Python
10.3- Closures
10.4- Decoradores
10.5- Iteradores
10.6- Generadores
10.7- Programación dinámica
10.8- Reconocimiento de patrones estructuales con Match
10.9- Arrays
Capitulo 11: Módulo math y cmath
Capítulo 12: Módulo Numpy
12.1- Arrays, creación, redimensión, slicing y filtrado
12.2- Matrices y vectores con numpy
12.3- Tipos de datos, copias y vistas
12.4- Bucles en arrays
12.5- Concatenación de arrays
12.6- Dividiendo arrays
12.7- Búsquedas y ordenamientos en un array
12.8- Aleatoriedad en numpy
12.9- Funciones univesales
12.10- Redondeo y matemáticas en numpy
12.11- Conjuntos en Numpy
Capítulo 13: Archivos importados en Python
13.1- Archivos txt
13.2- Ceando txt desde Python
13.3- Sobreescribiendo un txt
13.4- Cargando .CSV con open()
Capítulo 14: Módulo pandas y dataframes
14.1- Dataframes y su construcción
14.2- Dimensiones de los DataFrames
14.3- Extracción de columnas
14.4- Extracción de filas
14.5- Métodos de DataFrames
14.6- Bucles en dataframes
14.7- Ficheros CSV
14.8- Ficheros JSON
14.9- Datos faltantes
14.10- Filtrado de dataframes
14.11- Series de pandas
14.12- índice multinivel o Multiíndices de pandas
14.13- Tablas pivote o tablas dinámicas
14.14- Concatenación de dataframes
14.15- Conexión con bases de datos tipo SQL
14.16- Series de tiempo en pandas
Capítulo 15: Representación gráfica en Python
15.1- Preparativos iniciales
15.2- Diagrama de dispersión en matplotlib
15.3- Line plot con matplotlib
15.4- Bar plot con matplotlib
15.5- Gráfico de pastel
15.6- Histograma de frecuencias
15.7- Boxplot en matplotlib
15.8- Múltiples plots en uno de plt
15.9- Múltiples plots con POO
15.10- Metodo plt.add_subplot()
15.11- Múltiples plots con plt.subplots()
15.12- Ploteando dataframes directamente
15.13- Gráficas comunes en cálculo
15.14- El módulo seaborn y el gráfico de dispersión
15.15- Lineplot con seaborn
15.16- Bar plot con seaborn
15.17- Histograma con seaborn
15.18- Gráficos de densidad con seaborn
15.19- Boxplot con seaborn
15.20- Joinplot y pairplot
15.21- Múltiples plots con seaborn
15.22- Grafos con networkx
Capítulo 16: Módulos especiales de Python
16.1- os
16.2- pathlib
16.3- Random
16.4- pylint
16.5- collections
16.6- shutil
16.7- datetime y pytz
16.8- time y timeit
16.9- re
16.10- zipfile
16.11- pygame
16.12- sympy
16.13- manim
16.14- scipy
16.15- functools
Capítulo 17: Web Scraping
17.1- Elementos de un sitio web
17.2- beautifulsoup y requests
Capítulo 18: Tkinter
Capítulo 19: Django
Capítulo 20: Flask
Capítulo 21: Kivy
Capítulo 22: Cálculo de una y varias variables
Capítulo 23: Álgebra lineal
23.1- Escalares, Vectores, matrices y tensores
23.2- Propiedades sobre vectores, matrices y tensores
23.3- Operaciones entre vectores, matrices y tensores
23.4- Sistemas de ecuaciones lineales
23.5- Graficando vectores
23.6- valores y vectores propios y formas canónicas
Capítulo 24: Estadística y probabilidad
Capítulo 24: Machine Learning
Capítulo 25: Deep Learning
Capítulo 26: Inteligencia artificial
Capítulo 27: Blockchain

__________________________________________________________________________________________________________________________________
______________________

Capítulo 1: Entornos de trabajo

*****1.1- Python*****
Python es un lenguaje que tiene un gran uso en 4 áreas principales:
- Inteligencia artificial: Se centra en enseñarle a las computadoras a aprender para que ella misma pueda resolver un
problema
- Backend: Es la lógica almacenada en un servidor que hace que una aplicación funcione.
- Internet of things
- Data Science

Podemos encontrar todo acerca de la programación en Python en la "documentación", que se encuentra en el siguiente link:
https://docs.python.org/es/3/tutorial/index.html
sin embargo, está esccrita de un modo muy técnico, por lo cual no es tan amigable para quien empieza. Aún así sirve para
hacer consultas rápidas. Del lado izquierdo podemos encontrar las propuestas para mejorar Python en "PEP index".
La PEP número 8 es muy especial pues contiene las buenas prácticas de programación de Python.

En la página de Python https://www.python.org/ podemos encontrar la documentación más completa en la sección Docs

Existe una clasificación de los lenguajes de programación en:


Lenguaje compilado: este tipo de lenguajes manda el código a un programa (compilador) que lo transforma a lenguaje
de máquina.
Lenguaje interpretado: este tipo de lenguajes manda el código a un programa (intérprete) que transforma el código
a un tipo de lenguaje conocido como byte code que posteriormente se transforma a lenguaje de máquina.
Python es un lenguaje interpretado. Para aplicaciones sencillas, esto no tiene importancia, a menos que uno se dedique a
una rama muy avanzada que requiera muchísimo tiempo de ejecución, pues en estos casos, los lenguajes interpretados son
más lentos que los lenguajes compilados.

En python, hay una sección especial conocida como "garbage collector", toma los objetos y las variables que no están en uso
y los elimina, lo cual no sucede en lenguajes como C++.

Hay una carpeta __pycache__ que se crea automáticamente en nuestros proyectos que contiene el byte code reciente, nos sirve
para ahorrarnos tiempo, pues ya no se transforma el código a byte code si ya está almacenado en la carpeta, es decir, lo
transforma una sola vez o las veces mínimas que sean necesarias.

*****1.2- Editores de texto y la consola*****


Para crear archivos de Python podemos usar distintas herramientas, pero todas ellas son editores de texto plano, por ejemplo:
- Bloc de notas (en Windows)
- Visual Studio Code
- PyCharm
- Sublime Text
- Atom
- Spyder
estos son utilizados comúnmente con una consola basada en OS UNIX, por ejemplo la consola de sistemas operativos Linux o los
de MAC OS. En el caso de Windows, podemos bajar una consola portable llamada "cmder".

Una vez abierta, podemos limpiar la pantalla con ctrl + L o el comando clear

Para poder moverse entre carpetas se necesita hacer lo siguiente. Para ver donde estamos usamos el comando cd (change
directory).Para moverse una carpeta mas cercana al disco usamos cd.. , el doble punto significa "carpeta padre".
Para entrar a una carpeta usamos "cd nombre" donde el nombre es el nombre de la carpeta.

Para saber los archivos y carpetas que contiene una carpeta usamos ls (list)

Para crear una carpeta usamos el comando "mkdir Nombre" donde el nombre es el de la carpeta.

Para crear un archivo por consola en la carpeta en la que uno se encuentra, se usa el comando "touch nombre.extensión"

Para iniciar la consola interactiva de Python por consola usamos "py". La consola intercativa nos permite ejecutar línea
por línea de código.

Para regresar a la consola normal usamos exit()

Para ejecutar un script de Pyhton desde la consola, nos movemos a la carpeta del archivo que queremos abrir y escribimos:
py nombre.py

*****1.3- Google Colab*****

Google Colab trabaja como un formato tipo Markdown (lenguaje de marcas), es decir, un Notebook (o documento en blanco) en donde
se pueden combinar textos con código, de la misma manera que Jupyter en Anaconda. Es un formato dirigido a presentar contenido
de una forma elegante e interactiva.

Google Colab funciona con celdas, hay de dos tipos; de código y de texto. las de código tienen un signo de Play que ejecuta
la celda, aunque puede seleccionarse la celda y presionar Ctrl+Enter para ejecutar la celda seleccionada, una vez ejecutada una
celda, aparecerá un número entre corchetes, por ejempo [2], que significa que fue la segunda celda ejecutada en la máquina de
google. Con las flechas hacia arriba y hacia abajo que se muestran en las celdas creadas, pueden moverse para cambiar su orden,
como las canciones en una playlist de alguna app.

Las celdas se pueden comentar, este comentario es un comentario google colab, no del mismo lenguaje Python. Por ejemplo, si
se requiere notificar a una persona alguna observación de la celda, puede notificarse comentando @nombredelapersona.

Se puede eliminar la celda dando click en el cesto de basura de la celda.

Para realizar un paso para atrás se presiona Ctrl+Mayus+z en lugar de Ctrl+z como comúnmente hacemos en Windows.

En la columna izquierda podemos encontrar más herramientas; en el símbolo de lista, se encuentran las secciones, las
subsecciones etc. que se crean automáticamente al escribir cierto comando en una celda de texto que veremos más adelante.
En el símbolo <> encontraremos fragmento de código ya creado listo para usar. Puede copiarse y pegarse en una celda de código
o dar click en INSERTAR una vez que se muestra el código en la misma columna izquierda. El simbolo de Folder es una
herramienta que sirve para importar archivos con datos que procesaremos de Drive, pero que veremos más adelante.

En la barra superior encontraremos la opción "entorno de ejecución", en donde podremos encontrar comandos útiles, en particular
la opción "detener ejecución", bastante útil cuando hemos tenido un error en el código y se ejecuta de mala forma o no para de
ejecutarse debido a un error. Otra opción interesante es la de "cambiar entorno de ejecución" en donde podemos mejorar el
rendimiento en casos cuando lo requiera, en simulaciones por ejemplo o cuando se entrene una inteligencia artificial, estos
procesos pueden tardar horas. Sin embargo Google Colab tiene un límite tiempo de ejecución de 6 u 8 horas.

En la misma barra superior encontramos también la opción "Herramientas", en donde podremos cambiar el tema de Google Colab, el
formato, por ejemplo, viene una opción para controlar el equivalente de una tabulación pero en espacios, el tamaño de letra.
También hay opciones de ampliar Google Colab por algunos dolares mensuales, que en este caso no es necesario

_________________________________________________________________________

*****1.4- Formato Markdown*****

El formato del lenguaje de marcas es el siguiente:


Esto es un bloque de Texto

# Esto es un Título 1

## Esto es un Título 2
### Esto es un Título 3

#### Esto es un Título 4

**Texto en negrita**

*Texto en cursiva*

- Elemento 1 de una lista no numerada


- Elemento 2 de una lista no numerada
- Elemento 3 de una lista no numerada

1. Elemento 1 de una lista numerada

1.1. Elemento 1.1. de una lista numerada

1.2. Elemento 1.2. de una lista numerada


2. Elemento 2 de una lista numerada

a. Apartado a)

b. Apartado b)
3. Elemento 3 de una lista numerada

Nuevo párrafo dentro del tercer elemento de una lista numerada

Para insertar un enlace: [texto del enlace](link del enlace)

Para insertar una imagen: ![texto de imagen](link de imagen)

---

Fórmula matemática dentro de línea: $2 + 2 = 4$

Fórmula matemática centrada en línea aparte: $$2 + 2 = 4$$

__________________________________________________________________________________________________________________________________
______________________

Capítulo 2: Conceptos básicos de Python

*****2.1- Variables en Python*****

La variables en Python se definen colocando el nombre de la variable seguido de un igual y el objeto que guardaremos
en la variable, ya sea un número, una cadena de caracteres o más objetos que veremos más adelante.

Para guardar una cadena, se coloca el texto entre comillas

Restricciones sobre los nombres de las variables:


- No pueden empezar ni contener carácteres especiales
- No pueden empezar por números
- No pueden ser llamadas igual que las palabras claves reservadas en Python
- No pueden contener espacios

Conviene que al darle nombre a una variable, éste tenga sentido en cuanto al dato que guarde, para que así resulte mucho más
fácil la comprensión por parte de quien lea el código.

A día de hoy, si los nombres de las variables están compuestos por múltiples palabras, hay 4 formas de escribir dichos
nombres:
- camelCase: `nombreMascota`
- PascalCase: `NombreMascota`
- snake_case: `nombre_mascota`
- kebab-case: `nombre-mascota`

La variables tampoco pueden nombrarse con palabras reservadas, en el documento se encuantra una lista de algunas de ellas.
_________________________________________________________________________

*****2.2- Palabras reservadas y asignación simultánea*****

Las paqueterías pueden llamarse escribiendo la palabra "import" seguido de un espacio y del nombre de la paquetería.

Para visualizar algunas palabras reservadas podemos escribir:


import keyword
keyword.kwlist
Es posible declarar varias variables simultáneamente colocando sus nombres separados por comas y los valores que se le
asignan de manera correspondiente igualmente separados por comas, por ejemplo:
x , y = 1 , "María"
a esto se le conoce como el método "unpacking", más adelante lo revisitaremos de manera más profunda.
_________________________________________________________________________

*****2.3- Operaciones con números en Python*****

En Python las operaciones son:

+ suma
- resta
* multiplicación
/ división
// división entera
** potenciación
% módulo o resto

Para sobreescribir una variable numérica aumentando una cantidad, podemos escribir, por ejemplo:
x += 2
en lugar de escribir
x = x + 2
esta sintaxis puede combinarse con las operaciones aritméticas en Python que se mostraron antes. Lo análogo se puede hacer si
queremos restar, multiplicar, dividir o exponenciar una cantidad:
x -= 2
x *= 2

_________________________________________________________________________

*****2.4- Comentarios en Python*****

Para comentar en Python hay dos formas:


Comentario de una sola línea: Se coloca el símbolo #, por ejemplo:
# Este es un comentario
Comentario de varias líneas: Se coloca el comentario entre un par de comillas triples:
""" Este es un comentario
con más de una
línea
"""

_________________________________________________________________________

*****2.5- Importando librerías*****

Para traer una librería o módulo, se usa la palabra "import" seguida por un especio y el nombre de la librería, por ejemplo:
import math
Para utilizar los elementos y funciones de la librería math, escribimos:
math.nombredelafunción() math.nombredelaconstante
donde en los paréntesis se coloca el argumento de la función.

Algunas librerías tiene un nombre muy largo, lo que ocasiona que sea tedioso escribir el nombre cada vez que llamamos una
función o elemento de ella, por ello, Python nos permite darle un alias para acortar el nombre y volver más sencilla
la escritura. Para ello, al momento de llamar la librería colocamos la palabra "as" seguida del nombre corto que le
queremos dar a la librería. Por ejemplo:
import math as mt
a partir de ese momento las funciones y constantes se escriben:
mt.nombredelafunción() mt.nombredelaconstante

Si no queremos cargar toda la librería y solo necesitamos una función completa o una variable concreta, podemos importar la
función escribiendo:
from librería import funcion
en nuestro caso, por ejemplo:
from math import pi
En este caso, ya no será necesario referirnos a la librería al escribir la constante pi, es decir, ya no será necesario
escribir math.pi, simplemente escribiremos pi.
Esto nos ahorará RAM, pues no llamaremos a toda la librería.

Podemos también, no solo llamar a un elemento de una librería, sino también cambiarle el nombre escribiendo el comando "as"
seguido del nombre nuevo que le queremos asignar a la función o variable que hemos traido de la librería:
from math import pi as numero_pi
Esto no solo nos ahorra la librería, sino que también hemos cambiado el nombre al elemento de la librería por uno más cómodo.

Podemos importar más de una función o elemento de una librería simultáneamente, para ello simplemente escribimos las
funciones deseadas:
from math import pi, log, exp
y de igual manera podemos renombrarlas allí mismo:
from math import pi as numeropi, log as logaritmo, exp as potencia

Finalmente, podemos cargar toda una libreria y evitar escribir un alias, escribiendo:
from math import *
aunque no es muy recomendable, pues podría llegar a suceder que en dos librerías haya funciones con el mismo nombre, por
lo cual, se recomienda especificar la librería aunue sea con una alias.

__________________________________________________________________________________________________________________________________
_____

Capítulo 3: Datos básicos en Python

*****3.1- Tipos de números y operaciones*****

En Python hay 4 tipos de datos básicos:


- enteros
- flotantes
- strings
- booleanos
comencemos con los números.

En Python, hay dos clases de números, los enteros "int" y los flotantes (decimales) "float", para representar un entero en
su representación flotante podemos escribirlo como 3.0 o simplemente 3.

Para averiguar el tipo de elemento o número que almacena una variable o una constante, usamos la función type()

Podemos usar las funciones int() y float() para transformar un tipo de dato a otro, por ejemplo, si colocamos int(3.0) nos
devolverá 3. Por el contrario, si escribimos float(3) nos devolverá 3.0, es importante notar que al convertir decimales a
enteros, se trunca la parte decimal y se queda únicamente con el entero, ya sea negativo o positivo, es decir, no redondea
solo pierde la información.

Al hacer operaciones aritméticas con distintos tipos de números, Python conservará siempre los float, a menos que todos sean
enteros
o el resultado salga entero. Esto es válido para todas las operaciones aritméticas que vimos antes, salvo la división. La
división siempre devolverá float. Una recomendación, es hacer divisiones con al menos un float, pues las versiones viejas de
Python cortan los decimales al hacer divisiones entre enteros.

Para calcular la potencia N-ésima de un número podemos escribir 3**N, pero tambien podemos escribir pow(,) en donde el primer
argumento será el número, y el segundo la potencia a la que queremos elevar.

La función max() y min() calculan el máximo y mínimo de una colección de argumentos numéricos que le de demos, por ejemplo:
max( 5 , 3 , 9 , 4 )

La función round( número ) nos sirve para redondear un float, lo convierte a un entero. Podemos indicarle a round() un
segundo parámetro con la sintaxis round( numero , cifras ) eligiendo el número de decimales que queremos que devuelva.

_________________________________________________________________________

*****3.2- Números complejos en Python*****

Los complejos pueden representarse en Python utilizando la forma binómica, sin embargo, en lugar de i, Python usa j. Para
representar un complejo, hay una sintaxis precisa
z = 2 + 3j
nota que no escribimos el * para la multiplicación es decir, no podemos escribirlo como 2+3*j. Python tambien nos permite
escribirlo en el otro orden natural, es decir z = 3j + 2 , python no se quejará. Sin embargo, debemos escribir siempre los
coeficientes de manera precisa, por ejemplo, en Python no podemos escribir 2-j, debemos colocar 2-1j.

También podemos escribirlo como z = complex( 2 , 3 ) (esto define directamente a z como un objeto de la clase complex)

Para calcular las partes reales e imaginarias de un complejo, podemos escribir z.real y z.imag respectivamente, como si
se tratara de funciones de una paquetería (en relidad son atributos de la clase complex definidos en el constructor, más
adelante veremos esto)

Las cuatro operaciones aritméticas básicas se escriben de la manera usual.

Para el conjugado escribimos z.conjugate() y para el módulo usamos la función abs() como el valor absoluto en un real.

Para calcular el argumento será necesario llamar la librería cmath y usar la función phase(), es decir
import cmath
cmath.phase( z )
donde la fase está en radianes y va de -pi a pi
Para pasar de la forma binómica a la polar una vez corrida la librería cmath, usamos la función polar(), es decir
cmath.polar( z )

Para hacer el regreso usamos la función rect() pero en el argumento debe haber la dupla del módulo y del argumento
por lo que podemos escribir
cmath.rect( abs( z ) , cmath.polar( z ) )

_________________________________________________________________________

*****3.3- Introducción a strings*****

Es importante no colocar cadenas con acentos, pues Python tiene problemas con ellos, generalmente cuando se realizan scripts
y se corren en distintos sistemas operativos.

Para podes colocar comillas dentro de un string, es necesario alternar las comillas dobles y las simples, por ejemplo,
si queremos citar palabras de alguien podemos escribir:
s = "Juan Gabriel dijo: 'Hola amigos' "

Dentro de los strings, es posible colocar caracteres especiales conocidos como caracteres escapantes, enlistamos los más
usados, pero se puede consultar la documentación en el archivo asociado a la clase en el siguiente enlace:
https://docs.python.org/3.7/reference/lexical_analysis.html#string-and-bytes-literals
\\ Backslash \
\' Comilla simple '
\" Comilla doble "
\n Salto de línea
\t Tabulación horizontal
Es parecida a la sintaxis de LaTeX.

SPOILER: Con la función print() podemos imprimir en pantalla una variable que contenga una cadena.

Otra forma especial de definir un string es a través de dos pares de triples comillas dobles, por ejemplo:
cadena = """Esta es una cadena
Con más de un renglón
"""
esta manera de definir strings es común cando queremos mostrar un menú o algún texto de forma ordenada, además de que nos
ahorra definir varios renglones por separado. También se usará más adelante en el contexto de la documentación en Python,
las funciones y las clases.

_________________________________________________________________________

*****3.4- Operaciones con strings*****

Para concatenar strings podemos sumarlos, pero ojo, los concatenará completamente como se encuentran, habrá que controlar
bien los espacios. Por ejmplo:
s1 = "Hola "
s2 = "Juan."
s1 + s2
notemos que el número de espacios se conserva tal cual como lo escribimos sin comillas.

Para repetir un string, por ejemplo, 5 veces, podemos multiplicar una cadena con un entero, por ejemplo:
z = "¿Falta mucho? "
5*z
_________________________________________________________________________

*****3.5- Impresión en pantalla*****

La función print() sirve para cualquier tipo de dato, no solo cadenas.

Podemos concatenar cadenas y variables cadenas dentro de la función print(). Por ejemplo:
name = "Pepito "
print( "Hola Don " + name + "¿Cómo está?" )
nota que, al colocar cadenas dentro de la función print(), podemos usar caracteres especiales sin ningún problema,
no como vimos antes, en donde colocarle acentos o caracteres especiales a una variable nos puede traer problemas.

En lugar de concatenar cadenas con el símbolo + dentro del print, podemos separarlos con comas como si se tratara de
argumentos, por ejemplo:
name = "Pepito"
print( "Hola Don" , name , "¿Cómo está?" )
con esta sintaxis, no será necesario poner espacios dentro de las cadenas, las comas le indican a la función print()
que habrá que separar comas.

También se puede mezclar. Por ejemplo:


name = "Pepito"
print( "Hola Don " + name , "¿Cómo está?" )
También se puede repetir una cadena dentro de la función print(), multiplicando con un entero una cadena, como vimos
antes.

Si queremos imprimir dos líneas en una sola, podemos colocar end = " " en un segundo argumento de print, esto hará que queden
separadas solo por un espacio:
s = "Gerardo González García"
print( "Hola, mucho gusto," , end = " " )
print(s)

_________________________________________________________________________

*****3.6- Casteo y formateo de strings*****

La función str() convierte una variable a string.

La función .format() se le puede agregar a una cadena que contenga parejas de llaves, para rellenar su contenidose le
coloca inmediatamente después de una cadena y como argumentos se colocan las variable que rellenarán los espacio, por
ejemplo:
nombre = "Ricardo"
n_gatos = 3
print( "Mi abuelo se llama {} y tiene {} gatos".format( nombre ,n_gatos ) )
nota no importa el tipo de las variables, pues nombre es string y n_gatos es numérica

Esta función .format() es una mejora de la escritura de Python 2. En Python 2 podemos formatear cadenas colocando una f
antes de la cadena y los argumentos que queremos mostrar entre llaves. El ejemplo anterior se veería:
nombre = "Ricardo"
n_gatos = 3
print( f"Mi abuelo se llama { nombre } y tiene { n_gatos } gatos" )
esto se puede hacer igualmente en Python 3. Mucha gente sigue usando esta sintaxis, es más cuestión de gustos.

_________________________________________________________________________

*****3.7- Slicing en strings*****

Podemos manipular substrings o subcadenas de la siguiente forma. Definimos una variable string y entre corchetes
colocaremos ciertos números de las posiciones de los caracteres de la cadena:
s = "Soy fanático de zelda"
s[ 0 ] #Nos dará la 'S'
s[ 5 ] #Nos dará 'a'
nota que se empieza a contar desde el cero.

También podemos colocar negativos, esto indica comenzar del final y contar hacia atrás, es decir:
s[ -1 ] #Nos dará 'a'

Podemos recuperar cadenas más grandes con la siguiente sintaxis:


s[ 4 : 7 ] #Nos imprime 'fan', la última posición, en este caso el 7mo, nunca se escribe

Podemos escribir ciertas letras, saltándo a través de la cadena, para ello necesitarémos un tercer argumento:
s[ 4 : 14 : 2 ] #Nos imprime 'fntc e' pues va saltando de dos en dos según el tercer argumento

Si queremos imprimir del elemento cero al n-1 elemento colocamos s[ : n ]


Si queremos imprimir del elemento n hasta el final colocamos s[ n : ]
Lo análogo se puede hacer con negativos

Podemos invertir la cadena seleccionando toda la cadena con :: pero indicando el salto hacia atrás -1:
s[ : : -1 ]
_________________________________________________________________________

*****3.8- Métodos de strings*****

Para transformar todo el texto a minúsculas, podemos usar el método .lower() después de una variable tipo string. Para
transformar todo a mayúsculas utilizamos el método .upper(), por ejemplo s.upper()

Para contar el número de caracteres de un mismo tipo o subcadenas que aparecen en un string s, colocamos la terminación
.count(), es decir, s.count( caracter ).

Para convertir la primera letra de un string a mayúscula, usamos la terminacion .capitalize()


Para convertir a mayúscula el primer caracter de cada palabra de un string, usamos la terminación .title()
Para transforma de mayúsculas a minúsculas en un string y viceversa, se escribe la terminación .swapcase()

Podemos remplazar una subcadena por otra con la terminación .replace(,) cuyos argumentos son la subcadena a quitar y la
subcadena a colocar respectivamente, por ejemplo:
s = "Los villanos son buenos"
s.replace( "buenos" , "malos" )
Para quitar un caracter o palabra y seccionar un string se usa el método .split() y lo convierte en una lista (array),
donde el argumento es la letra o subcadena que queremos eliminar.
cadena = "El día de hoy iré a la facultad a darlo todo en mis clases"
cadena.split( " " )
Esto es bastante usado para tomar las palabra que contiene un texto, esto se logra quitando los espacios escribiendo
s.split(" ")

Para quitar los espacios sobrantes al inicio y al final de un string usamos la terminación .strip(), no lleva argumento
Para quitar los espacios al final de in string usamos la terminación .rstrip() y para los del principio .lstrip()

Para calcular la posición en la que una letra o subcadena aparece, usamos la terminación .find() donde el argumento es la
subcadena o caracter que buscas.
cadena = "El día de hoy iré a la facultad a darlo todo en mis clases"
cadena.find( "a" ) #Nos devuelve 5
A esta terminación podemos agregarle otros más argumentos. Podemos agregarle solo uno, en este caso
.find( string , nempieza ), en este caso el segundo argumento significa dónde comienza la búsqueda. Si le ponemos dos
argumentos, es decir .find( string , nempieza , ntermina ), el segundo parámetro agregado nos indicará dónde queremos que
termine la búsqueda.
Si .find() no encuentra el caracter, devolverá -1.

Un método idéntico a .find() es el método .index() que funciona exactamente igual que .find(). La diferencia radica en que,
si no se encuentra al caracter deseado, index marca error, pero find marca -1

La terminación .rindex(), calcula la última posición en donde se encuentra una cadena o un caracter, similar a .index() y
.find(), también se le pueden agregar los dos argumentos extra que mencionamos para .find() y tiene exactamente la misma
función.

Para calcular la cantidad de caracteres de una cadena, usamos la función len() donde el argumento es una variable string:
cadena = "El día de hoy iré a la facultad a darlo todo en mis clases"
len( cadena ) #Nos devuelve 58

Para que un usuario introduzca un string por consola, usamos la función input(). Para utilizarla, hay que asignar input()
a una variable nueva, en la que se guardará la cadena que el usuario teclee. Es importante cque, cuando necesitemos que el
usuario introduzca un número que operaremos, la convirtamos en un entero o en un flotante con las funciones int() o float()
respectivamente. Esta acción se puede hacer en una sola línea, es decir, pedirle al usuario y convertirlo al mismo tiempo.
Para ello escribimos:
n = float( input() )
esta función no lleva argumento.
El riesgo de hacer la conversión simultánea, es que si no se introduce un número, la conversión marcará un error.

En el argumento de input podemos colocar una cadena para indicar al lado izquierdo, la descripción del dato que
está introduciendo.
s = float( input( "Nombre: " ) )

En el tema de excepciones veremos como evitar los errores al introducir un dato de tipo equivocado, mucho más
adelante.

Si queremos pedir datos al usuario y mostrar algo con ellos, podemos ahorrarnos líneas de código introduciendo la
función input a un print, por ejemplo:
print( "Hola " + input( "¿Cuál el tu nombre?: " ) + " " + input( "¿Cuál es tu apellido: ?" ) )

_________________________________________________________________________

*****3.9- Booleanos y operadores lógicos*****

En Python, las variables booleanas deben escribirsae con la primera letra en mayúscula, es decir, True y False.

En Python, los operadores lógicos clásicos se escriben "or", "not", "and" y siguen las tablas de verdad correspondientes
ya conocidas. Los condicionales los veremos más adelante.

Para obtener valores booleanos, existen los siguientes operadores de comparación en Python:
>
>=
<
<=
==
!= #Este operador se lee "es distinto de"
pueden usarse con valores numéricos, cadenas o booleanos, en las cadenas se seguirá el orden lexicográfico pero
en los booleanos no tiene mucho sentido.
_________________________________________________________________________

*****3.10- Métodos booleanos en strings*****

La terminación .startswith( "cadena" ) se le aplica a un string y nos revela si el string comienza con la letra
o la cadena en el argumento, devolviéndonos un True o un False.
Lo análogo se sigue para la terminación .endswith( "cadena" )
La terminación .isalnum() sin argumento y aplicado a una cadena, nos dice si una cadena está compuesta únicamente
de caracteres alfanuméricos, es decir, letras y números, devolviéndonos un True o un False. Es importante mencionar
que un espacio no es considerado un caracter alfanumérico.

La terminación .isalpha() hace lo mismo pero solo con letras.

La terminación .isdigit() hace lo mismo pero solo con dígitos

La terminación .isspace() hace lo mismo pero solo si son espacios en blanco.

La terminación .islower() hace lo mismo pero si solo hay minúsculas

La terminación .isupper() hace lo mismo pero solo si hay mayúsculas.

La terminación .istitle() hace lo mismo pero solo si todas las palabras de la cadena comienza con mayúscula y el resto
en minúscula

Nota que estas terminaciones nos servirán como filtros en nuestros programas. Todas devlverán un booleano y están
aplicados a cadenas.

Finalmente, el operador "in" se aplica a dos cadenas y nos dice si la primera cadena es una subcadena de la segunda,
devolviendo un True o un False

___________________________________________________________________________________________________________________________

Capitulo 4: Condicionales y bucles

*****4.1- Estructura condicional (if-elif-else)*****

En Python hay varios operadores de decisión. El primero es la palabra if. Se coloca un if seguido de una condición
que nos devuelva un booleano y luego dos puntos. Después, indentada se coloca la consecuencia. Por ejemplo
if ( n > 5) and ( n < 15 ):
print( n + 1 )
si la condición es True, se ejecuta la linea indentada, si no lo es, simplemente no ejecuta nada y sigue el programa.

Para deshacer tabulaciones podemos presionar Shift + Tab

En caso de que necesitemos que se ejecuten ciertas cosas si da False o True en un if, podemos colocar la palabra else a la
misma altura del if, seguida de dos puntos. Enseguida se coloca la consecuencia que ejecuta el else en caso de que el valor
del if de False.
if ( n > 5) and ( n < 15 ):
print( n + 1 )
else:
print( "No se cumplió la condición" )

Si necesitamos varias condiciones, podemos colocar la palabra reservada elif antes de else. Podemos colocar tantos elif
como necesitemos, todos a la misma altura del if. De igual manera lleva dos puntos después de la condición y su ejecuición
indentada.
if ( n > 5) and ( n < 15 ):
print( n + 1 )
elif n > 20
print( "fue mayor a 20" )
else:
print( "No se cumplió la condición" )

Una manera de simplificar un if seguido de un else en una sola línea, se puede escribir con la siguiente sintaxis:
(acción ppal) if (condición) else (acción si no se cumple)
nota que no requiere dos puntos y todo va en una línea. Python es el único lenguaje que permite esto. A esta sintaxis se
le conoce como el "operador ternario". Tomando en cuenta el ejemplo con el else:
print( n + 1 ) if ( ( n > 5 ) and ( n < 15 ) ) else print( "No se cumplió la condición" )
_________________________________________________________________________

*****4.2- Bucle while*****

La idea de los bucles es automatizar tareas repetitivas sin hacerlas a mano, la pc hará todo el trabajo.

El primero de los bucles que veremos es el bucle "while" (mientras en inglés). Este bucle ejecutará instrucciones mientras
una condición sea cierta. La sintaxis del while es:
while (condición verdadera):
condición 1
condición 2
:
:
condición n
es importante modificar la variable que está en la condición para que el bucle acabe, si no, será un bucle infinito, aunque
después veremos como detener un bucle infinito. Por ejemplo, el siguiente bucle imprime los números del 1 al 100:
i = 0
while i < 101:
print( i )
i += 1

Existe una palabra reservada que puede detener el bucle while desde dentro de sus ejecuciones, la palabra "break". Muchas
veces se utiliza para realizar un proceso iterativo y detenerlo cuando ejecute a cierta acción que busquemos hacer. Por
ejemplo, el siguiente código busca los números divisibles entre 13 en un intervalo cuyos extremos proporciona el usuario:

print( "Este programa calcula el menor número divisible entre 13 en un intervalo dado que usted proporcione." )
print( "Escriba dos número enteros positivos, el primero menor que el segundo." )
a = int( input( "Primer número: ") )
b = int( input( "Segundo número: ") )
i = a

while i <= b :
if i % 13 == 0 :
print( "El primer número divisible en el intervalo ({},{}) es: {}".format( a , b , i ) )
break
i += 1

if i == b + 1 :
print("No hay ningún número divisible entre 13 en el intervalo que ha seleccionado.")

El while puede ser interpretado como un if que se repite hasta que la condición sea falsa, por ello en Python hay una
variación del while usando un else, esta sirve para ejecutar una linea de código una vez que finaliza el while. El siguiente
programa realiza una cuenta regresiva desde el 10 y al terminar lo anuncia:
i = 10
while i >= 0 :
print( i )
i -= 1
else:
print("Cuenta regresiva finalizada.")

La función str() nos convierte un número a un caracter ASCII, ord() hace lo contrario, cuyas equivalencias puedes encontrar a
continuación:
https://elcodigoascii.com.ar/
El siguiente programa rota el abecedario n lugares, donde n es un valor que se le pide al usuario
n = int( input( "Introduce una rotación: " ) )
i = 65
while i <= 90:
if i + n <= 90:
print( chr( i ) + ": " + chr( i + n ) )
else:
print( chr( i ) + ": " + chr( ( i - 26 ) + n ) )
i += 1
_________________________________________________________________________

*****4.3- Bucle for*****

Otro bucle de Python es el bucle "for", en este caso el bucle será un bucle finito, a diferencia del bucle while. Su sintaxis es
la misma que
while:
for (recorrido de iterador):
condición 1
condición 2
.
condición n
este comando se usa más con arreglos que veremos más adelante. Por ahora veremos una variante cos strings.

Podemos ir seleccionando cada caracter de un string mediante el ciclo for definiendo una variable que solo se usa
dentro del ciclo e indicandole la cadena donde queremos que muestre los caracteres. Esto con la siguiente línea:
for c in (string)
esto seleccionará uno por uno los caracteres del string. Por ejemplo, para imprimir por separado cada letra de
una palabra se puede escribir el siguiente código:
s = "Pepe pecas pica papas con un pico"
for c in s:
print( c )
nota que esto es equivalente a escribir:
s = "Pepe pecas pica papas con un pico"
for i in range( len( s ) ) :
print( s[ i ] )
(Ver siguiente clase para range())
Para invertir un string podemos escribir:
s = "Pepe pecas pica papas con un pico"
si = ""
for c in s:
si = c + si
print( si )

_________________________________________________________________________

*****4.4- Iterador range*****

Para iterar instrucciones con un for con valores definidos, es posible usar la función range( comienza , termina , paso )
después del in, es bastante uútil para ahorrar código, aunque podríamos hacer un proceso similar con un while. Por ejemplo
el siguiente código imprime los números del 1 al 10:
for i in range( 1 , 11 , 1 ) :
print( i )
nota que el argumento que termina jamás se toma.

También se le puede aplicar un solo argumento a range, esto Python lo interpreta como un argumeto de parada, con un inicio en cero
por defecto y un paso de 1 por defecto. Por ejemplo, el siguiente programa nos imprime los números del 0 al 10:
for i in range( 11 ):
print( i )

Si le colocamos dos argumentos a range(,), Python interpreta el inicio y el final pero coloca el salto como 1 por defecto.

Si queremos que se corra el índice al revés, basta poner el paso negativo y los argumentos en orden descendiente o jamás se
detiene.
_________________________________________________________________________

*****4.5- Continue en bucles*****

El comando "continue" permite cortar una sola iteración de un bucle, ya sea un for o un while, por ejemplo, si queremos
imprimir los números del 1 al 20 que no son divisibles entre 3 y 5 escribimos el programa:
for i in range( 1 , 21 , 1 ) :
if ( ( i % 3 == 0 ) or ( i % 5 == 0 ) ) :
continue
print( i )

Los bucles también se pueden anidar, por ejemplo, el siguiente programa imprime las tablas de multiplicar:
for i in range( 1 , 11 , 1 ) :
print( "\nTabla de multiplicar del {}.".format( i ) )
for j in range( 1 , 11 , 1 ) :
print( "{} x {} = {}".format( i , j , i * j ) )
__________________________________________________________________________________________________________________________________
__________________

Capítulo 5: Estructuras de datos

*****5.1- Listas*****

En Python existe un tipo de objeto conocido como lista, objetos que pueden guardar distintos tipos de objetos
simultáneamente en un sola variable y no crear muchas. Para crear una lista, definimos una variable y entre corchetes
separados por comas se colocan los elementos. Por ejemplo:
l = [ "Hola" , 2 , 2.558 , True ]
donde nuevamente, las posiciones comienzan con 0 como en las cadenas.

Para calcular la longitud de una lista usamos nuevamente la función len()

Al igual que las cadenas, para llamara un elemento de una lista, colocamos el nombre seguido con un par de corchetes que en
su interior contienen un número, a saber, el elemento de la lista. Siguiendo con el ejemplo de la clase anterior:
print( l[ 2 ] )
los elementos serán guardados con el mismo tipo

Por supuesto podemos colocar índices negativos para que cuente desde el final hacia atras.

Si queremos mostrar una sublista de de una lista l, podemos recuperar nuevamente las sintaxis l[ n : m ], l[ : m ] y l[ n : ].
Recuerda que jamas se toma el valor final.

Si queremos modificar una entrada de una lista, simplemente la definimos, nuevamente tomando el ejemplo de la clase anterior:
l[ 0 ] = "Adiós"
a esta propiedad de definir las entradas simplemente igualando, se le llama "mutabilidad"

Si queremos añadir un nuevo elemento al final de una lista usamos el método .append( elemento ), cuyo argumento es el
elemento que deseamos colocar al final:
l.append( "Hola k ase" )
entonces la lista quedará l = [ "Adiós" , 2 , 2.558 , True , "Hola k ase" ]
Si queremos añadir una entrada a la lista que no sea necesariamente al final, usamos el método .insert( posición , elemento )
cuyos argumentos son la posición y el elemento a colocar. Los demás elementos incluyendo al
original de la posición seleccionada, serán desplazados a la derecha.

Al igual que en las cadenas, podemos simplificar los ciclos for, solo colocando una varible que en este caso irá recorriendo
entrada por entrada de la lista. Por ejemplo, el siguiente programa imprime los elementos de una lista:
l = [ "Hola" , 1 , 2 , 3 ]

for c in l:
print( c )
nota que también podiamos haberlo escrito como:
l = [ "Hola" , 1 , 2 , 3 ]
for i in range( len( s ) ) :
print( l[ i ] )

La primera tiene la ventaja de escribir menos código, pero la segunda tiene el control sobre la posición del arreglo, que
será importante cuando queremos operar con la posición de un arreglo.
_________________________________________________________________________

*****5.2- Operaciones de listas y listas anidadas*****

Para concatenar dos listas simplemente las sumamos, aparecerán en el orden en que sumamos. De igual manera podemos
multiplicar listas por enteros para repetirlas en lugar de concatenarlas varias veces, algo parecido a los strings.

Es común que en ciertos programas se usen listas vacías que conforme avanza el progrma se van llenando, al agregar elementos
con el método .append(). Las listas vacías se crean simplemente no colocando elementos dentro de una lista:
l = []

La listas se pueden anidar, es decir, hacer listas que contienen otras listas como elementos. Por ejemplo:
l = [ "Hola , [ 2 , 4 , 6 , 8 ] , [ 15 , a , [ 9 , 2 , 1 ] ] ]
para que podamos distinguir fácilmente los elementos es común escribirla como:
l = [ "Hola ,
[ 2 , 4 , 6 , 8 ] ,
[ 15 , a , [ 9 , 2 , 1 ] ] ]

Para llamar una entrada de una entrada, simplemente colocamos los índices que corresponden al elemento que queremos pero en
orden, por ejemplo en la lista anterior l[ 1 ][ 3 ] corresponde al número 8.

_________________________________________________________________________

*****5.3- Métodos de listas*****

El método .count( elemento ) cuenta la cantidad de veces que se encuentra su argumento en una lista. El siguiente
programa cuenta la cantidad de elementos que hay en una lista. Para ello, rellena una lista vacía con los elementos
diferentes y luego los imprime:
l = [ 1 , 1 , 2 , 3 , 3 , 3 , 3 , 4 , 4 , 4 ]
lvacia = []

for c in l:
if c not in lvacia:
lvacia.append( c )
print( "El elemento {} se encuentra {} veces en la lista.".format( c , l.count( c ) ) )

El método .extend(), nos permite extender la lista con hasta objetos iterables. Por ejemplo, el siguiente programa
es similar al .append(), y extiende la lista con un 6.
l = [ 1 , 2 , 3 , 4 , 5 ]
l.extend( [ 6 ] )
nota que sí hay que agregar corchete. El siguiente extiende la lista con dos elementos:
l.extend( [ 7 , 8 ] )
el siguiente la extiende aún más iterando con un range, hasta el 15:
l.extend( range( 9 , 16 ) )

El método .index() nos devuelve la posición en la que su argumento se encuentra por primera vez en la lista.

El método .pop() sin argumento, es lo contrario al .append(), en este caso elimina el último elemento de una lista y
lo devuelve por si se quiere guardar en una variable. Es decir:
l = [ 1 , 2 , 3 , 4 , 6 , 6 , 7 , 8 , 9 ]

elemento_eliminado = l.pop()
print( "El elemnto {} ha sido eliminado de la lista".format( elemento_eliminado ) )

El método .remove( elemento ) elimina el elemento que se coloca en el argumento la primera vez que se encuentra y
recorre los demás hacia la izquierda.

El método .reverse() sin argumento, invierte el orden de una lista.


El método .sort() sin argumento, ordena números y cadenas de una lista de modo creciente, aunque deben ser de un solo
tipo , o son números , o son cadenas pero no ambos.

Se puede mezclar un sort con un reverse para ordenar de manera decreciente, para ello podemos colocar un argumento al
.sort() en donde vendrá involucrado el .reverse(). En lugar de llamar a cada terminación por separado en las versiones
actuales de Python puede escribirse:
l.sort( reverse = True )
si no queremos el orden descendente podemos colocar False, eso sirve para activar o desactivar el orden ascendente.

El siguiente programa le pide al usuario una letra de una lista de letras y el programa las quita todas de la lista
original luego imprime la lista actualizada:
l = [ "s" , "d" , "a" , "c" , "t" , "a" ]
letra = input( "Introduzca la letra que quiere eliminar: " )
for c in l:
if c == letra :
l.remove( c )
print( l )

Finalmente, aunque no es un método de listas propiamente, hay un método que nos permite juntar los strigns de una lista, el
método .join( lista ). Este método se le aplica a un string que servirá como separador de las palabras de la lista, por
ejemplo:
lista = [ "Hola" , "Amigo" ]
cadena = " ".join( lista )

print( cadena )

La función bool( cualquiercosa ) devuelve un valor boolean al introducir cualquier objeto dentro del argumento, si no hay
nada devuelve un False

La función max() y min() nos permiten encontrar el máximo de una lista de la siguiente forma:
lista = [ 2 , 5 , -1 , 75 , 12 ]
maximo = max( lista )
print( maximo )
Si en lugar de números colocamos solo cadenas, Python nos dará el máximo y mínimo usando el orden lexicográfico.

_________________________________________________________________________

*****5.4- Range y casteo a listas*****

la sintaxis range(,,) e un tipo "range",es un tipo de objeto iterable (es decir, que s epuede recorrer con un for), si lo
calculamos con type() nos dirá que es tipo "range", no tiene elemento alguno, no es una lista como tal, pero podemos
convertirlo a una lista mediante la función list(). Podemos convertir un range a una lista con la sintaxis
list( range(,,) )
_________________________________________________________________________

*****5.5- Matrices con listas*****

Podemos definir listas anidadas, por ejemplo.


l = [ [ 1 , 2 , 3 , 4 ] , [ 5 , 6 ] , [ 9 , 8 , 7 ] , [ 10 , 11 , 12 , 13 , 14 ] ]
para accder a sus elementos colocamos dos veces los corchete, por ejemplo l[ 0 ][ 3 ] nos devolverá 4.

Es común escribirlo con una sintaxis más extensa, sacrificando un poco deespacio en el código, por comprensión:
l = [ [ 1 , 2 , 3 ] ,
[ 4 , 5 , 6 ] ,
[ 7 , 8 , 9 ] ]

La matrices son tipos especiales de listas anidadas, donde cada sublista(renglón) tiene la misma cantidad de objetos
(columnas). Hay muchas formas de crear matrices, pero la más sencilla es a través de listas. Cada subarreglo representará una
fila. Hay que tener mucho cuidad, y recordar que la primera entrada no sería la 1,1, sino la 0,0, pues las listas comienzan
con el índice 0.

Para imprimir la matriz en la forma usual, podemos simplemente imprimir las entradas:
l = [ [ 1 , 2 , 3 ] ,
[ 4 , 5 , 6 ] ,
[ 7 , 8 , 9 ] ]

for c in l :
print( c )

Para imprimir todos los elementos seguidos de una matríz, podemos escribir:
l = [ [ 1 , 2 , 3 ] ,
[ 4 , 5 , 6 ] ,
[ 7 , 8 , 9 ] ]

for c in l :
for i in c :
print( i )

Podemos imprimir la matríz sin los corchetes podemos escribir:


m = len( matrix ) # m es el número de filas y n, el de columnas
n = len( matrix[ 0 ] )
for i in range( m ) :
for j in range( n ):
print( matrix[ i ][ j ] , end = " " )
print( "" ) # el caracter "" forza a Python a dar un salto de línea

Otra forma sin usar las dimensiones es:


for row in matrix:
for element in row:
print( element , end = " " )
print( "" )
_________________________________________________________________________

*****5.6- Suma y producto de matrices con listas*****

El siguiente programa le pide las dimensiones al usuario de dos matrices, luego le pide las entradas y las suma.
Muestra tanto las matrices iniciales como el resultado:
print( "Este programa suma dos matrices. Introduzca las dimensiones de las matrices a continuación." )
n = int( input( "Número de filas: " ) )
m = int( input( "Número de columnas: " ) )

l1 = []
l2 = []
suma = []

print( "Entradas de la primera matríz" )


for i in range( n ) :
l1.append( [] )
for j in range( 0 , m ) :
l1[ i ].append( int( input( "Entrada {},{}: ".format( i , j ) ) ) )

print( "\nEntradas de la segunada matríz: " )


for i in range( n ) :
l2.append( [] )
for j in range( m ) :
l2[ i ].append( int( input( "Entrada {},{}: ".format( i , j ) ) ) )

print( "\n" )

for c in l1 :
print( c )

print( "\n" )

for c in l2:
print( c )

print( "\n" )

for i in range( n ) :
suma.append( [] )
for j in range( m ) :
suma[ i ].append(l1[ i ][ j ] + l2[ i ][ j ] )

print("La suma es: ")

for c in suma:
print(c)

El siguiente programa le pide al usuario dos matrices y calcula su producto, pero primero le pide sus dimensiones.
print( "Este programa multiplica dos matrices. Introduzca las dimensiones de las matrices a continuación." )
n = int( input( "Número de filas de la primera: " ) )
m = int( input( "Número de columnas de la primera: " ) )

p = int( input( "Número de filas de la segunda: " ) )


q = int( input( "Número de columnas de la segunda: " ) )
l1 = []
l2 = []

producto=[]
if m == p :
print( "Entradas de la primera matríz" )
for i in range( n ) :
l1.append( [] )
for j in range( 0 , m ) :
l1[ i ].append( int( input( "Entrada {},{}: ".format( i , j ) ) ) )

print( "\nEntradas de la segunada matríz: " )


for i in range( p ) :
l2.append( [] )
for j in range( q ):
l2[ i ].append( int( input( "Entrada {},{}: ".format( i , j ) ) ) )

print( "\n" )

for c in l1:
print( c )

print( "\n" )

for c in l2:
print( c )

print( "\n" )

for i in range( n ):
producto.append( [] )
for j in range( q ):
ent = 0
for k in range( m ):
ent = ent + l1[ i ][ k ]*l2[ k ][ j ]
producto[ i ].append( ent )

print("La producto es: ")

for c in producto:
print( c )
else:
print( "El producto de sus matrices no está definido" )

________________________________________________________________________

*****5.8- Diccionarios*****

Los diccionarios son elementos parecidos a las listas, que no tienen orden y se construyen con llaves en lugar de corchetes.
Para acceder a sus elementos no se usa posición sino una "clave" que pueden ser strings o cantidades, separados del valor que
guardan por dos puntos. Por ejemplo:
l = { "primero":7 , "segundo":10 , "pato":-2 }
si queremos el elemnto -2 tenemos que invocarlo mediante la clave "pato" de la siguiente forma:
l[ "pato" ]

La claves deben ser únicas, y si se llega a repetir una, el diccionario guarda el último valor asignado a la clave.

Los elementos guardados en un diccionario pueden ser de cualquier tipo, strings, float, int, bool, listas e incluso otros
diccionarios

Para modificar los elementos de un diccionario, simplemente se invoca una clave y con el operador de asignación es suficiente.
Es decir, los diccionarios son mutables al igual que los diccionarios.
l[ "pato" ] = 20

Podemos generar más valores en un diccionario simplemente definiendo una nueva clave y su valor que le vamoa a almacenar, por
ejemplo:
l[ "tomate" ] = -256

Una forma cómoda de acomodar los elementos de un diccionario es como ya habíamos visto en el caso de listas:
l = { "primero":7 ,
"segundo":10 ,
"pato":-2 }

Para imprimir un diccionario, nos basamos nuevamente den la función print() y escribimos print(l). Podemos escribir solamente
un valor asignado a una clave en particular colocando el nombre del diccionario y la clave entre corchetes, al igual
que en las listas, por ejemplo l[ "segundo" ]

Cuando escribimos una clave que no exixte, nos mandará un error.

Podemos obtener una lista con las claves de un diccionario con el método .keys() sin argumento. Por ejemplo. keys()
De manera análoga podemos obtener una lista con los valores guardados con el método .values(). Podemos obtener una
lista con tuplas que contienen la clave y el valor, a través del método .items()

Si un elemento guardado es una lista, simplemente colocamos ambos pares de corchetes para mencionar la clave y la entrada
de la lista, por ejemplo l[ "segundo" ][ 2 ]

Podemos crear diccionartios vacíos simplemente escribiendo un par de llaves e ir llenando el diccionario pidiendo datos
al usuario definiendo una clave. Por ejemplo:
l = {}
l[ "Nombre" ] = input( "Introduzca su nombre: " )
l[ "Edad" ] = int( input( "Introduzca su edad: " ) )
l[ "Género" ] = "mujer" if input( "Si es mujer introduzca f , si es hombre introduzca h: " ) == "f" else "hombre"
print( l )

Para calcular la cantidad de objetos de un diccionario, usamos la función len(), es decir, las parejas clave-valor.
_________________________________________________________________________

*****5.9- Iterando en diccionarios*****

Podemos usar bucles en los diccionarios, para ello podemos usar el for e indicar cada variable como la clave, es decir:
for c in l:
print( c , ":" , l[ c ] )
es decir, al iterar con un for, se van tomando las claves, no los valores.

Otra forma es utilizar el método .items() sin argumento, esto convertirá nuestro diccionario en una lista de tuplas y
ahora si usar un for sobre esa lista, por ejemplo, para imprimir las tuplas:
for c in l.items():
print( c )

Podemos separar las tuplas de la lista .items() con un bucle con dos índices que se recorren simultáneamente, es decir,
usando el método unpacking, por ejemplo:
for k , v in l.items():
print( k , ":" , v )

_________________________________________________________________________

*****5.10- Métodos de diccionarios*****

Para borrar un diccionario y dejarlo vacio, se utiliza el método .clear() sin argumento.

Para copiar un diccionario y guardarlo en una nueva variable, usamos la terminación .copy() sin argumento. Este tipo de
comandos es útil, porque al aplicar ciertos métodos sobre un diccionario, lo modificarán de manera permanente, lo cual no
siempre es lo que queremos. Por ello puede usarse .copy() para clonar un diccionario y usar esa copia sin alterar el original.

Para construir un diccionario con a través de un iterable o lista con distintas llaves pero todos los valores iguales, se usa
el método dict.fromkeys( lista o elemento , lista o elemento ). Por ejemplo, el código:
l = dict.fromkeys( [ "a" , "b" , "c" , "d" ] , 2 )
nos devolverá un diccionario con claves a b c y todos guardan el valor 2. Si el valor deja un vacío nos devolverá un None.

El método .get( clave ) sirve para devolvernos el valor de la clave, pero cuando no existe la clave, nos devolverá un
None, no un error, e ahí su ventaja, no se detiene el programa, simplemente saca un None.

El método .pop( clave ) nos elimina el valor y la clave que elijamos y nos devuelve un valor del argumento que podemos
guardar, mientras que el diccionario queda ahora sin dicha clave y valor.

El método .popitem() sin argumento, nos elimina la última clave con su valor y nos devuelve una tupla con ellos.

El método .setdefault() que sirve como un get cuando tiene un solo argumento y nos devuelve el valor de la clave al igual que
el get, o para agregar un nuevo elemento cuando le ponemos dos argumentos l.setdefault( clave , valor )

El método .update( otro_diccionario )actualiza los valores de las claves repetidas y nos agrega los valores del
diccionario argumento al diccionario al que se aplica.

Aunque no es un método propio de diccionarios, la función max() y min() nos permiten encontrar el máximo de un diccionario
usando sus valores, Python nos devolverá la clave asociada al máximo o mínimo, por ejemplo:
diccionario = { "c1":10 , "c2":7 , "c3":12 }
maximo = max( diccionario )
print( maximo )

_________________________________________________________________________

*****5.11- Construcción de diccionarios*****

Podemos construir diccionarios con la función dict(), para ello hay varias maneras.

La primera es realizar construir una lista que tengan listas con dos elementos. El primero será la clave y el segundo el
valor asociado a la clave:
l = [ [ "x" , 1 ],
[ "y" , 2 ] ]
nuevo_diccionario = dict( l )

Otra forma es usar directamente la funció dict(), colocándole como argumentos las claves igualadas al valor que le queremos
asignar:
nuevo_diccionario = dict( x = 0 ,
y = 1 ,
z = 2 )
nota que no es necesario colocar las comillas a los nombres de las claves.

Otra forma es colocar un diccionario dentro de la función dict(). Pareciera no tener ningún sentido, pero se puede hacer.
nuevo_diccionario = dict( { "x" : 1 ,
"y" : 2 ,
"z" : 3 } )
la gracia de esto último es que se pueden combinar para generar un diccionario a partir de un diccionario dado:
viejo_diccionario = { "x" : 1 ,
"y" : 2 }
nuevo_diccionario = dict( viejo_diccionario ,
w = 7 ,
z = 0 )

_________________________________________________________________________

*****5.12- List y dict comprehensions*****

En ocasiones necesitamos crear una lista con ciertos elementos y esto generalmento lo hacemos con un for, por ejemplo, si
queremos crear una lista con los números del 1 al 100 elevados al cuadrado tales que el número original no sea
múltiplo de 3, podemos escribir el código:
numbers=[]

for i in range( 1 , 101 ) :


if i % 3 != 0 :
numbers.append( i**2 )

print( numbers )
ese código lo podemos acortar un poco usando una técnica conocida como list comprehensions que consiste en definir la lista
colocando el for dentro de la lista con la siguiente sintaxis:
lista = [ element for element in iterable if condition ]
la condición es opcional. En caso de nuestro ejemplo podemos simplemente escribir:
numbers = [ i**2 for i in range( 1 , 101 ) if i % 3 != 0 ]
print( numbers )

Podemos hacer lo mismo con diccionarios con la siguiente sintaxis:


lista = { key:value for value in iterable if condition }
Por ejemplo, un diccionario con los naturales por clave que no sean múltiplos de 3 y por valor el número al cubo se escribe:
my_dict = { i : i**3 for i in range( 0, 101 ) if i%3 != 0 }

_________________________________________________________________________

*****5.13- Conjuntos*****

Los conjuntos son otra clase de elementos nuevos en Python para definir uno podemos usar la sintaxis clásica de matemáticas
de las llaves {} y enlistar sus elmentos. Puede tener cualquier clase de elementos menos conjuntos y listas.

Es inmutable, es decir, no se pueden hacer modificaciones directas de los elementos de un conjunto como en los diccionarios.
Además no es un conjunto ordenado, es decir, no podemos acceder a sus elementos como en las listas o tuplas.

Otra forma de definir un conjunto con el inicio set() y como argumento podemos colocar una lista o una tupla
con los elementos, por ejemplo:
s=set( [ 1 , 2 , 3 , 4 , 5 ] )

Para crear un conjunto vacío hacemos:


s = set()

Los conjuntos, como en mates, no aceptan elementos repetidos, se pierden, además no tienen orden.

Para mostrar un conjunto nuevamente usamos print().

Una forma clásica de quitar los elementos repetidos de una lista es primero transformarla a un conjunto y posteriormente
transformarlo a una lista de nuevo.

Podemos verificar si un conjunto A es subconjunto de uno B y nos devolverá un booleano. Para ello usamos los símbolos <= o
< para la contención y la contención propia o A.issubset(B). Para la contensión inversa se usan los símbolos >=, > o
A.issuperset()
Las opreaciones de conjuntos se realizan con la siguiente sintaxis:
| o A.union( B ) para la unión
& o A.intersection( B ) para la intersección
A-B o A.difference( B ) para la diferencia
^ o A.symmetric_difference( B ) para la diferencia simétrica

Por suspuesto se puede usar el mísmo método de las list comprenhensions para construir conjuntos:
nuevo_diccionario = { i**2 for i in range( 101 ) if i%3 != 0 }

_________________________________________________________________________

*****5.14- Métodos de conjuntos******

Para añadir un elemento a un conjunto usamos el método .add( elemento ) cuyo argumento es lo que añadiremos.

Nuevamente podemos actualizar los elementos de un conjunto con el método .update( nuevocinjunto ) cuyo argumento
es una lista con el que se actualizará, es decir, agrega mas elementos.

Al método .update() se le pueden colocar varias listas o conjuntos al mismo tiempo, separándolos con coma como se hizo con
los diccionarios.

Podemos averiguar mediante un booleano si un valor está en un conjunto mediante el conector in. Por ejemplo
2 in s

Para eliminar elementos de un conjunto usamos los métodos .remove() o .discard() cuyos argumentos son el elemento
que queremos eliminar del conjunto. Si eliminamos un elemento de un conjunto que ya no existe, el .remove() da un error
pero .discard() no lo hará.

Para calcular el tamaño de un conjunto, nuevamente recurrimos a la función len( conjunto ).

Los bucles en los conjuntos se usan de la misma manera que en los diccionarios y listas. Por ejemplo, para escribir los
elementos de un conjunto, no respetando ningún orden:
for c in s:
print( c )

Otro método es la teminación .pop() que devuelve un elemento y lo elemina del conjunto, pero no controlamos cuál. Esto puede
ser útil para hacer extracciones aleatorias.

El método .clear() sin argumento, borra un conjunto y lo deja vacío.

Para crear un conjunto vacío escribimos set() sin argumento, y lo podemos guardar en una variable.

Para calcular el máximo de un conjunto de números, usamos la función max( conjunto ).

Podemos generar un conjunto casteanto un range a un conjunto, por ejemplo:


set( range( 5 , 100 ) )
_________________________________________________________________________

*****5.15- Tuplas*****

Las tuplas son arreglos de elementos inmutables pero que conservan el orden como en las listas. Pueden contener distintos
tipos de datos.

Para definirlas se escriben como vectores:


v = ( 2 , 3 , 4 , 5 )
aunque pueden definirse sin necesidad de los paréntesis aunque no es muy recomendable:
v = 2 , 3 , 4 , 5

También podemos declarar una tupla con la función tuple() cuyo argumento puede ser una tupla misma, una lista, un diccionario
o un conjunto:
v = tuple( [ 2 , 3 , 4 , 5 ] )

Para acceder a un elemento de una tupla, simplemente se pone el corchete como antes, es decir:
v = ( 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 )
v[ 0 ]

Al igual que en las lista podemos usar negativos, por ejemplo v[ -1 ]

También podemos usar : para un intervalo de valores, como en las listas, por ejemplo v[ 1 : 3 ] o sus variantes para los
valores finales e iniciales, v[ : 3 ] o v[ 3 : ]

Podemos también mezclarlo con negativos v[ -3 , -1 ]

Para saber si un objeto está en una tupla a través de un bool, usamos la función "in" que se usa mucho en los for:
for element in tuple:
print( element )

Las tuplas no pueden ser modificadas, pero podemos convertirlo primero a lista, modificarlo y luego reconvertirlo a una
tupla:
v = ( 2 , 3 , 4 , 5 )
v = list( v )
v[ 1 ] = 7
v = tuple( v )
print( v )
ahora en pantalla se verá que v = ( 2 , 7 , 4 , 5 ).

Para procersos con muchos datos no conviene usar tuplas, pues para modificarlos habrá que reconvertir y eso ocupan muchos
recursos, conviene mejor usar listas. Si se trata de pocos datos o de información
que debemos almacenar, conviene usar tuplas, pues suelen ser más ligeras que las listas.

_________________________________________________________________________

*****5.16- Forma de guardar variables (el método unpacking)*****

Hay una forma de definir variables que correspondan con entradas de una tupla. Esto es usado para no generar nuevas
variables, por ejemplo:
v = ( 2 , 3 , 4 , 5 )
( x , y , z , w ) = v
ahora x será 2, y = 3 y así...

De igual manera si no se declaran entre paréntesis:


v = ( 2 , 3 , 4 , 5 )
x, y, z, w = v

Si no nos interesa guardar todas, podemos agregar un *nombre para las que sobren:
v = ( 2 , 3 , 4 , 5 )
( x , y , *rest ) = v
aquí guardará a 2 en x, a 3 en y, pero en rest guardará una lista [ 4 , 5 ], (realmente no es una lista, sino un iterador, de
lo cual hablaremos más adelante).

otro ejemplo:
v = ( 2 , 3 , 4 , 5 )
(x , *rest , w ) = v
aquí guardará a 2 en x, [ 3 , 4 ] en rest, y 5 en w.

Cuando un valor en una tupla no nos interesa, podemos colocar:


v = ( 2 , 3 , 4 , 5 )
( x , y , _ , w ) = v
entonces 4 no se guarda en ningún lado.

Podemos mezclarlo:
v = ( 2 , 3 , 4 , 5 )
( x , *_ , w ) = v

a este método se le llama el método de unpacking y también se puede hacer con listas, por ejemplo:
x , y = [ 1 , 2 ]
aunque no es tan común, es más común con tuplas, pues como veremos más adelanta, son los parámetros que le pasamos a una
función. Esto es común en matemáticas, para T: VxW -> R , es común escribir T( v , w ) en lugar que T( ( v, w ) ).

_________________________________________________________________________

*****5.17- Operaciones y sutilezas de las tuplas*****

Podemos concatenar tuplas sumándolas como números, también podemos repetirlas en una sola tupla multiplicando por un número
entero.

Para calcular la longitud de una tupla, podemos usar nuevamente la función len().

Si queremos definir una tupla con un solo elemento hay que colocar un espacio vacío y escribir por ejemplo v = ( 2 ,). Si
solo colocamos v = ( 2 ) Python los interpreta como signo de agrupación.
_________________________________________________________________________

*****5.18- Iteraciones en tuplas*****

Nuevamente podemos usar el bucle for para iterar en sus elementos como en las listas, diccionarios y conjuntos
v = ( 2 , 3 , 4 , 5 )
for c in v:
print( c )

Si tenemos una tupla de tuplas de dos elementos, podemos usar el método unpacking como en los diccionarios
v = ( ( 2 , "a" ) , ( 3 , "b" ) , ( 4 , "d" ) , ( 5 , "e" ) )
for c , e in v:
print( c , e )

Cualquier objeto iterable puede convertirse a una tupla con la función tuple(), aunque en el caso de los diccionarios
pasará solo las claves y no los valores.

Dada una lista, podemos obtener un iterable de tuplas que contienen el índice en la primera entrada y el elemento
correspondiente de la lista en la segunda entrada a través de la función enumerate(). Por ejemplo, para imprimir el indice y
la entrada podemos usar el unpacking en un for de la siguiente forma :
lista = [ "a" , "e" , "i" , "o" , "u" ]

for indice, elemento in enumerate( lista ) :


print( f"El elemento { indice } es: { elemento }" )

Podemos castear un enumerate para convertirlo en un iterable cómodo y conocido, como una lista, por ejemplo:
lista = [ "a" , "e" , "i" , "o" , "u" ]
lista_indexada = list( enumarate( lista ) )
_________________________________________________________________________

*****5.19- La función zip()*****

La función zip(,) permite juntar dos listas en un solo objeto con tuplas que contienen en cada coordenada cada elemento de
cada lista, pero hay que convertirla pues el objeto zip no nos permite acceder a los datos. Por ejemplo:
l = [ 1 , 2 , 3 , 4 , 5 ]
s = [ "a" , "b" , "c" , " d" , "e" ]
items = zip( l , s )
print( list( items ) )

La función zip() es un iterable por lo que podemos aplicar un for a sus elementos:
for c in zip( l , s ):
print( c )
o usar el método unpacking
for c,d in zip( l , s ):
print( "El número {} tiene asociada la letra {}.".format( c , d ) )

Con el método zip se pueden unir muchas lista, no solo dos.

_______________________________________________________________________________________________________________________________

Capítulo 6: Funciones.

*****6.1- Conceptos básicos de funciones*****

Una función es un pedazo de código que no se ejecuta a menos que sea llamada, sirve para utilizarla en muchas partes del
código. Usar funciones nos da claridad, orden y economía en el código.

Las funciones constan de tres partes:


Inputs(parámetros): Valores que debemos introducir en las funciones para que los utilicen.
Cuerpo: El código que determina qué hará la función.
Output: Resultado que devuelven las funciones. Estos se especifican con una palabra especial.
Las funciones no necesariamente deben tener inputs, ni outputs, pero cuerpo sí, pues si no lo tienen entonces no hacen nada.

Para crear una función debemos hacer uso de la palabra def, seguido del nombre de la función y unos paréntesis para indicar
los inputs (si no tiene, se dejan vacíos). El cuerpo debe ser indentado como lo hacemos en los ciclos y habrá que colocar
nuevamente dos puntos. Por ejemplo:
def saludar():
print( "Hola, ¿Cómo estás?" )

Podríamos intentar guardar el texto de la función anterior en una variable:


def saludar():
print( "Hola, ¿Cómo estás?" )

x = saludar()
print( x )
pero como esta función no devuelve nada, al imprimir x nos devolverá un None.

Podemos hacer que nuestra función nos devuelva el valor que queremos con la palabra "return" seguida del objeto que queremos
devolver, por ejemplo:
def saludar():
return "Hola ¿Cómo estás?"
en este caso sí tiene sentido guardar el resultado de la función en una variable
def saludar():
return "Hola ¿Cómo estás?"

x = saludar()
print( x )
Las variables que creamos dentro de una función son "variables locales", es decir, solo funcionan dentro del bloque de
código que es la función, fuera del bloque no están definidad, por ejemplo
def saludar():
saludo = "Hola ¿Cómo estás?"
return saludo

print( saludo )
en esta caso Python nos dirá que saludo no está definido como variable. Pues es una variable que definimos dentro de la
función saludar(), pero fuera de ella no existe.

Para definir una función con parámetros, los colocamos dentro de los paréntesis después del nombre de la función, por ejemplo
def saludar( name , surname ):
print( "Hola {} {}. ¿Cómo estás?".format( name , surname ) )
esta función, no devuelve nada, pero utiliza dos parámetros para imprimir el saludo con el nombre y el apellido. Para usar
esta función debemos escribir su mismo nombre y especificar los parámetros que serán usados en el orden en que están
escritos en la función, se dice que los parámetros de una función son posicionales
def saludar( name , surname ):
print( "Hola {} {}. ¿Cómo estás?".format( name , surname ) )

saludar( "Gerardo" , "González" )

Podemos ser más especificos con los nombres de las variables utilizando los nombres que se le asigna a cada variable e
igualando al dato que queremos darle a la función, esto nos ayuda a esquivar el orden, por ejemplo
def saludar( name , surname ):
print( "Hola {} {}. ¿Cómo estás?".format( name , surname ) )

saludar( surname = "González" , name = "Gerardo" )

Podemos hacer que las funciones de parámetros nos devuelvan un valor, por ejemplo, la siguiente función es una función que
nos calcula el promedio de dos números:
def promedio( x, y ):
return ( x + y ) / 2
podemos hacer que una función nos devuelva listas, tuplas, diccionarios etc. Si queremos una función que nos devuelva dos
valores por ejemplo, lo usual es definir una función que nos devuelva una tupla mediante el método unpacking:
def euclidean_division( x , y ):
q = x // y
r = x % y
return q , r

En ocasiones, sobre todo cuando empezamos a crear un programa, uno comienza por planear una función pero aún no sabe como
escribirla, así que pasa a lo que sigue en el código. Si uno ejecuta una función sin cuerpo como:
def my_function()
nos saltará un error. Una forma de evitarlo es a través de la palabra "pass", la colocamos en el cuerpo y al ejecutar no
habrá errores:
def my_function():
pass
_________________________________________________________________________

*****6.2- Parámetros de una función*****

Las funciones deben recibir exactamente el número de parámetros para el que fueron preparadas, en caso contrario, saltan
un error.

En ocasiones no sabemos cuántos parámetros vamos a necesitar para una función, por ello Python nos permite colocar una
sintaxis especial para no preocuparnos por la cantidad de parámetros que le daremos. Para ello colocamos el nombre de un
parámetro pero antecedido por un asterisco. En este caso la variable será una lista que contiene los parámaetros que
ingresamos, por ello mismo podemos usarlo para iterar. Por ejemplo, la siguiente función calcula la suma de números que le
damos como parámetros.
def suma( *numeros )
for numero in numeros:
total = 0
total += numero
return total
por convención al parámetro que guarda estas variables le llamamos *args

También hay una manera de definir parámetros nombrados colocando el nombre del parámetro y colocando dos asteriscos previos,
esto nos guarda los nombres y los valores asignados en un diccionario al que podemos acceder con los métodos de diccionarios
usuales que ya vimos antes. La sigueinte función le pide al usuario su nombre y su o sus apellidos
def mi_nombre( name , **surname ):
print( "Mi nombre es {}".format( name ) , end = " " )

for element in surname.values():


print( "{}".format( element ) )
por ejemplo:
def prueba( **kwargs ):
print( kwargs )

prueba( uno = "a" , dos = "o" , tres = 1 )

Podemos definir parámetros por defecto, es decir, parámetros que podemos no escribir y que aún así tomarán un valor. Para
colocar un valor por defecto en un parámetro, se iguala al valor que se quiere dejar en los parámetros iniciales de la
función, por ejemplo:
def producto( n , m = 1 ):
return n*m
_________________________________________________________________________

*****6.3- Documentando funciones (Docstring)*****

Se le llama documentar una función a describir mediante un texto de qué trata la función, es decir, qué parámetros necesita,
qué hace la función y que regresa. Para realizar una documentación, antes de escribir el cuerpo de la función se escribe
texto entre un par de comillas dobles triple, es decir """ """. Por ejemplo:
def euclidean_division( x , y ):
"""
Esta función calcula el cociente y el resto
de la división entera entre x e y

Args:
x (int): dividendo
y (int): divisor

Returns:
( q , r ): tupla con el valor de (cociente, residuo)
"""
q = x // y
r = x % y
returns q , r
esta es la forma que prefiere Google para documentar las funciones y es la forma en que aparecen documentadas las funciones
en Colab.

Podemos imprimnir la documentación de una función escribiendo el método oculto .__doc__, por ejemplo:
print( euclidean division.__doc__ )
_________________________________________________________________________

*****6.4- Alcance de variables (scope), paso por copia y paso por referencia*****

Recordemos que las variable que definimos dentro de las funciones son variables locales, es decir, no están definidas dentro
del código. Sin embargo, en algunas ocasiones podemos definir variable fuera y usarlas dentro, por ejemplo:
n = 5

def sucesor():
return n + 1

sucesor()
en este caso nos devolverá 6. Sin embargo la función:
n = 5

def sucesor():
n = n+1
return n

sucesor()
nos dará un error. Al tratar a n dentro de una función como una variable, en automático Python nos lanza un error, pues
interpreta que esta variable debe ser local. Esto se puede reparar especificando a n como la variable global, es decir, la
que fue definida fuera de la función. Para ello es necesario escribir la palabra "global" de la siguiente forma:
n = 5

def sucesor():
global n
n = n+1
return n

sucesor()
en este caso no habrá error.

En Python, los tipos de variables básicos se pasan a las funciones por copia, es decir, la función crea una copia de la
variable que se le pasa, la usa en el programa y la variable que usa no se modifica, conserva su valor original.

Por otro lado, las estructuras de datos como las listas, tuplas, conjuntos, diccionarios, iteradors, las clases y los
objetos de clases(que vemos más adelante) se pasan por referencia, es decir, la función modifica el conjunto de datos
original, pues se le pasa el lugar en memoria justo donde se encuentra almacenado el conjunto de datos. Por ello los
conjuntos de datos tiene métodos que les permiten crear copias. Una forma de no pasar un conjunto de datos por referencia
es crear una copia disfrazada pasando un substring pero completo, por ejemplo:
l= [ 1 , 2 , 3 , 4 , 5 ]

def doble( lista )


for numero in lista:
numero *= 2
return lista

print( doble( lista[ : ] ) )


_________________________________________________________________________

*****6.5- Estándares en la comunidad de Python*****

Dentro de la comunidad de Python hay varios estándares comunes, veamos dos de ellos. El primero de ellos es que TODAS las
funciones deben definirse en la parte superior de un programa.

El segundo es la forma de escribir programas.Los programas en Python deben tener una estructura especial en el contexto del
desarrollo de software, debemos añadir una función principal que llamaremos run() y un condicional de la siguiente forma:
def run():
CONTENIDO PRINCIPAL

if __name__=="__main__":
run()
la función run(), conocida como la función principal, debe contener en su cuerpo todo el código que comúnmente escribimos.
Por otro lado, el condicional lo único que hace es ejecutar la función principal. A pesar de que esto pareciera innecesario,
es el estándar en muchos equipos de trabajo y empresas.

Por ejemplo, el siguiente programa nos dice si una palabra es un palíndromo:


def is_palin( frase ):
frase_modif = frase.replace( " " , "" )
frase_modif = frase_modif.lower()
if frase_modif[ : : ] == frase_modif[ : : -1 ] :
print( 'La frase: \n"{}" \nes un palíndromo.'.format( frase ) )
else:
print( 'La frase: \n"{}" \nno es un palíndromo.'.format( frase ) )

def run():
frase = input( "Escriba una plabra o frase: " )
is_palin( frase )

if __name__=="__main__":
run()

Nota que las funciones se definen en la parte superior, luego la función principal run() y finalmente el código que
inicializa el programa es el if como dijimos antes.

_________________________________________________________________________

*****6.6- Funciones recursivas*****

Una función recursiva es una función que se llama a sí misma. Es útil usarlas para hacer una tarea iterativa. Son poderosas
pero potencialmente peligrosas, pues es fácil crear funciones recursivas que hagan bucles infinitos. Por ejemplo, la
siguiente función es el factorial de un entero:
def factorial( n ):
if n = 0:
return 1
else:
return n*factorial( n - 1 )

print( factorial( 5 ) )
nota que las funciones recursivas tienen dos partes principales, el caso base n = constante y el caso inductivo, es decir
llamar a la función con un índice distinto.

La siguiente función nos devuelve el término n-ésimo de la sucesión de Fibonacci:


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

print( fibo( 8 ) )
En ocasiones, se pueden definir condiciones para deternar la función y que no genere un ciclo infinito
_________________________________________________________________________

*****6.7- Funciones helper*****

Las funciones no solo se pueden llamar a sí mismas, pueden llamar a otras funciones y ayudarse en su desempeño, para ello
simplemente se coloca la ejecución de una dentro de la otra. Por ejemplo
_________________________________________________________________________

*****6.8- Funciones lambda*****

Las funciones lambda son funciones que sirven para acortar la forma de escribir funciones, ya que se escriben en una sola
línea de código. Su sintaxis es:
lambda ( parámetros ) : ( expresión )
la limitación es que solo se escriben en una sla línea pero útiles para hacer transforaciones sencillas y combinándolos
con otros métodos. Estas funciones deben guardarse en una variable, posteriormente pueden usarse como una función normal,
es decir, con el nombre de la variable en donde la guardamos e introduciendo sus parámetros. Por ejemplo:
suma10 = lambda x : x + 10
con este código hemos definido la función suma10() que suma 10 a un número.

Otro ejemplo es la ya conocida función, para saber si una función es un palíndromo, pero en su versión lambda:
is_palin = lambda string : string == string[ : : -1 ]

Podemos definir funciones con más parámetros, por ejemplo:


prod = lambda x , y : x*y

La siguiente función devuelve una tupla con primera entrada un número y la segunda su cuadrado:
pareja = lambda x : ( x , x**2 )
_________________________________________________________________________

*****6.9- Funciones de orden superior*****

Una función de orden superior es una función que recibe por parámetro a otras funciones, por ejemplo:
def saludo( funcion )
funcion()
print( "Función ejecutada" )

def hola():
print( "Hola!" )

def adios():
print( "adiós!" )

saludo( hola )
saludo( adios )
la función saludo en este caso es la función de orden superior, nota que solo recibe el nombre de la función como parámetro,
sin argumento alguno.

Hay cuatro funciones de orden superior muy importantes en muchos lenguajes:


- Filter
- Reduce
- Map
- Sorted
en los siguientes capítulos las estudiaremos en Python.
_________________________________________________________________________

*****6.10- Filtrado de iterables con filter()*****

Hay funciones que se pueden mezclar con las funciones lambda. La primera de ellas es la función filter() quer sirve para
filtrar elementos de un iterable con una condición dada en forma de función, dicha función debe devolver un valor booleano.
Esta función devuelve un objeto oculto (generator) que hay que convertir a lista, tupla etc.

La sintaxis de la función filter es:


filter( función , iterable )

Por ejemplo. La función siguiente depura los múltiplos de 7 de una lista:


nums = [ 49 , 57 , 62 , 147 , 2101 , 22 ]
list( filter( lambda x : x % 7 == 0 , nums ) )
en este caso el resultado será una lista.

La función filter como tal, devuelve un iterador, por ello debe ser casteada a un iterable. El concepto de iterador será
explicado más adelante.

No solo podemos colocar funciones lambda dentro, puede ser cualquier función definida, por ejemplo:
def terceraletraess( word ):
return word[ 2 ] == "s"
words = [ "castaña" , "astronomía" , "masa" , "bolígrafo" , "mando" , "tostada" ]
list( filter( terceraletra , w ) )
notemos que dentro de filter no se escriben los parámetros, ni para las funciones, ni para las funciones lambda.

El siguiente programa sirve para filtrar los números positivos de una lista:
nums =[ 2 , 4 , 7 , 6 , 12 , 95 , -2 ]
list( filter( lambda x : x > 0 , nums ) )

Lo que se hace con filter, puede hacerse con una list comprehension de la siguiente forma:
lista = [ element for element in list if func( element ) ]
nota que la función nos debe devolver necesariamente un booleano para que pueda ser combinado con el if. Por ejemplo, en el
caso de las palabras:
palabras = [ "ana" , "memo" , "oso" , "panda" , "canto" ]

lista = [ string for string in palabras if is_palin( string ) ]

print( lista )
_________________________________________________________________________

*****6.11- Reducción de listas con reduce()*****

Otra función importante es la función reduce(). Para poder utilizarla hay que invocar al módulo functools.

La función reduce() nos sirve para iterar una función con dos parámetros, primero en los primeros dos elementos de un
iterable, luego al resultado y al tercero, luego al resultado y al cuarto etc. Como una sucesión recurrente.

La sintaxis de reduce es la siguiente:


functools.reduce( función( a , b ) , iterable )

El siguiente ejemplo es una forma de calcular el producto de todos los números de una lista:
from functools import reduce

nums = [ 1 , 2 , 3 , 4 , 5 , 6 ]
reduce( lambda x , y : x*y , nums )

Nuevamente, la función no tiene por qué ser una lambda Por ejemplo, el siguiente código nos sirve para calcular el máximo
de una lista sin la función max().
def mayorque( a , b )
if a > b :
return a
return b

nums = [ 5 , 6 , 12 , -56 , -6 ]
reduce( mayorque , nums )
_________________________________________________________________________

*****6.12- Funciones vectoriales de varias variables con map()*****

La función map aplica una función a todos los elementos de una lista, sin embargo hay que convertir nuevamente, pues nos
devuelve un iterador. Su sintaxis es:
map( función , lista )

Por ejemplo, el siguiente código nos devolverá una lista con las longitudes de las palabras otra lista dada:
w = [ "zapato" , "amigo" , "yoyo" , "barco" , "xilófono" ]
list( map( len , w ) )
recuerda que len() ya es una función.

De igual manera no se indican los parámetros y más aún, no tiene por qué ser una lambda.

Nota que podemos hacer lo mismo que un map con list comprehensions de la siguiente forma.
lista = [ func( element ) for element in list ]

La función map se puede aplicar a más de una lista, en este caso la función lambda deberá tener más variables, por ejmplo, el
siguiente programa nos regresa una lista con la suma de cada entrada de la lista:
lista1 = [ 1 , 2 , 3 , 4 , 5 ]
lista2 = [ 10 , 20 , 30 , 50 , 80 ]

suma = list( map( lambda x , y : x + y , lista1 , lista2 ) )


print( suma )
si los arreglos no tienen la misma longitud, no causará error, pero solo sumará la misma cantidad que la lista más corta.

La función map puede mezclarse con funciones más robustas para realizar transformaciones más complejas, por ejemplo, si se
tiene la siguiente lista de diccionarios:
productos = [ {
"prenda" : "pantalón" ,
"precio" : 400
} ,
{
"prenda" : "playera" ,
"precio" : 150
} ,
{
"prenda" : "camisa" ,
"precio" : 250
}
]
¿cómo agregamos el impuesto del 16 por ciento a cada diccionario? Una alternativa es la siguiente:
def agregar_impuesto( dicc ) :
dicc[ "IVA" ] = dicc[ "precio" ]
return dicc

lista = list( map( agregar_impuesto , productos ) )


print( lista )
lista es una variable con el iva incluido. Sin embargo, al hacer esto, modificaremos el diccionari original, pues lo estamos
pasando por referencia, habrpa qe crear una copia dentro de la función:
def agregar_impuesto( dicc_completo ) :
dicc = dicc_completo.copy()
dicc[ "IVA" ] = dicc[ "precio" ]
return dicc

lista = list( map( agregar_impuesto , productos ) )


print( lista )

_________________________________________________________________________

*****6.13- Ordenando listas con sorted()*****

Un última función que se lleva bien con las listas y lambdas es la función sorted(). Esta función ordena los elementos de una
lista con base al resultado de una función indicada. Su sintaxis es la siguiente:
sorted( lista , key = función )
la función nos tiene que devolver necesariamente un número o una letra para que pueda ser ordenado.

Por ejemplo, la siguiente función ordena los elementos de una lista de palabras con base en su longitud:
w = [ "xola" , "zapato" , "una", "américa" ]
sorted( w , key = len )

En la parte de la función podemos colocar cualquier función incluyendo las lambdas.

En automático sorted() ordena de manera ascendente, pero podemos colocar un tercer argumento a sorted() para invertirlo. En
este caso sería
sorted(,, reverse = True ).

Si no colocamos ninguna función, sorted() ordena por orden numérico y lexicográfico, por ejemplo:
sorted( w )

______________________________________________________________________________________________________________________________

Capítulo 7: Programación orientada a objetos (POO)

*****7.1- Conceptos básicos de POO*****

Los conceptos clave en la programación orientada a objetos(POO) son 2: clase y objeto.

Una clase será un tipo de objeto que vamos a declarar, hemos visto varias clases como los int, float, string, list, dict, set
etcétera.

Un objeto (o instancia) es un ente perteneciente a esta clase, por ejemplo [ 2 , 5 , 6 ] pertenece a la clase list. Dos
objetos pueden ser distintos pero pertenecer a la misma clase, por ejemplo [ 2 , 5 , 6 ] y [ 7 , 2 ] son distintos pero ambas
son listas.

En esta sección vamos a ver como se crean clases, objetos asociados a clases, atributos, propiedades, métodos y funciones porpios
de una clase.

Hay otras dos palabras importantes concernientes a las clases: atributo y propiedad. Un atributo es una característica que
define a todos los objetos de una clase, es decir, un parámetro que agregamos al crear un objeto de una clase. El concepto de
propiedad lo vemos más adelante

Para crear una clase usamos la palabra reservada "class" y el nombre de la clase, por convención humana, es común nombrar a
las clases comenzando con mayúsculas y a las funciones con minúsculas. Las clases tiene la misma estructura que las funciones,
pero se recomienda que su nombre comience con mayúscula para diferenciarla. No se le colocan parámetros. Por ejemplo, vamos a
crear una nueva clase, una clase de libros:
class Book() :
is_electronic = False
este objeto dentro de la clase is_electronic, es una variable estática, es decir, todos los objetos creados con la clase
tendrán siempre este parámetro.

Para crear un objeto con una clase, le asignamos la clase, es decir:


libro1 = Book()
esto nos ha creado un objeto de la clase Book.

Podmeos consultar la función type() a libro1 y veremos que es una nueva clase de objeto que hemos creado.

Si queremos acceder a la variable is_electronic de la clase, aplicamos su nombre a la clase como un método sin argumentos.
Por ejemplo:
Book.is_electronic
estas variables estáticas no cambiarán nunca a pesar de que se manipulen objetos creadas por la clase.

También podemos calcularle esta propiedad a los objetos creados por la clase, en nuestro caso escribimos:
libro1.is_electronic
la diferencia radica en que esta variable es de libro1 y en este caso si se puede cambiar, asignándole un valor. Por ejemplo:
libro1.is_electronic = True
pero Book.is_electronic no cambia.

Podemos añadir un docstring (documentación) a las clases al igual que hicimos con las funciones. Por ejemplo
class Book():
"""
Clase para trabajar con libros.
"""
is_electronic = False

Podemos acceder a la documentación de una clase escribiendo la terminación .__doc__ e imprimiéndola pr pantalla. En nuestro
caso:
print( Book.__doc__ )
_________________________________________________________________________

*****7.2- Método constructor y destructor*****

El método constructor es una función que nos permitirá introducir parámetros a la hora de definir un objeto a partir de una
clase para definirlos como atributos del objeto creado. El método constructor es la siguiente función
class Book():
def __init__( self , titulo , parámetro2 , parámetro3 ) :
self.titulo = titulo
self.autor = parámetro2
self.is_electronic = parámetro3
el método constructor es una función que jamás se llama, se ejecuta automáticamente pues la palabra __init__ es un nombre de
función reservado. Nota que estamos usando como primer parámetro "self". esta palábra reservada nos sirve para asociarle
un atributo al objeto que está siendo creado. Los atributos que define self son de los objetos creados, no de la clase.
Nota que los parámetros del constructor pueden o no coincidir con el nombre del atributo.

Podemos ahora crear un objeto Book de la siguiente forma


libro2 = Book( "Harry Potter" , "J.K. Rowling" , False )
nota que aunque el constructor tiene cuatro parámetros, el parámetro self es un parámetro invisible al que no se le asigna
un valor.

Una vez asignados los atributos, podemos verlos aplicando el nombre del atributo que va después del self. al objeto, por
ejemplo
libro2.is_electronic

Para acceder a todos los atributos al mismo tiempo usamos el método .__dict__, por ejemplo:
libros2.__dict__
esto nos mostrará un diccionario que tiene por claves el nombre del atributo y por valores los que le hemos asignado.

Para dar un valor por defecto a una variable de las que le pasamos, podemos igualar dentro del constructor:
class Book():
def __init__( self , titulo , parámetro2 = "Desconocido" , parámetro3 = False ) :
self.titulo = titulo
self.autor = parámetro2
self.is_electronic = parámetro3

Si queremos usar los parámetros por defecto, podemos escribir en el mismo orden en que se definieron los parámetros que
debemos pasar, los otros se rellenan automáticamente en orden.
libro3 = Book( "100 cuentos cortos" )

En ocasiones es buena práctica especificar los que vamos a pasarle:


libro3 = Book( title = "100 cuentos cortos" )

Así como exixte un método constructor, existe un método destructor, para ello colocamos la siguiente función __del__:
class Book():
def __init__( self , titulo , parámetro2 , parámetro3 ) :
self.titulo = titulo
self.autor = parámetro2
self.is_electronic = parámetro3

def __del__( self ) :


print( "Su variable ha sido eliminada." )

Para activarlo usamos la palabra reservada "del". Si creamos por ejemplo un libro libro4, podemos destruirlo con:
del libro4
y Python nos imprimirá en pantalla "Su variable ha sido eliminada."

Podemos cambiar un atributo de un objeto llamándolo y sobreescribiéndolo, por ejemplo:


libro4.titulo = "Hola k ase"

_________________________________________________________________________

*****7.3- Métodos de una clase*****

Hay tres tipos de métodos que podemos crearle a una clase; métodos de instancia, métodos estáticos y métodos de clase.

Los métodos de instancias son funciones que inician con el parámetro self, esto significa que son funciones que actúan sobre
los objetos y no sobre la clase. Sirven para tomar los atributos de un objeto que se encuentran en el constructor y operarlos
o modificarlos para que regresen algo basado en ellos. Por ejemplo, la siguiente clase define unos objetos que llamaremos
rectángulos y haremos un método de instancia que nos devuelva el área del rectángulo.
class Rectangulo():

def __init__( self , base = 1 , altura = 1 , color = "azul" ) :


sel.base = base
self.altura = altura
self.color = color

def area( self ):


return self.base*self.altura
ahora creamos un rectángulo y calculamos su área colocando el noombre del método de instancia como terminación sin argumentos
rect1 = Rectángulo( 2 , 5 )
print( "El área del rectángulo es:" , rect1.area() )

Los métodos de instancia pueden tener más parámetros que solo el self, por ejemplo, a continuación definimos un método que
calcula el volumen de una caja que tiene por base el rectángulo y que le suministramos la altura por parámetro:
class Rectangulo():

def __init__( self , base = 1 , altura = 1 , color = "azul" ) :


sel.base = base
self.altura = altura
self.color = color

def area( self ):


return self.base*self.altura

def volcaja(self , alto = 0 ) :


return self.base*self.altura*alto
entonces al escribir, para rect1:
rect1.volcaja( 7 )
obtendremos 70 como resultado, pues tenía dos de base y 5 de altura y le suministramos 7 de alto.

Hay un método de instancia especial, el __str__. Este método nos permite controlar lo que se mostrará en pantalla cuando
imprimamos un objeto de una clase. Por ejemplo:
class Rectangulo():

def __init__( self , base = 1 , altura = 1 , color = "azul" ) :


self.base = base
self.altura = altura
self.color = color

def area( self ) :


return self.base*self.altura

def volcaja( self , alto = 0 ) :


return self.base*self.altura*alto

def __str__( self ):


print( "Base = {}\nAltura ={}".format( self.base , self.altura ) )
al escribir y correr:
print( rect1 )
nos mostrará su base y altura:
"Base = 2"
"Altura =5"

Los decoradores son objetos en Python que nos permiten alterar el comportamiento de una función, estos son funciones de orden
superior que pueden llamarse directamente o mediante son llamados un @nombre antes de la función que queremos alterar.
Podemos construir nuestros propios decoradores, pero esto lo veremos mucho más adelante, por ahora utilizaremos ciertos
decoradores que Python ya tiene implementados. Estos decoradores nos permiten crear dos nuevos tipos de funciones; los
métodos estáticos y los métodos de clase.

Los métodos estáticos no requieren de objetos para ejecutarse. Requieren de un "decorador" @staticmethod que se añade antes
de definir la función. Podemos pasarle cualesquiera objetos, para que nos devuelvan valores. Se llaman a partir de la clase
y no como métodos sobre objetos. Por ejemplo, el siguiente método comprueba si dos rectángulos tienen las mismas medidas:
class Rectangulo():
def __init__( self , base = 1 , altura = 1 , color = "azul" ) :
sel.base = base
self.altura = altura
self.color = color

def area( self ):


return self.base*self.altura

def volcaja( self , alto = 0 ) :


return self.base*self.altura*alto

def __str__( self ) :


print( "Base = {}\nAltura ={}".format( self.base , self.altura ) )

@staticmethod
def medidasiguales( r1 , r2 )
if ( r1.base == r2.base ) and ( r1.altura == r2.altura )
return True
return False
para llamarlo, primero definimos dos rectángulos y lo llamamos con la clase:
rect1 = Rectangulo( 5 , 8 )
rect2 = Rectangulo( 2 + 3 , 5 + 3 )
print( Rectangulo.medidasiguales( rect1 , rect2 ) )

Los métodos de clase sirven para generar un objeto de la misma clase, para para ello es necesario colocar el decorador
@classmethod y llamar a la clase completa con la palabra reservada "cls" como parámetro. Por ejemplo, las siguiente clase
nos crea un rectángulo de medidas aleatorias (aunque es necesario importar la libreria random):
class Rectangulo():

def __init__( self , base = 1 , altura = 1 , color = "azul" ):


sel.base = base
self.altura = altura
self.color = color

def area( self ):


return self.base*self.altura

def volcaja( self , alto = 0 ) :


return self.base*self.altura*alto

def __str__( self ) :


print( "Base = {}\nAltura ={}".format( self.base , self.altura ) )

@staticmethod
def medidasiguales( r1 , r2 )
if ( r1.base == r2.base ) and ( r1.altura == r2.altura )
return True
return False

@classmethod
def randomrectangle( cls ) :
base = random.randrange( 1 , 10 )
altura = random.randrange( 1 , 10 )
return cls( base , altura )
para activar este método debemos invocarlo sobre toda la clase:
nuevo = Rectangle().randomrectangle()
print( nuevo )
No se le colocan argumentos ni a la clase ni al método de clase. Por supuesto que se le pueden colocar más argumentos a los
métodos de clase.

En ocasiones podemos usar distintos métodos de clase para hacer las mismas cosas. Eso ya depende de nuestra habilidad como
programadores.
_________________________________________________________________________

*****7.4- Propiedades*****

Existe un decorador @property que hace que podamos invocar un método como un atributo, es decir, sin colocar paréntesis.
Esto solo tiene sentido para los métodos de instancia en donde solo se coloca la palabra self. Para ello basta colocar
el decorador antes de las funciones. Por ejemplo, en la clase rectángulo hacemos el área como propiedad:
class Rectangulo():
def __init__( self , base = 1 , altura = 1 , color = "azul" ) :
sel.base = base
self.altura = altura
self.color = color

@property
def area( self ) :
return self.base*self.altura

def volcaja( self , alto = 0 ) :


return self.base*self.altura*alto

def __str__( self ):


print( "Base = {}\nAltura ={}".format( self.base , self.altura ) )

@staticmethod
def medidasiguales( r1 , r2 )
if ( r1.base == r2.base ) and ( r1.altura == r2.altura )
return True
return False

@classmethod
def randomrectangle( cls ):
base=random.randrange( 1 , 10 )
altura=random.randrange( 1 , 10 )
return cls( base , altura )
ahora podemos calcuar el área como:
print( rect1.area )

Es importante notar que las preopiedades no son atributos, es decir, no se pueden modificar simplemente asignándole
valores nuevos, es decir rect1.area = 20 es algo que nos dará error, pero rect1.base = 20 si cambiará su valor.

Sin embargo existe una manera de modificar un atributo a través de una propiedad declarada con @property. Para ello
hacemos uso del método .setter usándolo como decorador junto con el nombre de la función a usar. Por ejemplo:
@area.setter
y volvemos a definir una función con el mismo nombre que la que queremos modificar. En estra caso podemos agregarle
parámetros de entrada para modificar algún atributo del objeto. A continuación se ha escrito una manea de modificar la
altura a través del área:
class Rectangulo():

def __init__( self , base = 1 , altura = 1 , color = "azul" ):


sel.base = base
self.altura = altura
self.color = color
@property
def area( self ):
return self.base*self.altura

@area.setter
def area( self , area_nueva ) :
self.altura = area_nueva / self.base

def volcaja( self , alto = 0 ):


return self.base*self.altura*alto

def __str__( self ):


return "Base = {}\nAltura ={}".format( self.base , self.altura )

@staticmethod
def medidasiguales( r1 , r2 )
if ( r1.base == r2.base ) and ( r1.altura == r2.altura )
return True
return False

@classmethod
def randomrectangle( cls ):
base = random.randrange( 1 , 10 )
altura = random.randrange( 1 , 10 )
return cls( base , altura )
si creamos un rectángulo rect1 = Rectangulo( 4 , 5 ), vemos que su base es 4, pero ahora podemos modificar su área, por
ejemplo, si le colocamos 50:
rect1.area = 50
ahora vemos que su altura ha cambiado haciendo:
rect1.altura
realmente se trata de una nueva función, pero al colocar el .setter, nos permite usar el nombre de la propiedad, es decir,
podríamos quitar el @area.setter y definir la función con otro nombre, pero para asociarle el area hay que colocar el
decorador.

El método .setter también es usado comúnmente para lanzar errores en caso de que el usuario comience a introducir datos no
deseados, Por ejemplo, hacemos la clase circulo:
class Circle():
def __init__( self , radio = 1 , centro( 0 , 0 ) , color = "rojo" ) :
self.radio = radio
self.centro = centro
self.color = color

@property
def diametro( self ) :
return 2*self.radio

@diametro.setter
def diametro( self , entrada ) :
if entrada <= 0 :
raise ValueError( "El diámetro debe ser un número positivo." )
self.radio = entrada / 2
Si ahora creamos un círculo:
circ1 = Circle( 5 )
y luego queremos modificar su diámetro:
circ1.diametro = -5
nos lanzará un error. Las palabras "raise" y "ValueError" cobrarán sentido más adelante. Pero si el valor que uno intenta
introducir a diametro es negativo o cero, el programa lanzará un error y la leyenda que colocamos entre paréntesis del
ValueError.
_________________________________________________________________________

*****7.5- Herencia y polimorfismo*****

La herencia se refiere a una relación que hay entre clases, podemos hacer que una clase (hijo) use los métodos, atributos y
propiedades que ya definimos en otra (padre), incluyendo al constructor. Para ello lo único que hay que hacer es pasar el
nombre de la clase padre como parámetro a la hora de definir a la clase hijo. Por ejemplo, la siguiente clase
genera un objeto Persona dándole nombre, apellido y edad. Las siguientes dos clases generan ciertos objetos Nino, Adulto que
se generan de la misma forma con en la clase Persona pero que son clases que generan objetos independientes, de tipo
distinto.
class Persona():
def __init__( self , nombre , apellido , edad ) :
self.nombre = nombre
self.apellido = apellido
self.edad = edad

@property
def nombre_completo( self ):
return "{} {}".format( self.nombre , self.apellido )

@nombre_completo.setter
def nombre_completo( self , nombre1_nombre2 ) :
nombre1 , nombre2 = nombre1_nombre2.split( " " )
self.nombre1 = nombre1
self.nombre2 = nombre2

class Nino( Persona ) :


is_adult = False

class Adulto( Persona ) :


is_adult = True
podemos ahora generar un adulto con el siguiento código:
adulto1 = Adulto( "Juan" , "Pérez" , 25 )

Hay una clase que es padre de todas en python, la clase object. Cuando definimos una clase sin argumento inicial, se está
colocando object por defecto, pero podemos colocar su nombre para indicar que es la clase más básica que existe:
class Persona( object ) :
def __init__( self , nombre , apellido , edad ) :
self.nombre = nombre
self.apellido = apellido
self.edad = edad
@property
def nombre_completo( self ):
return "{} {}".format( self.nombre , self.apellido )

@nombre_completo.setter
def nombre_completo( self , nombre1_nombre2 ) :
nombre1 , nombre2 = nombre1_nombre2.split( " " )
self.nombre1 = nombre1
self.nombre2 = nombre2

class Nino( Persona ) :


is_adult = False

class Adulto( Persona ) :


is_adult = True

Aunque podemos volver a escribir un método que ya estaba en la clase padre, esto se llama sobreescribir y se usa cuando
queremos modificar un método.

En programación, copiar y pegar es una muy mala práctica, ya que s fuente de errores comúnmente. Podemos usar el método
super() para llamar una función, atributo o método de la clase padre a una clase hijo con la sintaxis super().metodo()
lo cual se usa usualmente cuando uno quiere ejecutar cosas del padre y aumentar cosas a un método sobreescribiendolo.
También es muy común usarlo en el constructor de una clase hijo cuando se requiere aumentarle atributos, por ejemplo:
class Point2D() :
def __init__(self , x , y) :
self.x = x
self.y = y

def __str__( self ):


return "( {} , {} )".format( self.x , self.y )

@classmethod
def zero( cls ):
return cls( 0 , 0 )

class Point3D( Point2D ):


def __init__( self , x , y , z ) :
super().__init__( x , y )
self.z = z

def __str__( self ):


return super().__str__()[ : -1 ] + ", {} )".format( self.z )

@classmethod
def zero( cls ):
return cls( 0 , 0 , 0 )

Dada una clase, podemos saber de quien hereda imprimiendo el método oculto __bases__ aplicado a la clase:
print( Point3D.__bases__ )
Para saber a quién hereda una clase usamos el método oculto __subclasses__ aplicado a la clase:
print( Point2D.__subclasses__ )

Para saber el orden en que hereda una clase de sus superclases usamos el método oculto __mro__ aplicado a la clase:
print( Point3D.__mro__ )

Podemos hacer que una clase herede funciones y métodos de varias clases padre, simplemente extendiendo los argumentos al
definir la clase, es decir
clase Nombre( Padre1 , Padre2 , Padre3 , ... )

En el caso de la herencia múltiple , el método super() solo llamará a los métodos y funciones de la primera clase padre
escrita en los argumentos. En el caso anterior, de Padre1.

Si solo queremos crear una clase con ciertos padres sin modificar nada, podemos definirlo pero al correrlo Python marca un
error, pues detecta que no se ha escrito nada dentro de la clase. Para solucionarlo escribimos la palabra reservada "pass",
por ejemplo:
class Hijo( Padre1 , Padre2 ) :
pass
esta palabra también funciona en un if, pues continue solo funciona para bucles.

Se le llama polimorfismo a la acción de crear funciones con el mismo nombre para clases distintas, como en el caso de la
función len()

_________________________________________________________________________

*****7.6- Variables privadas (mangling)*****


Se le llama Mangling a la actividad de proteger variables y funciones de una clase. En otros lenguajes de programación
como C++ hay palabras reservadas para indicar cuando una variable es privada. Curiosamente con la palabra "private". En
Python no existe esta palabra como tal pero hay una forma de dificultar acceso a funciones y atributos desde fuera.

En el caso de los atributos podemos colocar una doble barra baja en el nombre del atributo. Por ejemplo:
class Clase( object ):
def __init__( self , cosa ) :
self.__cosa = cosa
si creamos un objeto "nuevo" con esta clase y queremos acceder a su atributo __cosa mediante nuevo.__cosa Python nos
marcará un error, protegiendo el atributo.

Esa variable protegida puede mostararse si definimos una función dentro de la clase que nos la muestre, por ejemplo:
class Clase( object ):
def __init__( self , cosa ) :
self.__cosa = cosa

def muestra( self ) :


print( self.__cosa )
si escribimos nuevo.muestra() nos mostrará el atributo protegido inicialmente. A este tipo de funciones que nos muestran
variable protegidas se les conoce como método getter.

Lo mismo se puede hacer con funciones, por ejemplo, podemos definir funciones protegidas:
class Clase( object ):
def __init__( self , cosa ) :
self.__cosa = cosa

def muestra( self ):


print( self.__cosa )

def __otra(self):
self.__cosa = 2
si intentamos llamar a la función __otra, mediante nuevo.__otra() nos marcará error. Estas funciones pueden ser útiles para
usarse dentro de las clase o de los objetos, es decir, como funciones helper dentro de la clase.

_________________________________________________________________________

*****7.7- Forma alternativa para el setter, getter y del*****

Hemos visto hasta ahora que @property es un decorador, sin embargo, más adelante veremos que los decoradores son realmente
funciones, en este caso @property corresponde a la función property().

La función property() sirve para empaquetar un método getter, setter y delete( este último es un método dentro de la clase
que sirve para destruir un objeto a través de un método y no con el del fuera de la clase ), con la sintaxis
property( getter, setter , del ).
Se le asigna esta función a una variable con el nombre de la propiedad que queremos. Por 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 )


en este caso, si se define avion = Millas(), podemos directamente usar avion.distancia para que nos de la distancia que tiene
actualmente, esto es llamar al método getter ( o calcular una propiedad según vimos antes ), o podemos settear la distancia:
avion.distancia = 20
el valor asignado será el único parámetro que recibe la función setter además del self, en este caso es "recorrido". Por
último del avion.distancia elimina el valor de la distancia.

El decorador @property nos permite quitar el argumento de la función. Todo el proceso completo se ve:
class Millas:
def __init__( self ):
self._distancia = 0

# Función para obtener el valor de _distancia


# Usando el decorador property
@property
def obtener_distancia( self ):
print( "Llamada al método getter" )
return self._distancia

# Función para definir el valor de _distancia


@obtener_distancia.setter
def definir_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

# Creamos un nuevo objeto


avion = Millas()

# Indicamos la distancia
avion.distancia = 200

print( avion.distancia )
el del de la property() no sustituye al destructor __del__, se puede escribir si se desea modificar.

Finalmente, es importantísimo mencionar que las clases también deben ir documentadas, esto es una práctica universal para
funciones y clases.

_______________________________________________________________________________________________________________________________

Capítulo 8: Scripts y módulos

*****8.1- Conceptos básicos de módulos*****

Un script es un archivo que contiene el código de Python. Los archivos tienen por extensión .py y son leidos por un
intérprete, mientras que en Colab tienen la terminación .ipynb.

Los scripts pueden crearse con cualquier editor de texto, en Windows con un bloc de notas y cambiando la extensión
manualmente a .py o guardándolo.

Para importar un script en Colab, podemos subirlo a Drive, primero hay que cambiar la ruta de trabajo de Colab, por defecto
Colab trabaja en una carpeta de la pc que nos "prestan". Para cambiar de carpeta de trabajo tendremos que utilizar el
lenguaje BASH y su comando "cd" (change directory). Para escribir lineas de BASH en Colab, tendremos que colocar un signo de
porcentaje(%) o de admiración(!) y luego los comandos. En este caso colocamos "cd" seguido de la ruta de la carpeta donde
estarán nuestros scripts de trabajo:
%cd /content/drive/MyDrive/scripts
para importar usamos la sintaxis ya conocida para importar una librería en Python, por ejemplo:
import mi_primer_script

En el script podemos escribir funciones, constantes clases etc. Ahora podemos llamar a las funciones del script como llamamos
a las funciones de una librería.
import mi_primer_script as mps

mps.funcion()
mps.constante

A partir de ahora podemos usar la sintaxis de antes pero con nuestros módulos (scripts con varios métodos y constantes).
Pandas y Numpy son ejemplos de módulos creados que Python ya tiene por defecto.

Si cargamos un módulo y luego queremos modificarlos, podremos hacerlo, pero Colab ya no cargará el nuevo módulo modificado,
habrá que restablecer el entorno de ejecución (kernel) y volver a cargar todo.

También podemos correr las funciones de un notebook externo en lugar de las de un script. Para ello escribimos el comando
mágico %run seguido de la dirección donde encontremos el cuaderno a partir de donde estamos en string:
%run "./carpeta/cuaderno_auxiliar.ipynb"
esto nos exportará las funciones del notebook externo que queramos a nuestro kernel actual.

_________________________________________________________________________

*****8.2- Módulos comunes y funciones predeterminadas*****

Python tiene ciertos módulos principales, los más importantes son:


-pandas: para trabajar con DataFrames
-math y cmath: para trabajar con funciones y constantes matemáticas reales y complejas respectivamente.
-numpy: Para trabajar con elementos numéricos
-matplotlib: para hacer representaciones gráficas

La función dir() se puede aplicar a cualquier script de Python para que nos muestre sus funciones, clases y constantes. Por
ejemplo:
import mi_primer_script as mps

dir( mps )
habrá funciones que no están definidas en el script pero que todo módulo de Python trae por defecto:
__builtins__
__cached__
__doc__
__file__
__loader__
__name__
__package__
__spec__
recordemos que por ejemplo, __doc__ nos muestra la documentación. Esta se la podemos colocar al módulo con la sintaxis de
las tres comillas como ya vimos con las funciones. En módulos predeterminados, las clases comienzan con mayúscula.

En particular, la función predeterminada __file__ nos muestra la ruta donde el módulo se encuentra instalado. Por ejemplo:
import mi_primer_script as mps

mps.__file__
esto es muy útil a la hora de trabajar en local, en anaconda por ejemplo, pues en ocasiones los entornos de trabajo de
Anaconda trabajan con versiones propias de Anaconda y no de la pc donde se trabaja.

_________________________________________________________________________

*****8.3- El zen de Python*****

Python tiene un módulo oculto que contiene los 20 principios básicos con los que Python fue creado. Para visualizarlo
simplemente importamos la librería "this". Nos dará los 20 principios de Python. Están escritos en inglés y son los
siguiente:
The Zen of Python, by Tim Peters

Beautiful is better than ugly.


Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
_________________________________________________________________________

*****8.4- Entornos virtuales*****

Si estamos trabajando con un editor de texto y la consola, podemos crear entornos virtuales desde la consola. Un entorno
virtual es un espacio en la computadora en donde se desarrollan proyectos e instalan librerías de manera local, de manera
que al movernos a otro entorno, no poseerá los mismos módulos que otros, estos módulos también diferencían las versiones,
es decir, en un módulo podemos tener Pandas 2.0.3 y en otro Pandas 2.5.1.

Para inicializar un entorno local, primero entramos en git con git init(aunque esto no es necesario), luego hacemos:
py -m venv nombre
el nombre es el nombre del entorno que queremos ponerle, por convención se le pone de igual manera venv o env. Al hacer
ls nos mostrará una nueva carpeta llamada venv.

Para activar el entorno local, dentro del proyecto, escribimos:


.\venv\Scripts\activate
y nos saldrá un (venv) del lado izquierdo. Para salir del entorno virtual esccribimos:
deactivate
es importan te cuidar las llaves invertidas en cmder.

Para no tener que estar escribiedo todo esto, podemos definir un alias para el comando completo, por ejemplo, "avenv", algo
sugerente:
alias avenv=.\venv\Scripts\activate
_________________________________________________________________________
*****8.5- PIP y requerimientos*****

Pip es una herramienta que nos permite instalar librerías que no vienen en el Python local.

Para ver las librerías instaladas en el entorno virtual, escribimos dentro del entorno:
pip freeze

Para instalar una librería escribimos.


pip install nombre_librería

Si queremos compartir un proyecto que funciona con ciertas versiones con otros desarrolladores, ellos necesitarán las mismas
librerías con las mismas versiones exactas que ñas que tenemos instaladas. Para ello creamos un archivo que por convención
llamaremos requirements en nustra carpeta del proyecto:
pip freeze > requirements.txt
es común colocar este archivo requeriments.txt en cada proyecto de Github

Si nosotros somo esa persona que quiere replicar un proyecto ajeno en nuestro equipo, debemos instalar las versiones a través
del archivo creado, para ello hacemos:
pip install -r requirements.txt

Pip es el instralador de paquetes más usados en Python, pero hay otros como pyenv y pipenv.
_______________________________________________________________________________________________________________________________
Capítulo 9: Errores y excepciones

*****9.1- Errores y excepciones*****

Nos referimos a un error en Python a líneas de código que Python no puede interpretar, ya sea porque escribimos algo mal o
que haya procesos mal definidos y que Python no puede completar. Hay dos tipos de errores:
- SyntaxError: Estos son generados por una mala escritura de código (un error de typo), Python ni siquiera lo
ejecuta, sino que lanza en consola la leyenda "SyntaxError"
- Excepciones: Son errores en la lógica de progrmación

Al cometer un error de lógica, Python lanza una "excepción", es decir, Python ya tiene una clasificación de errores de
lógica y estos nos ayudan a localizar el error en el código.

Existen múltiples excepciones en Python que se nos muestran cuando se dan los errores correspondientes. Podemos mostrar
por pantalla todas las excepciones de Python usando la función locals(), esta nos devuelve un diccionario tal y como se
muestra a continuación
for i in dir( locals()[ '__builtins__' ] ) :
print( i )
locals[ "__builtins__" ] nos devuelve el módulo con las excepciones, funciones y atributos de Python. La función dir() nos
permite listar todos esos elementos como strings.

Algunas de las excepciones de Python más comunes al programar son:


Excepción Causa
-----------------------------------------------------------------------------
ArithmeticError Cuando falla una operación numérica
AssertionError Cuando falla una declaración assert
AtributeError Cuando falla una asignación de atributo o referencia
EOFError Cuando la función input() llega a la condición fin de archivo (end-of-file)
FloatingPointError Cuando falla una operación en coma flotante
ImportError Cuando un módulo importando no es encontrado
IndentationError Cuando la indentación no es correcta
IndexError Cuando el índice de una secuencia se sale del rango
KeyError Cuando una clave de un diccionario no es encontrada
KeyboardInterrupt Cuando el usuario pulsa la tecla de interrupción
LookupError Cuando el error no puede ser encontrado
MemoryError Cuando una operación se queda sin memoria
NameError Cuando se llama a una variable que no se encuentra a nivel global ni local
NotImplementedError Cuando un método abstracto requiere de una clase heredada para sobreescribir el método
OverflowError Cuando el resultado de una operación aritmética es demasiado grande para ser representado
RuntimeError Cuando un error no entra dentro de ninguna categoría
TabError Cuando la indentación consiste de tabulaciones y espacios en blanco inconsistentes
TypeError Cuando a una función u operación se le suministra un objeto de tipo incorrecto
ValueError Cuando una función obtiene un argumento del tipo correcto pero de valor incorrecto
ZeroDivisionError Cuando el divisor de una división es 0

Al cometer un error saldrá un mensaje largo, cuyos elementos básicos son los siguientes:
Traceback (most recent call last)
<ipython-input-1-2eb7afd35f49>, line 3, in <module>()
ZeroDivisionError: division by zero
estos "Traceback" se leen de abajo hacia arriba y es una buena idea leer específicamente estas partes señaladas.

Como programadores, necesitamos ser lo más específicos posible. Esto implica ser conscientes de los errores que podrían
ocurrir. Por suerte, Python permite a los programadores tratar con errores de forma eficiente. Podemos manejar excepciones
usando 5 sentencias:
try / except
try / finally
assert
raise
with / as
_________________________________________________________________________

*****9.2- Try/except*****

El bloque try permite comprobar si hay errores de código. El bloque except permite manejar el error sin que se genere la
maraña de mensajes que Python lanza al cometer errores.
En el siguiente chunk, en caso de que ocurra el error al dividir por cero, imprimimos un mensaje por pantalla:
a , b = 5 , 0

try:
print( a / b )
except ZeroDivisionError :
print( "¡Has querido dividir entre 0!" )

En el siguiente chunk, en caso de que ocurra el error, imprimimos el mensaje asociado a la excepción correspondiente por
pantalla sin saltar todos los mensajes de error:
a , b = 5 , 0

try :
print( a / b )
except ZeroDivisionError as message :
print( message )

Podríamos poner más de un bloque except en caso de que el error no sea el especificado
a , b = "a" , 0
try :
print( a / b )
except ZeroDivisionError :
print( "¡Has querido dividir entre 0!" )
except :
print( "Algo más ha salido mal" )
En el chunk anterior hemos intentado dividir un string entre 0. Por tanto, la execpción ya no se debe a intentar dividir
entre 0, sino a otro motivo: que un string no puede ser el dividendo de la división.

También podemos combinar try / except con else:


a , b = 5 , 2

try :
print( a / b )
except ZeroDivisionError:
print( "¡Has querido dividir entre 0!" )
else :
print( "Nada ha salido mal" )
El bloque else se ejecutará siempre y cuando no haya excepciones, junto al bloque try, tal cual ocurre en el ejemplo
anterior.

_________________________________________________________________________

*****9.3- Try/finally*****

El bloque try permite comprobar si hay errores de código. El bloque finally permite ejecutar el código a pesar del
resultado de los bloques try y except.
a , b = 5 , 0

try :
print( a / b )
except :
print( "Algo ha salido mal" )
finally :
print( "El proceso try / except ha finalizado" )
Sea cual sea el caso, el bloque finally siempre se ejecuta.

El bloque finally puede ser útil para cerrar objetos (como en el caso de los txt o csv) y limpiar recursos.
_________________________________________________________________________

*****9.4- Assert*****

La palabra reservada assert se utiliza para debuguear el código. Nos permite comprobar si una condición en nuestro código
devuelve True. De lo contrario, el programa nos devolverá un AssertionError :
x = "Hola"
# Si la condición devuelve True, no ocurre nada
assert x == "Adiós"

En el caso de que la condición devuelva False, podríamos indicar un mensaje del siguiente modo:
# Si la condición devuelve False, salta un AssertionError
assert x == "Adiós", "x debería de contener 'Hola'"
_________________________________________________________________________

*****9.5- Raise*****

Como programadores, podemos elegir cuando mostrar una excepción dada una condición. Para mostrar excepciones, usamos la
palabra reservada raise:
radius = -7
if radius < 0 :
raise Exception( "El radio no puede tomar valores menores a 0" )

En el chunk anterior hemos usado raise para mostrar una excepción. No obstante, podemos elegir qué tipo de excepción
mostrar y el texto que imprimir al usuario:
radius = "-5"

if not type( radius ) is int and not type( radius ) is float :


raise TypeError( "El radio debe ser de tipo numérico (int o float)" )
elif radius < 0:
raise Exception( "El radio no puede tomar valores menores a 0" )

_________________________________________________________________________

*****9.6- Pruebas de caja negra*****

Las pruebas de caja negra se basan en la especificación de la función o el programa, aquí debemos probar sus inputs y
validar los outputs. Se llama caja negra por que no necesitamos saber necesariamente los procesos internos del programa,
solo contrastar sus resultados.

Estos tipos de pruebas son muy importantes para 2 tipos de test:


Unit testing: se realizan pruebas a cada uno de los módulos para determinar su correcto funcionamiento.
Integration testing: es cuando vemos que todos los módulos funcionan entre sí.

Es una buena práctica realizar los test antes de crear tus lineas de código, esto es por que cualquier cambio que se realice
a futuro los test estaran incorporados para determinar si los cambios cumplen lo esperado.

En Python existe la posibilidad de realizar test gracias a la libreria unittest:


import unittest

def suma( num_1, num_2 ) :


return abs( num_1 ) + num_2

class CajaNegraTest( unittest.TestCase ) :


def test_suma_dos_positivos( self ) :
num_1 = 10
num_2 = 5

resultado = suma( num_1 , num_2 )

self.assertEqual( resultado , 15 )

def test_suma_dos_negativos( self ) :


num_1 = -10
num_2 = -7

resultado = suma( num_1 , num_2 )

self.assertEqual( resultado , -17 )

if __name__ == '__main__':
unittest.main()

El nombre de los test definidos como funciones, deben comenzar siempre con la palabra test()

_________________________________________________________________________

*****9.7- Pruebas de caja de cristal*****

Se basan en el flujo del programa, por lo que se asume que conocemos el funcionamiento del programa, por lo que podemos
probar todos los caminos posibles de una función. Esto significa que vamos a probar las ramificaciones, bucles for y while,
recursiónes, etc.

Este tipo de pruebas son muy buenas cuando debemos realizar Regression testing o mocks: descubrimos un bug cuando corremos
el programa, por lo que vamos a buscar el bug gracias a que conocemos como esta estructurado el código.
import unittest

def es_mayor_de_edad( edad ) :


if edad >= 18 :
return True
else:
return False

class PruebaDeCristalTest( unittest.TestCase ) :

def test_es_mayor_de_edad( self ) :


edad = 20

resultado = es_mayor_de_edad( edad )

self.assertEqual( resultado , True )

def test_es_menor_de_edad( self ):


edad = 15

resultado = es_mayor_de_edad( edad )

self.assertEqual( resultado , False )

if __name__ == '__main__':
unittest.main()
_______________________________________________________________________________________________________________________________

Capítulo 10: Funcionamiento avanzado de Python

*****10.1- Organización de proyectos*****

Un paquete es un conjunto de módulos(librerías) necesarios para que un proyecto funciones.

Un proyecto generalmente contiene lo siguiente:


README.md
.gitignore
requeriments.txt
venv/
paquete/
__init__.py
test.py
modulo1.py
:
módulom.py
el archivo __init__.py es el que denota que una carpeta es un paquete, no es necesario que tenga contenido, simplemente que
el archivo exista. Este archivo es necesario para versiones de Python inferiores a la 3.3, pero se coloca para dar
compatibilidad de un programa con estas versiones.
Un paquete es un conjunto de módulos(librerías). El archivo test.py es un módulo que generalmente se crea para debuguear el
código.

Una vez construido un paquete, podemos importar módulos del paquete usando su nombre como si fuera un módulo normal pero
usando la palabra from, por ejemplo, para importar todos los módulo:
from Paquete import *
si solo necesitamos un módulo:
from Paquete import modulo2

Uno podría intentar llamar funciones de un paquete importando primero el paquete y luego ejecutando funciones del módulo
sin haber importado el módulo, es decir:
import Paquete

Paquete.modulo1.funcion1()
esto podría dar error, por ello una buena práctica es llamar al módulo directamente:
import Paquete.modulo

Paquete.modulo1.funcion1()
llamarlo en el __init__.py. El __init__ puede contener constantes que son llamadas con el nombre del paquet, impresiones que
se inicializan al importar el paquete o módulos de él o importar automáticamente ciertos módulos como ya dijimos.

Podemos formar subpaquetes haciendo carpetas en el paquete principal y colocando en cada una de ellas un archivo __init__.py
Por ejemplo:
README.md
.gitignore
requeriments.txt
venv/
paquete/
subpaquete1/
submodulo1.py
:
submódulom.py
subpaquete2/
__init__.py
test.py
modulo1.py
:
módulom.py
para importar un módulo de un subpaquete usamos la sintaxis:
from Paquete.subpaquete1 import submodulo2

El README es un archivo en lenguaje Markdown que describe el proyecto. Se coloca principalmente cuando el proyecto se aloja
en repositorios remotos como Github.

El archivo .gitignore es el archivo que debemos colocar para que git ignore carpetas que no es necesario subir al
repositorio remoto, generalmente colocamos el venv.

En bash podemos colocar


tree -I venv
para ignorar la carpeta venv que esta llena de cosas. Si no lo colocamos, veremos todos los directorios del proyecto.
_________________________________________________________________________

*****10.2- Tipado y tipado estático en Python*****

Hay otras dos calificaciones de los lenguajes, la siguiente viene de acuerdo al tipado:
Estático: Lanzan los errores de tipo en tiempo de compilación, evitando que se ejecuten.
Dinámico: Lanzan los errores de tipo en tiempo de ejecución. Nos lo dice hasta que se ejecuta la línea
con el error
Fuerte: Tratan con mayor severidad a los datos, por ejemplo, no se pueden sumar dos variables de
distinto tipo.
Débil: Tratan con severidad leve a los datos, por ejemplo, se pueden sumar dos variables de distinto
tipo. Python es un lenguaje de tipado dinámico y fuerte. Java script por ejemplo es de tipado
débil.

El tipado dinámico es peligroso, y para verlo vamos a poner un ejemplo. Podemos hacer un programa para un cliente pero que
tiene un error que no salte hasta que se introduzca cierto tipo de datos. Python lo compilará todo bien, lo contrario a lo
que pasaría en un lenguaje de tipado estático, en ese caso ni siquiera podríamos hacer la compilación, y eso provoca que no
lo entreguemos al cliente.

Podmeos hacer que Python se convierta en un lenguaje de tipado estatico.

Para declarar una variable podemos escribirla con la siguiente sintaxis.


variable : type = value
por ejemplo.
a : int = 5

También podmeos hacerlo en las funciones y además indicar el tipo que retornará, de la siguiente forma:
def suma( a : int , b : int ) -> int :
return a + b
sin embargo, la función también puede usarse con cadenas, lo cual no es lo que queremos.

Para hacer lo mismo pero con tipos más complejos como las listas, diccionarios y tuplas, debemos primero importar de la
librería typing las clases Dict, List, Tuple (Ya viene typing en Python). Se definen variables de este tipo igual que como lo
hicimos arriba pero deben llevar con un slice el tipo de variable de sus elementos, por ejemplo:
from typing import List , Dict , Tuple
nueva_lista : List[ int ] = [ 1 , 2 , 3 , 4 , 5 , 6 ]
con diccionarios necesitamos especificar las llaves y los valores, por ejemplo:
usuarios : Dict[ str , int ] = { "Lalo":12 ,
"Norma":15 ,
"Luis":50
}
para las tuplas necesitamos definir los tipos de cada entrada, por ejemplo:
numeros : Tuple[ int , str , float ] = ( 2 , "Hola" , 8.1 )
combinando varios tipos de variable.
paises : List[ Dict[ str , str ] ] = [ { "name":"México",
"gente": "12000000" },
{ "name":"Colombia",
"gente": "5000000" },
{ "name":"Estados Unidos",
"gente": "32700000" } ,
]
En Python podemos crear alias para los tipos de datos, por ejemplo:
TupleMix : Tuple[ int , str , float ]
tupla1 : TupleMix = ( 2 , "Hola" , 8.1 )
tupla2 : TupleMix = ( 3 , "Amigo" , 7.5 )

Con el nuevo tipado que acabamos de introducir no será suficiente, ya que Python no es de tipado estático de fárica. Habrá
que instalarla el módulo mypy para correr de forma estática:
pip install mypy
Ahora sí popdemos correr un programa de manera estática usando en cosola la paquetería mypy de la siguiente forma:
mypy name.py --check-untyped-defs
esto nos mostrará con precisión si hay errores en los tipos de los argumentos que espera una función o una variable que se
defina a lo largo de la ejecución.
_________________________________________________________________________

*****10.3- Closures*****

Un closure es una forma de acceder a variables de otros scopes(alcance) a través de una nested (anidada) 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.
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

Básicamente es 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()

del( main )
my_func()

La siguiente función nos crea funciones a partir de un valor, es como indexar una función en matemáticas:
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
nota que primero definimos times10 y times4 asignando a estas variables una función que tiene por parámetro a su argumento.
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 👀.

_________________________________________________________________________

*****10.4- Decoradores*****

Un decorador es un closure, es una función que recibe como parámetro otra función, le añade cosas y retorna una función
diferente, a la que comúnmente se le llama "envoltura" (wrapper en inglés). 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 mas 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 mas 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

# 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

La envoltura y la función que pasamos por parámetro (la envoltura)deben tener los mismos argumentos, ya que si esto no se
hace, no funcionará de manera correcta. Por ejemplo.
def mayusculas( func ) :
def envoltura( texto ) :
return func( texto ).upper()
return envoltura

@mayusculas
def mensaje( nombre )
return "{ nombre }, recibiste un mensaje".format( nombre )

print( mensaje( "Cesar" ) )

Pronto veremos como ingresar argumentos arbitrarios a la misma función.

Usando el módulo datetime como dt , su clase datetime y su método now, podemos hacer un decorador que nos calcule el
tiempo de ejecución de una función, de la siguiente forma:
from datetime import datetime

def execution_time( func ) :


def wrapper() :
initial_time = datetime.now()
func()
final_time = datetime.now()
time_elapsed = final_time - initial_time

print( "Pasaron" , time_elapsed.total_seconds() , "segundos" )

return wrapper

@execution_time
def random_func() :
for _ in range( 1 , 10000 )
pass

random_func()
nota que en este programa colocamos _ como índice, esto es una convención en la industria cuando no es de interés el índice.
Este programa calcula cuánto tiempo tarda Python en hacer 10000 iteraciones.

Muchas veces queremos usar un mismo decorador para decorar distintas funciones, pero puede que estas tengan una cantidad de
argumentos distinta. Para solucionarlo usamos las notaciones *args y **kwargs. Ambas se colocan en los parámetros, por
ejemplo, vamos a calcular el tiempo de ejecución de varias funciones con el mismo decoradores que hicimos arriba:
from datetime import datetime

def execution_time( func ) :


def wrapper( *args , **kwargs ) :
initial_time = datetime.now()
func( *args , *kwargs )
final_time = datetime.now()
time_elapsed = final_time - initial_time

print( "Pasaron", time_elapsed.total_seconds(), "segundos" )

return wrapper

@execution_time
def random_func() :
for _ in range( 1 , 10000 )
pass

@execution_time
def suma( a : int , b : int ) -> int
return a + b

random_func()
suma( 5 , 6 )

Otra forma de calcular el tiempo de ejecución pero únicamente en notebooks es través del comando mágico:
%%timeit
al fial de la ejecución nos muestra el tiempo que ha tardado.
_________________________________________________________________________

*****10.5- Iteradores*****

Un iterables es un objeto de Python que puede recorrerse mediante un ciclo for, mientras que un iterador es un tipo de
objetos de Python al cual Python transforma un iterador, por ejemplo una lista. Python transforma una lista a un iterador y
lo itera de una forma especial cuando lo seleccionamos en un ciclo for. Es decir, todos los iterables deben ser convertidos
a un iteradoR para que el lenguaje los pueda recorrer. Por ejemplo, para transformar una lista a un iterador usamos la
función iter() de la siguiente manera:
lista = [ 1 , 2 , 3 , 4 , 5 , 6 ]
iterador = iter( lista )

Una vez convertido un iterable a un iterador, podemos acceder a sus elementos. Para ello usamos la función next() cuyo
argumento es justamente el iterador que acabamos de obtener de la conversión. Al usar la función next() accedemos al primer
elemento y nada más. En ese primer elemento queda registrado por Python una especie de apuntador. Si queremos acceder al
segundo elemento debemos escribir una vez más la función next() y el apuntador quedará en la segunda posición, y así
respectivamente.
Al pasarnos del último elemento, el apuntador ya no puede pasar al siguiente elemento y Python nos arroja un error del tipo
StopIteration.
Por ejemplo, el siguiente programa nos muestra los primeros dos elementos de una lista:
lista = [ 1 , 2 , 3 , 4 , 5 , 6 ]
iterador = iter( lista )

print( next( iterador ) )


print( next( iterador ) )

Acceder a los elementos de un iterador es sencillo, pues soslo colocamos el número de funciones next() igual al número de
elementos del iterador que queremos mostrar en pantalla. El problema aumenta de dificultad cuando se trata de un iterador
con una gran cantidad de elementos, por ejemplo 1000000, pues escribir esta misma cantidad de veces ele next() es algo
horrible. Para acceder a los elementos de un iterador hay un método eficaz y sencillo.
lista = [ 1 , 2 , 3 , 4 , 5 , 6 ]
iterador = iter( lista )

while True :
try :
element = next( iterator )
print( element )
except StopIterator
break
esto nos itera todos los elementos del iterador y más aún, nos funciona para cualquier iterable e iterador. Esto es complejo
pero Python tiene una forma más fácil de hacer todo esto que ya comocemos, el ciclo for. Más aún el ciclo for no es más que
azucar sintáctica (suggar syntax) de esto que hicimos. En Python realmente no exixte un ciclo for, sino que la palabra for
es un alías de lo que acabamos de hacer. Así es como funciona un ciclo for por dentro.

Si queremos construir un iterador desde cero, sin el casteo, podemos crear una clase sin parámetros y en esa clase va a
haber dos métodos además del constructor, el método __iter__() y el __next__(). El método iter es un método de instancia que
contiene los datos iniciales para construir el iterador, por ejemplo, el número inicial si se trata de un iterador de
números, al final se retorna. El método __next__ nos permite extraer cada uno de los elementos, pues a través de él
definimos el siguiente elemento del iterador, nos permite definir cómo nos movernos en el iterador e incluso nos permite
definir el error que lanzará Python cuando pasemos el máximo.

Por ejemplo, el siguiente iterador:


class EvenNumbers :
"""Clase que implementa 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 result
else :
raise StopIteration
la ventaja de usar iteradores de esta forma, es que ocupan poca memoria y son más fácil de procesar.

El siguiente código es un iterador de la sucesión de Fibonacci, que al final de muestra con un for. La función .sleep() de
la librería time, nos permite pausar el programa segundos para que no imprima tan rápido la sucesión y se quiebre la
ejecución:
import time

class FiboIter() :

def __iter__( self ) :


self.n1 = 0
self.n2 = 1
self.counter = 0
return self

def __next__( self ) :


if self.counter == 0 :
self.counter += 1
return self.n1
elif self.counter == 1 :
self.counter += 1
return self.n2
else :
self.aux = self.n1 + self.n2
# self.n1 = self.n2
# self.n2 = self.aux
self.n1 , self.n2 = self.n2 , self.aux
self.counter += 1
return self.aux

if __name__ == '__main__' :
fibonacci = FiboIter()
for element in fibonacci :
print( element )
time.sleep( 0.051 )

!!!!Una clase, por lo que creamos un objeto de la clase y este objeto justamente será el iterador.
_________________________________________________________________________

*****10.6- Generadores*****

Un generador se puede interpretar como sugar syntax de los iteradores. A diferencia de estos, los generadores no se
construyen con clases sino con funciones y la palabra reservada yield. Son funciones que guardan un estado.

La palabra yield es una especie de return pero con una característica especial, coloca un apuntador en el valor que
devuleve.

Un generador es una función instanciada que tiene varios returns pero hechos con yield, cada vez que llamamos a la instancia
el apuntador avanza al siguiente yield. Una vez que el apuntador sobrepase todos los yields, Python nos devuelve un
StopIteration, justo como los iteradores. Por ejemplo:
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

Por cierto que generator expression es como list comprehension pero mucho mejor, porque podemos manejar mucha cantidad
de información sin tener problemas de rendimiento. Las list comprehensions llaman a todo el iterador a memoria y después
construyen la lista, mientras que los generator expression van llamando a cada elemento del iterable de uno por uno,
evitando sobrecarcar memoria y dando mayor velocidad.
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

Para hacer nuestra sucesión de Fibonacci (y en general para guardar un iterador con un número infinito de elementos) usamos
un while dentro de la función:
import time

def fibo_gen() :
n1 = 0
n2 = 1
counter = 0
while True :
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 aux

if __name__ == '__main__' :
fibonacci = fibo_gen()
for element in fibonacci :
print( element )
time.sleep( 1 )
si queremos que sea finito, basta colocarle un argumento a la función y cambiar el True del while usando el argumento nuevo.

_________________________________________________________________________

*****10.7- Programación dinámica*****

L programación dinámica es una técnica de optimización de recursos. Los problemas que esta técnica puede optimizar son los
que tienen una subestructura optima y ademas tiene ser un tipo de problema empalmado
-Subestructura Optima: una solucion optima local se puede encontrar al combinar soluciones optimas de subproblemas
locales.
-Problemas empalmados: Una solucion optima que involucra resolber el mismo problema en varias ocaciones

La programación dinámica se basa en la “Memoization” (memorizacion). Es una tecnica para guardar computos previos con el fin
de no realizarlos nuevamente. Normalmente se utiliza basándode en guardar datos en un diccionario, pues las consultas son
un problema de orden O(1). Este estilo de programación intercambia tiempo por espacio, es decir, el proceso ocupará mucha
maás memoria pero el tiempo de ejecución se recortará brutalmente. El ejemplo más claro se da en la sucesión de Fibonacci. Si
recordamos su definición mediante una función recursiva:
def fibonacci( n ):
if n == 0 or n == 1:
return 1
else:
return fibonacci( n - 1 ) + fibonacci( n - 2 )

Si calculamos por ejemplo fibonacci( 500 ) con esta función, nuestro programa correrá poe muchísimo tiempo. Esto se debe a
que el cálculo con esta implementación es exponencial, es decir, para calcular fibonacci( n ) la función calcula primero
fibonacci( n - 1 ) y fibonacci( n - 2 ), pero para calcular cada uno de estos habrá que irse a calcular los dos anteriores
de cada uno y así sucesivamente, así que el cálculo crece como si se tratara de un arbol binario.
La versión usando programación dinámica hace uso de un diccionario en donde se van guardando los cálculos de esta recursión,
y es la siguiente:
def fibonacci_dinamico( n , memo = {} ):
if n == 0 or n == 1 :
return 1

try:
return memo[ n ]
except KeyError :
resultado = fibonacci_dinamico( n - 1 , memo ) + fibonacci_dinamico( n - 2 , memo )
memo[ n ] = resultado
return resultado
de esta forma, calcular fibonacci( 500 ) es inmediato.

Sin embargo, es necesario aclarar que la recursividad aunque sea dinámica, tiene un costo computacional alto. Su ventaja
radica en la sencillez para escribir una función (una vez que se entiende cómo hacerlo correctamente), pues si nos
ahorramos la recursividad el cálculo se vuelve aún más rápido:
def fibonacci_dinamico( n ):
a_0 , a_1 = 0 , 1

if n == 0 or n == 1:
return 1

for i in range( n - 1 ):
a_2 = a_1 + a_0
a_0 = a_1
a_1 = a_2

return a_2
con esta implementación, calcular fibonacci( 1000000 ) es inmediato.

Si es necesario usar recursión, podemos encontrar un problema común, el límite recursivo de Python. Podemos modificarlo a
través de la librería sys y su método recursionlimit( entero ):
import sys

sys.recursionlimit( 100000 )
_________________________________________________________________________

*****10.8- Reconocimiento de patrones estructuales con Match*****

Cuando queremos elegir qué acción realizar en nuestro codigo dependiendo de casos previos, utilizamos la estructura
if-elif-else, sin embargo en otros lenguajes de programación esto lo podemos lograr con las estructuras switch y switch-case.
Esta funcionalidad no la tiene Python, sin embargo, la versión 3.10 de Python implementa una nueva estructura que coincide
con el switch llamada match.

El código:
serie = "N-02"

if serie == "N-01" :
print( "Samsung" )
elif serie == "N-02" :
print( "Nokia" )
elif serie == "N-03" :
print( "Motorola" )
else:
print( "No existe ese producto" )
podemos escribir esto con la palabra match y enseguida la variable que queremos que coincida. Finalmente colocamos cada uno
de los casos indentándolos con la palabra case y el valor de coincidencia:
serie = "N-02"

match serie:
case "N-01" :
print( "Samsung" )
case "N-02" :
print( "Nokia" )
case "N-03" :
print( "Motorola" )
case _ :
print( "No existe ese producto" )

Las declaraciones match y case también permiten asociar acciones específicas basadas en las formas o patrones de tipos de
datos complejos.
lista = [ 1 , 2 , 3 ]
diccionario = { "Nombre": "Gerardo" ,
"Apellido":"González" }
iterable = [ lista , diccionario , 7 ]

for element in iterable :


match element :
case [ a , b , c ] :
print( "Es una lista" )
print( a , b , c )
case {"Nombre":nombre ,
"Apellido":apellido } :
print( "Es un diccionario" )
print( nombre , apellido )
case _ :
print( "No sé qué es" )
hay que tener cuidado, ya que mucha gente e incluso Colab ocupan Python 3.7.12
_________________________________________________________________________

*****10.9- Arrays*****

Además de las listas, tuplas, diccionarios y conjuntos, podemos implementar más estruct6uras de datos, ya que hay otras que
son típicas de otros lenguajes pero que Python no tiene implementadas de manera nativa. La primera estructura de la que
habalaremos son los arrays, que son colecciones que guardan datos en memoria de manera lineal y no como las listas, que para
cada valor reservan un poco de memoria incluso si los datos no necesitan tanta.

Los arrays son elementos iterables no dinámicos, es decir, no son mutables. Son estructuras ordenadas , es decir, admiten
una indexación, a las cuales no les podemos agregar ni quitar elementos. Solo almacenan números y caracteres.

Por lo general un array tiene los siguientes métodos:


- Creación
- Longitud
- Representación de sus elementos en string, o el mismo array
- Pertenecia
- Acceso por índices
- Remplazo

Los arrays irremediablemente se basan en listas en el caso de Python, así que lo construiremos en una clase con los métodos
de lista:
class Array() :
def __init__( self , capacity , fill_value = None ) :
self.items = [ fill_value for i in range( capacity )]

def __len__( self ) :


return len( self.items )

def __str__( self ) :


return str( self.items )

def __iter_( self ) :


return iter( self.items )

def __getitem__( self , index ) :


return self.items[ index ]

def __setitem__( self , index , new_item )


self.items[ index ] = new_item
así queda implementada la clase Array, lo mejor de todo es que podemos usar los métods de Python, o los que acabamos de
escribir, es decir, si definimos:
x = Array( 5 )
podemos aplicar len( x ) , str( x ) , print( x ) , print( x[ 4 ] ) y Python lo tratará como si ya los conociera. Esto en el
fondo es porque se trata de una lista. También podemos usar los métodos que construimos, por ejemplo: x.__len__()
x.__str__() , x.__getitem__( 4 ) , x.__setitem__( 4 , 100 ). Podemos incluso iterar directamente:
for element in x :
print( element )

Podemos implementarle nuevos métodos, por ejemplo, a continuación implementamos dos métodos, uno rellena con números
aleatorios del uno al 100 y el otro con números consecutivos iniciando por 0:
import random

class Array() :
def __init__( self , capacity , fill_value = None ) :
self.items = [ fill_value for i in range( capacity ) ]

def __len__( self ) :


return len( self.items )

def __str__( self ) :


return str( self.items )
def __iter__( self ) :
return iter( self.items )

def __getitem__( self , index ) :


return self.items[ index ]

def __setitem__( self , index , new_item ) :


self.items[ index ] = new_item

def __randomreplace__( self ) :


for i in range( len( self.items ) ) :
self.items[ i ] = random.randint( 1 , 100 )

def __sequencereplace__( self ) :


for i in range ( len( self.items ) ) :
self.items[ i ] = i

def __sum__( self ) :


return sum( self.items )

Podemos extender a dos( matrices ), tres o más dimensiones( tensores ) nuestros Arrays. A continuación presentamos una
forma de extenderlos a dos dimensiones:
class Grid( object ):
"""Represents a two-dimensional array."""
def __init__( self , rows , columns , fill_value = None ) :
self.data = Array( rows )
for row in range( rows ) :
self.data[ row ] = Array( columns , fill_value )

def get_height( self ) :


"Returns the number of rows."
return len( self.data )

def get_width( self ) :


"""Returns the number of columns."""
return len( self.data[ 0 ] )

def __getitem__( self , index ) :


"""Supports two-dimensional indexing with [row][column]."""
return self.data[ index ]

def __str__( self ) :


"""Returns a string representation of the grid."""
result = ""

for row in range( self.get_height() ) :


for col in range( self.get_width() ) :
result += str( self.data[ row ][ col ] ) + " "

result += "\n"

return str( result )

En Python, la librería numpy tiene implementados sus propios arrays n dimensionales. Más adelante veremos como usarlos.
_________________________________________________________________________

*****10.10- Nodos y singly linked list*****

Los nodos son estruturas que alamacenan datos y que apunta mediante un puntero a otro nodo. La implementación de nodos es
bastante sencilla, medianre la siguiente clase:
Class Node() :
def __init__( self , data , next = None ) :
sefl.data = data
self.next = next
podemos construir algunos nodos:
nodo1 = None
nodo2 = Node( "A" , nodo1 )
nodo3 = Node( "B" , nodo2 )

Podemos crear una cadena de nodos guardándola en una variable, por ejemplo:
head = None

for i in range( 5 ) :
head = Node( i**2 , head )
entonces head será una variable tipo __main__.Node que es una especie de iterador.
Para mirar su contenido hacemos:
while head != None:
print( head.data )
head = head.next
_____________________________________________________________________________________________________________________________

Capitulo 11: Módulo math y cmath

Para importar este módulo lo hacemos como siempre:


import math as mt

Las constantes obvias:


math.pi - Número pi
math.e - Número e
math.tau - 2 por pi
math.inf - infinito positivo (ocho dormido XD)
-math.inf - menos infinito
math.nan - not a number

Algunos métodos clásicos:


math.floor( x ) - función menor entero de x
math.ceil( x ) - funcion mayor entero de x
math.trunc( x ) - función parte entera de x
math.fmod( x , y ) - es x%y pero más preciso
math.fsum( iterable ) - suma de un iterable redondeada. Recomendable para no arrastrar errores
math.modf( x ) - devuelve una tupla con la parte decimal y la parte entera
math.exp( x ) - es e^x
math.expm1( x ) - es e^x - 1
math.frexp( x ) - nos devuelve la tupla ( m , i ) tal que x = m*2^i
math.ldexp( m , i ) - nos devuelve y = m*2^i
math.log( x ) - logaritmo natural de x
math.log( x , a ) - logaritmo base a de x
math.log1p( x ) - log( 1 + x )
math.log2( x ) - logaritmo base 2
math.log10( x ) - logaritmo base 10
math.pow( a , b ) - a^b
math.sqrt( x ) - raíz cuadrada de x
math.gcd( a , b ) - mcd de a y b
math.lcm( a , b ) - mcm de a y b (versión 3.9)
math.fabs( x ) - valor absoluto con presición
math.factorial( x ) - factorial de x
math.comb( n , m ) (versiones viejas) - combinaciones de n
math.copysign( a , b ) - compia el signo del segundo en el primero
math.radians( x ) - grados a radianes
math.degrees( x ) - radianes a grados
math.sin( x ) - función seno
math.cos( x ) - función coseno
math.tan( x ) - función tangente
math.atan( x ) - función arcoseno
math.acos( x ) - función arcocoseno
math.atan( x ) - función arcotangente
math.atan2( x , y ) - arco tangente a partír de x e y (coordenas cartesianas)
math.hypot( x , y ) - norma de ( x , y )
math.sinh( x ) - función seno hiperbólico
math.cosh( x ) - función coseno hiperbólico
math.tanh( x ) - función tangente hiperbólica
math.atanh( x ) - función arcoseno hiperbólico
math.acosh( x ) - función arcocoseno hiperbólico
math.atanh( x ) - función arcotangente hiperbólico
math.isclose( x , y ) - bool si x e y están cercanos en al menos 1 x 10^( -9 )
math.isclose( x , y , rel_tol = p ) - bool si x e y están cercanos en al menos max( x , y ) en porcentaje p
math.isfinite( x ) - True si es finito
math.isinf( x ) - True si es infinito
math.isnan( x ) - True sis es nan
math.erf( x ) - función error de 0 a x normalizada por 2/pi
math.erfc( x ) - 1 - erf( x )
math.gamma( x ) - función gamma
math.lgamma( x ) - log( gamma( x ) )

recordemos finalmente que podemos ver los métodos con dir(math) o dir(cmath)
_________________________________________________________________________

*****Módulo cmath*****

Las constantes clásicas:


cmath.pi
cmath.e
cmath.tau
cmath.inf
cmath.nan
cmath.infj - infinito en la parte imaginaria, 0 la real
cmath.nanj - nan en la parte imaginaria, 0 la real

Las coordenadas polares:


cmath.phase( z ) - argumento del complejo z
cmath.polar( z ) - tupla en polares ( radio , arg )
cmath.rect( radio , arg ) - complejo en cartesianas

Funciones clásicas:

cmath.exp( z ) - es e^z
cmath.log( x ) - logaritmo natural de x
cmath.log( x , a ) - logaritmo base a de x
cmath.log10( x ) - logaritmo base 10
cmath.sqrt( x ) - raíz cuadrada de x
cmath.sin( x ) - función seno
cmath.cos( x ) - función coseno
cmath.tan( x ) - función tangente
cmath.asin( x ) - función arcoseno
cmath.acos( x ) - función arcocoseno
cmath.atan( x ) - función arcotangente
cmath.sinh( x ) - función seno hiperbólico
cmath.cosh( x ) - función coseno hiperbólico
cmath.tanh( x ) - función tangente hiperbólica
cmath.atanh( x ) - función arcoseno hiperbólico
cmath.acosh( x ) - función arcocoseno hiperbólico
cmath.atanh( x ) - función arcotangente hiperbólico
math.isclose( x , y ) - bool si x e y están cercanos en al menos una distancia 1 x 10^( -9 )
math.isclose( x , y , rel_tol = p ) - bool si x e y están cercanos en al menos max(x,y) en porcentaje p
math.isfinite( x ) - True si es finito
math.isinf( x ) - True si es infinito nan al menos una parte
math.isnan( x ) - True si es nan al menos una parte

____________________________________________________________________________________________________________________________

Capítulo 16: Módulos especiales de Python

*****16.1- os*****

En Python hay un módulo que nos permite interactuar con nuestro sistema operativos y sus funciones, como apagar la pc,
eliminar archivos, entre otras cosas. Este módulo es el módulo os. Vamos a explorar solo algunas de sus funcionalidades.

Podemos obtener la ruta en la que estamos trabajando con el método .getcwd, es decir, es el análogo de pwd en BASH:
import os

ruta = os.getcwd()
print( ruta )

Para cambiar de ruta durante la ejecución usamos el método .chdir(ruta), con esto podemos los archivos de la ruta, por
ejemplo txt que debamos importar:
os.chdir( "C:\\Users\\user\\Desktop\\Cursos" )
en el caso de Windows hay que agregar dobles barras invertidas.

Para crear carpetas usamos el método .makesdirs(ruta) donde el final de la ruta es la carpeta que se quiere crear.
os.makedirs( "/content/nueva" )

Para borrar un fichero usamos el método .remove() con la ruta y la extensión, por ejemplo:
import os
os.remove("/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo.txt")
no podemos eliminar un archivo inexistente o python nos marca un error.

Antes de eliminar un archivo, una buena práctica es checar si existe. Para ello usamos el método os.path.exixts()
con el argumento la ruta del archivo que queremos eliminar. Una buena idea es colocar en un string la ruta para
sustituirla fácilmente:
ruta = "/content/drive/MyDrive/Colab Notebooks/ArchivosCSV/nuevo.txt"
if os.path.exists(ruta):
os.remove(ruta)
else:
print("El archivo que desea eliminar no existe.")

Para eliminar una carpeta usamos os.rmdir(path) (remove directory), donde el path es un string que contiene la ruta de la
carpeta, sin embargo, solo nos permite eliminar una carpeta cuando está vacía, primero debemos borrar sus elementos con
os.remove()
os.rmdir( "/content/nueva" )

Para listar los arhivos de una carpeta podemos usar la función os.listdir( ruta )

La función os.walk(ruta) nos devuelve un generador que podemos iterar. Contiene tuplas con 3 entradas, en la primera entrada
tiene la ruta de la carpeta examinada, en la segunda las subcarpetas de la carpeta examinada y en la tercera los archivos
que contiene. Esto lo hace con todas las subcarpetas de la ruta, incluyendo las que están anidadas con otras.
_________________________________________________________________________

*****16.2- pathlib*****

Este módulo nos permite controlar las rutas con un formato en común independientemente del sistema operativo, por ejemplo,
ya hemos visto que las rutas para windows necesitan dos barras invertidas para que Python no las considere caracteres
escapantes. Para importarla hacemos:
import pathlib

esta librería cuenta con muchas clases demasiado especializadas que podemos investigar fácilmente en la documentación
oficial de Python. La clase más práctica y que nos va a interesar es la clase Path, a esta clase se le pasa por parámetro
la ruta que queremos convertir a una ruta "universal" con el formato clásico de los sistemas Linux:
import pathlib
ruta = pathlib.Path(/Documentos/Proyecto)

Las rutas instanciadas con Path pueden concatenarse con una diagonal simple /, por ejemplo:
ruta = pathlib.Path(/Documentos/Proyecto)
ruta_archivo = ruta / "requeriments.txt"

Las rutas creadas con Path pueden pasarse sin problema a funciones que requieran una ruta, como la función open() que ya
vimos en el capítulo 14:
ruta = pathlib.Path(/Documentos/Proyecto)
ruta_archivo = ruta / "requeriments.txt"

f = open( ruta_archivo )
print( f )
f.close()

Para leer un archivo de texto podemos aplicar el método .read_text() a un objeto Path, por ejemplo:
ruta = pathlib.Path(/Documentos/Proyecto)
ruta_archivo = ruta / "requeriments.txt"
print( ruta_archivo.read_text() )
nota que esto es bastante útil, pues no es necesario abrirlo con open o utilizar la excepción with.

Al igual que el módulo os, pathlib puede checar si un archivo existe con el método .exists(), por ejemplo:
ruta = pathlib.Path(/Documentos/Proyecto)
ruta_archivo = ruta / "requeriments.txt"

if not ruta_archivo.exists():
print( "Este archivo no existe" )
else:
print( "Genial, el archivo sí existe" )

Otra clase de este módulo es PureWindowsPath(ruta), la cual transforma una ruta que le demos a una ruta de windows.

Ya que lo más común es solo usar la clase Path, es más cómodo importarla solamente a ella:
from pathlib import Path

La clase Path contiene un método de clase .home() que nos devuelve el home de la maquina donde trabajamos, el lenguaje BASH
esto es equivalente a la dirección que nos devuelve el comando cd por sí solo:
import pathlib as pl

ruta_home = pl.Path.home()
print( ruta_home )

Si ya tenemos la ruta de un archivo, podemos copiar toda la ruta exactamente igual salvo el nombre y colocársela a otro
archivo de la misma ubicación con el método .with_name(nombre_archivo), por ejemplo:
primer = pl.Path( "/Documentos/clase/archvo1.txt" )
segundo = primer.with_name( "archivo2.txt" )
_________________________________________________________________________

*****16.3- Random*****

El módulo random es un módulo dedicado a la generación de procesos aleatorios, elecciones, distribuciones etc. Aquí solo
exploramos los métodos más usados. Para importarla hacemos lo de siempre:
import random
es común encontrarlo con el alias rn:
import random as rn
El método .randint(n1,n2) nos devuelve un número aleatorio entre n1 y n2 de manera creciente.

El método .uniform(n1,n2) de la libreria random hace lo análogo a .randint() pero este devuelve un float contenido entre
n1 y n2 de manera creciente. Si el colocamos un tercer parámetro nos devolverá un aray de numpy.

El método .random() sin argumentos devuelve un float entre 0 y 1.

La función .choice(lista) elige un valor aleatorio de una lista.

El método .shuffle(lista), cambia de manera aleatoria el orden de los elementos de la lista.

El método .seed(n1) es el que alimenta internamente a aleatoriedad del módulo random, es la "semilla" y sirve para controlar
los valores que devuelven los métodos de la librería completa. Si escribimos rn.seed(3) por ejemplo, un mismo método nos
devolverá siempre el mismo valor al ejecutarlo:
rn.seed( 3 )
print( rn.random() )
_________________________________________________________________________

*****16.4- Pylint*****

Del lado izquierdo en la página de Python podemos encontrar las propuestas para mejorar Python en "PEP index". La PEP número
8 es muy especial pues contiene las buenas prácticas de programación de Python.

Pylint es un módulo que nos permite encontrar errores previos a la ejecución y correcciones de estilo de acuerdo a la PEP-8,
esto nos permite corregirlo para tener un código más limpio. Para instalar el módulo, usamos pip en la consola:
pip install pylint

Este módulo se usa desde la consola, para ello vamos hasta la carpeta del archivo a analizar, y escribimos pylint seguido
del nombre del archivo con extensión, una bandera -r de reporte y la y de sí en inglés:
pylint recetario.py -r y
en conslola se mostrará un mensaje parecido al siguiente:
Report
======
112 statements analysed.

Statistics by type
------------------

+---------+-------+-----------+-----------+------------+---------+
|type |number |old number |difference |%documented |%badname |
+=========+=======+===========+===========+============+=========+
|module |1 |NC |NC |0.00 |0.00 |
+---------+-------+-----------+-----------+------------+---------+
|class |0 |NC |NC |0 |0 |
+---------+-------+-----------+-----------+------------+---------+
|method |0 |NC |NC |0 |0 |
+---------+-------+-----------+-----------+------------+---------+
|function |11 |NC |NC |36.36 |0.00 |
+---------+-------+-----------+-----------+------------+---------+

Raw metrics
-----------

+----------+-------+------+---------+-----------+
|type |number |% |previous |difference |
+==========+=======+======+=========+===========+
|code |124 |55.61 |NC |NC |
+----------+-------+------+---------+-----------+
|docstring |43 |19.28 |NC |NC |
+----------+-------+------+---------+-----------+
|comment |2 |0.90 |NC |NC |
+----------+-------+------+---------+-----------+
|empty |54 |24.22 |NC |NC |
+----------+-------+------+---------+-----------+

Duplication
-----------

+-------------------------+------+---------+-----------+
| |now |previous |difference |
+=========================+======+=========+===========+
|nb duplicated lines |0 |NC |NC |
+-------------------------+------+---------+-----------+
|percent duplicated lines |0.000 |NC |NC |
+-------------------------+------+---------+-----------+
Messages by category
--------------------

+-----------+-------+---------+-----------+
|type |number |previous |difference |
+===========+=======+=========+===========+
|convention |29 |NC |NC |
+-----------+-------+---------+-----------+
|refactor |0 |NC |NC |
+-----------+-------+---------+-----------+
|warning |12 |NC |NC |
+-----------+-------+---------+-----------+
|error |0 |NC |NC |
+-----------+-------+---------+-----------+

Messages
--------

+---------------------------+------------+
|message id |occurrences |
+===========================+============+
|trailing-whitespace |14 |
+---------------------------+------------+
|missing-function-docstring |7 |
+---------------------------+------------+
|unused-variable |6 |
+---------------------------+------------+
|invalid-name |3 |
+---------------------------+------------+
|consider-using-f-string |3 |
+---------------------------+------------+
|unspecified-encoding |2 |
+---------------------------+------------+
|redefined-outer-name |2 |
+---------------------------+------------+
|redefined-builtin |2 |
+---------------------------+------------+
|missing-module-docstring |1 |
+---------------------------+------------+
|missing-final-newline |1 |
+---------------------------+------------+

-----------------------------------
Your code has been rated at 6.34/10
las tablas son muy transparentes en lo que evalúan. Nota que al final le da una calificación al código. Lo recomendable es
que la calificación mínima sea un 7. No debemos preocuparnos por llegar a un 10, es muy poco probable lograrlo y solo la
máquina se ve beneficiada, no nuestro entendimiento.
_________________________________________________________________________

*****16.5- collections*****

El módulo collections es un módulo ya cargado en Python que contiene métodos, clases y atributos diseñados para trabajar
algunas funcionalidades extras de los iterables.

La clase Counter(iterable) nos crea un diccionario que tiene por claves los elementos del iterable argumento y tiene por
valores el número de veces que se repite cada elemento en el iterable argumento. Por ejemplo:
from collections import Counter

numeros = [8,6,9,5,4,5,5,5,8,7,4,5,4,4]
print( Counter( numeros ) )
esto mismo funciona con los caracteres de una cadena (pues recordemos que son iterbales).

Una forma útil de usar Counter es averiguar cuántas veces se repite una palabra en una cadena, por ejemplo:
cadena = "al pan pan y al vino vio"
print( Counter( cadena.split() ) )

La clase Counter contiene los métodos de los diccionarios y algunos métodos extras, por ejemplo, el método .most_common()
nos devuelve una lista de tuplas que nuevamente contienen lo que vimos arriba pero en un orden descendente de aparición.
numeros = [8,6,9,5,4,5,5,5,8,7,4,5,4,4]
serie = Counter( numeros )
print( serie.most_common() )
podemos agregarle argumento a .most_common(numero) y nos muestra los primeros elementos de la lista que obtuvimos al no
colocar argumentos.

Al castear un objeto Counter, podemos obtener ls elementos sin repetir.


Una función útil para diccionarios es la función defaultdict(funcion), esta función permite que, al llamar a un diccionario
con una clave que no existe, no nos salte un error, sino que genera la clave y la asigna un valor por defecto:
from collections import defaultdict

mi_dic = defauldict( lambda : "nada" )


mi_dic[ "uno" ] = "verde"
print( mi_dic[ "dos" ] )

Una cola doblemente terminada, o deque, admite agregar y eliminar elementos de cualquier extremo de la cola. Las pilas y
colas más comunes son formas degeneradas de deques, donde las entradas y salidas están restringida a un solo extremo.
import collections

d = collections.deque('abcdefg')
print('Deque:', d)
print('Length:', len(d))
print('Left end:', d[0])
print('Right end:', d[-1])

d.remove('c')
print('remove(c):', d)
esto nos devuelve
Deque: deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
Length: 7
Left end: a
Right end: g
remove(c): deque(['a', 'b', 'd', 'e', 'f', 'g'])

Como las deques son un tipo de contenedor de secuencia, admiten algunas de las mismas operaciones que list, como examinar
los contenidos con __getitem__(), determinando la longitud y eliminando elementos desde el medio de la cola al hacer
coincidir la identidad.
_________________________________________________________________________

*****16.6- shutil*****

Este módulo tiene funcionalidades del sistema al igual que os, pero un poco distintas. El módulo ya viene en Python por
defecto.

Con shutil podemos mover archivos a través de la función .move( archivo.txt , ruta_nueva), donde el archivo debe estar
posicionad en la carpeta de trabajo o moviéndose a otra carpero con os.

Podemos eliminar una carpeta con todo y su contenido de manera irreversible a través de la función .rmtree( ruta ),
aunque ua alternativa es instalar el módulo send2trash y usar send2trash.send2trash( ruta o nombre ), esto nos elimina
el archivo, pero lo manda a la papelería de reciclaje para poder restaurarlo si es necesario.

Este módulo también nos permite comprimir y descomprimir archivos zip, para ello debemos hacer uso de la función
.make_archive(ruta_destino , formato , ruta_origen), donde las rtas deben contenet el nombre del archivo a comprimir y el
nombre del nuevo archivo comprimido, el segundo sin extensión:
import shutil

ruta_origen = "/Documentos/Carpeta"
ruta_destino ="/Documentos/Carpeta_comprimida"
shutil.make_archive( ruta_destino , "zip", ruta_origen )

Para descomprimir usamos la función .unpack_archive(ruta_origen , ruta_destino , formato):


import shutil

ruta_origen = "/Documentos/Carpeta_comprimida.zip"
ruta_destino ="/Documentos/Carpeta_descomprimida"
shutil.unpack_archive( ruta_origen ,ruta_destino , "zip" )

_________________________________________________________________________

*****16.7- datetime y pytz*****

datetime es un módulo de Python que nos permite acceder a fechas y horas. Es muy necesario renombrarlo a dt
import datetime as dt
ya que este módulo tiene una clase que se llama datetime.

El método dt.datetime.now() nos da la fecha y la hora hasta microsegundos. Podemos guardar esta fecha en una variable.

Podemos acceder a toda esa información con los atributos aplicados a una fecha:
.year: año
.month: mes
.day: día
.hour: hora
.minute: minuto
.second: segundo
.microsecond
por ejemplo:
hoy = dt.datetime.now()
hoy.microsecond

Este método date toma como parámetros el año (year), mes (month), día (day), hora (hour), minuto (minute), segundo (second)
y microsegundo (microsecond). Los 3 primeros, relacionados con la fecha, son parámetros obligatorios. Los parámetros
restantes, los relacionados con el tiempo, son opcionales.
d = dt.datetime(2020, 9, 22, 12, 30)
print(d)
el método dt.datetime() tiene un parámetro adicional tzinfo = al cual se le da la zona horaria

Si qeremos cambiar alguno de los datos introducidos en una fecha, podemos usar el método replace(), por ejemplo:
d.replace( month = 11 )

Para cambiar el formato de una fecha usamos el método .strftime(). En el parámetro format = de este método podemos indicar
alguna de las siguientes opciones en string:
Código Descripción
%Y Año, versión completa
%y Año, versión abreviada (sin siglo)
%B Mes, versión completa (idioma inglés)
%b Mes, versión abreviada (idioma inglés)
%m Mes, versión numérica
%d Día del mes
%j Día del año, 001-366
%A Día de la semana, versión completa (idioma inglés)
%a Día de la semana, versión abreviada (idioma inglés)
%w Día de la semana, versión numérica (0 = Domingo, 1 = Lunes, ..., 6 = Sábado)
%W Semana del año, con lunes como primer día
%U Semana del año, con domingo como primer día
%H Hora, 00-23
%I Hora, 00-12
%p AM / PM
%M Minuto, 00-59
%S Segundo, 00-59
%f Microsegundo, 000000-999999
%Z Zona horaria
%z Offset de UTC
%c Versión local de fecha y hora
%x Versión local de fecha
%X Versión local de hora
%% Caracter %
%G Año ISO 8601
%u Día de la semana ISO 8601
%V Semana del año ISO 8601
Por ejemplo:
d = dt.datetime(2020, 9, 22, 12, 30)
print(d.strftime("%B %d, %Y"))
la coma aparecerá justo como una coma. Otro ejemplo
d = dt.datetime(2020, 9, 22, 12, 30)
print(d.strftime("%d %b %Y, %I:%M%p"))

ISO 8601 es un estándar internacional para la representación de fechas y horas. Para imprimir una fecha en este estándar
usamos sobre una fecha el método .isoformat() sin parámetro. Existe el método más general, dada una fecha con un formato
podemos transformarlo en un objeto datetime con el método .strptime(), que nos permite crear un objeto datetime a partir
de un string. Este método tiene dos parámetros, el primero es la fecha en string y el segundo es un string con el formato
indicado por ejemplo:
dt.datetime.strptime("September 22, 2020", "%B %d, %Y")
¡Cuidado! El patrón debe coincidir a la perfección. Eso incluye las comas, los : y los espacios en blanco.

Los objetos datetime no tienen información de la zona horaria. Para acceder a la zona horaria uzamos el atributo
.zinfo sin parámetros. En el siguiente link podemos encontrar la zona horaria:
https://www.timeanddate.com/time/map/

Existe una forma de manejar husos horarios, esto se logra con la librería pytz (Python time zone). La propiedad
.all_timezones aplicado a la librería nos devuelve una lista con zonas horarias
import pytz
pytz.all_timezones

El módulo pytz también tiene el atributo .country_names, que se trata de un diccionario que por claves tiene un código de
2 letras (Código ISO Alpha-2) de los paises disponibles, cuyo valor es el nombre completo.
Podemos recorrerlo con el método .items() con en los diccionarios comunes:
for key, value in pytz.country_names.items():
print(key, "=", value)
Luego también está el atributo .country_timezones que también es un diccionario que por clave tiene los países en código
ISO Alpha-2 y por valor las zonas horarias para cada país clave:
for key, value in pytz.country_timezones.items():
print(key, "=", value)

pytz tiene una clase timezone y podemos crear un objeto de este tipo suministrándole la región que podemos averiguar del
diccionario anterior, haciendo por ejemplo:
zona = pytz.timezone("Europe/Madrid")
dada una fecha en datetime podemos asignarle un objeto timezone para especificar su zona horaria. Esto lo logramos a través
del método .localize(timezone), por ejemplo:
fecha = dt.datetime(2021,9,22,12,30)
zona = pytz.timezone("Europe/Madrid")
fecha = fecha.localize( zona )
aunque el método dt.now( ) puede recibir como argumeto un objeto pytz, por ejemplo:
bogota_timezone = pytz.timezone( "América/Bogota" )
bogota_date = datetime.now( bogota_timezone )

Hay otra forma de checar atributos de una fecha, el método de instancia .strftime() que usa el mismo código que el método
de clase dt.datime.strp(time), Más info en el siguiente link:
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior

Otra clase importante de dt es la clase date( año , mes , día), a esta le podemos colocar los parémetros mostrados e incluso
podemos operar con fechas, por ejemplo:
nacimiento = dt.date( 1994 , 3 , 5 )
defuncion = dt.date( 2085 , 6 , 19 )
vida = defuncion - nacimiento

print( vida )

Análogamente existe la clase time()

_________________________________________________________________________

*****16.8- Series temporales con datetime y matplotlib.pyplot*****

Dada una lista de fechas del tipo datetime, matplotlib contiene una función para graficar series temporales, el método
.plot_date(). En el siguiente ejemplo tomamos una lista de fechas en string, las convertimos al tipo datetime y luego las
graficamos con .plot_date()

dates = [ "1/9/2020" , "2/9/2020" , "3/9/2020" , "4/9/2020" , "5/9/2020" ,


"6/9/2020" , "7/9/2020" , "8/9/2020" , "9/9/2020" , "10/9/2020" ,
"11/9/2020" , "12/9/2020" , "13/9/2020" , "14/9/2020" , "15/9/2020" ,
"16/9/2020" , "17/9/2020" , "18/9/2020" , "19/9/2020" , "20/9/2020" ,
"21/9/2020" , "22/9/2020" , "23/9/2020" , "24/9/2020" , "25/9/2020" ,
"26/9/2020" , "27/9/2020" , "28/9/2020" , "29/9/2020" , "30/9/2020" ]

x = [ dt.datetime.strptime( d , "%d/%m/%Y" ).date() for d in dates] # .date() conserva solo la fecha sin la hora
y = np.random.randint( 10000 , 20000 , len( x ) )

plt.title( "Total Ventas en Septiembre 2020" )


plt.xlabel( "Día del mes" )
plt.xticks( rotation = 90 )
plt.ylabel( "Total ventas" )
plt.plot_date( x , y , c = "blue" , ls = "--" , lw = 2 , tz = "Europe/Madrid" )
plt.show()

información más detallada de las series temporales en esta paquetería las podemos encontrar en la siguiente página:
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.plot_date.html
_________________________________________________________________________

*****16.8- time y timeit*****


_________________________________________________________________________

*****16.9- re*****

El módulo re(expresiones regulares) es un módulo que nos permite buscar coincidencias de strings en carpetas, archivos y
contenidos. Este módulo ya viene por defecto en Python. Este módulo también es muy útil por ejemplo, si el usuario debe
introducir una clave y requieres que tenga un formato especial.

Para indicarle a Python que una expresión se trata de una expresión regular, debemos colocar la letra r antes de una cadena,
por ejemplo:
patron = r"\d\d\d-\d\d\d-\d\d\d\d"

para RE está el siguiente sistema de caracteres especiales:


caracterdescripción ejemplo

\d dígito numérico v\d.\d\d


\w caracter alfanumérico \w\w\w-\w\w
\s espacio en blanco numero\s\d\d
\D NO numérico \D\D\D\D
\W NO alfanumérico \W\W\W
\S No espacio en blanco \S\S\S\S

Por otro lado tenemos los cuantificadores:


caracterdescripción ejemplo

+ 1 o más veces código_\d-\d+


{n} se repite n veces \d-\d{4}
{n,m} se repite de n a m veces \w{3,5}
{n,} desde n hacia arriba -\d{4,}-
* 0 o más veces \w\s*\w
? 1 o 0 veces(usado en plurales) casas?

Para realizar una búsqueda, necesitamos usar la función re.search( patrón , texto ) y almacenarlo en una variable para
luego poder imprimirla, por ejemplo;
import re

texto = "si necesitas ayuda llama al (658)-598-9977 las 24 horas al servivio de ayuda online"
patron = "ayuda"
busqueda = re.search( patron , texto )
print( busqueda )
si se encuentra una coincidencia nos marcará un Match y en una tupla el span con las posiciones donde se encontró. Si no
encuentra coincidencias nos arroja un None. Nota que el código de arriba solo encuentra la primera palabra "ayuda" pero no
la segunda, por lo que, más adelante veremos como encontrar todas.

Podemos simplemente ver las posiciones colocando el método .span() sin argumentos, sobre la busqueda:
print( busqueda.span() )
para mostrar solo el inicio o el final aplicamos los métodos .start() y .end() sin argumentos a la busqueda como hicimos con
span().

Si queremos encontrar todas las coincidencias de ayuda, podemos usar la función re.findall( patron , texto ), esto nos arroja
una lista con las coincidencias. En el ejmeplo de arriba:
luego poder imprimirla, por ejemplo;
import re

texto = "si necesitas ayuda llama al (658)-598-9977 las 24 horas al servivio de ayuda online"
patron = "ayuda"
busqueda = re.findall( patron , texto )
print( busqueda )
no es muy efectivo, pues solo nos retorna una lista con las palabras ayuda. La función findall(), es más útil con las
expresiones regulares, por ejemplo, para encontrar todas las palabras que comienzan con la letra a del texto anterior
escribimos el código:
import re

texto = "si necesitas ayuda llama al (658)-598-9977 las 24 horas al servivio de ayuda online"
patron = r"a\w*"

busqueda = re.findall( patron , texto )


print( busqueda )
en este caso, la lista quedará llena con las palabras que comienzan con a.

Si ahora sí queremos encontrar las ubicaciones de "ayuda" en el texto original usamos el método re.finditer() que nos
devuelve un iterador que contiene el match y la tupla que ya habiamos comentado en re.search():
import re

texto = "si necesitas ayuda llama al (658)-598-9977 las 24 horas al servivio de ayuda online"
patron = "ayuda"

for elemento in re.finditer( patron , texto ):


print( elemento.span() )

Dada una búsqueda realizada con re.search( ) y con expresiones regulares, el método .group() nos devuelve el primer
resultado que encontró:
import re

texto = "si necesitas ayuda llama al (658)-598-9977 las 24 horas al servivio de ayuda online"
patron = r"\w{2}"

busqueda = re.search( patron , texto )


print( busqueda.group() )

Podemos también hacer búsquedas condicionadas con algunos operadores especiales, por ejemplo, el símbolo | significa "o",
entonces podemos buscar alguna de varias palabras, por ejemplo:
import re

texto = "No atendemos los lunes por la tarde"


buscar = re.search( r"lunes|martes" , texto )
print( buscar )

Otro operador especial es el punto o comodín, que nos permite buscar oincidencias junto con lo que sea que se encuente en lugar
del punto, por ejemplo:
import re

texto = "No atendemos los lunes por la tarde"


buscar = re.search( r"....demos" , texto )
print( buscar )

El circunflejo ^ nos indica si un caracter se encuentra al inicio de la cadena total, pero este se coloca a la izquierda:
import re

texto = "No atendemos los lunes por la tarde"


buscar = re.search( r"^N" , texto )
print( buscar )
el símbolo $ hace lo mismo que ^ pero al final de la cadena, de hecho también se colcoa al final:
buscar = re.search( r"e$" , texto )

Los corchetes con ^ dentro nos permiten omitit lo que se encuentr dentro de ellosn por ejemplo, el siguiente código nos
regresa una lista con los substrings de una letra que no son espacios en blanco:
import re

texto = "No atendemos los lunes por la tarde"


buscar = re.findall( r"[^\s]" , texto )
print( buscar )
el siguiente código nos regresa substrings que no tiene espacios en blanco, análogo a .split() para una cadena:
import re

texto = "No atendemos los lunes por la tarde"


buscar = re.findall( r"[^\s]+" , texto )
print( buscar )

_________________________________________________________________________

*****16.10- zipfile*****

El módulo zipfile como su nombre nos lo dice, permite comprimir y descomprimir archivos zip. Este módulo se maneja de
manera parecida al open() que vimos para .txt, .csv y. JSON. Para ello debemos usar la clase ZipFile(nombre,modo), donde
el primer parámetro es el nombre del archivo que queremos crear y el segundo es el modo. Para crear un archi, es necesario
usar el modo "w"(write), por ejemplo:
import zipfile
mi_zip = zipfile.ZipFile( "archivo_comprimido.zip" , "w")
podemos agregar archivos de la misma carpeta del proyecto aplicando el método .write( nombre ) aplicado al objeto ZipFile y
cerramos con .close():
import zipfile
mi_zip = zipfile.ZipFile( "archivo_comprimido.zip" , "w")
mi_zip.write( "mi_archivo.txt" )
mi_zip.close()
el archivo será movido al comprimido.

Para descomprimir un zip, cambiams el modo a "r" y aplicamos el método .


import zipfile

zip_abierto = zipfile.ZipFile( "archivo_comprimido.zip" , "a")


zip_abierto.extractall()
____________________________________________________________________________________________________________________________

Capítulo 18: Tkinter

*****18.1- Ventana principal*****

Como siempre comenzamos importando tkinter, aunque en este caso lo importaremos todo:
from tkinter import *

Para comenzar alguna aplicación, comenzamos inicializando un objeto TK() sin parámetros, que será nuestra aplicación y la
guardamos en una veriable:
aplicacion = Tk()
al ejecitar esta ventana, se inicia y se cierra automáticamente. Para que no se cierre aplicamos el método .mainloop() sin
parámetros a nuestra aplicación:
aplicación.mainloop()
al correr el código, ya nos aparecerá una ventana que incluso se puede maximizar. Es importante colocar esto al final

Para modificar el tamaño específico de la ventana, le aplicamos .geometry( "anchoxalto+x+y" ) a nuestra aplicación, el
tamaño y las coordenadas del pixel donde inicia se colocan en string como se muestra:
aplicacion.geometry( "1020x630+0+0" )
si el usuario maximiza la ventana, destrozará nuestro diseño inicial.

Para evitar que el usuario maximice la ventana aplicamos sobre nuestra aplicación el método .resizable(n1,n2) con argumentos
0,0, es decir.
aplicacion.resizable( 0 , 0 )
esto bloquea en automático el botón para maximizar y minimizar.

Para cambiar el título de la ventana, sobre la ventana aplicamos .title("titulo") cn el nombre del título en string:
aplicacion.title("Mi restaurante - sistema de facturación")

Para cambiar el color del fondo, a la aplicación de colocamos el método .config() con el argumento bg= (background)
asignándole el nombre de un color que viene en la página:
https://es.wikibooks.org/wiki/Python/Interfaz_gr%C3%A1fica_con_Tkinter/Los_nombres_de_los_colores
o con RGB como ya lo hicimos antes:
aplicacion.config( bg = "burlywood" )
_________________________________________________________________________

*****18.2.- Paneles*****

Al momento de crear una aplicación, es importante primero planear la disposición de sus partes, es decir, los lugares donde
irán los elementos de la aplicación, esto puede lograrse haciendo un diagrama boceto muy sencillo, ya sea a mano o mediante
alguna aplicación de dibujo tan simple como Paint de Windows

Para crear paneles en tkinter, creamos un objeto de la clase Frame(), su primer argumento es la aplicación donde estara
situada, otro argumento interesante es el borde bd = y el relieve relief = para darle un relieve a nuestro panel. Para el
relieve hay 5 opciones:
-FLAT
-RAISE
-SUNKEN
-GROOVE
-RIDGE
Para lanzar el panel a la aplicación usamos el método .pack( side = ), donde el argumento side es la parte de la ventana
principal donde se colocará, por ejemplo:
panel_superior = Frame( aplicacion , bd = 1 , relief = FLAT)
panel_superior.pack( side = TOP)
aunque corramos el código aún no se veerá nada pues no hemos colocado un contenido, para ello creamos una etiqueta, que es
un objeto del tipo Label( panel , text = "título" ), donde el primer argumento es el panel donde se arrojará la etiqueta, el
argumento text = es el contenido de la etiqueta y el argumento fg = (fontground) nos permite colocar el color del título:
etiqueta_titulo = Label( panel_superior , text = "Sistema de facturación" , fg = "FFFFFF" )
dentro de la misma etiqueta podemos controlar la fuente y el tamaño con el parámetro font = , dándole una tupla:
etiqueta_titulo = Label( panel_superior , text = "Sistema de facturación" , fg = "FFFFFF" , font = ( "Dosis" , 58 ) )
podemos agregar el color de fondo de la etiqueta nuevamente con bg = , pondremos el mismo color, además podemos controlar el
ancho de la etiqueta con el parámetro width = :
etiqueta_titulo = Label( panel_superior , text = "Sistema de facturación" , fg = "#FFFFFF" ,
font = ( "Dosis" , 58 ) , bg = "#0F5A92" , width = 27 )
para lanzar la etiqueta le aplicamos el método grid() que lo coloca en una cuadrícula imaginari, le damos la tupla con la
columna y la fila, en este caso como no hemos seccionado la cuadrícula imaginaria, le damos el ( 0 , 0 ):
etiqueta_titulo = Label( panel_superior , text = "Sistema de facturación" , fg = "#FFFFFF" ,
font = ( "Dosis" , 58 ) , bg = "#0F5A92" , width = 27 )
etiqueta_titulo.grid( row = 0 , column = 0 )
podemos seguir agregando más paneles par hacer más vistosa nuestra aplicación.

Además de Frame() y Label() hay otra clase LabelFrame() que es un cuadro que tiene la etiqueta integrada:
panel_comidas = LabelFrame( panel_izquierdo , text = "Comida" , font = ( "Dosis" , 19 , "bold" ) , bd = 1,
relief = FLAT , fg = "#0F5A92" )
panel_comidas.pack( side = LEFT )
_____________________________________________________________________________________________________________________________

También podría gustarte