Lightning bolt and Python code snippet with "Python Classes" in blocky caps

Python Classes

In this lesson, you’ll learn how to define classes, create objects, and use Object-Oriented Programming (OOP) principles like inheritance and encapsulation to write more modular and reusable code.

What is Object-Oriented Programming (OOP)?

OOP is a way of structuring programs by bundling related properties and behaviors into individual objects. It allows you to model real-world entities in code and provides a clean way to organize data and functions.

Key concepts in OOP include:

  • Class: A blueprint for creating objects (a type of data structure).
  • Object: An instance of a class (a specific example of the class).
  • Attributes: Variables that hold data related to an object.
  • Methods: Functions defined inside a class that describe the behaviors of the object.

Defining a Class

A class is a blueprint for creating objects. To define a class in Python, you use the class keyword followed by the class name.

Basic Syntax:

class ClassName:
    # Class body
    pass

Example:

class Dog:
    # This is an empty class
    pass

In this example, Dog is a class, but it doesn’t do anything yet. You can now create objects (instances) of the Dog class, even though the class is empty.

Creating Objects

Once you’ve defined a class, you can create objects from it. An object is an instance of the class, meaning it’s a concrete example based on the class definition.

Example:

class Dog:
    pass

# Creating an object of the Dog class
my_dog = Dog()

print(type(my_dog))  # Output: <class '__main__.Dog'>

Here, my_dog is an instance of the Dog class. You can create multiple objects from the same class, each representing a unique instance.

Attributes and Methods

Attributes are variables that store data related to the object, while methods are functions that define the behavior of the object. You define attributes and methods inside the class.

Example:

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

    def bark(self):  # Method
        return f"{self.name} says woof!"

Explanation:

  • __init__(): This is a special method called a constructor. It is automatically called when a new object is created. It initializes the object’s attributes.
  • self: Refers to the instance of the class. It allows you to access and modify the object’s attributes and methods.

Creating and Using Objects with Attributes and Methods:

# Creating objects of the Dog class
my_dog = Dog("Buddy", "Golden Retriever")
your_dog = Dog("Max", "German Shepherd")

# Accessing attributes
print(my_dog.name)  # Output: Buddy
print(your_dog.breed)  # Output: German Shepherd

# Calling methods
print(my_dog.bark())  # Output: Buddy says woof!

In this example, my_dog and your_dog are objects of the Dog class with their own unique attributes (name and breed). The bark() method allows each dog to “bark” with its name.

The __init__() Method (Constructor)

The __init__() method is a constructor that initializes an object’s attributes when it is created. It’s called automatically when you create a new instance of the class.

Example:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

When you create a Dog object, you must provide values for name and age, which are assigned to the object’s attributes.

my_dog = Dog("Buddy", 3)
print(my_dog.name)  # Output: Buddy
print(my_dog.age)   # Output: 3

Without the __init__() method, you would need to manually assign values to the attributes, but with it, initialization is much more convenient.

Class vs. Instance Attributes

There are two types of attributes in a class:

  • Instance attributes: Attributes that are specific to each object (defined inside the __init__() method).
  • Class attributes: Attributes shared by all instances of the class (defined directly in the class).

Example:

class Dog:
    species = "Canis familiaris"  # Class attribute

    def __init__(self, name, age):
        self.name = name  # Instance attribute
        self.age = age    # Instance attribute

In this example, species is a class attribute that is shared by all Dog objects, while name and age are instance attributes specific to each object.

Using Class and Instance Attributes:

dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

# Accessing instance attributes
print(dog1.name)  # Output: Buddy
print(dog2.name)  # Output: Max

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

Both dog1 and dog2 share the species attribute, but their name and age are unique.

Encapsulation

Encapsulation is the practice of restricting direct access to some of an object’s attributes and methods. In Python, you can make an attribute private by prefixing it with an underscore (_) or double underscore (__).

Example:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.__age = age  # Private attribute

    def get_age(self):  # Public method to access private attribute
        return self.__age

Here, __age is a private attribute and cannot be accessed directly from outside the class. Instead, you access it using the get_age() method.

dog = Dog("Buddy", 3)
print(dog.name)  # Output: Buddy
# print(dog.__age)  # This would raise an AttributeError

print(dog.get_age())  # Output: 3

Encapsulation is useful for hiding the internal state of an object and controlling how the data is accessed or modified.

Inheritance

Inheritance allows a class (called a subclass) to inherit attributes and methods from another class (called a parent class). This promotes code reuse and allows you to extend or modify the functionality of the parent class.

Example:

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

    def make_sound(self):
        return "Some sound"

# Subclass (inherits from Animal)
class Dog(Animal):
    def make_sound(self):  # Overriding the parent method
        return "Woof!"

In this example, Dog is a subclass of Animal. It inherits the name attribute and make_sound() method, but overrides the method to provide its own behavior.

Using Inheritance:

dog = Dog("Buddy")
print(dog.name)  # Output: Buddy (inherited from Animal)
print(dog.make_sound())  # Output: Woof! (overridden method)

Inheritance allows you to create a class hierarchy and share common functionality between classes.

Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common parent class. It enables you to call the same method on different objects and have each object respond in its own way.

Example:

class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

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

In this example, both Dog and Cat classes inherit from Animal and implement their own version of make_sound().

Using Polymorphism:

animals = [Dog(), Cat()]

for animal in animals:
    print(animal.make_sound())
# Output:
# Woof!
# Meow!

Polymorphism allows you to write flexible and generic code that can work with different types of objects that share a common interface.

Example: Building a Simple OOP Program

Let’s create a program that models a bank account using OOP principles.

BankAccount Class:

class BankAccount:
    def __init__(self, owner, balance=0):


 self.owner = owner
        self.__balance = balance  # Private attribute

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return f"Deposited {amount}. New balance: {self.__balance}"
        return "Deposit amount must be positive."

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return f"Withdrew {amount}. New balance: {self.__balance}"
        return "Insufficient balance or invalid amount."

    def get_balance(self):
        return self.__balance

Using the BankAccount Class:

# Creating an account for Alice
alice_account = BankAccount("Alice", 100)

# Depositing money
print(alice_account.deposit(50))  # Output: Deposited 50. New balance: 150

# Withdrawing money
print(alice_account.withdraw(30))  # Output: Withdrew 30. New balance: 120

# Checking balance
print(alice_account.get_balance())  # Output: 120

This example demonstrates how to create a bank account class with encapsulation, private attributes, and methods to deposit, withdraw, and check the balance.

Key Concepts Recap

  • A class is a blueprint for creating objects, which are instances of that class.
  • Attributes store data related to the object, and methods define its behavior.
  • The __init__() method is the constructor that initializes the object’s attributes.
  • Encapsulation restricts direct access to certain attributes to control how data is accessed and modified.
  • Inheritance allows a subclass to inherit attributes and methods from a parent class, promoting code reuse.
  • Polymorphism allows different objects to respond to the same method call in their own way, providing flexibility in code design.

Exercises

  1. Create a Car class with attributes for make, model, and year. Add methods to display the car’s details and honk the horn.
  2. Write a Student class that stores the name and a list of grades. Add methods to calculate the average grade.
  3. Create a Shape class with a method area() that returns 0. Then, create subclasses Rectangle and Circle that calculate the area for each shape type.
  4. Create a Library class with methods to add, borrow, and return books. Ensure that books cannot be borrowed if they are already checked out.

Next time, we’ll dive deeper into advanced OOP concepts like multiple inheritance, abstract classes, and method resolution order (MRO).

FAQ

Q1: What is the difference between a class attribute and an instance attribute?

A1:

  • Class attributes are shared by all instances of the class. They are defined directly inside the class, outside of any methods, and are the same for every object created from the class.
  • Example: class Dog: species = "Canis familiaris" # Class attribute Every Dog object will have the same species value: "Canis familiaris".
  • Instance attributes are specific to each object (instance) of the class. They are defined within the __init__() method or other methods using self, and their values can vary between different objects.
  • Example:
    python class Dog: def __init__(self, name): self.name = name # Instance attribute
    Each Dog object can have a different name.

In short, class attributes are shared among all objects, while instance attributes are unique to each object.

Q2: What does self mean, and why is it used in methods?

A2: self is a reference to the current instance of the class. It is used to access instance attributes and methods within the class. Whenever a method is called on an object, Python automatically passes the instance as the first argument to the method. This allows you to refer to the object’s data inside the method.

For example:

class Dog:
    def __init__(self, name):
        self.name = name  # 'self' refers to the specific instance

    def bark(self):
        print(f"{self.name} says woof!")

In this example, self.name refers to the name attribute of the specific Dog object. When you create multiple Dog objects, each object will have its own name attribute, but self ensures that each object refers to its own data.

Q3: Can I create a method in a class without using self?

A3: Yes, but only for class methods or static methods, which do not operate on individual instances of the class.

  • Class methods use @classmethod and take cls as the first argument, referring to the class itself.
  • Static methods use @staticmethod and do not take self or cls as arguments.

Example of a class method:

class Dog:
    species = "Canis familiaris"

    @classmethod
    def show_species(cls):
        print(f"All dogs are {cls.species}")

Example of a static method:

class Dog:
    @staticmethod
    def bark():
        print("Woof!")

Both methods don’t need self because they don’t operate on instance-specific data. Instance methods (regular methods) always require self to access attributes and methods of the specific object.

Q4: What is the difference between __init__() and __new__()?

A4:

  • __init__(): This is the constructor method in Python that initializes an instance of a class after it is created. It doesn’t create the instance but is called after the instance has been created to set up attributes.
  • Example: class Dog: def __init__(self, name): self.name = name
  • __new__(): This is a low-level method responsible for actually creating and returning a new instance of the class. It is rarely used unless you need to control object creation (such as for immutable types).
  • Example:
    python class Dog: def __new__(cls, *args, **kwargs): instance = super(Dog, cls).__new__(cls) return instance

In most cases, you only need __init__(), and __new__() is used in advanced scenarios like customizing object creation in immutable objects (e.g., integers, strings).

Q5: Can I have multiple constructors in a Python class?

A5: Python does not support multiple constructors in the same way as some other languages (like Java or C++). However, you can simulate multiple constructors by using default parameters or class methods to provide different ways to initialize an object.

Example using default parameters:

class Dog:
    def __init__(self, name="Unknown", age=0):
        self.name = name
        self.age = age

Example using a class method to create an alternative constructor:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name, birth_year):
        age = 2024 - birth_year
        return cls(name, age)

# Regular constructor
dog1 = Dog("Buddy", 3)

# Alternative constructor
dog2 = Dog.from_birth_year("Max", 2021)

This approach allows you to have flexible object creation without traditional multiple constructors.

Q6: What is the difference between public, protected, and private attributes?

A6:

  • Public attributes: These can be accessed freely from anywhere, inside or outside the class. In Python, all attributes are public by default.
  class Dog:
      def __init__(self, name):
          self.name = name  # Public attribute
  • Protected attributes: These are indicated with a single underscore (_) and are intended to be accessed only within the class or its subclasses. However, this is just a convention, not a strict rule, and they can still be accessed outside the class.
  class Dog:
      def __init__(self, name):
          self._age = 3  # Protected attribute
  • Private attributes: These are indicated with a double underscore (__) and are meant to be accessed only within the class. Python uses name-mangling to make these attributes harder to access from outside the class.
  class Dog:
      def __init__(self, name):
          self.__age = 3  # Private attribute

Private attributes can still be accessed outside the class, but it requires special syntax (e.g., _ClassName__attribute), which is not recommended.

Q7: How can I override a method from a parent class in a subclass?

A7: To override a method in a subclass, you simply define a method with the same name as the one in the parent class. The new method in the subclass will replace the one from the parent class when called on an instance of the subclass.

Example:

class Animal:
    def make_sound(self):
        return "Some generic sound"

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

Here, the Dog class overrides the make_sound() method of the Animal class.

If you want to call the parent class’s version of the method inside the overridden method, you can use super():

class Dog(Animal):
    def make_sound(self):
        original_sound = super().make_sound()
        return original_sound + " Woof!"

Q8: Can a class inherit from more than one parent class?

A8: Yes, Python supports multiple inheritance, meaning a class can inherit from more than one parent class. This can be useful but also introduces complexity in terms of method resolution order (MRO), which determines which parent class’s method is called if there are multiple methods with the same name.

Example:

class Animal:
    def eat(self):
        return "Eating"

class Pet:
    def play(self):
        return "Playing"

class Dog(Animal, Pet):
    pass

In this example, Dog inherits from both Animal and Pet, so it has access to both the eat() and play() methods.

To avoid issues with multiple inheritance, Python uses the C3 linearization algorithm, which determines the method resolution order (MRO). You can view the MRO of a class using the .__mro__ attribute:

print(Dog.__mro__)

Q9: What is polymorphism, and how does it work in Python?

A9: Polymorphism allows you to define methods in a parent class that can be overridden by subclasses, enabling different classes to have methods with the same name but different behavior. This allows objects of different classes to be treated in the same way, as long as they share a common interface (e.g., having the same method name).

Example:

class Animal:
    def make_sound(self):
        raise NotImplementedError("Subclasses must implement this method")

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

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

Both Dog and Cat implement the make_sound() method in different ways. Polymorphism allows you to use these objects interchangeably.

animals = [Dog(), Cat()]

for animal in animals:
    print(animal.make_sound())
# Output:
# Woof!
# Meow!

Q10: What is the purpose of super() in inheritance?

A10: The super() function allows you to call methods of the parent class from within a subclass. It is commonly used to invoke the parent class’s __init__() method or other methods that have been overridden in the subclass. This ensures that the parent class is properly initialized and its methods can be reused.

Example:

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

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

Here, super().__init__(name) ensures that the name attribute from the Animal class is properly initialized in the Dog class.

Q11: Can I create a class without a constructor (__init__())?

A11: Yes, you can create a class without defining an __init__() constructor. If no constructor is defined, Python will provide a default constructor, and you can still create objects of that class. However, without an __init__() method, you won’t be able to initialize any instance-specific attributes when creating objects.

Example:

class Dog:
    def bark(self):
        return "Woof!"

You can create a Dog object and call its bark() method, but you won’t be able to assign attributes like name unless you define an __init__() method.

Q12: What are abstract classes and methods?

A12: An abstract class is a class that cannot be instantiated and is meant to be subclassed. It typically contains one or more abstract methods, which are methods that are declared but contain no implementation. Subclasses are expected to implement these abstract methods.

In Python, abstract classes are defined using the abc module.

Example:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

You cannot create an object of the Animal class directly, but you can create objects of subclasses like Dog, as long as they implement the abstract methods.

Abstract classes are useful when you want to ensure that certain methods are implemented in all subclasses.

Similar Posts