Apuntes FP2 2021
Apuntes FP2 2021
Apuntes FP2 2021
Python ...................................................................................................................................... 6
Excepciones ..................................................................................................................... 6
Control de excepciones .................................................................................................... 6
Control de múltiples excepciones ...................................................................................... 7
Control de excepciones no específicas ............................................................................... 8
Bloques adicionales ......................................................................................................... 9
Excepciones en las funciones estándar ............................................................................ 10
Cómo generar excepciones ............................................................................................. 10
Sentencia raise sin argumentos ....................................................................................... 11
Asertos............................................................................................................................ 13
Clases.............................................................................................................................. 13
Definicón e instanciación de clases .................................................................................. 14
Definición e instanciación de clases (2) ............................................................................ 14
Atributos de un objeto .................................................................................................... 15
Atributos añadidos dinámicamente ................................................................................. 15
Añadir atributos en la inicialización usando __init__........................................................ 16
Métodos ......................................................................................................................... 16
Self.................................................................................................................................. 17
Cómo llamar a un método ............................................................................................... 17
Métodos mágicos ............................................................................................................ 18
Herencia.......................................................................................................................... 18
La clase object ................................................................................................................. 19
Inicialización de clases derivadas ..................................................................................... 19
Atributos de las subclases ............................................................................................... 20
Cómo averiguar la clase de un objeto .............................................................................. 20
Clases abstractas ............................................................................................................. 20
Polimorfismo................................................................................................................... 21
Sustitución de métodos (overriding) ................................................................................ 22
El polimorfismo y los métodos mágicos ........................................................................... 23
Cómo definir nuevas excepciones.................................................................................... 23
Ajuste de las nuevas excepciones .................................................................................... 24
Jerarquías de excepciones ............................................................................................... 24
Sobrecarga de operadores............................................................................................... 24
Sobrecarga de operadores de comparación ..................................................................... 25
1
Ordenación total ............................................................................................................. 25
Funciones y operadores unarios ...................................................................................... 26
Operadores aritméticos binarios ..................................................................................... 27
Operadores binarios reflejados ....................................................................................... 27
Sobrecarga de funciones (o métodos) ............................................................................. 28
Polimorfismo basado en parámetros por omisión ........................................................... 28
Polimorfismo basado en el tipo de los parámetros .......................................................... 29
Iteradores ....................................................................................................................... 30
Las funciones predefinidas iter y next .............................................................................. 30
Generadores ................................................................................................................... 31
Prueba de programas ...................................................................................................... 31
Casos de prueba .............................................................................................................. 32
Diseño general de las pruebas ......................................................................................... 32
Tipos de prueba .............................................................................................................. 33
Pruebas de caja blanca .................................................................................................. 34
Grafo del flujo de ejecución ............................................................................................. 34
Complejidad ciclomática y cálculo de los caminos básicos .......................................... 36
Diseño de casos de prueba .............................................................................................. 37
Pruebas de caja negra ................................................................................................... 38
Clases de equivalencia ................................................................................................... 38
Clases de equivalencia válidas y no válidas ...................................................................... 39
Test Driven Development ................................................................................................ 40
Tipos de clases de equivalencia ....................................................................................... 40
Prueba de los valores límite............................................................................................. 41
Prueba de los valores límite con rangos de números reales ............................................. 42
Prueba de contenedores ................................................................................................. 42
Prueba de clases ............................................................................................................. 43
Corredores de pruebas (test runners) .............................................................................. 44
Unittest ........................................................................................................................... 45
TestCase.......................................................................................................................... 46
Informe de pruebas ......................................................................................................... 46
Métodos de aserto (2) .................................................................................................... 48
est fixture........................................................................................................................ 49
Ejecución de tests. Test suite ......................................................................................... 49
T Estructuras encadenadas .............................................................................................. 50
2
Una estructura encadenada está formada por un conjunto de objetos, generalmente
denominados nodos de la estructura, que se referencian unos a otros. Deben existir
objetos terminales, que no referencian a otros objetos, así como al menos un objeto
inicial (según la estructura puede denominarse primero, frente, cima, raíz, etc.) que es
referenciado desde "fuera" de la estructura y sirve como "punto de entrada" a la misma
(en la ilustración, el objeto inicial, el primero de la cadena, es el referenciado por la
variable un_coche).est loader .......................................................................................... 51
Estructuras recursivas ..................................................................................................... 52
Flexibilidad y complejidad ............................................................................................... 52
Listas encadenadas.......................................................................................................... 53
Estructura de una lista encadenada ................................................................................. 54
Inserción por delante ...................................................................................................... 55
Inserción por detrás ........................................................................................................ 56
Inserción por detrás (2) ................................................................................................... 57
Inserción enmedio........................................................................................................... 58
Borrado de un elemento al principio de una lista encadenada ......................................... 58
Borrado del último elemento de una lista encadenada .................................................... 59
Borrar un elemento de una lista encadenada por posición. ............................................. 60
Iteración ................................................................................................................................. 61
Java ........................................................................................................................................ 62
¡Hola, mundo! ................................................................................................................. 62
Llaves, sangrado y punto y coma. .................................................................................... 62
Salida por consola ........................................................................................................... 63
Variables ......................................................................................................................... 64
Tipos de datos primitivos ................................................................................................ 65
Valores y referencias ..................................................................................................... 65
Entrada básica de datos .................................................................................................. 66
Selección binaria ............................................................................................................. 67
Encadenamiento else-if ................................................................................................... 67
Selección múltiple en Java ............................................................................................... 68
Sentencia for ................................................................................................................... 69
Sentencia for con rango .................................................................................................. 69
Sentencia while ............................................................................................................... 70
Funciones ........................................................................................................................ 70
Arrays ............................................................................................................................. 71
Recorrido de un array ..................................................................................................... 72
Estructuras multidimensionales....................................................................................... 73
3
Recorrido de estructuras multidimensionales .................................................................. 73
La clase Arrays (1) ......................................................................................................... 74
La clase Arrays (2) ........................................................................................................... 75
Strings............................................................................................................................ 76
Secuencias de escape. .................................................................................................... 77
Longitud de una string ..................................................................................................... 77
Comparación de strings ................................................................................................... 77
Concatenación de strings .............................................................................................. 78
Acceso a caracteres y substrings ...................................................................................... 79
Contención...................................................................................................................... 79
Búsqueda (I) .................................................................................................................... 80
Búsqueda (II) ................................................................................................................... 80
Otras operaciones útiles ................................................................................................ 81
Módulo para manejar expresiones regulares ............................................................... 82
Búsqueda de coincidencias ............................................................................................ 83
Búsqueda de coincidencias (2)......................................................................................... 84
Sustitución ...................................................................................................................... 84
Clases y objetos............................................................................................................... 85
Atributos de datos de objeto, inicialización ..................................................................... 86
Métodos de objeto .......................................................................................................... 87
Visibilidad ....................................................................................................................... 87
Encapsulamiento de datos .............................................................................................. 88
Atributos de clase............................................................................................................ 90
Herencia.......................................................................................................................... 91
Inicialización de subclases ............................................................................................... 91
Sustitución de métodos (overriding) ............................................................................. 92
Jerarquía de clases .......................................................................................................... 93
Métodos heredados de Object ....................................................................................... 94
Clase de un objeto........................................................................................................... 94
Bloques de control de excepciones .................................................................................. 95
Tratar varias excepciones del mismo modo ..................................................................... 96
Acceso a la información de las excepciones ..................................................................... 96
Control genérico de excepciones ..................................................................................... 97
Bloques adicionales ......................................................................................................... 98
Lanzamiento de excepciones ........................................................................................... 99
Definición de nuevas clases de excepciones .................................................................... 99
4
Declaración de excepciones salientes ............................................................................ 100
Clases abstractas ......................................................................................................... 100
Interfaces ...................................................................................................................... 101
Clases abstractas vs Interfaces ...................................................................................... 102
Cloneable ...................................................................................................................... 102
Comparable<T> ............................................................................................................. 104
Iterator<T> e Iterable<T> .............................................................................................. 104
Uso de los iteradores.................................................................................................... 105
Objeto iterable vs objeto iterador ............................................................................... 106
5
Python
Excepciones
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
a: 7
b: 2
3 1
a: 7
b: 0
Traceback (most recent call last):
File ".../test.py", line3, in <module>
print(a // b, a % b)
ZeroDivisionError: integer division or modulo by zero
Ocurrió un error al intentar dividir un número por cero. El programa se encontró una
situación excepcional (ocurrió algo inesperado o anormal) y se paró. Decimos que se
lanzó una excepción. En este caso, la excepción ZeroDivisionError (podemos verlo en
el mensaje de salida).
Control de excepciones
Las excepciones son, en realidad, bastante comunes, y los programas deben estar
preparados para manejarlas.
a = int(input('a: '))
b = int(input('b: '))
if b != 0:
print(a // b, a % b)
else:
print('El divisor no puede ser cero')
Pero, para manejar excepciones es mejor usar bloques de control, que, en Python, son
bloques try/except.
6
Si sospechamos que un código puede lanzar una excepción, podemos robustecer nuestro
programa encerrando el código sospechoso en un bloque try. El bloque try debe ir
seguido de un bloque except donde ponemos el código para manejar la excepción de la
mejor manera posible en caso de que ocurra.
Cuando ocurre una excepción dentro del bloque try, se saltan todas las instrucciones
restantes en dicho bloque y la ejecución continúa en el correspondiente bloque except.
try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b) # (1)
print('the results were printed out') # (2)
except ZeroDivisionError:
print('El divisor no puede ser cero') # (3)
Resultado:
a: 7
b: 0
El divisor no puede ser cero
Nótese que, al producirse la excepción en (1), (2) no se ejecuta y pasa a ejecutarse (3)
inmediatamente.
try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
except ZeroDivisionError:
print('division by zero')
Salida:
a: hihi
Traceback (most recent call last):
File ".../test.py", line1, in <module>
a = int(input('a: '))
ValueError: invalid literal for int() with base 10: 'hihi'
Un bloque try puede tener múltiples bloques except. Esto resulta útil cuando el código
contenido en el bloque try puede producir diferentes excepciones:
try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
except ValueError:
7
print('input numbers must be integers')
except ZeroDivisionError:
print('division by zero')
Cuando ocurre una excepción, es manejada por el bloque except que le corresponda. Si
no hay ningún bloque que corresponda con la excepción, el programa terminará al no
poder gestionarla.
try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
except:
print('some exception occured')
try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
except ValueError:
print('input numbers must be integers')
except ZeroDivisionError:
print('division by zero')
except:
print('some other type of exception was raised ')
try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
except (ValueError, ZeroDivisionError):
print('both numbers must be integers and the second one can not be
zero')
except:
print('some other type of exception was raised')
Las excepciones son objetos que representan diferentes tipos de errores. Los objetos
creados como resultado de una situación excepcional (las excepciones) contienen
información acerca del error. Se puede acceder a esa información creando una variable
para hacer referencia a la excepción, como muestra el siguiente ejemplo:
try:
a = int(input('a: '))
b = int(input('b: '))
8
print(a // b, a % b)
except Exception as err:
print('handling an exception: ', err) # Se muestra el valor como
string del objeto err
print(type(err)) # Podemos conocer su tipo
print(err.args) # El resto de la
información está en forma de tupla
Salida:
a: 5
b: 0
handling an exception: integer division or modulo by zero
<class 'ZeroDivisionError'>
('integer division or modulo by zero',)
Exception es un tipo de datos que representa una excepción genérica de la que derivan
todos los otros tipos de excepciones.
Bloques adicionales
Hemos visto que un bloque try puede ir acompañado de varios bloques except, cada uno
de los cuales puede controlar una o varias excepciones. También podemos tener un
bloque except genérico que maneje cualquier excepción no tratada previamente.
Además, detrás de los bloques except, podemos incluir un bloque else. El código en el
bloque else, si el código en el bloque try no produce ninguna excepción, es un buen
lugar para poner código que no necesite la protección del bloque try.
También podemos añadir un bloque finally. El bloqe finally se usa para poner el código
que deba ejecutarse siempre, independientemente de que el bloque try lance, o no, una
excepción.
try:
a = int(input('a: '))
b = int(input('b: '))
c = (a // b, a % b)
except ValueError:
print('input numbers must be integers')
except ZeroDivisionError:
print('division by zero')
else:
print(c)
finally:
print('this is always printed out')
a: 10
b: 2
5 0
this is always printed out
9
a: 10
b: 0
division by zero
this is always printed out
El bloque finally se usa, generalmente, para liberar recursos (por ejemplo, cerrar
ficheros o conexiones de red).
Muchas de las funciones y métodos estándar que usamos en nuestros programas pueden
producir excepciones de diferentes tipos. Por ejemplo, el método index() que usamos
para buscar una substring en una string:
my_string = 'Python'
input_string = input('> ')
print(my_string.index(input_string))
> thon
2
> a
Traceback (most recent call last): File ".../test.py", line 3, in
<module>
print(my_string.index(input_string))
ValueError: substring not found
try:
my_string = 'Python'
input_string = input('> ')
print(my_string.index(input_string))
except ValueError as err:
print(err) #substring not found
import math
def circle_content(radius):
return math.pi * radius ** 2
10
print(circle_content(1)) # 3.141592653589793
print(circle_content(-2)) # 12.566370614359172
import math
def circle_content(radius):
if radius < 0:
raise ValueError('The radius of a circle cannot be a negative
number.')
return math.pi * radius ** 2
La sentencia raise se usa para generar una excepción cuando se detecta una situación
anormal. Si el código puede encontrarse distintas situaciones anómalas, puede incluir
varias sentencias raise.
import math
def circle_content(radius):
if not(type(radius) == int or type(radius) == float):
raise TypeError('The radius must be a number')
if radius < 0:
raise ValueError('The radius of a circle cannot be a negative
number.')
return math.pi * radius ** 2
Podemos controlar las excepciones lanzadas por nuestro código igual que las lanzadas
por las funciones predefinidas, usando bloques try/except:,
try:
print(circle_content(1)) # Se muestra 3.141592653589793
print(circle_content(-2)) # Se produce la excepción
except ValueError as err:
print(err) # Se muestra: The radius of a circle
cannot be a negative number.
except TypeError as err:
print(err) # No se ejecuta nunca al no darse el
caso en este programa
11
La sentencia raise puede usarse sola, sin especificar ningún tipo de excepción:
raise
Esta notación se usa para propagar una excepción recibida hacia afuera en el código
(hacia el código que contiene el bloque try/except o la función que llamó a ese código).
Cuando un bloque except recibe una excepción puede:
def read_number():
n = 0
while True:
try:
return int(input('your number: '))
except ValueError:
n += 1
print('the input is not an integer!')
if n >= 3:
raise
try:
read_number()
12
except ValueError:
print('program terminated by input error')
Resultado:
your number: a
the input is not an integer!
your number: a
the input is not an integer!
your number: a
the input is not an integer!
program terminated by input error
Asertos
Un aserto es una sentencia que establece una condición que debe ser cierta en un punto
determinado de un programa para poder continuar su ejecución. Por ejemplo, si vamos a
dividir dos números, justo antes de realizar la división debe cumplirse que el divisor no
sea cero.
def average(items):
assert len(items) != 0, "Cannot calculate the average of an empty
list"
return sum(items) / len(items)
El ejemplo muestra una función que calcula la media de los elementos de una lista,
pero, para poder hacerlo, la lista no puede estar vacía. El aserto situado justo antes del
cálculo de la media asegura el cumplimiento de esta condición; si no se cumple, se lanza
una excepción AssertionError. El mensaje en el aserto es opcional.
def average(items):
if __debug__:
if len(items) == 0:
raise AssertionError("Cannot calculate the average of an
empty list")
return sum(items) / len(items)
Clases
En Programación Orientada a Objetos (POO), una clase es un modelo para crear objetos
de datos.
13
Las clases representan entidades abstractas o conceptos, mientras que los objetos son
instancias(1) de esas entidades o conceptos. Puede haber muchos objetos de una misma clase.
Por ejemplo, una silla es un concepto que hace referencia a un "asiento con
respaldo, por lo general con cuatro patas, y en que solo cabe una persona". Los
objetos de las fotos son instancias concretas del concepto silla.
(1)
El termino instancia se ha generalizado en POO como traducción del término inglés
"instance", del verbo "to instance", que vendría a significar "concretar o materializar
una idea o concepto", aunque esta acepción difiere del habitual de la palabra "instancia"
en español, que tiene que ver con un tipo de documento jurídico.
En Python, una clase se crea usando la palabra reservada class seguida por el nombre de
la clase:
class MyClass:
"""Optional docstring comment"""
pass
El ejemplo anterior crea una clase vacía llamada MyClass. Como se ve en el ejemplo, la
definición de una clase puede, y debería, incluir un comentario opcional (docstring) para
documentar la clase.
Para instanciar una clase (crear un objeto de esa clase) usamos una expresión
constructora formada por el nombre de la clase seguida de paréntesis, como si fuera una
llamada a una función crea el objeto y lo devuelve:
my_object = MyClass()
En el ejemplo anterior, my_object hace referencia a un objeto de tipo MyClass, es decir, una
instancia de MyClass.
Las clases incluyen, generalmente, una función especial llamada __init__ para
inicializar los objetos de la clase con algunos atributos de datos en el momento de su
creación.
class MyClass:
"""Docstring comment"""
def __init__(self, value1, value2):
self.attr1 = value1
self.attr2 = value2
14
En el ejemplo anterior, MyClass tiene una función __init__ que añade dos atributos
(attr1 y attr2) cuando se crea un nuevo objeto. Estos atributos se inicializan con los
valores que se pasan como segundo y tercer parámetros de la función __init__. El
primer parámetro de __init__ (o sea, self) representa el objeto recién creado, al que se le
añaden los atributos.
Cuando una clase tiene una función __init__, en la expresión constructora se deben
proporcionar valores para todos los parámetros de __init__, excepto el primero, cuando
se crea un nuevo objeto. El primer parámetro se asocia implícitamente con el nuevo
objeto.
o1 = MyClass(1, 2)
o2 = MyClass(3, 4)
El ejemplo anterior crea dos objetos (o1 y o2) de la clase MyClass. Los valores
numéricos 1 y 2 se asignan a los atributos attr1 y attr2 de o1, y los valores 3 y 4 se
asignan a los atributos attr1 y attr2 de o2.
Atributos de un objeto
Una clase define un nuevo tipo de objeto que se caracteriza por un conjunto de atributos
que pueden ser: atributos de datos (variables) o funciones (que se conocen como
métodos). Los atributos de datos se usan para representar el estado del objeto, mientras
que los métodos se usan para implementar el comportamiento del objeto, manipulando
los atributos de datos para examinar o modificar el estado del objeto.
Por ejemplo, si definimos una clase para representar rectángulos, necesitamos dos
atributos: la base y la altura. Un rectángulo podría tener, entre otros, un método para
calcular su área o uno para rotarlo, intercambiando su base y su altura.
Cuando creamos un nuevo objeto de una clase, se inicializa con sus propias copias de
los atributos de datos especificados en la definición de la clase y se le pueden aplicar los
métodos declarados allí.
En el ejemplo del rectángulo, cada nuevo rectángulo tiene sus propias base y altura,
independientes de las de cualquier otro, y a todos se les puede calcular el área o se
pueden rotar.
o1 = MyClass()
o2 = MyClass()
o1.size = 10
o2.length = 5
15
En el ejemplo anterior se crean dos objetos de la misma clase, MyClass (o1 y o2), y a
continuación se asigna el valor 10 al atributo size de o1 y el valor 5 al atributo length de
o2. Ambos atributos, en caso de que no existan ya, se crean con esas asignaciones.
Añadir atributos dinámicamente puede dar lugar a que existan objetos de la misma clase
con atributos y comportamientos diferentes, tal como ocurre en el ejemplo anterior,
pero, si es un modelo para la creación de objetos, una clase debería declarar todo
aquello que todos los objetos de la clase deben tener.
Las operaciones que se usan para inicializar los objetos cuando se crean se conocen
como constructores en terminología POO. En Python, se debe añadir a la definición de
la clase un método especial llamado __init__ para inicializar los objetos. Este método se
ejecuta siempre que se crea un objeto nuevo.
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
Cuando se crea un objeto, en la expresión constructora deben pasarse valores para todos
los parámetros de __init__, excepto para el primero (self) o los que tengan un valor por
omisión.
Métodos
Los objetos tienen métodos que manipulan sus datos. Esos métodos se conocen como
métodos de instancia (instance methods) y se definen en el cuerpo de la clase a la que
pertenece el objeto.
class Rectangle:
"""A quadrilateral with four right angles"""
def __init__(self, length, width):
16
"""Initialize a rectangle with length and width"""
self.length = length
self.width = width
def area(self):
"""Returns the area of the self rectangle"""
return self.length * self.width
En el ejemplo se definen dos métodos para los objetos de la clase Rectangle: el método
especial de inicialización __init__, y un método llamado area.
Self
En Python, los métodos reciben el objeto sobre el que actúan como primer parámetro
formal.
class Rectangle:
"""A quadrilateral with four right angles"""
def __init__(self, length, width):
"""Initialize a rectangle with length and width"""
self.length = length
self.width = width
def area(self):
"""Returns the area of the self rectangle"""
return self.length * self.width
El nombre self se usa por convención, aunque Python admite que ese primer parámetro
pueda tener cualquier otro nombre.
En Python, los métodos que actúan sobre un objeto se invocan como atributos de ese
objeto, usando la notación de punto para vincular el método con el objeto. Es el caso del
método area en el siguiente ejemplo. El método area devuelve 50 cuando se llama
vinculado a r1 y 24 cuando se le llama vinculado a r2 (ambos objetos, r1, y r2, son
creados previamente):
r1 = Rectangle(10, 5)
r2 = Rectangle(3, 8)
print(r1.area()) # prints 50
x = r2.area() # assigns 24 to x
class MyClass:
...
17
def method1(self, a, b):
...
def method2(self, a, b, c):
...
x = self.method1(a, b)
...
Métodos mágicos
El método __init__, que se usa para inicializar objetos, pertenece a un grupo de métodos
especiales de Python conocidos como "métodos mágicos" (magic methods).
class Rectangle:
def __init__(self, length, width):
"""Initialize a rectangle with length and width"""
self.length = length
self.width = width
Los métodos mágicos se usan para dotar a las clases de características especiales. Hay
métodos para inicializar objetos, para obtener la representación de un objeto en forma
de string, para redefinir los operadores aritméticos y poder usarlos con una nueva clase
de objetos, etc.
Los nombres de los métodos mágicos están predefinidos y empiezan y terminan con
dos guiones bajos ("__"). Son llamados, generalmente, de forma implícita al realizar
una operación (nótese que para crear un objeto no se llama directamente al método
__init__, se escribe una expresión constructora con los parámetros que necesita e
__init__ se llama automáticamente como parte del proceso interno de creación del
objeto).
r1 = Rectangle(10, 5)
La sintaxis de dobles guiones bajos "__*__ " está reservada para los métodos mágicos,
aunque en las versiones actuales de Python podríamos usarla para dar nombre a otros
métodos. No se aconseja porque podría no funcionar en versiones futuras sin ningún
aviso.
Herencia
Las nuevas clases definidas de esta manera son clases derivadas, o subclases, de la clase
de la que derivan, que se conoce como clase base o superclase.
18
Las subclases heredan las características y el comportamiento de sus superclases.
Generalmente, cambian (override) algunos aspectos de las características y
comportamiento heredados e introducen otros nuevos. El principal beneficio de la
herencia es la reutilización del código: se aprovechan las características de la clase base
sin tener que volver a replicarlas.
Un ejemplo de la vida real: un ave es una clase de animal. Las aves tienen pico, dos
patas, alas (características) y, generalmente vuelan (comportamiento). Los avestruces,
los canarios y los pingüinos son tipos de aves. Pertenecen a clases que son subclases de
la clase ave. Todos ellos tienen pico, dos patas y alas, los pingüinos no vuelan pero
nadan como si "volaran" bajo el agua (modificación, override, del comportamiento).
Los avestruces tampoco vuelan, pero corren mejor que la mayoría de las aves, y que
otros muchos animales. Por su parte, los canarios sí pueden volar, y, además cantan
muy bien (característica añadida).
La clase object
En Python 3.x, todas las clases heredan de forma implícita de una clase base común
llamada object.
Cuando escribimos un método de inicialización para una clase derivada, tenemos que
llamar al inicializador de la superclase. Lo hacemos usando el prefijo "super()" para
hacer referencia a la superclase:
class ClassOne():
def __init__(self, attr1_value):
self.attr1 = attr1_value
...
class ClassTwo(ClassOne):
def __init__(self, attr1_value, attr2_value):
super().__init__(attr1_value)
self.attr2 = attr2_value
...
19
Atributos de las subclases
Un objeto de una clase derivada es también un objeto de la clase base de la que esa clase
deriva. El objeto tiene, tanto los atributos de instancia definidos en la subclase, como los
definidos en la superclase; sin embargo, solo puede acceder a los atributos públicos de
la superclase.
class ClassOne:
def __init__(self, attr1_value):
self.__attr1 = attr1_value
def get_attr1(self):
return self.__attr1
class ClassTwo(ClassOne):
def __init__(self, attr1_value, attr2_value):
super().__init__(attr1_value)
self.__attr2 = attr2_value
def sum_attributes(self):
return self.get_attr1() + self.__attr2
class ClassOne:
def __init__(self, attr1_value):
...
class ClassTwo(ClassOne):
def __init__(self, attr1_value, attr2_value):
...
c1 = ClassOne(1)
c2 = ClassTwo(1, 2)
Clases abstractas
20
Una clase abstracta es una clase que contiene métodos abstractos, que son métodos
declarados pero no implementados. En Python, las clases abstractas deben derivar de la
clase ABC (Abstract Base Class) y los métodos abstractos deben estar marcados con el
decorador @abstractmethod. En el siguiente ejemplo se declara la propiedad area
usando un método abstracto. Nótese que el mero hecho de que el método contenga solo
la instrucción pass no lo convierte en abstracto:
class Shape(ABC):
@property
@abstractmethod
def area(self): pass
No pueden crearse objetos de una clase abstracta. Las clases abstractas solo sirven para
definir un conjunto de métodos a modo de "contrato" al que otras clases se pueden
adherir declarándose herederas de la clase abstracta, con lo que se obliga a implementar
todos los métodos abstractos declarados en la misma. La clase Square del siguiente
ejemplo es una subclase concreta de la clase abstracta Shape, y puede instanciarse:
class Square(Shape):
def __init__(self, side):
self.__side = side
@property
def area(self):
return self.__side ** 2
Una subclase de una clase abstracta que no implemente todos los métodos abstractos
declarados en aquella sigue siendo abstracta y no puede instanciarse.
Una clase abstracta puede contener métodos no abstractos, así como otros atributos de
datos.
Polimorfismo
Los operadores son un ejemplo típico de polimorfismo; por ejemplo, el mismo símbolo
'+' se puede usar para sumar dos números o concatenar dos strings, obteniendo el
resultado adecuado a cada caso:
x = 5 + 10; # 15
greetings = "Hello " + "world" # "Hello world"
21
class BaseClass:
def message(self):
return "This is a message from BaseClass";
class ClassOne(BaseClass):
def message(self):
return "This is a message from ClassOne";
class ClassTwo(BaseClass):
def message(self):
return "This is a message from ClassTwo";
Y esta función:
def showMessage(obj):
if isinstance(obj, BaseClass):
print(obj.message())
Si pasamos a la función un objeto de cualquiera de las tres clases,
siempre obtendremos el resultado apropiado:
obj1 = BaseClass()
obj2 = ClassOne()
obj3 = ClassTwo()
En Python, para sustituir un método, una subclase solo necesita tener un método con el
mismo nombre, el cual oculta al correspondiente de la superclase.
class BaseClass:
def message(self):
return "This is a message from BaseClass";
class ClassOne(BaseClass):
def message(self):
return "This is a message from ClassOne";
class ClassOne(BaseClass):
def message(self):
return super().message() + " and then a message from ClassOne"
#"This is a message from BaseClass and then a message from ClassOne"
22
El polimorfismo y los métodos mágicos
En Python, el polimorfimo se usa con frecuencia con los métodos mágicos. Por
naturaleza, un método mágico es un método especial que una clase puede sustituir para
dar un comportamiento adecuado a sus objetos.
Las clases nuevas casi siempre sustituyen el método __init__ para realizar su propia
inicialización, o el método __str__ para proporcionar una representación como string
adecuada. Cualquier método mágico se puede sustituir.
class ClassOne(object):
def __init__(self, value):
self.attr1 = value
class ClassTwo(ClassOne):
def __init__(self, value_1, value_2):
super().__init__(value_1)
self.attr2 = value_2
Python tiene muchos tipos de excepciones predefinidas, sin embarpo, hay situaciones en
las que necesitamos definir un nuevo tipo de excepción para indicar con precisión un
error que puede surgir al ejecutar un programa en el contexto de un problema
específico. Para definir un nuevo tipo de excepción, creamos una clase que herede,
directa o indirectamente, de la clase predefinida Exception:
class MyOwnExceptionError(Exception):
pass
En Python, las clases que representan excepciones suelen recibir nombres que terminan
con la palabra "Error", a semejanza de las excepciones predefinidas (TypeError,
ValueError, AttributeError,...). Una vez creada su clase, la nueva excepción puede
lanzarse cuando haga falta usando la sentencia raise, igual que para las excepciones
predefinidas:
if condition:
raise MyOwnExceptionError()
if condition:
raise MyOwnExceptionError("Error message")
23
Ajuste de las nuevas excepciones
Una clase de excepción, como cualquier otra clase, puede incluir atributos de datos y
métodos, generalmente, con el propósito de proporcionar información adicional sobre la
excepción.
class MyOwnExceptionError(Exception):
def __init__(self, obj, message):
super().__init__(message)
self.obj = obj
Aunque esto puede ser útil, suele ser aconsejable mantener la definición de la excepción
lo más simple posible.
Jerarquías de excepciones
class DateError(Exception):
"""Base class for erroneus dates"""
pass
class MonthError(DateError):
"""Raised when you try to create a date with a month value
that is not between 1 and 12
"""
pass
class DayError(DateError):
"""Raised when you try to create a date with a day value which
does not match
with the month value
"""
En el ejemplo, hemos creado una excepción llamada DateError que hereda de la clase
Exception y sirve de base para nuestra propia jerarquía, formada por las excepciones
MonthError y DayError. Esta jerarquía está pensada para su uso en un módulo que
ofrezca una clase para manejar fechas.
Sobrecarga de operadores
En Python la sobrecarga de operadores para que puedan operar con objetos de nuevas
clases se hace definiendo determinados métodos mágicos, los cuales se pueden
clasificar como:
24
• Operadores de comparación
• Funciones y operadores unarios
• Operadores aritméticos binarios
• Operadores aritméticos reflejados
Ejemplo:
class Rational:
def __init__(self, num_value, den_value):
self.num = num_value
self.den = den_value
def __eq__(self, other):
if type(other) == Rational:
return self.num * other.den == self.den * other.num
else:
return False
...
r1 = Rational(3, 4)
r2 = Rational(6, 8)
print(r1 == r2) # Muestra True
Nótese la pregunta en la implementación del método __eq__ para saber si el objeto con
el que se está comparando es un Rational; solo en ese caso se puede acceder a sus
atributos num y den para compararlos. En cualquier caso, si el otro objeto no es un
Rational, el resultado es False (dos objetos de diferentes tipos no pueden ser iguales).
Ordenación total
25
No es necesario implementar todos los métodos mágicos de comparación para poder
usar correctamente todos los operadores relacionales. Si añadimos a la clase el
decorador @functools.total_ordering, basta con implemenar el operador __eq__ y uno
de los métodos de comparación enriquecida __lt__, __le__, __gt__, __ge__. El
decorador crea el resto.
Ejemplo:
@total_ordering
class Rational:
def __init__(self, num_value, den_value):
self.num = num_value
self.den = den_value
Ejemplo:
class Rational:
def __init__(self, num_value, den_value):
26
self.num = num_value
self.den = den_value
def __neg__(self):
return Rational(-self.num, self.den)
...
r1 = Rational(3, 4)
r2 = -r1 # Equivale a r2 = Rational(-3, 4)
Ejemplo:
class Rational:
def __init__(self, num_value, den_value):
self.num = num_value
self.den = den_value
...
def __add__(self, other):
if type(other) == Rational:
return Rational(
self.num * other.den + self.den * other.num,
self.den * other.den
)
elif type(other) == int:
return self + Rational(other, 1)
Los métodos mágicos para los operadores reflejados están pensados para cuando se usa
un operador con sus operandos "intercambiados", es decir, other <op> self en vez de
self <op> other. Se aplican cuando los operandos son de tipos diferentes y el operando
de la izquierda no tiene definido el correspondiente método mágico para el operador.
27
Los nombres de los métodos mágicos para los operadores reflejados son los mismos que
para los operadores normales correspondienres, pero precedidos de la letra 'r'. La
versión reflejada del operador __add__(self, other), es el operador __radd__(self,
other).
Ejemplo:
class Rational:
def __init__(self, num_value, den_value):
self.num = num_value
self.den = den_value
...
def __add__(self, other):
if type(other) == Rational:
return Rational(
self.num * other.den + self.den * other.num,
self.den * other.den
)
elif type(other) == int:
return self + Rational(other, 1)
Python no permite tener dos funciones con el mismo nombre en el mismo espacio de
nombres, pero permite el uso de parámetros opcionales, con un valor definido por
omisión. Además, Python no requiere declarar el tipo de los parámetros de una función,
por lo que una función puede estar preparada para mostrar un comportamiento en
función del tipo de los parámetros reales que se le pasen.
28
El siguiente ejemplo muestra una clase, Parallelogram, con un inicializador polimórfico
capaz de crear cualquiera de entre cuatro posibles clases de paralelogramos (cuadrado,
rectángulo, rombo y romboide). Se supone que todos los parámetros son números de
tipo int o float.
class Parallelogram:
def __init__(self, side1, angle = 90, side2 = None):
self.side1 = side1
if angle != 90:
self.kind = "rhomboid"
self.angle = angle
else:
self.kind = "rectangle"
square = Parallelogram(12)
rhombus = Parallelogram(12, 60)
rectangle = Parallelogram(12, side2 = 8)
rhomboid = Parallelogram(12, 60, 8)
Iteradores
El siguiente ejemplo, implementa una clase que permite recorrer una secuencia de
números dentro de un rango (star:stop) y con un incremento (step) establecidos.
class MyRange():
def __init__(self, start=0, stop=0, step = 1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current <= self.stop:
result = self.current
self.current += self.step
return result
else:
raise StopIteration
Una ventaja importante de los iteradores es que podemos tratar grandes conjuntos de datos,
elemento por elemento, sin necesidad de gastar una gran cantidad de memoria almacenando
el conjunto completo.
Podemos usar un iterador en cualquier sitio donde se requiera un objeto iterable, como
en un bucle for:
30
Realmente, funciona como se ve a continuación:
while True:
try:
i = next(iter_obj) # Se obtiene el siguiente valor
print(i)
except StopIteration:
break # Si se lanzó la excepción StopIteration se interrumpe
el bucle
Generadores
En Python, los generadores son mecanismos simples para definir iteradores. Tienen la
apariencia de una función normal, con la diferencia de que la sentencia return es
sustituida por la sentencia yield.
while True:
try:
i = next(iter_obj)
print(i)
except StopIteration:
break
Los generadores se pueden usar en cualquier sitio donde se pueda usar un iterador.
Prueba de programas
31
tarde se detecte un defecto, más costoso será repararlo). El proceso de prueba se basa en
comparar el comportamiento real del programa con el esperado al aplicarlo a diversos
casos de prueba. Cuando estos comportamientos difieren, se ha detectado la existencia
de un posible defecto cuyas causas se determinarán mediante técnicas de depuración
Una prueba tiene éxito si detecta un defecto. Una prueba es mejor que otra cuando tiene
más posibilidades de detectar un defecto. Las pruebas que no detectan ningún defecto
sólo demuestran que el programa funciona bien para los casos probados, pero no que el
programa no tenga defectos. En general, es impracticable realizar pruebas exhaustivas
de todos los casos posibles, dado el número de posibles combinaciones de entradas de
un programa.
Las pruebas tienen mayor probabilidad de éxito si son realizadas por programadores que
no han participado en el desarrollo del programa y están, por tanto, libres de prejuicios.
Casos de prueba
Para probar un caso, se ejecuta el código pasándole los datos de entrada y se comparan
los resultados con los esperados. Se deben probar desde datos de entrada válidos y
esperados hasta no válidos e inesperados.
Por ejemplo, si se tiene una clase para representar números racionales (compuestos por
una pareja de números reales, llamados numerador y denominador, tales que el
denominador no puede ser cero), se debe probar que la expresión constructora acepta
denominadores distintos de cero (válido y esperado), pero también se debe probar que
no acepta que el denominador sea cero (no válido y no esperado).
No se debe dejar de probar casos por suponer a priori que el programa es correcto para
esos casos.
Las metodologías clásicas de la Ingeniería del Software contemplan las pruebas como
una etapa en el desarrollo del software, a realizar después de la etapa de programación
(las etapas suelen ser: análisis de requisitos, diseño y arquitectura, programación,
pruebas, documentación y mantenimiento). Las metodologías ágiles y de desarrollo
extremo más recientes abogan por el diseño guiado por pruebas (Test Driven Design, o
TDD), es decir, preparar primero las pruebas y realizar luego la programación que las
satisfaga.
Las pruebas se diseñan para descubrir si el programa no hace lo que debería o para
descubrir si el programa hace lo que no debería. Las pruebas no deben ser redundantes.
El tiempo es un recurso muy valioso y no hay motivo para perderlo repitiendo pruebas
para un mismo caso. Cada prueba debe tener un objetivo distinto.
32
Las pruebas deben realizarse de modo ascendente: primero se deben probar las unidades
independientes más básicas, como funciones y clases (Pruebas de unidades o Pruebas
unitarias), y después, la integración de esas unidades, ya probadas, para formar otros
componentes más complejos (Pruebas de integración), subiendo el nivel de
complejidad hasta probar el programa completo.
Tipos de prueba
Las pruebas de caja blanca, que se basan en el conocimiento del código y persiguen
probar todos los posibles caminos de ejecución consiguiendo la total cobertura del
código. Por ejemplo, si el código tiene una estructura if: else:, hay que diseñar casos
para que se ejecute la parte if: y casos para que se ejecute la parte else:, de forma que
todas las instrucciones del programa se ejecuten al menos una vez.
33
También se habla de pruebas de caja gris, que se diseñan como las de caja negra, sin
un conocimiento exhaustivo de los detalles de implementación del código, pero se
aprovechan del uso de herramientas de prueba de cobertura, para mejorar las pruebas.
El objetivo de las pruebas de caja blanca es probar todos los caminos diferentes por los
que pasa el flujo de ejecución de un programa. El número de caminos aumenta
exponencialmente con el número de condiciones y bucles.
Existen varias metodologías de pruebas de caja blanca. La llamada Prueba del Camino
Básico sigue los siguientes pasos:
Por ejemplo, de la función para calcular la multiplicación de dos números naturales por
el método de la multiplicación rusa que se muestra a la izquierda, se obtiene el grafo del
flujo de ejecución de la derecha. (El método de multiplicación rusa consiste en
multiplicar sucesivamente por 2 el multiplicando y dividir por 2 el multiplicador hasta
que el multiplicador tome el valor 1. Luego, se suman todos los multiplicandos
correspondientes a los multiplicadores impares).
34
El grafo muestra los diferentes caminos que puede seguir la ejecución, en función de los
datos de entrada. Los vértices 2 (correspondiente a la condición de la instrucción while)
y 3 (correspondiente a la condición de la instrucción if) son vértices predicado.
35
Complejidad ciclomática y cálculo de los caminos básicos
El grafo del flujo de ejecución de un algoritmo nos muestra los diferentes caminos de
ejecución que pueden darse en función de los datos de entrada:
Para diseñar las pruebas de caja blanca, nos interesa determinar el conjunto de caminos
básicos diferentes que hay, entendiendo por conjunto de caminos básicos el conjunto
mínimo de caminos que garantiza que cada arco del grafo aparece al menos en un
camino. La forma sencilla de encontrar dichos caminos es empezar con el camino más
corto que vaya de principio a fin:
• 1->2->6
El siguiente camino se construye usando el primero nodo predicado y tomando ahí una
alternativa no usada anteriormente para construir un nuevo camino, lo más corto
posible, hasta el fin:
• 1->2->3->5->2->6
• 1->2->3->4->5->2->6
Una vez encontrados los caminos básicos, se pasa a preparar pruebas seleccionando
conjuntos de entrada para garantizar la ejecución de cada uno de ellos. En el ejemplo:
Hemos encontrado tres cáminos básicos. La forma de diseñar los casos de prueba es
componiendo conjuntos de datos de prueba que cumplan la secuencia de condiciones
representadas por los arcos salientes de los nodos predicados en cada camino básico.
No existe ningún conjunto de datos de entrada que permita recorrer el camino 1->2->3-
>5->2->6 tal cual: para recorrerlo, hace falta que num1 sea mayor que cero, para entrar
en el while, pasando del vértice 2 al 3, pero también que sea par, para pasar del vértice 3
al 5 saltándose el 4 y yendo luego del 5 al 2. El problema es que, al pasar por el vértice
5, cualquier número par se dividiría por dos, dando lugar a un número más pequeño,
pero mayor que cero, por lo que no se podría pasar del 2 al 6, sino que se volvería a
entrar en el while, pasando del nodo 2 al 3 y luego al 4 o al 5, en función de si num1 es
par o impar, de forma repetida, hasta que alcance el valor 1, momento en que se finaliza
con la secuencia 2->3->4->5>-2->6, dado que al dividir uno entre 2, al pasar por el
vértice 5, se convierte en cero y permite ejecutar la transición 2->6.
37
Por lo tanto, el camino mínimo que comienza con la secuencia 1->2->3->5 es: 1->2->3-
>5->2->3->4->5->2->6; este camino se da cuando num1 es igual a 2.
La siguiente tabla tiene todo esto en cuenta para proponer posibles conjuntos de prueba:
El objetivo es encontrar las entradas cuya probabilidad de causar un fallo sea lo más alto
posible. Se pretende comprobar que la funcionalidad del software es la especificada. Es
impracticable estudiar todas las posibles entradas y salidas, por lo que hay que
seleccionarlas; para ello existen dos técnicas:
• Clases de equivalencia
• Prueba de los valores límite
Clases de equivalencia
Es una técnica que consiste en dividir las posibles entradas de la unidad a probar
(programa, función, método, ...) en particiones (subconjuntos disjuntos) de los que se
pueden derivar los casos de prueba.
38
Las particiones se forman reuniendo las entradas para las que la unidad tiene un
comportamiento equivalente. Los casos de prueba se diseñan de forma que se pruebe al
menos un elemento de cada partición.
Atendiendo a las posibles longitudes del valor pasado, tenemos tres clases de
equivalencia en este problema:
Supongamos que estamos desarrollando una clase para representar números racionales
que tiene dos propiedades: numerador y denominador y queremos probar el método
inicializador, que tendría dos parámetros, aparte de self: uno para inicializar el
numerador y otro para inicializar el denominador.
Las clases de equivalencia para las pruebas vendrían dadas por la combinación de las
clases de equivalencia de cada parámetro:
• El numerador podría ser cualquier número entero; tendría una única clase de
equivalencia formada por el conjunto de los enteros.
• El denominador podría ser cualquier número entero distinto de cero. Esto nos da dos
clases de valores válidos: los negativos y los positivos. ¿Qué ocurre con el cero?
Las clases de equivalencia no válidas se dan porque el tipo de datos usado en el código
tiene un dominio (rango de valores) mayor que el dominio del problema en el "mundo
real".
39
Por ejemplo, en el problema de comprobar la longitud de una password, teníamos tres
clases de equivalencia válidas: las strings con longitud entre 0 y 7 caracteres, las strings
con entre 8 y 15 caracteres y las strings con más de 15 caracteres. No hay posibilidad de
una clase de equivalencia no válida porque esto cubre todas las posibles strings, ya que,
en el "mundo real" o en cualquier lenguaje de programación, no existen strings con
longitud negativa.
La función factorial en matemáticas está definida para los números naturales. Hay
lenguajes de programación en los que existe un tipo natural, unsigned int o similar; si el
parámetro de la función factorial se restringe a este tipo, no hay posibilidad de que se le
pase a la función "informática" un valor fuera del dominio de la función "matemática",
pero, en muchos lenguajes, se usa el tipo int, que incluye los naturales, pero también los
enteros negativos, que constituyen una clase no válida para el factorial.
Las pruebas deben tener en cuenta, tanto las clases de equivalencia válidas, como las
inválidas, pero solo pueden probar aquello que esté contemplado en las
especificaciones, que muchas veces se hacen teniendo en cuenta el problema del
"mundo real" y no el contexto informático. Qué hacer con las clases de equivalencia no
válidas depende de dichas especificaciones.
La ventaja de las pruebas de caja negra es que se pueden diseñar antes de comenzar el
desarrollo del código a probar, lo que puede permitir la detección anticipada de lagunas
en la especificación.
Estas lagunas deben resolverse: puede especificarse que se lance una excepción
determinada (por ejemplo, ValueError cuando se intenta inicializar el denominador de
un número racional con el valor cero), que se devuelva un valor determinado (por
ejemplo, None, cuando se intenta calcular el factorial de un número negativo), o no
definir el resultado, responsabilizando al usuario del código del buen uso del mismo. En
este último caso, no hay nada que probar, ya que para realizar una prueba hay que saber
cuál es el valor esperado para el dato que se prueba.
El diseñar las pruebas antes de implementar el código forma parte de una técnica de
desarrollo de software conocida como Test Driven Development (desarrollo guiado por
pruebas o TDD), cuyo propósito es lograr un código limpio que funcione: la idea es
programar primero las pruebas, que no funcionarán, desarrollar luego el código para que
funcionen y, finalmente, refactorizar este código, optimizándolo.
Es muy frecuente que las clases de equivalencia tomen la forma de un rango: por
ejemplo, en el problema de determinar si una string tiene la longitud adecuada para ser
una password, tenemos tres clases de equivalencia que vienen determinadas por tres
rangos para las posibles longitudes de las strings:
40
[0 ·· 7] [8 ·· 15] [16 ·· ∞]
Pero las clases de equivalencia son conjuntos de datos, que pueden no constituir un
rango. Por ejemplo, podemos establecer que los colores de un semáforo son
rojo, amarillo o verde.
La teoría de las clases de equivalencia es que, dado que el código a probar se debe
comportar igual para todos los valores de una clase, basta con probar un valor
cualquiera de la clase, ya que se supone que el código funcionará igual para todos los
valores de la clase.
Sin embargo, es un hecho probado que, cuando las clases de equivalencia se definen
como un rango de valores, la mayoría de los errores se producen en torno a los valores
extremos, o límites, de la clase.
0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 | 16 17 18 ···
Los rangos abiertos, como la tercera clase del ejemplo, no existen realmente en
informática, ya que, en un ordenador, siempre hay un límite máximo a lo que se puede
41
representar. Algunos lenguajes, tienen esos límites definidos a priori (mínimo y máximo
valor entero, mínimo y máximo valor real, longitud máxima de una string, ...) por lo que
podremos tratarlos como rangos cerrados. No es el caso de Python, por lo que, en este
caso, para la tercera clase probaríamos los valore 16, 17 y alguna(s) string de una
longitud razonablemente más alta.
Los rangos de números reales presentan un problema a la hora de identificar los límites
exactos de una clase.
Por ejemplo, si queremos desarrollar una función para obtener el área de un círculo a
partir de su radio, tenemos que la clase de valores válidos para el radio de un círculo
está formada por el rango de los números reales mayores que 0.0, pero, ¿cuál es el
primer valor real mayor que 0.0? y, ya puestos, ¿cuál es el segundo?
Matemáticamente hay infinitos números reales entre cada pareja de números reales,
pero en un ordenador el número de valores distintos que se pueden representar es finito.
Cuando se trata de una representación de números reales en coma flotante, son muchos
los valores que no se pueden representar y se asimilan al valor más próximo
representable, introduciendo errores de redondeo.
Todo se reduce, en última instancia, a la precisión con la que necesitemos plantear las
pruebas (en función de lo crítico que resulte tener un mayor o menor error). En un caso
como el planteado, podría ser suficiente con probar los valores 0.5 y 1.0, o, a lo mejor,
0.00001 y 0.00002, si necesitamos mayor precisión. Una pista nos la puede dar conocer
la unidad en la que, en el problema, se supone que está representado el radio ¿metros,
centímetros, milímetros, micras, ...?
if esperado == obtenido:
...
Prueba de contenedores
42
otros lenguajes son también ejemplos de contenedores. Las pruebas de código en que
intervienen contenedores se complican un poco respecto a aquellas en que solo aparecen
tipos simples.
Supongamos que queremos desarrollar una función para contar el número de veces que
aparece un valor en una lista (lo mismo que hace el método predefinido .count()).
Para las listas de tamaño 1, tenemos dos casos: que el único elemento coincida con el
valor a buscar o que no coincida.
Para las listas de tamaño mayor, nos planteamos cuántas veces puede aparecer el valor a
contar, lo que, para una lista de tamaño n, es entre 0 (no aparece) y n (todos los
elementos coinciden). Atendiendo a los valores límite, tenemos que probar:
Pero, para los casos en que tenemos un número de apariciones menor que el tamaño de
la lista, tenemos que considerar otra característica, la posición que ocupa. Los extremos
de una secuencia son límites en sí mismos (no haríamos esa consideración para
contenedores que no sean secuencias, ya que no presentan extremos).
En una lista de tamaño n en la que el elemento a buscar aparezca una sola vez, puede ser
el primero, el último, o estar en una posición intermedia.
En una lista de tamaño n en la que el elemento a buscar aparezca n−1 veces, el elemento
diferente puede ser el primero, el último o uno intermedio.
Además, fuera de los valores extremos, deberíamos probar una lista con n elementos en la que
el valor a buscar aparezca distribuido un número intermedio de veces (digamos,
arbitrariamente, entre n/3 y 2n/3).
Prueba de clases
43
Una clase se prueba a través del comportamiento de los objetos de la clase y de la
propia clase. Este comportamiento viene determinado por los métodos, que son
funciones.
Por ejemplo, si desarrollamos una clase para manejar números racionales, y queremos
probar el método que realiza la suma de dos números racionales, tendremos, antes de
llamar al método que realiza la suma, que inicializar los dos números que queramos
sumar, por lo que dependemos de que el método de inicialización funcione
correctamente.
Una vez que los métodos más básicos están implementados y probados, desarrollamos
pruebas para los que dependen de ellos y los implementamos a su vez, y así
continuamos hasta tener implementada y probada la clase completa.
Para realizar una prueba, básicamente hay que ejecutar la función a probar pasándole la
entrada del caso a probar y comparar el resultado obtenido con el esperado para ese
caso. Lo habitual es usar la técnica conocida como "Una función, una prueba", que
consiste en implementar una función diferente para cada caso de prueba.
Suponiendo que se quiere probar una función llamada sum_evens que debe devolver la
suma de los números pares contenidos en una lista de números enteros que se le pasa
como parámetro, las siguientes funciones ilustran la forma de probar "manualmente" el
caso de que la entrada sea una lista que no contiene ningún número par:
Ejemplo 1:
def test_no_evens():
"""Una lista sin números pares"""
result = sum_evens([1, 3, 5, 7])
44
if result != 0:
raise Exception("Si no hay números pares el resultado debe ser
0")
Ejemplo 2:
def test_no_evens():
"""Una lista sin números pares"""
result = sum_evens([1, 3, 5, 7])
assert result == 0, "Si no hay números pares el resultado debe ser
0"
En el primer caso se usa una sentencia if para comprobar que el resultado obtenido
coincide con el esperado, y en el segundo, más habitual, un aserto.
Podríamos programar tantas funciones de este estilo como casos de prueba y llamarlas
desde un programa principal para probar los distintos casos:
if __name__ == "__main__":
test_no_evens()
...
print("Pruebas terminadas")
Pero dado que cuando se detecta el fallo de un caso se lanza una excepción, el programa
solo ejecutaría hasta el primer caso que fallase, a menos que lo compliquemos con un
intricado control de excepciones.
Unittest
• test fixture.- representa la preparación necesaria para realizar una o más pruebas y
cualquier acción de limpieza asociada. Esto puede implicar, por ejemplo, crear bases
de datos temporales o proxy, directorios o iniciar un proceso de servidor.
• test case.- un caso de prueba es la unidad individual de prueba. Comprueba una
respuesta específica a un conjunto particular de entradas. unittest proporciona una
clase base, TestCase, que puede usarse para crear nuevos casos de prueba.
• test suite.- una colección de casos de prueba, conjuntos de pruebas o ambos. Se utiliza
para agregar pruebas que deben ejecutarse juntas.
• test runner.- corredor de pruebas, herramienta diseñada para organizar la ejecución
de un conjunto de pruebas y proporcionar un informe del resultado de las mismas,
45
detallando los casos fallados y superados.
TestCase
El framework unittest ofrece una clase llamada TestCase como clase base para
implementar casos de prueba. Para hacerlo, hay que crear una clase heredera de
TestCase e implementar cada caso de prueba como un método cuyo nombre debe
empezar por el prefijo "test".
import unittest
from functions import sum_evens
class SumEvensTest(unittest.TestCase):
def test_no_evens(self):
"""Una lista sin números pares"""
result = sum_evens([1, 3, 5, 7])
self.assertEqual(result, 0, "Si no hay números pares la suma
debe ser 0")
def test_all_evens(self):
"""Una lista en la que todos los números son pares"""
result = sum_evens([2, 6, 4, 8])
self.assertEqual(result, 20, "La suma no coincide")
if __name__ == "__main__":
unittest.main()
Podemos implementar tantos métodos de prueba como sea necesario, siempre con la
política "un método, una prueba". Como se observa en el ejemplo, sustituimos la
cláusula assert estándar de Python por un método de aserto propio de la clase TestCase
(.assertEqual() en el ejemplo). Este método tiene tres parámetros: los dos valores a
comparar (el resultado obtenido y el esperado) y un tercero, opcional, con un mensaje
explicativo.
Como también se puede ver en el ejemplo, para ejecutar las pruebas solo se requiere
llamar al método .main() de la clase unittest. La ejecución de este método de clase
desencadenará la ejecución de todos los tests definidos en el módulo "__main__". Los
test se ejecutan en orden alfabético.
Informe de pruebas
46
La primera línea (1) contiene una secuencia de puntos y letras. Los puntos representan
un test ejecutado con éxito. Las letras pueden ser 'F', por "failure", o 'E', por "error". Un
fallo es el incumplimiento de un aserto. Un error es una excepción inesperada durante la
ejecución de un test.
A continuación viene una secuencia de bloques de información sobre cada test fallido o
erróneo, un bloque por cada uno. La primera línea del bloque (2) identifica el test e
indica si se ha producido un fallo o un error.
Seguidamente (3), viene la descripción del test, sacada del docstring del método
correspondiente.
Luego (4), se muestra el stack trace de la excepción producida por el fallo de un aserto,
o por un error, y el mensaje asociado (5).
Tras los bloques de información de cada test aparece un resumen con el número de test
ejecutados y el tiempo empleado en su ejecución (6), así como cuántos test han
resultado fallidos o erróneos (7).
Métodos de aserto
47
Método Comprueba que
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)
Todos estos métodos admiten como parámetro extra opcional un mensaje para cuando el
aserto falle.
Además de los métodos listados en la tabla anterior, que sirven para comprobar si el
resultado del código probado coincide con el esperado, existen otros que controlan que
el código probado produzca una excepción, un warning o un mensaje de log
determinados, y se suelen usar en el contexto de una cláusula with. Por ejemplo,
supongamos que, en el ejemplo de sumar los valores pares de una lista, se hubiese
especificado que la función no debe admitir una lista vacía, lanzando una excepción
ValueError, si se da el caso. Para comprobar que la función lanza la excepción cuando
debe hacerlo, podemos escribir un método de prueba como el siguiente:
def test_empty_list(self):
"""Una lista vacía"""
with self.assertRaises(ValueError, msg = "No se lanza la
excepción esperada"):
result = sum_evens([])
El framework unittest ofrece algunos métodos de aserto más específicos que las
comparaciones de igualdad/desigualdad:
48
Método Comprueba que
assertNotRegex(s, r) not r.search(s)
a y b tienen los mismos elementos en igual
assertCountEqual(a, b)
número, independientemente de su orden
Nótese que los dos primeros sirven para comparar números reales (para los que
típicamente no tiene sentido hacer comparaciones de igualdad o desigualdad exactas,
debido a los errores de redondeo en la representación). El número de dígitos decimales
para la función round() se puede cambiar, dando un valor al parámetro opcional places
del método de aserto, o bien se puede dar un valor al parámetro opcional delta, con lo
que se comprobará si la distancia entre a y b es menor o igual, o mayor o igual, según el
aserto, que ese valor de delta.
est fixture
Internamente, .main() usa un test loader para crear una test suite con el conjunto de test
implementados en su módulo y un test runner para ejecutarlos.
Una test suite es un conjunto de test que queremos ejecutar juntos. Los test que
componen una test suite pueden pertenecer a distintas clases herederas de TestClass y a
distintos módulos.
En el siguiente ejemplo, la función suite() crea una test suite añadiendo un par de casos
de prueba de la clase sampleTest.SumEvensTest identificados por su nombre. Una vez
formada la test suite, se usa un test runner, de la clase TextTestRunner, para ejecutarla.
49
import unittest
import sampletests
def suite():
suite = unittest.TestSuite()
suite.addTest(sampletests.SumEvensTest('test_no_evens'))
suite.addTest(sampletests.SumEvensTest('test_all_evens'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suite())
Como parámetro de .addTest() se puede usar una test suite, de manera que se pueden
agrupar en una misma test suite casos de prueba provenientes de varias test suite
construidas previamente.
Los métodos main y run muestran los resultados de los test en consola. El método run
devuelve, además, un resultado de tipo TestResult que puede asignarse a una variable si
se quiere manejar luego programáticamente este resultado. El método main devuelve un
resultado de tipo TestProgram, que tiene un atributo, result, de tipo TestResult; si se
llama a main con el parámetro exit = false y se asigna su resultado a una variable, ese
resultado podrá manejarse luego programáticamente.
if __name__ == '__main__':
runner = unittest.TextTestRunner()
result = runner.run(suite())
print(result.failures)
T Estructuras encadenadas
Los diferentes atributos de datos de un objeto son a su vez, referencias a los objetos que
representan los valores de esos atributos.
Por medio de las referencias, los objetos se encadenan unos con otros, formando
estructuras más o menos complejas, dependiendo de las necesidades de la información a
representar.
50
Una estructura encadenada está formada por un conjunto de objetos,
generalmente denominados nodos de la estructura, que se referencian
unos a otros. Deben existir objetos terminales, que no referencian a otros
objetos, así como al menos un objeto inicial (según la estructura puede
denominarse primero, frente, cima, raíz, etc.) que es referenciado desde
"fuera" de la estructura y sirve como "punto de entrada" a la misma (en
la ilustración, el objeto inicial, el primero de la cadena, es el referenciado
por la variable un_coche).est loader
La clase TestLoader permite crear una TestSuite a partir de los casos de prueba
presentes en una clase heredera de TestClass, en un módulo, o en un directorio.
loader = unittest.TestLoader()
suite1 = loader.loadTestsFromName("sampletests")
suite2 = loader.loadTestsFromName("sampletests.SumEvensTest")
suite3 = loader.loadTestsFromName(
"sampletests.SumEvensTest.test_no_evens"
)
suite4 = loader.loadTestsFromNames(
["sampletests.SumEvensTest.test_no_evens",
"sampletests.SumEvensTest.test_all_evens"
]
)
suite5 = loader.loadTestsFromTestCase(sampletests.SumEvensTest)
suite6 = loader.loadTestsFromModule(sampletests)
Crea una suite a partir de los contenidos e un directorio (en este caso el actual):
suite7 = loader.discover(".")
suite = unittest.TestSuite()
suite.addTest(suite1)
51
suite.addTest(suite2)
suite.addTest(suite3)
suite.addTest(suite4)
suite.addTest(suite5)
suite.addTest(suite6)
suite.addTest(suite7)
Estructuras recursivas
Si un objeto, de una clase, X, tiene campos que son referencias a otros objetos, podrían
ser referencias a otros objetos de la misma clase X. En el siguiente ejemplo se hace
precisamente eso, al añadir a la clase Persona un atributo, padre, que es, a su vez, una
referencia a un objeto de la clase Persona. La idea es que los objetos de la clase
Persona tengan acceso a información sobre el padre (persona) de la persona
representada.
class Persona:
def __init__(self, nombre, primer_apellido, segundo_apellido,
padre = None):
self.__nombre = nombre
self.__primer_apellido = primer_apellido
self.__segundo_apellido = segundo_apellido
self.padre = padre
Esta definición da lugar a una estructura recursiva que puede expandirse indefinidamente,
formando una cadena en la que cada objeto de la clase Persona referencia a otro objeto de la
clase Persona. Naturalmente, en la praxis computacional esta estructura no puede ser infinita,
de tal manera que la recursión debe acabar con una "condición de base", que,
necesariamente, es un objeto Persona que no referencia a su padre.
Flexibilidad y complejidad
52
en la clase Persona atributos para referenciar al padre y la madre, permitiendo construir
el árbol genealógico de una persona (las referencias a None se han dejado en blanco):
Este otro ejemplo muestra una estructura que mantiene dos secuencias de
encadenamientos, correspondientes a dos formas de ordenar los elementos: orden
alfabético (línea continua) y orden de longitud (línea discontinua):
Listas encadenadas
Podemos ilustrar este uso comparando las listas (tipo list) de Python con un contenedor
que llamaremos LinkedList, implementado mediante listas encadenadas. Ambos son
53
versiones del concepto abstracto de lista, entendida como una secuencia ordenada de
elementos.
El tipo list de Python suele implementarse internamente como un array dinámico, lo que
se traduce en una porción contigua de memoria donde almacenar, una detrás de otra, las
referencias a los objetos contenidos en la lista, que pueden distribuirse por la memoria.
Normalmente, al crear el array, se reserva más espacio del estrictamente necesario en
ese momento, para facilitar el crecimiento de la lista.
Si la memoria está muy fragmentada (no hay suficiente memoria contigua para las
necesidades del array), bien no se podrá crear, bien habrá que reorganizarla para hacer
hueco.
Con las listas encadenadas, sin embargo, no se requiere almacenamiento contiguo, cada
nodo referencia al que le sigue, que puede estar en una posición en la memoria
completamente diferente. Esta ventaja, puede ser, al mismo tiempo, una desventaja: con
un array, es fácil acceder directamente a cualquier elemento de la lista usando un índice,
ya que, internamente, solo hay que sumar a la dirección de comienzo del array el valor
resultante de multiplicar el tamaño de cada referencia por el índice correspondiente; sin
embargo, con una lista encadenada solo se puede acceder a un elemento tras pasar por
todos los que le preceden.
Por otra parte, los nodos de una lista encadenada requieren espacio extra para almacenar
las referencias al siguiente nodo, espacio que no es requerido en la representación
mediante arrays. También es cierto que en la representación mediante listas
encadenadas nunca se requiere reservar espacio extra para facilitar el crecimiento.
Para crear una lista encadenada, primero hace falta crear una clase para representar los
nodos de la lista. Como es una clase que solo queremos que se use para este propósito,
podemos definirla como clase anidada, dentro de la definición de la clase LinkedList:
class LinkedList:
class Node:
def __init__(self, value, next_node = None):
self.value = value
self.next_node = next_node
54
La definición de la clase Node inicaliza un atributo con el objeto a referenciar por el
nodo (value) y otro con la referencia al siguiente nodo (next_node), que, por omisión, es
None (lista con un solo elemento o último nodo de una lista).
def __init__(self):
self.__first = None
self.__len = 0
Además, hemos añadido un atributo, __len, para almacenar la longitud de la lista, lo que
permite, con el pequeño sobrecosto de actualizar ese atributo cada vez que se añade o se
quita un elemento, tener una implementación eficiente para el método __len__(), que
permite aplicar a los objetos de tipo LinkedList la función predefinida len():
def __len__(self):
return self.__len
def __len__(self):
count = 0
current = self.__first
return count
my_list.insert(0, value)
Vamos a hacer la inserción por delante en una lista encadenada mediante una operación
específica:
El método insert_as_first solo tiene que crear un nodo nuevo en el que el valor es el que
se quiere insertar (value) y al atributo next_node se le asigna self.__first, de tal manera
que el nuevo nodo indica que le sigue el que hasta ahora era el primero de la lista (si lo
había). A continuación solo hay que asignar el nuevo nodo a self._first, convirtiéndolo
en el primero de la lista. Además, se actualiza el atributo __len:
55
El coste de insertar un elemento al principio de una lista encadenada es siempre el
mismo: solo hay que crear un nodo y hacer tres asignaciones. Sin embargo, en una
implementación tipo array list es una operación cuyo coste crece con cada inserción, ya
que habrá que desplazar las referencias a los elementos previamente insertados para
hacer hueco a la referencia del nuevo elemento en la zona reservada para ello:
my_list.append(value)
Insertar un elemento al final de una lista encadenada, puede ser muy costoso si hay que
recorrer toda la lista para localizar el final:
56
Inserción por detrás (2)
El coste de insertar un valor al final, que viene dado por el recorrido de la lista, puede
reducirse a uno equivalente a insertar al principio, si se añade a los objetos de la clase
LinkedList un atributo para referenciar el último nodo:
def __init__(self):
self.__first = self.__last = None
self.__len = 0
De esta manera, no hay que recorrer la lista, sino que se usa este atributo para acceder al
último nodo, y se actualiza una vez insertado el nuevo:
if self.__first == None:
self.__first = new_node
else:
self.__last.next_node = new_node
self.__last = new_node
self.__len += 1
En este caso, la inserción al final con una implementación de array list tampoco resulta
costosa, ya que es inmediato calcular la posición de inserción a partir de la longitud
actual de la lista y no es necesario desplazar elementos para abrir hueco. El único
57
problema puede ser que se agote el espacio incialmente reservado y haya que reubicar el
array list completo en otra zona de memoria.
Inserción enmedio
Para ilustrar cómo insertar enmedio de una lista encadenda, vamos a reproducir el
funcionamiento del método insert() de las listas de Python:
Las preguntas iniciales separan los casos particulares; que el nuevo valor vaya al
principio o al final de la lista, o que se haya proporcionado un índice inválido para la
inserción. Una vez descartados estos casos, se busca la posición de inserción, teniendo
cuidado de quedarse en el nodo justo anterior.
En el caso de una implementación con array lists, el problema vuelve a ser desplazar los
elementos desde el punto de inserción hasta el final para abrir hueco al nuevo elemento.
Si el nuevo elemento va al principio, habrá que desplazar todos los que ya están en el
array; si va al final, no habrá que desplazar ninguno; en promedio, habrá que desplazar
la mitad de los elementos.
58
Para borrar un elemento del principio de una lista encadenada, lo único que hay que
hacer es que la referencia al primer nodo pase a referenciar al nodo que era el siguiente
del que queremos quitar:
def delete_first(self):
if self.__first != None:
self.__first = self.__first.next_node
self.__len -= 1
if sel.__len == 0:
self.__last = None
En una implementación con arrays, borrar el primer elemento es una operación costosa
porque hay que desplazar todos los demás para tapar el hueco dejado por el elemento
elinminado:
Para borrar el último elemento de una lista encadenda, salvo que solo haya un elemento,
debemos encontrar el penúltimo elemento para convertirlo en último. De esta manera, el
tener una referencia al último elemento, que proporcionaba una gran ventaja al insertar,
no disminuye el coste del borrado:
def delete_last(self):
if self.__len > 0:
if self.__len == 1: # Si solo hay un elemento, la lista
queda vacía
self.__last = self.__first = None
else:
# Hay que encontrar el penúltimo elemento para
convertirlo en último
prev = self.__first
current = self.__first.next_node
59
current = current.next_node
self.__len -= 1
Borrar un elemento que ocupa una posición determinada en una lista encadenada
requiere encontrar su posición, de forma parecida a como se hace cuando se quiere
borrar el último elemnto. El borrardo del primer o el último elemento se convierten en
casos particulares del borrado de cualquier elemento por posición:
if index in range(self.__len):
if index == 0: # Borrar el primer elemento
self.__first = self.__first.next_node
self.__len -= 1
if self.__len == 0:
self.__last = None
else:
# Hay que encontrar el nodo anterior al que queremos
borrar
prev = self.__first
current = self.__first.next_node
current_index = 1
self.__len -= 1
else:
raise IndexError
60
Una implementación con arrays ahorraría tener que buscar la posición, pero habría que
desplazar los elementos que sigan al eliminado para tapar el hueco dejado por éste.
Iteración
La clase LinkedList usada en los ejemplos es un contenedor de elementos implementado
mediante una lista encadenada.
class LinkedList:
class Node:
def __init__(self, value, next_node = None):
self.value = value
self.next_node = next_node
def __init__(self):
self.__first = None
self.__len = 0
def __len__(self):
return self.__len
Los contenedores predefinidos de Python (Tuple, List, Dict,…) son iterables. En Python
es muy fácil hacer que un contenedor sea iterable: basta como implementar el método
mágico __iter__(), pero haciéndolo como una función generadora:
def __iter__(self):
current = self.__first
l = LinkedList()
for item in l:
print(item)
61
Java
¡Hola, mundo!
En Python, un programa mínimo está formado por una simple secuencia de una o más
instrucciones. Por ejemplo, el típico ¡Hola, mundo! puede escribirse en una única línea:
print("¡Hola, mundo!")
En Java, un programa consta, como mínimo de una clase, que debe tener, al menos, un
método de clase llamado main. El típico ¡Hola, mundo! requiere unas 5 líneas:
class HolaMundo {
public static void main(String[] arg) {
System.out.println("¡Hola, mundo!");
}
}
La clase puede llamarse como se quiera, pero el método principal no solo debe llamarse
main, sino, además:
• Tener el modificador public. Las variables y métodos declarados en una clase pueden
tener tres posibles modificadores de visibilidad: public, protected y private.
• Tener el modificador static, que indica que es un método de clase, no un método de
instancia.
• Declarar un resultado de tipo void. En Java, los métodos deben declarar en su
cabecera, antes de su nombre, el tipo del resultado que devuelven con una instrucción
return. El tipo void, significa que el método no devuelve ningún resultado vía return, es
decir, no devuelve nada (en Python devolverían el valor None).
• Tener un parámetro llamado arg, de tipo String[]. El tipo String[] designa un array (un
tipo de secuencia con índices, parecido a las tuplas o listas de Python) cuyos valores
son de tipo String (en Java, todos los valores de un array tienen que ser del mismo
tipo). El parámetro arg sirve para pasarle datos al programa cuando se ejecuta.
for i in range(10):
print("¡Hola, mundo!")
En Java, una secuencia de instrucciones que pertenece al ámbito de una clase, método o
estructura de control, debe encerrarse entre llaves:
class HolaMundo {
public static void main(String[] arg) {
for(int i = 0; i < 10; i += 1) {
System.out.println("¡Hola, mundo!");
}
}
}
62
También se sangra, pero esto se hace como convención de estilo para mejorar la
legibilidad del programa.
Nótese que, en Java, las instrucciones "simples" deben acabar siempre con un punto y
coma. En Python el punto y coma es opcional, y solo es obligatorio para separar varias
instrucciones simples en una misma línea, cosa que tampoco se aconseja.
print("¡Hola mundo!")
Por omisión, la instrucción print() inserta un salto de línea detrás del mensaje; esto se
puede cambiar, especificando una string de finalización. En el siguiente ejemplo,
simplemente no se inserta nada detrás del primer mensaje (al pasarse la string vacía); los
dos mensajes se muestran en la misma línea:
class HolaMundo {
public static void main(String[] arg) {
System.out.println("¡Hola mundo!");
}
}
class HolaMundo {
public static void main(String[] arg) {
System.out.print("¡Hola mundo!");
System.out.println("Otro mensaje");
}
}
63
Los métodos System.out.println() y System.out.print() no admiten más de un parámetro,
por lo que no ha lugar a separadores.
Variables
int_var = 100
float_var = 100.80
str_var = "Hola"
int_var = 3.14
Nótese la letra f detrás del valor en la asignación a la variable floatVar para indicar que
es un valor de tipo float (por omisión, los literales de números reales son de tipo double,
que tiene más precisión que el tipo float). El tipo declarado para una variable no se
puede cambiar, por lo que esta no puede recibir un valor de un tipo diferente.
El tipo de la variable sólo hay que declararlo la primera vez que esta aparece en el
código. En subsiguientes asignaciones no se debe especificar el tipo, pues se vería como
un intento de declarar una nueva variable con el mismo nombre que una que ya existe, y
produciría un error.
Se puede declarar una variable sin asignarle un valor explícitamente; esto, según los
casos puede suponer que la variable quede con un valor imprevisible o con uno
establecido por omisión.
int other;
64
Si la declaración de la variable la antecedemos del modificador final, impedimos que
posteriormente se le pueda cambiar el valor asignado en esa única vez.
Python integra una variedad de tipos de datos, entre los más simples están int (números
enteros), float (números reales), boolean (valores True/False) y string (secuencias de
caracteres), además del tipo NoneType, cuyo único valor es None.
• Cinco tipos de números enteros, de menor a mayor rango: byte, char, short, int y long
• Dos tipos de números reales: float y double
• Otros tipos: boolean (valores true/false) y void, que representa "ningún tipo" y se usa
para indicar que un método no devuelve ningún valor.
También existe el literal null, valor que indica, para una variable que puede referenciar a
un objeto, que no está referenciando en este momento a ninguno.
Los tipos byte y char, aunque son numéricos, se usan también para representar
caracteres (texto).
class Main {
public static void main(String[] arg) {
char x = 'a';
x = 64;
System.out.println(x); // Muestra: @ (código ascii 64)
}
}
Valores y referencias
En la memoria dinámica que usa un programa distinguimos dos zonas: el stack, o pila
de ejecución, y el heap, o zona "desorganizada". En la pila se ubican y desubican,
dinámica y organizadamente, datos de las distintas activaciones de subrutinas (esto es,
para cada llamada que aún no haya retornado), principalmente los ejemplares de
variables locales que corresponden a cada activación. En el heap se ubican y desubican
dinámicamente objetos de datos de forma más arbitraria, conforme a las necesidades del
programa en ejecución. La aparición y desaparición de las variables en la pila por tanto
siguen un patrón claro de acuerdo con las llamadas y retornos, mientras que en el heap
puede ser muy variable y en gran medida independiente del orden en que se producen
llamadas y retornos.
Podemos distinguir entre dos categorías de tipos de datos según cómo se almacena y
accede a sus datos: valores y referencias. En el primer caso, las variables contienen
directamente los valores, y se almacenan en muchos casos en el stack; en el segundo, las
65
variables contienen referencias (típicamente direcciones de memoria) a los objetos de
datos (sus valores propiamente dichos), que se almacenan usualmente en el heap.
En Python, todos los tipos de datos representan sus valores como objetos referenciados
por variables que contienen su dirección de memoria, es decir, son tipos referencia.
En Java, los tipos de datos no primitivos se implementan como referencias a objetos (de
alguna clase) en el heap, mientras que los tipos de datos primitivos almacenan
directamente su valor. No obstante, a cada tipo primitivo se asocia una clase envoltorio
(wrapper) equivalente para cuando hace falta representar ese tipo de valores en formato
objeto: Byte, Character, Short, Integer, Long, Float, Double, Boolean.
En el ejemplo vemos dos ejemplares de variables locales: una de tipo int (primitivo) y
otra de tipo Integer (clase). La primera (a) almacena su valor directamente en el stack,
mientras que la segunda (b) almacena en el stack la dirección del objeto que representa
su valor en el heap.
En Python, para entrar datos a un programa, se usa la función input(), que lee strings, y,
en su caso, se hace una conversión al tipo de datos que se espera:
En Java, hay diversas formas de entrar datos a un programa. Una de las más sencillas
es usar un objeto de la clase Scanner:
import java.util.Scanner;
class Input {
public static void main(String[] args) {
66
System.out.print("Dame un número entero: ");
int number = input.nextInt();
System.out.println("Tecleaste " + number);
}
}
1. Importar java.util.Scanner
2. Crear un objeto de tipo Scanner con System.in.
3. Usar el método adecuado de dicho objeto, dependiendo del tipo de datos que se
quiere leer (.nextInt(), .nextFloat(), .nextBoolean(),...). Las strings se leen con el
método .nextLine().
Selección binaria
if a > b:
print("a es mayor")
else:
print("a es menor o igual")
if (a > b) {
System.out.println("a es mayor");
} else {
System.out.println("a es menor o igual");
}
Encadenamiento else-if
if a > b:
print("a es mayor")
elif b > a:
print("a es menor")
else:
print("son iguales")
En Java, existe una sentencia especial para seleccionar un curso de acción en función
de que una expresión discriminante tome un valor concreto dentro de un conjunto de
opciones:
switch (day) {
case 1: System.out.println("lunes");
break;
case 2: System.out.println("martes");
break;
case 3: System.out.println("miércoles");
break;
case 4: System.out.println("jueves");
break;
case 5: System.out.println("viernes");
break;
case 6: case 7: System.out.println("fin de semana");
break;
default: System.out.println("no es un día");
break;
}
En el ejemplo anterior, se mostrará un mensaje distinto en función del valor de day, que
es la expresión discriminante, la cual puede ser de tipo byte, char, short o int, o de un
tipo enumerado. Ha de ir entre paréntesis, y se requieren llaves para delimitar el cuerpo
del switch. La opción default, de existir, recoge todos los casos no indicados
expresamente; de no existir, no se tratarían tales valores, terminando el switch.
Obsérvese que la secuencia de instrucciones para tratar cada caso típicamente termina
con una instrucción break, que termina la ejecución del switch (más interno). De no
incluirse alguna de estas instrucciones break, la ejecución continuaría en la siguiente
opción, aunque no corresponda con el valor discriminante.
Obsérvese también cómo, por lo dicho anteriormente, los casos de los valores 6 y 7 (o
más valores) podemos agruparlos para tratarlos con el mismo grupo de instrucciones, ya
que para el caso 6, estrictamente hablando, no hay instrucción alguna, ni siquiera break,
por lo que "caemos" en el caso 7, donde se acaba tratando.
if day == 1:
print("lunes")
elif day == 2:
print("martes")
elif day == 3:
68
print("miércoles")
elif day == 4:
print("jueves")
elif day == 5:
print("viernes")
elif day in (6, 7):
print("fin de semana")
else:
print("no es un día")
Sentencia for
En Python, podemos usar una sentencia for para recorrer una secuencia de valores:
En Java, podemos usar una sentencia for para recorrer una secuencia de valores:
Nótese que la principal diferencia con Python es que hay que declarar el tipo de la
variable que se usa para ir accediendo a cada elemento (name en el ejemplo). Se da por
supuesto que todos los valores de la secuencia son del mismo tipo, a diferencia de
Python, en que las secuencias pueden incluir valores de tipos diferentes.
En Python, se puede usar una sentencia for para recorrer un rango de valores:
for i in range(10):
print(i)
Se puede especificar un valor inicial, un valor final y un paso para el rango a recorrer:
El for con rango se puede usar para recorrer una secuencia accediendo a los elementos a
través de sus índices:
69
for i in range(len(names)):
print(names[i])
En Java, se puede usar una sentencia for para recorrer un rango de valores:
El control de una sentencia for con rango en Java va entre paréntesis y se divide en tres
partes separadas por punto y coma: la declaración de una variable de control de tipo int
y su inicialización con un valor inicial, una condición de iteración que terminará el for
cuando deje de cumplirse antes del comienzo de una interación, y una instrucción que
típicamente se emplea para (in/de)crementar la variable de control al final de cada
iteración.
El for con rango se puede usar para recorrer una secuencia accediendo a los elementos a
través de sus índices:
Sentencia while
En Python, podemos usar una sentencia while para ejecutar un bucle controlado por una
condición booleana:
i = 0
while i < 5:
print(i)
i += 1
En Java, podemos usar una sentencia while para ejecutar un bucle controlado por una
condición booleana entre paréntesis:
int i = 0;
while (i < 5) {
System.out.println(i);
i++;
}
Funciones
70
print(fun(10, 5))
En Java, no existen las funciones libres, todas las funciones son métodos de instancia o
de clase. En el siguiente ejemplo, la función fun es un método de clase (modificador
static), público y que devuelve un resultado de tipo int. En Java hay que declarar, tanto
el tipo de los parámetros que se pasan a un método, como el tipo del resultado que
devuelve:
class Main {
public static int fun(int param1, int param2) {
return param1 + param2;
}
Arrays
Python ofrece dos tipos de datos básicos para representar secuencias de valores: las
tuplas y las listas:
En Java, el tipo de datos básicos para representar secuencias de valores son los arrays.
A diferencia de las tuplas o listas de Python, todos los elementos de un array tienen que
ser del mismo tipo, que debe declararse. Para declarar una variable como array, hay que
poner el tipo de los elementos seguido por corchetes:
int[] arrayOfInt;
Los arrays de Java se parecen a las tuplas de Python en que, una vez creados, no pueden
cambiar de tamaño (no se les puede añadir elementos ni quitar elementos), pero se
parecen a las listas en que los elementos pueden modificarse.
Igual que en las secuencias de Python, los elementos de un array en Java se numeran
con índices empezando con el valor 0. Para acceder a un elemento de un array, se usa
su índice entre corchetes junto al nombre de la variable que lo referencia, igual que en
las secuencias de Python:
71
int value = arrayOfInt[3];
Para cambiar un valor, se pone esa expresión a la izquierda de una asignación, igual que
en las listas de Python:
arrayOfInt[3] = 10;
Esto mismo se puede hacer con las secuencias de Python usando la función len():
size = len(list_of_int)
Recorrido de un array
En Python, se puede recorrer una secuencia usando un bucle que itere por los índices de
la misma:
for i in range(len(my_list)):
print(my_list[i])
class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};
}
}
Análogamente en Java:
class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};
72
}
}
Estructuras multidimensionales
my_list = [
[10, 15, 20, 30, 40],
[12, 14, 16, 18],
[11, 17, 23, 29, 31]
]
int[][] myArray = {
{10, 15, 20, 30, 40},
{12, 14, 16, 18},
{11, 17, 23, 29, 31}
};
Para acceder a un elemento concreto hay que especificar los índices que lo identifican,
tanto en Python:
print(my_list[1][3])
Como en Java:
System.out.println(myArray[1][3]);
Para recorrer una estructura multidimensional, habrá que anidar tantos bucles como
dimensiones tenga la estructura (bidimensional en los ejemplos), independientemente de
si se recorre por índices o por valores, tanto en Python:
Como en Java:
73
// Recorrido por índices
for (int rowInd = 0; rowInd < myArray.length; rowInd++) {
for (int colInd = 0; colInd < myArray[rowInd].length; colInd++) {
System.out.println(myArray[rowInd][colInd]);
}
}
La clase Arrays de Java (paquete java.util) ofrece funciones de clase para realizar varias
operaciones útiles sobre arrays.
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] array1 = {80, 70, 60, 50, 40, 30, 20, 10};
Arrays.sort(array1, 2, 5); // [80, 70, 40, 50, 60, 30, 20, 10]
Arrays.sort(array1); // [10, 20, 30, 40, 50, 60, 70, 80]
}
}
El método binarySearch, dado un array ordenado y un valor, busca el valor en el array,
usando un algoritmo de búsqueda binaria, y devuelve su posición. Si no lo encuentra,
devuelve el valor (-(punto de inserción)-1), donde punto de inserción representa la
posición en la que debería estar:
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};
System.out.println(Arrays.binarySearch(myArray, 15)); //
Muestra 1
System.out.println(Arrays.binarySearch(myArray, 45)); //
Muestra -6
}
}
El método copyOf() copia un array, creando uno nuevo con una longitud especificada.
Si la longitud especificada es menor que la del array original, lo trunca, y si es mayor,
rellena con el valor de inicialización del tipo de los elementos:
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};
74
int[] shorter = Arrays.copyOf(myArray, 3)); // {10, 15, 20}
int[] longer = Arrays.copyOf(myArray, 7)); // {10, 15, 20,
30, 40, 0, 0}
}
}
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};
int[] other = Arrays.copyOfRange(myArray, 2, 5); // {20, 30,
40}
}
}
El método equals(), devuelve true si los dos arrays que compara son iguales:
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] array1 = {10, 15, 20, 30, 40};
int[] array2 = {15, 10, 30, 40, 20};
System.out.println(Arrays.equals(array1, array2)); // Muestra
false
}
}
Cuando los elementos de los arrays son objetos (no valores de tipos primitivos), el
método equals() hace una comparación superficial (shallow), para hacer una
comparación en profundidad (deep), hay que usar el método deepEquals().
El método fill() rellena los elementos de un array con un valor determinado. Se puede
especificar un rango de índices a rellenar:
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] array1 = new int[10]; // {0, 0, 0, 0, 0, 0, 0, 0, 0,
0}
Arrays.fill(array1, 1); // {1, 1, 1, 1, 1, 1, 1, 1, 1,
1}
Arrays.fill(array1, 4, 8, 2); // {1, 1, 1, 1, 2, 2, 2, 2, 1,
1}
}
}
El método toString() devuelve un objeto de tipo String que representa el contenido del
array que se le pasa:
75
import java.util.Arrays;
class Main {
public static void main(String[] arg) {
int[] array1 = {10, 15, 20, 30, 40};
String string1 = Arrays.toString(array1); // "[10, 15, 20, 30,
40]"
}
}
Nótese que, como string, el contenido del array se representa entre corchetes, aunque en
Java, los literales array se construyen usando llaves.
Strings
Una string vacía puede crearse usando comillas sin nada enmedio, o llamando a la
expresión constructora de la clase str sin parámetros:
empty1 = ""
empty2 = str()
number = 10
number_str = str(number)
Las comillas simples se reservan para los literales de tipo char (representan un carácter
individual, no una secuencia de caracteres).
Una string vacía puede crearse usando comillas dobles sin nada enmedio, o llamando a
la expresión constructora de la clase String sin parámetros:
76
int number = 10;
String numberStr = ((Integer) number).toString();
(Nótese que para aplicar el método .toString(), se ha convertido el valor de number del
tipo primitivo int a la clase Integer).
Secuencias de escape.
En Python, se usan secuencias de escape que comienzan con una barra invertida para
incluir ciertos caracteres especiales en una string:
En Java, se usan secuencias de escape que comienzan con una barra invertida para
incluir ciertos caracteres especiales en una string:
Algunos ejemplos de secuencias de escape (se usan las mismas secuencias en ambos
lenguajes):
print(len(s1)) # 18
Comparación de strings
En Python, se pueden comparar strings usando los operadores relacionales (<, <=, ==,
!=, >=, >):
77
print(s1 < s2) # False
print(s1 >= s2) # True
System.out.println("Hola".equalsIgnoreCase("hola")); // true
Para comparar si una string es menor, igual o mayor que otra, usamos el método
.compareTo(), que devuelve cero si las strings comparadas son iguales, un número
positivo si la primera (el objeto this de la llamada al método) es mayor que la segunda y
un número negativo en otro caso.
System.out.println(s1.compareTo(s2)); // 6
System.out.println(s2.compareTo(s1)); // -6
Concatenación de strings
s1 = "Hola"
s2 = "mundo"
s3 = s1 + " " + s2 # "Hola mundo"
String s1 = "Hola";
String s2 = "mundo";
String s3 = s1 + " " + s2; // "Hola mundo"
También se puede usar el método .concat():
78
String s1 = "Hola";
String s2 = "mundo";
String s3 = s1.concat(" ").concat(s2); // "Hola mundo"
s1 = "Hola mundo"
c1 = s1[3]; # 'a'
En Python, se puede copiar una substring de una string usando una operación de
segmentación:
s1 = "Hola mundo"
s2 = s1[1:4]; # "ola"
s1 = "Hola mundo"
s2 = s1[5:]; # "mundo"
En Java, se puede copiar una substring de una string usando el método .substring():
Contención
En Python, se puede averiguar si una substring forma parte de una string usando el
operado in:
s1 = "Hola mundo"
print("la mu" in s1) # True
print("casa" in s1) # False
En Java, se puede averiguar si una substring forma parte de una string usando el
método .contains():
79
String s1 = "Hola mundo";
System.out.println(s1.contains("la mu")); // true
System.out.println(s1.contains("casa")); // false
Búsqueda (I)
También se puede usar el método .index(), pero éste lanza una excepción si no se
encuentra la substring buscada.
Búsqueda (II)
En Python, se puede localizar la última aparición de una substring en una string usando
el método .rfind():
80
Se puede indicar que la búsqueda se realice dentro de una substring de la string de
búsqueda:
En Java, se puede localizar la última aparición de una substring en una string usando el
método .lastIndexOf():
Se puede especificar que la búsqueda se realice (hacia atrás) desde una posición
diferente del final de la string de búsqueda:
Tanto en Python como en Java, las strings tienen muchas más operaciones. A
continuación se listan algunas de las que se usan con más frecuencia.
Python Java
Python Java
Convertir a mayúsculas/minúsculas:
81
Python Java
Python tiene un módulo llamado re para trabajar con expresiones regulares. Este
módulo permite usar objetos de coincidencia, objetos de expresión regular
precompilados y diversas funciones. La mayoría de las funciones admiten como
expresión regular una string o un objeto de expresión regular precompilado y muchas
devuelven como resultado objetos de coincidencia, con múltiples atributos para obtener
información sobre las coincidencias de un patrón encontradas en un texto. El módulo re
también ofrece una excepción específica (re.error) para indicar que una expresión
regular está mal formada.
Los objetos de expresión regular ofrecen, a su vez, métodos para conseguir resultados
similares a los que proporcionan las funciones.
Java tiene un paquete llamado java.util.regex para trabajar con expresiones regulares.
Este paquete ofrece dos clases: Matcher (objetos para manipular expresiones regulares)
y Pattern (objetos de expresión regular precompilada); y una excepción:
PatternSyntaxException, que indica que una expresión regular está mal formada.
82
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
public static void main(String[] args) {
String text = "una fecha 12-12-2020 y otra fecha 01-10-2019";
Pattern pattern = Pattern.compile("\\d{2}-\\d{2}-\\d{4}");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group(0));
}
}
}
Búsqueda de coincidencias
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
public static void main(String[] args) {
String text = "una fecha 12-12-2020 y otra fecha 01-10-2019";
Pattern pattern = Pattern.compile("\\d{2}-\\d{2}-\\d{4}");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.print(matcher.start() + "..");
System.out.print(matcher.end() + ": ");
System.out.println(matcher.group(0));
}
}
}
83
La diferencia es que, en Python, una llamada a .finditer() devuelve una secuencia con
todas las coincidencias, mientras que, en Java, cada llamada a .find() devuelve una
única coincidencia (la siguiente no devuelta anteriormente).
En Python una llamada a .finditer() devuelve una secuencia con todas las coincidencias,
mientras que en Java cada llamada a .find() devuelve una única coincidencia (la
siguiente no devuelta anteriormente).
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
public static void main(String[] args) {
String text = "una fecha 12-12-2020 y otra fecha 01-10-2019";
Pattern pattern = Pattern.compile("\\d{2}-\\d{2}-\\d{4}");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.print(matcher.start() + "..");
System.out.print(matcher.end() + ": ");
System.out.println(matcher.group(0));
}
}
}
while match:
print(match.start(), end = "..")
print(match.end(), end = ": ") # como en Java: 1 + índice del
último carácter en la coincidencia
print(match.group(0))
Sustitución
84
new_text = pattern.sub('<DATE>', text)
print(new_text)
En el ejemplo, new_text recibe el valor "una fecha <DATE> y otra fecha <DATE>".
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
public static void main(String[] args) {
String text = "una fecha 12-12-2020 y otra fecha 01-10-2019";
Pattern pattern = Pattern.compile("\\d{2}-\\d{2}-\\d{4}");
Matcher matcher = pattern.matcher(text);
String newText = matcher.replaceAll("<DATE>");
System.out.println(newText);
}
}
En el ejemplo, newText recibe el valor "una fecha <DATE> y otra fecha <DATE>".
Clases y objetos
En Python iniciamos la definición de una nueva clase de objetos con la palabra class
seguida del nombre de la clase:
class MyClass:
pass
Para crear un nuevo objeto de la clase, usamos una expresión constructora formada por
el nombre de la clase seguida de una lista de parámetros de inicialización (vacía en el
siguiente ejemplo):
my_obj = MyClass()
En Java iniciamos la definición de una nueva clase de objetos con la palabra class
seguida del nombre de la clase:
class MyClass {}
Para crear un nuevo objeto de la clase, usamos una expresión constructora formada por
la palabra reservada new, seguida del nombre de la clase, seguida de una lista de
parámetros de inicialización (vacía en el siguiente ejemplo):
85
Atributos de datos de objeto, inicialización
class MyClass:
def __init__(self, value1, value2):
self.attribute1 = value1
self.attribute2 = value2
class MyClass {
int attribute1;
int attribute2;
class Main {
public static void main(String[] arg) {
MyClass myObject = new MyClass(10, 15);
}
}
El objeto que se está inicializando se referencia mediante la variable this, que se crea
automáticamente.
Hay que fijarse en que, en Python, las variables creadas en el cuerpo de la clase, fuera
de cualquier método, como se hace con attribute1 y attribute2 en el ejemplo en Java,
serían variables de clase, no de objeto.
• En Python sólo es posible tener un método __init__(), mientras que en Java se pueden
tener (y se suelen tener), varios constructores, con el requisito de que las listas de
parámetros de esos constructores sean diferentes en número o tipos.
• En Java no se pueden añadir atributos dinámicamente a los objetos después de
inicializados, como sí se puede hacer en Python. Un objeto en Java sólo tiene los
atributos declarados en la definición de su clase.
86
Métodos de objeto
class MyClass:
...
class MyClass {
...
class Main {
public static void main(String[] arg) {
MyClass myObject = new MyClass(10, 15);
System.out.println(myObject.myMethod(5));
}
}
Visibilidad
En Python todos los atributos (variables y métodos) definidos en una clase son, en
principio, públicos; pueden ser accedidos por cualquier código ajeno a la clase (siempre
que importe el módulo correspondiente). Si el nombre de un atributo comienza con dos
guiones bajos, y no termina con dos guiones bajos (notación reservada a los métodos
mágicos), se oculta, de forma que no puede ser accedido directamente desde fuera de la
clase:
class MyClass:
def __init__(self, value1, value2):
self.__attribute1 = value1
self.__attribute2 = value2
87
def my_method(self, param):
return self.__aux_method(param)
En el ejemplo anterior, los métodos __init__() y my_method() son los únicos atributos
"públicos" de MyClass.
Comenzar el nombre con un solo guión bajo se entiende como un aviso para tratar ese
atributo como privado, pero no tiene ningún efecto práctico más allá de la observancia
de dicha convención por parte de los programadores.
En Java los atributos pueden llevar uno de entre tres posibles modificadores de
visibilidad, o no llevar ninguno:
Un atributo que no lleve ningún modificador de visibilidad es visible sólo para la clase y
las clases definidas en el mismo package, pero no para las clases derivadas, si
pertenecen a un package diferente.
class MyClass {
private int attribute1;
private int attribute2;
Las propias clases deben llevar el modificador public para poder usarse fuera del
package en el que están definidas:
Encapsulamiento de datos
88
Es recomendable proteger los datos de los objetos usando las técnicas de
encapsulamiento para ocultar los atributos que los representan y proporcionar
mecanismos que controlen cómo se pueden acceder y modificar.
class Person:
def __init__(self, name):
self.name = name
@property
def name(self):
return self.__name
@name.setter
def name(self, new_name):
self.__name = new_name
a_person = Person("Pepe")
print(a_person.name)
a_person.name = "Juan"
print(a_person.name)
Por la misma razón, en Java los atributos que representan datos de un objeto deberían
en principio llevar el modificador private. Para proporcionar capacidad controlada de
acceso y modificación de los datos, una vez ocultados los atributos que los representan,
se escriben getters y setters. Un getter es un método sin parámetros cuyo nombre
típicamente se forma con el prefijo get seguido del nombre del atributo con la primera
letra en mayúscula. Un setter es un método con un parámetro y cuyo nombre se forma
igual con el prefijo set:
class Person {
private String name;
Person(String name) {
this.name = name;
}
class Main {
public static void main(String[] arg) {
Person aPerson = new Person("Pepe");
System.out.println(aPerson.getName());
aPerson.setName("Juan");
System.out.println(aPerson.getName());
89
}
}
La solución, tanto en Python como en Java, pasa por crear un getter (método de
acceso) y un setter (método de modificación), pero Python va un paso más allá con el
decorador @property y consigue que las propiedades se usen como si fueran atributos
normales, mientras que en Java hay que llamar explícitamente a los getters y setters.
Atributos de clase
En POO, las clases pueden tener atributos de clase: datos que no pertenecen a un objeto
concreto sino que son comunes a todos los objetos de la clase, o métodos que no se
asocian a un objeto concreto y que sirven, fundamentalmente, para manejar los datos de
clase "directamente" (sin necesidad de usar un objeto de la misma).
class MyClass:
__cls_attr = 0
@classmethod
def set_cls_attr(cls, value):
cls.__cls_attr = value
@classmethod
def get_cls_attr(cls):
return cls.__cls_attr
MyClass.set_cls_attr(3)
print(MyClass.get_cls_attr())
En Java, los atributos de clase, sean variables o métodos, se distinguen por llevar el
modificador static:
class MyClass {
private static int clsAttr = 0;
private int objAttr1, objAttr2;
90
this.objAttr2 = value2;
}
Herencia
La herencia es uno de los mecanismos principales de la POO que permite crear nuevas
clases derivadas de clases existentes, extendiendo sus características típicamente
mediante variables y métodos adicionales, o la sustitución (overriding, redefinición
normalmente para adecuar el comportamiento) de algunos de los ya disponibles en la
clase(s) de la que se deriva. Estas nuevas clases son subclases de aquellas de las que
derivan, que son sus superclases.
En Python, para indicar que una clase hereda de otra, se pone el nombre de la
superclase entre paréntesis detrás del nombre de la subclase en la cabecera de la
definición de ésta:
En Java, para indicar que una clase hereda de otra, se pone la cláusula extends seguida
del nombre de la superclase, detrás del nombre de la clase en la cabecera de la
definición de ésta:
La subclase tiene acceso a todos los atributos disponibles en su superclase excepto los
que tengan el modificador private.
Inicialización de subclases
91
En Python, cuando escribimos un método de inicialización para una clase derivada,
normalmente tenemos que llamar al inicializador de la superclase. Lo hacemos usando
la función super para hacer referencia a la superclase:
En Python, para sustituir un método, una subclase sólo necesita tener un método con el
mismo nombre, el cual oculta al correspondiente de la superclase:
class BaseClass:
def message(self):
return "This is a message from BaseClass";
class ClassOne(BaseClass):
def message(self):
return "This is a message from ClassOne";
92
Cuando usando un objeto de la clase BaseClass se llama a método de nombre message,
se ejecuta el definido en BaseClass, pero si el objeto es de tipo ClassOne, se ejecuta el
definido en ClassOne. Además, desde un método de un objeto de tipo ClassOne
podemos llamar a un método sustituido de BaseClass usando la función super():
def message(self):
return super().message() + " and then a message from ClassOne"
En Java, para sustituir un método en una subclase, el nuevo método debe tener mismo
nombre y parámetros (misma signatura), preferentemente con la anotación @Override,
resultando ocultado el correspondiente de la superclase:
class BaseClass { // Superclase
public String message() {
return "This is a message from BaseClass";
}
}
Jerarquía de clases
En Python todas las clases heredan de forma implícita de una clase base común llamada
object. La clase object define una serie de métodos mágicos con un comportamiento por
omisión que puede ser sustituido (override) redefiniendo esos métodos en las clases
derivadas.
En Java todas las clases heredan de forma implícita de una clase base común llamada
Object. La clase Object define una serie de métodos con un comportamiento por
omisión que puede ser sustituido (override) redefiniendo esos métodos en las clases
derivadas.
Python permite la herencia múltiple: una clase puede heredar de varias superclases,
cuyos nombres se separan por comas dentro de los paréntesis.
93
En Java una clase solo puede tener una superclase.
En Java, si una clase tiene el modificador final, no pueden crearse subclases que
hereden de ella.
protected Object clone() Crea y devuelve una copia del objeto this
boolean equals(Object obj) Indica si otro objeto es igual al objeto this
protected void finalize() Lo llama el garbage collector cuando el objeto this
ya no es accesible al programa, antes de liberar su
memoria
Class<?> getClass() Devuelve la clase del objeto this
int hashCode() Devuelve un código de hash para el objeto this
String toString() Devuelve una representación en forma de string
del objeto this
void notify() Métodos relacionados con la ejecución
void notifyAll() multithreading (concurrencia)
void wait()
void wait(long timeout)
void wait(long timeout, int nanos)
Clase de un objeto
s = Square(10)
print(s.__class__) # Equivalente a print(type(s))
94
También se puede averiguar si un objeto es instancia de una clase (directa o
indirectamente), ejecutando la función isinstance():
@Override
public boolean equals(Object other) {
if other instanceof Square {
....
} else {
return false;
}
}
try:
dividendo = int(input('Dividendo: '))
divisor = int(input('Divisor : '))
print(dividendo // divisor)
except ValueError as err:
print('Se esperaba un número entero')
except ZeroDivisionError as err:
print('El divisor no puede ser cero')
import java.util.Scanner;
class Excepciones {
public static void main(String[] arg) {
try {
Scanner in = new Scanner(System.in);
System.out.print("Dividendo: ");
int dividendo = in.nextInt();
System.out.print("Divisor : ");
int divisor = in.nextInt();
System.out.println(dividendo / divisor);
95
} catch (java.util.InputMismatchException err) {
System.out.println("Se esperaba un número entero");
} catch (ArithmeticException err) {
System.out.println("El divisor no puede ser cero");
}
}
}
try:
dividendo = int(input('Dividendo: '))
divisor = int(input('Divisor : '))
print(dividendo // divisor)
except (ValueError, ZeroDivisionError) as err:
print('Error en la entrada de datos')
import java.util.Scanner;
class Excepciones {
public static void main(String[] arg) {
try {
Scanner in = new Scanner(System.in);
System.out.print("Dividendo: ");
int dividendo = in.nextInt();
System.out.print("Divisor : ");
int divisor = in.nextInt();
System.out.println(dividendo / divisor);
} catch (java.util.InputMismatchException |
ArithmeticException err) {
System.out.println("Error en la entrada de datos");
}
}
}
import sys
try:
dividendo = int(input('Dividendo: '))
divisor = int(input('Divisor : '))
print(dividendo // divisor)
except ZeroDivisionError as err:
96
print(err.args)
print(sys.exc_info())
El atributo args es una tupla con información sobre la excepción. Dependiendo del tipo
de excepción, puede tener atributos adicionales.
import java.util.Scanner;
class Excepciones {
public static void main(String[] arg) {
try {
Scanner in = new Scanner(System.in);
System.out.print("Dividendo: ");
int dividendo = in.nextInt();
System.out.print("Divisor : ");
int divisor = in.nextInt();
System.out.println(dividendo / divisor);
} catch (ArithmeticException err) {
System.out.println(err.getMessage());
}
}
}
La clase base Exception ofrece varios métodos para obtener información diversa
(getCause, getLocalizedMessage, getMessage, getStackTrace,...). Dependiendo del tipo
de excepción, podría haber métodos adicionales.
En Python se puede poner un último bloque except para recoger cualquier excepción no
contemplada explícitamente en los bloques except anteriores:
try:
dividendo = int(input('Dividendo: '))
divisor = int(input('Divisor : '))
print(dividendo // divisor)
except (ValueError, ZeroDivisionError):
print('Error en la entrada de datos')
except:
print("Excepción inesperada")
En Java se puede poner un último bloque catch para recoger cualquier excepción no
contemplada explícitamente en los bloques catch anteriores:
import java.util.Scanner;
class Excepciones {
public static void main(String[] arg) {
try {
97
Scanner in = new Scanner(System.in);
System.out.print("Dividendo: ");
int dividendo = in.nextInt();
System.out.print("Divisor : ");
int divisor = in.nextInt();
System.out.println(dividendo / divisor);
} catch (java.util.InputMismatchException |
ArithmeticException err) {
System.out.println("Error en la entrada de datos");
} catch (Exception err) {
System.out.println("Excepción inesperada");
}
}
}
Nótese que en Java el último bloque recoge Exception, la clase base de la jerarquía de
excepciones, mientras que en Python no hay que especificar ninguna excepción concreta
(aunque se puede especificar Exception, la correspondiente clase base de Python, sobre
todo si se quiere acceder a información de la excepción).
Bloques adicionales
try:
dividendo = int(input('Dividendo: '))
divisor = int(input('Divisor : '))
print(dividendo // divisor)
except ZeroDivisionError as err:
print(err.args)
else:
print("La operación se ejecutó con éxito")
finally:
print("Ejecución terminada")
import java.util.Scanner;
class Excepciones {
public static void main(String[] arg) {
try {
Scanner in = new Scanner(System.in);
System.out.print("Dividendo: ");
int dividendo = in.nextInt();
System.out.print("Divisor : ");
int divisor = in.nextInt();
System.out.println(dividendo / divisor);
System.out.println("Operación realizada con éxito");
} catch (ArithmeticException err) {
System.out.println(err.getMessage());
} finally {
98
System.out.println("Ejecución terminada");
}
}
}
Lanzamiento de excepciones
def average(list_of_int):
if len(list_of_int) == 0:
raise ValueError("la lista no puede estar vacía")
else:
return sum(list_of_int) / len(list_of_int)
import java.util.Arrays;
class Excepciones {
public static double average(int[] listOfInt) {
if (listOfInt.length == 0) {
throw new IllegalArgumentException("la lista no puede
estar vacía");
} else {
return Arrays.stream(listOfInt).sum() / listOfInt.length;
}
}
...
En Python definimos nuevas clases de excepciones creando una clase que herede,
directa o indirectamente, de la clase base Exception:
class MyException(Exception):
pass
En Java definimos nuevas clases de excepciones creando una clase que herede, directa
o indirectamente, de la clase base Exception:
Tanto en Python como en Java la nueva clase de excepción puede añadir sus propios
atributos de datos y métodos.
99
Declaración de excepciones salientes
En Java, cuando un método deja salir una excepción de cualquier clase que no sea
heredera, directa o indirecta, de la clase RunTimeException, debe declararla en su
cabecera usando una cláusula throws:
Clases abstractas
En Python una clase abstracta es una clase que hereda de la clase ABC (Abstract Base
Class) y contiene métodos abstractos (métodos declarados pero no implementados)
marcados con el operador @abstractmethod:
class Shape(ABC):
@property
@abstractmethod
def area(self): pass
100
No se pueden crear objetos de una clase abstracta. Las clases abstractas solo sirven para
crear subclases que implementen los métodos abstractos. Una subclase de una clase
abstracta que no implemente todos los métodos abstractos definidos por aquella, es, a su
vez, una clase abstracta.
En Java una clase abstracta es una clase marcada con el modificador abstract. Una
clase abstracta puede incluir, o no, métodos abstractos (métodos declarados pero no
implementados) marcados con el modificador abstract.
No se pueden crear objetos de una clase abstracta. Las clases abstractas sólo sirven para
crear subclases que implementen los métodos abstractos. Una subclase de una clase
abstracta que no implemente todos los métodos abstractos definidos por aquella, es, a su
vez, una clase abstracta y debe llevar el modificador abstract.
Interfaces
En Java una interfaz es algo parecido a una clase completamente abstracta que se usa
para proporcionar un listado de declaraciones de métodos (signaturas) que las clases que
quieran cumplir la interfaz deben implementar:
Todos los métodos declarados en una interfaz son públicos, por lo que el modificador
public podría omitirse.
Una clase que quiera adherirse al cumplimiento de una intrefaz, lo indica con la palabra
implements e implementa todos los métodos definidos en la interfaz:
Square(double side) {
this.side = side;
}
101
public class Square extends Paralelogram implements IShape,
Cloneable{...}
Las clases abstractas y las interfaces son dos mecanismos bastante parecidos. Usaremos
una clase abstracta cuando:
Cloneable
Las clases que implementan esta interfaz deben sustituir el método protegido
Object.clone() con un método público.
102
public class CloneableClass implements Cloneable {
int[] data;
CloneableClass() {
this.data = new int[]{1, 2, 3, 4, 5};
}
@Override
public CloneableClass clone() throws CloneNotSupportedException {
CloneableClass newObject = (CloneableClass) super.clone();
if (data != null) {
newObject.data = this.data.clone();
}
return newObject;
}
}
El método Object.clone() debe realizar una copia en profundidad (deep copy). Por eso
en el ejemplo se clona el atributo data en vez de asignarlo directamente.
Nótese que el método tiene que declarar que puede lanzarse la excepción
CloneNotSupporterException. Esto podría ocurrir si la superclase no es cloneable o si
no lo fueran los elementos del array data (que, en este ejemplo son números enteros,
por lo que sí son cloneables).
El objeto clonado debe ser igual al objeto original cuando se comparan usando el
método .equals(), por lo que, generalmente, también será necesario redefinir este
método:
@Override
public boolean equals(Object other){
if (other instanceof CloneableClass) {
CloneableClass compared = (CloneableClass) other;
if (this.data.length == compared.data.length) {
for (int i = 0; i < data.length; i++) {
if (this.data[i] != compared.data[i]) return
false;
}
return true;
}
}
return false;
}
103
En Java hay, además, que hacer a continuación un downcast del parámetro (que es de
tipo Object) al tipo correspondiente para poder tener acceso a sus atributos y
compararlos con los del objeto this.
Comparable<T>
En Java no se pueden sobrecargar los operadores. Para definir cómo comparar los
objetos de una clase, definiendo un orden total, esta debe implementar la interfaz
Comparable<T>, donde T representa el tipo de la clase en cuestión.
Diferentes valores positivos o negativos pueden dar información más detallada sobre la
diferencia de ambos objetos; por ejemplo, el método .comparteTo() de la clase String
devuelve la diferencia entre los primeros caracteres que sean diferentes en ambas
strings, o, si una es prefijo de la otra, la diferencia entre sus longitudes.
Iterator<T> e Iterable<T>
En Python una clase es iterable si define el método mágico __iter__() que devuelve un
iterador sobre un objeto de esa clase. Un iterador es un objeto de una clase que define el
método mágico __next__(), que devuelve, uno por uno, los elementos de una secuencia
obtenida a partir del objeto sobre el que itera y lanza la excepción StopIteration cuando
no quedan elementos por devolver. La clase RangeIterator es iterable y es su propio
iterador.
104
class RangeIterator():
def __init__(self, start = 0, stop = 0, step = 1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current <= self.stop:
result = self.current
self.current += self.step
return result
else:
raise StopIteration
import java.util.Iterator;
import java.lang.Iterable;
105
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
RangeIterator range = new RangeIterator(1, 2, 15);
Iterator<Integer> iterator = range.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
Pero es muy usual usar bucles for, donde los métodos para obtener el iterador de un
objeto iterable y usarlo se invocan automáticamente.
public class Main {
public static void main(String[] args) {
for (int e : new RangeIterator(1, 2, 15)) {
System.out.println(e);
}
}
}
Un objeto iterable puede ser su propio iterador, pero es más usual que el objeto iterable
y el iterador que lo recorre sean objetos diferentes:
import java.util.Iterator;
import java.lang.Iterable;
/**
* Range es un objeto iterable que representa una secuencia de enteros
*/
public class Range implements Iterable<Integer> {
/**
* RangeIterator es un iterador sobre objetos de clase Range
*/
class RangeIterator implements Iterator<Integer> {
private Range range;
private int current;
106
private int stop = 0;
System.out.println(sum);
}
}
}
107