Los conceptos de la POO con Python
Clase
1. Declaración
Una clase es la definición de un concepto de negocio, contiene atributos (valores) y métodos (funciones).
En Python, el nombre de una clase no puede comenzar con un número o un signo de puntuación, y no puede ser una palabra clave del leguaje como while o if. Aparte de estas restricciones, Python es muy permisivo con los nombres de clases y variables, incluso permite caracteres acentuados. Sin embargo, esta práctica está extremadamente desaconsejada, debido a problemas de compatibilidad entre diferentes sistemas.
Aquí está la implementación de una clase en Python sin miembros: ni atributo ni método.
class MiClase:
# Por el momento, la clase se declara vacía,
# de ahí el uso de la palabra clave 'pass'.
pass
La palabra clave class está precedida por el nombre de la clase. El cuerpo de la clase está indentado como lo estaría el cuerpo de una función. En un cuerpo de clase, es posible definir:
-
funciones (que se convertirán en métodos de la clase);
-
variables (que se convertirán en atributos de la clase);
-
clases anidadas, internas a la clase principal.
Los métodos y atributos se presentarán en las siguientes secciones del mismo nombre.
Es posible organizar el código de forma aún más precisa, gracias al anidamiento de clases. Tanto como es posible, incluso recomendable, distribuir las clases de una aplicación en varios archivos (llamados "módulos" en Python), puede ser mucho más legible y lógico declarar ciertas clases en una clase "host". La declaración de una clase anidada en otra se parece a esto:
# Clase continente, declarada normalmente.
class Humano :
# Clases contenidas, declaradas normalmente también,
# pero en el cuerpo de la clase continente.
class Mujer :
pass
class Hombre:
pass
La única implicación del anidamiento es que a partir de ahora es necesario pasar por la clase Humano para usar las clases Mujer y Hombre...
Herencia
1. Construcción
Herencia es el mecanismo por el que una clase es propietaria de los miembros de otra clase, con el objetivo de especializarlos o agregar nuevos. La sintaxis de Python es la siguiente:
# Definición de la clase de base.
class Forma:
x = 0
y = 0
# Definición de la clase derivada.
class Circulo(Forma):
# El cuerpo de la clase derivada está vacío.
pass
c = Circulo()
print(c.x, c.y)
>>> 0 0
La clase Circulo hereda de la clase Forma y por tanto recupera los dos atributos x e y, que representan las coordenadas de su centro. Sin embargo, este centro es propio de la instancia de la forma, y estos atributos se deben inicializar en el constructor, como se vio en las secciones anteriores. Cuando se instancia una clase derivada, es su constructor el que llama al constructor de la clase base. En el caso de un constructor predeterminado, como es el caso de la clase Circulo, esta tarea se realiza automáticamente.
Pero en caso de reimplementación del constructor, no debemos olvidarnos de hacer explícitamente esta llamada, corriéndose el riesgo de obtener un comportamiento inesperado:
# Definición de la clase de base.
class Forma:
# Constructor de la clase de base.
def __init__(self):
print("Init Forma")
# Inicialización de los atributos de instancia.
self.x = 0
self.y = 0
# Definición de la clase derivada.
class Circulo(Forma):
# Constructor de la clase derivada, que no llama
# al constructor de la clase de base.
def __init__(self):
print("Init Circulo")
c = Circulo()
>>> Init Circulo # Constructor de Circulo llamado,
# pero no el de Forma.
print(c.x, c.y)
>>> Traceback (most recent call last):
File "/Users/mankalas/a.py", line 14, in <module>
print(c.x, c.y)
AttributeError: 'Circulo' object has no attribute 'x'...
Agregación y composición
1. Agregación
La agregación es una relación contenido-contenedor denominada "débil", en el sentido de que el contenido sobrevive a la destrucción de su contenedor. Para usar el diagrama UML del capítulo Los conceptos de la POO, un automóvil tiene de cero a cinco ruedas (no olvidemos la rueda de repuesto) y una rueda está asociada con cero o un automóvil.
Una versión simple de esta relación podría implementarse como:
class Automovil:
def __init__(self):
# Declaración de una lista que puede
# contener las ruedas del automóvil.
self.ruedas = []
class Rueda:
def __init__(self):
# Declaración de un atributo que contiene una referencia
# al automóvil con el que está relacionada la rueda.
self.automovil = None
# Creación de una lista con cuatro instancias de Rueda.
ruedas = [Rueda() for i in range(4)]
# Creación de una instancia de Automovil.
automovil = Automovil()
# Asignación de la lista de ruedas al Automovil.
automovil.ruedas=ruedas
# Para cada rueda de la lista...
for rueda in ruedas:
# ... se le asocia el automóvil.
rueda.automovil = automovil
print("Instancia de automóvil : {}".format(automovil))
>>> Instancia de automóvil : <__main__.Automovil object at 0x105829ac8>
print("Las 4 ruedas del automóvil {} son {}".format(automovil,
automovil.ruedas))
>>> Las 4 ruedas del automóvil <__main__.Automovil object at
0x105829ac8> son [<__main__.Rueda object at 0x1058299e8>,
<__main__.Rueda object at 0x105829a20>, <__main__.Rueda object at
0x105829a58>, <__main__.Rueda object at 0x105829a90>]
print("El automóvil de la rueda {} es {}".format(ruedas[0],
ruedas[0].automovil))
>>> El automóvil de la rueda <__main__.Rueda object at 0x1058299e8> ...
Excepción
1. Desencadenamiento
Incluso si el sistema de excepción no es estrictamente hablando un componente de la programación orientada a objetos, todavía está presente en los lenguajes orientados a objetos, especialmente en Python.
Hay varias formas de gestionar los errores en un programa (abrir un archivo inexistente, dividir por cero, acceder a un miembro desconocido, etc.). Una función puede devolver un determinado número entero (-1 por ejemplo) para indicar un error.
Una desventaja es que, si ya se supone que la función devuelve un valor lógico, devolver este valor "técnico" de error alteraría la semántica de la función. Por ejemplo, uno esperaría intuitivamente que una función division() devolviera un valor en coma flotante, no un booleano.
Además, si alguna vez la función llamadora no sabe qué hacer con este valor, está obligada a propagarlo ella misma a quien la llamó, y así sucesivamente hasta que se utilice el código de error para solucionar el problema, o al menos informarlo (a través de un mensaje al usuario o una entrada en un diario de registro):
import sys
def division(a, b):
if b == 0:
return False
return a / b
def test(a, b):
resultado = division(a, b)
if resultado == False:
return False
return resultado + 42
resultado = test(55, int(sys.stdin.readline()))
if not resultado:
print("Error: división por cero")
else:
print("El resultado es {}".format(resultado))
En este ejemplo, se le pide al usuario que introduzca un número que se someterá a operaciones aritméticas. La función division() devuelve False si el divisor es 0. Dado que test() suma 42 al resultado de la división, es necesario probarlo para evitar agregar False y 42, lo que causaría un error. Por lo tanto, probamos el valor que devuelve division(). Pero test() no tiene forma de superar este problema: su función es solo sumar 42 al resultado de la división. Entonces, la función no tiene más remedio que transmitir el hecho de que la llamada...
Conceptos de la POO no nativos
1. Clase abstracta
Python no proporciona un mecanismo nativo para declarar clases abstractas porque el duck typing evita el problema. De hecho, en POO, una clase abstracta es útil para definir un contrato común entre varias clases (las clases derivadas concretas) y quienes van a usarlas. El término "contrato" designa los comportamientos y los datos que una clase garantiza poseer y producir. Técnicamente, se trata de sus atributos y métodos, pero la noción de contrato agrega una dimensión de responsabilidad.
Es la clase Forma la que declara la existencia de los métodos de cálculo de perímetro y área, así como de los atributos de coordenadas (x, y). Por tanto, es gracias a ella que el módulo de dibujo podrá colocar las formas concretas (cuadrado, círculo...), en un plano.
Con el duck typing, la declaración de este contrato ya no tiene sentido: tratamos las instancias como formas, pidiéndoles sus coordenadas para ubicarlas. Si alguna de estas instancias no es una forma pero tiene estos atributos de coordenadas, no hay problema, el procesamiento puede continuar. Si grazna, es un pato. Una clase fingió ser una Forma cuando no conocía el contrato establecido por Forma. Entonces, al final, Forma realmente ya no tiene ninguna utilidad, ya que otras clases pueden cumplir el contrato que ella declara sin heredarlo.
Sí pero...
Cuando usamos un atributo x o y de un objeto, esperamos un cierto tipo de retorno, con una cierta importancia de negocio. Ahí es donde Forma certifica que x e y son las coordenadas de una forma geométrica, una instancia de cariotipo (el conjunto de los cromosomas de una célula en biología) proporcionará una interpretación completamente diferente de la llamada...
Enumeración
Las enumeraciones son la mejor manera de representar un conjunto finito de elementos, como los días de la semana o los colores del arco iris. Dado que Python no proporciona de forma nativa una manera de declarar una enumeración, existen varias soluciones. Una de los más simples consiste en crear una clase y declarar atributos de clase, asignándoles enteros de diferentes valores:
class Color:
ROJO = 1
VERDE = 2
AZUL = 3
tomate = Color.ROJO
ensalada = Color.VERDE
Este método todavía tiene algunos inconvenientes. Por un lado, el valor de la enumeración no está tipada correctamente: se trata de un entero:
print(tomate)
>>> 1
print(ensalada)
>>> 2
Esto puede plantear dificultades de depuración (¿qué representa el valor 1? ¿Es realmente un número entero o una enumeración?). También puede fomentar usos aritméticos incorrectos, mientras que una enumeración no se hace para eso claramente:
print(Color.ROJO + Color.VERDE == Color.AZUL)
>>> True
Por otro lado, los valores de enumeración siendo números enteros públicos, un error de descuido o un error tipográfico, pueden producir resultados bastante desastrosos:
Color.ROJO = Color.VERDE
print(Color.VERDE == Color.ROJO)
>>>...
Duck typing
El principio del duck typing consiste en tratar como pato a cualquier entidad que grazne, nade y vuele. Incluso si no es un pato. Por tanto, al leer código escrito en Python, el tipo de objetos manipulados solo se puede deducir a partir del uso que hacemos de ellos.
Sin embargo, en determinados casos de uso, puede resultar tentador realizar determinados tratamientos en función del tipo de la variable manipulada. Python también ofrece la función nativa isinstance(), que devuelve True si el objeto dado como primer parámetro es una instancia de la clase dada como segundo parámetro o una de sus subclases.
Por ejemplo:
for animal in cuidador.animales :
if isinstance(animal, Serpiente) :
cuidador.alimentar(animal, new Insecto())
elif isinstance(animal, Pingüino) :
cuidador.alimentar(animal, new Pez())
El uso de isinstance() es comprensible: para evitar alimentar a un animal con comida que no le corresponde, comprobamos cuál es el tipo de animal y adaptamos la comida en consecuencia. Sin embargo, esta técnica va en contra de la filosofía de Python. De hecho, si en el zoológico tenemos un animal que puede graznar, nadar y volar, pero que no es un pato, lo trataremos de manera diferente a los patos reales, mientras que si respetamos el principio de duck...