Abstract classes
Problem
Zero square meters
You are writing a program that orders tiles for a terrace. The terrace has three parts: a 4 × 3 m rectangle, a round part with a radius of 1 m, and a triangle with a base of 2 m and a height of 3 m. There is a parent Shape with area(), which returns 0, and each shape overrides it.
The colleague who wrote Triangle forgot:
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"Tiles: {total:.2f} m²")Tiles: 15.14 m²You need 18.14 m². The program gives no error, and the order is 3 m² short. The parent's zero has hidden the missing method.
And what is Shape()? A shape with no shape. An object like that has no reason to exist.
The idea
Abstract class: a promise the child classes keep
Shape is not a real shape. All it says is "every shape has an area". So make it abstract:
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): passABC(Abstract Base Class) andabstractmethodcome from theabcmodule, so the first line isfrom abc import ABC, abstractmethod.- An abstract method has
@abstractmethodabove it. Its body is onlypass, "nothing". You cannot give a sensible area to "a shape in general", so you give none. - An abstract class inherits from
ABCand cannot have objects:Shape()stops the program withTypeError. - Every child class has to write the method. Otherwise you cannot make objects from it either.
Python has no compiler that looks over the program ahead of time. The check happens when the object is created. Now the forgotten Triangle stops the program right on the line that builds the terrace, before a wrong order gets calculated:
"TypeError: Can't instantiate abstract class Triangle without an implementation for abstract method 'area'".
A program that stops with a clear error is better than a silent error in the order.More precisely, if you are curious
- A child class that does not write the abstract method stays abstract by itself, without you saying so. The promise passes on to its own child classes.
- A child of
ABCwithout a single abstract method is not abstract: you can make objects from it. The ban comes from the abstract methods. @abstractmethodwithoutABCstops nothing:Shape()gets created, andarea()returnsNone.
See it
The triangle keeps the promise
Add the method to Triangle:
class Triangle(Shape): # ... __init__ as before
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"Tiles: {total:.2f} m²")Tiles: 18.14 m²The loop is the same. The list holds shapes of three classes, and s.area() calls the method of each one. The polymorphism from the last lesson works with an abstract parent too.
Try it
Find the bug
Pick the line with the bug.
Shape() is an attempt to create an object of an abstract class. The first line is fine: [None] * 3 creates a list of three empty slots for shapes, not shapes. You can put a rectangle, a circle or a triangle in them. Python stops on the fourth line with TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'.
The idea
Abstract, but not empty
An abstract class can have everything an ordinary one has: fields, a constructor, ordinary methods. Let every shape have a name and a method that describes it:
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() is written once, in Shape. It calls area(), which in Shape is only a promise. That is not a problem: describe() is always called on a real shape, and a real shape has an area.
You do not make objects from Shape, but its __init__ is useful: the child classes call it.
class Circle(Shape): def __init__(self, radius): super().__init__("circle") self._radius = radius
# ... area() as beforeterrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]for s in terrace: print(s.describe())rectangle: 12.00 m²circle: 3.14 m²triangle: 3.00 m²The parent writes what is shared (the name, the description), and the child classes write what is different (the area).
@abstractmethoddef area(self): pass
def describe(self): return f"{self._name}: {self.area():.2f} m²"
s = Circle(1)print(s.describe())Step by step, in words
- Shape promises area(): @abstractmethod, and the body is only pass. There can be no object that is only a Shape.
- The object is a circle: the Circle part has its own area(). It keeps the promise.
- s.describe() is written in Shape and works for every shape.
- It calls area(). The object is a circle, so the Circle method runs: π · 1 · 1.
- describe() puts the text together. What is shared lives in the parent, what differs lives in the child class.
- The output is the area of the circle, with two digits after the decimal point.
Try it
Whose method?
What will the program print?
# Shape and Circle are from the card above
s = Circle(2)print(s.describe())The object is a circle with a radius of 2. describe() belongs to Shape, but the area() inside it is the circle's: π · 2 · 2 ≈ 12.57.
Try it
What is missing?
What has to go in the blank so that you cannot create an object of Shape?
class Shape():
@abstractmethod
def area(self):
passABC. Without it, @abstractmethod stops nothing: Shape() gets created, and area() returns None.Exercise
Shapes and animals
Solve the tasks in your editor.
1. Area and perimeter. An abstract class Shape with a name and two abstract methods: area() and perimeter(). Child classes Circle (radius) and Rectangle (width and height). Put a circle with a radius of 5 and a 4 × 2.5 rectangle in a list and print both values with two digits after the decimal point.
Circle: area 78.54, perimeter 31.42Rectangle: area 10.00, perimeter 13.002. Animals. An abstract class Animal with an abstract method sound() and an ordinary method breathe() that prints "The animal breathes." Child classes Cat and Dog. Call the sound of both in a loop, then breathe() on the first one.
Meow, meow!Woof, woof!The animal breathes.Exercise
Vehicles and performances
3. Vehicles. An abstract class Vehicle with an abstract method move(). SimpleCar and Bicycle inherit from it. Bicycle also has its own method pedal().
The car sets offThe bicycle sets offPedalinggarage[1].pedal(): it works, because there is a bicycle there. And garage[0].pedal()? Why does the program stop only on that line and not earlier?4. Performances. An abstract class Performance with a title and an abstract method start(). DanceMove and Song inherit from it. Song also has a method sing().
Starting the dance moonwalkStarting the song 'Bohemian Rhapsody'Singing 'Bohemian Rhapsody'More tasks
- Triangle. Add
Trianglewith its three sides to task 1. The perimeter is their sum, and the area comes from Heron's formula: s = perimeter / 2, area = √(s(s − a)(s − b)(s − c)). For sides 3, 4 and 5 the area is 6.00 and the perimeter is 12.00. - Abstract employee. Make
Employeefrom the last lesson abstract, with an abstractmonthly_pay(). AddFullTime(the salary is paid in full) and keepManagerandIntern. What does the program gain? What does it lose?
Test