description: "Object-Oriented Programming — Object-oriented programming lets you bundle data and the behavior that acts on it into a single unit — a class. Level 1 used…"---
01 · Object-Oriented Programming¶
Object-oriented programming lets you bundle data and the behavior that acts on it into a single unit — a class. Level 1 used dictionaries to represent things like a to-do item; classes give you a more structured, extensible way to model real-world entities with their own methods and rules.
Defining a class¶
class Dog:
species = "Canis familiaris" # class attribute, shared by all instances
def __init__(self, name, age):
self.name = name # instance attribute
self.age = age
def bark(self):
return f"{self.name} says woof!"
rex = Dog("Rex", 3)
print(rex.bark()) # Rex says woof!
print(rex.species) # Canis familiaris
print(Dog.species) # Canis familiaris — shared across instances
__init__ is the constructor: Python calls it automatically when you create a
new instance. self refers to the specific instance being created or acted on.
Inheritance¶
A subclass reuses and extends the behavior of a parent class.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclasses must implement speak()")
def introduce(self):
return f"I am {self.name} and I say {self.speak()}"
class Cat(Animal):
def speak(self):
return "Meow"
class Cow(Animal):
def speak(self):
return "Moo"
for animal in (Cat("Whiskers"), Cow("Bessie")):
print(animal.introduce())
# I am Whiskers and I say Meow
# I am Bessie and I say Moo
Polymorphism¶
The loop above is polymorphism in action: the same introduce() call behaves
differently depending on the actual (runtime) type of animal, because each
subclass provides its own speak().
def make_them_speak(animals):
for animal in animals:
print(animal.speak())
make_them_speak([Cat("Tom"), Cow("Milka")])
super() — calling the parent implementation¶
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def describe(self):
return f"{self.name} earns {self.salary}"
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary) # reuse the parent's __init__
self.team_size = team_size
def describe(self):
base = super().describe() # reuse the parent's method
return f"{base} and manages {self.team_size} people"
print(Manager("Priya", 95000, 4).describe())
# Priya earns 95000 and manages 4 people
Dunder (magic) methods¶
Dunder methods let your objects integrate with Python's built-in syntax
(print(), +, ==, len(), iteration, and more).
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __len__(self):
return int((self.x ** 2 + self.y ** 2) ** 0.5)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 == Vector(1, 2)) # True
print(len(v2)) # 5
| Dunder method | Triggered by |
|---|---|
__init__ |
creating an instance |
__repr__ |
repr(obj), debugger/console display |
__str__ |
str(obj), print(obj) |
__eq__ |
== |
__lt__ |
< (and enables sorted()) |
__len__ |
len(obj) |
__add__ |
+ |
__getitem__ |
obj[key] |
__iter__ |
for x in obj |
Properties — controlled attribute access¶
@property lets you expose a method as if it were a plain attribute, so you
can validate or compute values without changing the calling code.
class Circle:
def __init__(self, radius):
self.radius = radius # goes through the setter below
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("radius must be positive")
self._radius = value
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(2)
print(c.area) # 12.56636
c.radius = 5 # goes through the setter, validated
print(c.area) # 78.53975
try:
c.radius = -1
except ValueError as e:
print(e) # radius must be positive
Class methods and static methods¶
class Pizza:
def __init__(self, toppings):
self.toppings = toppings
@classmethod
def margherita(cls):
"""Alternate constructor — a common use of classmethod."""
return cls(["tomato", "mozzarella", "basil"])
@staticmethod
def slice_count(diameter_inches):
"""Doesn't need self or cls — just lives on the class for organization."""
return diameter_inches // 2
print(Pizza.margherita().toppings)
print(Pizza.slice_count(12)) # 6
How It Actually Works¶
A class statement is executable code that builds an object. When the
interpreter reaches class Dog:, it:
- Runs the class body as a mini-scope, collecting every name it defines
(
species,__init__,bark) into a fresh namespace dict. - Calls the metaclass —
typeby default — astype("Dog", (object,), namespace). That call allocates a new class object whose__dict__is (a mappingproxy over) that namespace, links its__bases__and computes its__mro__. - Binds the name
Dogto that class object. Classes are just objects;Dogis an instance oftype.
Creating an instance: Dog("Rex", 3) calls type.__call__(Dog, ...),
which does instance = Dog.__new__(Dog) (allocates a blank object with its own
__dict__), then instance.__init__("Rex", 3) (your constructor, which fills
that dict), then returns the instance.
Attribute lookup (rex.bark) is type.__getattribute__ running a
precise algorithm: check the type's MRO for a data descriptor named bark;
if none, check the instance's own __dict__; if none, check the MRO for a
non-data descriptor or plain class attribute. Functions stored on a class are
non-data descriptors: Dog.__dict__["bark"].__get__(rex, Dog) returns a
bound method that remembers rex, which is how self gets passed
automatically. rex.species misses the instance dict and finds the shared
class attribute — assign rex.species = ... and you create an instance-level
entry that shadows it.
super().describe() doesn't mean "my parent" — it walks to the next class
after the current one in the instance's MRO, which is what makes cooperative
multiple inheritance work.
Exercise¶
Model a small library system: a Book class with title, author, and a
checked_out boolean; and a Library class that holds a list of Book
instances and has methods check_out(title) and return_book(title). Add
__repr__ to Book so printing a list of books is readable, and a @property
on Library called available_titles that returns titles not checked out.