Weeks 7–10 — OOP Part 1: Classes & Instances · OOP Part 2: Inheritance & Dunder Methods · Regular Expressions · Intermediate Capstone: Text Analysis Tool
Model real-world entities with classes. Learn to define attributes in __init__, write instance methods, use properties for validation, and simplify data classes with @dataclass. Extend classes with inheritance, call parent methods with super(), implement polymorphism, and customize object behavior with dunder (magic) methods. Unlock powerful text-matching with regular expressions. Learn pattern syntax, groups, lookahead/lookbehind, and use re.findall, re.sub, and re.fullmatch for text extraction and cleaning. Combine the intermediate track skills to build a command-line text analysis tool that reads files, tokenizes text, computes statistics, and produces a formatted report.
What you'll learn
- 1Define a class with __init__ and instance attributes
- 2Write instance methods that operate on self
- 3Use @property for computed attributes and validation
- 4Distinguish class attributes from instance attributes
- 5Create simple data containers with @dataclass
- 6Create a child class that inherits from a parent
- 7Call parent __init__ with super()
- 8Override methods for polymorphism
- 9Define abstract base classes with ABC
- 10Implement __str__, __repr__, __eq__, __lt__, __len__, __add__
- 11Write regex patterns for common scenarios (email, phone, date)
- 12Use groups () and named groups (?P<name>...)
- 13Apply re.match, re.search, re.findall, re.sub
- 14Use regex flags: re.IGNORECASE, re.MULTILINE
- 15Understand greedy vs non-greedy quantifiers
- 16Integrate file I/O, regex, OOP, and collections in one project
- 17Design a class hierarchy for the analysis pipeline
- 18Handle command-line arguments with argparse
- 19Produce formatted tabular output
- 20Write unit tests for the tokenizer and statistics functions
Week 7 — OOP Part 1: Classes & Instances
Model real-world entities with classes. Learn to define attributes in __init__, write instance methods, use properties for validation, and simplify data classes with @dataclass.
1. Classes and Instances
class Dog:
species = "Canis lupus familiaris" # class attribute
def __init__(self, name, age):
self.name = name # instance attribute
self.age = age
def bark(self):
return f"{self.name} says: Woof!"
def __repr__(self):
return f"Dog(name={self.name!r}, age={self.age})"
fido = Dog("Fido", 3)
print(fido.bark())
print(repr(fido))
print(Dog.species)2. Properties
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # private by convention
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
acct = BankAccount("Alice", 1000)
acct.deposit(500)
acct.withdraw(200)
print(acct.balance) # 13003. @dataclass
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
def distance_to(self, other):
return ((self.x-other.x)**2 + (self.y-other.y)**2)**0.5
p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2)) # 5.0
print(p1) # Point(x=0, y=0)4. Common Mistakes
- Forgetting self as the first parameter of instance methods.
- Using a mutable default (list/dict) in __init__ signature — use field(default_factory=list) with dataclass.
- Confusing class attributes (shared by all instances) with instance attributes (unique per instance).
Week 8 — OOP Part 2: Inheritance & Dunder Methods
Extend classes with inheritance, call parent methods with super(), implement polymorphism, and customize object behavior with dunder (magic) methods.
1. Inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return f"{self.name}: Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name}: Meow!"
for a in [Dog("Fido"), Cat("Whiskers")]:
print(a.speak())2. Abstract Base Classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
def __str__(self):
return f"{self.__class__.__name__}: area={self.area():.2f}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
def perimeter(self): return 2 * 3.14159 * self.r
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def perimeter(self): return 2 * (self.w + self.h)
shapes = [Circle(5), Rectangle(3, 4)]
for s in shapes: print(s)3. Dunder Methods
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __abs__(self):
return (self.x**2 + self.y**2)**0.5
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(abs(v2)) # 5.04. Common Mistakes
- Forgetting to call super().__init__() in the child — parent attributes won't be initialized.
- Overriding __eq__ without __hash__ makes the object unhashable (can't be used in sets/dicts).
- Multiple inheritance with diamond problem — Python's MRO (C3 linearization) resolves this, but be cautious.
Week 9 — Regular Expressions
Unlock powerful text-matching with regular expressions. Learn pattern syntax, groups, lookahead/lookbehind, and use re.findall, re.sub, and re.fullmatch for text extraction and cleaning.
1. Pattern Basics
| Pattern | Matches |
|---|---|
| . | Any character (except newline) |
| \d | Digit [0-9] |
| \w | Word char [a-zA-Z0-9_] |
| \s | Whitespace |
| ^ | Start of string |
| $ | End of string |
| * | 0 or more |
| + | 1 or more |
| ? | 0 or 1 |
| {n,m} | n to m times |
| [abc] | Any of a, b, c |
| (abc) | Group |
2. re Functions
import re
text = "Call us at 010-1234-5678 or 02-987-6543"
# findall: return all matches
phones = re.findall(r"\d{2,3}-\d{3,4}-\d{4}", text)
print(phones) # ['010-1234-5678', '02-987-6543']
# sub: replace
masked = re.sub(r"\d{4}$", "****", phones[0])
print(masked) # 010-1234-****
# Named groups
m = re.search(r"(?P<area>\d{2,3})-(?P<number>\d+-\d+)", text)
print(m.group("area"), m.group("number"))3. Common Patterns
EMAIL = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
URL = r"https?://[\w./%-]+"
DATE = r"\d{4}-\d{2}-\d{2}"
IP_ADDR = r"\b(?:\d{1,3}\.){3}\d{1,3}\b"
for pattern, sample in [
(EMAIL, "user@example.com"),
(URL, "https://python.org/docs"),
(DATE, "2024-05-15"),
]:
m = re.fullmatch(pattern, sample)
print(f"{'OK' if m else 'FAIL'} — {sample}")4. Common Mistakes
- Forgetting to use raw strings (r'...') — backslash sequences get misinterpreted.
- Greedy quantifiers match as much as possible. Use *? or +? for non-greedy matching.
- re.match() only matches at the start; re.search() scans the whole string.
Week 10 — Intermediate Capstone: Text Analysis Tool
Combine the intermediate track skills to build a command-line text analysis tool that reads files, tokenizes text, computes statistics, and produces a formatted report.
1. Project Specification
python analyze.py [OPTIONS] FILE
Options:
--top N Show top N words (default 10)
--format FMT Output format: text|csv|json (default text)
--no-stop Exclude common stop words- Total words, unique words, average word length
- Top-N word frequency (with ASCII bar chart)
- Sentence count, average sentence length
- Most common bigrams
- Readability score (Flesch Reading Ease approximation)
2. Architecture
- Tokenizer class — regex-based word/sentence splitter
- TextStats dataclass — holds all computed metrics
- Analyzer class — orchestrates tokenization and metric computation
- Reporter class — formats and prints TextStats (text/csv/json)
- main() — argparse + Analyzer + Reporter pipeline
3. Sample Output
$ python analyze.py hamlet.txt --top 5
File: hamlet.txt (5447 words, 1234 unique)
Top 5 words:
the ████████████████ 250
and ██████████████ 215
to ████████████ 180
of ██████████ 150
a ████████ 130
Sentences: 423 (avg 12.9 words)
Readability: 68.4 (Standard)💻 Examples
Run these examples and check the output yourself.
class BankAccount:
def __init__(self, owner):
self.owner = owner
self._balance = 0.0
self._history = []
@property
def balance(self): return self._balance
def deposit(self, amount):
if amount <= 0: raise ValueError("Amount must be positive")
self._balance += amount
self._history.append(f"+{amount:.2f}")
def withdraw(self, amount):
if amount > self._balance: raise ValueError("Insufficient funds")
self._balance -= amount
self._history.append(f"-{amount:.2f}")
def statement(self):
print(f"Account: {self.owner}")
for tx in self._history:
print(f" {tx}")
print(f" Balance: {self._balance:.2f}")
acct = BankAccount("Alice")
acct.deposit(1000)
acct.deposit(500)
acct.withdraw(200)
acct.statement()
Account: Alice
+1000.00
+500.00
-200.00
Balance: 1300.00import math
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): ...
@abstractmethod
def perimeter(self): ...
def __str__(self):
return f"{type(self).__name__}(area={self.area():.2f}, perim={self.perimeter():.2f})"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r**2
def perimeter(self): return 2 * math.pi * self.r
class Rect(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def perimeter(self): return 2*(self.w+self.h)
for s in [Circle(5), Rect(3, 4), Circle(1)]:
print(s)
Circle(area=78.54, perim=31.42)
Rect(area=12.00, perim=14.00)
Circle(area=3.14, perim=6.28)import re
LOG = '127.0.0.1 - frank [10/Oct/2024:13:55:36 -0700] "GET /index.html HTTP/1.1" 200 2326'
pattern = re.compile(
r'(?P<ip>[\d.]+) \S+ (?P<user>\S+) '
r'\[(?P<time>[^\]]+)\] '
r'"(?P<method>\S+) (?P<path>\S+) [^"]+"'
r' (?P<status>\d{3}) (?P<size>\d+)'
)
m = pattern.match(LOG)
if m:
for key, val in m.groupdict().items():
print(f" {key:<8}: {val}")
ip : 127.0.0.1
user : frank
time : 10/Oct/2024:13:55:36 -0700
method : GET
path : /index.html
status : 200
size : 2326📝 Exercises
Try them yourself first, then open the solution to compare.
Student class
Goal: Model a student with name, scores list, and a computed grade property.
- __init__(name)
- add_score(score) method
- average property (float)
- grade property: A/B/C/D/F based on average
- __repr__ returning Student(name, avg=XX.X, grade=X)
▶Toggle solution
class Student:
def __init__(self, name):
self.name = name
self._scores = []
def add_score(self, score):
self._scores.append(score)
@property
def average(self):
return sum(self._scores) / len(self._scores) if self._scores else 0.0
@property
def grade(self):
a = self.average
if a >= 90: return "A"
if a >= 80: return "B"
if a >= 70: return "C"
if a >= 60: return "D"
return "F"
def __repr__(self):
return f"Student({self.name!r}, avg={self.average:.1f}, grade={self.grade})"
s = Student("Alice")
for sc in [90, 85, 92]: s.add_score(sc)
print(s)
Student('Alice', avg=89.0, grade=BFraction class
Goal: Implement a Fraction class with arithmetic operators.
- __init__(numerator, denominator)
- Reduce to lowest terms using gcd
- __add__, __sub__, __mul__, __truediv__
- __str__ → '3/4', __repr__ → Fraction(3, 4)
- __eq__ for comparison
▶Toggle solution
from math import gcd
class Fraction:
def __init__(self, n, d):
if d == 0: raise ZeroDivisionError
g = gcd(abs(n), abs(d))
self.n, self.d = n//g * (1 if d>0 else -1), abs(d)//g
def __repr__(self): return f"Fraction({self.n}, {self.d})"
def __str__(self): return f"{self.n}/{self.d}"
def __add__(self, o): return Fraction(self.n*o.d + o.n*self.d, self.d*o.d)
def __sub__(self, o): return Fraction(self.n*o.d - o.n*self.d, self.d*o.d)
def __mul__(self, o): return Fraction(self.n*o.n, self.d*o.d)
def __truediv__(self, o): return Fraction(self.n*o.d, self.d*o.n)
def __eq__(self, o): return self.n == o.n and self.d == o.d
a, b = Fraction(1,2), Fraction(1,3)
print(a+b, a-b, a*b, a/b)
5/6 1/6 1/6 3/2Email Extractor & Validator
Goal: Extract all email addresses from a block of text and validate each one.
- Use re.findall with EMAIL pattern
- Validate: local part ≤ 64 chars, domain ≤ 255 chars
- Print valid/invalid for each found address
▶Toggle solution
import re
EMAIL = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
text = "Contact admin@example.com or info@co.uk. Invalid: @bad, x@"
emails = re.findall(EMAIL, text)
for e in emails:
local, domain = e.split("@", 1)
if len(local) <= 64 and len(domain) <= 255:
print(f" VALID: {e}")
else:
print(f" INVALID: {e}")
VALID: admin@example.com
VALID: info@co.ukBuild the analyzer
Goal: Implement the text analysis tool with all features above.
- All metrics computed correctly
- argparse with all flags
- text/csv/json output formats
- Unit tests for Tokenizer
- · Metrics correct — 40%
- · argparse flags work — 20%
- · Output formats — 20%
- · Tests pass — 20%
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy