← View series: python tutorials
~/blog
OOP - Advanced Concepts
Introduction
In this tutorial, you'll learn advanced OOP concepts in Python: encapsulation, dunder methods (special methods), property decorators, and class methods. These concepts will help you write more robust and professional Python code.
What You'll Learn
- Encapsulation (public, protected, private)
- Property decorators
- Dunder methods (special methods)
- Class methods and static methods
- Operator overloading
Encapsulation
Encapsulation restricts access to certain attributes and methods. In Python, this is achieved using naming conventions:
Access Modifiers
| Type | Convention | Description |
|---|---|---|
| Public | name | Accessible anywhere |
| Protected | _name | Conventionally internal (by convention) |
| Private | __name | Name mangling (harder to access) |
Public (Default)
class Person:
def __init__(self, name):
self.name = name # Public - accessible anywhere
person = Person("Alice")
print(person.name) # Works fineProtected (Single Underscore)
class Person:
def __init__(self, name):
self._name = name # Protected - internal use
def _internal_method(self): # Internal method
return "Internal"
person = Person("Alice")
print(person._name) # Works but "private" by conventionPrivate (Double Underscore)
class Person:
def __init__(self, name, ssn):
self.name = name
self.__ssn = ssn # Private - name mangled
def get_ssn(self):
return self.__ssn # Access through method
person = Person("Alice", "123-45-6789")
print(person.name) # Alice
# print(person.__ssn) # Error! Can't access directly
print(person.get_ssn()) # 123-45-6789
# Name mangling in action
print(person._Person__ssn) # 123-45-6789 (still accessible!)Note: Python doesn't truly enforce privacy. The underscore conventions signal "don't touch this" to other developers.
Property Decorators
Properties allow you to control access to attributes:
Getter and Setter
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
# Getter
@property
def celsius(self):
return self._celsius
# Setter
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero!")
self._celsius = value
# Convert to Fahrenheit (read-only property)
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.celsius = 30 # Use setter
print(temp.celsius) # 30
# temp.celsius = -300 # Raises ValueErrorRead-Only Property
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def diameter(self):
return self.radius * 2
@property
def area(self):
return 3.14159 * self.radius ** 2
@property
def circumference(self):
return 2 * 3.14159 * self.radius
circle = Circle(5)
print(circle.radius) # 5
print(circle.diameter) # 10
print(circle.area) # 78.53975
print(circle.circumference) # 31.4159Dunder Methods (Special Methods)
Dunder (double underscore) methods are special methods that start and end with __. They let you customize object behavior.
str and repr
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# Human-readable string
def __str__(self):
return f"Person: {self.name}, {self.age}"
# Developer string (unambiguous)
def __repr__(self):
return f"Person(name='{self.name}', age={self.age})"
person = Person("Alice", 25)
print(str(person)) # Person: Alice, 25
print(repr(person)) # Person(name='Alice', age=25)eq, lt, le, etc.
class Movie:
def __init__(self, title, rating):
self.title = title
self.rating = rating
def __eq__(self, other):
return self.rating == other.rating
def __lt__(self, other):
return self.rating < other.rating
def __le__(self, other):
return self.rating <= other.rating
def __str__(self):
return f"{self.title} ({self.rating})"
m1 = Movie("Movie A", 8.5)
m2 = Movie("Movie B", 9.0)
m3 = Movie("Movie C", 8.5)
print(m1 == m3) # True (same rating)
print(m1 < m2) # True (8.5 < 9.0)
print(m1 <= m3) # True (8.5 <= 8.5)add, sub, etc.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 5)
print(v1 + v2) # (3, 8)
print(v1 - v2) # (1, -2)
print(v1 * 3) # (6, 9)len, contains
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
def __contains__(self, member):
return member in self.members
def __iter__(self):
return iter(self.members)
team = Team(["Alice", "Bob", "Charlie"])
print(len(team)) # 3
print("Alice" in team) # True
print("Dave" in team) # False
for member in team:
print(member)call
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
counter = Counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3Class Methods and Static Methods
Instance Methods
class MyClass:
def instance_method(self):
return "Instance method", selfClass Methods
class MyClass:
class_variable = 0
@classmethod
def class_method(cls):
return f"Class method, class: {cls.class_variable}"
@classmethod
def create_with_value(cls, value):
# Can create new instances
obj = cls()
obj.class_variable = value
return obj
print(MyClass.class_method()) # Class method, class: 0
obj = MyClass.create_with_value(100)
print(obj.class_variable) # 100Static Methods
class MyClass:
@staticmethod
def static_method(x, y):
return x + y
# No need to instantiate
print(MyClass.static_method(3, 5)) # 8When to Use What
| Method Type | Use When |
|---|---|
| Instance | Need access to instance (self) |
| Class | Need access to class (cls) or create instances |
| Static | Utility function that doesn't need class/instance |
Complete Example: Bank Account
class BankAccount:
# Class variable for interest rate
interest_rate = 0.02
def __init__(self, account_number, balance=0):
self.__account_number = account_number
self.__balance = balance
self.__transactions = []
# Properties
@property
def balance(self):
return self.__balance
@property
def account_number(self):
return self.__account_number
# String representations
def __str__(self):
return f"Account {self.__account_number}: ${self.__balance:.2f}"
def __repr__(self):
return f"BankAccount('{self.__account_number}', {self.__balance})"
# Comparison
def __eq__(self, other):
return self.__account_number == other.__account_number
def __lt__(self, other):
return self.__balance < other.__balance
# Core methods
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
self.__transactions.append(f"Deposited: ${amount}")
return self.__balance
def withdraw(self, amount):
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
self.__transactions.append(f"Withdrew: ${amount}")
return self.__balance
# Class method
@classmethod
def create_premium(cls, account_number):
return cls(account_number, 1000) # Start with $1000
@classmethod
def set_interest_rate(cls, rate):
cls.interest_rate = rate
# Static method
@staticmethod
def validate_account_number(number):
return len(str(number)) == 10
# Test
account1 = BankAccount("1234567890", 500)
account2 = BankAccount("0987654321", 1000)
print(account1) # Account 1234567890: $500.00
print(account1 < account2) # True
account1.deposit(200)
account1.withdraw(100)
print(account1.balance) # 600
# Using class method
premium = BankAccount.create_premium("1111111111")
print(premium.balance) # 1000Summary
In this tutorial, you learned:
- ✅ Encapsulation (public, protected, private)
- ✅ Property decorators (getters, setters, read-only)
- ✅ Dunder methods (str, repr, eq, add, etc.)
- ✅ Class methods and static methods
- ✅ Operator overloading
🧑💻 Practice Exercise
Create a Rectangle class that:
- Uses private attributes for width and height
- Has properties for getting/setting width and height
- Has a read-only property for area
- Implements str, repr
- Implements add to combine two rectangles (areas add)
- Implements eq to check if two rectangles have same area
Click to see solution
pythonclass Rectangle:
"""A class to represent a rectangle"""
def __init__(self, width, height):
self.__width = width # Private attribute
self.__height = height # Private attribute
# Property for width
@property
def width(self):
return self.__width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self.__width = value
# Property for height
@property
def height(self):
return self.__height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be positive")
self.__height = value
# Read-only property for area
@property
def area(self):
return self.__width * self.__height
# String representations
def __str__(self):
return f"Rectangle({self.__width}x{self.__height})"
def __repr__(self):
return f"Rectangle(width={self.__width}, height={self.__height})"
# Add two rectangles (areas add)
def __add__(self, other):
new_area = self.area + other.area
# Create new rectangle with same ratio
return Rectangle(self.__width + other.__width, self.__height + other.__height)
# Check equality (same area)
def __eq__(self, other):
return self.area == other.area
def __lt__(self, other):
return self.area < other.area
# Test the Rectangle class
print("=== Creating Rectangles ===")
r1 = Rectangle(5, 3)
r2 = Rectangle(4, 4)
print(f"r1: {r1}") # Rectangle(5x3)
print(f"r2: {r2}") # Rectangle(4x4)
print(f"r1.area: {r1.area}") # 15
print(f"r2.area: {r2.area}") # 16
print("\n=== Property Tests ===")
r1.width = 6
print(f"r1 after width change: {r1}") # Rectangle(6x3)
print(f"r1.area: {r1.area}") # 18
print("\n=== Comparison ===")
print(f"r1 == r2: {r1 == r2}") # False (18 != 16)
print(f"r1 < r2: {r1 < r2}") # False
print("\n=== Addition ===")
r3 = r1 + r2
print(f"r1 + r2: {r3}") # Rectangle(10x7)
print(f"r3.area: {r3.area}") # 70 (18 + 52)
print("\n=== Repr ===")
print(repr(r1))
Output:
=== Creating Rectangles ===
r1: Rectangle(5x3)
r2: Rectangle(4x4)
r1.area: 15
r2.area: 16
=== Property Tests ===
r1 after width change: Rectangle(6x3)
r1.area: 18
=== Comparison ===
r1 == r2: False
r1 < r2: False
=== Addition ===
r1 + r2: Rectangle(10x7)
r3.area: 70
=== Repr ===
Rectangle(width=6, height=3)
class Rectangle:
"""A class to represent a rectangle"""
def __init__(self, width, height):
self.__width = width # Private attribute
self.__height = height # Private attribute
# Property for width
@property
def width(self):
return self.__width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self.__width = value
# Property for height
@property
def height(self):
return self.__height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be positive")
self.__height = value
# Read-only property for area
@property
def area(self):
return self.__width * self.__height
# String representations
def __str__(self):
return f"Rectangle({self.__width}x{self.__height})"
def __repr__(self):
return f"Rectangle(width={self.__width}, height={self.__height})"
# Add two rectangles (areas add)
def __add__(self, other):
new_area = self.area + other.area
# Create new rectangle with same ratio
return Rectangle(self.__width + other.__width, self.__height + other.__height)
# Check equality (same area)
def __eq__(self, other):
return self.area == other.area
def __lt__(self, other):
return self.area < other.area
# Test the Rectangle class
print("=== Creating Rectangles ===")
r1 = Rectangle(5, 3)
r2 = Rectangle(4, 4)
print(f"r1: {r1}") # Rectangle(5x3)
print(f"r2: {r2}") # Rectangle(4x4)
print(f"r1.area: {r1.area}") # 15
print(f"r2.area: {r2.area}") # 16
print("\n=== Property Tests ===")
r1.width = 6
print(f"r1 after width change: {r1}") # Rectangle(6x3)
print(f"r1.area: {r1.area}") # 18
print("\n=== Comparison ===")
print(f"r1 == r2: {r1 == r2}") # False (18 != 16)
print(f"r1 < r2: {r1 < r2}") # False
print("\n=== Addition ===")
r3 = r1 + r2
print(f"r1 + r2: {r3}") # Rectangle(10x7)
print(f"r3.area: {r3.area}") # 70 (18 + 52)
print("\n=== Repr ===")
print(repr(r1))What's Next
In the next tutorial, we'll learn about Error Handling - how to deal with exceptions gracefully in Python.