Problem overview
A drawing app keeps different shapes in one list. The task is to write the shape classes and two functions that work on any list of shapes. Every shape inherits from Shape and has two methods: name(), the kind of shape as text, and perimeter(), the length around it.
Shapeitself: the name"Shape"and the perimeter 0.Rectangle(width, height): perimeter2 * (width + height).Square(side): perimeter4 * side.Triangle(a, b, c): perimetera + b + c.
total_perimeter(shapes) returns the sum of all the perimeters, and largest_shape(shapes) returns the name of the shape with the largest perimeter. On a tie, the shape that comes first wins.
For a rectangle 3 by 4, a square of side 5 and a triangle with sides 3, 4 and 5, the perimeters are 14, 20 and 12. The total is 46 and the largest is the square.
Approach: ask the object, never check its kind
The two functions could check what kind each shape is and apply a formula for it, but then every new kind of shape would force a change in both functions. The lesson does it the other way round: each class knows its own formula, and the functions only call shape.perimeter() and shape.name(). This is polymorphism: one call, and each object runs the version from its own class.
A subclass replaces a parent's method by defining one with the same name. Each subclass here also has its own __init__ that stores the sizes in self, because perimeter needs them later.
Step by step
- Write
Shape's defaults.namereturns"Shape"andperimeterreturns 0. The subclasses override both, so these give the common type its two methods. - Store the sizes in each constructor.
Rectanglekeepsself.widthandself.height,Squarekeepsself.side, andTrianglekeepsself.a,self.bandself.c. - Override
nameandperimeterin each class with the text and the formula above. - Total: start
totalat 0, loop over the shapes, add eachshape.perimeter()and returntotalafter the loop. - Largest: start with
largest = shapes[0]. The list is never empty, so the first shape exists. Loop over the shapes and replacelargestonly whenshape.perimeter()is strictly greater thanlargest.perimeter(). Returnlargest.name().
The solution in Python
class Shape:
def name(self):
return "Shape"
def perimeter(self):
return 0
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def name(self):
return "Rectangle"
def perimeter(self):
return 2 * (self.width + self.height)
class Square(Shape):
def __init__(self, side):
self.side = side
def name(self):
return "Square"
def perimeter(self):
return 4 * self.side
class Triangle(Shape):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def name(self):
return "Triangle"
def perimeter(self):
return self.a + self.b + self.c
def total_perimeter(shapes):
total = 0
for shape in shapes:
total += shape.perimeter()
return total
def largest_shape(shapes):
largest = shapes[0]
for shape in shapes:
if shape.perimeter() > largest.perimeter():
largest = shape
return largest.name()
Running it
The three examples, built as lists and passed to both functions:
print(total_perimeter([Rectangle(3, 4), Square(5), Triangle(3, 4, 5)]),
largest_shape([Rectangle(3, 4), Square(5), Triangle(3, 4, 5)]))
print(total_perimeter([Rectangle(2, 4), Square(3), Triangle(4, 4, 4)]),
largest_shape([Rectangle(2, 4), Square(3), Triangle(4, 4, 4)]))
print(total_perimeter([Square(1), Rectangle(1, 1), Triangle(2, 2, 3)]),
largest_shape([Square(1), Rectangle(1, 1), Triangle(2, 2, 3)]))
46 Square
36 Rectangle
15 Triangle
In the second list all three perimeters are 12, so the strict > keeps the first shape, the rectangle. If the test were >=, the last tied shape would win instead.
In the third list the perimeters are 4, 4 and 7. largest starts as the square (4), the 1 by 1 rectangle does not beat it, and the triangle (7) does, so the answer is Triangle. The loop also compares the first shape with itself, which changes nothing.
A new kind of shape
Because the functions use only the common methods, a class added later works without touching them:
class Circle(Shape):
def __init__(self, r):
self.r = r
def name(self):
return "Circle"
def perimeter(self):
return 2 * 3 * self.r
shapes = [Square(2), Circle(3)]
print(total_perimeter(shapes), largest_shape(shapes))
26 Circle
This Circle uses 3 for pi to keep the numbers whole; it only shows that the functions need no change.
Python details worth knowing
Inheritance syntax. class Rectangle(Shape): puts the parent in parentheses. A Rectangle then has every method of Shape and can replace any of them. Because the parent Shape has no __init__ of its own, each subclass defines the constructor it needs.
Attributes live on the object. The line self.width = width creates an attribute on that one rectangle. Two rectangles keep their own sizes, and perimeter reads them back through self whenever it is called.
No type checks needed. Python does not require the functions to declare that they get Shape objects. Any object with a perimeter and a name method would work in the loops. Inheriting from Shape documents the promise and gives sensible defaults.
Defaults that never show. In the tests every object is a rectangle, a square or a triangle, so Shape's own "Shape" and 0 are never printed. They still matter: they make a plain Shape a valid object with both methods, and a new subclass that forgets one of them gets a harmless answer instead of an error.
Edge cases and common mistakes
- Same sizes, different kinds. A 1 by 1 rectangle has the same sides as a square of side 1, but its name is still
"Rectangle". Each object answers for its own class. - Using
>=for the largest. This breaks the tie rule, since the later shape would replace the earlier one. - Forgetting to override
name. A subclass without its ownnameinherits"Shape". - Forgetting
self.in the constructor. Writingwidth = widthonly sets a local variable;perimeterwould then fail with anAttributeError. - Checking the class in the functions. Avoid
if type(shape) == Square. It defeats the purpose and breaks for new shapes.
Complexity
Each function visits every shape once, so the time is O(n) for n shapes. No extra space is used beyond a few variables, since each perimeter takes constant time.
