Documentacion Selenium Python
Documentacion Selenium Python
v1.1.4 2020-01-09
Contenidos
1. Introducción a Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2.3. Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.6.1. Intro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3. Estructuras de control. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.1. Condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.2. Bucles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.3.1. Ejercicio 1: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.3.2. Laboratorio 02 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4. Definición de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2. PEP8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.3.3. Ejercicio 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.3.4. Ejercicio 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.3.5. Ejercicio 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.3.6. Ejercicio 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.3.7. Ejercicio 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.3.8. Ejercicio 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.3. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
6.6.1. Laboratorio 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
6.6.2. Laboratorio 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
• Python tiene su origen en el lenguaje ABC, cuyo fin era ser una alternativa a Basic como
elección para principiantesRossum. Este lenguaje con un codigo compacto y legible no
1
trascendio por incompatibilidades con el software de la época.
• Guido Van Rossum decidió darle una segunda oportunidad con Python.
• En 1991 publica en alt.sources el codigo de la versión 0.9.0 que ya tenía disponibles, clases con
herencia, manejo de excepciones, funciones y los tipos modulares.
• La versión 1.4 trajó nuevas características inspiradas en Modula 3 y soporte "built in" para
numeros complejos.
• Tras la versión 1.6 la Free Software Foundation en acuerdo con el CNRI acordaron que Python
tuviera licencia de Software Libre dando lugar a la versión 1.6.1
• La versión 2.0 trajo algo tan importante como es la creación de listas y un sistema de
recolección de basura capar de recolectar referencias cíclicas.
• Como lenguaje a acumulado nuevas y redundantes formas de programar una misma tarea. Por
eso esta versión hace enfasis en eliminar construtores duplicados y módulos con un solo
objetivo: "tener solo un modo obvio de hacer las cosas".
• Python 2.6 y Python 3.0 se lanzaron casi al mismo tiempo, fueron planeadas para coexistir. Por
ejemplo la 2.6 mantenia herramientas eliminadas en la versión 3.0
• Con Python 3.0 se rompe la retrocompatibilidad del lenguaje, y algunos objetos modifican su
sintaxis.
2
1.4. Lab: Introducción
En este capítulo vamos a aprender a utilizar VS Code como un IDE para Python, usandolo como un
entorno de Python en el que vamos a poder:
3
Iniciando desde la raíz usando la linea de comandos
$ code .
Una vez abierto de este modo para desplazarnos por nuestro sistema de archivos tendremos que
usar los comandos correspondientes en la terminal seleccionada que estemos usando en VS Code
Usamos el "." para indicar que lo abrimos desde el directorio en el que nos encontramos, en este
caso la raíz. Si queremos iniciar VS Code en un directorio concreto, lo hariamos con el mismo
comando ejecutado en ese directorio.
$ cd /home/vagrant/Documents
$ code .
4
1.4.2. Instalando la extensión de Python
Una vez iniciado el IDE lo primero que tenemos que hacer es instalar la extensión de Python
Un entorno virtual es un directorio que contiene una instalación de Python de una versión en
particular, además de unos cuantos paquetes adicionales.
Diferentes aplicaciones pueden entonces usar entornos virtuales diferentes. Para resolver el
problema de requerimientos en conflicto, la aplicación A puede tener su propio entorno virtual con
la versión 1.0 instalada mientras que la aplicación B tiene otro entorno virtual con la versión 2.0. Si
la aplicación B requiere que actualizar la librería a la versión 3.0, esto no afectará el entorno virtual
de la aplicación A.
Para la creación de este entorno Python 3 incluye el módulo "venv", que nos permitirá crearlo de
una manera sencilla.
• En la consola ejecutamos: Directorio> python -m venv env (podemos sustituir env, por el
nombre que queramos)
Esto creará la carpeta tutorial-env si no existe, y también creará las subcarpetas conteniendo la
copia del intérprete Python, la librería estándar y los archivos de soporte.
5
$ source virtualenv/bin/activate
(virtualenv) [vagrant@fedora Documents]$
$ deactivate
PS C:\Users\benja\OneDrive\Documents\PRONOIDE\CURSOS\PYTHON> virtualenv\Scripts\Activate.ps1
(virtualenv) PS C:\Users\benja\OneDrive\Documents\PRONOIDE\CURSOS\PYTHON>
Es probable que al intentar ejecutar este scrip desde la PowerShell de Windows obtengamos un
error en el que nos diga que no se puede ejecutar el script, esto es debido a la política de
restricciones. Para solucionarlo ejecutamos la PowerShell como administrador y ejecutamos:
Set-ExecutionPolicy Unrestricted
Como hemos visto, Python es un lenguaje interpretado y debemos indicarle a VS Code que
interprete queremos usar para poder usar todas las características del IDE. Para asegurarnos que
tenemos elegido el interprete correcto vamos a abrir el menú de comandos con
Ctrl+Shift+P
Una vez abierto este menú, escrbiremos en el buscador "Python: Select Interpreter"
6
Puede que el VS Code tarde en reconocer el entorno virtual, es recomendable reiniciar el sistema
para que aparezca y se pueda continuar.
Una vez iniciado el Entorno Virtual y el Interprete de Python vamos a escribir nuestro primer script
en Python usando VS Code. Para ello usamos cualquier de las opciones disponibles para crear un
nuevo archivo
print(wlcm)
Para ejecutarlo es tan sencillo como utilizar el icono a la derecha de la sección de archivos
7
Aunque hay otras tres manera de hacerlo:
• Botón derecho del ratón sobre cualquier parte vacía del script
8
1.4.6. Configurando Debugg
Vamos a utilizar nuestro sencillo ejemplo para ver como podemos configurar el debugg de VS Code.
Lo primero es establecer un breakpoint con F9
Una vez seleccionado debemos iniciar el debugg con con F5. La primera vez que lo iniciemos nos
pedirá que elijamos el tipo
En Python los módulos contienen interesantes librerias a las que podemos acudir en caso de
necesitar una función especifica. Para este ejemplo vamos a usar los módulos matplotlib y numpy.
standarplot.py
x = np.linspace(0, 20, 100) # Create a list of evenly-spaced numbers over the range
plt.plot(x, np.sin(x)) # Plot the sine of each x point
plt.show()
Al ejecutarlo es normal que nos devuelva una salida en la que nos indique que no encuentra el
módulo matplotlib Para ello abrimos la terminal y procedemos de la siguiente manera:
9
python3 -m pip install matplotlib
A través de está consola podemos instalar los módulos necesarios. Hay que recordar que estamos
dentro de un entorno virtual, por lo tanto la instalación del módulo dentro del mismo y solo se
podrá usar en dicho entorno.
10
Capítulo 2. Python y los tipos de datos
Antes de empezar con la sintáxis, un pequeño comentario sobre los tipos de datos y las variables en
Python
2 * 4 - (7 - 1) / 3 + 1.0 # 7.0
1/0
La división entre enteros en Python 3 devuelve un número real, al contrario que en Python 2.
3 / 2 # 1.5
3 // 2 # 1
11
Se puede elevar un número a otro con el operador **:
2 ** 16 # 65536
Otro tipo que nos resultará muy útil son los complejos:
2 + 3j # (2+3j)
int(18.6) # 18
round(18.6) # 19
float(1) # 1.0
complex(2) # (2+0j)
str(256568) # '256568'
a = 2.
type(a) # float
La función print es la que se usa para imprimir resultados por pantalla. Por si lo
ves en algún sitio, en Python 2 era una sentencia y funcionaba de manera distinta,
sin paréntesis y sin posibilidad de pasar argumentos adicionales.
12
max(1,5,8,7) # 8
min(-1,1,0) # -1
Por cuestiones de estilo, las variables suelen empezar con minúscula, reservando la mayúcula para
clases. Algunos nombres no pueden ser usados porque son usados por python:
and, as, assert, break, class, continue, def, del, elif, else, except, exec, finally,
for, from, global, if, import, in, is, lambda, not, or, pass, print, raise, return,
try, while, with, yield
a = 1 + 2j
b = 3.14159
b
x, y = 1, 2
x = y, y = x
x, y # (2, 1)
• '==' igual a
• '!=' distinto de
13
• '<' menor que
x = 3
y = 1
x < y # False
x <= y # False
x > y # True
x >= y # True
x <= 2 < y # True
'a' < 'b' # True
1 < 'hola'
2.3. Strings
Uno de los mayores cismas entre Python 2 y Python 3
Uno de los mantras del programador: El texto plano no existe. Cualquier texto ha
de llevar asociado una codificación de caracteres, para saber cómo transformar los
bytes en texto legible
Hay idiomas que pueden ser completamente representados por la codificación de caracteres 'ASCII',
como el inglés. Para otros idiomas, como el español, es popular la codificación de caracteres CP-
1252, también conocida como windows-1252, por ser utilizada en sistemas Windows.
En CP-1252, los bytes 0 a 127 son iguales que en ASCII (se usan para las letras del alfabeto, los
números, etc). Los bytes 128 a 255 se utilizan para representar otros caracteres, como la ñ.
Tanto ASCII como CP-1252 utilizan un solo byte para representar cada caracter.
14
Otros idiomas, como el koreano, requieren más de un byte para representar todos los caracteres
disponibles.
• 'UTF-16': Codificación Unicode que usa 2 bytes para representar todos los caracteres. Para poder
representar idiomas que requieren de más de 65535 caracteres, utiliza algunos hacks un poco
sucios…
• 'UTF-32': Codificación Unicode que usa 4 bytes para representar todos los caracteres existentes.
Cualquier idioma conocido es representable.
Lo más razonable parecía usar 'UTF-32', ya que es capaz de representar cualquier idioma. Pero 4
bytes por caracter es mucho espacio. Hay idiomas, como el español o el inglés, que pueden
representarse con un byte por caracter.
Para solucionar este desperdicio de espacio que supone usar 4 bytes por caracter, surgió UTF-8
UTF-8 es una codificación Unicode de longitud variable. Usa la misma representación que 'ASCII'
para los 128 primeros caracteres, dos bytes para alfabeto latino extendido, 3 bytes para caracteres
chinos, y 4 para el resto (no representables con menos de 4 bytes)
Con 'UTF-8' se optimiza el espacio utilizado para representación, pero tiene la desventaja de que la
búsqueda de caracteres es de 'O(N)', siendo 'N' la longitud del texto, al usar diferente número de
bytes para diferentes caracteres
# El elemento string es un array de caracteres, podemos acceder a ellos como a cualquier array
s[0] # 深
15
sources/t03/t03ej01.py
sources/t03/t03ej04.py
16
2.4. Secuencias: Listas y Tuplas
Otro tipo de datos muy importante que vamos a usar son las secuencias: las tuplas y las listas. Los
dos son conjuntos ordenados de elementos: las tuplas se demarcan con paréntesis y las listas con
corchetes.
sources/t03/t03ej05.py
tupla_sin_parentesis = 2,5,6,9,7
print(type(tupla_sin_parentesis)) # tuple
Puedes ver las tuplas como una estructura de datos similar al 'struct' de C: un
almacén de longitud fija de datos potencialmente heterogéneos.
En los dos tipos podemos comprobar fácilmente la longitud, o si contienen un elemento concreto
17
Fíjate en dos detalles:
• Cuando especificamos índices negativos, recorremos el array desde el final hasta el principio.
Por eso [-1] da el último elemento, [-2] el penúltimo y así sucesivamente.
• Hemos utilizado la notación [::2] para el último caso. Si no especificamos nada para el inicio y el
fin, empezamos en el principio y terminamos en el final. Por eso [:] devuelve todos los
elementos de la secuencia.
Podemos complicarlo un poco más y hacer cosas como una lista de listas:
a = [
[1, 2, 3],
[4, 5],
]
print(a) # [[1, 2, 3], [4, 5]]
print(a[0]) # [[[1, 2, 3], [4, 5]]]
print(a[0][0]) # 1
Las listas en Python nos ayudan a programar como un pythonista. Por ejemplo, mediante el uso de
list comprehensions
print(multiplos_de_3)
print(multiplos_de_3)
18
# Mal, esto no es Pythonista
mis_datos = ['perro', 'Miko', 8]
animal = mis_datos[0]
nombre = mis_datos[1]
edad = mis_datos[2]
Si solo queremos desempaquetar parte de una estructura, podemos hacer uso de *, junto con el
placeholder _:
print(primero)
print(segundo)
print(tercero)
print(primero)
print(segundo)
print(tercero)
Para los que hayáis trabajado con LISP, esto se llamaba desctructuring bind
(extraer valores de una estructura de datos y enlazarlos con variables).
Es fácil crear sets, usando llaves, y metiendo los elementos separados por comas
19
sources/t03/t03ej15.py
Podemos crear un set vacío, pero ojo, que si intentamos crearlo con dos llaves y nada en medio, nos
crea un diccionario, no un conjunto (veramos a continuación los diccionarios)
not_sure = {}
print(type(not_sure)) # dict
# Añadimos elementos con add. Si añadimos un elemento que ya existe, no hace nada
a_set = {1, 2, 3}
print(a_set) # {1, 2, 3}
a_set.add(4)
print(a_set) # {1, 2, 3, 4}
a_set.add(1)
print(a_set) # {1, 2, 3, 4}
a_set.update({4, 5, 6})
print(a_set) # {1, 2, 3, 4, 5, 6}
20
Para borrar elementos de un set, tenemos 4 opciones
• El método discard
• El método remove
• El método pop
• El método clear
# pop devuelve el último elemento extraído, y clear elimina todos los elementos
a_set = {1, 3, 6, 10, 15, 21, 28, 36, 45}
print(a_set) # {1, 3, 6, 10, 15, 21, 28, 36, 45}
a_set.pop() # 1
a_set.pop() # 3
a_set.clear()
print(a_set) # set()
# Discard y remove se diferencian en que, si el elemento no existe, remove lanza una excepcion y discard no
a_set = {1, 3, 6, 10, 15, 21, 28, 36, 45}
print(a_set) # {1, 3, 36, 6, 10, 45, 15, 21, 28}
a_set.discard(10)
print(a_set) # {1, 3, 36, 6, 45, 15, 21, 28}
a_set.discard(10)
print(a_set) # {1, 3, 36, 6, 45, 15, 21, 28}
a_set.remove(21)
print(a_set) # {1, 3, 36, 6, 45, 15, 28}
a_set.remove(21) # Excepción
Las operaciones típicas de teorías de conjuntos se pueden hacer también con set
21
La diferencia simétrica es el equivalente a la operación booleana XOR, pero con el
tipo de dato 'set'. Es decir, nos devuelve los elementos que se encuentran en uno u
otro de los conjuntos a comparar, pero no en los dos a la vez.
Al igual que las secuencias, los set también nos ayudan a programar como un pythonista. Un caso
de uso típico es eliminar los duplicados de una lista: set lo hace de manera automática
# Podemos construir un set a partir de una lista, y se eliminarán los duplicados directamente
a_list = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
print(a_list) # [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
a_set = set(a_list)
print(a_set) # {1, 2, 3, 4}
Los diccionarios son como los set, pero tienen tuplas clave:valor, en lugar de elementos sueltos.
# Se pueden añadir todos los elementos que se quiera a un diccionario. Y también podemos modificar los existentes
a_dict['user'] = 'Jorge'
a_dict['database'] = 'PostgreSQL'
print(a_dict) # {'database': 'PostgreSQL', 'user': 'Jorge', 'server': 'server.com'}
Los diccionarios, como no, también nos dan la oportunidad de programar como un pythonista.
22
# Mal, esto no es pythonista
frutas = {'verde': 'pera', 'amarilla': 'plátano', 'roja': 'fresa'}
if 'azul' in frutas:
fruta_azul = frutas['azul']
else:
fruta_azul = 'piña'
print(fruta_azul)
# Esto si es pythonista
frutas = {'verde': 'pera', 'amarilla': 'plátano', 'roja': 'fresa'}
23
2.6. Lab: Introducción y tipo de datos
2.6.1. Intro
Buenos días, Sr. Hunt. Su misión, si decide aceptarla, implica la recuperación de un artículo robado
denominado "Quimera''. Puede seleccionar dos miembros del equipo, pero es esencial que el tercer
miembro de su equipo sea Nyah Nordoff-Hall. Ella es una civil y una ladrona profesional altamente
capacitada. Tiene cuarenta y ocho horas para reclutar a la señorita Hall y reunirse conmigo en Sevilla
para recibir su asignación. Como siempre, si algún miembro de su equipo es capturado o asesinado, el
Secretario desautorizará todo conocimiento de sus acciones. Y Sr. Hunt, la próxima vez que se vaya de
vacaciones, tenga la bondad de decirnos adónde va. Este mensaje se autodestruirá en cinco segundos.
Problema
Usted es el científico informático que tiene que proteger valiosos datos de inteligencia nacional.
Estos datos se encuentran en una computadora sin conexión a internet en una habitación con suelo
táctil que detecta la presión. Pero ha descubierto que más de un espía se ha infiltrado y robado los
datos, por lo que debe implementar y probar el nuevo sistema de seguridad.
# Variables
dbs = [0, 1, 0, 0, 0, 0, 1, 0, 1, 23, 1, 0, 1, 1, 0, 0, 0,
1, 1, 0, 20, 1, 1, 15, 1, 0, 0, 0, 40, 15, 0, 0]
• Muestra los valores que estén por encima de 10 dBs (guárdalo en una lista)
Solución
db1 = []
for e in dbs:
if e > 10:
db1 += [e]
print(db1)
24
db_vol10 = [e for e in dbs if e > 10]
print(db_vol10)
• Muestra el momento (índices de la lista) en los cuales el sonido excede de los 10 dBs
Solución
vol10_i = []
for i in range(len(dbs)):
if dbs[i] > 10:
vol10_i += [i]
print(f"los indices son {vol10_i}")
Combina los dos apartados anteriores, obtén una lista de tuplas donde el primer elemento de la
tupla sea el índice y el segundo el valor de los sonidos superiores a 10 dBs
Solución
vol10 = []
for i in vol10_i:
vol10 += [(i, dbs[i])]
print(vol10)
[(9, 23), (20, 20), (23, 15), (28, 40), (29, 15)]
25
vol10 = [(i, dbs[i]) for i in range(len(dbs)) if dbs[i] > 10]
print(vol10)
[(9, 23), (20, 20), (23, 15), (28, 40), (29, 15)]
a = list(enumerate(dbs))
dbe = []
for (e, e1) in a:
if e1 > 10:
dbe += [(e, e1)]
print(f"Los valores son {dbe}")
Los valores son [(9, 23), (20, 20), (23, 15), (28, 40), (29, 15)]
[(9, 23), (20, 20), (23, 15), (28, 40), (29, 15)]
◦ Pista: ten cuidado con los extremos, no tengas un error de tipo IndexError: list index out of
range
Solución
for i in range(len(dbs)):
if dbs[i] >10 and dbs[i+1] > 10:
print('ALARM')
print(f"los indices consecutivos son {i} y {i+1} con valores {dbs[i]} y {dbs[i+1]}")
• Ahora vamos a tener el rol del hacker que acompaña a Ethan. Y por tanto, engañaremos al
nuevo sistema de seguridad de sonidos que han implementado. Y para toda señal igual o
superior a 10 dB la cambiaremos por 2 dB
Solución
[0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 2, 1, 1, 2, 1, 0, 0, 0, 2, 2, 0, 0]
26
Problema del caracol y el pozo
Un caracol cae al fondo de un pozo de 125 cm. Cada día el caracol se eleva 30 cm. Pero por la noche,
mientras duerme, se desliza 20 cm porque las paredes están mojadas. ¿Cuántos días se necesitan
para escapar del pozo?
# Variables
well_height = 125
daily_advance = 30
night_retreat = 20
Solución
well_height = 125
daily_advance = 30
night_retreat = 20
adv = (daily_advance - night_retreat)
dia = 1
while well_height >= daily_advance:
dia += 1
rest_height = well_height - adv
well_height = rest_height
• Ahora en lugar de avanzar una cantidad constante diaria, su avance está definido por una lista
de los cm que avanza cada día
well_height = 125
advance_cm = [30, 21, 33, 77, 44, 45, 23, 45, 12, 34, 55]
night_retreat = 20
Solución
27
#la idea sigue siendo la misma, solo que ahora variara el avance diario.
dia = 1
adv = [advance_cm[i] - night_retreat for i in range(len(advance_cm))]
for i in range(len(advance_cm)):
while well_height > advance_cm[i]:
dia += 1
rest_height = well_height - adv[i]
well_height = rest_height
break
print(f"el caracol tarda {dia} dias en salir del pozo")
Bus
Este autobús tiene un sistema de control de entrada y salida de pasajeros para monitorizar el
número de ocupantes que transporta y así detectar cuando hay una capacidad demasiado alta.
En cada parada, la entrada y salida de pasajeros está representada por una tupla que consta de dos
números enteros.
# Variables
stops = [(10, 0), (4, 1), (3, 5), (3, 4), (5, 1), (1, 5), (5, 8), (4, 6), (2, 3)]
Solución
paradas = len(stops)
print(f"El numero de paradas es {paradas}")
El numero de paradas es 9
• Asigna a una variable una lista cuyos elemento son los números de los pasajeros en cada
parada. Cada elemento depende del anterior y de los pasajeros que suban y bajen
Solución
28
A = []
for i in range(len(stops)):
for i2 in range(1):
#Resto cada tupla para que me de la diferencia de entre "in" y "out"
A += [stops[i][i2] - stops[i][i2 + 1]]
passengers = [A[0]]
B = A[0]
for i in range(len(A) - 1):
B = B + A[i + 1]
passengers += [B]
print(f"los pasajeros que quedan despues de cada para es {passengers}")
los pasajeros que quedan despues de cada parada es [10, 13, 11, 10, 14, 10, 7, 5, 4]
# 3.
Max_ocu = max(passengers)
print(f"La maxima ocupación del bus es {Max_ocu}")
Duelo de magos
Estás presenciando una batalla épica entre dos poderosos hechiceros: Gandalf y Saruman. Cada
hechicero tiene 10 hechizos de poder variable en su mente y los lanzarán uno tras otro. El ganador
del duelo será el que gane más de esos enfrentamientos entre hechizos. Los hechizos se
representan como una lista de 10 números enteros cuyo valor es igual al poder del hechizo.
gandalf = [10, 11, 13, 30, 22, 11, 10, 33, 22, 22]
saruman = [23, 66, 12, 43, 12, 10, 44, 23, 12, 17]
Por ejemplo: 1. Saruman gana el primer enfrentamiento: 10 contra 23, gana 23 2. El segundo gana
Saruman: 11 contra 66, gana 66 3. etc.
# Variables
gandalf = [10, 11, 13, 30, 22, 11, 10, 33, 22, 22] # conjuros de Gandalf
saruman = [23, 66, 12, 43, 12, 10, 44, 23, 12, 17] # conjuros Saruman
gandalf_wins = 0 # victorias de Gandalf
saruman_wins = 0 # victorias de Saruman
tie = 0 # empate
• Imprime por pantalla cuántas victorias tiene cada uno y el número de empates
Solución
29
gandalf = [10, 11, 13, 30, 22, 11, 10, 33, 22, 22] # conjuros de Gandalf
saruman = [23, 66, 12, 43, 12, 10, 44, 23, 12, 17] # conjuros Saruman
gandalf_wins = 0 # victorias de Gandalf
saruman_wins = 0 # victorias de Saruman
tie = 0
for i in range(len(gandalf)):
if gandalf[i] > saruman[i]:
gandalf_wins += 1
elif gandalf[i] < saruman[i]:
saruman_wins += 1
else:
tie += 1
print(f"Las victorias de Gandalf son {gandalf_wins}")
print(f"Las victorias de Saruman son {saruman_wins}")
print(f"{tie} empates")
victorias de Gandalf: 6
victorias de Saruman: 4
Empates: 0
Solución
30
# Comprueba quién ha ganado la batalla
Victoria = 'Gandalf' if gandalf_wins > saruman_wins else 'Saruman'
print(f'la victoria es de {Victoria}')
la victoria es de Gandalf
• Ahora en lugar de tener una lista con el poder de sus hechizos, la lista será el nombre de los
hechizos, y tendremos un diccionario con el poder de dichos hechizos
POWER = {
'Fireball': 50,
'Lightning bolt': 40,
'Magic arrow': 10,
'Black Tentacles': 25,
'Contagion': 45
}
Cambia las listas de gandalf y saruman para que en lugar de los nombres tengan el poder de los
hechizos
Solucion
31
POWER = {
'Fireball': 50,
'Lightning bolt': 40,
'Magic arrow': 10,
'Black Tentacles': 25,
'Contagion': 45
}
gandalf: [50, 40, 40, 10, 50, 10, 40, 50, 50, 50]
Saruman: [45, 45, 25, 50, 25, 40, 10, 45, 10, 10]
32
Capítulo 3. Estructuras de control
3.1. Condicionales
Los condicionales en Python tienen este aspecto:
if <condition>:
<do something>
elif <condition>:
<do other thing>
else:
<do other thing>
x,y = 8, 4
if x > y:
print("x es mayor que y")
print("x es el doble de y")
if x > y:
print("x es mayor que y")
else:
print("x es menor o igual que y")
if x < y:
print("x es menor que y")
elif x == y:
print("x es igual a y")
else:
print("x es mayor que y")
Las reglas del buen pythonista dicen que evites las comparaciones directas con valores como 'True',
'False' o 'None' (el equivalente a 'null' en otros lenguajes de programación)
33
sources/t03/t03ej24.py
También nos dicen que evitemos usar demasiados condicionales en la misma sentencia, si es
posible
3.2. Bucles
En Python existen dos tipos clásicos de bucles:
• Bucles while
• Bucles for
Los bucles while repetiran las sentencias anidadas en él mientras se cumpla una condición:
while <condition>:
<things to do>
Como en el caso de los condicionales, los bloques se separan por indentación sin necesidad de
sentencias del tipo end
34
ii = -2
while ii < 5:
print(ii)
ii += 1 # Operador in-place +=
sources/t03/t03ej27.py
ii = 0
while ii < 5:
print(ii)
ii += 1
if ii == 3:
break
Un bloque else justo después del bucle se ejecuta si este no ha sido interrumpido por nosotros:
ii = 0
while ii < 5:
print(ii)
ii += 1
if ii == -8:
break
else:
print("El bucle no ha sido interrumpido")
Hay maneras más pythonistas que otras de escribir bucles while, como no podría ser de otra forma.
Por ejemplo, si queremos recorrer una lista de elementos:
El otro bucle en Python es el bucle for, y funciona de manera un que puede resultar chocante al
principio. La idea es recorrer un conjunto de elementos:
35
for <element> in <iterable_object>:
<do whatever...>
for ii in (1,2,3,4,5):
print(ii)
for ii in range(3):
print(ii)
Al igual que en los bucles while, se puede usar else para ejecutar código después de un bucle,
siempre que éste no haya sido interrumpido
if not hay_impar:
print('Todos los números son pares')
# El else se va a ejecutar después del bucle siempre y cuando no lo rompamos de manera abrupta con un break
else:
print('Todos los números son pares')
Si queremos seguir ejecutando un bucle y excluir algún elemento del mismo podemos hacer uso de
continue, que representaria el caso contrario a break que vimos en el ejemplo del bucle while. Si
por ejemplo definimos el siguiente bucle:
36
number = 0
for number in range(10):
if number == 5:
print (f"numero {number} no se va a ejecutar pero el bucle va a continuar")
continue
print('El numero es el ' + str(number))
print ("Ya estamos fuera del bucle")
En cuanto a programar bucles for como un pythonista, existe el caso de uso en el que queremos
acceder al índice de un elemento desde dentro del bucle. Para ello usamos enumerate:
37
3.3. Lab: Estructuras de control
3.3.1. Ejercicio 1:
Usted es el científico informático que tiene que proteger valiosos datos de inteligencia nacional.
Estos datos se encuentran en una computadora sin conexión a internet en una habitación con suelo
táctil que detecta la presión. Pero ha descubierto que más de un espía se ha infiltrado y robado los
datos, por lo que debe implementar y probar el nuevo sistema de seguridad.
• Muestra los valores que estén por encima de 10 dBs (guárdalo en una lista)
• Muestra el momento (índices de la lista) en los cuales el sonido excede de los 10 dBs
• Combina los dos apartados anteriores, obtén una lista de tuplas donde el primer elemento de la
tupla sea el índice y el segundo el valor de los sonidos superiores a 10 dBs
◦ Pista: ten cuidado con los extremos, no tengas un error de tipo IndexError: list index out of
range
• Ahora vamos a tener el rol del hacker que acompaña a Ethan. Y por tanto, engañaremos al
nuevo sistema de seguridad de sonidos que han implementado. Y para toda señal superior a 10
dB la cambiaremos por 2 dB
Solución
38
# Variables
dbs = [0, 1, 0, 0, 0, 0, 1, 0, 1, 23, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 20, 1, 1, 15, 1, 0, 0, 0, 40, 15, 0, 0]
# Muestra los valores que estén por encima de 10 dBs (guárdalo en una lista)
db1 = []
for e in dbs:
if e > 10:
db1 += [e]
print(db1)
# Muestra el momento (índices de la lista) en los cuales el sonido excede de los 10 dBs
vol10_i = []
for i in range(len(dbs)):
if dbs[i] > 10:
vol10_i += [i]
print(f"los indices son {vol10_i}")
a = list(enumerate(dbs))
dbe = []
for (e, e1) in a:
if e1 > 10:
dbe += [(e, e1)]
print(f"Los valores son {dbe}")
Ahora vamos a tener el rol del hacker que acompaña a Ethan. Y por tanto, engañaremos al nuevo
sistema de seguridad de sonidos que han implementado. Y para toda señal superior a 10 dB la
39
cambiaremos por 2 dB
3.3.2. Laboratorio 02
Escribir un programa que le pregunte al usuario por una palabra y le siga preguntando mientras no
la acierte, o hasta un máximo de 10 veces. La comparación la puedes hacer case insensitive, y para
leer la entrada de usuario, puedes usar la funcion input
Solución
veces = 0
palabra_correcta = "cacahuete"
palabra = ""
40
Capítulo 4. Definición de funciones
Ya lo hemos visto en algún ejemplo, pero para definir nuestra propia función utilizamos la
sentencia def seguida del nombre de la misma y entre paréntesis los argumentos de entrada. La
primera línea de la función suele ser una cadena de documentación.
Los valores de retorno de la función se especifican con la sentencia return. Por ejemplo:
def al_cuadrado(x):
"""Función que eleva un número al cuadrado."""
y = x ** 2
return y
assert(al_cuadrado(4) == 16)
# Definir función que multiplica dos números. Por defecto, el segundo de ellos valdrá 2
def multiplica(x, y=2.0):
"""Multiplica dos números, por defecto el primero por 2."""
return x * y
assert(multiplica(2, 3) == 6)
assert(multiplica(4) == 8) # 8
Evita el uso de '[]', '{}' o '''' como parámetros por defecto para funciones
41
Potencial error aquí. El valor por defecto de una función es evaluado una sola vez. Si dicho argumento es un objeto
mutable,
como una lista o un diccionario, la función acumula los argumentos pasados en llamadas sucesivas
"""
def f(a, L=[]):
L.append(a)
return L
# Si queremos evitar que se acumule el valor del argumento por defecto, lo podemos escribir así
def f(a, L=None):
if not L:
L = []
L.append(a)
return L
Definiendo *args como parámetro de una función, permitimos que dicha función reciba cualquier
número de parámetros. Dentro de la función, *args es desempaquetado como una lista, de manera
que espera que se le pase una lista de elementos
def func(*args):
print("Argumentos:")
for arg in args:
print(arg)
Argumentos:
134
543
sfds
[]
Definiendo kwargs como parámetro de una función, permitimos que dicha función reciba un
diccionario con un número arbitrario de elementos clave:valor. Dentro de la función, kwargs
42
es desempaquetado como un diccionario
def func(**kwargs):
print("Argumentos:")
for k,v in kwargs.items():
print("{0} => {1}".format(k, v))
Argumentos:
a => 134
b => 543
c => sfds
print ("Kwargs:")
for k,v in kwargs.items():
print("{0} => {1}".format(k, v))
Args:
2342
43534
5645
Kwargs:
x => 456
y => 3
4.2. PEP8
PEP8 es una guía de estilo para desarrollar en Python. Fue creada en 2001, y su última actualización
hasta la fecha es de 2013.
43
• Usa sangrado de 4 espacios, no tabuladores
• Usa líneas en blanco para separar funciones y bloques de código dentro de ellas.
Traducido de aquí
Para comprobar si nuestro código cumple con las reglas de estilo, existe el paquete pep8.
Lo instalamos mediante
Y lo usamos así
pep8 mifichero.py
Se nos informará de las violaciones a pep8 que encierra nuestro código, si las hay.
44
4.3. Lab: Funciones
4.3.1. Ejercicio 1
Crear una función que reciba un número y nos diga si es mayor, menor o igual a 5. La función ha
de tener una sola línea, y ha de estar escrita lo más pythonista posible. Para ello, es posible que
tengas que averiguar lo que es un ternary operator, y cómo se implementa en Python.
Solución
def comparar(num):
return 'El número es menor que 5' if num < 5 else 'El número es igual a 5' if num == 5 else 'El número es mayor que
5'
print(comparar(2))
print(comparar(5))
print(comparar(7))
4.3.2. Ejercicio 2
Implementar la función sumatorio, de manera que devuelva la suma de los n primeros números
Solución
def sumatorio(n):
"""
Suma los n primeros números.
Ejemplos
--------
>>> sumatorio(4)
10
"""
s = 0
for x in range(1, n + 1):
s = s + x
return s
assert(sumatorio(4) == 10)
4.3.3. Ejercicio 3
Implementar la función sumatorio\_tope, de manera que sume los n primeros números hasta que
lleguen al valor establecido como tope.
Solución
45
def sumatorio_tope(tope):
"""
Va sumando números naturales hasta llegar al tope.
Ejemplos
--------
>>> sumatorio_tope(9)
6
# 1 + 2 + 3
>>> sumatorio_tope(10)
10 # 1 + 2 + 3 + 4
>>> sumatorio_tope(12)
10 # 1 + 2 + 3 + 4
>>> sumatorio_tope(16)
15 # 1 + 2 + 3 + 4 + 5
"""
s = 0
n = 1
while s + n <= tope:
s += n
n += 1
return s
assert(sumatorio_tope(9) == 6)
assert(sumatorio_tope(10) == 10)
assert(sumatorio_tope(12) == 10)
assert(sumatorio_tope(16) == 15)
4.3.4. Ejercicio 4
Crear una función para determinar si la longitud de un determinado examen cumple la normativa.
Dicha normativa es: si un examen dura más de 3 horas, entonces debe tener un descanso
Los argumentos de la función son el tiempo en horas y un valor booleano que indica si hay
descanso o no. Puntos extra si lo haces en una sola línea.
Solución
46
def cumple_normativa(tiempo, descanso):
"""
Decide si los criterios establecidos para un determinado examen pasan la normativa o no.
@param tiempo: tiempo en horas
@param descanso: booleano que define si hay descanso o no
@return: True si se cumple la normativa. False en caso contrario
"""
return True if tiempo <= 3 else descanso
4.3.5. Ejercicio 5
Calcular
x = \sqrt{S}
x = \frac{S} {2}
Repetir el paso 2 hasta que se cumpla que el valor obtenido no cambie con respecto al obtenido en
el paso anterior (hasta que se alcance la convergencia). Tendras que importar el módulo math
Solución
47
import math
def raiz(S):
"""
Devuelve la raiz cuadrada de S calculada mediante el método babilónico
"""
x = S / 2
while True:
temp = x
x = (x + S / x) / 2
S = 10
x = raiz(S)
Puedes comparar x con math.sqrt(S). Es posible que el valor sea ligeramente diferente, debido a la
precisión print(x) print(math.sqrt(S))
4.3.6. Ejercicio 6
Implementar:
• Una función 'fib(n)' que devuelve el término n de la secuencia de Fibonacci usando iteración +
Una función 'fib_recursivo(n)' que haga lo mismo pero de manera recursiva
• Una función 'n_primeros(n)' que devuelva una lista con los n primeros números de la secuencia,
llamando a cualquiera de las dos funciones anteriores. Puntos extra si obtienes una expresión
Python equivalente para obtener lo mismo (aunque la manera de obtener esa expresión se verá
en el tema siguiente…)
Puedes obviar los problemas de memoria que supondría obtener un término muy grande. No te
preocupes de eso por ahora…
Solución
48
def fib(n):
"""
Devuelve el término n de la secuencia de Fibonacci
@param n: posición a devolver
@return: El término n de la secuencia
"""
a, b = 0, 1
for i in range(n):
a, b = b, a + b # Bendita asignación múltiple
return a
def fib_recursivo(n):
"""
Función recursiva que devuelve el término n de la secuencia de Fibonacci
@param n: posición a devolver
@return: El término n de la secuencia
"""
if n == 0:
res = 0
elif n == 1:
res = 1
else:
res = fib_recursivo(n - 1) + fib_recursivo(n - 2)
return res
def n_primeros(n):
"""
Devuelve los n primeros términos de la secuencia de Fibonacci
@param n: Número de términos a devolver
@return: Lista con los n primeros términos
"""
F = fib_recursivo
lista = []
for ii in range(n):
lista.append(F(ii))
return lista
assert(fib(5) == 5)
assert(fib_recursivo(0) == 0)
assert(fib_recursivo(3) == 2)
assert(fib(8) == 21)
assert(n_primeros(10) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34])
4.3.7. Ejercicio 7
Crear una función que nos devuelva el dia de que es hoy en formato texto.
49
Pista: Usa el módulo 'datetime' para averiguar la fecha de hoy
Pista: Usa el módulo 'calendar' para obtener una lista con los días de la semana
Pista: Usa el módulo ['locale'] para obtener esos días escritos en español
Solución
1. Obtén el día de la semana que es hoy, en formato entero (1, 2, 3, etc). Usa datetime.
2. Obtén en una lista, los días de la semana en español en formato string. Usa calendar y locale.
3. Usa el resultado de 1 para indexar el array de 2
return weekdays[num_weekday - 1]
print("Hoy es {}".format(get_today_as_string()))
4.3.8. Ejercicio 8
Implementa una función 'suma(*args)' que devuelva la suma de una lista de argumentos de
longitud arbitraria. Puedes asumir que todos son números.
Solución
50
def suma(*args):
"""
Función que devuelve el sumatorio de todos sus argumentos
@param *args: lista de números de longitud arbitraria
@returns: Sumatorio de todos los argumentos
"""
s = args[0]
for a in args[1:]:
s = s + a
return s
assert(suma(1, 2, 3, 4, 5, 6, 7) == 28)
51
Capítulo 5. Clases y objetos en Python
Python es un lenguaje que permite la programación orientada a objetos, pese a no haber sido
especificamente diseñado para ello. Podemos crear nuestras propias clases, heredar de otras clases
ya existentes e instanciar cualquier clase para trabajar con ella. Veamos cómo.
c = MiClase()
Una clase puede tener atributos y métodos. Y se pueden definir de manera estática (cuando
escribimos el código de la clase) como de manera dinámica (en tiempo de ejecución)
c = MiClase()
print(c.x, c.y) # 1, 2
# Nuevo atributo
c.z = 3
print(c.z) # 3
Los métodos de nuestra clase, tendrán siempre un primer argumento self, que representa la
instancia de la propia clase con la que es llamado el método (el equivalente al this en otros
lenguajes orientados a objetos). Esto es una convención en Python.
52
# Podemos llamar a los métodos de la clase
class MiClase:
x = 1
y = 2
def saludar(self):
print("Hola")
c = MiClase()
c.saludar()
print(c.x, c.y)
# También podemos asignar el método a una variable y llamarlo después. Ojo: usamos el nombre del método sin paréntesis
saludo = c.saludar
# Ahora lo llamamos
saludo()
Podemos controlar lo que pasa en la instanciación de nuestra clase mediante el método init, que
hace el papel de constructor de nuestra clase
# Implementamos un constructor para nuestra clase, y dentro del constructor asignamos variables.
class MiClase:
def __init__(self, x, y):
c = MiClase(7, 12)
print(c.x, c.y)
Otro método interesante es str, que devuelve una representación textual de la clase
# Implementamos un método __unicode__ que será llamado cuando pasemos una instancia de la clase como argumento de print()
class MiClase:
def __init__(self, x, y):
def __str__(self):
return "x = {0}, y={1}".format(self.x, self.y)
c = MiClase(7, 12)
print(c) # x=7, y=12
53
5.1.2. Herencia en Python
Python permite herencia múltiple, de manera que una clase puede heredar de una o más clases
padre
# Creamos una clase padre y una clase hija, que heredará sus métodos y atributos
class ClasePadre:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return "x = {0}, y={1}".format(self.x, self.y)
class OtraClasePadre:
def __init__(self, x, y):
self.a = x
self.b = y
def foo(self):
return "foo"
def __str__(self):
# Podemos llamar a cualquier método de nuestra clase padre, igual que llamamos a __init__
return "{0}, z={1}".format(super().__str__(), self.z)
padre = ClasePadre(3, 4)
hija = ClaseHija(5, 6, 7)
print(padre) # x = 3, y=4
print(hija) # x = 5, y=6, z=7
print(hija.foo())
La sobrecarga de métodos es también es conocida por Overriding Methods, nos permite sustituir un
método proveniente de la Clase padre, en la Clase hijo debe definir un método con el mismo
nombre de método y mismo número de parámetros que como está definido en la Clase Padre.
Vamos a definir una clase Humano, con un metodo mensaje que nos muestre el mensaje "Es un
54
humano'' con nombre, planeta de origen y edad. Después vamos a definir una clase "Jedi'' que
herede los metodos de la clase padre pero que el metodo mensaje lo cambie por la "El humano es
un''Jedi".
class Humano:
def __init__(self, nombre, planeta, edad):
self.nombre = nombre
self.planeta = planeta
self.edad = edad
def mensaje(self):
print(f' Nombre: {self.nombre}, Origen: {self.planeta}, Edad: {self.edad}, Especie: Humano')
class Jedi(Humano):
def __init__(self, nombre, planeta, edad):
super().__init__(nombre, planeta, edad)
def mensaje(self):
print(f' Nombre: {self.nombre}, Origen: {self.planeta}, Edad: {self.edad}, Especie: Humano Jedi')
#Si lo probamos
Las clases abstractas, como su nombre lo indica, son algo abstracto, no representan algo específico
y las podemos usar para crear otras clases. No pueden ser instanciadas, por lo que no podemos
crear nuevos objetos con ellas.
Podemos imaginar una clase Animal, con métodos como caminar y comer, como una clase base que
podemos heredar para construir otras clases como León o Pájaros. Ambas van a heredar de nuestra
clase animal con sus respectivos métodos y tendremos la posibilidad de crear nuestros objetos. De
esta manera podemos reducir código duplicado y mejorar la calidad del código.
También podemos hacer lo mismo con los métodos: si una clase tiene métodos abstractos, entonces
nuestra clase deberá ser abstracta. Los métodos abstractos son aquellos que solamente tienen una
declaración, pero no una implementación detallada de las funcionalidades.
Nuestro método abstracto será compartido por las clases que hereden de nuestra clase abstracta.
En el ejemplo, un animal puede comportarse de manera similar y realizar las mismas acciones,
como caminar, comer y dormir, pero el sonido emitido no será igual en todos ellos, no escucharás a
un pájaro rugir como un león.
Para poder crear clases abstractas en Python es necesario importar la clase ABC y el decorador
abstractmethod del módulo abc (Abstract Base Classes). Un módulo que se encuentra en la librería
estándar del lenguaje, por lo que no es necesario instalar. Así para definir una clase privada
solamente se tiene que crear una clase heredada de ABC con un método abstracto.
55
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def mover(self):
pass
@abstractmethod
def comer(self):
print('Animal come')
Las subclases tienen que implementar todos los métodos abstractos, en el caso de que falta alguno
de ellos Python no permitirá instancias tampoco la clase hija.
class Animal(ABC):
@abstractmethod
def mover(self):
pass
@abstractmethod
def comer(self):
print('Animal come')
class Gato(Animal):
def mover(self):
print('El gato se mueve')
g = Gato()
56
class Animal(ABC):
@abstractmethod
def mover(self):
pass
@abstractmethod
def comer(self):
print('Animal come')
class Gato(Animal):
def mover(self):
print('El gato se mueve')
def comer(self):
super().comer()
print('El gato come')
g = Gato()
g.mover()
g.comer()
El gato se mueve
Animal come
El gato come
Por ahora hemos visto algunas pinceladas de orientación a objetos en Python. Para los
acostumbrados a Java o C++ puede resultar una sintáxis un poco forzada. Que se pueden hacer las
cosas, pero no parece que el lenguaje esté pensado para ello.
Como reflexión totalmente personal, la orientación a objetos es un paradigma muy útil cuando
nuestra aplicación ha de modelar objetos con una funcionalidad asociada a un estado y que han
de ser perdurables mientras nuestra aplicación esté funcionando. Ejemplos clásicos serían el
desarrollo de aplicaciones de escritorio, o videojuegos, donde deberemos modelar botones,
57
ventanas, personajes, etc. Estos objetos estarán presentes en memoria, y seguramente pasen por
diferentes estados, mientras nuestra aplicación esté corriendo.
Una de las primeras ideas que nos viene a la cabeza cuando comenzamos a diseñar nuestra
aplicación en Python usando el paradigma de orientación a objetos es: ¿cómo declaro miembros
privados en mis clases?
La respuesta a esta pregunta es… no lo haces. Y no lo haces porque eso solo serviría para complicar
las cosas.
La filosofía tras el diseño de Python es, en palabras de su creador, Guido Van Rosuum:
Es decir, que por defecto todo lo que haya en tu código, es público. Y es público por sencillez.
La justificación tras esa filosofía es que la única razón para implementar miembros privados en
una clase, es encapsular el funcionamiento de la misma. Separar la implementación de la interfaz.
O en otras palabras: que los usuarios de la clase no tengan que preocuparse de su funcionamiento
para poder usarla. Y si la clase está bien diseñada, y su interfaz es clara e intuitiva, no habría
porqué preocuparse de cómo funciona.
• Acceder a miembros internos de una clase en lenguajes como Java o C++ sigue siendo posible
mediante introspección. Así que, ¿para qué inventar un método que nos permita acceder a los
miembros privados de una clase, pudiendo simplemente dejarlos públicos? Recordemos que
uno de los mantras de Python es Simple is better than complex
• Dejar que todas las internalidades de una clase sean accesible facilita mucho la depuración del
código (no hay que hacer nada para mostrar atributos privados en una sesión de depuracón,
simplemente imprimirlos)
A pesar de este choque entre Python y los lenguajes orientados a objetos clásicos, tenemos a
nuestra disposición algunas facilidades que nos permiten programar de manera más parecida a
como se acostumbra en los lenguajes que siguen fielmente este paradigma, como Java o C++.
58
Mediante el uso de @property y setter puedes simular el patrón get/set para
acceso a los atributos de una clase,pero de forma más elegante (o como mínimo,
menos verbosa)
class Person:
def __init__(self, name):
# En esta asignación, realmente estamos llamando al setter de más abajo
self.name = name
@property ①
def name(self):
return self._name
@name.setter ②
def name(self, value):
self._name = value
p = Person('Paco')
print(p.name)
p.name = 'Jorge'
print(p.name)
del p.name
print(p.name) # Esto provoca una excepcion, porque el campo ya no existe
① El decorador @property hace que se pueda llamar al método sin necesidad de ponerle
paréntesis. Es decir, poder llamar a p.name, y que se llame a ese método
② El decorador @name.setter hace que podamos asignar un valor a p.name. Por supuesto,
también lo podíamos hacer antes de implementar el método, pero con este decorator,
estaríamos ocultando el acceso a la variable nombre en sí. Además, podemos hacer
comprobaciones previas a la asignación
59
# Aquí haremos algunas validaciones dentro de nuestros métodos get/set
class Person:
def __init__(self, name, edad=18):
# Aquí estamos llamando a los setters
self.name = name
self.edad = edad
@property
def name(self):
return "{} es una bestia sexy".format(self._name) if self._name == "Jorge" else self._name
@name.setter
def name(self, value):
self._name = value
@property
def edad(self):
return self._edad
@edad.setter
def edad(self, value):
if value > 17:
self._edad = value
else:
raise ValueError("No puedes entrar a la discoteca, eres menor de edad")
p = Person('Paco')
print(p.name)
p.name = 'Jorge'
print(p.name)
p.edad = 15
Hemos conseguido la encapsulación de nuestros datos, pero con una sintáxis más elegante que la
clásica get/set
Además de estos conceptos, conviene mencionar también el caso especial de init y str. Cuando
precedemos el nombre de un miembro de una clase con un doble subrayado, __, Python
automáticamente añade en tiempo de ejecución el nombre de la clase delante del nombre del
miembro. Este comportamiento se llama name mangling.
60
# Python añadirá internamente el nombre de la clase delante de __baz
class Foo(object):
def __init__(self):
self.__baz = 42
def foo(self):
print(self.__baz)
# Como el método __init__ empieza por __, en realidad será Bar__init__, y el del padre Foo__init__. Así existen por
separado
class Bar(Foo):
def __init__(self):
super().__init__()
self.__baz = 21
def bar(self):
print(self.__baz)
x = Bar()
Otro uso que tradicionalmente se ha dado al __ es, precisamente, simular la existencia de métodos
privados en clases. Es posible, pero no demasiado útil. Lo vemos a continuación.
def __private(self):
print("Soy un método privado y no me puedes llamar, mwahahahaha")
c = MiClase()
c.public()
# Esta llamada nos dará un AttributeError, diciendo que no existe el método 'private'...
#c.__private()
En Python es posible crear métodos de clase (es decir, métodos que pueden ser llamados
directamente sin instanciar la clase). Para ello, se usa el decorador @classmethod. Simplemente,
tengamos en cuenta que estos métodos no reciben self como parámetro, sino que reciben un objeto
representando la clase en si
61
# Definimos un método de clase, que podemos llamar tanto desde la clase en si como desde una instancia de la misma
class MiClase:
def f(self):
print("Esto es un método de instancia")
c = MiClase()
c.f()
Un caso de uso claro para @classmethod es definir constructores alternativos para una clase.
# MiClase tendrá un constructor alternativo, que usará una lista en lugar de una cadena
class Persona:
def __init__(self, nombre=''):
self.nombre = nombre
@classmethod
def fromList(cls, l):
# Lo rellenamos y lo devolvemos
x.nombre = ' '.join([l[0], l[1]])
return x
def __str__(self):
return self.nombre
p = Persona("Pepito Perez")
p2 = Persona.fromList(["Jorge", "Blanco"])
62
No confundamos @classmethod con @staticmethod. El segundo decorador
simplemente sirve para declarar una función dentro de la clase. No tiene mucha
más utilidad que definir funciones útiles para la clase pero que no necesitan
acceder a ninguno de sus miembros.
# Ejemplo de uso de función estática, no recibe ningún argumento, pero hace alguna operación útil dentro de la clase
class Math:
@staticmethod
def es_par(n):
return not n % 2
m = Math()
Lanzamos excepciones mediante la palabra reservada raise, y las capturamos mediante bloques try
… except
if altura <= 0:
raise ValueError("La altura del triángulo tiene que medir más que 0")
t = Triangulo(2, 3)
# Capturamos la excepción
try:
t = Triangulo(-2, 4)
except ValueError as e:
print(e)
# Esto se ejecuta siempre, tanto si salta la excepción como si no. No es obligatorio ponerlo
finally:
print("Programa terminado")
63
5.3. Conclusiones
La orientación a objetos en Python tiene más complejidad, pero con lo visto hasta ahora, es
suficiente para comparar Python con los lenguajes orientados a objetos clásicos, y ver los puntos de
fricción. En el próximo capítulo veremos otros aspectos propios de Python que dan mucho poder al
lenguaje, y tal vez convenzan a los acostumbrados a los lenguajes orientados a objetos de que hay
otras maneras de desarrollar en Python que pueden ser tanto o más interesantes, dependiendo del
problema a resolver
Pero ya sabemos que no todo puede ser perfecto constantemente, si estamos manejando clases
pequeñas y con atributos conocidos lo anterior puede llegar a generar un cuello de botella. El uso
de elementos de python del tipo dict (diccionarios) desperdicia una gran cantidad de memoria RAM
y Python no puede asignar una cantidad de memoria estática para almacenar los atributos. Si
creamos muchos objetos, en el orden de miles o millones, estaremós desperdiciando RAM de forma
inútil.
Para solucionar lo anterior es cuando entran en juego "Slots". Este método nos permite decirle a
Python que use un diccionario y que solo asigne memoraria para una cantidad fija de atributos, los
que conocemos.
class MiClase(object):
def __init__(self, nombre, identificador):
self.nombre =nombre
self.identificador = identificador
...
Si usaramos Slots:
class MiClase(object):
__slots__ = ['nombre', 'identificador']
def __init__(self, nombre, identificador):
self.nombre = nombre
self.identificador = identificador
...
64
¿Que ganamos con el uso de slots?. Usando este metodo reducimos drasticámente el uso de RAM. En
algunos casos podemos llegar hasta un 50%. Es importante tener claro que este metodo hay que
usarlo siempre al empezar a definir la clase.
65
5.5. Lab: Programación Orientada a Objetos
A continuación repasaremos los conceptos aprendidos con unos ejercicios
5.5.1. Laboratorio 01
Escribir una clase en Python llamada 'Rectangle' con propiedades length y width y métodos para
calcular el área y el perímetro. Debemos poder asignar ancho y alto, comprobando que son
números mayores de 0.
Solución
class Rectangle:
def __init__(self, l, w):
# Aqui estamos llamando a los setters (ver abajo)
self.length = l
self.width = w
@property
def area(self):
return self._length*self._width
@property
def perimeter(self):
return self._length * 2 + self._width * 2
@property
def length(self):
return self._length
@property
def width(self):
return self._width
@length.setter
def length(self, value):
if value < 0:
raise ValueError("El valor de length ha de ser mayor que 0")
else:
self._length = value
@width.setter
def width(self, value):
if value < 0:
raise ValueError("El valor de width ha de ser mayor que 0")
else:
self._width = value
# Esto implica que no ha de ser posible crear objeto con dimensiones negativas.
try:
66
newRectangle = Rectangle(-12, 10) ①
except ValueError as e:
print(e)
newRectangle.width = 5
newRectangle.length = 8
print(newRectangle.area)
print(newRectangle.perimeter)
newRectangle.width = -10
5.5.2. Laboratorio 02
Escribir una clase 'Circulo' con una propiedad radius y métodos para calcular el área y el
perímetro. También se ha de poder comprobar que el valor de radius es positivo, al asignarlo
Solución
67
import math
class Circle:
def __init__(self, r):
self.radius = r
@property
def area(self):
return self._radius**2*math.pi
@property
def perimeter(self):
return 2*self._radius*math.pi
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
print("El valor de radius ha de ser mayor que 0")
else:
self._radius = value
NewCircle = Circle(8)
print(NewCircle.area) # 201.06192982974676
print(NewCircle.perimeter) # 50.26548245743669
NewCircle.radius = 5
print(NewCircle.area) # 78.53981633974483
print(NewCircle.perimeter) # 31.41592653589793
5.5.3. Laboratorio 03
Escribir una clase Shape con métodos abstractos para calcular el área y el perímetro. Las dos clases
de los ejercicios anteriores podrán heredar entonces de esta clase padre
Solución
class Shape:
@property
68
def area():
raise NotImplementedError("Por favor, implementa este método en una clase hija")
@property
def perimeter():
raise NotImplementedError("Por favor, implementa este método en una clase hija")
class Rectangle(Shape):
def __init__(self, l, w):
self.length = l
self.width = w
@property
def area(self):
return self._length*self._width
@property
def perimeter(self):
return self._length * 2 + self._width * 2
@property
def length(self):
return self._length
@property
def width(self):
return self._width
@length.setter
def length(self, value):
if value < 0:
print("El valor de length ha de ser mayor que 0")
else:
self._length = value
@width.setter
def width(self, value):
if value < 0:
print("El valor de width ha de ser mayor que 0")
else:
self._width = value
newRectangle.width = 5
newRectangle.height = 8
print(newRectangle.area) # 60
print(newRectangle.perimeter) # 34
69
newRectangle.width = -10 # El valor de width ha de ser mayor que 0
5.5.4. Laboratorio 04
Escribir un programa que itere sobre una lista conteniendo datos de personas en forma de tuplas:
username, email, age y añada al usuario en un almacen de datos si al menos tiene 18 años.
Deberemos crear una clase para almacenar los datos de los usuarios, y también deberemos crear
clases de excepciones para controlar los siguientes casos de error:
• La dirección de email no es válida (basta con comprobar que está el símbolo @ y un nombre de
dominio)
El programa deberá detectar estos casos de error y lanzar excepciones donde corresponda. Además,
las excepciones han de ser capturadas y se debe mostrar un mensaje diferente por cada una de
ellas.
Solución
class InvalidAgeError(Exception):
pass
class UnderageError(Exception):
pass
class InvalidEmailError(Exception):
pass
# Clase para almacenar los datos de los usuarios: nombre, edad y email
class User:
def __init__(self, username, age, email):
self.username = username
self.age = age
self.email = email
@property
def age(self):
return self._age
@age.setter
def age(self, a):
if int(a) < 0:
raise InvalidAgeError("La edad no puede ser menor que cero")
70
elif int(a) < 18:
raise UnderageError("El usario es menor de 18 años")
else:
self._age = int(a)
@property
def email(self):
return self._email
@email.setter
def email(self, e):
if '@' not in e:
raise InvalidEmailError("El email no tiene un formato correcto")
else:
self._email = e
def __str__(self):
return "{} is {} years old and has the email {}".format(self.username, self.age, self.email)
example_list = [
("jane", "[email protected]", 21),
("bob", "bob@example", 19),
("jane", "[email protected]", 25),
("steve", "steve@somewhere", 15),
("joe", "joe", 23),
("anna", "[email protected]", -3),
]
directory = {}
5.5.5. Laboratorio 05
Construye una clase llamada 'Numbers'. que cumpla los siguientes requisitos
• Ha de tener un constructor, que reciba dos parámetros, 'x' e 'y', y los almacene por separado
• Ha de tener un método 'add', que devuelva la suma de los atributos 'x' + 'y'
• Ha de tener un método estático 'substract', que reciba dos parámetros 'a', 'b', y devuelva 'a' - 'b'
71
• Ha de tener un método 'value' que devuelva una tupla formada por 'x', 'y'. Convertir dicho
método en una propiedad, y escribirle un setter y un deleter.
Solución
class Numbers:
MULTIPLIER = 3.5
def add(self):
return self.x + self.y
@classmethod
def multiply(cls, a):
return cls.MULTIPLIER * a
@staticmethod
def substract(b, c):
return b - c
@property
def value(self):
return (self.x, self.y)
@value.setter
def value(self, xy_tuple):
self.x, self.y = xy_tuple
@value.deleter
def value(self):
del self.x
del self.y
# Pruebas
assert(Numbers.MULTIPLIER == 3.5)
n = Numbers(4, 4)
assert(n.add() == 8)
assert(Numbers.multiply(2) == 7.0)
assert(Numbers.substract(5, 3) == 2)
assert(n.value == (4, 4))
n.value = (8, 6)
assert(n.value == (8, 6))
del n.value
72
try:
print(n.x)
except Exception as e:
print(e)
try:
print(n.y)
except Exception as e:
print(e)
5.5.6. Laboratorio 06
Escribir una clase 'Box' y usarla para definir ciertos métodos que una caja debería tener:
• 'add': para añadir items a la caja. Recibe una cantidad indeterminada de items, pero por
separado, no en forma de lista.
• 'empty': para sacar todos los items de la caja y devolverlos como una lista
De manera que los métodos de la clase padre deberían informar de que no son métodos destinados
a ser llamados, sino a ser sobreescritos por sus hijos. ¿Cómo lo conseguiríamos?
PISTA: Seguramente haya una excepción que los métodos de la clase base puedan lanzar en un caso
como éste
Escribir también una clase 'Item', genérica, que simplemente contenga como atributos un nombre y
un valor. Estos son los items que se guardarán en la caja
Solución
class Item:
def __init__(self, name, value):
self.name = name
self.value = value
def __str__(self):
return "{} => {}".format(self.name, self.value)
class Box:
def add(self, *items):
raise NotImplementedError()
73
def empty(self):
raise NotImplementedError()
def count(self):
raise NotImplementedError()
def items(self):
raise NotImplementedError()
class ListBox(Box):
def __init__(self):
self._items = []
def empty(self):
items = self._items
self._items = []
return items
def count(self):
return len(self._items)
def items(self):
return self._items
class DictBox(Box):
def __init__(self):
self._items = {}
def empty(self):
items = list(self._items.values())
self._items = {}
return items
def count(self):
return len(self._items)
def items(self):
return self._items
74
i2 = Item("Item 2", 2)
i3 = Item("Item 3", 3)
listbox = ListBox()
dictbox = DictBox()
assert(listbox.count() == 3)
assert(dictbox.count() == 3)
for i in listbox.items():
print(i)
listbox.empty()
dictbox.empty()
assert(listbox.count() == 0)
assert(dictbox.count() == 0)
for i in listbox.items():
print(i)
75
Capítulo 6. Excepciones con Python
Es interesante conocer la creación y manejo de excepciones en Python, nos ayudar a realizar una
programación defensiva adelantandos a los posibles errores que pueden sucedernos durante la
ejecución de nuestras aplicaciones. Es decir, podemos controlar los errores.
Aunque una expresión o declaración sea sintactimente correcta, puede generarnos un error cuando
la intentemos ejecutar. Este tipo de errores es lo que denominaremos una excepción. En este
capítulo del temario vamos a aprender a gestionarlos en nuestros programas de Python.
while True:
try:
x = int((input("Introduce un número: ")))
break
except ValueError:
print("Eyyyy!! El número introducido no es válido")
El ejemplo anterior pide al usuario que introduzca un número, pero vamos a ver que pasa en su
interior. La sentencia try funciona de la siguiente forma:
• Se ejecuta la cláusula try. Es decir se ejecutan todas las líneas de código que hay entre try y
except.
• Si durante la ejecución de try se produce una excepción se omitirá el resto de esta cláusula. * Si
la excepción coincide con la que hemos nombrado después de la palabra reservada except, se
ejecutará esta cláusula y la ejecución continua después del try/except.
• Si ocurre una excepción que no coincide con la indicada en except se pasa a los try mas
externos, si no se encuentra el gestor se genera una excepción no gestionada (unhandled
exception) y la ejecución se interrumpe con un mensaje como el mostrado en el print del
ejemplo.
Tenemos que tener en cuenta que una declaración try puede tener más de una cláusula except, de
esta manera podemos especificar gestores para varias excepciones. Pero tenemos que saber que:
• Los gestores solo manejan las excepciones que ocurren en la cláusula try correspondiente, no
en otros gestores de la misma declaración try.
• Una clausula except puede nombrar múltiples excepciones como una tupla
76
except (RuntimeError, TypeError, NameError):
◦ Hay que se cuidadosos, ya que podemos ocultar un error de programación de esta manera.
◦ Se puede usar para imprimir un mensaje de error y luego volver a generar la excepción. De
esta manera permitimos que la función que llama tambiñen maneje la excepción.
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print(f"OS error: {err}")
except ValueError:
print("No se puede convertir el dato en un entero")
except BaseException as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
Una declaración try/except tiene una cláusula else opcional, que, cuando está incluida debe seguir
todas las cláusulas except. Esto es útil para el código que debe ejecutarse si la cláusula try no lanza
una excepción.
• El uso de else es mejor que el añadir mas código a la cláusula try ya que evita capturar
accidentalmente una excepción que no fue generada por el código protegido por la declaración
try/except
• Cuando sucede una exepción puede darse el caso de que tenga un valor asociado, este valor es
conocido como el argumento de la excepción. La presencia y el tipo de este argumento
dependerá del tipo de excepción.
Una cláusula except puede especificar una variable después del nombre de la excepción:
• La variable está vinculada a una instancia de excepción con los argumentos almacenados en
instance.args.
• Por convenio la instancia de excepción define _str_() para que los argumentos se puedan
imprimir directamente sin tener que hacer referencia a .args.
• Podemos crear una instancia de una excepción antes de generarla y agregarle los atributos.
77
try:
raise Exception('spam', 'eggs') # Estamos forzando el lanzar una excepción
except Exception as inst:
print(type(inst)) #Instanciamos la excepción
print(inst.args) #Imprimimos los argumentos almacenados en .args
print(inst) #Estamos usando __str__ para imprimir los resultados directamente
x, y = inst.args
print("x =", x)
print("y =", y)
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
Los gestores de excepciones no solo gestionan las excepciones si ocurren en la cláusula try ,
también las gestionan si ocurren dentro de funciones que son llamadas (aunque se indirectamente)
en las cláusulas try.
def esto_falla():
x = 1/0
try:
esto_falla()
except ZeroDivisionError as err:
print("Manejando error en ejecución:", err)
• Este argumento debe ser una instancia de excepción o una clase de excepción (que hereda de
Exception)
78
raise NameError('HiThere')
• Si fuera necesario determinar si una excepción fue lanzada pero sin intención de gestionarla,
podemos usar una versión simplificada de la instrucción raise para relanzarla
try:
raise NameError('Que tal')
except NameError:
print('Aquí tenemos una excepción')
raise
79
def func():
raise ConnectionError
try:
func()
except ConnectionError as exc:
raise RuntimeError ("Failed to open") from exc
The above exception was the direct cause of the following exception:
try:
open('database.sqlite')
except OSError:
raise RuntimeError from None
Son las que utilizamos como clases padre para otras excepciones.
exception BaseException
La hemos usado anteriormente, es la clase padre para todas las excepciones predefinidas.
• No hay que confundirla con Exception, esta clase no está pensada para que la hereden las
clases definidas por el usuario.
• Si se llama al método _str()_ en una instancia de esta clase nos devuelve la representación
de los argumentos de la instancia o la cadena vacia cuando no habia argumentos.
80
• args: Es la tupla de argumentos dada al constructor de excepción.
◦ Otras excepciones se llaman solo con una sola cadena que da un mensaje de error.
exception Exception
• Todas las excepciones predefinidas quie no salen del sistema heredan de esta clase.
• Todas las excepciones definidas por el usuario también deben heredar de esta clase
exception ArithmeticError
• Esta es la clase padre para las excepciones predefinidas que se generan para varios errores
aritméticos (OverflowError,ZeroDivisionError,FloatingPointError)
exception BufferError
• Se genera cuando buffer no puede realizar una operación relacionada
exception LookupError
• Clase padre para las excepciones que se generan cuando una clave o índice utilizado en un
mapa o secuencia que no es válido (` IndexError`,KeyError).
exception AssertionError
exception AttributeError
exception EOFError
• Se genera cuando la función input() alcanza una condición de fin de archivo sin leer ningún
dato.
exception GeneratorExit
exception ImportError
81
• Se genera cuando una instrucción import tiene problemas al intentar cargar un módulo.
exception IndexError
exception KeyError
exception NameError
exception OSError([arg])
• Esta excepción se produce cuando una función del sistema retorna un error relacionado con el
sistema, que incluye fallas de E/S como file not found o disk full (no para tipos de argumentos
ilegales u otros errores incidentales).
Por supuesto hay muchas mas excepciones, puedes consultarlas en este enlace
Lo primera que tendremos que hacer es crear nuestra propia clase que herede de la ya mencionada
clase Exception
class EstaEsMiExcepcion(Exception):
pass
Por supuesto podemos pasarle parámetros de entrada al lanzarla. Esto nos va a permitir dar
información adicional a la excepción. Para pasar estos parametros deberemos modificar el método
_init_
class EstaEsMiExcepcion(Exception):
def __init__(self, parametro1, parametro2):
self.parametro1 = parametro1
self.parametro2 = parametro2
Una vez creada nuestra excepción personalizada podemos lanzarla con raise. Además podemos
acceder a los parámetros pasados como argumentos al lanzar la excepción.
82
class EstaEsMiExcepcion(Exception):
def __init__(self, parametro1, parametro2):
self.parametro1 = parametro1
self.parametro2 = parametro2
try:
raise EstaEsMiExcepcion("Valor1", "Valor2")
except EstaEsMiExcepcion as ex:
p1, p2 = ex.args
print(type(ex))
print("Primer Parametro =", p1)
print("Segundo Parametro =", p2)
<class '__main__.EstaEsMiExcepcion'>
Primer Parametro = Valor1
Segundo Parametro = Valor2
83
6.6. Lab: Excepciones
6.6.1. Laboratorio 1
Escribe un programa que pida al usuario introducir dos números y realice la división del primero
por el segundo devolviendo el cociente como resultado. Para escribir el programa debes utilizar
try/except para que arroje una excepción si el denominador es cero o el dato introducido no es un
número valido.
def division(a,b):
c = a/b
return print(f"el resultado de dividir {a} entre {b} es {c}")
try:
division(int(a),int(b))
except (ZeroDivisionError, ValueError) as error:
print(f"Existe un error: {error}")
finally:
print("\nFin del programa")
6.6.2. Laboratorio 2
Modifica el código anterior para siga pidiendo datos al usuario mientras que estos no sean validos.
def division(a,b):
c = a/b
return print(f"el resultado de dividir {a} entre {b} es {c}")
while True:
a = input("Introduce el primer número: ")
b = input("Introduce el segundo número: ")
try:
division(int(a),int(b))
except (ZeroDivisionError, ValueError) as error:
print(f"Existe un error: {error}")
print(f"Por favor vuelva a introducir los valores")
else:
break
84
Capítulo 7. Creación de paquetes propios
7.1. Manejando módulos
Lo primero que debemos aclarar es a que llamamos módulo y paquete en Python:
Estos módulos los podemos argrupar en paquetes, es decir una carpeta que contiene archivos .py
pero además debe contener un archivo de inicio llamado init.py Este último archivo no tiene
porque contener ninguna instrucción , puede estar completamente vacio.
└── paquete
├── __init__.py
├── modulo1.py
├── modulo2.py
└── modulo3.py
└── paquete
├── __init__.py
├── modulo1.py
└── subpaquete
├── __init__.py
├── modulo1.py
└── modulo2.py
Tampoco hay que olvidar el destarcar que un módulo no está obligado a pertenecer a un paquete.
El contenido de un módulo podrá ser usado por otros módulos mediante la ya conocida instrucción
import
7.1.2. Namespaces
Para acceder (desde el módulo donde se realizó la importación), a cualquier elemento del módulo
importado, se realiza mediante el namespace, seguido de un punto (.) y el nombre del elemento que
85
se desee obtener. En Python, un namespace, es el nombre que se ha indicado luego de la palabra
import, es decir la ruta (namespace) del módulo:
print módulo.CONSTANTE_1
print paquete.módulo1.CONSTANTE_1
print paquete.subpaquete.módulo1.CONSTANTE_1
7.1.3. Alias
Es posible también, abreviar los namespaces mediante un alias. Para ello, durante la importación,
se asigna la palabra clave as seguida del alias con el cuál nos referiremos en el futuro a ese
namespace importado:
import módulo as m
import paquete.módulo1 as pm
import paquete.subpaquete.módulo1 as psm
Luego, para acceder a cualquier elemento de los módulos importados, el namespace utilizado será
el alias indicado durante la importación:
print m.CONSTANTE _1
print pm.CONSTANTE _1
print psm.CONSTANTE_1
En Python, es posible también, importar de un módulo solo los elementos que se desee utilizar.
Para ello se utiliza la instrucción from seguida del namespace, más la instrucción import seguida
del elemento que se desee importar:
• Es posible también, importar más de un elemento en la misma instrucción. Para ello, cada
elemento irá separado por una coma (,) y un espacio en blanco.
• si los elementos importados desde módulos diferentes tienen los mismos nombres usaremos
alias para prevenir fallos
2. Primero debemos importar los módulos de Python, después los de terceros y finalmente los
propios de la aplicación.
86
3. Entre cada bloque de imports, debemos dejar una línea en blanco.
Los módulos, como hemos venido haciendo hasta ahora, podemos ejecutarlos como scripts
El código en el módulo será ejecutado, tal como si lo hubieses importado, pero con name con el
valor de "main". Eso significa que agregando este código al final de tu módulo:
if __name__ == "__main__":
import sys
fib(int(sys.argv[1]))
Esto nos permite usar el archivo como script o como módulo importable. Al analizar la linea de
ordenes sólo se ejecutará si el módulo es ejecutado como archivo principal.
• Por ejemplo el nombe del módulo A.B designara un submódulo B de un paquete llamado A.
• El uso de los módulos salva a los autores de los diferentes módulos de tener que preocuparse
por los nombres de las variables globales de cada uno
• El uso de nombres de módulo con puntos salva a los autores de paquetes con múltiples módulos
de preocuparse por los nombres de los módulos de cada uno.
Imaginemos que queremos usar una colección de módulos para el manejo uniforme de archivos y
datos de sonido, donde podemos encontrar varios tipos de archivos. Esto nos obligaria a crear y
mantener una coleccion siempre creciente de módulos para la conversión entre los distintos
formatos de archivos. Podríamos ejecutar muchas operaciones sobre estos datos, de esta forma
estariamos escribiendo una lista sin fin de módulos para realizar las operaciones. Imaginemos esta
posible estructura:
87
sound/ Top-level package
__init__.py Initialize the sound package
formats/ Subpackage for file format conversions
__init__.py
wavread.py
wavwrite.py
aiffread.py
aiffwrite.py
auread.py
auwrite.py
...
effects/ Subpackage for sound effects
__init__.py
echo.py Dentro de este módulo está la función echofilter
surround.py
reverse.py
...
filters/ Subpackage for filters
__init__.py
equalizer.py
vocoder.py
karaoke.py
...
Los archivos init.py son obligatorios para que Python trate los directorios que contienen los
archivos como paquetes. Esto evita que los directorios con un nombre común, como string, oculten
involuntariamente módulos válidos que se producen luego en el camino de búsqueda del módulo.
En el caso mas simple, init.py puede ser solo un archivo vacío, pero también puede ejecutar código
de inicialización para el paquete o el conjunto de variables all, descriptas luego.
import sound.effects.echo
88
De esta forma cargariamos el submodulo echo y lo estaríamos dejando disponible sin su prefijo de
paquete. Podriamos usarlo así:
Hay que destacar que al usar esta forma from package import ítem este item puede ser tanto un
módulo o subpaquete como algún otro nombre definido en el paquete, como una función, clase, o
variable. Realmente funciona de la siguiente manera:
Cuado usamos la sintaxis _import item.subitem.subsubitem, cada ítem excepto el último debe ser
un paquete; el mismo puede ser un módulo o un paquete pero no puede ser una clase, función o
variable definida en un ítem previo.
Hay que tener cuidado si se nos ocurre usar from sound.effects import * Lo lógico o
esperable es que fuera al sistema de archivos, encuentra los submódulos en el
paquete y los importe todos. Pero hay que tener cuidado porque esto puede tardar
demasiado e importar submódulos que nos acarren efectos no esperados.
Para solucionar esto lo único que podemos hacer es como autores del paquete proveer un indice
explicito del paquete, porque la declaración import usa la siguiente convención: * Si el codigo
dentro de init.py define una lista llamada _ _ all _ _.
• Esta lista se toma como la lista de los nombres de los módulos que deberian ser importados con
from package import *|
• Es responsabilidad del autor del paquete mantener esta lista actualizada con las nuevas
versiones. Los autores de paquetes podrían no soportar un importar * si no ven un uso en sus
paquetes.
• Para el ejemplo de archivos de sonido nuestro "init" pdría contener el siguiente código
De esta forma con from sound.effects import * importaria los tres submódulos del paquete sound
89
Si no se define all, la declaración from sound.effects import * no importa todos los submódulos del
paquete sound.effects al espacio de nombres actual; sólo se asegura que se haya importado el
paquete sound.effects (posiblemente ejecutando algún código de inicialización que haya en init.py)
y luego importa aquellos nombres que estén definidos en el paquete. Esto incluye cualquier
nombre definido (y submódulos explícitamente cargados) por init.py. También incluye cualquier
submódulo del paquete que pudiera haber sido explícitamente cargado por declaraciones import
previas. Considere este código:
import sound.effects.echo
import sound.effects.surround
from sound.effects import *
En este ejemplo, los módulos echo y surround se importan en el espacio de nombre actual porque
están definidos en el paquete sound.effects cuando se ejecuta la declaración from…import. (Esto
también funciona cuando se define all).
A pesar de que ciertos módulos están diseñados para exportar solo nombres que siguen ciertos
patrones cuando uses import *, también se considera una mala práctica en código de producción.
Recuerda, ¡no hay nada malo al usar from package import specific_submodule! De hecho, esta es la
notación recomendada a menos que el módulo que importamos necesite usar submódulos con el
mismo nombre desde un paquete diferente.
Cuando nuestro paquete está dividido en subpaquetes podemos usar import absolutos para para
referirte a submódulos de paquetes hermanos. Por ejemplo, si tenemos el módulo
sound.filters.vococer puede necesitar usar el módulo echo que se encuentra en sound.effects. Lo
importariamos de esta forma from sound.effects import echo
Es verdad, que también podríamos usar import relativos con la forma from module import name_.
Estos imports hacen uso de puntos para indicar los paquetes actuales o paquetes padre
involucrados en el import relativo. Por ejemplo con surround:
Los import relativos se basan en el nombre del modulo actual porque los el
nombre del modulo principal es siempre "main", los módulos pensados para
usarse como modulo principal de una aplicación Python siempre deberían usar
import absolutos.
Ademas de los atributos o métodos vistos hasta ahora los paquetes soportan un atributo especial
mas, path. Este path inicializa una lista que contiene el nombre del directorio donde está init del
90
paquete, antes de que el codigo en ese archivo se ejecute.
Aunque esta característica no se necesita frecuentemente, puede usarse para extender el conjunto
de módulos que se encuentran en el paquete.
91
7.3. Lab: Módulos y paquetes
7.3.1. Creando un módulo
Crea un módulo propio llamado fibo.py en el que vamos a calcular la secuencia de Fibbonacci
Solución
De esta manera solo hemos añadido el nombre del módulo al espacio actual. Para usar las
funciones que contiene deberemos usar nombredemódulo.funcion para acceder a ellas.
Solución
>>> fibo.fib(1000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> fibo.fib2(100)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> fibo.__name__
'fibo'
Solución
92
>>> fib = fibo.fib
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
Solución
De esta forma hacemos lo contrario que en el caso anterior estamos introduciendo en nuestro
espacio el nombre de las funciones pero el del módulo.
• Ahora importa fibo asignandole el alias fib. De esta forma el alias queda ligado al módulo
importado. Es lo mismo que un import fibo solo que ahora en nuestro espacio usaremos fib para
referirnos a fibo.
Solución
Crea un paquete con el nombre que elijas, dentro debe contener dos subpaquetes:
93
── paquete
│ ├── despedida
│ │ ├── adios.py
│ │ └── __init__.py
│ ├── hola
│ │ ├── __init__.py
│ │ └── saludos.py
│ └── __init__.py
└── script.py
Esta es la estructura que debemos tener para que funcione, una vez creados el paquete,
subpaquetes y módulos crearemos el script.py para ejecutarlo.
Solución
Lo primero es crear
__init.py__
paquete/hola/saludos.py
def saludar():
print("Hola, ¿como estás? Yo estoy bien, puedes encontrarme dentro de la funcion saludar del módulo saludos")
paquete/despedida/adios.py
def despedir():
print("Hasta luego, me despido del curso de Python desde la funciñon despedir del módulo adios")
saludar()
despedir()
94
7.3.3. Distribuyendo un paquete
Vamos a hacer ahora un paquete que podamos distribuir. Para ello, lo primero es crear un fichero
setup.py fuera de la raíz del paquete. Indicando una información básica de la siguiente forma:
.
├── paquete
│ ├── despedida
│ │ ├── adios.py
│ │ ├── __init__.py
│ │ └── __pycache__
│ │ ├── adios.cpython-39.pyc
│ │ └── __init__.cpython-39.pyc
│ ├── hola
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-39.pyc
│ │ │ └── saludos.cpython-39.pyc
│ │ └── saludos.py
│ ├── __init__.py
│ └── __pycache__
│ └── __init__.cpython-39.pyc
├── script.py
└── setup.py
Para escribir setup.py nos vamos a ayudar del modulo setuptools y la función setup
Cuando ya hemos definido la información básica, incluyendo los paquetes y subpaquetes que lo
forman como los scripts (en caso de tenerlos), vamos a crear el paquete distribuible. Dedemos
utilzar el siguiente comando en el directorio donde tengamos setup.py
Si todo ha salido bien deberemos ver una carpeta comprimida llamadas dist. Este fichero es
unuestro distribuible, ya podemos compartirlo con todo el mundo para que instale nuestro
paquete.
95
Para instalar un paquete en Python, y hacer que se pueda usar desde cualquier lugar lo haremos de
esta forma, siempre desde el directorio donde este el paquete comprimido.
96
Capítulo 8. Manipulación de parametros de
entrada/salida
Los programas que escribamos, recibirán ciertos parámetros de entrada para trabajar con ellos.
Veremos en este capítulo como gestionar todo tipo de parámetros.
• Parámetros posicionales: Nuestro programa hará una u otra cosa en función de la posición que
ocupe dicho parámetro. El ejemplo más claro es la orden 'cp origen destino'
• Parámetros de ayuda: Explican cómo usar nuestro programa. Es habitual el uso del parámetro
--help en muchos programas para mostrar ayuda de uso
En Python 3 tenemos a nuestra disposición al menos 3 módulos que nos permitirán gestionar todos
estos tipos de parámetros:
• getopt: Otro parseador, más familiar para los programadores de C (se parece mucho a la función
homónima de C), pero algo más limitado
Nosotros vamos a usar argparse, del cual tenemos un buen resumen en este enlace.
import argparse
parser = argparse.ArgumentParser()
parser.parse_args()
Si corremos el script anterior con la opción --help: 'python argparse01.py --help', obtenemos esta
salida:
97
usage: argparse01.py [-h]
optional arguments:
-h, --help show this help message and exit
Es decir, las opciones -h y --help se gestionan automáticamente. No está mal. Pero no podemos ir
mucho más allá por el momento. Intentemos otro parámetro opcional: 'python argparse01.py
--verbose'
La salida será:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print(args.echo)
Ahora, nuestro programa requerirá un parámetro echo para funcionar. Es decir, que si
ejecutamos 'python argparse02.py', obtendremos
Usemos la ayuda para ver qué parámetros podemos usar: 'python argparse02.py --help'
98
usage: argparse02.py [-h] echo
positional arguments:
echo
optional arguments:
-h, --help show this help message and exit
Ya sabemos que nuestro programa requiere un argumento posicional, pero aun no sabemos para
qué se usa. Vamos a añadir esa información. Volvemos a rescribir el archivo y lo guardamos como
argparse03.py
positional arguments:
echo simplemente muestra por pantalla la cadena que le pasemos en
esta posición
optional arguments:
-h, --help show this help message and exit
Gracias a que estamos trabajando con Python 3, que usa unicode para todas las cadenas de texto, no
tenemos problemas con el caracter acentuado que hemos añadido en nuestro script. Pero en
Python 2 habríamos obtenido el siguiente error:
Como se indica en el enlace que nos sugiere el error, para poder trabajar de manera segura en
Python 2 con un fichero que contiene caracteres no ASCII, debemos añadir lo siguiente como
primera línea del fichero
99
# -*- coding: utf-8 -*-
Ahora que ya sabemos más sobre nuestro script, podemos llamarlo con un argumento, para que
nos devuelva el eco: 'python sources/t05/t05ej03.py "hola mundo"'
Vamos ahora a intentar hacer algo con las variables que le pasemos de entrada, además de
mostrarlas. (Lo guardamos como argparse04.py)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="Muestra el número que le hayamos pasado elevado al cuadrado", type=int) ①
args = parser.parse_args()
print(args.square**2)
① Pasamos type=int para indicar que el parámetro sea convertido a un número entero
Lo lanzamos con 'python argparse04.py 5', y obtenemos '25', que es el resultado de elevar 5 al
cuadrado.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="incrementa la verbosidad de la salida por pantalla")
args = parser.parse_args()
if args.verbosity:
print("Ahora saco más cosas por pantalla")
Si lo que queremos es construir un parámetro optativo que realmente actúe como un flag, que es
como se comporta --help, tendremos que especificárselo.
100
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", help="incrementa la verbosidad de la salida por pantalla", action="store_true") ①
args = parser.parse_args()
if args.verbose:
print("Ahora saco más cosas por pantalla")
① Pasamos store=true para indicar que, caso de estar, ese parámetro tome el valor True
Ya podemos llamarlo 'python argparse06.py --verbosity', para obtener la misma salida que en el
caso anterior.
La mayoría de parámetros optativos tienen una version abreviada. Es muy fácil añadirla.
(argpase07.py):
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", "-v", help="incrementa la verbosidad de la salida por pantalla", action="store_true") ①
args = parser.parse_args()
if args.verbose:
print("Ahora saco más cosas por pantalla")
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
help="Muestra el número que le hayamos pasado elevado al cuadrado")
parser.add_argument("-v", "--verbose", action="store_true",
help="incrementa la verbosidad de la salida por pantalla")
args = parser.parse_args()
answer = args.square**2
if args.verbose:
print("El número {} elevado al cuadrado es {}".format(args.square, answer))
else:
print(answer)
Ahora podemos llamarlo de dos maneras: 'python argparse08.py -v 4' ó 'python argparse08.py 4 -v'.
En ambos casos, obtendremos la misma salida:
101
También podemos limitar los valores de una determinada opción a un rango (argparse09.py)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
help="Muestra el número que le hayamos pasado elevado al cuadrado")
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2], ①
help="incrementa la verbosidad de la salida por pantalla")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
print("El número {} elevado al cuadrado es {}".format(args.square, answer))
elif args.verbosity == 1:
print("{}^2 == {}".format(args.square, answer))
else:
print(answer)
python argparse09.py -v 0
python argparse09.py -v 1
python argparse09.py -v 2
16
4^2 == 16
El número 4 elevado al cuadrado es 16
Vamos a cambiar la manera de trabajar de -v, para que se asemeje más a los típicos programas
escritos en Python, o incluso en C. En lugar de especificar un nivel de verbosidad, contaremos el
número de veces que aparece -v. (argparse10.py)
102
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
help="Muestra el número que le hayamos pasado elevado al cuadrado")
parser.add_argument("-v", "--verbosity", action="count", default=0, ①
help="incrementa la verbosidad de la salida por pantalla")
args = parser.parse_args()
answer = args.square**2
if args.verbosity >= 2:
print("El número {} elevado al cuadrado es {}".format(args.square, answer))
elif args.verbosity >= 1:
print("{}^2 == {}".format(args.square, answer))
else:
print(answer)
python argparse10.py 4
python argparse10.py 4 -v
python argparse10.py 4 -vv
python argparse10.py 4 -vvvvvvvvv
16
4^2 == 16
El número 4 elevado al cuadrado es 16
El número 4 elevado al cuadrado es 16
103
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="la base")
parser.add_argument("y", type=int, help="el exponente")
args = parser.parse_args()
answer = args.x**args.y
if args.quiet:
print(answer)
elif args.verbose:
print("{} elevado a {} es igual a {}".format(args.x, args.y, answer))
else:
print("{}^{} == {}".format(args.x, args.y, answer))
Probemos las dos opciones mutuamente exclusivas con 'python argparse11.py -q -v'
Y para terminar, añadamos una explicación de lo que nuestro programa hace, salvamos el código
como argparse12.py
import argparse
if args.quiet:
print(answer)
elif args.verbose:
print("{} elevado a {} es igual a {}".format(args.x, args.y, answer))
else:
print("{}^{} == {}".format(args.x, args.y, answer))
104
usage: argparse12.py [-h] [-v | -q] x y
positional arguments:
x la base
y el exponente
optional arguments:
-h, --help show this help message and exit
-v, --verbose
-q, --quiet
105
8.7. Lab: Manipulacion E/S
A continuación repasaremos los conceptos aprendidos con unos ejercicios
8.7.1. Laboratorio 01
Crea un script python que lea de un fichero de texto pasado como argumento (obligatorio) línea a
línea. De cada línea, ha de eliminar símbolos de puntuación y pasar todo a minúsculas. Además, ha
de sacar cada línea por pantalla.
Para probarlo, lanza el intérprete de python3 desde consola, y usa cualquiera de los ficheros de
texto disponibles con estos cuadernos (ej: quijote.txt)
Solucion
"""
Script que lee un fichero, elimina espacios en blanco y símbolos de puntuación, y pone todo en minúsculas.
"""
import argparse
import string
def main():
parser = argparse.ArgumentParser()
parser.add_argument("source_file", help="Fichero de entrada")
args = parser.parse_args()
filename = args.source_file
if __name__ == '__main__':
main()
8.7.2. Laboratorio 02
• Cuando el usuario lance el script con la opción de mostrar ayuda de los parámetros, lo primero
que le ha de aparecer es el texto: Crea gráfica de polinomios de Chebysev de órdenes M a N para
valores entre -X y +X
• Igualmente, lo último que ha de ver el usuario cuando pida ayuda es tu nombre y correo
electrónico: Autor: Fulanito de tal [email protected]
• El programa ha de exigir un único parámetro posicional, que será la ruta completa a un fichero
106
de imagen en formato PNG que será creado. Ejempo: grafica.png. La variable que almacenará el
valor de dicho fichero se llamará 'file', y será del tipo necesario para permitir que el programa
escriba en el fichero datos de tipo binario (como de hecho, hace). El texto de ayuda asociado a la
opción será Nombre del fichero para guardar la gráfica (obligatorio)
• El programa ha de aceptar un parámetro opcional '-x' (modo extendido '--limit') de tipo 'float'
con valor por defecto 1.0 y con texto de ayuda: Rango de valores para x. La variable que
almacenará su valor se llamará 'limit'
• El programa ha de aceptar un parámetro opcional '-m' (modo extendido '--min') de tipo entero
con valor por defecto 1 y con texto de ayuda: Orden mínimo del polinomio. La variable que
almacenará su valor se llamará 'min'
• El programa ha de aceptar un parámetro opcional '-M' (modo extendido '--max') de tipo entero
con valor por defecto 3 y con texto de ayuda: Orden maximo del polinomio. La variable que
almacenará su valor se llamará 'max'
• El programa ha de aceptar un parámetro opcional '-t' (modo extendido '--title') de tipo string con
valor por defecto vacío y con texto de ayuda: Título de la gráfica. La variable que almacenará su
valor se llamará 'title'
Pista: Investiga el uso de argparse.FileType para ver cómo declarar argumentos de tipo válido para
que puedan ser tratados como ficheros
Pista: Investiga la clase argparse.ArgumentParser para averiguar cómo obtener lo que se pide en el
primer requisito
107
import argparse
import numpy
npts = arguments.npts
limit = arguments.limit
x = numpy.linspace(-1.0*limit, limit, npts)
pyplot.plot(x,y)
if arguments.title:
pyplot.suptitle(arguments.title)
f = arguments.file
pyplot.savefig(f)
f.close()
Solución
108
import argparse
import numpy
npts = arguments.npts
limit = arguments.limit
x = numpy.linspace(-1.0*limit, limit, npts)
pyplot.plot(x,y)
if arguments.title:
pyplot.suptitle(arguments.title)
f = arguments.file
pyplot.savefig(f)
f.close()
8.7.3. Laboratorio 03
Escribir una aplicación de línea de comandos que devuelva la siguiente respuesta cuando se ejecuta
con el flag -h
109
usage: cli.py [-h] [-u USERNAME] [-p]
My connection client
optional arguments:
-h, --help show this help message and exit
-u USERNAME, --username USERNAME
Username. If not provided it will be read from USERNAME env
var
-p, --password Ask for password
• Si se le llama sin argumentos, deberá fallar con el mensaje: Error: user and password needed
110
# Llamada
python cli.py
# Salida
Traceback (most recent call last):
File "cli.py", line 62, in <module>
raise Exception("Error: user and password needed")
Exception: Error: user and password needed
# Llamada
USERNAME=johndoe python cli.py
# Salida
Traceback (most recent call last):
File "cli.py", line 62, in <module>
raise Exception("Error: user and password needed")
Exception: Error: user and password needed
# Llamada
USERNAME=johndoe PASSWORD=wrongpassword python cli.py
# Salida
Wrong credentials
# Llamada
USERNAME=johndoe PASSWORD=mypassword python cli.py
# Salida
Correct credentials
# Llamada
python cli.py -u johndoe -p
# Salida
Introduce password:
(en función de si metemos el password bien o mal, pondrá Correct credentials o Wrong
credentials)
111
import argparse
import getpass
import hashlib
import os
# CONSTANTS
HELP_TEXT = 'My connection client'
SIGN_OFF = 'Author: John Doe <[email protected]>'
# FUNCTIONS
def users_dict():
USERNAME = 'johndoe'
PASSWORD = 'mypassword'
return users
salt = users[username]['salt']
key = users[username]['key']
new_key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
# MAIN PROGRAM
if __name__ == "__main__":
args = handle_arguments(HELP_TEXT, SIGN_OFF)
# Tu codigo aqui
Solución
112
import argparse
import getpass
import hashlib
import os
# CONSTANTS
HELP_TEXT = 'My connection client'
SIGN_OFF = 'Author: John Doe <[email protected]>'
# FUNCTIONS
def users_dict():
USERNAME = 'johndoe'
PASSWORD = 'mypassword'
return users
parser.add_argument("-u", "--username", help="Username. If not provided it will be read from USERNAME env var")
parser.add_argument("-p", "--password", action="store_true", help="Ask for password")
args = parser.parse_args()
return args
salt = users[username]['salt']
key = users[username]['key']
new_key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
# MAIN PROGRAM
if __name__ == "__main__":
args = handle_arguments(HELP_TEXT, SIGN_OFF)
113
if verify(username, password):
print("Correct credentials")
else:
print("Wrong credentials")
114
Capítulo 9. Test, depuración y loggin
En este capítulo vamos a hacer un workshop. Simularemos una sesión de testing y depuración
usando las herramientas que Python pone a nuestra disposición:
Vamos a utilizar el código disponible en este repositorio de Github. Se puede seguir el taller de dos
maneras:
• Clonando el repositorio
Existe también la rama 'solutions' que muestra las soluciones cuando trabajamos
con Python 2 en vez de Python 3. La diferencia está únicamente en el último
ejemplo: test_chicago_result_loader
F..
======================================================================
FAIL: test_true_is_true (tests.test_basic.FailingTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/jorge/pronoide/nicar2016-python-testing-debugging-
exercises/tests/test_basic.py", line 13, in test_true_is_true
self.assertEqual(False, True)
AssertionError: False != True
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
115
Abriendo el fichero tests/test_basic.py, vemos el test que ha fallado. Es bastante sencillo
self.assertEqual(False, True)
En este punto, aun no hemos visto nada sobre tests unitarios en Python, pero mirando el código,
entendemos lo básico:
• El módulo que importamos se llama 'unittest'. Contiene lo necesario para implementar tests
unitarios
• Dentro de cada clase hija de 'TestCase', definiremos tantos métodos como pruebas queramos
hacer
• Compararemos los valores esperados con los valores devueltos mediante la familia de funciones
'assert'
Continuamos lanzando tests. Dentro una test suite podemos lanzar tests cases individuales
116
E
======================================================================
ERROR: test_load_bad_json (tests.test_result_loader.SimpleResultLoaderTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/jorge/pronoide/nicar2016-python-testing-debugging-
exercises/tests/test_result_loader.py", line 13, in test_load_bad_json
results = loader.load(sample_json)
File "/home/jorge/pronoide/nicar2016-python-testing-debugging-
exercises/results/__init__.py", line 10, in load
parsed = json.loads(s)
File "/usr/lib/python3.5/json/__init__.py", line 319, in loads
return _default_decoder.decode(s)
File "/usr/lib/python3.5/json/decoder.py", line 339, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/lib/python3.5/json/decoder.py", line 357, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 4 column 9 (char 42)
----------------------------------------------------------------------
Ran 1 test in 0.003s
FAILED (errors=1)
Abrimos el fichero, y vemos que en esa línea se llama a una función que carga un dato de tipo JSON,
pero dentro de la función, no se comprueba si el dato ha sido correctamente cargado o no antes de
operar con él. Eso provoca un error al intentar acceder a uno de sus campos
Esto tipo de problemas casi siempre se da porque damos por sentadas cosas sin probarlas antes.
Por ejemplo, ¿qué pruebas ejecutarías en este trozo de código para tener controlados los errores?
def pruebame(items):
return [items[1].upper(), items[0].upper()]
9.3. TDD
Un posible enfoque a la hora de desarrollar software es escribir primero el código de prueba, y
luego el código a probar, una vez has cubierto todos los casos que se necesitan. Sus defensores
117
mantienen que ayuda a escribir mejor código y más mantenible.
El fichero tests/test_names.py está vacío. Crea una clase de tests llamada 'TestNames'. Dentro, un
solo método, llamado 'test_parse_names'. En dicho método, llamarás a una función 'parse_names',
que implementarás dentro del fichero names/init.py.
La función recibirá como argumento una cadena con las partes del nombre de una persona,
separadas por comas. Por ejemplo:
{
"first_name": "Jorge",
"last_name": "de Soto",
"middle_name": "Arévalo",
"suffix": "Sr"
}
Por supuesto, habrás de implementar luego la clase 'parse_names', importarla, y llamarla con los
casos de prueba que se te hayan ocurrido.
Una vez termines tanto el código de tests como la función en si, podrás ejecutar el test de esta forma
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
9.4. Refactorizando
A veces, diseñamos nuestro código de manera que es complicado de probar. Creamos clases o
funciones excesivamente complejas, cuando lo ideal sería crear pequeñas funciones que fueran
118
sencillas de probar.
Si abres el código del test, verás que se prueba el método ChicagoResultsLoader.load, que abre un
fichero de texto y carga datos, devolviendo una estructura de tipo JSON, cuyos valores se
comprueban en el test.
• función 'load', para cargar los datos del fichero, línea a línea
Y pudiéramos probar esas dos funciones por separado desde nuestra función de test.
Dado que en esas circunstancias pasaríamos a tener más de una función de test, hay ciertas tareas
repetitivas que ejecutamos en cada caso de test, y que podríamos extraer fuera (instanciar la clase a
probar, y obtener la ruta al fichero de datos).
Por último, podría ser interesante añadir funcionalidad de log al código a probar. De esta forma,
además de probarlo con los tests, también generaríamos en un fichero de texto la salida que la
función probada va generando
• En cada caso de test, habrá código repetitivo que podrás sacar fuera (instanciar
la clase a probar, y obtener la ruta al fichero de datos). Investiga el uso de la
función unittest.TestCase.setUp para centralizar tareas repetitivas que se
tendrían que realizar en todos los tests
119
A grandes rasgos, hay dos situaciones en las que voy a usar 'pdb':
*. Metiendo en mi código llamadas a 'pdb_set_trace' para que el intérprete pare en ese punto
(añadir puntos de parada)
import pdb
*. Entrando en una sesión del intérprete de Python y depurando explícitamente la función mediante
una llamada a 'pdb.runcall'
import pdb
# Pero ya la llamo yo
pdb.runcall(debug_this, 10, 20)
En cualquiera de los dos casos, entraríamos en una sesión del depurador y podríamos ejecutar las
instrucciones paso a paso, entre otros comandos. Los más populares que podríamos usar son:
(Pdb) l
1 def debug_this(i1, i2):
2 -> result = i1
3 for i in range(5):
4 result += i2
5 return result
120
9.5.2. Comando (w)here
Para ver dónde nos encontramoms dentro de la pila de llamadas (puede ser bastante profunda)
(Pdb) w
/usr/lib/python3.5/bdb.py(465)runcall()
-> res = func(*args, **kwds)
> /home/jorge/cursos/python/sources/tdebug/tdebugej02.py(5)debug_this()
-> result = i1
(Pdb) l
1 def foo():
2 a = "hola"
3 b = "bola"
4 -> bar()
5
[EOF]
(Pdb) s
--Call--
> <ipython-input-4-d5b067988c12>(1)bar()
-> def bar():
(Pdb) l
1 -> def bar():
2 x = 2
3 y = 3
4 z = x + y
5
[EOF]
• b [n]: Siendo n una linea del fichero actual. Si es una sesión de depuración en vivo, es decir,
desde dentro del intérprete de Python, no habiendo cargado un fichero, simplemente indica el
número de línea.
• b [funcion]: Siendo funcion una función concreta. Añade un punto de parada en la primera línea
evaluable de dicha función.
121
pdb.runcall(foo)
> <ipython-input-5-22ce54bbfd1f>(2)foo()
-> a = "hola"
(Pdb) l
1 def foo():
2 -> a = "hola"
3 b = "bola"
4 bar()
5
[EOF]
(Pdb) b bar
Breakpoint 2 at <ipython-input-4-d5b067988c12>:1
Para continuar hasta el siguiente punto de parada, o hasta el final del programa, si no hay ninguno
más.
pdb.runcall(foo)
> <ipython-input-5-22ce54bbfd1f>(2)foo()
-> a = "hola"
(Pdb) l
1 def foo():
2 -> a = "hola"
3 b = "bola"
4 bar()
5
[EOF]
(Pdb) b bar
Breakpoint 2 at <ipython-input-4-d5b067988c12>:1
(Pdb) c
> <ipython-input-4-d5b067988c12>(2)bar()
-> x = 2
(Pdb) l
1 B def bar():
2 -> x = 2
3 y = 3
4 z = x + y
5
(Pdb) c
[EOF]
122
(Pdb) result
11
Si has creado una variable con un nombre que entra en conflicto con alguno de los
comandos de pdb, puedes acceder a la misma poniéndole un símbolo de
exclamación delante: !c
Al igual que existe una versión mejorada del intérprete de Python, llamada
iPython, existe una versión mejorada del depurador, llamada 'ipdb'. Puedes
instalarla con 'pip install ipdb'
File "/home/jorge/cursos/nicar2016-python-testing-debugging-
exercises/tests/test_chicago_result_loade
r.py", line 27, in test_load
self.assertEqual(alvarez['contest_code'], '0079')
AssertionError: '0790' != '0079'
- 0790
? -
+ 0079
? +
----------------------------------------------------------------------
Ran 1 test in 0.005s
FAILED (failures=1)
Por lo que se deduce del error, unos datos de un diccionario parecen erróneos, o desplazados con
respecto al valor esperado.
123
Ejercicio: Investiga mediante una sesión de depuración cuáles son los valores
esperados para ese campo, y cuáles son los valores que en realidad se están
leyendo y porqué. Una vez identificado el error, arréglalo y vuelve a pasar los
tests.
Pista: En el método 'BrokenChicagoResultsLoader.load' explica el formato del
fichero Pista: Fíjate en el nombre que causa el error. Tal vez te interese centrarte
en ese nombre, generando una versión reducida del fichero para que te resulte
más sencillo
Recuerda que el error será diferente si usas Python 2 en lugar de Python 3. Puedes
cambiar a la rama 'solutions' para verlo.
124
Capítulo 10. Selenium Mozilla IDE
Selenium IDE es un plugin para nuestro navegador que básicamente lo que nos va a permitir es
grabar nuestras interacciones con las páginas web para generar unos tests automáticos y
reproducirlos. Mientras estamos interactuando con la página, se van a ir generando unos comandos
(Selenese) que representan las acciones que hemos realizado en el navegador y que se han estado
grabando. Estos comandos se pueden reproducir y nos mostrarán todas las acciones que hemos ido
haciendo mientras se grababan.
Una vez instalado, dentro de firefox, podemos abrir el plugin de las siguientes dos formas:
125
Selenium nos va a permitir grabar lo que nosotros hacemos en el navegador, y según vamos
interactuando con él, se van a ir generando unos comandos de selenium (Selenese), y una vez que
paremos la grabación, podremos reproducir todos los comandos que se han ido guardando.
126
Despues de pinchar sobre la opción nos pedirá:
• URL de la web sobre la que queremos realizar la grabación. El primer paso será que Selenium
abra la web.
Una vez pulsado, podemos empezar a interactuar con el navegador para que se vayan guardando
las acciones que vamos realizando. Y cuando ya hayamos reproducido nuestro caso de prueba
paramos la grabación volviendo a pulsar sobre el botón. La interfaz del plugin, nos mostrará algo
como en la siguiente imagen:
127
En la anterior imagen, podemos ver que todas las acciones que se han ido realizando se han
guardado en una tabla, con la información necesaria para poder reproducir nuestras acciones
automáticamente.
Una vez que le damos a uno de los dos botones, se puede ver en el navegador como se van
realizando todas las acciones que hemos grabado nosotros.
Al lado de los botones de reproducción, podemos encontrar una barra para modificar la velocidad a
la que se reproducen nuestros tests.
128
10.3. Debugger
Selenium IDE nos permite debuggear los tests que se han grabado para poder ver las acciones una a
una. Para ello pulsamos con el botón derecho sobre el comando en el que queremos poner un
breakpoint y le damos Toggle Breakpoint. Ahora al darle a reproducir el test, se parará en ese
comando.
129
Una vez que se ha parado en el punto de interrupción, podemos ejecutar el resto de comandos paso
a paso, o darle al botón de continuar con la reproducción.
• Aserciones: comprueban que algo es cierto, y en caso de no serlo se termina la ejecución de los
tests. Por ejemplo, si queremos testear que dentro de la documentación de React hay una
sección sobre como se crean los componentes, y el test carga la página de Angular, no tiene
sentido continuar reproduciendo el resto de comandos, porque para empezar ya no estamos en
la página correcta, entonces este sería un caso en el que se puede usar las aserciones.
130
• Verificaciones: comprueban que existe lo que se busca, pero en caso de no hacerlo, el resto de
tests se seguirán ejecutando aunque esa verificación no la pase. Por ejemplo, en la página de
React, buscamos que haya dos secciones, una de como pasar propiedades a los componentes, y
otra como inicializar el estado en un componente, y a una de ellas se la ha cambiado el nombre
y ya no coincide con lo que se está verificando, pero la otra sin embargo, si que pasará el test.
131
10.5. Bancos de pruebas
Los bancos de pruebas son colecciones de tests. Ahora mismo solo hemos creado uno, pero
supongamos que queremos probar las distintas funcionalidades de una aplicación, Selenium IDE
nos permite añadir más casos de prueba en los que cada uno debería de encargarse de testear una
funcionalidad distinta de la aplicación. Vamos a crear un nuevo caso de prueba como aparece en la
siguiente imagen:
132
Una vez que lo tenemos creado, grabamos la secuencia de acciones a testear, y una vez que lo
paramos, ya tenemos dos casos de prueba distintos en nuestro banco de tests.
10.6. Selenese
Selenese son los comandos de Selenium que se van guardando en la tabla y que sirven para
ejecutar los tests. Estos comandos no pertenecen a ningún lenguaje de programación. En esta tabla
se van guardando tres valores: el comando (obligatorio), el objetivo (opcional) y un valor (opcional).
133
A esta tabla de comandos le podemos añadir nuestros propios comandos de una forma muy
sencilla. Botón derecho y le damos a Añadir nuevo comando.
134
Todos los comandos de Selenese que hay disponibles se pueden ver
link::https://docs.katalon.com/display/KD/Selenese+%28Selenium+IDE%29+Commands+Reference[a
quí].
• waitForPageToLoad: para la ejecución hasta que la página esperada se carga. Es llamada por
defecto automáticamente cuando se ejecuta clickAndWait.
• verifyElementPresent: verifica que el UIElement esperado está definido como etiqueta HTML
en alguna parte de la página.
135
• assertConfirmation: verifica si sale una ventana de confirmación con el texto esperado.
10.7. Exportar
Todos los comandos que se van guardando en la tabla, se pueden exportar a un archivo con el
código en distintos lenguajes (Java, Ruby, Python, JavaScript…) que ya usa WebDriver. Para ello solo
tenemos que darle export test suite as… y elegir el lenguaje.
Para que se vea más claro a que comando pertenece cada instrucción que hay en el archivo que
acabamos de exportar, le podemos indicar que los añada como comentarios. Para ello marcamos la
opción de Include step description as a separate comment
136
Y si lo volvemos a exportar nos queda el código de la siguiente forma:
class TestPruebaspython2():
def setup_method(self, method):
self.driver = webdriver.Firefox()
self.vars = {}
def test_pruebaspython2(self):
self.driver.get("https://www.imdb.com/")
self.driver.set_window_size(1920, 899)
self.driver.find_element(By.CSS_SELECTOR, ".ipc-button--core-baseAlt #iconContext-arrow-drop-down").click()
self.driver.find_element(By.CSS_SELECTOR, "#language-option-es-ES > .language-menu-item-span").click()
self.driver.find_element(By.ID, "suggestion-search").click()
self.driver.find_element(By.CSS_SELECTOR, ".imdb-header__signin-text > .ipc-button__text").click()
self.driver.find_element(By.CSS_SELECTOR, ".list-group > .list-group-item:nth-child(1) > .auth-provider-text").click
()
self.driver.find_element(By.ID, "ap_email").click()
self.driver.find_element(By.ID, "ap_email").send_keys("[email protected]")
self.driver.find_element(By.ID, "ap_password").send_keys("curso")
assert self.driver.find_element(By.ID, "suggestion-search").text == "Super Mario"
self.driver.find_element(By.ID, "signInSubmit").click()
137
ide/[https://addons.mozilla.org/es/firefox/addon/test-results-selenium-ide/]
Una vez instalado, ejecutamos los tests, y despues de que hayan terminado, exportamos los
resultados desde la pestaña de Archivo.
Si abrimos el archivo que hemos exportado nos mostrará una tabla por cada caso de prueba con
sus comandos. En verde saldrá aquellos tests que se han pasado correctamente, y en rojo los que
han fallado.
138
139
10.9. Lab: Mozilla IDE
10.9.1. Ejercicio 1
10.9.2. Ejercicio 2
10.9.3. Ejercicio 3
• Usando Selenium IDE, genera 3 tests para testear las busquedas avanzadas de la Wikipedia:
140
Capítulo 11. Selenium WebDriver con
Python
WebDriver es el motor de pruebas automatizadas de Selenium. Nos proporciona una API que nos
permite realizar las pruebas y automatizar los navegadores web. Necesitamos un entorno de
desarrollo y algunas dependencias externas para poder crear los tests con Selenium WebDriver.
Una vez instalado Python, necesitaremos instalar la librería de Selenium WebDriver para Python
que viene en el paquete de Selenium. Para ello solo necesitamos usar el gestor de paquetes de
Python, pip. Para instalar el paquete lanzamos el siguiente comando en el proyecto en el que vamos
a usar Selenium.
Con este comando podremos hacer uso de las clases necesarias para generar los tests. Una vez
instalado ya podemos empezar a escribir nuestros tests.
El objeto WebDriver nos permite usar instrucciones para automatizar el navegador Web. A través
de este objeto, podemos acceder a todo el contenido Web. Y soporta varios lenguajes (Java, Ruby,
Python…).
Selenium tiene implementado diferentes WebDrivers para dar soporte a distintos navegadores.
141
Propiedad Descripción Ejemplo
$ tar xf geckodriver-v0.32.0-linux64.tar.gz
$ sudo mv geckodriver /usr/local/bin/
/selenium/ejemplos/navegadores/ejemplo_firefox.py
driver = webdriver.Firefox()
driver.get('http://www.google.es')
Como en el caso de Firefox, debemos descargar su Driver, siempre en la versión mas estable desde
142
este enlace
$ unzip chromedriver_linux64.zip
$ sudo mv chromedriver /usr/local/bin/
/selenium/ejemplos/navegadores/ejemplo_chrome.py
driver = webdriver.Chrome()
driver.get('http://www.google.es')
$ sudo yum update google-chrome
/selenium/ejemplos/navegadores/ejemplo_explorer.py
driver = webdriver.Ie()
driver.get('http://www.google.es')
143
import unittest
from selenium import WebDriver
class Tests(unittest.TestCase):
def setUp(self):
# Inicializamos lo necesario para ejecutar los tests
def test1(self):
# Test 1
def tearDown(self):
x# Limpiamos
11.7. Herramientas
Podemos usar algunas herramientas para inspeccionar los elementos y poder elegir uno de los
métodos de busqueda que se van a ver a continuación para obtener los elementos necesarios para
automatizar las pruebas.
Firebug tiene opciones que nos permiten obtener la ruta CSS del elemento, o incluso el XPath.
144
En Google Chrome podemos usar las Chrome Dev Tools que funcionan de forma parecida a
Firebug.
145
Método Descripción Ejemplo
11.9. Aserciones
Las aserciones son unos métodos que nos proporciona la librería unittest y nos van a permitir
validar alguna condición para decidir si el test se pasa o no se pasa. En caso de que el test no se
pase, se mostrará un error en la consola y se parará la ejecución.
Método Condición
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertIsNot(a, b) a is not b
assertGreater(a, b) a>b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a<b
assertLessEqual(a, b) a⇐b
assertListEqual(a, b) a == b
146
Puede que a veces sea complicado obtener algún elemento, pero siempre hay algún método para
conseguirlo.
Para acceder a los elementos de la página web lo vamos a hacer a través de los objetos de
WebDriver y WebElement. Vamos a usar los métodos:
• find_element(By.xxx)
• find_elements(By.xxx)
Estos métodos devuelven una instancia de WebElement en caso de que hayan encontrado el
elemento buscado. En caso de no hacerlo, el primero lanza una excepción NoSuchElementFound,
mientras que el segundo devuelve una lista vacía.
11.11.1. Id
Busca elementos por el id. Este método es el más útil porque el id no se debería repetir entre los
elementos de una página, por lo que siempre vamos a obtener el que necesitamos.
147
/var/www/html/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="lista-productos">
<ul>
<li><input type="text" value="Producto 1" autofocus enabled /></li>
<span>ooo</span>
<li><input type="text" value="Producto 2" disabled /></li>
<li><input type="text" value="Producto 3" enabled /></li>
<li><input type="text" value="Producto 4" disabled /></li>
</ul>
</div>
<img src="" alt="Imagen">
<img src="">
<form id="login-form">
<div>
<label for="username">Username</label>
<input type="text" id="username" class="login-u" name="user">
</div>
<div>
<label for="password">Password</label>
<input type="password" id="password" class="login-p" name="pass">
</div>
<input type="submit" name="login" id="btn-login" class="btn" value="Enviar">
</form>
</body>
</html>
Ahora vamos a usar los métodos antes mencionados para obtener los elementos de la página.
148
/selenium/ejemplos/buscar_elementos/find_by_id.py
class FindElementId(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testFindId(self):
username = self.driver.find_element(By.ID, 'username')
password = self.driver.find_element(By.ID,'password')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.11.2. Name
Ahora vamos a usar los métodos antes mencionados para obtener los elementos de la página.
/selenium/ejemplos/buscar_elementos/find_by_name.py
class FindElementName(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testFindName(self):
username = self.driver.find_element(By.NAME,'user')
password = self.driver.find_element(By.NAME,'pass')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
149
11.11.3. ClassName
Busca elementos por la clase. En caso de que haya varios elementos con la misma clase, tendremos
que especificar cual queremos.
Ahora vamos a usar los métodos antes mencionados para obtener los elementos de la página.
/selenium/ejemplos/buscar_elementos/find_by_class.py
class FindElementClass(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testFindClass(self):
username = self.driver.find_element(By.CLASS_NAME,'login-u')
password = self.driver.find_element(By.CLASS_NAME,'login-p')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.11.4. TagName
Este busca el elemento por el nombre de la etiqueta HTML. En este caso seguro que tenemos que
especificar a que etiqueta nos referimos porque estará repetida por toda la página.
Ahora vamos a usar los métodos antes mencionados para obtener los elementos de la página. En
este caso como podemos recibir una lista de elementos, la podemos recorrer como se muestra en el
siguiente código:
150
/selenium/ejemplos/buscar_elementos/find_by_tag_name.py
class TagName(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testTagName(self):
labels = self.driver.find_elements(By.TAG_NAME,'label')
for label in labels:
self.assertIsNotNone(label)
print(label.text)
self.assertEqual(2, len(labels))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
WebElement también soporta el método find_element(By.xxx) por lo que podemos buscar entre los
hijos de un elemento web, filtrando los resultados.
151
/selenium/ejemplos/buscar_elementos/find_by_tag_name.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TagName(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testTagName(self):
labels = self.driver.find_elements(By.TAG_NAME,'label')
for label in labels:
self.assertIsNotNone(label)
print(label.text)
self.assertEqual(2, len(labels))
form = self.driver.find_element(By.TAG_NAME,'form')
username_label = form.find_element(By.TAG_NAME,'label')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.11.5. LinkText
152
/selenium/ejemplos/buscar_elementos/find_by_link_text.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class LinkText(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://www.google.com')
def testLinkText(self):
gmail_link = self.driver.find_element(By.LINK_TEXT,'Gmail')
self.assertEqual('https://mail.google.com/mail/&ogbl', gmail_link
.get_attribute('href'))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.11.6. PartialLinkText
Busca los Links que contengan parte del texto dado. Se suele usar para aquellos enlaces que no son
fijos, sino que cambian el texto que muestran (por ejemplo un enlace que te muestre cuantas tareas
te quedan por hacer, 13 tareas).
153
/selenium/ejemplos/buscar_elementos/find_by_partial_link_text.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class PartialLinkText(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://www.google.com')
def testPartialLinkText(self):
gmail_link = self.driver.find_element(By.PARTIAL_LINK_TEXT,'Gma')
self.assertEqual('https://mail.google.com/mail/&ogbl', gmail_link
.get_attribute('href'))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.11.7. XPath
Este es otro método avanzado que será el que vamos a usar cuando no nos sirve ninguno de los
anteriores. XPath permite construir expresiones que recorren y procesan un documento XML. La
mayor diferencia entre usar XPath y los usar los selectores CSS, es que XPath nos permite navegar
por el XML en todas las direcciones: de hijos a padres y de padres a hijos.
Terminología:
• Padres: cada elementos nodo tiene un nodo padre del que cuelga.
XPath tiene las siguientes expresiones que nos permiten acceder a los elementos buscados:
• nodename: selecciona todos los nodos con el que coinciden con el nombre dado.
154
indica la relación entre padres e hijos (html/body/table).
• //: indica la ruta relativa al nodo. //table (todos los elementos table), //a//img (todos los elementos
img dentro de un elemento a).
• @: se usa para indicar que tiene que tener un atributo, //img/@alt (todos los elementos img que
tengan el atributo alt).
Con la ruta absoluta indicamos la ruta completa hasta acceder al nodo que queremos, pero esto
tiene una desventaja, y es que si el nodo cambia de lugar, no lo va a encontrar.
/selenium/ejemplos/buscar_elementos/find_by_xpath_abs.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class XPath(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testXPathAbsolute(self):
submit_button = self.driver.find_element(By.XPATH,'/html/body/form/input')
self.assertEqual('Enviar', submit_button.get_attribute('value'))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
155
/selenium/ejemplos/buscar_elementos/find_by_xpath_rel.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class XPath(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('https://es.wikipedia.org')
def testXPathRelative(self):
logo = self.driver.find_element(By.XPATH,"//*[@id='p-logo']")
self.assertIsNotNone(logo)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
/selenium/ejemplos/buscar_elementos/find_by_xpath_attrs.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class XPath(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testXPathWithAttr(self):
password = self.driver.find_element(By.XPATH,
"/html/body/form/div[2]/input[@class='login-p']")
self.assertIsNotNone(password)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Con XPath podemos usar funciones para refinar las busquedas. Algunas de estas funciones son las
156
siguientes:
157
/selenium/ejemplos/buscar_elementos/find_by_xpath_functions.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class XPath(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testXPathWithFunctions(self):
btn1 = self.driver.find_element(By.XPATH,"//input[@type='submit' and
@value='Enviar']")
btn2 = self.driver.find_element(By.XPATH,"//input[@type='submit' or
@value='Enviar']")
images_with_alt = self.driver.find_elements(By.XPATH,'//img[not(@alt)]')
self.assertEqual('Enviar', btn1.get_attribute('value'))
self.assertEqual('Enviar', btn2.get_attribute('value'))
self.assertEqual(1, len(images_with_alt))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
158
/selenium/ejemplos/buscar_elementos/find_by_xpath_any_attr.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class XPath(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testWithAnyAttr(self):
username_input = self.driver.find_element(By.XPATH,"//input[@*='user']")
self.assertIsNotNone(username_input)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Expresión Descripción
CSS es un lenguaje de estilos que sirve para modificar la apariencia de los elementos HTML o XML.
Este es un método avanzado porque podemos buscar un elemento por los selectores que se han
usado para darle los estilos.
• Ruta absoluta:
159
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_abs.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsAbsolute(self):
username_label_1 = self.driver.find_element(By.CSS_SELECTOR,"html body form
div label")
username_label_2 = self.driver.find_element(By.CSS_SELECTOR,"html > body >
form > div > label")
self.assertEqual('Username', username_label_1.text)
self.assertEqual('Username', username_label_2.text)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_rel.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsRelative(self):
username_label = self.driver.find_element(By.CSS_SELECTOR,"label")
self.assertEqual('Username', username_label.text)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
160
• Usando la clase y el id:
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_id_and_class.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsIdAndClass(self):
username_input = self.driver.find_element(By.CSS_SELECTOR,"input.login-u")
password_input_1 = self.driver.find_element(By.CSS_SELECTOR,"input#password")
password_input_2 = self.driver.find_element(By.CSS_SELECTOR,"#password")
self.assertIsNotNone(username_input)
self.assertIsNotNone(password_input_1)
self.assertIsNotNone(password_input_2)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
• Otros atributos:
161
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_attrs.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsAttrs(self):
username_input = self.driver.find_element(By.CSS_SELECTOR,"input[name='user']
")
img = self.driver.find_element(By.CSS_SELECTOR,"img[alt='Imagen']")
self.assertIsNotNone(username_input)
self.assertIsNotNone(img)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
162
Pseudo-Clases Ejemplo Descripción
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_pseudo_classes.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsPseudoClasses(self):
username_label = self.driver.find_element(By.CSS_SELECTOR,"form#login-form >
div :first-child")
password_label = self.driver.find_element(By.CSS_SELECTOR,"form#login-form >
div:nth-child(2) :first-child")
self.assertIsNotNone(username_label)
self.assertIsNotNone(password_label)
self.assertEqual('Username', username_label.text)
self.assertEqual('Password', password_label.text)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
163
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_siblings.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsSiblings(self):
list_item_2 = self.driver.find_element(By.CSS_SELECTOR,"div#lista-productos >
ul > li + li > input")
self.assertIsNotNone(list_item_2)
self.assertEqual('Producto 2', list_item_2.get_attribute('value'))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
164
/selenium/ejemplos/buscar_elementos/find_by_css_selectors_pseudo_classes.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CssSelectors(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCssSelectorsPseudoClasses(self):
input_focus = self.driver.find_element(By.CSS_SELECTOR,"input:focus")
self.assertIsNotNone(input_focus)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.11.9. jQuery
También es posible usar la API de jQuery con Selenium para buscar los elementos. Para ello, solo
hay que crear una instancia de JavaScript del WebDriver y con ella usar el método
execute_script(código) para inyectar ese código JS.
165
/selenium/ejemplos/buscar_elementos/find_by_jquery.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class JQuery(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&pro
file=advanced')
def testJQuery(self):
checked = self.driver.execute_script("return jQuery.find(':checked')")
not_checked = self.driver.execute_script("return
jQuery.find('input:checkbox:not(:checked)')")
self.assertEqual(3, len(checked))
self.assertEqual(27, len(not_checked))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
En caso de testear aplicaciones sobre las que no tenemos ningún control, tendremos que apañarnos
con estos métodos para obtener los elementos que necesitamos, mientras que si tenemos acceso al
código de la aplicación que vamos a testear y podemos modificarla, podemos aprovechar a añadir
ids a todos aquellos elementos a los que se les pueda poner para poder automatizar los tests de una
forma más sencilla y rápida.
166
11.12. Lab: Python Webdriver
11.12.1. Ejercicio 1
• Navegar a 'http://es.wikipedia.org
• Obtener los enlaces que hay dentro del elemento con id 'p-navigation'
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio1(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org
self.driver.get('http://es.wikipedia.org')
def testCorrectLinks(self):
# Obtener los enlaces que hay dentro del elemento con id 'p-navigation'
portada = self.driver.find_element(By.ID,'n-mainpage-description')
portal = self.driver.find_element(By.ID,'n-portal')
ayuda = self.driver.find_element(By.ID,'n-help')
donaciones = self.driver.find_element(By.ID,'n-sitesupport')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.12.2. Ejercicio 2
• Navegar a 'http://es.wikipedia.org
• Obtener los enlaces que hay dentro del elemento con id 'p-navigation'
167
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio2(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org
self.driver.get('http://es.wikipedia.org')
def testCorrectNumberOfLinks(self):
# Obtener los enlaces que hay dentro del elemento con id 'p-navigation'
navigation_div = self.driver.find_element(By.ID,'p-navigation')
body = navigation_div.find_element(By.CLASS_NAME,'body')
links = body.find_elements(By.TAG_NAME,'a')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.12.3. Ejercicio 3
• Navegar a 'http://es.wikipedia.org
• Encuentra todos los links que hay en el div con id 'p-personal' y comprueba que existen
• Encuentra algún link que contenga texto dinámico y comprueba que existe
168
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio3(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org
self.driver.get('http://es.wikipedia.org')
def testCorrectLinks(self):
# Encuentra todos los links que hay en el div con id 'p-personal' y comprueba que existen
discussion = self.driver.find_element(By.LINK_TEXT,'Discusión')
contributions = self.driver.find_element(By.LINK_TEXT,'Contribuciones')
account = self.driver.find_element(By.LINK_TEXT,'Crear una cuenta')
access = self.driver.find_element(By.LINK_TEXT,'Acceder')
self.assertIsNotNone(discussion)
self.assertIsNotNone(contributions)
self.assertIsNotNone(account)
self.assertIsNotNone(access)
# Encuentra algún link que contenga texto dinámico y comprueba que existe
month = self.driver.find_element(By.PARTIAL_LINK_TEXT,'enero')
self.assertIsNotNone(month)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.12.4. Ejercicio 4
• Navegar a 'http://es.wikipedia.org
169
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio4(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org
self.driver.get('http://es.wikipedia.org')
def testFindTitlesWithXPath(self):
# Encuentra los títulos de todos los 'mw-panel' usando un XPATH absoluto
print = self.driver.find_element(By.XPATH,'/html/body/div[4]/div[2]/div[3]/h3')
other_projects = self.driver.find_element(By.XPATH,'/html/body/div[4]/div[2]/div[4]/h3')
tools = self.driver.find_element(By.XPATH,'/html/body/div[4]/div[2]/div[5]/h3')
languages = self.driver.find_element(By.XPATH,'/html/body/div[4]/div[2]/div[6]/h3')
self.assertIsNotNone(print)
self.assertIsNotNone(other_projects)
self.assertIsNotNone(tools)
self.assertIsNotNone(languages)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.12.5. Ejercicio 5
• Navegar a 'http://www.w3schools.com/html/html_tables.asp'
170
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio5(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://www.w3schools.com/html/html_tables.asp'
self.driver.get('http://www.w3schools.com/html/html_tables.asp')
def testTable(self):
# Encontrar la primera tabla de la página
table = self.driver.find_element(By.XPATH,"//*[@id='customers']")
# Comprueba que la tabla existe
self.assertIsNotNone(table)
# Comprueba que tiene el número de filas correcto
rows = table.find_elements(By.TAG_NAME,'tr')
self.assertEqual(7, len(rows))
# Comprueba que la última fila tiene el número de celdas correcto
lastRowCells = self.driver.find_elements(By.XPATH,"//*[@id='customers']/tbody/tr[last()]/td")
self.assertEqual(3, len(lastRowCells))
# Comprueba que después de la quinta fila, hay dos filas más
last2Rows = self.driver.find_elements(By.XPATH,"//*[@id='customers']/tbody/tr[position() > 5]")
self.assertEqual(2, len(last2Rows))
# Comprueba que todas las celdas tienen contenido
cells = table.find_elements(By.TAG_NAME,'td')
for cell in cells:
self.assertFalse(len(cell.text) == 0)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
11.12.6. Ejercicio 6
• Navegar a
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=ad
vanced'
171
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio6(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=advanced'
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=advanced')
def testCheckboxes(self):
# Encuentra los checkboxes de la página
checkboxes = self.driver.find_elements(By.CSS_SELECTOR,"#mw-searchoptions input[name^='ns']")
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
172
Capítulo 12. Interacciones
Ahora vamos a ver como podemos interactuar con los diferentes elementos de la Web como pueden
ser las cajas de texto, los checkboxes, los desplegables…
/selenium/ejemplos/interactions/text_boxes.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TextBoxesInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://www.google.com')
def testGoogleSearch(self):
input = self.driver.find_element(By.NAME,"q")
input.clear()
input.send_keys('Angular')
input.clear()
input.send_keys('React')
input.submit()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
173
/selenium/ejemplos/interactions/text_boxes.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TextBoxesInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://www.google.com')
def testGoogleSearch(self):
input = self.driver.find_element(By.NAME,"q")
input.clear()
input.send_keys('Angular')
input.clear()
input.send_keys('React')
button = self.driver.find_element(By.XPATH,
"/html/body/div[1]/div[3]/form/div[1]/div[1]/div[4]/center/input[1]")
button.click()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div>
<input type="text" id="message" value="Hola a todos!" align="justify" />
</div>
</body>
</html>
174
/selenium/ejemplos/interactions/interactions_get_text.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TextBoxesInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testGetTextFromInput(self):
input = self.driver.find_element(By.ID,'message')
message = input.get_attribute("value")
print(message)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Podemos recoger cualquier atributo de los elementos a través del método get_attribute(). Y si
queremos obtener el valor de una propiedad CSS del elemento usamos value_of_css_property().
/styles.css
input {
color: rgba(0, 120, 120, 0.5);
width: 150px;
}
175
/selenium/ejemplos/interactions/interactions_attr_and_props_css.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class AttributtesAndPropertiesCss(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testGetAttrAndPropCss(self):
input = self.driver.find_element(By.ID,'message')
self.assertEqual('rgba(0, 120, 120, 0.5)', input.value_of_css_property('color
'))
self.assertEqual('justify', input.get_attribute('align'))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
12.2. Desplegables
Selenium WebDriver nos proporciona una manera para poder trabajar con las listas desplegables
creadas como elementos HTML <select>. Para ello tiene la clase Select con la que podemos
interactuar directamente con estos elementos.
Función Descripción
176
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="car-list">
<select name="cars" id="car-options">
<option value="bmw">BMW</option>
<option value="tesla">Tesla</option>
<option value="audi">Audi</option>
<option value="mercedes">Mercedes</option>
</select>
</div>
</body>
</html>
177
/selenium/ejemplos/interactions/interactions_dropdowns.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
class DropdownInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testDropdowns(self):
cars = Select(self.driver.find_element(By.NAME,"cars"))
self.assertFalse(cars.is_multiple)
self.assertEqual(4, len(cars.options))
cars.select_by_visible_text('Tesla')
self.assertEqual('Tesla', cars.first_selected_option.text)
cars.select_by_value('audi')
self.assertEqual('Audi', cars.first_selected_option.text)
cars.select_by_index(0)
self.assertEqual('BMW', cars.first_selected_option.text)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Función Descripción
178
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="color-list">
<select name="colors" id="color-options" multiple>
<option value="red">Red</option>
<option value="yellow">Yellow</option>
<option value="blue">Blue</option>
<option value="black">Black</option>
</select>
</div>
</body>
</html>
179
/selenium/ejemplos/interactions/interactions_dropdowns_multiple.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
class DropdownInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testDropdownsMultipleOptions(self):
colors = Select(self.driver.find_element(By.NAME,"colors"))
self.assertTrue(colors.is_multiple)
self.assertEqual(4, len(colors.options))
colors.select_by_visible_text('Yellow')
colors.select_by_value('red')
colors.select_by_index(3)
self.assertEqual(3, len(colors.all_selected_options))
colors.deselect_by_index(0)
self.assertEqual(2, len(colors.all_selected_options))
colors.deselect_by_value('yellow')
self.assertEqual(1, len(colors.all_selected_options))
colors.deselect_by_visible_text('Black')
self.assertEqual(0, len(colors.all_selected_options))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Cuando seleccionamos/deseleccionamos por índice tenemos que tener cuidado porque puede que
los elementos sean dinámicos y obtengamos fallos inesperados al ejecutar los tests.
También podemos testear si todas las opciones del desplegable son las que esperamos.
180
/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="styles.css">
<title>Document</title>
</head>
<body>
<div id="month-list">
<select name="months" id="month-options" multiple>
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
</select>
</div>
</body>
</html>
181
/src/test/java/com/selenium/examples/Interactions.java
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
class DropdownListInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testDropdownsOptions(self):
expected_months = ["January", "February", "March", "April", "May", "June"]
select_months = Select(self.driver.find_element(By.CSS_SELECTOR,"#month-
options"))
months = [month.text for month in select_months.options]
self.assertListEqual(expected_months, months)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
182
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="working-radios">
<input type="radio" id="opt1" name="working" value="Yes">
<label for="opt1">Yes</label>
<input type="radio" id="opt2" name="working" value="No" checked>
<label for="opt2">No</label>
</div>
</body>
</html>
/selenium/ejemplos/interactions/interactions_radio_buttons.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class RadioButtonInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testRadioButtons(self):
working = self.driver.find_element(By.XPATH,"//input[@type='radio' and
@value='Yes']")
if not working.is_selected():
working.click()
self.assertTrue(working.is_selected())
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
183
12.4. Checkbox
De la misma manera que utilizamos los radio buttons podemos usar los elementos de checkbox de
una página.
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="hobbie-list">
<input type="checkbox" id="cine" name="hobbies">
<label for="cine">Cine</label>
<input type="checkbox" id="deportes" name="hobbies">
<label for="deportes">Deportes</label>
<input type="checkbox" id="musica" name="hobbies">
<label for="musica">Musica</label>
</div>
</body>
</html>
184
/selenium/ejemplos/interactions/interactions_checkboxes.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class CheckboxesInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testCheckboxes(self):
cine = self.driver.find_element(By.XPATH,"//input[@id='cine']")
if not cine.is_selected():
cine.click()
self.assertTrue(cine.is_selected())
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
12.5. Alerts
WebDriver nos da acceso a los elementos Alerts que se generan mediante código JavaScript para
mostrar mensajes de error o warnings.
Función Descripción
Y podemos usar los siguientes métodos para interactuar con estos pop-ups.
Función Descripción
185
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button type="button" id="show-alert">Pulsa para mostrar alert</button>
</body>
<script>
window.onload = () => {
var btn = document.getElementById('show-alert');
btn.addEventListener('click', () => {
alert('Has pulsado el botón...');
});
}
</script>
</html>
186
/selenium/ejemplos/interactions/interactions_alerts.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class AlertsInteractions(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testAlerts(self):
btn = self.driver.find_element(By.ID,'show-alert')
btn.click()
alert = self.driver.switch_to.alert
self.assertEqual('Has pulsado el botón...', alert.text)
alert.accept()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
187
12.6. Lab: Interacciones
12.6.1. Ejercicio 7
• Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio7(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
self.driver.get('http://es.wikipedia.org/wiki/Wikipedia:Portada')
def testCheckboxes(self):
# Encuentra el elemento input
search_input = self.driver.find_element(By.CSS_SELECTOR,"#searchInput")
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
12.6.2. Ejercicio 8
• Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
188
• Comprobar que el tamaño de la letra del input es la correcta
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio8(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
self.driver.get('http://es.wikipedia.org/wiki/Wikipedia:Portada')
def testCheckboxes(self):
# Encuentra el elemento input
search_input = self.driver.find_element(By.CSS_SELECTOR,"#searchInput")
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
12.6.3. Ejercicio 9
• Navegar a 'http://es.wikipedia.org/wiki/Especial:Contribuciones'
• Encuentra el desplegable
189
• Comprobar que el número de opciones es el correcto
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
class Ejercicio9(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/wiki/Especial:Contribuciones'
self.driver.get('http://es.wikipedia.org/wiki/Especial:Contribuciones')
def testCheckboxes(self):
# Encuentra el desplegable
select = Select(self.driver.find_element(By.CSS_SELECTOR,"#namespace"))
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
12.6.4. Ejercicio 10
• Navegar a 'http://es.wikipedia.org/wiki/Especial:Contribuciones'
190
• Comprobar que uno está seleccionado y el otro no
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio10(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/wiki/Especial:Contribuciones'
self.driver.get('http://es.wikipedia.org/wiki/Especial:Contribuciones')
def testRadioButtons(self):
# Obtener los radio buttons
newbie = self.driver.find_element(By.CSS_SELECTOR,"#newbie")
user = self.driver.find_element(By.CSS_SELECTOR,"#user")
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
12.6.5. Ejercicio 11
• Navegar a
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=ad
vanced'
191
• Comprobar que todos los checkboxes están seleccionados
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio11(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=advanced'
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=advanced')
def testCheckboxes(self):
# Obtener el primer checkbox seleccionado
check1 = self.driver.find_element(By.CSS_SELECTOR,"#mw-search-ns0")
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
192
Capítulo 13. Selenium API
Es una buena práctica detectar la presencia de un elemento dentro de una web antes de realizar
alguna acción sobre él. Para ellos vamos a escribir un método muy simple que capture la excepción
NoSuchElementException si no encuentra el elemento.
Función Descripción
193
/selenium/ejemplos/selenium_api/double_click.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
class DoubleClick(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://cookbook.seleniumacademy.com/DoubleClickDemo.html')
def testDoubleClick(self):
message = self.driver.find_element(By.ID,'message')
ActionChains(self.driver).double_click(message).perform()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
194
/selenium/ejemplos/selenium_api/drag_and_drop.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
class DragAndDrop(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://cookbook.seleniumacademy.com/DragDropDemo.html')
def testDragAndDrop(self):
source = self.driver.find_element(By.ID,'draggable')
target = self.driver.find_element(By.ID,'droppable')
ActionChains(self.driver).drag_and_drop(source, target).perform()
self.assertEqual("Dropped!", target.text)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
13.4. JavaScript
Podemos ejecutar código JavaScript igual que lo haciamos cuando buscábamos elementos con
jQuery, usando el método execute_script().
195
/selenium/ejemplos/selenium_api/javascript.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
class JavaScript(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://www.google.com')
def testJavaScript(self):
title = self.driver.execute_script("return document.title")
self.assertEqual('Google', title)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
13.5. ScreenShots
También es posible sacar capturas de pantalla de la página web usando save_screenshot(). Esto nos
permite ver en que estado se encontraba la página cuando ha fallado un test.
196
/selenium/ejemplos/selenium_api/screenshots.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
class Screenshots(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://www.google.com')
def testScreenshots(self):
self.driver.save_screenshot('screenshot.png')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Función Descripción
197
Función Descripción
/selenium/ejemplos/selenium_api/event_firing_webdriver.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.events import EventFiringWebDriver
from selenium.webdriver.support.events import AbstractEventListener
class TraceListener(AbstractEventListener):
def on_exception(self, exception, driver):
driver.save_screenshot('exception.png')
print("Screenshot saved")
198
print('Before find: ' + value)
class TestEventFiringWebDriver(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver = EventFiringWebDriver(self.driver, TraceListener())
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&pro
file=advanced')
def testTestEventFiringWebDriver(self):
checkboxes = self.driver.find_elements(By.CSS_SELECTOR,"#mw-searchoptions
input[name^='ns']")
for check in checkboxes:
if not check.is_selected():
check.click()
search_box = self.driver.find_element(By.CSS_SELECTOR,'#searchText.oo-ui-
inputWidget-input')
search_box.clear()
search_box.send_keys('Selenium')
search_box.submit()
self.driver.find_element(By.CSS_SELECTOR,"#no-existe")
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
199
13.8. Lab: Selenium API
13.8.1. Ejercicio 12
• Navegar a 'http://es.wikipedia.org/w/index.php?title=Especial:Crear_una_cuenta'
• Comprobar que un elemento no está presente y mostramos una excepción con error
personalizado
200
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
class Ejercicio12(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/w/index.php?title=Especial:Crear_una_cuenta'
self.driver.get('http://es.wikipedia.org/w/index.php?title=Especial:Crear_una_cuenta')
def testCheckboxes(self):
# Comprobamos que un elemento no está presente y mostramos un mensaje
if not self.is_element_present(By.ID, 'este-no-esta'):
print('No estaba')
# Comprobar que un elemento no está presente y mostramos una excepción con error personalizado
if not self.is_element_present(By.NAME, 'tampoco'):
self.fail('Tampoco estaba')
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
13.8.2. Ejercicio 13
• Navegar a
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=ad
vanced'
201
import unittest, datetime, time
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio11(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=advanced'
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Buscar&search=&fulltext=Buscar&profile=advanced')
def testCheckboxes(self):
# Obtener el checkbox principal y comprobar que está seleccionado
check1 = self.driver.find_element(By.CSS_SELECTOR,"#mw-search-ns0")
self.assertTrue(check1.is_selected())
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
13.8.3. Ejercicio 14
• Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
202
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class Ejercicio14(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
self.driver.get('http://es.wikipedia.org/wiki/Wikipedia:Portada')
def testCheckboxes(self):
# Comprobar que hay una cookie con el nombre 'GeoIP'
geo_ip_cookie = self.driver.get_cookie("GeoIp")
self.assertIsNotNone(geo_ip_cookie)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
203
Capítulo 14. Sincronización
La sincronización nos va a permitir indicarle al navegador que le de tiempo a los elementos a que
aparezcan en la página.
/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-
scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
</style>
<title>Document</title>
</head>
<body>
<div id="container">
</div>
</body>
<script>
setTimeout(() => {
var boton = document.createElement('button');
boton.setAttribute('id', 'btn');
boton.setAttribute('type', 'button');
boton.appendChild(document.createTextNode('Pulsa aquí'));
document.getElementById('container').appendChild(boton);
}, 2000)
</script>
</html>
204
/selenium/ejemplos/syncronization/implicit_wait.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
class ImplicitWait(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testImplicitWait(self):
self.driver.implicitly_wait(6)
button = self.driver.find_element(By.CSS_SELECTOR,'#btn')
self.assertIsNotNone(button)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
Aunque no pongamos este wait, el sistema por defecto espera 10 segundos si no encuentra el
elemento dentro de un find_element_by_xxx().
Función Descripción
205
/selenium/ejemplos/syncronization/explicit_wait.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
class ExplicitWait(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testExplicitWait(self):
wait = WebDriverWait(self.driver, 6)
wait.until(expected_conditions.presence_of_element_located((By.ID, 'btn')))
button = self.driver.find_element(By.CSS_SELECTOR,'#btn')
self.assertIsNotNone(button)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
14.3. FluentWait
Nos permite indicar al sistema una cantidad de tiempo a esperar y la frecuencia con la que va a
buscar un elemento en el DOM de la página web. También podemos indicarle que ignore cierto tipo
de excepciones como NoSuchElementException.
206
/selenium/ejemplos/syncronization/fluent_wait.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
class FluentWait(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://localhost')
def testFluentWait(self):
wait = WebDriverWait(self.driver, 6, poll_frequency=0.2, ignored_exceptions=
[NoSuchElementException])
wait.until(lambda s: s.find_element(By.ID,'btn').is_displayed())
button = self.driver.find_element(By.CSS_SELECTOR,'#btn')
self.assertIsNotNone(button)
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
207
Capítulo 15. DataDriven
Gracias al testing basado en los datos, podemos usar el mismo código de test para testear diferentes
condiciones (datos). Podemos testear, por ejemplo toda una tabla, sin tener que hacer un test por
cada registro. Selenium por si solo no proporciona ningún mecanismo para realizar este tipo de
test, pero vamos a ver diferentes formas de hacerlo.
Para poder parametrizar los tests, vamos a necesitar usar la librería ddt. Con ella vamos a poder
definir un conjunto de datos y pasarselos a los tests, que se ejecutarán repetidas veces con los
distintos datos.
Para usar esta librería, vamos a usar unos decoradores en la clase y en los métodos de tests.
• @ddt: permite ejecutar un test múltiples veces, pero con distintos datos.
• @data(args): recibe como argumentos los datos que se les va a pasar a los tests como
parámetros.
/selenium/ejemplos/data_driven/data_driven.py
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from ddt import ddt, data, unpack
@ddt
class DataDriven(unittest.TestCase):
15.1. CSV
Estos datos que hemos hardcodeado en el script de python, los podemos leer desde un archivo CSV.
Para ello solo tendremos que implementar un método que se encargue de leer estos datos y
devolver una lista con ellos. Este método se le pasará como parámetro al decorador @data.
208
• open(filename, mode): para abrir el archivo file en modo mode (lectura, escritura…). Este
método devuelve un objeto file.
• reader(file): este método devuelve un objeto reader que va a ir iterando sobre las líneas del
archivo csv.
/selenium/ejemplos/data_driven/data_driven_csv.py
import unittest
import csv
from selenium import webdriver
from selenium.webdriver.common.by import By
from ddt import ddt, data, unpack
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
filename = "/home/python/workspace/registerData.csv"
def get_data(filename):
rows = []
data_file = open(filename, 'r')
reader = csv.reader(data_file)
next(reader, None)
for row in reader:
rows.append(row)
print(rows)
return rows
@ddt
class DataDrivenCsv(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Crear_una_cuenta')
@data(*get_data('registerData.csv'))
@unpack
def testDataDrivenCsv(self, username_value, password_value,
confirm_password_value, email_value, captcha_value):
username = self.driver.find_element(By.CSS_SELECTOR,'#wpName2')
username.clear()
username.send_keys(username_value)
password = self.driver.find_element(By.CSS_SELECTOR,'#wpPassword2')
password.clear()
password.send_keys(password_value)
confirm_pass = self.driver.find_element(By.CSS_SELECTOR,'#wpRetype')
confirm_pass.clear()
209
confirm_pass.send_keys(confirm_password_value)
email = self.driver.find_element(By.CSS_SELECTOR,'#wpEmail')
email.clear()
email.send_keys(email_value)
captcha = self.driver.find_element(By.CSS_SELECTOR,'#mw-input-captchaWord')
captcha.clear()
captcha.send_keys(captcha_value)
button = self.driver.find_element(By.CSS_SELECTOR,'#wpCreateaccount')
button.click()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
15.2. Excel
Los datos los podemos obtener de un archivo de excel. Para poder leer los datos de un archivo en
excel, necesitamos la librería xlrd que se instala con el siguiente comando:
Para poder leer el excel, vamos a necesitar los siguientes métodos y propiedades:
• row_values(row-index, init, end): nos devuelve una lista con los valores de la fila row-index
desde la columna init a la end.
• nrows: nos devuelve el número de las filas que hay en la hoja de excel.
• ncols: nos devuelve el número de las columnas que hay en la hoja de excel.
/selenium/ejemplos/data_driven/data_driven_excel.py
210
filename = '/home/python/workspace/python-selenium-datadriven/registerData.xlsx'
def get_data(filename):
rows = []
book = xlrd.open_workbook(filename, 'r')
sheet = book.sheet_by_index(0)
for row_idx in range(1, sheet.nrows):
rows.append(list(sheet.row_values(row_idx, 0, sheet.ncols)))
return rows
@ddt
class DataDrivenExcel(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get(
'http://es.wikipedia.org/w/index.php?title=Especial:Crear_una_cuenta')
@data(*get_data('registerData.xlsx'))
@unpack
def testDataDrivenExcel(self, username_value, password_value,
confirm_password_value, email_value, captcha_value):
username = self.driver.find_element(By.CSS_SELECTOR,'#wpName2')
username.clear()
username.send_keys(username_value)
password = self.driver.find_element(By.CSS_SELECTOR,'#wpPassword2')
password.clear()
password.send_keys(password_value)
confirm_pass = self.driver.find_element(By.CSS_SELECTOR,'#wpRetype')
confirm_pass.clear()
confirm_pass.send_keys(confirm_password_value)
email = self.driver.find_element(By.CSS_SELECTOR,'#wpEmail')
email.clear()
email.send_keys(email_value)
captcha = self.driver.find_element(By.CSS_SELECTOR,'#mw-input-captchaWord')
captcha.clear()
captcha.send_keys(captcha_value)
button = self.driver.find_element(By.CSS_SELECTOR,'#wpCreateaccount')
button.click()
211
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
212
15.3. Lab: DataDriver
15.3.1. Ejercicio 15
• Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
• Crea una espera con timeout de 10s y que realice la búsqueda del elemento cada 200ms,
ignorando las excepciones
213
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.common.exceptions import NoSuchElementException
class Ejercicio14(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
# Navegar a 'http://es.wikipedia.org/wiki/Wikipedia:Portada'
self.driver.get('http://es.wikipedia.org/wiki/Wikipedia:Portada')
def testCheckboxes(self):
# Pulsar el botón de buscar
search_button = self.driver.find_element(By.CSS_SELECTOR,'#searchButton')
search_button.click()
# Crea una espera con timeout de 10s y que realice la búsqueda del elemento cada 200ms, ignorando las excepciones
custom_wait = WebDriverWait(self.driver, 10, poll_frequency=0.2, ignored_exceptions=[NoSuchElementException])
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
15.4. Ejercicio 16
• Usar ddt para pasar datos como parámetros a los tests
214
• Obtener el campo de captcha y rellenarlo
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from ddt import ddt, data, unpack
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
@ddt
class DataDriven(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.get('http://es.wikipedia.org/w/index.php?title=Especial:Crear_una_cuenta')
password = self.driver.find_element(By.CSS_SELECTOR,'#wpPassword2')
password.clear()
password.send_keys(password_value)
confirm_pass = self.driver.find_element(By.CSS_SELECTOR,'#wpRetype')
confirm_pass.clear()
confirm_pass.send_keys(confirm_password_value)
email = self.driver.find_element(By.CSS_SELECTOR,'#wpEmail')
email.clear()
email.send_keys(email_value)
captcha = self.driver.find_element(By.CSS_SELECTOR,'#mw-input-captchaWord')
captcha.clear()
captcha.send_keys(captcha_value)
button = self.driver.find_element(By.CSS_SELECTOR,'#wpCreateaccount')
button.click()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
215
Capítulo 16. Pruebas Multi Browser y
Selenium Grid
Hasta ahora hemos realizado nuestros test trabajando con un solo navegador. En este punto del
temario vamos a ver como podemos realizar las pruebas en paralelo en varios navegadores, en
nuestro caso Firefox y Chrome.
Además también exploraremos Selium Grid, que nos permitirá realizar nuestros test en varios
dispositivos con diferentes sistemas operativos y navegadores diferentes.
16.1. Multibrowse
En ocasiones necesitaremos probar nuestra aplicación en diferentes navegadores. Está claro que si
estamos realizando un test sobre una página sencilla escrita en HTML no veremos diferencia entre
un navegador y otro. Pero si nuestra aplicación es mas complicada, aunque el código sea el mismo
para todos los navegadores, los resultados de los procesos de renderización son diferentes. Por
ejemplo podemos tener botones que no estén correctamente alineados ya que el navegador no está
renderizando como esperamos.
216
import unittest
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
class PythonOrgSearchChrome(unittest.TestCase):
def setUp(self):
chrome_options = webdriver.ChromeOptions()
self.driver = webdriver.Chrome()
self.driver.set_window_size(1920, 1080)
self.driver.maximize_window()
def test_search_in_python_chrome(self):
driver = self.driver
driver.get('http://www.google.com')
driver.find_element(By.XPATH,"/html/body/div[2]/div[2]/div[3]/span/div/div/div/div[3]/div[1]/button[2]/div")
.click()
self.assertIn("Google", driver.title)
time.sleep(5)
search_box = driver.find_element(By.NAME,'q')
search_box.send_keys('Pronoide')
#nos aseguramos de que la pagina de resultados devuelve algo
assert "No results found." not in driver.page_source
search_box.submit()
time.sleep(5)
driver.save_screenshot('screenshot-deskto-chrome.png')
def tearDown(self):
self.driver.close()
class PythonOrgSearchFireFox(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.set_window_size(1920, 1080)
self.driver.maximize_window()
def test_search_in_python_firefox(self):
driver = self.driver
driver.get('http://www.google.com')
driver.find_element(By.XPATH,"/html/body/div[2]/div[2]/div[3]/span/div/div/div/div[3]/div[1]/button[2]/div")
.click()
self.assertIn("Google", driver.title)
time.sleep(5)
search_box = driver.find_element(By.NAME,'q')
search_box.send_keys('Pronoide')
assert "No results found." not in driver.page_source
search_box.submit()
time.sleep(5)
driver.save_screenshot('screenshot-deskto-firefox.png')
def tearDown(self):
self.driver.close()
if __name__ == "__main__":
unittest.main()
217
16.2. Selenium Grid
Gracias a Selenium Grid podemos realizar nuestros test en diferentes máquinas que se encuentran
en diferentes localizaciones y con diferentes caractéristicas (navegador, sistema operativo).
Selenium Grid trabaja con un modelo de HUB/NODO. De esta forma arranca el test en el HUB, pero
la ejecución se hace en los nodos
HUB
• Es un servidor proxy que nos facilita ejecutar test en paralelo en los diferentes nodos
• Recoge las instrucciones desde el cliente y las ejecuta de manera remota en varios nodos.
Nodo
• Recibe las peticiones desde el HUB en formato Json y las ejecuta usando el webdriver
Una vez hemos levantado nuestro servidor tenemos que modificar nuestros scripts de testing.
Podemos usar WebDriver de forma remota de la misma manera que trabajamos en local, pero
tenemos que tener en cuenta un par de detalles:
• En primer lugar debemos configurar Remote Webdriver para que pueda ejecutarse en una
218
máquina remota.
• Esto es debido a que un Remote Webdriver esta compuesto de dos piezas: un cliente y un
servidor
• Para ejecutar un remote webdriver haremos uso de RemoteWebDriver, para ello apuntaremos a
la url del server
firefox_options = webdriver.FirefoxOptions()
driver = webdriver.Remote(
command_executor='http:url_server',
options=firefox_options
)
driver.get("http://www.google.com")
driver.quit()
Podemos seguir costumizando nuestro test, con las opciones del navegador. Si por ejemplo
quisieramos ejecutar Chrome en un Windows 11, usando una determinada versión de chrome:
chrome_options = webdriver.ChromeOptions()
chrome_options.set_capability("browserVersion", "67")
chrome_options.set_capability("platformName", "Windows XP")
driver = webdriver.Remote(
command_executor='http://www.example.com',
options=chrome_options
)
driver.get("http://www.google.com")
driver.quit()
Para poder transferir archivos entre el cliente y el servidor usaremos LocalFileDetector. Esto es
util cuando por ejemplo tenemos que subir un archivo a a una aplicación web. Gracias al
webdriver esta transferencia del desde la máquina local al servidor se realiza en tiempo de
ejecución.
219
from selenium.webdriver.remote.file_detector import LocalFileDetector
driver.file_detector = LocalFileDetector()
driver.get("http:www.paginatest.com")
driver.find_element(By.ID, "myfile").send_keys("/ruta/a/nuestro/archivo")
Lo primero que debemos hacer es descargar e instalar nuestro servidor selenium. Creamos un
directorio llamado selenium y lo descargamos allí
En este curso vamos a configurar las dos primeras opciones, pera ver las opciones relativas a la
tercera opción puedes visitar este enlace
Configuración Standalone
220
$ java -jar selenium-server-4.5.3.jar node --port 6666
import unittest
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
class PythonOrgSearchChrome(unittest.TestCase):
def setUp(self):
chrome_options = webdriver.ChromeOptions()
self.driver = webdriver.Remote(
command_executor="http://localhost:4444",
options= chrome_options
)
self.driver.set_window_size(1920, 1080)
self.driver.maximize_window()
def test_search_in_python_chrome(self):
driver = self.driver
driver.get('http://www.google.com')
driver.find_element(By.XPATH,"/html/body/div[2]/div[2]/div[3]/span/div/div/div/div[3]/div[1]/button[2]/div")
.click()
self.assertIn("Google", driver.title)
time.sleep(5)
search_box = driver.find_element(By.NAME,'q')
search_box.send_keys('Pronoide')
#nos aseguramos de que la pagina de resultados devuelve algo
assert "No results found." not in driver.page_source
search_box.submit()
time.sleep(5)
driver.save_screenshot('screenshot-deskto-chrome.png')
def tearDown(self):
self.driver.close()
class PythonOrgSearchFireFox(unittest.TestCase):
def setUp(self):
firefox_options = webdriver.FirefoxOptions()
self.driver = webdriver.Remote(
command_executor="http://localhost:4444",
options=firefox_options
)
self.driver.set_window_size(1920,1080)
self.driver.maximize_window
def test_search_in_python_firefox(self):
driver = self.driver
driver.get('http://www.google.com')
driver.find_element(By.XPATH,"/html/body/div[2]/div[2]/div[3]/span/div/div/div/div[3]/div[1]/button[2]/div")
.click()
self.assertIn("Google", driver.title)
time.sleep(5)
search_box = driver.find_element(By.NAME,'q')
search_box.send_keys('Pronoide')
assert "No results found." not in driver.page_source
search_box.submit()
221
time.sleep(5)
driver.save_screenshot('screenshot-deskto-firefox.png')
def tearDown(self):
self.driver.close()
if __name__ == "__main__":
unittest.main()
222
Capítulo 17. Web Scraping
17.1. ¿Qué es el Web Scraping?
A través del web scraping examinamos el código fuente de las páginas web en busca de patrones
concretos. Para extraer esta información se útilizan programas que reciben el nombre de web
scrapers, crawlers, spiders o bots.
Los datos que obtenemos a partir de esos programas son posteriormente resumidos, evaluados,
combinados o almacenados.
Un web scraper se desarrolla teniendo en cuenta la estructura específica de una página web, por lo
tanto si esa estructura cambia tambien debe modificarse el programa. Esto es algo que con Python
resulta sencillo.
Además Python tiene como fortalezas dos de las bases técnicas del web scraping:
• Procesamiento de texto
223
Junto a todo lo anterior hay que tener en cuenta que herramientas como Scrapy, Selenium y
BeatifulSoup están diseñadas para poder usar Python a la hora de hacer web scraping.
17.4. Scrapy
Scrapy como deciamos anteriormente es una plataforma en la que podemos aplicar tecnicas de web
scraping usando Python: * Cuenta con un pipeline integrado para procesar los datos extraidos. * La
apertura de páginas se produce de forma asincrona, dicho de otro modo esto permite la descarga
de varias páginas de forma simultanea. * Es una buena opción cuando hay que procesar grandes
volúmenes de páginas.
Scrapy utiliza un analizador sintáctico o parser HTML para extraer los datos buscados del
código fuente de la web (HTML).
La clave de Scrapy son los web spiders: * Progrmas sencillos basados en Scrapy. * Cada uno de
estos programas está creado para trabajar sobre una web concreta. * Van "Descolgandose" página a
página * Se desarrollan en POO → Cada Spider es una clase de Python
Al instalar Scrapy, ademas del paquete y módulos de Python se incluye una herramienta en linea de
comandos, la Scrapy Shell para el control de los spiders.
También existe la posibilidad de almacenar los spiders ya creados en la Scrapy Cloud. De esta
forma, ya que se ejecutan en tiempos establecidos, podemos trabajar sobre sitios complejos sin
necesidad de ordenador o conexión a internet.
Para trabajar con Scrapy primero debemos instalarlo, si no lo tenemos lo haremos mediante pip
224
Crear el proyecto
Desde el directorio en que vayamos a almacenar nuestro código ejecutamos::
.
├── proyecto # Módulo de Python del proyecto. Podemos importarlo
│ ├── __init__.py
│ ├── items.py #Definición de los items del proyecto
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders #Subpaquete donde poodemos almacenar nuestros spiders.
│ └── __init__.py
└── scrapy.cfg #Archivo de configuración
$ cd /home/python/Desktop/proyecto/spiders
Antes de comenzar a escribir nuestro código vamos a ver como funciona scrapy a través de la shell,
esto nos ayudara a comprender mejor lo que pasa en los siguientes ejemplos.
225
$ scrapy shell https://quotes.toscrape.com/page/1/
Para extraer las citas y los autores nos basaremos en el siguiente codigo fuente de la página, cada
una de las citas tendrá esta estructura
<div class="quote">
<span class="text">“The world as we have created it is a process of our
thinking. It cannot be changed without changing our thinking.”</span>
<span>
by <small class="author">Albert Einstein</small>
<a href="/author/Albert-Einstein">(about)</a>
</span>
<div class="tags">
Tags:
<a class="tag" href="/tag/change/page/1/">change</a>
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
<a class="tag" href="/tag/world/page/1/">world</a>
</div>
</div>
>>> response.css("div.quote")
Cada uno de estos selectores es cada una de las citas que hay en el web. Vamos a asignar el primer
selector a un varible para poder trabajar sobre ella.
226
Vamos a extraer text, author, tags de la primera cita almacenada en "quote".
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
urls = [
'https://quotes.toscrape.com/page/1/',
'https://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
Nuestra clase QuotesSpider es una subclase de de scrapy.Spider que define los atributos y
métodos usados:
• parse() → Es el método responsable de manejar las respuestas que se han descargado para cada
una de las solicitudes
227
nuevas URL para seguir y crea nuevas solicitudes a partir de ellas.
Para ejecutar nuestro spider debemos hacerlo desde el directorio superior, en nuestro caso:
$ cd /home/python/Documents/proyecto
...
2022-07-20 10:28:02 [scrapy.core.engine] INFO: Spider opened
2022-07-20 10:28:02 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2022-07-20 10:28:02 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2022-07-20 10:28:03 [scrapy.core.engine] DEBUG: Crawled (404) <GET https://quotes.toscrape.com/robots.txt> (referer:
None)
2022-07-20 10:28:03 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com/page/1/> (referer: None)
2022-07-20 10:28:03 [quotes] DEBUG: Saved filequotes-1.html
2022-07-20 10:28:03 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com/page/2/> (referer: None)
2022-07-20 10:28:03 [quotes] DEBUG: Saved filequotes-2.html
2022-07-20 10:28:03 [scrapy.core.engine] INFO: Closing spider (finished)
Se han creado dos archivos, tal y como le indicabamos en nuestro parse. Si consultamos en nuestro
directorio, los podemos ver
$ cd /home/python/Documents/proyecto/proyecto
$ ls
Hasta aquí simplemente le hemos dicho a nuestro spider que creara los archivos, pero todavía no
hemos extraido ningun dato. Vamos integrar la lógica que necesitamos para que esto suceda en
nuestro spider.
228
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'https://quotes.toscrape.com/page/1/',
'https://quotes.toscrape.com/page/2/',
]
...
2022-07-20 10:52:10 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/page/2/>
{'text': '“Good friends, good books, and a sleepy conscience: this is the ideal life.”', 'author': 'Mark Twain', 'tags':
['books', 'contentment', 'friends', 'friendship', 'life']}
2022-07-20 10:52:10 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/page/2/>
{'text': '“Life is what happens to us while we are making other plans.”', 'author': 'Allen Saunders', 'tags': ['fate',
'life', 'misattributed-john-lennon', 'planning', 'plans']}
...
Pero todavía nos queda almacenarlos, eso lo haremos a través de la terminal a la hora de la
ejecución. Podemos usar dos opciones:
Hasta ahora hemos recorrido solo dos páginas. ¿Como podemos descolgarnos por todas las
páginas?.
• Lo primero es localizar y extraer el link de la página a la que nos queremos dirigir. En nuestras
páginas de ejemplo encontramos lo siguiente
229
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
En nuestro caso queremos obtener el elemento "href", vamos a probarlo con la scrapy shell
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'https://quotes.toscrape.com/page/1/',
]
17.5. Selenium
Selenium es un framework diseñado para realizar test automatizados de software sobre
aplicaciones web. Aunque como decimos se creó para poner a prueba paginas y aplicaciones web,
su WebDriver puede usarse con Python para el scraping.
• Carga la página en un navegador sin interfaz de usuario (No trabaja con el código fuente)
230
• El navegador interpreta el código fuente y crea un DOM (Document Object Model)
• La interfáz estandarizada permite probar las interacciones del usuario (clicks, rellenar
formularios). Los cambios se reflejarán en el DOM.
• Podemos hacer scraping en páginas web creadas con JavaScript ya que el DOM se genera de
manera dinámica.
Al igual que el caso de Scrapy lo primero será instalar Selenium en nuestro equipo
Tras su instalación necesitamos descargar e instalar el Web Driver del navegador que vayamos a
usar. Como deciamos antes, nos permitira movernos por la web, ejecutar JavaScript y demás.
Selenium soporta multiples navegadores, para elegir con el que vamos a trabajar (Chrome)
debemos ir a la página de Selenium y descargar la última versión estable.
231
Una vez que lo tengamos descargado debemos descomprimirlo y ejecutarlo desde la terminal.
$ cd Downloads/
$ ls
chromedriver_linux64.zip
$ unzip chromedriver_linux64.zip
Archive: chromedriver_linux64.zip
inflating: chromedriver
$ ls
chromedriver chromedriver_linux64.zip
$ ./chromedriver
Starting ChromeDriver 103.0.5060.134 (8ec6fce403b3feb0869b0732eda8bd95011d333c-refs/branch-heads/5060@{#1262}) on port
9515
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.
El Web Driver de Selenium nos ofrece varias funciones que nos ayudaran a localizar los elementos
en la web. Vamos a verlos a continuacion
driver.find_element(By.ID, 'id')
232
find_element(By.NAME, "name")
◦ El lenguaje Xpath nos permite localizar nodos en un documento XML, por ello al usar
selenium tenemos acceso a los atributos de un HTML (HTML es una implementación de
XML)
find_element(By.XPATH, "xpath")
◦ Xpath = //tagname[@Attribute='Value']
▪ @ → Selecciona el atributo
• Si queremos usar una parte del texto del link a modo de patrón podemos usar
233
En los casos anteriores hemos usado find_element pero también podemos usar find_elements,
que nos devolvera una lista con múltiples elementos que coincidan con nuestra busqueda.
Junto a estas opciones para localizar elementos no hay que olvidar que Selenium nos permite
interactuar con ellos, y con la web en general. Así que logicamente, lo que haremos después de
localizar un elemento es justamente eso, interactuar con él.
Hoy en día nos encontramos con que cualquier web usa ajax para cargar su contenido, diferentes
partes de la web cargarán a diferentes velocidades. Esto es un problema a la hora de hacer scraping
en ellas. Para resolver esto debemos usar Wait, que nos permite definir un intervalo entre dos
acciones consecutivas.
• Wait explicito
◦ Es un fragmento de código que espera a que suceda cierta condición antes de comenzar a
ejecutar
• Wait implicito
◦ Indica al Web Driver que sondee el DOM durante un cierto período de tiempo cuando este
intentando encontrar elemento o elementos que no estén disponibles de inmediato.
234
from selenium.webdriver.common.action_chains import ActionChains
actions= ActionChains(driver)
actions.send_keys(Keys.TAB) ## Presiona una tecla
actions.send_keys(Keys.ENTER) ## Presiona una tecla
actions.send_keys(username) ## input de una variable
actions.send_keys(Keys.Enter) ## presiona una tecla
actions.send_keys(Keys.Tab) ## presiona una tecla
actions.perform() ## Ejecuta las acciones una a una.
235
17.6. Lab: Web Scraping
17.6.1. Lab 1
Vamos a usar Selenium para realizar scraping en la pagina web de Box Office Mojo que contiene un
listado de 200 películas. Nuestro objetivo es conseguir los datos de estás películas y almacenarlas en
un csv
path = ('/home/python/Downloads/chromedriver')
s = Service(path)
driver = webdriver.Chrome(service=s)
driver.get('https://www.boxofficemojo.com/chart/top_lifetime_gross/?area=XWW')
• Obten los nombres de las películas, ten en cuenta que tienen esta estructura. Localizalos
mediante XPATH
236
release_year = driver.find_elements(By.XPATH,'//td[@class="a-text-left mojo-field-type-year"]/a[@class="a-link-normal"]')
release_year_list = []
for year in range(len(release_year)):
release_year_list.append(release_year[year].text)
print(release_year_list)
• Busca la recaudación y almacenala en una lista. Puedes guiarte por este código
• Une las tres listas anteriores y exportalo a un csv con los nombres de los campos como cabecera
('Movie Name', 'Release Date','Lifetime Earnings')
237
Capítulo 18. Planificando tareas con Python
Siempre es útil poder planificar tareas o ejecuciones en un momento determinado. Esto lo logramos
en Python gracias a los módulos Sched y Time
Lo primero que debemos hacer una vez importados los módulos y funciones necesarias es declarar
un programador el objeto de Python sobre el que vamos a aplicar diferentes métodos.
Una vez declarado este programador, sobre él podemos usar los siguientes métodos:
Donde:
• priority → establece la prioridad de ejecución si dos o mas eventos tienen el mismo momento de
ejecución. A menor valor, mayor prioridad
Otros métodos que podemos usar para planificar tareas son los siguientes:
• cancel() → Siempre que el evento no haya iniciado su ejecución usará el valor devuelto por
enterabs y enter para cancelarlo con posterioridad
• run() → Este método pone en marcha el programador una vez que hayamos definido los
238
eventos. Hace que el programador espere a que los eventos se vayan ejecutando uno a uno, en
el tiempo establecido y hasta que no quede ninguno pendiente.
import sched
import time
def ejecutar_programa(estado):
if estado:
print("Comenzado la ejecución...")
else:
print("Ejecución terminada...")
comienzo = int(time.time())
t1 = comienzo + 1
t2 = t1 + 4
programador.run()
print("Programador finalizado", int(time.time()))
El ejemplo anterior es la versión mas sencilla. Podemos añadir la opción de cancelar en la función
que es llamada por los eventos, y manejar eventos con prioridad
239
import sched
import time
import random
if estado:
print("Iniciando ejecución...")
else:
print("Terminando ejecución...")
numero = random.randint(1, 5)
if numero %2:
print("No terminar ejecución")
continuar_programa = True
else:
print("Terminar ejecución")
continuar_programa = False
#Declarar el programador
programador = sched.scheduler(time.time, time.sleep)
comienzo = int(time.time())
programador.run()
print("Programador finalizado", int(time.time()))
240
18.1. Lab: Planificando tareas con Python
18.1.1. Laboratorio 1
Escribe un script con un programador con tres eventos que ejecuten la misma tarea después de
esperar 1, 2 y 3 segundos respectivamente cada uno. Para ello debes usar los módulos sched y time.
Solución
import sched
import time
programador = sched.scheduler()
inicio = time.time()
print('inicio:', inicio)
18.1.2. Laboratorio 2
Escribe un script con un programador con tres eventos que ejecuten la misma tarea, el primer
evento se ejecutará después de un segundo, los otros dos eventos se ejecutaran en el mismo tiempo
(3 Segundos). Deberas establecer un la prioridad de estos dos últimos para que el tercero se ejecute
antes que el segundo.
Solución
241
import sched
import time
def tiempo():
formato = '%d-%m-%Y %H:%M:%S'
242
Capítulo 19. Envío de Email con Python
Una vez más nos ayudaremos de módulos específicos para el envío de correos electrónicos con
Python. En este caso vamos a usar los módulos smtplib y email.
• smtplib → Módulo de Python que define un objeto de sesión de cliente SMTP que se puede usar
para enviar un correo a cualquier máquina de internet con un daemon de escucha SMTP o
ESMTP.
• email → Módulo externo a Python, que nos permitirá añadir formatos a nuestro correo, así
como archivos o poder identificar al remitente, destinatario o asunto del mensaje. Dentro de
este módulo podemos encontrar las siguientes funciones:
243
19.1. Lab: Enviando un mail con Python
Para poder realizar este laboratorio lo primero que vamos a hacer es abrir una cuenta en Gmail, en
la que activaremos la doble autenticación y generaremos una contraseña para aplicaciones.
Una vez que nos hayamos registrado debemos activar la verificación en dos pasos, para ello iremos
al menú de configuración de nuestra cuenta.
244
Después de activar la doble verificación debemos generar nuestra contraseña para aplicaciones.
Para ello debemos elegir las siguientes opciones en los menús desplegables y pulsar el botón
general. Tras ello nos mostrara una contraseña que deberemos almacenar, ya que la usaremos mas
adelante.
245
import smtplib
smtp_address = 'smtp.gmail.com'
smtp_port = 587
Lo primero sera añadir a nuestro código las funciones que vamos a utilizar.
smtp_address = 'smtp.gmail.com'
smtp_port = 587
email_address = 'dirección envio'
email_password = "contraseña para aplicaciones"
email_receiver = "dirección de destino"
246
# Creamos la instancia del objeto menssage
msg = MIMEMultipart()
server.sendmail(msg['FROM'], msg['To'],msg.as_string())
print(f"Correo enviado a {msg['To']}")
Lo siguiente sera añadir una imagen adjunta al mensaje. Para ello importaremos la función
MIMEImage
Para poder adjuntar la imagen debemos abrirla como un archivo y usar .attch sobre nuestro
mensaje para añadirla
247
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.text import MIMEText
import smtplib
smtp_address = 'smtp.gmail.com'
smtp_port = 587
email_address = 'dirección envio'
email_password = "contraseña para aplicaciones"
email_receiver = "dirección de destino"
msg = MIMEMultipart()
server.sendmail(msg['FROM'], msg['To'],msg.as_string())
print(f"Correo enviado a {msg['To']}")
Ahora ya tenemos la imagen pero nuestro texto sigue siendo plano. Vamos a añadir texto en HTML
para enriquecer nuestro correo. Para ello creamos un string con nuestro código HTML
248
msg.attach(MIMEText(html,"html"))
smtp_address = 'smtp.gmail.com'
smtp_port = 587
email_address = 'dirección envio'
email_password = "contraseña para aplicaciones"
email_receiver = "dirección de destino"
msg = MIMEMultipart()
server.sendmail(msg['FROM'], msg['To'],msg.as_string())
print(f"Correo enviado a {msg['To']}")
249
Capítulo 20. Librerias SciKit y OpenCV
20.1. SciKit
SciKit es el módulo de Python desarrollado para poder realizar analisis predictivos sobre nuestros
datos.
• Forma parte del ecosistema Python al estar construido sobre Numpy, SciPy y Matplotlib
• Al compartir con Pandas trabajar sobre Numpy tienen una buena interacción entre ambos.
SciKit implementa una gran cantidad de algoritmos de machine learning, procesamiento de datos y
selección de modelos.
◦ Esta es una de sus características mas importantes, muchas veces tendremos que iterar
entre diferentes algoritmos mientras desarrollamos nuestros modelos para decidir cual es el
que mejor se ajusta.
◦ A la vez, esta abstracción no nos impide poder configurar los algoritmos, en resumén
podemos decir que tenemos el control absoluto de parametros y configuración.
SciKit nos permite trabajar los siguientes métodos sobre nuestros datos.
Clasificación
Regresión
250
• Lo podemos aplicar por ejemplo a evolución de precios
Clustering
• Los algoritmos con los que trabaja, entre otros, son k-Means, spectral clustering, mean-shift
251
Selección de modelos
Preprocesamiento
252
• Podemos preprocesar nuestros datos cuando por ejemplo queremos transformar nuestros
datos, por ejemplo un texto para su uso posterior con algoritmos de machine learning
Filtros Morfologicos
20.2. OpenCV
OpenCV es una libreria de Python diseñada para resolver problemas de visualización
• Hace uso de Numpy, todos las estructuras de tipo array de OpenCV pueden ser convertidas en
253
arrays de Numpy y viceversa.
◦ Abrir y guardar
◦ Escribir en Imagenes
◦ Para entender bien como funciona OpenCV debemos pensar en las imagenes como Matrices
Para nosotros una imagen es una matriz de estandar de Numpy que contiene píxeles de puntos de
datos:
• Para poder procesar una imagen debemos pasarla a forma binaria, para ello necesitamos
calcular el color de una imagen mediante la siguiente formula:
• Según la formula anterior a mas cantidad de píxeles mas colores puede tener nuestra imagen.
Por ejemplo para 16 bits/pixel tendremos 65000 posibles colores.
◦ Blanco - valor 1
◦ Negro - valor 2
254
20.3.2. Escala de grises
• Consta de 8 bits/pixel
◦ Negro - valor 0
• Son una combinzacion de los colores rojo, azul y verde. El resto de colores se consigue con la
mezcla de estos en distintas proporciones.
◦ Negro - valor 0
255
20.4. Comandos y herramientas de OpenCV
20.4.1. Comandos básicos
Para poder trabajar con OpenCV debemos importar el propio módulo y los relacionados con él.
import cv2
import matplotlib.pyplot as plt
import numpy as np
Todas las imagenes que vamos a usar en estos ejemplos estan en el directorios
workspace/python/python-scikit
• Leer una imagen Para leer una imagen usaremos la función imread (convierte la imagen en
un array de numpy)
new_img = cv2.imread("imagen")
De esta forma estamos transformando la imagen en un array de numpy. Con shape podemos
conocer sus dimensiones y canales
• m → dimension
• n → dimension
256
• c → nº de canales
new_img.shape
(m,n,c)
plt.imshow(new_img)
plt.show()
Existe una discrepancia entre como usan los colores primarios OpenCV y
Matplotlib:
Podemos guardar la imagen en el directorio en el que estemos trabajando con la función imwrite
cv2.imwrite('image_name.png',new_img)
Donde:
Donde:
257
• coordendas → Se dibuja desde Pt1 (Esquina superior izquierda) hasta Pt2(Esquina inferior
derecha)
• color → Color para la forma. Debemos pasarlo como una tupla por ejemplo (150,0,0). Si usamos
escala de grises será la escala de brillo
Agregar texto a las imágenes es muy parecido a dibujar formas. Pero antes de hacerlo debemos
indicar algunos argumentos antes de empezar
font = cv2.FONT_HERSHEY_SIMPLEX
text = cv2.putText(img, 'texto',(10,500), font, 4,(255,255,255),2,cv2.LINE_AA)
plt.imshow(text)
plt.show()
20.5. Scikit-Image
Scikit-image es una colección de algoritmos para el procesamiento de imágenes.
Las imagenes en scikit-image están representadas como un array de numpy, por lo tanto muchas de
las modíficaciones que podemos realizar sobre ellas las podemos hacer usando simplemente las
funciones que está libreria pone a nuestra disposición.
Es una libreria externa a Python por lo tanto lo primero que debemos hacer es instalarla:
import skimage
Como deciamos las imagenes son interpretadas como un array de numpy, podemos comprobarlo
con una de las imagenes de ejemplo que incluye la libreria, camera
258
Podemos comprobar como el objeto generado es un array de numpy
type(camera)
numpy.ndarray
Es importante, como en los casos anteriores conocer las dimensiones y el tamaño de nuestro array:
camera.shape
(512,512)
camera.size
262144
Hay que tener en cuenta que las dimensiones sigue las reglas de las matrices,
origen (0,0) estará en esquina superior izquierda
• Primero vamos a crear una mascara que comprenda todos los elementos del array cuyo valor
sea menor a 87, y la vamos a aplicar a nuestra imagen para convertir todos esos elementos a
259
"blanco" (255)
• Vamos a añadir mas modificaciones al código anterior. Vamos a convertir a negro los elementos
que cumplan las condiciones dadas para cada dimensión.
camera = data.camera()
type(camera)
camera = cv2.cvtColor(camera, cv2.COLOR_BGR2RGB)
mask = camera <87
camera[mask] = 255
inds_r = np.arange(len(camera))
inds_c = 4 * inds_r % len(camera)
camera[inds_r,inds_c]=0
• Esta modificacion hara que nos aparezcan lineas diagonales de color negro en la imagen
260
• Por último vamos a crear un marco negro circular usando la ecuación de la circunferencia
261
262
20.6. Lab: SciKit y OpenCV
Antes de comenzar el laboratorio es necesario instalar el módulo OpenCV
Usando openCV abre y visualiza, con los colores correctos, la imagen muestra-01.png que
encontraras en directorio workspace/python-scikit.
import numpy as np
import cv2
import matplotlib.pyplot as plt
new_img = cv2.imread("/home/python/workspace/python-scikit/muestra-01.png")
new_img = cv2.cvtColor(new_img, cv2.COLOR_BGR2RGB)
cv2.imwrite('imagen_ok.png',new_img)
plt.imshow(new_img)
plt.show()
263
20.6.2. Laboratorio: Formas y texto sobre una imagen
Crea una imagen completamente negra a través de numpy y openCV de 500x500 pixeles, dibuja
sobre ella:
• Una diagonal azul que recorra la imagen desde la esquina inferior izquierda a la esquina
superior derecha. (color:(0,0,255))
264
import numpy as np
import matplotlib.pyplot as plt
import cv2
blue_line = cv2.line(black_img,(500,0),(0,500),(0,0,255),5)
plt.imshow(blue_line)
green_rectangle = cv2.rectangle(black_img,(0,300),(200,500),(0,255,0),5)
plt.imshow(green_rectangle)
plt.show()
yellow_circle = cv2.circle(black_img,(100,400),100,(255,255,0), 2)
plt.imshow(yellow_circle)
2
plt.show()
Para este laboratorio vamos a usar la imagen muestra-02.jpg la puedes encontrar en el directorio
workspace/python-scikit
◦ Ayuda para ello puedes usar la función cv2.resize como se muestra a continuancion:
265
import cv2
import skimage
img_new = skimage.io.imread("muestra-02.jpg")
cv2.imwrite("muestra-02-resize.png", output)
266