Apuntes FP2 2021

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 107

Contenido

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

Probemos el siguiente programa con los valores de entrada 7 y 2:

a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)

Obtetemos la siguiente salida:

a: 7
b: 2
3 1

Probemos ahora con otros valores de entrada:

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

En general, cuando un programa en Python encuentra una situación que no puede


manejar, lanza una excepción y finaliza.

En Python, una excepción es un objeto que representa un error.

Control de excepciones

Las excepciones son, en realidad, bastante comunes, y los programas deben estar
preparados para manejarlas.

En el ejemplo, chequeamos la posibilidad de que el divisor sea cero antes de efectuar la


operación:

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.

Control de múltiples excepciones

En el ejemplo siguiente se controla la excepción ZeroDivisionError, pero pueden


ocurrir otras, p.e. ¿y si alguna entrada no pudiera convertirse a int?

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'

La excepción ValueError ocurrió porque el valor entrado no es apropiado para la


función de conversión int().

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.

Control de excepciones no específicas

El tipo de excepción se puede omitir en el bloque except. En ese caso, cualquier


excepción que ocurra será enviada a ese bloque:

try:
a = int(input('a: '))
b = int(input('b: '))
print(a // b, a % b)
except:
print('some exception occured')

Aunque, lo más corriente es contemplar primero algunas excepciones específicas y


poner un bloque except al final para tratar cualquier otra que no se haya tenido en
cuenta:

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 ')

Si tenemos que manejar varias excepciones específicas distintas de la misma manera,


podemos listarlas en el mismo bloque, formando una tupla:

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',)

La variable err es una referencia a la excepción.

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')

Esta es la salida cuando no ocurre ninguna excepción:

a: 10
b: 2
5 0
this is always printed out

Y esta es la salida cuando b es igual a 0:

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

Excepciones en las funciones estándar

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))

Salida cuando la substring buscada está en la string de búsqueda:

> thon
2

Salida cuando la string buscada no está en la string de búsqueda:

> a
Traceback (most recent call last): File ".../test.py", line 3, in
<module>
print(my_string.index(input_string))
ValueError: substring not found

En el primer caso, se muestra el índice de aparición de la substring. En el segundo, la


función index() produce una excepción (ValueError) porque no encuentra la substring
buscada.

La excepción puede controlarse usando bloques try/except:

try:
my_string = 'Python'
input_string = input('> ')
print(my_string.index(input_string))
except ValueError as err:
print(err) #substring not found

Cómo generar excepciones

Cuando escribimos nuestras propias funciones, podemos lanzar excepciones si es


necesario.

Por ejemplo, tenemos la siguiente función que devuelve el área de un círculo:

import math

def circle_content(radius):
return math.pi * radius ** 2

10
print(circle_content(1)) # 3.141592653589793
print(circle_content(-2)) # 12.566370614359172

En ambos casos, se muestra un resultado, pero es obvio que el segundo no es correcto,


ya que el radio no puede ser un número negativo. La función, en vez de dar un resultado
erróneo, debería informar de que no puede dar un resultado correcto:

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

Generalmente, ponemos un mensaje descriptivo entre paréntesis acompañando a la


excepción:

raise ValueError('the input need to be an integer')


raise ZeroDivisionError('fraction’s denominator is zero')
raise TypeError('bad operand')
raise ValueError

En el último caso, al lanzar la excepción ValueError, no hemos añadido ningún


mensaje; en este caso se usa el mensaje por defecto establecido para este tipo de error.

Sentencia raise sin argumentos

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:

• controlarla de manera adecuada.


• propagarla hacia el código llamador.

Si nadie controla una excepción, el programa termina.

En la siguiente función se intenta obtener un número entero del usuario. El usuario


puede cometer hasta tres errores, que se materializan en el lanzamiento de la excepción
ValueError, y se le vuelve a pedir el número, tras informarle con un mensaje adecuado.
Tras el tercer intento, se deja que la excepción siga su curso, relanzándola con la
sentencia raise, y es tratada en el programa principal. Si no existiese el bloque try/except
del programa principal, el programa abortaría al propagarse la excepción hasta el
intérprete de Python:

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.

Cuando la ejecución de un programa llega a un aserto, se evalúa la condición del


mismo. Si es verdadera, la ejecución continúa, si no, se lanza una excepción.

En Python se usa la sentencia assert para escribir asertos en un programa:

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.

Los asertos solo se ejecutan si la variable de entorno predefinida __debug__ tiene el


valor True, que es el caso a menos que el intérprete de Python se esté ejecutando con la
opción de optimización activada.

El ejemplo anterior es equivalente a:

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.

Definicón e instanciación de clases

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.

Definición e instanciación de clases (2)

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.

Atributos añadidos dinámicamente

En Python, se pueden añadir atributos a un objeto en cualquier momento escribiendo el


nombre del objeto seguido de un punto, el nombre del atributo y la asignación de un
valor para el mismo, como en el siguiente ejemplo.

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.

La llamada "notación de punto", <nombre de objeto>.<nombre de atributo> se usa,


tanto para crear nuevos atributos, como para acceder a los ya existentes.

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.

En el caso de los atributos de datos, esto se consigue definiendo un método de


inicialización, __init__, que se ejecuta cada vez que se crea un objeto.

Añadir atributos en la inicialización usando __init__

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

El primer parámetro (self en el ejemplo) representa el objeto al que se aplica el método.


En el ejemplo, el método __init__ se usa para inicializar el objeto recién creado (self),
con dos atributos de datos (self.length y self.width) que se inicializan con los parámetros
correspondientes de __init__ (length y 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.

my_rect = Rectangle(1.0, 2.0)


Al método __init__ no se le llama de forma explícita, es llamado automáticamente tras la
creación del objeto y, en esa llamada, el objeto recién creado, se le pasa automáticamente
como primer parámetro, que no está dado explícitamente en la expresión constructora.

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

En el ejemplo, ambos métodos, __init__ y area, tienen un primer parámetro llamado


self que representa el objeto sobre el que actúan dichos métodos.

El nombre self se usa por convención, aunque Python admite que ese primer parámetro
pueda tener cualquier otro nombre.

Cómo llamar a un método

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

El primer parámetro (el objeto vinculado) se omite, ya que se pasa implícitamente al


hacer la vinculación. Cuando un método de un objeto quiere llamar a otro método del
mismo objeto, utiliza su primer parámetro formal (self) para vincular la llamada con el
objeto.

class MyClass:
...

17
def method1(self, a, b):
...
def method2(self, a, b, c):
...
x = self.method1(a, b)
...

La llamada a métodos especiales, como __init__, tiene su propia sintaxis. En el caso de


__init__ es invocado automáticamente cuando se construye el objeto usando la
expresión constructora. Otros métodos especiales son llamados implícitamente en
situaciones específicas, aunque también puedan ser llamados de forma normal en
muchos casos.

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

En Programación Orientada a Objetos, la herencia es un mecanismo que permite definir


nuevas clases a partir de clases existentes.

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.

class MyClass: # In Python 3.x MyClass inherits from object


class MyClass(object): # Equivalent declaration in Python 2.x

La herencia automática de una clase base común es habitual en muchos lenguajes y se


usa para proporcionar algunas características básicas que deben tener todas las clases.
En Python, la herencia de la clase object facilita los métodos de clase, métodos estáticos
y propiedades, determina el "Method Resolution Order", etc.

Inicialización de clases derivadas

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

En el ejemplo, ClassOne tiene un inicializador con un parámetro que se usa para


inicializar el atributo attr1. El inicializador de la clase ClassTwo tiene dos parámetros,
uno que se pasa al inicializador de ClassOne (su superclase) y otro para inicializar un
nuevo atributo. El inicializador de una clase derivada es responsable de proporcionar los
parámetros que necesita el inicializador de su superclase, bien recibiéndolos, a su vez,
como parámetros, bien infiriéndolos a partir de los que recibe.

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

En el ejemplo, el método sum_attributes de ClassTwo tiene acceso al método get_attr1


de ClassOne, pero el atributo __attr1 permanece oculto.

Cómo averiguar la clase de un objeto

Cuando tenemos objetos de diferentes clases y subclases...

class ClassOne:
def __init__(self, attr1_value):
...
class ClassTwo(ClassOne):
def __init__(self, attr1_value, attr2_value):
...
c1 = ClassOne(1)
c2 = ClassTwo(1, 2)

Podemos averiguar si un objeto es instancia de una clase determinada usando la función


isinstance:

print(isinstance(c1, ClassOne)) # Prints True


print(isinstance(c2, ClassOne)) # Prints True
print(isinstance(c1, ClassTwo)) # Prints False

Podemos saber la clase del objeto usando el atributo __class__:

print(type(c1)) # Prints <class 'myclass.ClassOne'>


print(c1.__class__) # Prints <class 'myclass.ClassOne'>

Y podemos saber si una clase es subclase de otra usando la función issubclass:

print(issubclass(myclass.ClassTwo, myclass.ClassOne)) # Prints True

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:

from abc import ABC, abstractmethod

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

El polimorfismo puede definirse como la capacidad de usar una misma interfaz y


obtener comportamientos diferentes, dependiendo del tipo de los objetos con los que se
use.

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"

En Programación Orientada a Objetos, el polimorfismo permite que una clase derivada


pueda sustituir, o redefinir (to override) un método de su superclase, de forma que,
donde se requiera un objeto de la superclase, pueda usarse un objeto de cualquier clase
derivada, y que, cuando se llame a un método redefinido, se use la versión
correspondiente a la subclase específica. Por ejemplo, veamos estas tres clases:

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()

showMessage(obj1) # Prints "This is a message from BaseClass"


showMessage(obj2) # Prints "This is a message from ClassOne"
showMessage(obj3) # Prints "This is a message from ClassTwo"
El polimorfismo se implementa mediante los mecanismos de sustitución de métodos (methods
overriding) y sobrecarga (overloading).

Sustitución de métodos (overriding)

La sustitución de métodos es un mecanismo que permite a una subclase proporcionar su


propia implementación de un método ya implementado en alguna de las clases de las
que hereda.

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";

Cuando un objeto de la clase BaseClass llama al método message, se ejecuta el


definido en BaseClass, pero si lo llama un objeto de tipo ClassOne, se ejecuta el
definido en ClassOne.
Existe la posibilidad de que el método sustituido pueda ser llamado desde un método de
un objeto de tipo ClassOne usando el método super().

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.

En cualquier caso, cuando se sustituye un método mágico, se puede acceder a la


implementación disponible en la superclase, usando super().

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

Cómo definir nuevas excepciones

Cuando ocurre una situación anormal durante la ejecución de un programa (por


ejemplo, un intento de dividir por cero, o un intento de acceder a un fichero que no
existe), se lanza una excepción. Una excepción es un objeto que contiene información
sobre el error.

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()

Y de la misma manera que con las excepciones predefinidas, se puede incluir un


mensaje descriptivo:

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

Cuando creamos un módulo que puede lanzar distintos tipos de excepciones, es


corriente crear una clase base para las excepciones definidas en ese módulo, creando
subclases de la misma para las distintas situaciones de error, formando de esta manera
una jerarquía 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

La sobrecarga de operadores es un tipo de polimorfismo que habilita que el mismo


operador funcione de manera diferente dependiendo del tipo de los datos a los que se
aplique. Por ejemplo, el operador '+' para sumar números enteros o reales o para
concatenar strings u otro tipo de secuencias.

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

Sobrecarga de operadores de comparación

Los siguientes métodos mágicos pueden redefinirse para implementar el funcionamiento


de los operadores de comparación correspondientes:

__eq__(self, other) .- define el comportamiento del operador de


igualdad ==
__ne__(self, other) .- define el comportamiento del operador de
desigualdad !=. Si __eq__ está definido, __ne__ está definido de forma
implícita
__lt__(self, other) .- define el comportamiento del operador menor que
<
__gt__(self, other) .- define el comportamiento del operador mayor que
>
__le__(self, other) .- define el comportamiento del operador menor o
igual <=.
__ge__(self, other) .- define el comportamiento del operador mayor o
igual >=.

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

Si no se hubiese definido el operador __eq__, el ejemplo anterior mostraría False, dado


que compararía si las dos variables referencian el mismo objeto. Los métodos __lt__,
__le__, __gt__, y __ge__ se conocen, generalmente, como métodos de comparación
enriquecida.

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:

from functools import total_ordering

@total_ordering
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

def __lt__(self, other):

return self.num * other.den < self.den * other.num

Nótese que en la implementación del método mágico __lt__ no se ha preguntado por el


tipo del otro objeto con el que se compara. En el caso de la igualdad, podemos afirmar
que, si los objetos no son del mismo tipo, no pueden ser iguales pero, ¿cómo podemos
decidir cuál es menor? Hemos optado por dejar que se produzca un error en esa
situación, cuando se intenta usar la operación con un tipo de datos para el que no está
definida.

Funciones y operadores unarios


__pos__(self) .- implementa el comportamiento del operador unario
+
__neg__(self) .- implementa el comportamiento del operador unario
-
__abs__(self) .- implementa el comportamiento de la función abs()
__invert__(self) .- implementa el comportamiento del operador ~
(complemento bit a bit)
__round__(self, n) .- implementa el comportamiento de la función
round()
__floor__(self) .- implementa el comportamiento de la función
floor()
__ceil__(self) .- implementa el comportamiento de la función
ceil()
__trunc__(self) .- iplementa el comportamiento de la función
trunc()

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)

Operadores aritméticos binarios


__add__(self, other) .- implementa el comportamiento del operador de
adición +.
__sub__(self, other) .- implementa el comportamientod el operador de
sustracción -.
__mul__(self, other) .- implementa el comportamiento del operador de
multiplicación *.
__floordiv__(self, other) .- implementa el comportamiento del operador
de división entera // .
__div__(self, other) .- implementa el comportamiento del operador de
división /.
__mod__(self, other) .- implementa el comportamiento del operador de
módulo %.
__divmod__(self, other) .- implementa el comportamiento de la función
divmod().
__pow__(self, other[, modulo]) .- implementa el comportamiento del
operador de exponenciación **.
__lshift__(self, other) .- implementa el comportamiento del operador
de desplazamiento a la izquierda <<.
__rshift__(self, other) .- implementa el comportamiento del operador
de desplazamiento a la derecha >> .
__and__(self, other) .- implementa el comportamiento del operador &.
__or__(self, other) .- implementa el comportamiento del operador |.
__xor__(self, other) .- implementa el comportamiento del operador ^.

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)

Operadores binarios reflejados

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)

def __radd__(self, other):


return self + other

Sobrecarga de funciones (o métodos)

La sobrecarga de funciones o métodos es un tipo de polimorfismo que permite usar un


mismo nombre de función (o método) con diferentes parámetros en el mismo espacio de
nombres. Muchos lenguajes de programación permiten escribir diferentes funciones con
el mismo nombre, siempre que cambie el número de parámetros o el tipo de algún
parámetro. Otra forma son las funciones con parámetros por omisión.

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.

Por tanto, en Python, tenemos una especie de "funciones polimórficas", en vez de


funciones sobrecargadas. El siguiente ejemplo muestra una típica función "Hello world"
con un parámetro opcional:

def hello(name = None):


if name != None:
print("Hello " + name + "!")
else:
print("Hello!")

Esta función se puede invocar de dos maneras, con y sin parámetro:

hello() # Muestra "Hello!"


hello("David") # Muestra "Hello David!"

Polimorfismo basado en parámetros por omisión

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 side2 == None or side1 == side2:


if angle != 90:
self.kind = "rhombus"
self.angle = angle
else:
self.kind = "square"
else:
self.side2 = side2

if angle != 90:
self.kind = "rhomboid"
self.angle = angle
else:
self.kind = "rectangle"

A continuación, se muestran cuatro formas diferentes de crear un objeto de la clase


Parallelogram, usando uno, dos o tres parámetros:

square = Parallelogram(12)
rhombus = Parallelogram(12, 60)
rectangle = Parallelogram(12, side2 = 8)
rhomboid = Parallelogram(12, 60, 8)

Polimorfismo basado en el tipo de los parámetros

El siguiente ejemplo muestra una función que devuelve un número fraccionario


(racional) o un número complejo, dependiendo de si sus parámetros son de tipo int o
float.

from fractions import Fraction

def numberFunc(number1, number2):


if type(number1) == int and type(number2) == int:
return Fraction(number1, number2)
elif type(number1) == float or type(number2) == float:
return complex(number1, number2)

A continuación, se muestran tres formas de llamar a la función. Obsérvese que si el tipo


de los parámetros no es int o float, la función devuelve None:
29
print(numberFunc(1, 2)) # Muestra 1/2
print(numberFunc(1.0, 2.0)) # Muestra (1+2j)
print(numberFunc("1", "2")) # Muestra None

Iteradores

En Python, un iterador es un objeto que permite recorrer en secuencia un conjunto de


elementos, obteniendo uno en cada iteración. Para hacer esto, debe definir dos métodos
mágicos: __iter__(self) y __next__(self).

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

El método __init__ simplemente establece los parámetros de iteración, el método


__iter__ prepara el objeto para empezar la iteración y lo devuelve. El método __next__
devuelve el elemento actual de la secuencia y avanza al siguiente. La excepción
StopIteration señala el final del recorrido; podemos hacer un iterador infinito si no
lanzamos dicha excepción, pero deberemos manejarlo con cuidado.

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.

Las funciones predefinidas iter y next

Podemos usar un iterador en cualquier sitio donde se requiera un objeto iterable, como
en un bucle for:

for i in MyRange(5, 20, 3):


print(i) # Muestra la secuencia 5, 8, 11, 14, 17, 20
Los bucles for manejan los iteradores por medio de dos funciones predefinidas, iter y next, que
se basan en los métodos mágicos correspondientes, __iter__ y __next__. Un bucle como el
siguiente:
for i in MyRange(5, 20, 3):
print(i)

30
Realmente, funciona como se ve a continuación:

iter_obj = iter(MyRange(5, 20 ,3)) # Se crea un iterador

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.

def range_generator(start, stop, step = 1):


current = start

while current <= stop:


yield current
current += step

La primera ejecución de un generador devuelve un iterador que puede iterarse usando la


función next. Cada llamada a next ejecuta la función generadora hasta la siguiente
sentencia yield. Una sentencia yield pausa la ejecución y devuelve un valor. La
ejecución continúa cuando se vuelve a llamar a la función next.

iter_obj = range_generator(3, 20 ,3)

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.

for i in range_generator(3, 20, 3):


print(i)

Prueba de programas

Las probabilidades de que se cometan errores al desarrollar un programa son elevadas.


Los errores se plasman en defectos en el código que pueden dar lugar a fallos de
funcionamiento cuando se ejecute. Algunos defectos pueden no dar lugar a fallos; por
ejemplo, código que no se ejecuta nunca y, por tanto, no tiene ningún efecto en el
funcionamiento del programa, pero no debería estar.

Las pruebas de programas tienen como objetivo detectar la existencia de posibles


defectos en el código con el menor coste y la máxima anticipación posibles (cuanto más

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

Las pruebas se concretan en la ejecución de una serie de casos de prueba seleccionados


para detectar defectos con la mayor probabilidad. Cada caso de prueba se compone de
un conjunto de datos de entrada y un conjunto de resultados esperados para esos datos.

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.

Diseño general de las pruebas

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.

Con el programa completo se realizan Pruebas de Sistema, en las que intervienen


grupos escogidos de usuarios: pruebas alfa, que pueden realizarse con usuarios
simulados cuando el programa está aún en desarrollo, y pruebas beta, pruebas release
candidate, o Pruebas de aceptación, con el programa terminado, antes de su
lanzamiento definitivo.

Cuando se hace cualquier modificación al código, se aplican Pruebas de Regresión,


que no son más que la repetición de todas o parte de las pruebas ya ejecutadas para ver
si el nuevo cambio afecta a las características preexistentes del programa.

Tipos de prueba

Básicamente, se distinguen dos tipos principales de pruebas:

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.

Las pruebas de caja negra, que se basan exclusivamente en el conocimiento de la


interfaz del código, desconociendo los detalles de su implementación, y se limitan a
comparar que, para todos los casos de prueba, las entradas suministradas producen los
resultados esperados.

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.

Pruebas de caja blanca

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:

1. Se obtiene el grafo del flujo de ejecución del programa.


2. Se calcula la complejidad ciclomática.
3. Se obtiene el conjunto de caminos básicos.
4. Se diseñan casos de prueba que fuercen la ejecución de cada camino.

Grafo del flujo de ejecución

Es un grafo orientado en el que los vértices representan o bien instrucciones o conjuntos


de instrucciones que se ejecutan como una unidad, o bien condiciones. Los vértices que
representan condiciones se conocen como vértices predicados. Un arco dirigido entre
dos vértices representa la posibilidad de que, cuando se acabe la ejecución del primero,
se pase a ejecutar el segundo.

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.

En el caso de haber condiciones múltiples, deben descomponerse, añadiendo un vértice


por cada componente, como en el siguiente ejemplo:

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

Los siguientes caminos se construyen aplicando la misma técnica a los ya encontrados,


hasta que tengamos cubiertas todas las alternativas:

• 1->2->3->4->5->2->6

¿Cómo sabemos que tenemos suficientes caminos? Calculando previamente la


complejidad ciclomática, que nos dice el número de caminos necesarios para cubrir
todos los arcos del grafo. Se puede calcular de varias maneras; una de ellas, es contar el
número de vértices predicado y sumar uno más. En el ejemplo hay dos vértices
36
predicado, el 2 (por la condición de la instrucción while) y el 3 (por la condición de la
instrucción if), luego la complejidad ciclomática del algoritmo es 3, que es el número de
caminos que ya hemos encontrado.

Diseño de casos de prueba

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.

El camino 1->2->6 sólo puede recorrerse cuándo num1 es igual a cero,


independientemente del valor de num2.

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.

El camino 1->2->3->4->5->2->6 es el que se recorre cuando num1 es inicialmente igual


a 1, pero, como se puede observar, todos sus arcos están contenidos en el camino que
acabamos de obtener, por lo que no es necesaria una prueba específica. La complejidad
ciclomática nos dice cuál es el número máximo de pruebas que debemos preparar, pero,
a veces, es posible conseguir el objetivo de cubrir todos los arcos cubriendo un número
menor de caminos.

La siguiente tabla tiene todo esto en cuenta para proponer posibles conjuntos de prueba:

Camino Condiciones de los Posibles datos de Salida


datos prueba esperada
1->2->6 num1 == 0 num1 = 0; num2 = 5 0
1->2->3->5->2->3->4->5- num1 == 2 num1 = 2; num2 = 5 10
>2->6

Pruebas de caja negra

Las pruebas de caja negra, que se basan exclusivamente en el conocimiento de la


interfaz del código, desconociendo los detalles de su implementación, se limitan a
comparar que, para todos los casos de prueba, las entradas suministradas producen los
resultados esperados.

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.

Por ejemplo, supongamos que un sistema informático acepta passwords en forma de


strings con una longitud de 8 a 15 caracteres y tenemos que probar una función para
verificar la longitud de una password pasada como parámetro. La función debe devolver
una string con uno de los tres valores: "SHORT", "CORRECT", "LONG", dependiendo
de si el valor pasado es demasiado corto, tiene una longitud válida, o es demasiado
largo.

Atendiendo a las posibles longitudes del valor pasado, tenemos tres clases de
equivalencia en este problema:

• strings con entre 0 y 7 caracteres (demasiado cortas)


• strings con entre 8 y 15 caracteres (longitud correcta)
• strings con más de 15 caracteres (demasiado largas)

Deberíamos probar la función con al menos una string de cada clase.

Clases de equivalencia válidas y no válidas

En el problema de determinar si una password tiene la longitud correcta, todas las


posibles longitudes de una string son valores de entrada válidos que se dividen en tres
clases: las strings demasiado cortas, las que tienen el tamaño adecuado y las demasiado
largas.

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?

El cero no es una entrada válida para inicializar el denominador de un número racional.


Representaría una clase de equivalencia no válida, que podríamos describir que es una
formada por valores que son válidos de acuerdo con el tipo de datos del parámetro, pero
que no lo son de acuerdo con las especificaciones del problema.

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.

Test Driven Development

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.

La decisión de cómo completar una especificación incompleta dependerá de la gravedad


de las consecuencias de dejarla indeterminada y de la probabilidad razonable de que se
pase un valor inválido.

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.

Tipos de clases de equivalencia

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 ·· ∞]

En este caso, igual que en el de valores válidos para el denominador de un número


racional, se trata de rangos de números enteros. También podría ser un rango de valores
reales (por ejemplo, el rango de valores válidos como radio de un círculo lo forman
todos los números reales mayores que cero), o de otro tipo (por ejemplo, el rango de las
letras mayúsculas [A ·· Z]).

También pueden darse rangos, o pseudorangos, cuando la clase de equivalencia está


formada por un conjunto de valores ordenables, aunque no sean consecutivos. Por
ejemplo, si queremos desarrollar una función para determinar si un número entero es
primo, tenemos tres clases de equivalencia: el rango [−∞ ·· 1], que no son primos, y los
conjuntos formados por los enteros positivos mayores que 1 que son primos (2, 3, 5, 7,
11, 13, ···), y por los números enteros positivos que no son primos (4, 6, 8, 9, 10, ···).

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.

Prueba de los valores límite

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.

El método de prueba de los valores límite complementa el método de prueba de las


clases de equivalencia proponiendo que para cada clase de equivalencia se prueben
siempre dichos valores extremos (sin dejar de probar también algunos valores
centrales). Concretamente, se propone que, para cada clase, se prueben el valor mínimo,
el siguiente al mínimo, el máximo y el anterior al máximo.

Por ejemplo, en el problema de validar si la longitud de una password está entre 8 y 15


caracteres, teníamos tres clases: longitudes de 0 a 7, longitudes de 8 a 15 y longitudes
mayores que 15:

0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 | 16 17 18 ···

Los límites de la primera clase son el 0 y el 7, los de la segunda el 8 y el 15, y la tercera


es una clase abierta que solo tiene límite inferior, el 16. Siguiendo el método de prueba
de los valores límite, para la primera clase, deberíamos probar strings de longitudes: 0,
1, 6 y 7. Además, para que las pruebas fuesen más completas, deberíamos probar algún
valor intermedio, por ejemplo, 3 o 4.

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.

Prueba de los valores límite con rangos de números reales

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, ...?

La cuestión de la precisión también afecta a la comparación entre los resultados


esperados y los obtenidos. Por ello, nunca deberemos comparar directamente si dos
números reales son iguales, como en:

if esperado == obtenido:
...

Para ello, lo que debemos hacer es comparar es si están suficientemente cercanos,


dentro de un margen de error de precisión que se considere admisible, como en:

if abs(esperado - obtenido) < 0.00001:


...

Prueba de contenedores

Entendemos por contenedor cualquier clase o tipo de datos diseñado específicamente


para almacenar elementos de algún tipo. En Python son ejemplos de contenedores: los
diversos tipos de secuencias, los diccionarios y los conjuntos. Los arrays típicos de

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()).

Cuando tratamos con contenedores, la primera dimensión a tener en cuenta es el tamaño


(número de elementos). En el caso de nuestro problema, podemos contar las apariciones
de un valor en listas de cualquier tamaño (mayor o igual que cero); eso, atendiendo a los
métodos de las clases de equivalencia y los valores límite, nos deja que tenemos que
probar, al menos, con:

• una lista vacía (tamaño 0)


• una lista de tamaño 1
• una lista con varios elementos (tamaño n)

A partir de aquí, introducimos las especificidades del problema; en nuestro caso, el


número de veces que el valor a buscar puede aparecer en la lista.

En una lista de tamaño cero, el resultado siempre será cero.

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:

• Una lista con n elementos en la que el valor a buscar aparezca 0 veces


• Una lista con n elementos en la que el valor a buscar aparezca una sola vez
• Una lista con n elementos en la que el valor a buscar aparezca n−1 veces
• Una lista con n elementos en la que el valor a buscar aparezca n veces

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.

Probar una clase consiste en probar el funcionamiento de sus métodos, lo que no es


diferente a probar cualquier otra función, salvo por el hecho de que puede haber
dependencias entre ellos y que, aparte de los resultados que puedan devolver, pueden
modificar propiedades/atributos, cambiando el estado del objeto.

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.

Lo que haremos es probar primero que el método de inicialización inicializa


correctamente las propiedades observables del objeto. Si estas propiedades son
escribibles, la implementación del método de inicialización podría depender de que el
setter de las mismas funcione correctamente, por lo que lo primero que haríamos sería
probar que la ejecución de dicho setter produce que el getter nos devuelva los valores
adecuados (aquí el setter y el getter están vinculados, no se pueden probar por
separado). Cuando tengamos probadas las parejas setter/getter de cada propiedad
observable, probamos el método de inicialización y, cuando este se haya probado,
podemos pasar a probar los métodos que realizan operaciones aritméticas como la suma.

En resumen, la estrategia, para cualquier clase, es identificar sus propiedades y aquellos


métodos, como el de inicialización, que, a priori, no dependen de otros. Desarrollamos
pruebas para estas métodos y los implementamos.

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.

Corredores de pruebas (test runners)

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.

La alternativa es usar un "test runner" que permita ensayar un conjunto de casos de


manera independiente, produciendo un informe final de los casos fallidos y exitosos.

Unittest

Unitest es el framework oficial de pruebas de Python. Está inspirado en JUnit, el


framework de pruebas por excelencia de Java, y es similar a los principales frameworks
de pruebas de otros lenguajes. Facilita la automatización de las pruebas, reusar código
de inicialización y finalización en las distintas pruebas, agrupar casos de prueba e
independizar las pruebas respecto del sistema de generación del informe de las mismas.
Esto lo logra implementando los conceptos fundamentales de las pruebas con un
modelo orientado a objetos, tal como podemos leer en la página oficial (Unit testing
framework):

• 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

El framework unittest genera un informe de pruebas con el formato mostrado en la


imagen:

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

La clase TestCase incluye un gran número de métodos de aserto específicos, además de


.assertEqual():

Método Comprueba que


assertEqual(a, b) a == b
assertNotEqual(a, b) a != b

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([])

La llamada a la función se ejecuta dentro del contexto controlado por el aserto


.assertRaises(), que comprueba que la excepción esperada se produce, fallando en caso
contrario.

Además de .assertRaises(), otros métodos de aserto del mismo estilo son:


assertRaisesRegex(), assertWarns(), assertWarnsRegex(), y assertLogs().

Métodos de aserto (2)

El framework unittest ofrece algunos métodos de aserto más específicos que las
comparaciones de igualdad/desigualdad:

Método Comprueba que


assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a,
round(a-b, 7) != 0
b)
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s)

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

La ejecución de un test puede requerir de una preparación previa (establecer una


conexión con una base de datos, cargar información desde un fichero, crear uno o varios
objetos de una determinada clase, ...) y de una "limpieza" posterior que libere los
recursos adquiridos para la prueba (por ejemplo, cerrando la conexión previamente
abierta con la base de datos).

Los test a ejecutar pueden ser numerosos y, en muchas ocasiones, la preparación y


limpieza requeridas pueden ser iguales para todos los test. Por ello la clase TestCase
proporciona dos métodos llamados .setUp() y .tearDown() que se ejecutan,
respectivamente, antes y después de cada método de prueba. De esta manera, se puede
redefinir el método .setup() para que ejecute las acciones necesarias en la preparación de
cada test y el método .tearDown() para la limpieza posterior.

Existen otros dos métodos, .setUpClass() y .tearDownClass(), que se pueden redefinir


para implementar cualquier preparación que sea necesario realizar una única vez, antes
de la ejecución de cualquier test y cualquier limpieza que haya que realizar al final,
después de que todos los tests hayan sido ejecutados.

Ejecución de tests. Test suite

El método unittest.main() ejecuta los test implementados en el módulo en que se


ejecuta, cuando éste es el módulo principal.

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

Python, como otros lenguajes modernos orientados a objetos, trabaja en base a


referencias. Cuando se inicializa una variable asignándole un objeto de una clase
determinada, lo que la variable almacena es la dirección de ese objeto.

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()

Crea una suite a a partir del nombre de un módulo:

suite1 = loader.loadTestsFromName("sampletests")

Crea una suite a partir del nombre de una TestClass:

suite2 = loader.loadTestsFromName("sampletests.SumEvensTest")

Crea una suite a partir del nombre de un un test case:

suite3 = loader.loadTestsFromName(
"sampletests.SumEvensTest.test_no_evens"
)

Crea una suite a partir de una lista de nombres de test cases:

suite4 = loader.loadTestsFromNames(
["sampletests.SumEvensTest.test_no_evens",
"sampletests.SumEvensTest.test_all_evens"
]
)

Crea una suite a partir de una TestClass:

suite5 = loader.loadTestsFromTestCase(sampletests.SumEvensTest)

Crea una suite a partir de un módulo:

suite6 = loader.loadTestsFromModule(sampletests)

Crea una suite a partir de los contenidos e un directorio (en este caso el actual):

suite7 = loader.discover(".")

Crea una suite combinando las suites anteriores:

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

Las estructuras encadenadas proporcionan una gran flexibilidad en la creación de


estructuras adaptadas a las necesidades de la información a representar, y permiten
implementar estructuras de gran complejidad. En el siguiente ejemplo se han incluido

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

El encadenamiento de objetos mediante campos que sirven para referenciar de unos a


otros permite crear estructuras complejas y flexibles, adaptadas a las características
particulares de un problema concreto. Un campo de aplicación es la implementación de
contenedores, clases cuyos objetos están diseñados para contener colecciones de objetos
de otra clase.

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.

Estructura de una lista encadenada

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

La inicialización de la clase LinkedList requiere, al menos, un atributo para referenciar


al primer nodo de la lista (__first), que se inicializará a None (lista inicialmente vacía):

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

De no tener este atributo, el cálculo de la longitud de la lista requeriría recorrerla


completa:

def __len__(self):
count = 0
current = self.__first

while current != None:


count += 1
current = current.next_node

return count

Inserción por delante

La forma más fácil de insertar en una lista encadenada es hacerlo al principio de la


misma, lo que equivaldría a:

my_list.insert(0, value)

Si my_list referenciara un objeto de tipo list de Python.

Vamos a hacer la inserción por delante en una lista encadenada mediante una operación
específica:

def insert_as_first(self, value):


self.__first = self.Node(value, self.__first)
self.__len += 1

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:

Inserción por detrás

La inserción por detrás es la operación equivalente a:

my_list.append(value)

Siendo my_list una referencia a un objeto de tipo list de Python.

Insertar un elemento al final de una lista encadenada, puede ser muy costoso si hay que
recorrer toda la lista para localizar el final:

def append(self, value):


new_node = self.Node(value) # Se crea un nodo con next_node
None

if self.__first == None: # El nuevo nodo va a ser el


primero de la lista
self.__first = new_node
else: # Empezando al principio de la lista, se busca el último
nodo
current = self.__first

while current.next_node != None: # El último nodo es aquel


cuyo next_node es None
current = current.next_node

current.next_node = new_node # El nuevo nodo se engancha


detrás del último (se convierte en último)
self.__len += 1

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:

def append(self, value):


new_node = self.Node(value)

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:

def insert(self, where, value):


if where == 0:
self.insert_as_first(value)
elif where == len(self):
self.append(value)
elif where < 0 or where > len(self):
raise IndexError
else:
current = self.__first
current_pos = 1

while current_pos < where:


current = current.next_node
current_pos += 1

current.next_node = self.Node(value, current.next_node)


self.__len += 1

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.

Esta es la clave de la inserción enmedio de una lista, necesitamos conocer cuál es el


nodo que va a preceder al que estmos insertando, ya que el nuevo nodo debe ser sucesor
de aquel y predecesor del que, hasta ese momento, era sucesor de aquel.

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.

Borrado de un elemento al principio de una lista encadenada

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

Nótese que, además, en el método mostrado se ha tenido en cuenta la actualización de


los atributos __len y __last para mantener la coherencia de la estructura.

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:

Borrado del último elemento de una lista encadenada

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

while current != self.__last:


prev = current

59
current = current.next_node

# Convertimos el penúltimo elemento en último


prev.next_node = None
self.__last = prev

self.__len -= 1

Igual que la inserción, el borrado de un elemento al final de una lista implementada


mediante arrays es una operación de bajo coste, ya que no hay que buscar el elemento,
al que se accede de forma rápida calculando su posición a partir de la longitud de la
lista, y tampoco hay que desplazar ninguno de los otros elementos.

Borrar un elemento de una lista encadenada por posición.

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:

def delete(self, index):


if index < 0: # Ajustamos los índices negativos
index = self.__len + index

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

while current_index != index:


prev = current
current = current.next_node
current_index += 1

# Saltamos el nodo a borrar


prev.next_node = current.next_node

if current == self.__last: # Por si es el último nodo


self.__last = prev

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

def insert_as_first(self, value):


self.__first = self.Node(value, self.__first)
self.__len += 1

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

while current != None:


value = current.value
current = current.next_node
yield value

De esta manera, cuando en un bucle como el siguiente se llama automáticamente a la


función iter():

l = LinkedList()

for item in l:
print(item)

El método __iter__(), actuando como función generadora, crea y devuelve un iterador


sobre los elementos del conenedor.

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.

Llaves, sangrado y punto y coma.

En Python, una secuencia de instrucciones que pertenece al ámbito de una clase,


función/método o estructura de control, debe sangrarse un nivel respecto a ésta:

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.

Excepcionalmente, las sentencias de control que controlan una única instrucción,


pueden prescindir de las llaves, pero no se aconseja hacerlo.

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.

Salida por consola

En Python, para mostrar un mensaje en la consola se usa la instrucción print():

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:

print("¡Hola mundo!", end = "") # Se muestra:


print("Otro mensaje") # ¡Hola mundo!Otro mensaje

Si se pasan varios valores a mostrar en la instrucción print(), se separan


automáticamente por espacios, a menos que se especifique otro separador:

print("¡Hola", "mundo!") # Se muestra: ¡Hola mundo!


print("¡Hola", "mundo!", sep = "-") # Se muestra: ¡Hola-mundo!(y el
cursor pasará a la siguiente línea)

En Java, para mostrar un mensaje en la consola se puede usar el método


System.out.println():

class HolaMundo {
public static void main(String[] arg) {
System.out.println("¡Hola mundo!");
}
}

El método System.out.println() inserta un salto de línea detrás del mensaje; esto se


puede evitar usando el método System.out.print(). En el siguiente ejemplo no hay salto
de línea detrás del primer mensaje, el siguiente mensaje, si lo hubiera, se mostraría en la
misma línea:

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

En Python, una variable se crea asignándole un valor. El tipo de la variable viene


determinado por el valor que se le asigne. El nombre de una variable es una secuencia
de caracteres de cualquier longitud formada por letras mayúsculas y minúsculas, dígitos
y guiones bajos (_), con la restricción de que no pueden empezar por un dígito. Por
convención, se suelen usar letras minúsculas para formar palabras y guiones bajos para
separar las palabras en caso de nombres compuestos (convención
lower_case_with_underscores):

int_var = 100
float_var = 100.80
str_var = "Hola"

A una variable que se le ha asignado inicialmente un valor de un tipo, se le puede


asignar más tarde un valor de un tipo diferente:

int_var = 3.14

En Java, una variable se crea declarando su tipo y su nombre, pudiendo asignársele, o


no, un valor en el momento de la declaración; en cualquier caso, deberá asignársele un
valor antes de su primer uso. El nombre de una variable es una secuencia de caracteres
de cualquier longitud que debe empezar con una letra, un guión bajo (_) o un símbolo
de dólar ($), y va seguida por cualquier combinación de letras, dígitos, guiones bajos y
símbolos de dólar. Por convención, suelen empezar por una letra minúscula, usar letras
minúsculas para formar palabras, y, en el caso de nombres compuestos, poner la inicial
de cada palabra, menos la primera, en mayúscula (convención mixedCase):

int intVar = 100;


float floatVar = 100.80f;
String strVar = "Hola";

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.

final int pi = 3.14;

Tipos de datos primitivos

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.

Java integra los siguientes tipos denominados primitivos:

• 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)
}
}

El tipo String, que representa secuencias de caracteres, no se considera primitivo pero


recibe un tratamiento particularmente integrado.

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.

Entrada básica de datos

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:

number = int(input("Dame un número entero: "))

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) {

Scanner input = new Scanner(System.in);

66
System.out.print("Dame un número entero: ");
int number = input.nextInt();
System.out.println("Tecleaste " + number);
}
}

Básicamente se requieren 3 pasos:

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

En Python, la sentencia if:else: permite ejecutar una secuencia u otra de instrucciones,


en función de una condición:

if a > b:
print("a es mayor")
else:
print("a es menor o igual")

La cláusula else: puede omitirse.

En Java, la sentencia if-else permite ejecutar una secuencia u otra de instrucciones, en


función de una condición:

if (a > b) {
System.out.println("a es mayor");
} else {
System.out.println("a es menor o igual");
}

La cláusula else puede omitirse.

Nótese que, en Java, la condición de la sentencia if debe ir encerrada entre paréntesis.

Encadenamiento else-if

En Python, cuando hay que evaluar múltiples condiciones alternativas, se puede


encadenar una secuencia de if..else if...else, La palabra reservada elif se usa para acortar
la secuencia else if.

if a > b:
print("a es mayor")
elif b > a:
print("a es menor")
else:
print("son iguales")

En Java, cuando hay que evaluar múltiples condiciones alternativas, se puede


encadenar una secuencia de if..else if...else.
67
if (a > b) {
System.out.println("a es mayor");
} else if (b > a) {
System.out.println("a es menor");
} else {
System.out.println("son iguales");
}

Selección múltiple en Java

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.

En Python, no hay una sentencia equivalente a la switch de Java; para conseguir el


mismo resultado hace falta encadenar una secuencia if...else if...else:

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:

names = ("Juan", "María", "Pedro", "Antonio")

for name in names:


print(name)

En Java, podemos usar una sentencia for para recorrer una secuencia de valores:

String[] names = {"Juan", "María", "Pedro", "Antonio"};

for (String name: names) {


System.out.println(name);
}

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.

La variable names en el ejemplo en Java corresponde a un array de elementos de tipo


String. Un array es un tipo de secuencia indizada, con parecidos con las tuplas y listas
de Python. No debe confundirse con los set de Python, que son conjuntos no ordenados,
aunque se use el mismo símbolo (las llaves) como forma de crear un array.

Sentencia for con rango

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:

for i in range(10, 20, 2):


print(i)

El for con rango se puede usar para recorrer una secuencia accediendo a los elementos a
través de sus índices:

names = ("Juan", "María", "Pedro", "Antonio")

69
for i in range(len(names)):
print(names[i])

En Java, se puede usar una sentencia for para recorrer un rango de valores:

for (int i = 0; i < 10; i++) {


System.out.println(i);
}

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.

for (int i = 10; i < 20; i += 2) {


System.out.println(i);
}

El for con rango se puede usar para recorrer una secuencia accediendo a los elementos a
través de sus índices:

String[] names = {"Juan", "María", "Pedro", "Antonio"};


for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}

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

En Python, podemos definir una función a la que se le pasan unos parámetros y


devuelve un resultado:

def fun(param1, param2):


return param1 + param2

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;
}

public static void main(String[] arg) {


System.out.println(fun(10, 5));
}
}

Arrays

Python ofrece dos tipos de datos básicos para representar secuencias de valores: las
tuplas y las listas:

tuple_of_int = (10, 15, 20, 30, 40)


list_of_int = [10, 15, 20, 30, 40]

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;

Se puede inicializar un array asignándole una secuencia de valores, separados por


comas y encerrados entre llaves:

int[] arrayOfInt = {10, 15, 20, 30, 40};


También se puede inicializar creando un objeto array de un tamaño determinado con el
operador new:
int[] arrayOfInt = new int[5];
en cuyo caso sus elementos se inicializan todos con cero, false o null, de acuerdo con su
tipo.

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;

El número de elementos de un array se puede conocer accediendo al atributo length:

int size = arrayOfInt.length;

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:

my_list = [10, 15, 20, 30, 40]

for i in range(len(my_list)):
print(my_list[i])

En Java se puede hacer lo mismo:

class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};

for (int i = 0; i < myArray.length; i++) {


System.out.println(myArray[i]);
}

}
}

En Python, si no necesitamos conocer los índices o modificar elementos, también se


puede recorrer una secuencia iterando por los elementos:

my_list = [10, 15, 20, 30, 40]

for value in my_list:


print(value)

Análogamente en Java:

class Main {
public static void main(String[] arg) {
int[] myArray = {10, 15, 20, 30, 40};

for (int value: myArray) {


System.out.println(value);
}

72
}
}

Estructuras multidimensionales

En Python, se pueden tener estructuras multidimensionales, por ejemplo, listas de


listas:

my_list = [
[10, 15, 20, 30, 40],
[12, 14, 16, 18],
[11, 17, 23, 29, 31]
]

De la misma manera, en Java se pueden tener arrays de arrays:

int[][] myArray = {
{10, 15, 20, 30, 40},
{12, 14, 16, 18},
{11, 17, 23, 29, 31}
};

Como se observa en el ejemplo, en la declaración hay que poner análogamente tantos


niveles de corchetes como dimensiones tegamos.

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]);

Recorrido de estructuras multidimensionales

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:

# Recorrido por índices


for row_ind in range(len(my_list)):
for col_ind in range(len(my_list[row_ind])):
print(my_list[row_ind][col_ind])

# Recorrido por valores


for row in my_list:
for value in row:
print(value)

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]);
}
}

// Recorrido por valores


for (int[] row: myArray) { // declarar row de una dimensión menos que
myArray
for (int value: row) {
System.out.println(value);
}
}

La clase Arrays (1)

La clase Arrays de Java (paquete java.util) ofrece funciones de clase para realizar varias
operaciones útiles sobre arrays.

El método sort() ordena de menor a mayor un array. Se le puede especificar un rango de


índices para ordenar solo los elementos comprendidos en él:

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}
}
}

El método copyOfRange() copia un trozo de un array, especificado por un rango de


índices, creando un nuevo array:

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}
}
}

La clase Arrays (2)

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

En Python, los objetos de la clase str representan secuencias de caracteres, codificados


en UTF-8 a menos que se declare otra codificación Unicode para el fichero fuente. Se
puede crear una string simplemente encerrado caracteres entre comillas dobles o
simples:

string1 = "Hola mundo"


string2 = 'adiós mundo'

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()

También puede crearse una string convirtiendo un valor de otro tipo:

number = 10
number_str = str(number)

En Java, los objetos de la clase String representan secuencias de caracteres, codificados


en UTF-16. Se puede crear una string simplemente encerrado caracteres entre comillas
dobles:

String string1 = "Hola mundo";

Las comillas simples se reservan para los literales de tipo char (representan un carácter
individual, no una secuencia de caracteres).

char ch1 = 'a';

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:

String empty1 = "";


String empty2 = new String();

También puede crearse una string convirtiendo un valor de otro tipo:

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:

string1 = "Pedro dijo \"hola\"" # incluye comillas dobles en una


string delimitada por comillas dobles

En Java, se usan secuencias de escape que comienzan con una barra invertida para
incluir ciertos caracteres especiales en una string:

string1 = "Pedro dijo \"hola\""; // incluye comillas dobles en una


string

Algunos ejemplos de secuencias de escape (se usan las mismas secuencias en ambos
lenguajes):

\n Nueva línea: coloca el cursor de la pantalla al inicio de la siguiente línea


\t Tabulador horizontal: desplaza el cursor hasta la siguiente posición de tab
\r Retorno de carro: coloca el cursor de la pantalla al inicio de la línea actual
\" Un carácter de doble comilla
\\ Un carácter de barra invertida

Longitud de una string

En Python, la longitud de una string se puede averiguar usando la función len():

print(len(s1)) # 18

En Java, la longitud de una string se puede averiguar usando el método .length():

String s1 = "esto es una string";


System.out.println(s1.length()); // 18

Comparación de strings

En Python, se pueden comparar strings usando los operadores relacionales (<, <=, ==,
!=, >=, >):

s1 = "esto es una string"


s2 = "esto es otra string"
s3 = "esto es una string"
print(s1 == s2) # False
print(s1 == s3) # True

77
print(s1 < s2) # False
print(s1 >= s2) # True

En Java, no se pueden usar los operadores relacionales para comparar strings. Si se


quiere ver si dos strings son iguales, hay que usar el método .equals():

String s1 = "esto es una string";


String s2 = "esto es otra string";
String s3 = "esto es una string";
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equals(s3)); // true

Si no se quiere distinguir entre mayúsculas y minúsculas, se usa el método


.equals.IgnoreCase():

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

El número se calcula restando los valores numéricos de la codificación de la primera


pareja de caracteres que se diferencie, o si no hay diferencia de caracteres, las
longitudes de las strings. Por ejemplo, en la comparación:

"esto es una string".compareTo("esto es otra string")

La primera diferencia es la pareja de caracteres ('u', 'o'), en la posición de índice 8, la


resta 'u' (ASCII 117) - 'o' (ASCII 111) = 6.

También existe la versión .compareToIgnoreCase() si se quiere ignorar la diferencia


entre mayúsculas y minúsculas al comparar.

Concatenación de strings

En Python, se pueden concatenar strings usando el operador +:

s1 = "Hola"
s2 = "mundo"
s3 = s1 + " " + s2 # "Hola mundo"

En Java, se pueden concatenar strings usando el operador +:

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"

Acceso a caracteres y substrings

En Python, se puede acceder a un carácter individual de una string usando, entre


corchetes, su índice de posición; el resultado es una string con un solo carácter:

s1 = "Hola mundo"
c1 = s1[3]; # 'a'

En Java, se puede acceder a un carácter individual de una string usando el método


.charAt() con su índice de posición como parámetro; el resultado es un carácter (tipo
char), no una string:

String s1 = "Hola mundo";


char c1 = s1.charAt(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"

Si se omite el segundo índice de la operación de segmentación, se copia hasta el final:

s1 = "Hola mundo"
s2 = s1[5:]; # "mundo"

En Java, se puede copiar una substring de una string usando el método .substring():

String s1 = "Hola mundo";


String s2 = s1.substring(1, 4); // "ola"

Si se omite el segundo parámetro del método .substring(), se copia hasta el final:

String s1 = "Hola mundo";


String s2 = s1.substring(5); // "mundo"

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)

En Python, se puede localizar la posición de la primera aparición de una substring en


una string usando el método .find():

s1 = "Hola, bienvenidos a mi mundo"


pos = s1.find("bienvenidos") # 6

Si la substring a buscar no se encuentra en la string de búsqueda, el método .find()


devuelve -1.

También se puede usar el método .index(), pero éste lanza una excepción si no se
encuentra la substring buscada.

Se puede hacer que la búsqueda se realice en una substring de la string de búsqueda,


especificando los índices de comienzo y fin:

s1 = "Hola, bienvenidos a mi mundo"


pos = s1.find("bienvenidos", 3, 12) # -1

Si no se especifica el índice de fin, la búsqueda se realiza desde el de comienzo hasta el


final:

s1 = "Hola, bienvenidos a mi mundo"


pos = s1.find("bienvenidos", 3) # 6

En Java, se puede localizar la primera aparición de una substring, o un carácter, en una


string usando el método .indexOf():

String s1 = "Hola, bienvenidos a mi mundo";


int pos = s1.indexOf("bienvenidos"); // 6

Si la substring a buscar no se encuentra en la string de búsqueda, el método .indexOf()


devuelve -1.

Se puede hacer que la búsqueda se realice a partir de una posición determinada:

String s1 = "Hola, bienvenidos a mi mundo";


int pos = s1.indexOf("bienvenidos", 9); // -1

Búsqueda (II)

En Python, se puede localizar la última aparición de una substring en una string usando
el método .rfind():

s1 = "La araña con maña teje la telaraña"


pos = s1.rfind("araña") # 29

80
Se puede indicar que la búsqueda se realice dentro de una substring de la string de
búsqueda:

s1 = "La araña con maña teje la telaraña"


pos = s1.rfind("araña", 0, 9) # 3

En Java, se puede localizar la última aparición de una substring en una string usando el
método .lastIndexOf():

String s1 = "La araña con maña teje la telaraña";


int pos = s1.lastIndexOf("araña"); // 29

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:

String s1 = "La araña con maña teje la telaraña";


int pos = s1.lastIndexOf("araña", 9); // 3

Otras operaciones útiles

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.

Ver si una string comienza/termina de una determinada forma:

Python Java

string.startswith(value, start, end) public boolean startsWith(String chars)

string.endswith(value, start, end) public boolean endsWith(String chars)


Devuelve un valor booleano que indica si la string (self, this) empieza/acaba con la
substring pasada como primer parámetro. En Python se pueden añadir dos parámetros
para indicar que la comparación se circunscriba a una substring de la string self.

Truncar una string:

Python Java

string.strip(characters) public String trim()


Elimina los espacios de ambos extremos de la string y devuelve una nueva string sin
modificar la original. En Python existe la opción de pasar una string con el conjunto de
caracteres que se quiere eliminar en vez de los espacios. En Python existen las variantes
lstrip y rstrip para eliminar los espacios/caracteres solo de un extremo (left o right) de la
string.

Convertir a mayúsculas/minúsculas:

81
Python Java

string.lower() public String toLowerCase()

string.upper() public String toUpperCase()


Dividir una string:
Python Java

string.split(separator, maxsplit) public String[] split(String regex)

public String[] split(String regex, int limit)


La versión de Python devuelve una lista con las substrings resultantes de dividir la
string usando el separador pasado como primer parámetro. La versión de Java es
similar, pero devuelve un array de strings (un tipo de secuencia similar a las listas). En
Python, si no se especifica el separador, se usan espacios, mientras que en Java hay que
especificarlo siempre. En ambos casos, existe la posibilidad de pasar un segundo
parámetro para indicar el número máximo de divisiones a realizar. En Java, el separador
puede indicarse mediante una expresión regular.

Módulo para manejar expresiones regulares

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.

Ejemplo con patrón compilado Ejemplo sin patrón compilado


from re import compile
from re import finditer
text = "una fecha 12-12-2020 y
text = "una fecha 12-12-2020 y
otra fecha 01-10-2019"
otra fecha 01-10-2019"
pattern = compile(r"\d{2}-\d{2}-
pattern = r"\d{2}-\d{2}-\d{4}"
\d{4}")
for match in finditer(pattern,
for match in
text):
pattern.finditer(text):
print(match.group(0))
print(match.group(0))

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

En Python, para encontrar y procesar todas la coincidencias de un patrón en un texto


podemos usar la función finditer() del módulo re, o el método .finditer() de los objetos
de expresión regular de dicho módulo. Ambos devuelven una secuencia de objetos de
coincidencia, cada uno de los cuales tiene información sobre una coincidencia del
patrón en el texto.

from re import compile

text = "una fecha 12-12-2020 y otra fecha 01-10-2019"


pattern = compile(r"\d{2}-\d{2}-\d{4}")

for match in pattern.finditer(text):


print(match.start(), end = "..")
print(match.end(), end = ": ")
print(match.group(0))

En Java, para encontrar y procesar todas la coincidencias de un patrón en un texto


podemos usar el método .find() de la clase Matcher, que devuelve en cada iteración la
siguiente coincidencia encontrada. La clase Matcher ofrece métodos apropiados para
acceder a la información de la coincidencia encontrada en cada iteración. Cuando no
hay más coincidencias, el resultado del método .find() es false.

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

Búsqueda de coincidencias (2)

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));
}
}
}

En Python se puede obtener un esquema parecido, usando la función o el método


.search(), que encuentra la primera coincidencia a partir de una posición determinada
del texto:

from re import compile

text = "una fecha 12-12-2020 y otra fecha 01-10-2019"


pattern = compile(r"\d{2}-\d{2}-\d{4}")
match = pattern.search(text)

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))

match = pattern.search(text, pos = match.end())

Sustitución

En Python, La función re.sub() o el método .sub() de los objetos de expresión regular


sirven para sustituir las coincidencias del patrón en la string de búsqueda por un nuevo
valor:

from re import compile

text = "una fecha 12-12-2020 y otra fecha 01-10-2019"


pattern = compile(r"\d{2}-\d{2}-\d{4}")

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

En Java, el método .replaceAll() de la clase Matcher sirve para sustituir las


coincidencias del patrón en la string de búsqueda por un nuevo valor:

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):

MyClass myObject = new MyClass();

Nótese cómo, en ambos lenguajes, la cabecera de la clase debe ir seguida


obligatoriamente del cuerpo de la clase. Si no quisiéramos incorporar nada a la nueva
clase, en Python usaríamos la instrucción pass en el cuerpo, y en Java un bloque de
llaves vacío.

85
Atributos de datos de objeto, inicialización

En Python los atributos de datos de un objeto se crean en el método de inicialización


__init__(), inicializándolos con los valores adecuados pasados, en su caso, como
parámetros a dicho método:

class MyClass:
def __init__(self, value1, value2):
self.attribute1 = value1
self.attribute2 = value2

my_obj = MyClass(10, 15)

El primer parámetro del método __init__(), generalmente llamado self, representa el


objeto que se está inicializando.

En Java los atributos de un objeto se declaran en el cuerpo de su clase y se inicializan,


en su caso, con los valores pasados al constructor de la clase que se haya utilizado. Un
constructor es un método especial de inicialización que tiene el mismo nombre que la
clase, no devuelve nada, y no tiene modificadores (salvo el de acceso).

class MyClass {
int attribute1;
int attribute2;

public MyClass(int value1, int value2) {


this.attribute1 = value1;
this.attribute2 = value2;
}
}

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.

Hay dos diferencias importantes entre Python y Java en la inicialización y adición de


atributos a un 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

En Python un método de objeto, o método de instancia, es una función que se define en


el cuerpo de una clase y cuyo primer parámetro (generalmente, llamado self) es una
referencia al objeto concreto con el que se ejecuta:

class MyClass:
...

def my_method(self, param):


return (self.attribute1 + self.attribute2) // param

my_obj = MyClass(10, 15)


print(my_obj.my_method(5))

En Java un método de objeto, o método de instancia, es una función que se define en el


cuerpo de una clase sin usar el modificador static, el cual se usa para definir atributos de
clase. A diferencia de Python, en Java no existen funciones "independientes", que no
sean métodos de instancia o de clase.

class MyClass {
...

public int myMethod(int param) {


return (this.attribute1 + this.attribute2) / param;
}
}

class Main {
public static void main(String[] arg) {
MyClass myObject = new MyClass(10, 15);
System.out.println(myObject.myMethod(5));
}
}

Dentro de un método de objeto, la variable automática this referencia al objeto concreto


sobre el que se ejecuta en cada ocasión. Java permite omitirla si no se genera
ambigüedad.

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

def __aux_method(self, param):


return (self.__attribute1 + self.__attribute2) // param

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:

• public: visible para todo el mundo


• protected: visible para la clase, sus clases derivadas (subclases) y las clases definidas en
el mismo package
• private: visible sólo para la clase

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.

En el siguiente ejemplo, el único método público es myMethod:

class MyClass {
private int attribute1;
private int attribute2;

MyClass(int value1, int value2) {


this.attribute1 = value1;
this.attribute2 = value2;
}

private int auxMethod(int param) {


return (this.attribute1 + this.attribute2) / param;
}

public int myMethod(int param) {


return auxMethod(param);
}
}

Las propias clases deben llevar el modificador public para poder usarse fuera del
package en el que están definidas:

public class MyClass {}

Las clases no pueden llevar los modificadores private o protected. Además, en un


archivo pueden definirse varias clases pero sólo puede haber una clase con el
modificador public, que debe tener el mismo nombre que el archivo.

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.

En Python, para ocultar un atributo, se le da un nombre que comience, y no termine,


con dos guiones bajos. Para proporcionar capacidad controlada de acceso y
modificación de los datos una vez ocultados los atributos que los representan, se usan
las propiedades:

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;
}

public String getName() {


return this.name;
}

public void setName(String newName) {


this.name = newName;
}
}

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.

En ambos lenguajes, se puede tener una "propiedad" de solo lectura si no se define el


setter. En Java, se puede tener una propiedad de "solo escritura" si se define el setter
pero no el getter; esto no es posible en Python usando el decorador @property, ya que
la propiedad se crea al decorar el getter con el decorador @property, pero sí se puede
hacer usando el descriptor property, si se asigna None al getter:

name = property(None, set_name)

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

En Python, cualquier variable declarada en el cuerpo de una clase, fuera de cualquier


método, es una variable de clase. Los métodos de clase se declaran con el decorador
@classmethod. El primer parámetro de un método de clase, generalmente llamado cls,
representa a la propia clase:

class MyClass:
__cls_attr = 0

def __init__(self, value1, value2):


self.__obj_attr1 = value1
self.__obj_attr2 = value2

@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;

MyClass(int value1, int value2) {


this.objAttr1 = value1;

90
this.objAttr2 = value2;
}

public static void setClsAttr(int value) {


clsAttr = value;
}

public static int getClsAttr() {


return clsAttr;
}
}
class Main {
public static void main(String[] arg) {
MyClass.setClsAttr(3);
System.out.println(MyClass.getClsAttr());
}
}

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:

class One: # Superclase de Two (y posiblemente de otras)


pass

class Two(One): # Subclase de One


pass

La subclase tienen acceso a todos los atributos no ocultos de su superclase; puede


acceder a los ocultos usando mangled names.

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:

class One {} // Superclase de Two (y posiblemente de otras)

class Two extends One {} // Subclase de One

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:

class One: # Superclase


def __init__(self, value):
self.attr1 = value

class Two(One): # Subclase


def __init__(self, value1, value2):
super().__init__(value1)
self.attr2 = value2

En Java, cuando escribimos el constructor de una subclase, normalmente tenemos que


llamar al constructor de su superclase usando el identificador super:

class One { // Superclase


int attr1;

public One(int value) {


this.attr1 = value;
}
}

class Two extends One { // Subclase


int attr2;

public Two(int value1, int value2) {


super(value1);
this.attr2 = value2;
}
}

En ambos lenguajes, el constructor/inicializador de la subclase debe recibir, además de


los propios, los parámetros a trasladar (normalmente como primera instrucción) al
constructor/inicializador de la superclase, a menos que los calcule a partir de los
propios.

Sustitución de métodos (overriding)

La sustitución de métodos es un mecanismo que permite a una subclase proporcionar su


propia implementación de un método ya implementado en alguna de las clases de las
que hereda.

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";
}
}

class ClassOne extends BaseClass { // Subclase


@Override
public String message() {
return "This is a message from ClassOne";
}
}

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 el identificador super:

class ClassOne extends BaseClass { // Subclase


@Override
public String message() {
return super.message() + " and then a message from ClassOne";
}
}

La mencionada anotación @Override permite al compilador avisarnos si, por error, no


reproducimos bien la signatura, con lo que en realidad estaríamos definiendo un método
distinto, y el original no quedaría oculto sino que sería el que se invocara al usar su
signatura desde un objeto de tipo ClassOne.

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.

final class NoInheritableClass {}

Métodos heredados de Object

En Python, la herencia de la clase base object proporciona un conjunto de métodos


mágicos que pueden ser sustituidos en las clases herederas para desarrollar
comportamientos específicos:

['__repr__', '__call__', '__getattribute__', '__setattr__', '__delattr__', '__init__',


'__new__', 'mro', '__subclasses__', '__prepare__', '__instancecheck__',
'__subclasscheck__', '__dir__', '__sizeof__', '__basicsize__', '__itemsize__', '__flags__',
'__weakrefoffset__', '__base__', '__dictoffset__', '__mro__', '__name__',
'__qualname__', '__bases__', '__module__', '__abstractmethods__', '__dict__', '__doc__',
'__text_signature__', '__hash__', '__str__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__',
'__ge__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__',
'__format__', '__class__']

En Java, heredar de la clase base Object proporciona un conjunto de métodos que


pueden ser sustituidos en las clases herederas para desarrollar comportamientos
específicos:

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

En Python se puede averiguar la clase de un objeto accediendo al atributo __class__ (o


usando la función type()):

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():

def __eq__(self, other):


if isinstance(other, Square):
...
else:
return False

En Java se puede averiguar la clase de un objeto ejecutando el método .getClass():

Square s = new Square(10);


System.out.println(s.getClass());

También se puede averiguar si un objeto es instancia de una clase (directa o


indirectamente), con el operador instanceof:

@Override
public boolean equals(Object other) {
if other instanceof Square {
....
} else {
return false;
}
}

Bloques de control de excepciones

En Python, cuando en una pieza de código se pueden producir excepciones, lo


protegemos encerrándolo en un bloque try, acompañado de uno o varios bloques except
para controlar las excepciones que se produzcan:

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')

En Java, cuando en una pieza de código se pueden producir excepciones, lo


protegemos encerrándolo en un bloque try, acompañado de uno o varios bloques catch
para controlar las excepciones que se produzcan:

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");
}
}
}

Tratar varias excepciones del mismo modo

En Python, si queremos tratar varias excepciones diferentes de la misma forma, las


ponemos en el mismo bloque except, formando una tupla:

try:
dividendo = int(input('Dividendo: '))
divisor = int(input('Divisor : '))
print(dividendo // divisor)
except (ValueError, ZeroDivisionError) as err:
print('Error en la entrada de datos')

En Java, si queremos tratar varias excepciones diferentes de la misma forma, las


ponemos en el mismo bloque catch, separadas por el operador |:

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");
}
}
}

Acceso a la información de las excepciones

En Python podemos acceder a la información asociada a la excepción que se está


tratando en un bloque except. Por un lado, podemos dar un nombre al objeto que
representa la excepción y acceder luego a sus atributos. Por otro, podemos usar la
función sys.exec_info() para obtener información de la excepción que está siendo
tratada.

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.

La función sys.exec_info() devuelve una tupla de tres valores: el tipo de la excepción, su


valor, y el traceback de la pila de ejecución en le momento de ocurrir la excepción.

En Java podemos acceder a la información asociada a la excepción que se está tratando


en un bloque catch usando el nombre dado al objeto que representa la excepción en la
cabecera del bloque.

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.

Control genérico de excepciones

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

En Python podemos añadir al control de excepciones un bloque else y/o un bloque


finally. El bloque else ejecuta código que sólo debe ejecutarse si en el bloque try no se
producen excepciones. El bloque finally ejecuta código que debe ejecutarse siempre
después del bloque try, hayan ocurrido, o no, excepciones:

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")

En Java no existen bloques equivalentes al else de Python, lo que deba ejecutarse


después del bloque try si no ocurre ninguna excepción simplemente se añade al bloque
try. Sí se puede añadir un bloque finally con el mismo objetivo que en Python:

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

En Python podemos lanzar una excepción cuando identificamos una situación


problemática usando la sentencia raise:

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)

En Java podemos lanzar una excepción cuando identificamos una situación


problemática usando la sentencia throw:

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;
}
}
...

Tanto en Python como en Java, se puede relanzar la excepción recibida en un bloque


de control usando, respectivamente, la sentencia raise o la sentencia throw solas, sin
especificar ninguna excepción.

Definición de nuevas clases de excepciones

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:

class MyException extends 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:

public static double average(int[] listOfInt) throws MyException {


if (listOfInt.length == 0) {
throw new MyException();
} else {
return Arrays.stream(listOfInt).sum() / listOfInt.length;
}
}

En el ejemplo, la clase MyException es heredera directa de Exception, por lo que el


método average, que la lanza, pero no la controla, debe declararla. El objetivo de la
declaración es que quien use el método sepa que la excepción puede ocurrir y pueda
poner sus propios controles.

Jerarquía de excepciones en Java:

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:

from abc import ABC, 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.

public abstract class Shape {


public abstract double area();
}

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:

public interface IShape {


public double area();
public double perimeter();
}

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:

public class Square implements IShape {


double side;

Square(double side) {
this.side = side;
}

public double area() {


return side * side;
}

public double perimeter() {


return side * 4;
}
}

Nótese que, en el ejemplo, Square no hereda de IShape, implementa IShape. Java, a


diferencia de Python no permite la herencia múltiple, pero una clase puede heredar de
otra e implementar varias interfaces diferentes al mismo tiempo.

101
public class Square extends Paralelogram implements IShape,
Cloneable{...}

Aparte de las declaraciones de métodos, una interface puede incluir: declaraciones de


constantes, tipos anidados, y la implementación de métodos static y métodos "por
defecto", que llevan el modificacor default y proporcionan una implementación "por
defecto" en caso de que la clase que implementa la interfaz no la proporcione.

public interface IShape {


double area();
double perimeter();

default RGB color() {


return new RGB(0, 0, 0);
}
}

Clases abstractas vs Interfaces

Las clases abstractas y las interfaces son dos mecanismos bastante parecidos. Usaremos
una clase abstracta cuando:

• La queremos como base para un conjunto de clases estrechamente relacionadas que


posiblemente compartan algunos atributos (variables que se puedan modificar y/o
métodos no abstractos) que estarían implementados en dicha clase base.
• Queremos que algunos de los métodos que deben implementar las clases derivadas
puedan llevar los modificadores protected o private (los métodos declarados en una
interface deben ser public).

Por el contrario, usaremos una interfaz cuando:

• Queremos definir un comportamiento que puede ser implementado por diferentes


clases no relacionadas.
• Queremos disponer de una aproximación a la herencia múltiple, posiblemente
heredando de una clase e implementando los comportamientos de varias interfaces.

Cloneable

En Python se utilizan los métodos copy.copy() or copy.deepcopy() para copiar objetos,


ya que la asignación sólo copia referencias. Para controlar cómo se hacen esas copias, la
clase puede implementar los métodos mágicos __copy__() y __deepcopy__().

En Java una clase implementa la interfaz Cloneable para indicarle al método


Object.clone() que es legal que ese método haga una copia campo por campo de las
instancias de esa clase.

Al invocar el método de clonación de Object en una instancia que no implementa la


interfaz Cloneable, se genera la excepción CloneNotSupportedException.

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.

Para copiar un objeto hay que llamar explícitamente al método:

CloneableExample object2 = object1.clone();

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;
}

Ya conocemos de Python que, al redefinir la comparación de igualdad, lo primero que


hay que comprobar es que el objeto a comparar es del mismo tipo que el objeto actual
(this en Java).

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 Python se pueden redefinir los métodos mágicos de comparación (__eq__, __ne__,


__lt__, __gt__, __le__, __ge__) para definir cómo deben funcionar los operadores
relacionales correspondientes con los objetos de esa clase. También se puede conseguir
el mismo efecto de "ordenación total" usando el decorador @functools.total_ordering e
implementando el operador __eq__ y uno de los métodos de comparación enriquecida
__lt__, __le__, __gt__, __ge__.

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.

public class Pareja implements Comparable<Pareja>{


int a, b;
public Pareja(int a, int b){
this.a= a;
this.b= b;
}

public int compareTo(Pareja o){


if(a > o.a) return 1;
if(a < o.a) return -1;
if(b > o.b) return 1;
if(b < o.b) return -1;
return 0;
}
}

La interfaz Comparable<T> declara el método .compareTo(), que devuelve un valor de


tipo int, el cual será un número negativo si el objeto this es menor que el pasado como
argumento, 0 si son iguales y positivo si el objeto this es mayor que el pasado como
argumento.

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

En Java una clase es iterable si implementa la interfaz java.lang.Iterable, que obliga a


definir un método llamado iterator(), que devuelve un iterador sobre un objeto de esa
clase. Un iterador es un objeto de una clase que implementa la interfaz java.util.Iterator,
que obliga a definir dos métodos: next(), que devuelve, uno por uno, los elementos de
una secuencia obtenida a partir del objeto sobre el que itera, y el método hasNext(), que
indica si quedan, o no, elementos por devolver. La clase RangeIterator es iterable y es
su propio iterador.

import java.util.Iterator;
import java.lang.Iterable;

public class RangeIterator implements Iterator<Integer>, Iterable<Inte


ger> {
private int start= 0;
private int step = 0;
private int stop = 0;
private int current;

public RangeIterator(int start, int step, int stop) {


this.start = start;
this.step = step;
this.stop = stop;
}
public Iterator<Integer> iterator() {
this.current = this.start;
return this;
}
public boolean hasNext() {
return this.current < this.stop;
}
public Integer next() {
int result = this.current;
this.current += this.step;
return result;
}
}

Uso de los iteradores

Un iterador se puede usar invocando explícitamente a sus métodos.

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);
}
}
}

Objeto iterable vs objeto iterador

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;

public RangeIterator(Range range) {


this.range = range;
this.current = range.start;
}

public boolean hasNext() {


return this.current < this.range.stop;
}

public Integer next() {


int result = this.current;
this.current += this.range.step;
return result;
}
}

private int start= 0;


private int step = 0;

106
private int stop = 0;

public Range(int start, int step, int stop) {


this.start = start;
this.step = step;
this.stop = stop;
}

public Iterator<Integer> iterator() {


return new RangeIterator(this);
}
}
Esto permite permite tener varios iteradores actuando simultáneamente sobre el mismo
objeto iterable:
public class Main {
public static void main(String[] args) {
Range range = new Range(1, 2, 15);

for (int e: range) { // Primer iterador


int sum = 0;

for (int e1: range) { // Segundo iterador


if (e1 != e) {
sum += e1;
}
}

System.out.println(sum);
}
}
}

107

También podría gustarte