Classes & Objects: The So Called Advanced Concepts

Classes & Objects: The So Called Advanced Concepts

In our previous letter, we introduced classes and objects as being the blueprint, and the manufactured products or instances. From the context of say cooking, they are the recipe, and actual meals ready to eat. there are some advanced aspects to Python classes, just like professional kitchens are equipped for advanced techniques to meet demands that are beyond just basic cooking.

Lets now jump into professional grade Python classes,

Inheritance: evolution of our family recipe

for generations, every family has their unique recipe that gets passed down since generations from our grandmother, to mother, and further. Now say the same recipe, might have some added twist, to include a ingredient or two that's in demand these days- back then it was butter cookies, next we started adding choco chips and now even nuts too!

This is inheritance!

class SmartDevice:  # Base class (Grandma's basic recipe)
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        self.isSwitchedOn = False
        
    def switchOn(self):
        self.isSwitchedOn = True
        
    def switchOff(self):
        self.isSwitchedOn = False

# Smartphone inherits from SmartDevice (Your generation's twist on the recipe)
class Smartphone(SmartDevice):
    def __init__(self, brand, model, screenSize, battery):
        # Call parent's init first (start with Grandma's base)
        super().__init__(brand, model)
        # Add our own ingredients
        self.screenSize = screenSize
        self.battery = battery
        
    def makeCall(self, number):
        if self.isSwitchedOn:
            return f"Calling {number} from {self.brand} {self.model}"
        return "Phone is off. Switch it on first!" 

 This is how, using inheritance, our Smartphone gets all the properties and functionalities of a SmartDevice, without having to mention them explicitly.

Polymorphism: our kitchen all purpose cutter

ever noticed how a chef has the ability to chop vegetables, slice meat with the same knife and basic motion- but slightly adapted. This is polymorphism- same setup or interface but different behaviour as per the context

class SmartSpeaker(SmartDevice):
    def __init__(self, brand, model, voiceAssistant):
        super().__init__(brand, model)
        self.voiceAssistant = voiceAssistant
    
    # Notice how this has the same name as smartphone's method!
    def makeCall(self, number):
        if self.isSwitchedOn:
            return f"{self.voiceAssistant} is calling {number} through {self.brand} speaker"
        return "Speaker is off. Switch it on first!"

# Same function works with different object types
def initiateCall(device, number):
    return device.makeCall(number)

myPhone = Smartphone("Apple", "iPhone 14", 6.1, 3200)
mySpeaker = SmartSpeaker("Amazon", "Echo", "Alexa")

print(initiateCall(myPhone, "555-123-4567"))  # Uses Smartphone's version
print(initiateCall(mySpeaker, "555-123-4567"))  # Uses SmartSpeaker's version

the fun here? our initiateCall does not know what kind of object or device it is working with, but as long as both objects have the same functionality makeCall defined, it just works!

Encapsulation: the open kitchen boundaries

Ever visited restaurants with open kitchen setup. It's quite good, that we are able to see our food getting cooked, but at the same time there's a bay that restricts access.

This is encapsulation. In a similar way, Python uses underscore to denote or signal restriction but does not enforce it,

class BankAccount:
    def __init__(self, accountNumber, ownerName, balance):
        self.accountNumber = accountNumber
        self.ownerName = ownerName
        self._balance = balance  # Single underscore: "protected"
        self.__transactionLog = []  # Double underscore: "private"
    
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            self.__logTransaction("deposit", amount)
            return f"Deposited {amount}. New balance: {self._balance}"
        return "Invalid amount"
    
    def __logTransaction(self, type, amount):  # Private method
        import datetime
        self.__transactionLog.append({
            "type": type,
            "amount": amount,
            "date": datetime.datetime.now()
        })

In this example, while _balance is something that can be viewed by customers, but can not be directly modified. customers can make changes to their _balance by using deposit() to add money.

While the __transaction and __logTransaction are purely confidential since they are supposed to be bank's internal records, and not meant for end customer.

Composition: the appliance assembled parts

We have food processor, and blenders. Wouldn't a food processor be able to perform the same function a blender is supposed to? answer is yes. but it does not need to inherit those functionality, rather has it composed or embedded as another object:

class Battery:
    def __init__(self, capacity):
        self.capacity = capacity
        self.currentCharge = capacity
    
    def charge(self, amount):
        self.currentCharge = min(self.capacity, self.currentCharge + amount)
    
    def use(self, amount):
        if self.currentCharge >= amount:
            self.currentCharge -= amount
            return True
        return False

class SmartphoneWithComposition:
    def __init__(self, brand, model, screenSize, batteryCapacity):
        self.brand = brand
        self.model = model
        self.screenSize = screenSize
        self.battery = Battery(batteryCapacity)  # Composition!
        
    def makeCall(self, number, callDuration=1):
        if self.battery.use(callDuration * 5):  # Calls use 5 battery units per minute
            return f"Calling {number}..."
        return "Not enough battery for this call!"

In this example, we see that since a SmartphoneWithComposition isn't an instance of Battery, rather it contains battery as an object- hence inheritance isn't applicable, rather composition, since a phone "has-a" battery is is not "is-a" battery!

End of the day, remember these patterns of implementation aren't anything fancy or out of the box- its just the way we expect objects in our real world to behave or exemplify itself in a certain way.

Next time, we dive into the world of libraries, and how these principles apply in larger systems. Till then, have fun learning, and explore how you can apply this in code. Cheers!