Menu iconMenu iconPython & SQL Bible
Python & SQL Bible

Chapter 6: Object-Oriented Programming in Python

6.1 Classes, Objects, and Inheritance

In the world of programming, Object-Oriented Programming (OOP) is a popular and effective paradigm that uses the concept of "objects" to design applications and software. This programming paradigm revolves around the idea of creating objects that have specific properties and methods that can be manipulated and controlled within the programming environment. With OOP, programming becomes more intuitive and manageable by creating modular and reusable code.

Python is an object-oriented programming language that has gained popularity due to its ease of use and versatility. Almost everything in Python is an object, which means that you can manipulate and control these objects with ease. In fact, Python has a vast library of built-in objects and modules that make programming in Python a breeze.

In this chapter, we'll introduce you to the fundamental principles of object-oriented programming in Python. We'll focus on classes, objects, and inheritance - concepts that are essential for understanding how Python works. By the end of this chapter, you'll have a solid understanding of object-oriented programming in Python and be well on your way to mastering this powerful programming paradigm.

Let's dive into our first topic!

In Python, a class is a fundamental concept used to create objects, which are instances of the class. A class is, in essence, a blueprint for creating objects, providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods).

In object-oriented programming, classes are important because they allow you to model complex systems in a way that is both intuitive and modular. By encapsulating functionality within a class, you can create a clean, reusable design that promotes separation of concerns and reduces the complexity of your code.

Furthermore, the use of classes in Python allows for the creation of custom data types that can be used in a variety of ways. For example, you could create a class that represents a person, with attributes such as name, age, and address, and methods that allow you to interact with that person. This can be useful in many different applications, from building GUIs to creating data structures.

Overall, understanding classes in Python is essential for effective object-oriented programming and can help you create more modular, reusable, and maintainable code.

Example:

Let's understand this through a simple example:

# Define a class
class Dog:
    # A simple class attribute
    species = "Canis Familiaris"

    # Initializer / instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

# Create instances of the Dog class
buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4)

# Access the instance attributes
print(buddy.description())  # output: Buddy is 9 years old
print(miles.description())  # output: Miles is 4 years old

# Call our instance methods
print(buddy.speak("Woof Woof"))  # output: Buddy says Woof Woof
print(miles.speak("Bow Wow"))  # output: Miles says Bow Wow

In this example, Dog is a class with class attribute species, and it has the __init__ method that acts as a constructor to initialize new objects of this class. The methods description and speak are behaviors that the Dog class objects can perform.

Now, let's look at inheritance, which is a way of creating a new class using details of an existing class without modifying it. The newly formed class is a derived class (or child class). The existing class is a base class (or parent class).

# Parent class
class Bird:
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# Child class
class Penguin(Bird):
    def __init__(self):
        # call super() function
        super().__init__()
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()  # Output: Penguin
peggy.swim()  # Output: Swim faster
peggy.run()  # Output: Run faster

In this example, we have two classes Bird (parent class) and Penguin (child class). The child class inherits the functions of the parent class. We can see this from the swim method. Also, the child class modified the behavior of the parent class. We can see this from the whoisThis method. Furthermore, the child class extended the functions

super() is a powerful built-in function in Python, designed to return a temporary object of the superclass, which allows the developer to call that superclass’s methods. This is useful in cases where a subclass needs to inherit and extend the functionality of its superclass.

To illustrate this, let's consider a hypothetical scenario where you are developing a software system for managing a zoo. You are building a hierarchy of classes, starting with an Animal class that represents the shared characteristics of all animals in the zoo. You then create a Bird class that inherits from the Animal class and adds bird-specific characteristics. Finally, you create a Penguin class that inherits from the Bird class, adding penguin-specific characteristics.

Now, imagine that you want to reuse some of the code from the Animal class in the Bird class. You could copy and paste the code, but that would be tedious and prone to errors. Instead, you can use super() to call the initializer of the Animal class in the initializer of the Bird class, like this:

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

class Bird(Animal):
    def __init__(self, name, species, wingspan):
        super().__init__(name, species)
        self.wingspan = wingspan

This code creates a Bird class that has all the properties of the Animal class, as well as a wingspan property. By using super().__init__() in the initializer of the Bird class, we can reuse the code from the Animal class without duplicating it.

In larger and more complex hierarchies, this technique becomes especially useful, as it can help avoid duplicating code and makes it easier to update or modify your classes. By using super(), you can create a flexible and extensible class hierarchy that is easy to maintain and modify over time.

Here is another example that might help illustrate this concept:

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

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

    def perimeter(self):
        return 2 * self.length + 2 * self.width

# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

square = Square(4)
print(square.area())  # Output: 16
print(square.perimeter())  # Output: 16

In this example, Square is a subclass of Rectangle. We're using super() to call the __init__() of the Rectangle class, allowing us to use it in the Square class. This sets both the length and width to be the same given length, effectively making a square. Now, the Square class can use the area and perimeter methods of the Rectangle class, again reducing redundancy in our code.

This highlights the power of inheritance and the use of super(): you can easily build upon classes, reusing and modifying code as needed.

Method overriding

In Object-Oriented Programming (OOP), method overriding is a powerful feature that allows a subclass to provide a different implementation for a method that has already been defined in its superclass. This object-oriented design principle is applied when a subclass wants to modify or extend the behavior of its superclass. Essentially, method overriding is a way of customizing the behavior of an existing method so that it better fits the needs of the subclass.

Furthermore, method overriding is a key aspect of polymorphism in OOP. This means that the same method can be called on objects of different classes, and each object will respond with its own implementation of the method. This is an incredibly useful feature for designing large-scale software systems because it allows programmers to write code that is reusable and flexible.

It is important to note that when overriding a method, the subclass must adhere to the method signature of the superclass method. The method signature consists of the method name, the number of parameters, and the types of the parameters. By maintaining the method signature, the subclass ensures that it can be used in the same way as the superclass method it is overriding.

In summary, method overriding is a fundamental feature of OOP that allows a subclass to customize the behavior of a method that has already been defined in its superclass. This feature is essential for creating reusable and flexible code in large-scale software systems, and it is a key aspect of polymorphism.

This is how method overriding would work:

class Bird:
    def intro(self):
        print("There are many types of birds.")

    def flight(self):
        print("Most of the birds can fly but some cannot.")

class Sparrow(Bird):
    def flight(self):
        print("Sparrows can fly.")

class Ostrich(Bird):
    def flight(self):
        print("Ostriches cannot fly.")

b1 = Bird()
b2 = Sparrow()
b3 = Ostrich()

b1.intro()
b1.flight()

b2.intro()
b2.flight()

b3.intro()
b3.flight()

When you run this code, you’ll see that when the flight method is called on an instance of the Sparrow or Ostrich class, the overridden method in the subclass is used instead of the one in the Bird class. This is a central part of how inheritance works in Python and many other object-oriented languages, allowing for a high degree of code reuse and modularity.

With method overriding, you can customize the behavior of parent class methods according to the needs of your subclass, making it a powerful tool for creating flexible and organized code structures.

6.1 Classes, Objects, and Inheritance

6.1 Classes, Objects, and Inheritance

In the world of programming, Object-Oriented Programming (OOP) is a popular and effective paradigm that uses the concept of "objects" to design applications and software. This programming paradigm revolves around the idea of creating objects that have specific properties and methods that can be manipulated and controlled within the programming environment. With OOP, programming becomes more intuitive and manageable by creating modular and reusable code.

Python is an object-oriented programming language that has gained popularity due to its ease of use and versatility. Almost everything in Python is an object, which means that you can manipulate and control these objects with ease. In fact, Python has a vast library of built-in objects and modules that make programming in Python a breeze.

In this chapter, we'll introduce you to the fundamental principles of object-oriented programming in Python. We'll focus on classes, objects, and inheritance - concepts that are essential for understanding how Python works. By the end of this chapter, you'll have a solid understanding of object-oriented programming in Python and be well on your way to mastering this powerful programming paradigm.

Let's dive into our first topic!

In Python, a class is a fundamental concept used to create objects, which are instances of the class. A class is, in essence, a blueprint for creating objects, providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods).

In object-oriented programming, classes are important because they allow you to model complex systems in a way that is both intuitive and modular. By encapsulating functionality within a class, you can create a clean, reusable design that promotes separation of concerns and reduces the complexity of your code.

Furthermore, the use of classes in Python allows for the creation of custom data types that can be used in a variety of ways. For example, you could create a class that represents a person, with attributes such as name, age, and address, and methods that allow you to interact with that person. This can be useful in many different applications, from building GUIs to creating data structures.

Overall, understanding classes in Python is essential for effective object-oriented programming and can help you create more modular, reusable, and maintainable code.

Example:

Let's understand this through a simple example:

# Define a class
class Dog:
    # A simple class attribute
    species = "Canis Familiaris"

    # Initializer / instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

# Create instances of the Dog class
buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4)

# Access the instance attributes
print(buddy.description())  # output: Buddy is 9 years old
print(miles.description())  # output: Miles is 4 years old

# Call our instance methods
print(buddy.speak("Woof Woof"))  # output: Buddy says Woof Woof
print(miles.speak("Bow Wow"))  # output: Miles says Bow Wow

In this example, Dog is a class with class attribute species, and it has the __init__ method that acts as a constructor to initialize new objects of this class. The methods description and speak are behaviors that the Dog class objects can perform.

Now, let's look at inheritance, which is a way of creating a new class using details of an existing class without modifying it. The newly formed class is a derived class (or child class). The existing class is a base class (or parent class).

# Parent class
class Bird:
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# Child class
class Penguin(Bird):
    def __init__(self):
        # call super() function
        super().__init__()
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()  # Output: Penguin
peggy.swim()  # Output: Swim faster
peggy.run()  # Output: Run faster

In this example, we have two classes Bird (parent class) and Penguin (child class). The child class inherits the functions of the parent class. We can see this from the swim method. Also, the child class modified the behavior of the parent class. We can see this from the whoisThis method. Furthermore, the child class extended the functions

super() is a powerful built-in function in Python, designed to return a temporary object of the superclass, which allows the developer to call that superclass’s methods. This is useful in cases where a subclass needs to inherit and extend the functionality of its superclass.

To illustrate this, let's consider a hypothetical scenario where you are developing a software system for managing a zoo. You are building a hierarchy of classes, starting with an Animal class that represents the shared characteristics of all animals in the zoo. You then create a Bird class that inherits from the Animal class and adds bird-specific characteristics. Finally, you create a Penguin class that inherits from the Bird class, adding penguin-specific characteristics.

Now, imagine that you want to reuse some of the code from the Animal class in the Bird class. You could copy and paste the code, but that would be tedious and prone to errors. Instead, you can use super() to call the initializer of the Animal class in the initializer of the Bird class, like this:

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

class Bird(Animal):
    def __init__(self, name, species, wingspan):
        super().__init__(name, species)
        self.wingspan = wingspan

This code creates a Bird class that has all the properties of the Animal class, as well as a wingspan property. By using super().__init__() in the initializer of the Bird class, we can reuse the code from the Animal class without duplicating it.

In larger and more complex hierarchies, this technique becomes especially useful, as it can help avoid duplicating code and makes it easier to update or modify your classes. By using super(), you can create a flexible and extensible class hierarchy that is easy to maintain and modify over time.

Here is another example that might help illustrate this concept:

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

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

    def perimeter(self):
        return 2 * self.length + 2 * self.width

# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

square = Square(4)
print(square.area())  # Output: 16
print(square.perimeter())  # Output: 16

In this example, Square is a subclass of Rectangle. We're using super() to call the __init__() of the Rectangle class, allowing us to use it in the Square class. This sets both the length and width to be the same given length, effectively making a square. Now, the Square class can use the area and perimeter methods of the Rectangle class, again reducing redundancy in our code.

This highlights the power of inheritance and the use of super(): you can easily build upon classes, reusing and modifying code as needed.

Method overriding

In Object-Oriented Programming (OOP), method overriding is a powerful feature that allows a subclass to provide a different implementation for a method that has already been defined in its superclass. This object-oriented design principle is applied when a subclass wants to modify or extend the behavior of its superclass. Essentially, method overriding is a way of customizing the behavior of an existing method so that it better fits the needs of the subclass.

Furthermore, method overriding is a key aspect of polymorphism in OOP. This means that the same method can be called on objects of different classes, and each object will respond with its own implementation of the method. This is an incredibly useful feature for designing large-scale software systems because it allows programmers to write code that is reusable and flexible.

It is important to note that when overriding a method, the subclass must adhere to the method signature of the superclass method. The method signature consists of the method name, the number of parameters, and the types of the parameters. By maintaining the method signature, the subclass ensures that it can be used in the same way as the superclass method it is overriding.

In summary, method overriding is a fundamental feature of OOP that allows a subclass to customize the behavior of a method that has already been defined in its superclass. This feature is essential for creating reusable and flexible code in large-scale software systems, and it is a key aspect of polymorphism.

This is how method overriding would work:

class Bird:
    def intro(self):
        print("There are many types of birds.")

    def flight(self):
        print("Most of the birds can fly but some cannot.")

class Sparrow(Bird):
    def flight(self):
        print("Sparrows can fly.")

class Ostrich(Bird):
    def flight(self):
        print("Ostriches cannot fly.")

b1 = Bird()
b2 = Sparrow()
b3 = Ostrich()

b1.intro()
b1.flight()

b2.intro()
b2.flight()

b3.intro()
b3.flight()

When you run this code, you’ll see that when the flight method is called on an instance of the Sparrow or Ostrich class, the overridden method in the subclass is used instead of the one in the Bird class. This is a central part of how inheritance works in Python and many other object-oriented languages, allowing for a high degree of code reuse and modularity.

With method overriding, you can customize the behavior of parent class methods according to the needs of your subclass, making it a powerful tool for creating flexible and organized code structures.

6.1 Classes, Objects, and Inheritance

6.1 Classes, Objects, and Inheritance

In the world of programming, Object-Oriented Programming (OOP) is a popular and effective paradigm that uses the concept of "objects" to design applications and software. This programming paradigm revolves around the idea of creating objects that have specific properties and methods that can be manipulated and controlled within the programming environment. With OOP, programming becomes more intuitive and manageable by creating modular and reusable code.

Python is an object-oriented programming language that has gained popularity due to its ease of use and versatility. Almost everything in Python is an object, which means that you can manipulate and control these objects with ease. In fact, Python has a vast library of built-in objects and modules that make programming in Python a breeze.

In this chapter, we'll introduce you to the fundamental principles of object-oriented programming in Python. We'll focus on classes, objects, and inheritance - concepts that are essential for understanding how Python works. By the end of this chapter, you'll have a solid understanding of object-oriented programming in Python and be well on your way to mastering this powerful programming paradigm.

Let's dive into our first topic!

In Python, a class is a fundamental concept used to create objects, which are instances of the class. A class is, in essence, a blueprint for creating objects, providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods).

In object-oriented programming, classes are important because they allow you to model complex systems in a way that is both intuitive and modular. By encapsulating functionality within a class, you can create a clean, reusable design that promotes separation of concerns and reduces the complexity of your code.

Furthermore, the use of classes in Python allows for the creation of custom data types that can be used in a variety of ways. For example, you could create a class that represents a person, with attributes such as name, age, and address, and methods that allow you to interact with that person. This can be useful in many different applications, from building GUIs to creating data structures.

Overall, understanding classes in Python is essential for effective object-oriented programming and can help you create more modular, reusable, and maintainable code.

Example:

Let's understand this through a simple example:

# Define a class
class Dog:
    # A simple class attribute
    species = "Canis Familiaris"

    # Initializer / instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

# Create instances of the Dog class
buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4)

# Access the instance attributes
print(buddy.description())  # output: Buddy is 9 years old
print(miles.description())  # output: Miles is 4 years old

# Call our instance methods
print(buddy.speak("Woof Woof"))  # output: Buddy says Woof Woof
print(miles.speak("Bow Wow"))  # output: Miles says Bow Wow

In this example, Dog is a class with class attribute species, and it has the __init__ method that acts as a constructor to initialize new objects of this class. The methods description and speak are behaviors that the Dog class objects can perform.

Now, let's look at inheritance, which is a way of creating a new class using details of an existing class without modifying it. The newly formed class is a derived class (or child class). The existing class is a base class (or parent class).

# Parent class
class Bird:
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# Child class
class Penguin(Bird):
    def __init__(self):
        # call super() function
        super().__init__()
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()  # Output: Penguin
peggy.swim()  # Output: Swim faster
peggy.run()  # Output: Run faster

In this example, we have two classes Bird (parent class) and Penguin (child class). The child class inherits the functions of the parent class. We can see this from the swim method. Also, the child class modified the behavior of the parent class. We can see this from the whoisThis method. Furthermore, the child class extended the functions

super() is a powerful built-in function in Python, designed to return a temporary object of the superclass, which allows the developer to call that superclass’s methods. This is useful in cases where a subclass needs to inherit and extend the functionality of its superclass.

To illustrate this, let's consider a hypothetical scenario where you are developing a software system for managing a zoo. You are building a hierarchy of classes, starting with an Animal class that represents the shared characteristics of all animals in the zoo. You then create a Bird class that inherits from the Animal class and adds bird-specific characteristics. Finally, you create a Penguin class that inherits from the Bird class, adding penguin-specific characteristics.

Now, imagine that you want to reuse some of the code from the Animal class in the Bird class. You could copy and paste the code, but that would be tedious and prone to errors. Instead, you can use super() to call the initializer of the Animal class in the initializer of the Bird class, like this:

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

class Bird(Animal):
    def __init__(self, name, species, wingspan):
        super().__init__(name, species)
        self.wingspan = wingspan

This code creates a Bird class that has all the properties of the Animal class, as well as a wingspan property. By using super().__init__() in the initializer of the Bird class, we can reuse the code from the Animal class without duplicating it.

In larger and more complex hierarchies, this technique becomes especially useful, as it can help avoid duplicating code and makes it easier to update or modify your classes. By using super(), you can create a flexible and extensible class hierarchy that is easy to maintain and modify over time.

Here is another example that might help illustrate this concept:

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

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

    def perimeter(self):
        return 2 * self.length + 2 * self.width

# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

square = Square(4)
print(square.area())  # Output: 16
print(square.perimeter())  # Output: 16

In this example, Square is a subclass of Rectangle. We're using super() to call the __init__() of the Rectangle class, allowing us to use it in the Square class. This sets both the length and width to be the same given length, effectively making a square. Now, the Square class can use the area and perimeter methods of the Rectangle class, again reducing redundancy in our code.

This highlights the power of inheritance and the use of super(): you can easily build upon classes, reusing and modifying code as needed.

Method overriding

In Object-Oriented Programming (OOP), method overriding is a powerful feature that allows a subclass to provide a different implementation for a method that has already been defined in its superclass. This object-oriented design principle is applied when a subclass wants to modify or extend the behavior of its superclass. Essentially, method overriding is a way of customizing the behavior of an existing method so that it better fits the needs of the subclass.

Furthermore, method overriding is a key aspect of polymorphism in OOP. This means that the same method can be called on objects of different classes, and each object will respond with its own implementation of the method. This is an incredibly useful feature for designing large-scale software systems because it allows programmers to write code that is reusable and flexible.

It is important to note that when overriding a method, the subclass must adhere to the method signature of the superclass method. The method signature consists of the method name, the number of parameters, and the types of the parameters. By maintaining the method signature, the subclass ensures that it can be used in the same way as the superclass method it is overriding.

In summary, method overriding is a fundamental feature of OOP that allows a subclass to customize the behavior of a method that has already been defined in its superclass. This feature is essential for creating reusable and flexible code in large-scale software systems, and it is a key aspect of polymorphism.

This is how method overriding would work:

class Bird:
    def intro(self):
        print("There are many types of birds.")

    def flight(self):
        print("Most of the birds can fly but some cannot.")

class Sparrow(Bird):
    def flight(self):
        print("Sparrows can fly.")

class Ostrich(Bird):
    def flight(self):
        print("Ostriches cannot fly.")

b1 = Bird()
b2 = Sparrow()
b3 = Ostrich()

b1.intro()
b1.flight()

b2.intro()
b2.flight()

b3.intro()
b3.flight()

When you run this code, you’ll see that when the flight method is called on an instance of the Sparrow or Ostrich class, the overridden method in the subclass is used instead of the one in the Bird class. This is a central part of how inheritance works in Python and many other object-oriented languages, allowing for a high degree of code reuse and modularity.

With method overriding, you can customize the behavior of parent class methods according to the needs of your subclass, making it a powerful tool for creating flexible and organized code structures.

6.1 Classes, Objects, and Inheritance

6.1 Classes, Objects, and Inheritance

In the world of programming, Object-Oriented Programming (OOP) is a popular and effective paradigm that uses the concept of "objects" to design applications and software. This programming paradigm revolves around the idea of creating objects that have specific properties and methods that can be manipulated and controlled within the programming environment. With OOP, programming becomes more intuitive and manageable by creating modular and reusable code.

Python is an object-oriented programming language that has gained popularity due to its ease of use and versatility. Almost everything in Python is an object, which means that you can manipulate and control these objects with ease. In fact, Python has a vast library of built-in objects and modules that make programming in Python a breeze.

In this chapter, we'll introduce you to the fundamental principles of object-oriented programming in Python. We'll focus on classes, objects, and inheritance - concepts that are essential for understanding how Python works. By the end of this chapter, you'll have a solid understanding of object-oriented programming in Python and be well on your way to mastering this powerful programming paradigm.

Let's dive into our first topic!

In Python, a class is a fundamental concept used to create objects, which are instances of the class. A class is, in essence, a blueprint for creating objects, providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods).

In object-oriented programming, classes are important because they allow you to model complex systems in a way that is both intuitive and modular. By encapsulating functionality within a class, you can create a clean, reusable design that promotes separation of concerns and reduces the complexity of your code.

Furthermore, the use of classes in Python allows for the creation of custom data types that can be used in a variety of ways. For example, you could create a class that represents a person, with attributes such as name, age, and address, and methods that allow you to interact with that person. This can be useful in many different applications, from building GUIs to creating data structures.

Overall, understanding classes in Python is essential for effective object-oriented programming and can help you create more modular, reusable, and maintainable code.

Example:

Let's understand this through a simple example:

# Define a class
class Dog:
    # A simple class attribute
    species = "Canis Familiaris"

    # Initializer / instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # instance method
    def description(self):
        return f"{self.name} is {self.age} years old"

    # another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

# Create instances of the Dog class
buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4)

# Access the instance attributes
print(buddy.description())  # output: Buddy is 9 years old
print(miles.description())  # output: Miles is 4 years old

# Call our instance methods
print(buddy.speak("Woof Woof"))  # output: Buddy says Woof Woof
print(miles.speak("Bow Wow"))  # output: Miles says Bow Wow

In this example, Dog is a class with class attribute species, and it has the __init__ method that acts as a constructor to initialize new objects of this class. The methods description and speak are behaviors that the Dog class objects can perform.

Now, let's look at inheritance, which is a way of creating a new class using details of an existing class without modifying it. The newly formed class is a derived class (or child class). The existing class is a base class (or parent class).

# Parent class
class Bird:
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# Child class
class Penguin(Bird):
    def __init__(self):
        # call super() function
        super().__init__()
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()  # Output: Penguin
peggy.swim()  # Output: Swim faster
peggy.run()  # Output: Run faster

In this example, we have two classes Bird (parent class) and Penguin (child class). The child class inherits the functions of the parent class. We can see this from the swim method. Also, the child class modified the behavior of the parent class. We can see this from the whoisThis method. Furthermore, the child class extended the functions

super() is a powerful built-in function in Python, designed to return a temporary object of the superclass, which allows the developer to call that superclass’s methods. This is useful in cases where a subclass needs to inherit and extend the functionality of its superclass.

To illustrate this, let's consider a hypothetical scenario where you are developing a software system for managing a zoo. You are building a hierarchy of classes, starting with an Animal class that represents the shared characteristics of all animals in the zoo. You then create a Bird class that inherits from the Animal class and adds bird-specific characteristics. Finally, you create a Penguin class that inherits from the Bird class, adding penguin-specific characteristics.

Now, imagine that you want to reuse some of the code from the Animal class in the Bird class. You could copy and paste the code, but that would be tedious and prone to errors. Instead, you can use super() to call the initializer of the Animal class in the initializer of the Bird class, like this:

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

class Bird(Animal):
    def __init__(self, name, species, wingspan):
        super().__init__(name, species)
        self.wingspan = wingspan

This code creates a Bird class that has all the properties of the Animal class, as well as a wingspan property. By using super().__init__() in the initializer of the Bird class, we can reuse the code from the Animal class without duplicating it.

In larger and more complex hierarchies, this technique becomes especially useful, as it can help avoid duplicating code and makes it easier to update or modify your classes. By using super(), you can create a flexible and extensible class hierarchy that is easy to maintain and modify over time.

Here is another example that might help illustrate this concept:

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

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

    def perimeter(self):
        return 2 * self.length + 2 * self.width

# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

square = Square(4)
print(square.area())  # Output: 16
print(square.perimeter())  # Output: 16

In this example, Square is a subclass of Rectangle. We're using super() to call the __init__() of the Rectangle class, allowing us to use it in the Square class. This sets both the length and width to be the same given length, effectively making a square. Now, the Square class can use the area and perimeter methods of the Rectangle class, again reducing redundancy in our code.

This highlights the power of inheritance and the use of super(): you can easily build upon classes, reusing and modifying code as needed.

Method overriding

In Object-Oriented Programming (OOP), method overriding is a powerful feature that allows a subclass to provide a different implementation for a method that has already been defined in its superclass. This object-oriented design principle is applied when a subclass wants to modify or extend the behavior of its superclass. Essentially, method overriding is a way of customizing the behavior of an existing method so that it better fits the needs of the subclass.

Furthermore, method overriding is a key aspect of polymorphism in OOP. This means that the same method can be called on objects of different classes, and each object will respond with its own implementation of the method. This is an incredibly useful feature for designing large-scale software systems because it allows programmers to write code that is reusable and flexible.

It is important to note that when overriding a method, the subclass must adhere to the method signature of the superclass method. The method signature consists of the method name, the number of parameters, and the types of the parameters. By maintaining the method signature, the subclass ensures that it can be used in the same way as the superclass method it is overriding.

In summary, method overriding is a fundamental feature of OOP that allows a subclass to customize the behavior of a method that has already been defined in its superclass. This feature is essential for creating reusable and flexible code in large-scale software systems, and it is a key aspect of polymorphism.

This is how method overriding would work:

class Bird:
    def intro(self):
        print("There are many types of birds.")

    def flight(self):
        print("Most of the birds can fly but some cannot.")

class Sparrow(Bird):
    def flight(self):
        print("Sparrows can fly.")

class Ostrich(Bird):
    def flight(self):
        print("Ostriches cannot fly.")

b1 = Bird()
b2 = Sparrow()
b3 = Ostrich()

b1.intro()
b1.flight()

b2.intro()
b2.flight()

b3.intro()
b3.flight()

When you run this code, you’ll see that when the flight method is called on an instance of the Sparrow or Ostrich class, the overridden method in the subclass is used instead of the one in the Bird class. This is a central part of how inheritance works in Python and many other object-oriented languages, allowing for a high degree of code reuse and modularity.

With method overriding, you can customize the behavior of parent class methods according to the needs of your subclass, making it a powerful tool for creating flexible and organized code structures.

6.1 Classes, Objects, and Inheritance