Skip to main content

Command Palette

Search for a command to run...

Part 2: Comprehensive Understanding of Classes in Python

Updated
9 min readView as Markdown
U

Transformative Tech Leader | Serial Entrepreneur & Machine Learning Engineer Leveraging 3+ years of expertise in Machine Learning and a background in Web Development, I drive innovation through building, mentoring, and educating. Passionate about harnessing AI to solve real-world problems."

3: Class vs Instance Variables

In Python, variables defined inside a class can be either class variables or instance variables. Understanding the difference is crucial when working with object-oriented programming.

3.1 Instance Variables

Instance variables are defined inside the __init__ method and are unique to each object (instance) of a class. Each object has its own copy of instance variables.

Example:

class Dog:
    def __init__(self, name, breed):
        self.name = name  # Instance variable
        self.breed = breed  # Instance variable

# Creating objects
dog1 = Dog("Max", "Labrador")
dog2 = Dog("Bella", "Beagle")

# Each object has its own instance variables
print(dog1.name)  # Output: Max
print(dog2.name)  # Output: Bella

In this example, the name and breed are instance variables. The dog1 object has its own name (Max), while dog2 has its own name (Bella).

3.2 Class Variables

Class variables are shared across all instances of a class. They are defined directly inside the class, but outside any method. If you modify a class variable, it changes for all objects.

Example:

class Dog:
    species = "Canis lupus familiaris"  # Class variable

    def __init__(self, name, breed):
        self.name = name  # Instance variable
        self.breed = breed  # Instance variable

# Creating objects
dog1 = Dog("Max", "Labrador")
dog2 = Dog("Bella", "Beagle")

# Accessing class variable
print(dog1.species)  # Output: Canis lupus familiaris
print(dog2.species)  # Output: Canis lupus familiaris

Here, species is a class variable. Both dog1 and dog2 share the same value for species. If you change species at the class level, it will affect all instances.

3.3 Modifying Class Variables

If you modify a class variable via an instance, you actually create a new instance variable instead of changing the class variable.

Example:

dog1.species = "Canis lupus"  # This creates an instance variable 'species' for dog1
print(dog1.species)  # Output: Canis lupus
print(dog2.species)  # Output: Canis lupus familiaris

In this case, dog1 now has its own species instance variable, while dog2 continues to use the shared class variable.

Practical Exercise 3: Class vs Instance Variables

  1. Define a class Employee with instance variables name and salary.

  2. Add a class variable company_name that is shared by all employees.

  3. Create two employee objects, and access both the class variable and instance variables.

  4. Modify the class variable via the class, and observe the changes in both objects.


4: Class Methods and Static Methods

4.1 Instance Methods Recap

Instance methods are functions that operate on the instance of the class (i.e., the object). They can access and modify object attributes and are defined with self.

4.2 Class Methods

Class methods are bound to the class itself, not the object. They are defined using the @classmethod decorator and take cls (the class itself) as the first parameter, rather than self.

Example:

class Dog:
    species = "Canis lupus familiaris"

    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    @classmethod
    def change_species(cls, new_species):
        cls.species = new_species

# Creating an object
dog1 = Dog("Max", "Labrador")

# Changing the class variable using class method
Dog.change_species("Canis lupus")
print(dog1.species)  # Output: Canis lupus

In this example, change_species is a class method that modifies the class variable species. This change applies to all instances of the class.

4.3 Static Methods

Static methods do not take self or cls as their first parameter. They are just like regular functions but are part of the class's namespace. Static methods are defined using the @staticmethod decorator.

Example:

class MathOperations:
    @staticmethod
    def add_numbers(a, b):
        return a + b

# Using static method without creating an instance
result = MathOperations.add_numbers(5, 10)
print(result)  # Output: 15

Here, add_numbers is a static method. It has no access to class-level data or instance-level data. It simply performs an operation related to the class.

4.4 When to Use Static Methods vs Class Methods

  • Use class methods when you need to modify class variables or when the method pertains to the class as a whole rather than a specific instance.

  • Use static methods for utility functions that don’t modify class or instance data but are still logically related to the class.

Practical Exercise 4: Class and Static Methods

  1. Define a class Calculator with a class method set_precision() that changes the precision of the calculator (a class variable).

  2. Add a static method multiply(a, b) to perform multiplication.

  3. Use the class method to change the precision, and use the static method to perform a multiplication.


5: Inheritance in Python

Inheritance allows one class to inherit the properties and methods of another class. The class that is inherited from is called the parent class or superclass, and the class that inherits is called the child class or subclass.

5.1 Creating a Parent Class

Let’s start by defining a simple parent class Animal:

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

    def make_sound(self):
        print(f"{self.name} is making a sound.")

5.2 Creating a Child Class

Now, we’ll create a Dog class that inherits from Animal:

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Call the parent class constructor
        self.breed = breed

    def bark(self):
        print(f"{self.name} is barking.")

In this example:

  • super().__init__(name): The super() function calls the constructor of the parent class (Animal). This ensures the name attribute is initialized correctly.

  • The Dog class inherits the make_sound() method from the Animal class and adds a new method bark().

5.3 Using Inheritance

dog1 = Dog("Buddy", "Golden Retriever")
dog1.make_sound()  # Inherited method from Animal class
dog1.bark()  # Method defined in Dog class

5.4 Method Overriding

You can override methods in the child class to change their behavior.

class Cat(Animal):
    def make_sound(self):
        print(f"{self.name} is meowing.")

# Creating an object of Cat
cat1 = Cat("Whiskers")
cat1.make_sound()  # Output: Whiskers is meowing.

Here, the Cat class overrides the make_sound() method of the Animal class to provide its own implementation.

Practical Exercise 5: Inheritance

  1. Create a parent class Vehicle with an attribute brand and a method start().

  2. Create a child class Car that inherits from Vehicle and adds an attribute model.

  3. Override the start() method in the Car class to print a specific message for cars.


6: Polymorphism in Python

Polymorphism allows objects of different classes to be treated as objects of a common superclass. In simpler terms, polymorphism lets you define methods in the parent class that can be overridden by child classes with specific implementations.

6.1 Method Overriding in Polymorphism

You have already seen an example of method overriding in inheritance. When a child class provides its own version of a method that is defined in the parent class, it is an example of polymorphism.

Example:

class Animal:
    def make_sound(self):
        print("This animal makes a sound.")

class Dog(Animal):
    def make_sound(self):
        print("The dog barks.")

class Cat(Animal):
    def make_sound(self):
        print("The cat meows.")

# Using polymorphism
animals = [Dog(), Cat()]

for animal in animals:
    animal.make_sound()  # Output: The dog barks. The cat meows.

Here, make_sound() is defined in the parent class Animal, but the child classes Dog and Cat override this method with their own behavior. The key aspect of polymorphism is that you can treat all objects as instances of the parent class but get specific behavior based on the actual object type.

6.2 Polymorphism with Functions and Methods

Polymorphism can also work with functions. For instance, a single function can accept objects of different types and call the respective methods.

Example:

def animal_sound(animal):
    animal.make_sound()

# Polymorphism in action
dog = Dog()
cat = Cat()

animal_sound(dog)  # Output: The dog barks.
animal_sound(cat)  # Output: The cat meows.

In this example, the animal_sound() function accepts any object of type Animal and calls the make_sound() method. Based on whether the object is a Dog or Cat, the function behaves differently.

Practical Exercise 6: Polymorphism

  1. Create a parent class Shape with a method area(). Leave the method unimplemented (use the pass statement).

  2. Create two child classes, Rectangle and Circle, and override the area() method to calculate the area for each shape.

  3. Create a function print_area() that accepts a Shape object and calls its area() method. Test it with objects of both Rectangle and Circle.


7: Encapsulation in Python

Encapsulation is the principle of bundling data and methods that operate on that data within a single unit, i.e., a class. It also restricts access to some attributes to prevent accidental modification.

7.1 Public, Protected, and Private Attributes

  • Public Attributes: These can be accessed from anywhere. In Python, all attributes are public by default.

  • Protected Attributes: These should not be accessed directly outside the class. They are indicated by a single underscore _.

  • Private Attributes: These cannot be accessed directly from outside the class. They are indicated by a double underscore __.

Example:

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # Protected attribute

    def get_balance(self):
        return self._balance

    def set_balance(self, amount):
        if amount >= 0:
            self._balance = amount
        else:
            print("Invalid amount.")

# Accessing protected attribute
account = BankAccount(1000)
print(account.get_balance())  # Output: 1000

# Changing balance using method
account.set_balance(500)
print(account.get_balance())  # Output: 500

In this example, _balance is a protected attribute. It’s accessed and modified only through methods like get_balance() and set_balance().

7.2 Private Attributes

Private attributes, marked by a double underscore, are not directly accessible from outside the class.

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private attribute

    def get_balance(self):
        return self.__balance

    def set_balance(self, amount):
        if amount >= 0:
            self.__balance = amount
        else:
            print("Invalid amount.")

# Accessing private attribute
account = BankAccount(1000)
# print(account.__balance)  # This will raise an AttributeError

# However, it can still be accessed via name mangling:
print(account._BankAccount__balance)  # Output: 1000

Private attributes can’t be accessed directly, but can still be accessed using a technique called name mangling (as seen in the last print statement). However, it’s recommended to avoid doing so and stick to accessing attributes through methods.

Practical Exercise 7: Encapsulation

  1. Create a class Employee with a private attribute __salary.

  2. Provide methods get_salary() and set_salary() to access and modify the salary.

  3. Try accessing __salary directly from outside the class, and observe what happens.


Part 8: Special Methods (__str__, __repr__, etc.)

Python provides several special methods that you can override to define how objects behave for built-in operations, like how they should be printed or compared.

8.1 The __str__() and __repr__() Methods

  • __str__(): This method defines how an object should be represented when you print it. It’s meant to be user-friendly.

  • __repr__(): This method defines the “official” string representation of an object. It’s meant for developers and should be unambiguous.

Example:

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def __str__(self):
        return f"{self.year} {self.make} {self.model}"

    def __repr__(self):
        return f"Car('{self.make}', '{self.model}', {self.year})"

car = Car("Toyota", "Corolla", 2020)

# User-friendly string representation
print(str(car))  # Output: 2020 Toyota Corolla

# Developer-friendly representation
print(repr(car))  # Output: Car('Toyota', 'Corolla', 2020)

8.2 Other Special Methods

  • __eq__(self, other): Defines behavior for the equality operator ==.

  • __lt__(self, other): Defines behavior for the less-than operator <.

  • __len__(self): Defines the behavior for the len() function.

Example:

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __len__(self):
        return self.pages

    def __eq__(self, other):
        return self.pages == other.pages

book1 = Book("Book One", 300)
book2 = Book("Book Two", 300)

print(len(book1))  # Output: 300
print(book1 == book2)  # Output: True

Here, we use the __len__() method to define how the len() function works for Book objects. We also use __eq__() to compare two books based on the number of pages.

Practical Exercise 8: Special Methods

  1. Create a class Person with attributes first_name, last_name, and age.

  2. Override the __str__() method to return a user-friendly string.

  3. Override the __repr__() method to return a developer-friendly representation.

  4. Add a method __eq__() to compare two people by their age.


More from this blog

Coder Connect

23 posts

Join the coding revolution: learn, innovate, transform. The only limit is your mind."