24. OOP ‐ Abstraction (4th pillar) - MantsSk/CA_PTUA14 GitHub Wiki

Abstract classes and methods

In object-oriented programming, abstraction is a principle that helps to separate the implementation details of a class from its interface (look bellow ⏬ ), or the way that other classes interact with it. This allows for greater flexibility and reusability of code, as the implementation of a class can be changed without affecting other parts of the system.

In simpler terms, it's about identifying what an object does rather than how it does it. This separation of 'what' from 'how' not only simplifies understanding but also enhances the flexibility and reusability of code.

Abstract Classes: These are classes that are defined in Python but are not meant to be used directly. Think of them as outlines or templates for other classes. They lay out a certain structure that other 'concrete' classes will follow. An abstract class is like a contract, specifying what its subclasses should do without dictating how they should do it.

Abstract Methods: These methods are declared, but they contain no implementation in the abstract class. Instead, they are like placeholders, meant to be overridden by subclasses. Each subclass of the abstract class is required to provide its own unique implementation of these methods.

Here is an example of an abstract class in Python:

from abc import ABC, abstractmethod
from typing import Union

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

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

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

In the above example, Shape is an abstract class that defines an abstract method area(). The Rectangle and Circle classes are subclasses of Shape and provide implementations for the area() method.

It's important to notice that ABC and abstractmethod are part of python built-in library abc, This library provided python with abstract base class(ABC) and decorator (check another lesson, TBA) abstractmethod which needs to be imported and used, when implementing abstract classes.

It's also important to mention that it's not allowed to create instance of an abstract class, If you try to create an instance of an abstract class, Python will raise a TypeError exception.

In addition to the above example, one more example is:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def no_of_wheels(self):
        pass

class Car(Vehicle):
    def no_of_wheels(self):
        return 4

class Bike(Vehicle):
    def no_of_wheels(self):
        return 2


c = Car()
print(c.no_of_wheels()) # 4

a = Vehicle() # TypeError: Can't instantiate abstract class Vehicle with abstract method no_of_wheels

In this example, Vehicle is an abstract class that defines an abstract method no_of_wheels(), and Car, Bike are concrete subclasses, which provides an implementation for no_of_wheels method.

One more example about payment systems (more complex: abstraction, inheritance):

from abc import ABC, abstractmethod

class Payment(ABC):
    @abstractmethod
    def get_transactions(self):
        pass

    @abstractmethod
    def process_payment(self, amount):
        pass

class CreditCardPayment(Payment):
    def __init__(self, card_number):
        self.card_number = card_number
        self.transactions = []

    def get_transactions(self):
        return self.transactions

    def process_payment(self, amount):
        transaction_id = "CC" + str(hash(str(amount) + self.card_number))
        self.transactions.append(transaction_id)
        return f"Payment of {amount} processed with card number {self.card_number}. Transaction ID: {transaction_id}"

class PayPalPayment(Payment):
    def __init__(self, email):
        self.email = email
        self.transactions = []

    def get_transactions(self):
        return self.transactions

    def process_payment(self, amount):
        transaction_id = "PP" + str(hash(str(amount) + self.email))
        self.transactions.append(transaction_id)
        return f"Payment of {amount} processed with PayPal account {self.email}. Transaction ID: {transaction_id}"

# Creating instances of CreditCardPayment and PayPalPayment
credit_card_payment = CreditCardPayment("1234-5678-9012-3456")
paypal_payment = PayPalPayment("[email protected]")

# Processing a payment with a credit card
credit_card_result = credit_card_payment.process_payment(100.50)
print(credit_card_result)  # Output: Payment of 100.5 processed with card number 1234-5678-9012-3456. Transaction ID: CC...

# Processing a payment with PayPal
paypal_result = paypal_payment.process_payment(75.00)
print(paypal_result)  # Output: Payment of 75.0 processed with PayPal account [email protected]. Transaction ID: PP...

# Retrieving transactions for the credit card payment
credit_card_transactions = credit_card_payment.get_transactions()
print("Credit Card Transactions:", credit_card_transactions)

# Retrieving transactions for the PayPal payment
paypal_transactions = paypal_payment.get_transactions()
print("PayPal Transactions:", paypal_transactions)

In this example, there's an abstract class Payment, which defines two abstract methods get_transactions() and process_payment().

CreditCardPayment and PayPalPayment are subclasses of Payment, which provide concrete implementations of the two abstract methods defined in the parent class.

Each of the subclasses has their own way of processing payments and generating transaction ID.

Each class also has an instance variable transactions which contains a list of transactions that have been made through this payment method. The get_transactions() method of each class returns the list of transactions made through this payment method.

This example demonstrates the use of abstraction to separate the implementation details of the different payment methods from their common interface, defined by the Payment class. The Payment class provides a template for the subclasses to follow, and the subclasses provide concrete implementations of the abstract methods.

It's best practice to keep all the abstract methods in a separate abstract class, this way you can ensure that the common properties and methods for a group of similar classes, will be written and managed in a single place.

In a nutshell - the abstraction principle in python allows for the separation of implementation details from the interface, and abstract classes and methods provide a way to define this separation by providing a template for subclasses to follow, this way subclasses are forced to implement certain methods which are defined in the abstract class.

Interfaces

An interface in Python is more of a conceptual construct, guiding how to structure your classes. Python does not have explicit support for interfaces like some other languages (e.g., Java). However, you can use abstract classes to create something similar to an interface. An interface is a class where all the methods are abstract. Thus, an interface defines a set of methods that the implementing class must provide.

An example:

from abc import ABC, abstractmethod

class IShape(ABC):
    @abstractmethod
    def draw(self):
        pass

    @abstractmethod
    def area(self):
        pass

class Circle(IShape):
    def __init__(self, radius):
        self.radius = radius

    def draw(self):
        print("Drawing a circle")

    def area(self):
        return 3.14 * self.radius * self.radius

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

    def draw(self):
        print("Drawing a rectangle")

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

circle = Circle(5)
circle.draw()  # Outputs: Drawing a circle
print(circle.area())  # Outputs: Area of the circle

rectangle = Rectangle(3, 4)
rectangle.draw()  # Outputs: Drawing a rectangle
print(rectangle.area())  # Outputs: Area of the rectangle

Exercises:

  • Task Nr.1:

    Create an abstract class Animal with which takes name of animal as an input and initialize it. The create speak abstract method, to be overridden by subclasses. And get_name method which returns name of the animal.

    Now create two subclasses of Animals: Dog and Cat which overrides the speak method, and provide the implementation which returns a string "Dog says Woof!" and "Cat says Meow!" respectively.

  • Task Nr.2:

    Create an abstract class Money which takes currency and value as input and initializes it. A class must have these methods:

    • get_value method which returns the value of the money.
    • get_currency method which returns the currency of the money.
    • convert_to_currency abstract method, which takes target currency and conversion rate as input and converts the value of the money to the target currency.

    Now create two subclasses of Money: Cash and Card. The Cash class should take the denomination of the cash as input in the constructor, and should implement the convert_to_currency method. The Card class should take the credit limit of the card as input in the constructor, and should implement the convert_to_currency method using the conversion rate to convert the value of the card to the target currency.

🌐 Extra reading (or watching 📺 ):