OOP via Analogies
The whole module on one page β analogy on the left of your memory, definition on the right. Print it (Ctrl/Cmd+P) and stick it above your desk.
A class is a type definition: it declares the fields (state) and methods (behaviour) that its instances will have, plus how they are initialised. It allocates nothing on its own; instantiation allocates memory and binds the fields to that instance..
class House:
def __init__(self, colour):
self.colour = colour
def open_door(self):
return "creak"
# No house exists yet β this is only the drawing.An object is an instance: a distinct allocation with its own field values, referenced by an identity. Mutating one instance's state does not affect another instance of the same class; only shared/static members are common to all..
a = House("white")
b = House("white")
a.colour = "ochre"
print(a.colour) # "ochre"
print(b.colour) # "white" <- untouched
print(a is b) # False β two objectsAttributes (fields/properties) hold state; methods are functions bound to the instance that read or mutate that state. Methods receive the instance implicitly (this/self), which is how they reach the fields without being handed them..
class House:
def __init__(self):
self.colour = "white" # HAS
self.lights_on = False # HAS
def switch_lights(self): # DOES
self.lights_on = not self.lights_on
def repaint(self, colour): # DOES
self.colour = colourThe constructor runs at instantiation to establish the object's invariants: assign required fields, validate arguments, acquire resources. Code that depends on a fully-initialised object should never run before the constructor returns..
class House:
def __init__(self, colour, address):
if not colour:
raise ValueError("a house needs a colour")
self.colour = colour
self.address = address
self.ready = True # invariants now hold
# __init__ runs once, at creation. Never again.Encapsulation restricts direct access to internal state and exposes a controlled interface. Private fields let the class enforce invariants and change its implementation without breaking callers, since callers only depend on the public surface..
class House:
def __init__(self):
self.__safe_code = "4821" # name-mangled
def ring_bell(self): # public
return "who is it?"
h = House()
h.ring_bell() # fine
h.__safe_code # AttributeErrorA subclass inherits the fields and methods of its superclass and may extend them. This models an is-a relationship; the subclass can be used anywhere the superclass is expected (the Liskov substitution principle)..
class House:
def open_door(self):
return "creak"
class Townhouse(House): # inherits everything
pass
class Cottage(House):
pass
Townhouse().open_door() # "creak" β for freeOverriding replaces the superclass implementation of a method with the subclass's own, keeping the signature identical. Dispatch is dynamic: the runtime picks the implementation from the object's actual type, not the declared type..
class House:
def open_door(self): return "creak"
class Townhouse(House):
def open_door(self): return "buzz"
h: House = Townhouse()
h.open_door() # "buzz"
# resolved by the OBJECT, not the annotationPolymorphism lets one interface serve many concrete types. Callers program against the abstraction, so adding a new implementation requires no change at the call site β the core mechanism behind extensible designs..
class Dog:
def make_sound(self): return "woof"
class Cat:
def make_sound(self): return "meow"
class Car:
def make_sound(self): return "honk"
for thing in (Dog(), Cat(), Car()):
print(thing.make_sound())
# the loop never checks a typeAbstraction exposes essential behaviour and hides implementation detail, usually via an interface or abstract base. It reduces coupling: the consumer depends on a contract, so the detail behind it can be swapped freely..
def steer(angle):
_column_rotate(angle) # you never call these
def _column_rotate(a):
_rack_translate(a * 0.6)
def _rack_translate(mm):
_hydraulics_assist(mm)
steer(-15) # the only line you writeComposition builds behaviour by holding collaborators (has-a) rather than inheriting it (is-a). It avoids deep, rigid hierarchies and the fragile-base-class problem, and allows behaviour to be swapped at runtime by injecting a different collaborator..
class Car:
def __init__(self, engine):
self.engine = engine # HAS-A
def swap_engine(self, engine):
self.engine = engine # at runtime
car = Car(PetrolEngine())
car.swap_engine(ElectricEngine())