Part 2: Comprehensive Understanding of Classes in Python
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
Define a class
Employeewith instance variablesnameandsalary.Add a class variable
company_namethat is shared by all employees.Create two employee objects, and access both the class variable and instance variables.
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
Define a class
Calculatorwith a class methodset_precision()that changes the precision of the calculator (a class variable).Add a static method
multiply(a, b)to perform multiplication.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): Thesuper()function calls the constructor of the parent class (Animal). This ensures thenameattribute is initialized correctly.The
Dogclass inherits themake_sound()method from theAnimalclass and adds a new methodbark().
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
Create a parent class
Vehiclewith an attributebrandand a methodstart().Create a child class
Carthat inherits fromVehicleand adds an attributemodel.Override the
start()method in theCarclass 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
Create a parent class
Shapewith a methodarea(). Leave the method unimplemented (use thepassstatement).Create two child classes,
RectangleandCircle, and override thearea()method to calculate the area for each shape.Create a function
print_area()that accepts aShapeobject and calls itsarea()method. Test it with objects of bothRectangleandCircle.
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
Create a class
Employeewith a private attribute__salary.Provide methods
get_salary()andset_salary()to access and modify the salary.Try accessing
__salarydirectly 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 thelen()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
Create a class
Personwith attributesfirst_name,last_name, andage.Override the
__str__()method to return a user-friendly string.Override the
__repr__()method to return a developer-friendly representation.Add a method
__eq__()to compare two people by their age.

