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!