Data Structures and Object-Oriented Programming

Chapter 10: Object-Oriented Programming

1. Basic Classes - "blueprints for objects"

TermWhat it meansIn the code
ClassA recipe that describes what data an object stores and what it can do.class Person: ...
Object / InstanceA single thing built from that recipe.person = Person("John", 30)
Constructor (__init__)Runs automatically when you create the object; sets up its data.Stores name and age.
Instance methodA function that belongs to every object of the class.greet()

Why it matters: Classes bundle data (name, age) together with behavior (greet) so you can treat "a person" as one coherent unit.


2. Containment (Has-A) - objects inside objects

  • A Student has-a list of Courses.
  • We model that by storing self.courses: List[Course] = [] inside Student.
student.add_course(math)   # puts a Course object into the list
student.get_courses()      # returns ['Mathematics']

Takeaway: When something "owns" or "is made of" other objects, store them as attributes-often a list. This keeps related pieces neatly grouped.


3. Inheritance (Is-A) - building families of classes

  • Parent / base class: shared code (Animal with speak).
  • Child / derived class: gets everything the parent has plus its own tweaks (Dog, Cat override speak).
dog = Dog("Buddy")   # behaves like an Animal, but its speak() is "Woof!"

Why bother? Write common code once, specialize only where needed. This avoids repetition and lets you treat many related objects the same way (polymorphism).


4. Abstract Classes - "contracts" for future classes

class Shape(ABC):
    @abstractmethod
    def area(self): ...
  • Mark methods with @abstractmethod to promise every real Shape must implement them.
  • You can't create Shape() directly; you must create a concrete subclass like Rectangle.

Benefit: Enforces a consistent interface-handy in big projects where many people write different shapes but everyone can trust shape.area() exists.


5. Relational & Arithmetic Operators - making objects act like built-ins

Using the Fraction class:

MethodPurpose
__eq__== compares two fractions properly.
__lt__< compares them.
@total_orderingFills in >, <=, >= automatically if you supply __eq__ and one ordering method.
__add__Lets you use + to add fractions.
__str__Defines how print(fraction) looks ("1/2").

Idea: Overloading dunder (double-underscore) methods lets your objects behave like numbers, strings, etc.-making code feel natural.


6. Iterators and Iterables - custom "for-loopable" objects

To make an object work in a for loop:

  1. Iterable (NumberSequence): must define __iter__, returning an iterator.

  2. Iterator (NumberIterator): must define

    • __iter__ returning self, and
    • __next__ returning the next value or raising StopIteration at the end.

Result:

for n in NumberSequence(1, 3):
    print(n)   # 1  2  3

Why it's useful: You control how values are produced-great for infinite streams, filtered views, reading files line-by-line, etc.


7. Access Modifiers & Encapsulation - protecting your data

PrefixTypical meaning (by convention)Example
nonePublic - anyone can use it.account_number
_singleProtected - "internal use," but not enforced._balance
__doublePrivate - name mangled (_BankAccount__transactions) to discourage direct access.__transactions

Encapsulation hides internal details so the rest of your program can't accidentally break invariants. Interact through methods instead (deposit, get_balance).


8. Quick Recap

  1. Classes bundle data + behavior.
  2. Containment ⇒ "has-a" relationships inside objects.
  3. Inheritance ⇒ "is-a" hierarchy + code reuse.
  4. Abstract classes set rules future classes must follow.
  5. Operator overloading makes custom objects feel like built-ins.
  6. Iterators let your objects power for loops.
  7. Encapsulation shields internal state; use public methods instead of direct access.
Back to Data Structures and Object-Oriented Programming