27. OOP ‐ Class Methods - MantsSk/CA_PTUA14 GitHub Wiki

Class methods

Python’s classmethod() function is a powerful tool that allows developers to manipulate class-level data in a clean and efficient way. With its ability to create factory methods, alternative constructors, manage class-level state, and implement class-level operations, classmethod() provides a flexible and elegant solution to many programming challenges. We will explore how to use classmethod() in Python to improve your code’s organization and readability. Using classmethod() will enhance your Python skills and make your code more efficient and maintainable.

What is classmethod() in Python?

classmethod() is a built-in function in Python that is used to define a method that operates on the class instead of the instance of the class. It is used to create a method that can be called directly on the class, rather than on an instance of the class. This means that the method can be used to manipulate class-level data or perform operations that are not specific to any one instance of the class. The syntax of classmethod() is as follows:

class MyClass:
    @classmethod
    def my_class_method(cls, arg1, arg2):
        # code here

The classmethod() function is applied as a decorator to a method of a class. The first argument of a class method is always the class itself, represented by the parameter cls. This allows the method to access and modify class-level data.

Here's an example of a class method that calculates how many vehicles we created:

class Vehicle:
    total_vehicles = 0

    def __init__(self):
        Vehicle.total_vehicles += 1

    @classmethod
    def get_total_vehicles(cls):
        return cls.total_vehicles

v1 = Vehicle()
v1 = Vehicle()

print("Total Vehicles:", Vehicle.get_total_vehicles())  # Would print the total number of Vehicle instances

Why use classmethod()?

The main benefit of using classmethod() is that it allows you to define methods that operate on the class itself, rather than on an instance of the class. This can be useful in a variety of situations, such as:

  • Creating factory methodsclassmethods can be used to create factory methods that create and return new instances of a class with a specific configuration:

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

    @classmethod
    def from_birth_year(cls, name, birth_year):
        age = cls.get_age(birth_year)
        return cls(name, age)

    @staticmethod
    def get_age(birth_year):
        return datetime.date.today().year - birth_year

person = Person.from_birth_year('John', 1990)
print(person.name)  # John
print(person.age)  # 33

In this example, the from_birth_year() class method acts as a factory method that creates a new Person instance from a birth year. It calculates the age of the person using a static method get_age() and returns a new instance of the Person class.

  • Managing class-level stateclassmethods can be used to manage class-level state. This means that you can define a method that modifies or accesses a variable that is shared by all instances of the class (just like in previously shown example)
class Car:
    total_cars_sold = 0

    def __init__(self, make, model):
        self.make = make
        self.model = model
        Car.total_cars_sold += 1

    @classmethod
    def get_total_cars_sold(cls):
        return cls.total_cars_sold

car1 = Car('Toyota', 'Camry')
car2 = Car('Honda', 'Civic')

print(Car.get_total_cars_sold())  # 2

In this example, the total_cars_sold class-level variable is incremented every time a new instance of the Car class is created. The get_total_cars_sold() class method returns the current value of the total_cars_sold variable.

  • Implementing class-level operationsclassmethods can be used to implement class-level operations, such as sorting or filtering a list of instances of the class:
class Student:
    all_students = []

    def __init__(self, name: str, grade: float):
        self.name = name
        self.grade = grade
        Student.all_students.append(self)

    @classmethod
    def get_highest_grade(cls):
        return max(cls.all_students, key=lambda student: student.grade)

    @classmethod
    def get_lowest_grade(cls):
        return min(cls.all_students, key=lambda student: student.grade)

student1: Student = Student('John', 90)
student2: Student = Student('Jane', 95)
student3: Student = Student('Alice', 80)

print(Student.get_highest_grade().name)  # Jane
print(Student.get_lowest_grade().name)  # Alice

The all_students attribute is declared as a class-level list that holds Student objects. The name attribute of the Student instances is declared as a string, and the grade attribute is declared as a float. The get_highest_grade and get_lowest_grade class methods return the Student object with the highest and lowest grade, respectively. The student1, student2, and student3 instances are explicitly annotated with the Student class.

  • Static vs Class methods – Using @classmethod allows the method to access class-level data and it works well with inheritance. The cls variable in class methods allows for dynamic access to class properties. Here are examples of the same code implemented both in static and class methods.

Class Method:

class Vehicle:
    total_vehicles = 0

    def __init__(self):
        Vehicle.total_vehicles += 1

    @classmethod
    def get_total_vehicles(cls):
        return cls.total_vehicles

class Car(Vehicle):
    total_cars = 0

    def __init__(self):
        super().__init__()
        Car.total_cars += 1

    @classmethod
    def get_total_vehicles(cls):
        return super().get_total_vehicles() + cls.total_cars

class Truck(Vehicle):
    total_trucks = 0

    def __init__(self):
        super().__init__()
        Truck.total_trucks += 1

    @classmethod
    def get_total_vehicles(cls):
        return super().get_total_vehicles() + cls.total_trucks

v1 = Vehicle()
c1 = Car()
c2 = Car()
t1 = Truck()

print("Total Vehicles:", Vehicle.get_total_vehicles())  # Would print the total number of Vehicle instances
print("Total Cars:", Car.get_total_vehicles())          # Would include all Vehicles + Cars
print("Total Trucks:", Truck.get_total_vehicles())      # Would include all Vehicles + Trucks

Static Method:

class Vehicle:
    total_vehicles = 0

    def __init__(self):
        Vehicle.total_vehicles += 1

    @staticmethod
    def get_total_vehicles():
        return Vehicle.total_vehicles

class Car(Vehicle):
    total_cars = 0

    def __init__(self):
        super().__init__()
        Car.total_cars += 1

    @staticmethod
    def get_total_vehicles():
        return Vehicle.get_total_vehicles() + Car.total_cars

class Truck(Vehicle):
    total_trucks = 0

    def __init__(self):
        super().__init__()
        Truck.total_trucks += 1

    @staticmethod
    def get_total_vehicles():
        return Vehicle.get_total_vehicles() + Truck.total_trucks


v1 = Vehicle()
c1 = Car()
c2 = Car()
t1 = Truck()

print("Total Vehicles:", Vehicle.get_total_vehicles())  # Would print the total number of Vehicle instances
print("Total Cars:", Car.get_total_vehicles())          # Would include all Vehicles + Cars
print("Total Trucks:", Truck.get_total_vehicles())      # Would include all Vehicles + Trucks

Notice how static methods require more explicit class name references. Each get_total_vehicles method explicitly references the specific class (Vehicle, Car, or Truck). This makes the code less flexible and more prone to error if the class hierarchy changes.

Script that uses instance methods, class methods, and static methods:

class Person:
    population = 0

    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.population += 1

    def introduce_self(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

    @classmethod
    def get_population(cls):
        return cls.population

    @staticmethod
    def is_adult(age):
        return age >= 18

# Creating instances
alice = Person("Alice", 30)
bob = Person("Bob", 17)

# Using instance method
alice.introduce_self()
bob.introduce_self()

# Using class method
print("Population:", Person.get_population())

# Using static method
print("Is Alice an adult?", Person.is_adult(alice.age))
print("Is Bob an adult?", Person.is_adult(bob.age))

How the Script Works: Instance Method (introduce_self): Each Person instance can introduce itself. This method uses the instance's own data (name and age).

Class Method (get_population): This method provides the count of Person instances. It uses the class variable population, which is shared across all instances.

Static Method (is_adult): This method determines if a given age is considered adult. It's a utility method that operates independently of any instance or class variables.

Exercises:

  1. Create a class method to return the factorial of a given number.

  2. Create a class method to return the reversed string of a given string.

  3. Create a Python class named BankAccount that demonstrates the use of instance methods, class methods, and static methods. The class and its methods should provide functionality as described below. After implementing the class, create instances and use all the methods to showcase their functionality.

Class Specification

  • Class Name: BankAccount

  • Instance Attributes: owner: The name of the account owner (a string). balance: The account balance (a float). Instance Method: show_balance: Prints the current balance of the account.

  • Class Attribute: total_accounts: A class-level attribute that tracks the total number of BankAccount instances created.

  • Class Method: get_total_accounts: Returns the total number of BankAccount instances.

  • Static Method: validate_amount: Accepts an amount and returns True if it is a positive number; otherwise, it returns False.

Task for Students:

  • Implement the BankAccount class as per the specifications given above.
  • Create Instances: Create at least two instances of BankAccount with different owners and balances.
  • Use Instance Method: Use the show_balance method for each instance to display the account balance.
  • Use Class Method: Use the get_total_accounts method to display the total number of bank accounts created.
  • Use Static Method: Use the validate_amount method to validate a couple of amounts (one positive and one negative) to demonstrate how the method works.
  1. Create a simple bank account class, BankAccount, with the following specifications:

    • The BankAccount class should have an attribute balance which starts at 0.
    • It should have an instance method deposit that allows an amount to be added to the balance.
    • It should have an instance method withdraw that allows an amount to be taken from the balance. If the balance is less than the withdrawal amount, print a message that says "Insufficient funds".
    • Add a class method from_balance that takes a starting balance as an argument and returns a new BankAccount instance with that starting balance.
    • Add a static method transfer that takes two BankAccount instances and an amount as arguments. It should withdraw the amount from the first account and deposit it into the second account.
  2. Create a SpaceStation class with the following specifications:

    • The SpaceStation class should have an attribute astronauts which is a list of dictionaries. Each dictionary represents an astronaut and has keys: name, nationality, and mission_duration.
    • It should have an instance method add_astronaut that takes a name, nationality, and mission duration, creates a new astronaut dictionary, and adds it to the astronauts list.
    • It should have an instance method find_astronaut that takes a name and returns the astronaut dictionary with that name, or None if no such astronaut is found.
    • Add a class method from_astronaut_list that takes a list of astronauts (each represented as a dictionary) and returns a new SpaceStation instance with those astronauts.
    • Add a static method is_long_term_mission that takes an astronaut dictionary and returns True if the astronaut's mission duration is more than 6 months, and False otherwise.
    • Add an instance method remove_astronaut that takes a name and removes the astronaut with that name from the astronauts list.
  3. Create a class to represent a library system. The library system should have a collection of books that can be borrowed by users. Users can register to the library system, borrow books, and return books. The library system should keep track of the books borrowed by users, and the number of available copies of each book. Specifications:

  • Library class should have instance attribute 'books', that is a dictionary of books. Example format: {'Title': available_copy_count, 'Harry Potter': 1}
  • Library class should have instance attribute 'user', that is a dictionary of users. Example format: {'user_id': ['name', ['list', 'of', 'book', 'titles'], '1': ['John', ['Harry Potter', 'The adventures of Tom Sawyer']}
  • Library class should have instance method 'add_book', that adds a new book to 'books' list
  • Library class should have instance method 'register_user', that adds a new user to 'users' list
  • Library class should have instance method 'borrow_book', that adds a selected book's title to selected user's list of books and decreases the number of available book copies
  • Library class should have instance method 'return_book', that removes selected book from the user and increases the number of available book copies
  • Library class should have instance method 'register_user', that adds a new user to 'users' list
  • Library class should have class method 'create_library_system', that accepts user and book lists and returns a new library system with the users and books
  • Library class should have static method 'are_copies_available', that accepts book list and a title as parameters and returns True if the specified book has available copies and False if not

🌐 Extra reading (or watching 📺 ):