OOP 101 for
ELE3795
Presented By
Vilijam Cekov
What is OOP?
A way to bundle related data and the
functions that use it.
Class = blueprint/recipe; Object =
one real thing made from it.
Result: code that’s easier to read,
change, and reuse.
Concepts we will
learn:
1.
2.
3.
4.
Classes & Objects
Attributes & Methods
Encapsulation
Inheritance
1. CLASSES & OBJECTS
The essentials: class, __init__, and self
• __init__ runs when you make an object and sets up its state.
• self means “this object right here.” Python passes it for you.
• We’ll define what the object has (attributes) and does (methods).
First class: Person (greet and birthday)
• class Person:
•
def __init__(self, name, age):
•
# attributes (data the object carries around)
•
self.name = name
•
self.age = age
•
•
•
# methods (actions the object can do)
def birthday(self):
self.age += 1
•
•
def greet(self):
print(f"Hi, I'm {self.name} and I'm {self.age}.")
•
•
•
•
p = Person("Ava", 19)
p.greet()
# Hi, I'm Ava and I'm 19.
p.birthday()
p.greet()
# Hi, I'm Ava and I'm 20.
2. ATTRIBUTES & METHODS
Attributes &
Methods
• Attributes = state (e.g., name, age, is_on).
• Methods = behaviors that read or change that state.
• Keeping them together in a class is the whole point.
• class Light:
•
def __init__(self):
•
self.is_on = False
Example:
Light
•
•
def switch_on(self):
self.is_on = True
•
•
def switch_off(self):
self.is_on = False
•
•
•
•
lamp = Light()
print(lamp.is_on)
lamp.switch_on()
print(lamp.is_on)
# False
# True
# attribute (data)
# method (action)
Printing more
pleasantly
• Printing an object can be readable if we add
__repr__ or __str__.
• __repr__ is great for debugging; it should show
key info.
• This helps a ton when you’re testing and logging.
Readable printing with __repr__
• class Point:
•
def __init__(self, x, y):
•
self.x, self.y = x, y
•
•
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
• p = Point(3, 4)
• print(p) # Point(x=3, y=4)
3. ENCAPSULATION
Encapsulation
• Keep internal details inside the class; give safe
methods to use it.
• Python uses a naming convention: _internal
means “please don’t touch.”
• Validate inputs at the boundary, so your object
stays in a good state.
• class BankAccount:
•
def __init__(self, owner, balance=0):
•
self.owner = owner
•
self._balance = balance
# underscore = internal detail
Example:
BankAccount
•
•
•
•
def deposit(self, amount):
if amount < 0:
raise ValueError("No negative deposits.")
self._balance += amount
•
•
•
•
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds.")
self._balance -= amount
•
•
def show_balance(self):
return self._balance
• acct = BankAccount("Sam", 100)
• acct.deposit(50)
• print(acct.show_balance()) # 150
Multiple objects &
references (watch
for aliases)
• Each object has its own state—changing one
doesn’t change another.
• But two names can point to the same object;
changes show up in both.
• If something changes “mysteriously,” check if it’s
the same object.
Same object
vs. copy
•
•
•
•
a = BankAccount("A", 50)
b = BankAccount("B", 50)
a.deposit(10)
print(a.show_balance(), b.show_balance())
# 60 50
• c = a
# c and a refer to the same object
• c.withdraw(10)
• print(a.show_balance()) # 50 (changed through c)
Composition
• A class can keep a list of other objects — very
common pattern.
• Great for representing “has-a” relationships
(Course has Students).
Composition:
Course with
Person
objects
• class Course:
•
def __init__(self, name):
•
self.name = name
•
self.students = [] # a list of Person objects
•
•
def add_student(self, person):
self.students.append(person)
•
•
def roster(self):
return [s.name for s in self.students]
•
•
•
•
c = Course("Intro Python")
c.add_student(Person("Ava", 20))
c.add_student(Person("Liam", 19))
print(c.roster()) # ['Ava', 'Liam']
First class: Person (greet and birthday)
• class Person:
•
def __init__(self, name, age):
•
# attributes (data the object carries around)
•
self.name = name
•
self.age = age
•
•
•
# methods (actions the object can do)
def birthday(self):
self.age += 1
•
•
def greet(self):
print(f"Hi, I'm {self.name} and I'm {self.age}.")
•
•
•
•
p = Person("Ava", 19)
p.greet()
# Hi, I'm Ava and I'm 19.
p.birthday()
p.greet()
# Hi, I'm Ava and I'm 20.
Class vs. instance
attributes, rule of
thumb
• Inside __init__: per-object data (each object has
its own copy).
• In the class body: shared by all objects (use for
constants).
Dog — class
and instance
attributes
• class Dog:
•
species = "Canis familiaris"
# class attribute (shared)
•
•
def __init__(self, name):
self.name = name
# instance attribute (per object)
•
•
•
•
d1 = Dog("Luna")
d2 = Dog("Koda")
print(d1.species, d2.species)
print(d1.name, d2.name)
# Canis familiaris Canis familiaris
# Luna Koda
Common beginner
mistakes and fixes
• Forgetting 'self' in methods → always put self
first.
• Modifying internals directly → use methods
instead.
• Accidentally sharing mutable class data → move
it into __init__.
• Printing looks cryptic → add a helpful __repr__.
Exercises
• A) Dog energy: play(+10), nap(+20), walk(-15, not below 0).
Add __repr__.
• B) Todo item: title + done flag; mark_done, rename, info()
'☐/ title'.
• C) Student gradebook: add 0–100 scores, average() or 0 if
none.
Exercise A
Solution
• class Dog:
•
def __init__(self, name, energy=50):
•
self.name = name
•
self.energy = energy
•
def play(self):
•
self.energy += 10
•
def nap(self):
•
self.energy += 20
•
def walk(self):
•
self.energy = max(0, self.energy - 15)
•
def __repr__(self):
•
return f"Dog(name={self.name}, energy={self.energy})"
• d = Dog("Milo")
• d.play(); d.walk(); print(d)
# Dog(name=Milo, energy=45)
Exercise B
Solution
• class Todo:
•
def __init__(self, title):
•
self.title = title
•
self.done = False
•
def mark_done(self):
•
self.done = True
•
def rename(self, new_title):
•
self.title = new_title
•
def info(self):
•
box = " " if self.done else "☐"
•
return f"{box} {self.title}"
• t = Todo("Buy milk")
• print(t.info()) # ☐ Buy milk
• t.mark_done()
• print(t.info()) #
Buy milk
• class Student:
•
def __init__(self, name):
•
self.name = name
•
self.scores = []
•
def add(self, score):
•
Exercise C
Solution
if 0 <= score <= 100:
•
•
self.scores.append(score)
else:
•
•
•
raise ValueError("Score out of range")
def average(self):
return sum(self.scores) / len(self.scores) if self.scores else 0.0
• s = Student("Jules")
• for n in (100, 80, 90):
•
s.add(n)
• print(s.average())
# 90.0
4. INHERITANCE
Inheritance
• Idea: a new class reuses and customizes another
class (is-a).
• Why: avoid repeating code; tweak behavior by
overriding a method.
• Tip: prefer composition unless 'is-a' really fits
your problem.
Basic
example:
Animal →
Dog/Cat
• class Animal:
•
def __init__(self, name):
•
self.name = name
•
def speak(self):
•
return "..."
• class Dog(Animal):
•
def speak(self):
•
return "woof"
# Dog is-an Animal
# override
• class Cat(Animal):
•
def __init__(self, name, lives=9):
•
super().__init__(name) # call parent setup
•
self.lives = lives
•
def speak(self):
•
return "meow"
• pets = [Dog("Luna"), Cat("Milo")]
• for p in pets:
•
print(p.name, p.speak())
# Luna woof / Milo meow
Inheritance vs.
composition
• Use inheritance: true 'is-a' (Dog is an Animal).
• Use composition: 'has-a' (Car has an Engine),
often simpler.
• Keep hierarchies shallow when you’re starting
out.
• class Engine:
•
def start(self):
•
print("vroom")
Contrast:
composition
• class Car:
•
def __init__(self):
•
self.engine = Engine()
•
def start(self):
•
self.engine.start()
• car = Car()
• car.start()
# vroom
# Car has-an Engine
Inheritance,
exercise D
• Make Vehicle(name), subclass Car that overrides
describe().
• Add ElectricCar with battery_level and its own
describe().
• Create one of each and print their describe()
results.
• class Vehicle:
•
def __init__(self, name):
•
self.name = name
•
def describe(self):
•
return f"Vehicle: {self.name}"
• class Car(Vehicle):
•
Exercise D
Solution
•
def describe(self):
return f"Car: {self.name}"
• class ElectricCar(Car):
•
def __init__(self, name, battery_level=100):
•
super().__init__(name)
•
self.battery_level = battery_level
•
•
def describe(self):
return f"ElectricCar: {self.name} ({self.battery_level}% battery)"
• fleet = [Vehicle("Thing"), Car("Sedan"), ElectricCar("Model Z", 85)]
• for v in fleet:
•
print(v.describe())
Class = definition, Object = one
instance of it.
__init__ & self set up and work with
each object.
Quick recap
Encapsulation keeps internals tidy;
composition builds bigger pieces.
Inheritance, think animals ->
cats/dogs