Python Polymorphism and Encapsulation
Polymorphism means "many shapes": different classes can be used in the same way because they offer methods with the same names. Encapsulation means keeping an object's internal data behind its methods, so the rest of the program uses the object without depending on how it works inside. Together they make object-oriented programs flexible and safe to change.
One loop, many classes
import math
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return math.pi * self.r ** 2
class Triangle:
def __init__(self, base, height):
self.base, self.height = base, height
def area(self):
return 0.5 * self.base * self.height
shapes = [Square(3), Circle(1), Triangle(4, 5)]
for s in shapes:
print(f"{type(s).__name__:8} {s.area():6.2f}")
print("Total:", round(sum(s.area() for s in shapes), 2))Square 9.00 Circle 3.14 Triangle 10.00 Total: 22.14
The loop does not care which class each object belongs to; it only needs an area method. Adding a new shape class later needs no change to the loop.
Duck typing
These classes do not even share a parent. Python checks only that the method exists when it is called: "if it walks like a duck and quacks like a duck, it is a duck". Built-in functions work the same way: len works on anything that has a length, and for works on anything that can be looped over.
Private attributes by convention
Python has no truly private attributes. Instead, a name starting with an underscore, such as _balance, tells other programmers "this is internal, do not use it directly". Methods form the public way in.
Properties
A property looks like a plain attribute from outside, but runs a method when it is read or set. That lets a class check values without changing how it is used:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # goes through the setter below
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius, t.fahrenheit)
t.celsius = 100
print(t.fahrenheit)
try:
t.celsius = -300
except ValueError as e:
print("Error:", e)25 77.0 212.0 Error: below absolute zero
fahrenheit has no setter, so it is read-only: it is always worked out from the stored Celsius value and can never get out of step.
Practice exercises
- Add a
Rectangleclass with anareamethod to the shapes example and check that the loop still works. - Write classes
Dog,CatandDuckwith asoundmethod, and make each one speak in a loop. - Give a
Productclass apriceproperty that refuses negative prices. - Add a read-only property
kelvintoTemperature.