Menu iconMenu iconPython Programming Unlocked for Beginners
Python Programming Unlocked for Beginners

Chapter 8: Object-Oriented Programming

8.4: Polymorphism

Polymorphism is an extremely important concept in object-oriented programming because it allows us to use objects from different classes as if they were objects of the same class, thus making code more flexible and reusable. By treating objects as if they were part of the same class, we can perform the same operations on them, even if they have different internal structures or methods. 

In Python, polymorphism can be achieved in a number of ways. One method is through method overloading, which involves defining multiple methods with the same name but different parameters. Another way is through method overriding, where a subclass method replaces a method of the same name in its parent class. Finally, duck typing is a concept in Python where the type of an object is determined by its behavior rather than its class, allowing for greater flexibility in code design. 

By using polymorphism in our code, we can create more robust and scalable applications that are able to handle a wide range of input data and perform a variety of operations depending on the objects used. In short, polymorphism is a cornerstone of object-oriented programming and a key tool for software developers to ensure that their code is both flexible and efficient.

8.4.1: Method Overloading:

Method overloading is a programming concept that allows a class to have multiple methods with the same name but different arguments. This feature is not supported in Python in the traditional sense, but there are workarounds that can achieve similar functionality. One such workaround is to provide default values for arguments. This can be helpful when you want to provide a default behavior for a method, but still allow users to override it if they need to. 

Another way to achieve similar functionality in Python is by using variable-length argument lists. You can use args and *kwargs to pass variable-length arguments to a method. This is useful when you are not sure how many arguments a method will need to accept, or when you want to provide a flexible interface for users to interact with your code.

8.4.2: Method Overriding:

Method overriding is a key technique in object-oriented programming that enables a subclass to inherit methods and attributes from its superclass while still allowing it to customize its behavior. This is accomplished by providing a new implementation for a method that has already been defined in the superclass. By doing so, the subclass can extend and modify the functionality of the method to meet its specific needs.

The benefits of method overriding are numerous. First and foremost, it allows for greater flexibility in the design of a program. By being able to customize the behavior of inherited methods, subclasses can tailor their functionality to better fit the specific requirements of their individual use cases. Additionally, method overriding promotes code reuse by enabling subclasses to inherit and modify existing code rather than having to recreate it from scratch.

However, it is important to note that method overriding should be used judiciously. Overriding too many methods can lead to code that is difficult to understand and maintain, and can also lead to unexpected behavior if not done properly. Therefore, it is important to carefully consider the design of a program and the requirements of its use cases before using method overriding.

class Animal:
    def speak(self):
        return "An animal makes a sound"

class Dog(Animal):
    def speak(self):
        return "A dog barks"

animal = Animal()
dog = Dog()

print(animal.speak())  # Output: An animal makes a sound
print(dog.speak())     # Output: A dog barks

8.4.3: Duck Typing:

Duck typing is a programming concept that allows you to use an object based on its behavior rather than its class. This means that you can treat any object like a duck, as long as it walks like a duck and quacks like a duck. In other words, if an object behaves like a duck (it has the required methods and properties), you can treat it as a duck, regardless of its actual class. 

Python is a language that makes extensive use of duck typing to achieve polymorphism, which allows you to write code that can work with objects of different classes without having to know their exact types in advance. This flexibility is one of the key strengths of Python and has made it a popular choice for developers working on projects with complex, dynamic data structures.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * (self.radius ** 2)

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def get_area(shape):
    return shape.area()

circle = Circle(5)
rectangle = Rectangle(4, 6)

print(get_area(circle))     # Output: 78.5
print(get_area(rectangle))  # Output: 24

In the example above, the get_area() function can calculate the area of any shape object that has an area() method, regardless of the shape's class. This demonstrates polymorphism in action through duck typing.

Exercise 8.4.1: Method Overloading

Title: Calculate the area of different shapes

Create a class Shape that has a method area() which accepts different numbers of arguments to calculate the area of different shapes (circle and rectangle).

Instructions:

  1. Create a class Shape.
  2. Implement an area() method that accepts different numbers of arguments.
  3. If there's one argument, treat it as the radius of a circle and calculate the area of the circle.
  4. If there are two arguments, treat them as the width and height of a rectangle and calculate the area of the rectangle.
  5. If no arguments or more than two arguments are provided, raise a ValueError with an appropriate error message.
class Shape:
    def area(self, *args):
        if len(args) == 1:
            radius = args[0]
            return 3.14 * (radius ** 2)
        elif len(args) == 2:
            width, height = args
            return width * height
        else:
            raise ValueError("Invalid number of arguments")

shape = Shape()
print(shape.area(5))          # Output: 78.5
print(shape.area(4, 6))       # Output: 24
try:
    print(shape.area())
except ValueError as e:
    print(e)                  # Output: Invalid number of arguments

Exercise 8.4.2: Method Overriding

Title: Custom __str__ method for Person and Employee

Create a class Person and a subclass Employee. Both classes should have a custom __str__ method to return a string representation of the object.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Implement a custom __str__ method for the Person class that returns the full name.
  3. Create a subclass Employee that inherits from Person and has an additional attribute position.
  4. Implement a custom __str__ method for the Employee class that returns the full name and position.
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, position):
        super().__init__(first_name, last_name)
        self.position = position

    def __str__(self):
        return f"{super().__str__()}, Position: {self.position}"

person = Person("John", "Doe")
employee = Employee("Jane", "Doe", "Software Engineer")

print(person)     # Output: John Doe
print(employee)   # Output: Jane Doe, Position: Software Engineer

Exercise 8.4.3: Duck Typing

Title: Implement a SoundMaker function

Description: Create a SoundMaker function that accepts an object and calls its make_sound() method, demonstrating duck typing.

Instructions:

  1. Create a class Dog with a method make_sound() that returns the string "Woof!".
  2. Create a class Cat with a method make_sound() that returns the string "Meow!".
  3. Implement a function SoundMaker() that accepts an object and calls its make_sound() method.
  4. Test the SoundMaker() function with instances of both Dog and Cat.
class Dog:
    def make_sound(self):
        return "Woof!"

class Cat:
    def make_sound(self):
        return "Meow!"

def SoundMaker(animal):
    return animal.make_sound()

dog = Dog()
cat = Cat()

print(SoundMaker(dog))  # Output: Woof!
print(SoundMaker(cat))  # Output: Meow!

In this exercise, we have created two classes, Dog and Cat, each with their own make_sound() method. The SoundMaker() function takes an object as an argument and calls its make_sound() method without needing to know the exact type of the object. This demonstrates the concept of duck typing in Python.

8.4: Polymorphism

Polymorphism is an extremely important concept in object-oriented programming because it allows us to use objects from different classes as if they were objects of the same class, thus making code more flexible and reusable. By treating objects as if they were part of the same class, we can perform the same operations on them, even if they have different internal structures or methods. 

In Python, polymorphism can be achieved in a number of ways. One method is through method overloading, which involves defining multiple methods with the same name but different parameters. Another way is through method overriding, where a subclass method replaces a method of the same name in its parent class. Finally, duck typing is a concept in Python where the type of an object is determined by its behavior rather than its class, allowing for greater flexibility in code design. 

By using polymorphism in our code, we can create more robust and scalable applications that are able to handle a wide range of input data and perform a variety of operations depending on the objects used. In short, polymorphism is a cornerstone of object-oriented programming and a key tool for software developers to ensure that their code is both flexible and efficient.

8.4.1: Method Overloading:

Method overloading is a programming concept that allows a class to have multiple methods with the same name but different arguments. This feature is not supported in Python in the traditional sense, but there are workarounds that can achieve similar functionality. One such workaround is to provide default values for arguments. This can be helpful when you want to provide a default behavior for a method, but still allow users to override it if they need to. 

Another way to achieve similar functionality in Python is by using variable-length argument lists. You can use args and *kwargs to pass variable-length arguments to a method. This is useful when you are not sure how many arguments a method will need to accept, or when you want to provide a flexible interface for users to interact with your code.

8.4.2: Method Overriding:

Method overriding is a key technique in object-oriented programming that enables a subclass to inherit methods and attributes from its superclass while still allowing it to customize its behavior. This is accomplished by providing a new implementation for a method that has already been defined in the superclass. By doing so, the subclass can extend and modify the functionality of the method to meet its specific needs.

The benefits of method overriding are numerous. First and foremost, it allows for greater flexibility in the design of a program. By being able to customize the behavior of inherited methods, subclasses can tailor their functionality to better fit the specific requirements of their individual use cases. Additionally, method overriding promotes code reuse by enabling subclasses to inherit and modify existing code rather than having to recreate it from scratch.

However, it is important to note that method overriding should be used judiciously. Overriding too many methods can lead to code that is difficult to understand and maintain, and can also lead to unexpected behavior if not done properly. Therefore, it is important to carefully consider the design of a program and the requirements of its use cases before using method overriding.

class Animal:
    def speak(self):
        return "An animal makes a sound"

class Dog(Animal):
    def speak(self):
        return "A dog barks"

animal = Animal()
dog = Dog()

print(animal.speak())  # Output: An animal makes a sound
print(dog.speak())     # Output: A dog barks

8.4.3: Duck Typing:

Duck typing is a programming concept that allows you to use an object based on its behavior rather than its class. This means that you can treat any object like a duck, as long as it walks like a duck and quacks like a duck. In other words, if an object behaves like a duck (it has the required methods and properties), you can treat it as a duck, regardless of its actual class. 

Python is a language that makes extensive use of duck typing to achieve polymorphism, which allows you to write code that can work with objects of different classes without having to know their exact types in advance. This flexibility is one of the key strengths of Python and has made it a popular choice for developers working on projects with complex, dynamic data structures.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * (self.radius ** 2)

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def get_area(shape):
    return shape.area()

circle = Circle(5)
rectangle = Rectangle(4, 6)

print(get_area(circle))     # Output: 78.5
print(get_area(rectangle))  # Output: 24

In the example above, the get_area() function can calculate the area of any shape object that has an area() method, regardless of the shape's class. This demonstrates polymorphism in action through duck typing.

Exercise 8.4.1: Method Overloading

Title: Calculate the area of different shapes

Create a class Shape that has a method area() which accepts different numbers of arguments to calculate the area of different shapes (circle and rectangle).

Instructions:

  1. Create a class Shape.
  2. Implement an area() method that accepts different numbers of arguments.
  3. If there's one argument, treat it as the radius of a circle and calculate the area of the circle.
  4. If there are two arguments, treat them as the width and height of a rectangle and calculate the area of the rectangle.
  5. If no arguments or more than two arguments are provided, raise a ValueError with an appropriate error message.
class Shape:
    def area(self, *args):
        if len(args) == 1:
            radius = args[0]
            return 3.14 * (radius ** 2)
        elif len(args) == 2:
            width, height = args
            return width * height
        else:
            raise ValueError("Invalid number of arguments")

shape = Shape()
print(shape.area(5))          # Output: 78.5
print(shape.area(4, 6))       # Output: 24
try:
    print(shape.area())
except ValueError as e:
    print(e)                  # Output: Invalid number of arguments

Exercise 8.4.2: Method Overriding

Title: Custom __str__ method for Person and Employee

Create a class Person and a subclass Employee. Both classes should have a custom __str__ method to return a string representation of the object.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Implement a custom __str__ method for the Person class that returns the full name.
  3. Create a subclass Employee that inherits from Person and has an additional attribute position.
  4. Implement a custom __str__ method for the Employee class that returns the full name and position.
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, position):
        super().__init__(first_name, last_name)
        self.position = position

    def __str__(self):
        return f"{super().__str__()}, Position: {self.position}"

person = Person("John", "Doe")
employee = Employee("Jane", "Doe", "Software Engineer")

print(person)     # Output: John Doe
print(employee)   # Output: Jane Doe, Position: Software Engineer

Exercise 8.4.3: Duck Typing

Title: Implement a SoundMaker function

Description: Create a SoundMaker function that accepts an object and calls its make_sound() method, demonstrating duck typing.

Instructions:

  1. Create a class Dog with a method make_sound() that returns the string "Woof!".
  2. Create a class Cat with a method make_sound() that returns the string "Meow!".
  3. Implement a function SoundMaker() that accepts an object and calls its make_sound() method.
  4. Test the SoundMaker() function with instances of both Dog and Cat.
class Dog:
    def make_sound(self):
        return "Woof!"

class Cat:
    def make_sound(self):
        return "Meow!"

def SoundMaker(animal):
    return animal.make_sound()

dog = Dog()
cat = Cat()

print(SoundMaker(dog))  # Output: Woof!
print(SoundMaker(cat))  # Output: Meow!

In this exercise, we have created two classes, Dog and Cat, each with their own make_sound() method. The SoundMaker() function takes an object as an argument and calls its make_sound() method without needing to know the exact type of the object. This demonstrates the concept of duck typing in Python.

8.4: Polymorphism

Polymorphism is an extremely important concept in object-oriented programming because it allows us to use objects from different classes as if they were objects of the same class, thus making code more flexible and reusable. By treating objects as if they were part of the same class, we can perform the same operations on them, even if they have different internal structures or methods. 

In Python, polymorphism can be achieved in a number of ways. One method is through method overloading, which involves defining multiple methods with the same name but different parameters. Another way is through method overriding, where a subclass method replaces a method of the same name in its parent class. Finally, duck typing is a concept in Python where the type of an object is determined by its behavior rather than its class, allowing for greater flexibility in code design. 

By using polymorphism in our code, we can create more robust and scalable applications that are able to handle a wide range of input data and perform a variety of operations depending on the objects used. In short, polymorphism is a cornerstone of object-oriented programming and a key tool for software developers to ensure that their code is both flexible and efficient.

8.4.1: Method Overloading:

Method overloading is a programming concept that allows a class to have multiple methods with the same name but different arguments. This feature is not supported in Python in the traditional sense, but there are workarounds that can achieve similar functionality. One such workaround is to provide default values for arguments. This can be helpful when you want to provide a default behavior for a method, but still allow users to override it if they need to. 

Another way to achieve similar functionality in Python is by using variable-length argument lists. You can use args and *kwargs to pass variable-length arguments to a method. This is useful when you are not sure how many arguments a method will need to accept, or when you want to provide a flexible interface for users to interact with your code.

8.4.2: Method Overriding:

Method overriding is a key technique in object-oriented programming that enables a subclass to inherit methods and attributes from its superclass while still allowing it to customize its behavior. This is accomplished by providing a new implementation for a method that has already been defined in the superclass. By doing so, the subclass can extend and modify the functionality of the method to meet its specific needs.

The benefits of method overriding are numerous. First and foremost, it allows for greater flexibility in the design of a program. By being able to customize the behavior of inherited methods, subclasses can tailor their functionality to better fit the specific requirements of their individual use cases. Additionally, method overriding promotes code reuse by enabling subclasses to inherit and modify existing code rather than having to recreate it from scratch.

However, it is important to note that method overriding should be used judiciously. Overriding too many methods can lead to code that is difficult to understand and maintain, and can also lead to unexpected behavior if not done properly. Therefore, it is important to carefully consider the design of a program and the requirements of its use cases before using method overriding.

class Animal:
    def speak(self):
        return "An animal makes a sound"

class Dog(Animal):
    def speak(self):
        return "A dog barks"

animal = Animal()
dog = Dog()

print(animal.speak())  # Output: An animal makes a sound
print(dog.speak())     # Output: A dog barks

8.4.3: Duck Typing:

Duck typing is a programming concept that allows you to use an object based on its behavior rather than its class. This means that you can treat any object like a duck, as long as it walks like a duck and quacks like a duck. In other words, if an object behaves like a duck (it has the required methods and properties), you can treat it as a duck, regardless of its actual class. 

Python is a language that makes extensive use of duck typing to achieve polymorphism, which allows you to write code that can work with objects of different classes without having to know their exact types in advance. This flexibility is one of the key strengths of Python and has made it a popular choice for developers working on projects with complex, dynamic data structures.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * (self.radius ** 2)

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def get_area(shape):
    return shape.area()

circle = Circle(5)
rectangle = Rectangle(4, 6)

print(get_area(circle))     # Output: 78.5
print(get_area(rectangle))  # Output: 24

In the example above, the get_area() function can calculate the area of any shape object that has an area() method, regardless of the shape's class. This demonstrates polymorphism in action through duck typing.

Exercise 8.4.1: Method Overloading

Title: Calculate the area of different shapes

Create a class Shape that has a method area() which accepts different numbers of arguments to calculate the area of different shapes (circle and rectangle).

Instructions:

  1. Create a class Shape.
  2. Implement an area() method that accepts different numbers of arguments.
  3. If there's one argument, treat it as the radius of a circle and calculate the area of the circle.
  4. If there are two arguments, treat them as the width and height of a rectangle and calculate the area of the rectangle.
  5. If no arguments or more than two arguments are provided, raise a ValueError with an appropriate error message.
class Shape:
    def area(self, *args):
        if len(args) == 1:
            radius = args[0]
            return 3.14 * (radius ** 2)
        elif len(args) == 2:
            width, height = args
            return width * height
        else:
            raise ValueError("Invalid number of arguments")

shape = Shape()
print(shape.area(5))          # Output: 78.5
print(shape.area(4, 6))       # Output: 24
try:
    print(shape.area())
except ValueError as e:
    print(e)                  # Output: Invalid number of arguments

Exercise 8.4.2: Method Overriding

Title: Custom __str__ method for Person and Employee

Create a class Person and a subclass Employee. Both classes should have a custom __str__ method to return a string representation of the object.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Implement a custom __str__ method for the Person class that returns the full name.
  3. Create a subclass Employee that inherits from Person and has an additional attribute position.
  4. Implement a custom __str__ method for the Employee class that returns the full name and position.
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, position):
        super().__init__(first_name, last_name)
        self.position = position

    def __str__(self):
        return f"{super().__str__()}, Position: {self.position}"

person = Person("John", "Doe")
employee = Employee("Jane", "Doe", "Software Engineer")

print(person)     # Output: John Doe
print(employee)   # Output: Jane Doe, Position: Software Engineer

Exercise 8.4.3: Duck Typing

Title: Implement a SoundMaker function

Description: Create a SoundMaker function that accepts an object and calls its make_sound() method, demonstrating duck typing.

Instructions:

  1. Create a class Dog with a method make_sound() that returns the string "Woof!".
  2. Create a class Cat with a method make_sound() that returns the string "Meow!".
  3. Implement a function SoundMaker() that accepts an object and calls its make_sound() method.
  4. Test the SoundMaker() function with instances of both Dog and Cat.
class Dog:
    def make_sound(self):
        return "Woof!"

class Cat:
    def make_sound(self):
        return "Meow!"

def SoundMaker(animal):
    return animal.make_sound()

dog = Dog()
cat = Cat()

print(SoundMaker(dog))  # Output: Woof!
print(SoundMaker(cat))  # Output: Meow!

In this exercise, we have created two classes, Dog and Cat, each with their own make_sound() method. The SoundMaker() function takes an object as an argument and calls its make_sound() method without needing to know the exact type of the object. This demonstrates the concept of duck typing in Python.

8.4: Polymorphism

Polymorphism is an extremely important concept in object-oriented programming because it allows us to use objects from different classes as if they were objects of the same class, thus making code more flexible and reusable. By treating objects as if they were part of the same class, we can perform the same operations on them, even if they have different internal structures or methods. 

In Python, polymorphism can be achieved in a number of ways. One method is through method overloading, which involves defining multiple methods with the same name but different parameters. Another way is through method overriding, where a subclass method replaces a method of the same name in its parent class. Finally, duck typing is a concept in Python where the type of an object is determined by its behavior rather than its class, allowing for greater flexibility in code design. 

By using polymorphism in our code, we can create more robust and scalable applications that are able to handle a wide range of input data and perform a variety of operations depending on the objects used. In short, polymorphism is a cornerstone of object-oriented programming and a key tool for software developers to ensure that their code is both flexible and efficient.

8.4.1: Method Overloading:

Method overloading is a programming concept that allows a class to have multiple methods with the same name but different arguments. This feature is not supported in Python in the traditional sense, but there are workarounds that can achieve similar functionality. One such workaround is to provide default values for arguments. This can be helpful when you want to provide a default behavior for a method, but still allow users to override it if they need to. 

Another way to achieve similar functionality in Python is by using variable-length argument lists. You can use args and *kwargs to pass variable-length arguments to a method. This is useful when you are not sure how many arguments a method will need to accept, or when you want to provide a flexible interface for users to interact with your code.

8.4.2: Method Overriding:

Method overriding is a key technique in object-oriented programming that enables a subclass to inherit methods and attributes from its superclass while still allowing it to customize its behavior. This is accomplished by providing a new implementation for a method that has already been defined in the superclass. By doing so, the subclass can extend and modify the functionality of the method to meet its specific needs.

The benefits of method overriding are numerous. First and foremost, it allows for greater flexibility in the design of a program. By being able to customize the behavior of inherited methods, subclasses can tailor their functionality to better fit the specific requirements of their individual use cases. Additionally, method overriding promotes code reuse by enabling subclasses to inherit and modify existing code rather than having to recreate it from scratch.

However, it is important to note that method overriding should be used judiciously. Overriding too many methods can lead to code that is difficult to understand and maintain, and can also lead to unexpected behavior if not done properly. Therefore, it is important to carefully consider the design of a program and the requirements of its use cases before using method overriding.

class Animal:
    def speak(self):
        return "An animal makes a sound"

class Dog(Animal):
    def speak(self):
        return "A dog barks"

animal = Animal()
dog = Dog()

print(animal.speak())  # Output: An animal makes a sound
print(dog.speak())     # Output: A dog barks

8.4.3: Duck Typing:

Duck typing is a programming concept that allows you to use an object based on its behavior rather than its class. This means that you can treat any object like a duck, as long as it walks like a duck and quacks like a duck. In other words, if an object behaves like a duck (it has the required methods and properties), you can treat it as a duck, regardless of its actual class. 

Python is a language that makes extensive use of duck typing to achieve polymorphism, which allows you to write code that can work with objects of different classes without having to know their exact types in advance. This flexibility is one of the key strengths of Python and has made it a popular choice for developers working on projects with complex, dynamic data structures.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * (self.radius ** 2)

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def get_area(shape):
    return shape.area()

circle = Circle(5)
rectangle = Rectangle(4, 6)

print(get_area(circle))     # Output: 78.5
print(get_area(rectangle))  # Output: 24

In the example above, the get_area() function can calculate the area of any shape object that has an area() method, regardless of the shape's class. This demonstrates polymorphism in action through duck typing.

Exercise 8.4.1: Method Overloading

Title: Calculate the area of different shapes

Create a class Shape that has a method area() which accepts different numbers of arguments to calculate the area of different shapes (circle and rectangle).

Instructions:

  1. Create a class Shape.
  2. Implement an area() method that accepts different numbers of arguments.
  3. If there's one argument, treat it as the radius of a circle and calculate the area of the circle.
  4. If there are two arguments, treat them as the width and height of a rectangle and calculate the area of the rectangle.
  5. If no arguments or more than two arguments are provided, raise a ValueError with an appropriate error message.
class Shape:
    def area(self, *args):
        if len(args) == 1:
            radius = args[0]
            return 3.14 * (radius ** 2)
        elif len(args) == 2:
            width, height = args
            return width * height
        else:
            raise ValueError("Invalid number of arguments")

shape = Shape()
print(shape.area(5))          # Output: 78.5
print(shape.area(4, 6))       # Output: 24
try:
    print(shape.area())
except ValueError as e:
    print(e)                  # Output: Invalid number of arguments

Exercise 8.4.2: Method Overriding

Title: Custom __str__ method for Person and Employee

Create a class Person and a subclass Employee. Both classes should have a custom __str__ method to return a string representation of the object.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Implement a custom __str__ method for the Person class that returns the full name.
  3. Create a subclass Employee that inherits from Person and has an additional attribute position.
  4. Implement a custom __str__ method for the Employee class that returns the full name and position.
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, position):
        super().__init__(first_name, last_name)
        self.position = position

    def __str__(self):
        return f"{super().__str__()}, Position: {self.position}"

person = Person("John", "Doe")
employee = Employee("Jane", "Doe", "Software Engineer")

print(person)     # Output: John Doe
print(employee)   # Output: Jane Doe, Position: Software Engineer

Exercise 8.4.3: Duck Typing

Title: Implement a SoundMaker function

Description: Create a SoundMaker function that accepts an object and calls its make_sound() method, demonstrating duck typing.

Instructions:

  1. Create a class Dog with a method make_sound() that returns the string "Woof!".
  2. Create a class Cat with a method make_sound() that returns the string "Meow!".
  3. Implement a function SoundMaker() that accepts an object and calls its make_sound() method.
  4. Test the SoundMaker() function with instances of both Dog and Cat.
class Dog:
    def make_sound(self):
        return "Woof!"

class Cat:
    def make_sound(self):
        return "Meow!"

def SoundMaker(animal):
    return animal.make_sound()

dog = Dog()
cat = Cat()

print(SoundMaker(dog))  # Output: Woof!
print(SoundMaker(cat))  # Output: Meow!

In this exercise, we have created two classes, Dog and Cat, each with their own make_sound() method. The SoundMaker() function takes an object as an argument and calls its make_sound() method without needing to know the exact type of the object. This demonstrates the concept of duck typing in Python.