Clases abstractas
En resumen: Una clase abstracta en Python hereda de ABC y no puede tener objetos, y cada clase hija tiene que escribir los métodos marcados con @abstractmethod.
Problema
Cero metros cuadrados
Estás escribiendo un programa que pide baldosas para una terraza. La terraza tiene tres partes: un rectángulo de 4 × 3 m, una parte redonda con un radio de 1 m y un triángulo con una base de 2 m y una altura de 3 m. Hay una clase padre Shape con area(), que devuelve 0, y cada figura lo sobrescribe.
El colega que escribió Triangle se olvidó:
class Triangle(Shape): def __init__(self, base, height): self._base = base self._height = heightterrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]total = 0for s in terrace: total += s.area()print(f"Baldosas: {total:.2f} m²")Baldosas: 15.14 m²Necesitas 18,14 m². El programa no da ningún error, y al pedido le faltan 3 m². El cero de la clase padre ha escondido el método que falta.
¿Y qué es Shape()? Una figura sin forma. Un objeto así no tiene ninguna razón para existir.
La idea
Clase abstracta: una promesa que cumplen las clases hijas
Shape no es una figura real. Lo único que dice es «toda figura tiene un área». Así que hazla abstracta:
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): passABC(Abstract Base Class) yabstractmethodvienen del móduloabc, así que la primera línea esfrom abc import ABC, abstractmethod.- Un método abstracto lleva
@abstractmethodencima. Su cuerpo es solopass, «nada». No puedes dar un área razonable a «una figura en general», así que no das ninguna. - Una clase abstracta hereda de
ABCy no puede tener objetos:Shape()detiene el programa conTypeError. - Cada clase hija tiene que escribir el método. Si no, tampoco puedes crear objetos de ella.
Python no tiene un compilador que revise el programa por adelantado. La comprobación se hace cuando se crea el objeto. Ahora el Triangle olvidado detiene el programa justo en la línea que construye la terraza, antes de que se calcule un pedido incorrecto:
"TypeError: Can't instantiate abstract class Triangle without an implementation for abstract method 'area'".
Un programa que se detiene con un error claro es mejor que un error silencioso en el pedido.Más precisamente, para curiosos
- Una clase hija que no escribe el método abstracto sigue siendo abstracta por sí sola, sin que lo digas. La promesa pasa a sus propias clases hijas.
- Una hija de
ABCsin un solo método abstracto no es abstracta: puedes crear objetos de ella. La prohibición viene de los métodos abstractos. @abstractmethodsinABCno detiene nada:Shape()se crea, yarea()devuelveNone.
Míralo
El triángulo cumple la promesa
Añade el método a Triangle:
class Triangle(Shape): # ... __init__ como antes
def area(self): return self._base * self._height / 2terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]total = 0for s in terrace: total += s.area()print(f"Baldosas: {total:.2f} m²")Baldosas: 18.14 m²El bucle es el mismo. La lista guarda figuras de tres clases, y s.area() llama al método de cada una. El polimorfismo de la lección anterior también funciona con una clase padre abstracta.
Pruébalo
Encuentra el error
Elige la línea con el error.
Shape() es un intento de crear un objeto de una clase abstracta. La primera línea está bien: [None] * 3 crea una lista de tres casillas vacías para figuras, no figuras. En ellas puedes poner un rectángulo, un círculo o un triángulo. Python se detiene en la cuarta línea con TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'.
La idea
Abstracta, pero no vacía
Una clase abstracta puede tener todo lo que tiene una normal: campos, un constructor, métodos normales. Haz que cada figura tenga un nombre y un método que la describa:
class Shape(ABC): def __init__(self, name): self._name = name
@abstractmethod def area(self): pass
def describe(self): return f"{self._name}: {self.area():.2f} m²"describe() se escribe una vez, en Shape. Llama a area(), que en Shape es solo una promesa. No es un problema: describe() siempre se llama sobre una figura real, y una figura real tiene un área.
De Shape no creas objetos, pero su __init__ es útil: lo llaman las clases hijas.
class Circle(Shape): def __init__(self, radius): super().__init__("círculo") self._radius = radius
# ... area() como antesterrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]for s in terrace: print(s.describe())rectángulo: 12.00 m²círculo: 3.14 m²triángulo: 3.00 m²La clase padre escribe lo común (el nombre, la descripción), y las clases hijas escriben lo distinto (el área).
@abstractmethoddef area(self): pass
def describe(self): return f"{self._name}: {self.area():.2f} m²"
s = Circle(1)print(s.describe())Paso a paso, en palabras
- Shape promete area(): @abstractmethod, y el cuerpo es solo pass. No puede haber un objeto que sea solo un Shape.
- El objeto es un círculo: la parte Circle tiene su propio area(). Cumple la promesa.
- s.describe() está escrito en Shape y sirve para toda figura.
- Llama a area(). El objeto es un círculo, así que se ejecuta el método de Circle: π · 1 · 1.
- describe() arma el texto. Lo común está en la clase padre, lo distinto en la clase hija.
- La salida es el área del círculo, con dos cifras después del punto decimal.
Pruébalo
¿De quién es el método?
¿Qué imprimirá el programa?
# Shape y Circle son de la tarjeta de arriba
s = Circle(2)print(s.describe())El objeto es un círculo con un radio de 2. describe() pertenece a Shape, pero el area() de dentro es el del círculo: π · 2 · 2 ≈ 12,57.
Pruébalo
¿Qué falta?
¿Qué tiene que ir en el hueco para que no puedas crear un objeto de Shape?
class Shape():
@abstractmethod
def area(self):
passABC. Sin él, @abstractmethod no detiene nada: Shape() se crea, y area() devuelve None.Ejercicio
Figuras y animales
Resuelve las tareas en tu editor.
1. Área y perímetro. Una clase abstracta Shape con un nombre y dos métodos abstractos: area() y perimeter(). Clases hijas Circle (radio) y Rectangle (ancho y alto). Pon un círculo con un radio de 5 y un rectángulo de 4 × 2,5 en una lista e imprime los dos valores con dos cifras después del punto decimal.
Círculo: área 78.54, perímetro 31.42Rectángulo: área 10.00, perímetro 13.002. Animales. Una clase abstracta Animal con un método abstracto sound() y un método normal breathe() que imprime «El animal respira.». Clases hijas Cat y Dog. Llama al sonido de los dos en un bucle, y después a breathe() del primero.
¡Miau, miau!¡Guau, guau!El animal respira.Ejercicio
Vehículos y actuaciones
3. Vehículos. Una clase abstracta Vehicle con un método abstracto move(). SimpleCar y Bicycle heredan de ella. Bicycle tiene además su propio método pedal().
El coche arrancaLa bicicleta arrancaPedaleogarage[1].pedal(): funciona, porque ahí hay una bicicleta. ¿Y garage[0].pedal()? ¿Por qué el programa se detiene solo en esa línea y no antes?4. Actuaciones. Una clase abstracta Performance con un título y un método abstracto start(). DanceMove y Song heredan de ella. Song tiene además un método sing().
Empieza el baile moonwalkEmpieza la canción 'Bohemian Rhapsody'Canto 'Bohemian Rhapsody'Más tareas
- Triángulo. Añade a la tarea 1 un
Trianglecon sus tres lados. El perímetro es su suma, y el área sale de la fórmula de Herón: s = perímetro / 2, área = √(s(s − a)(s − b)(s − c)). Para los lados 3, 4 y 5 el área es 6.00 y el perímetro es 12.00. - Empleado abstracto. Haz abstracta la clase
Employeede la lección anterior, con unmonthly_pay()abstracto. AñadeFullTime(el salario se paga completo) y conservaManagereIntern. ¿Qué gana el programa? ¿Qué pierde?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Métodos de instancia · Encapsulamiento · Herencia · Polimorfismo
- Continúa en: Interfaces como clases abstractas · Comprobaciones de tipo · Principios de la POO · Lanzar excepciones y crear las tuyas
- Ejercicios sobre el tema: Herencia y polimorfismo