Start learning free

LESSON 560 OF 569 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in Python: Inheritance and Polymorphism

Practice O(n) Read the full explanation

A drawing app keeps rectangles, squares and triangles in one list. Write the shape types so that code holding that list can ask any shape for its name and its perimeter (the length all the way around it) without knowing which kind of shape it is. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

Every shape class inherits from the class Shape, the common type. Shape has two methods:

  • name() returns the kind of shape as text: "Shape" for a plain Shape.
  • perimeter() returns the length around the shape: 0 for a plain Shape.

Each shape class overrides both:

  • Rectangle(width, height): its name is "Rectangle", its perimeter 2 × (width + height).
  • Square(side): its name is "Square", its perimeter 4 × side.
  • Triangle(a, b, c): its name is "Triangle", its perimeter a + b + c.

In Python a class inherits with class Rectangle(Shape):, and a method with the same name as the parent's replaces it for that class. Each __init__ stores the sizes in self.

Then write two plain functions that get a list of any shapes:

  • total_perimeter(shapes) returns the sum of the perimeters of all the shapes.
  • largest_shape(shapes) returns the name of the shape with the largest perimeter. If several shapes share the largest perimeter, the one that comes first wins.

Both functions must work through the common type's methods only: never check which kind a shape is. Then a new kind of shape, added later, works with them unchanged.

The starter code already has every class, constructor and method header: write their bodies. For each test, the harness reads the shapes, builds them in the order given, calls total_perimeter and largest_shape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    Rectangle(3, 4)
    Square(5)
    Triangle(3, 4, 5)
Output: 46 Square
Explanation: The perimeters are 2 × (3 + 4) = 14, 4 × 5 = 20 and 3 + 4 + 5 = 12. Their total is 46, and the square's 20 is the largest.

Example 2:

Input:
    Rectangle(2, 4)
    Square(3)
    Triangle(4, 4, 4)
Output: 36 Rectangle
Explanation: All three perimeters are 12: 2 × (2 + 4), 4 × 3 and 4 + 4 + 4. On a tie the shape that comes first in the list wins, so the answer is the rectangle.

Example 3:

Input:
    Square(1)
    Rectangle(1, 1)
    Triangle(2, 2, 3)
Output: 15 Triangle
Explanation: The perimeters are 4, 4 and 7, so the last shape, the triangle, is the largest. A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle: each object answers name() for its own class.

 

Constraints:

  • Every test has between 1 and 100 shapes: the list is never empty.
  • Every size (width, height, side, and each side of a triangle) is an integer from 1 to 104.
  • A triangle's three sides always make a real triangle: each side is shorter than the other two together.

Note:

Each function looks at every shape once: O(n) time for n shapes, and no extra space.

STEP 1 OF 20
    Ctrl + Enter to check

    PYTHON · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in Python, explained

    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.

    • Shape itself: the name "Shape" and the perimeter 0.
    • Rectangle(width, height): perimeter 2 * (width + height).
    • Square(side): perimeter 4 * side.
    • Triangle(a, b, c): perimeter a + 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

    1. Write Shape's defaults. name returns "Shape" and perimeter returns 0. The subclasses override both, so these give the common type its two methods.
    2. Store the sizes in each constructor. Rectangle keeps self.width and self.height, Square keeps self.side, and Triangle keeps self.a, self.b and self.c.
    3. Override name and perimeter in each class with the text and the formula above.
    4. Total: start total at 0, loop over the shapes, add each shape.perimeter() and return total after the loop.
    5. Largest: start with largest = shapes[0]. The list is never empty, so the first shape exists. Loop over the shapes and replace largest only when shape.perimeter() is strictly greater than largest.perimeter(). Return largest.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 own name inherits "Shape".
    • Forgetting self. in the constructor. Writing width = width only sets a local variable; perimeter would then fail with an AttributeError.
    • 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.

    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free