Classes and Objects
Object-Oriented Programming (OOP) is a programming paradigm that organizes code into objects, which are instances of classes. Python is an object-oriented language that supports classes and objects, allowing you to model real-world entities and their behaviors.
What is a Class?
A class is a blueprint or template for creating objects. It defines the attributes (data) and methods (functions) that the objects created from it will have.
What is an Object?
An object is an instance of a class. It’s a concrete realization of the class blueprint, with its own set of attribute values.
Think of a class as a cookie cutter and objects as the cookies made from it. The cookie cutter (class) defines the shape, while each cookie (object) can have different decorations (attribute values).
Defining a Class
To define a class in Python, use the class keyword followed by the class name:
class Person: pass # Empty classNote
By convention, class names in Python use PascalCase (each word starts with a capital letter), e.g.,
Person,BankAccount,StudentRecord.
Creating Objects
To create an object (instance) from a class, call the class name like a function:
class Person: pass
# Create objects (instances)person1 = Person()person2 = Person()
print(type(person1)) # <class '__main__.Person'>print(person1) # <__main__.Person object at 0x...>Instance Variables
Instance variables are attributes that belong to a specific instance (object). Each object has its own copy of instance variables.
You can add instance variables to an object by simply assigning values to them:
class Person: pass
person1 = Person()person1.name = "Alice"person1.age = 25person1.phone_number = "1234567890"
person2 = Person()person2.name = "Bob"person2.age = 30
print(person1.name, person1.age, person1.phone_number) # Alice 25print(person2.name, person2.age) # Bob 30# If we try to print person2.phone_number, it will raise an AttributeErrorThe __init__ Method (Constructor)
The __init__ method is a special method called a constructor. It’s automatically called when an object is created. Use it to initialize instance variables.
class Person: def __init__(self, name, age): self.name = name self.age = age
person1 = Person("Alice", 25)person2 = Person("Bob", 30)
print(person1.name, person1.age) # Alice 25print(person2.name, person2.age) # Bob 30Understanding self
The self parameter refers to the instance of the class. It’s automatically passed when you call a method on an object. You must include self as the first parameter of instance methods.
class Person: def __init__(self, name): self.name = name # self refers to the current instance
def introduce(self): print(f"My name is {self.name}")
person = Person("Alice")person.introduce() # My name is Alice# When calling person.introduce(), Python automatically passes person as selfInstance Methods
Instance methods are functions defined inside a class that operate on instance variables. They must have self as their first parameter.
class Rectangle: def __init__(self, length, width): self.length = length self.width = width
def area(self): return self.length * self.width
def perimeter(self): return 2 * (self.length + self.width)
def display(self): print(f"Rectangle: {self.length}x{self.width}") print(f"Area: {self.area()}") print(f"Perimeter: {self.perimeter()}")
rect = Rectangle(5, 3)rect.display()# Rectangle: 5x3# Area: 15# Perimeter: 16Public, Protected, and Private Attributes
Python uses naming conventions to indicate the intended visibility of attributes and methods. Unlike some languages, Python doesn’t enforce strict access control, but these conventions help communicate intent.
Public Attributes
Public attributes are accessible from anywhere. By default, all attributes in Python are public (no special prefix):
class Person: def __init__(self, name, age): self.name = name # Public attribute self.age = age # Public attribute
def display(self): # Public method print(f"{self.name} is {self.age} years old")
person = Person("Alice", 25)print(person.name) # Alice (accessible)print(person.age) # 25 (accessible)person.display() # Alice is 25 years old (accessible)Protected Attributes
Protected attributes are indicated by a single underscore prefix _. This is a convention that signals “this is for internal use” but Python doesn’t prevent access:
class BankAccount: def __init__(self, balance): self._balance = balance # Protected attribute (convention)
def get_balance(self): return self._balance
def _validate_amount(self, amount): # Protected method return amount > 0
account = BankAccount(1000)print(account.get_balance()) # 1000 (using public method)print(account._balance) # 1000 (accessible but not recommended)print(account._validate_amount(50)) # True (accessible but not recommended)Note
Protected attributes can still be accessed from outside the class, but the underscore prefix signals to other developers that these are intended for internal use and shouldn’t be accessed directly.
Private Attributes
Private attributes use a double underscore prefix __. Python performs name mangling on these attributes, making them harder (but not impossible) to access from outside the class:
class BankAccount: def __init__(self, balance, account_number): self.balance = balance # Public self._account_number = account_number # Protected self.__pin = "1234" # Private (name mangling)
def get_pin(self): return self.__pin
def __validate_pin(self, pin): # Private method return self.__pin == pin
account = BankAccount(1000, "ACC001")print(account.balance) # 1000 (public - accessible)print(account._account_number) # ACC001 (protected - accessible but not recommended)print(account.get_pin()) # 1234 (using public method)# print(account.__pin) # AttributeError: 'BankAccount' object has no attribute '__pin'# print(account.__validate_pin("1234")) # AttributeError
# Name mangling: Python changes __pin to _BankAccount__pinprint(account._BankAccount__pin) # 1234 (not recommended, but possible)How Name Mangling Works
When you use __attribute_name, Python automatically renames it to _ClassName__attribute_name:
class MyClass: def __init__(self): self.public = "public" self._protected = "protected" self.__private = "private"
obj = MyClass()print(obj.public) # publicprint(obj._protected) # protected# print(obj.__private) # AttributeError
# Name mangling in actionprint(obj._MyClass__private) # private (mangled name)When to Use Each
- Public (
no prefix): Use for attributes and methods that are part of the class’s public API - Protected (
_single_underscore): Use for internal attributes/methods that subclasses might need - Private (
__double_underscore): Use for attributes/methods that should not be accessed from outside, even by subclasses
class Employee: def __init__(self, name, salary): self.name = name # Public - part of API self._department = "Engineering" # Protected - internal use self.__salary = salary # Private - sensitive data
def get_salary(self): # Public method to access private data return self.__salary
def _calculate_bonus(self): # Protected - internal calculation return self.__salary * 0.1
def get_total_compensation(self): # Public method return self.__salary + self._calculate_bonus()
emp = Employee("Alice", 50000)print(emp.name) # Alice (public)print(emp._department) # Engineering (protected - accessible)print(emp.get_salary()) # 50000 (using public method)print(emp.get_total_compensation()) # 55000.0# print(emp.__salary) # AttributeErrorClass Variables
Class variables are shared by all instances of a class. They are defined at the class level, outside of any method.
class Dog: species = "Canis familiaris" # Class variable (shared by all instances)
def __init__(self, name, breed): self.name = name # Instance variable self.breed = breed # Instance variable
dog1 = Dog("Buddy", "Golden Retriever")dog2 = Dog("Max", "German Shepherd")
print(dog1.species) # Canis familiarisprint(dog2.species) # Canis familiarisprint(Dog.species) # Canis familiaris (can access via class name)
# Modifying class variable affects all instancesDog.species = "Canis lupus"print(dog1.species) # Canis lupusprint(dog2.species) # Canis lupusClass Methods
Class methods are methods that are bound to the class rather than an instance. They can access and modify class variables. Use the @classmethod decorator and cls as the first parameter.
class Student: total_students = 0 # Class variable
def __init__(self, name): self.name = name Student.total_students += 1
@classmethod def get_total_students(cls): return cls.total_students
@classmethod def reset_count(cls): cls.total_students = 0
student1 = Student("Alice")student2 = Student("Bob")student3 = Student("Charlie")
print(Student.get_total_students()) # 3Student.reset_count()print(Student.get_total_students()) # 0Static Methods
Static methods are methods that don’t access instance or class data. They are utility functions related to the class but don’t need access to instance or class variables. Use the @staticmethod decorator.
class MathUtils: @staticmethod def add(a, b): return a + b
@staticmethod def multiply(a, b): return a * b
# Can be called on the class or instanceresult1 = MathUtils.add(5, 3) # 8result2 = MathUtils.multiply(4, 7) # 28
obj = MathUtils()result3 = obj.add(10, 20) # 30 (also works on instance)The __del__ Method (Destructor)
The __del__ method is called when an object is about to be destroyed. It’s rarely used in Python due to automatic garbage collection.
class Person: def __init__(self, name): self.name = name print(f"{self.name} created")
def __del__(self): print(f"{self.name} destroyed")
person1 = Person("Alice")del person1 # Alice destroyedNote
The
__del__method is not guaranteed to be called immediately whendelis used, as Python uses garbage collection. It’s called when the object is actually destroyed by the garbage collector.
Special Methods (Magic Methods / Dunder Methods)
Python provides special methods (also called magic methods or dunder methods) that allow you to define how objects behave with built-in operations. They are surrounded by double underscores.
__str__ and __repr__
__str__: Returns a human-readable string representation (for end users)__repr__: Returns an unambiguous string representation (for developers)
class Person: def __init__(self, name, age): self.name = name self.age = age
def __str__(self): return f"{self.name}, {self.age} years old"
def __repr__(self): return f"Person('{self.name}', {self.age})"
person = Person("Alice", 25)print(str(person)) # Alice, 25 years oldprint(repr(person)) # Person('Alice', 25)print(person) # Alice, 25 years old (calls __str__)__len__
Defines the behavior of len() function:
class ShoppingCart: def __init__(self): self.items = []
def add_item(self, item): self.items.append(item)
def __len__(self): return len(self.items)
cart = ShoppingCart()cart.add_item("Apple")cart.add_item("Banana")print(len(cart)) # 2__eq__ and __ne__
Define equality and inequality comparison:
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): if isinstance(other, Point): return self.x == other.x and self.y == other.y return False
def __ne__(self, other): return not self.__eq__(other)
p1 = Point(1, 2)p2 = Point(1, 2)p3 = Point(3, 4)
print(p1 == p2) # Trueprint(p1 == p3) # Falseprint(p1 != p3) # True__lt__, __le__, __gt__, __ge__
Define comparison operators (<, <=, >, >=):
class Student: def __init__(self, name, grade): self.name = name self.grade = grade
def __lt__(self, other): return self.grade < other.grade
def __le__(self, other): return self.grade <= other.grade
def __gt__(self, other): return self.grade > other.grade
def __ge__(self, other): return self.grade >= other.grade
student1 = Student("Alice", 85)student2 = Student("Bob", 90)student3 = Student("Charlie", 85)
print(student1 < student2) # Trueprint(student1 > student2) # Falseprint(student1 <= student3) # True__add__, __sub__, __mul__, etc.
Define arithmetic operations:
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"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)v2 = Vector(1, 4)v3 = v1 + v2v4 = v1 * 3
print(v3) # Vector(3, 7)print(v4) # Vector(6, 9)__getitem__ and __setitem__
Enable indexing and slicing:
class MyList: def __init__(self, items): self.items = list(items)
def __getitem__(self, index): return self.items[index]
def __setitem__(self, index, value): self.items[index] = value
def __len__(self): return len(self.items)
my_list = MyList([1, 2, 3, 4, 5])print(my_list[0]) # 1print(my_list[1:4]) # [2, 3, 4]my_list[2] = 10print(my_list[2]) # 10Properties
Properties allow you to define methods that can be accessed like attributes, providing a way to add getters, setters, and deleters.
class Temperature: def __init__(self, celsius): self._celsius = celsius
@property def celsius(self): return self._celsius
@celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Temperature cannot be below absolute zero") self._celsius = value
@property def fahrenheit(self): return (self._celsius * 9/5) + 32
@fahrenheit.setter def fahrenheit(self, value): self._celsius = (value - 32) * 5/9
temp = Temperature(25)print(temp.celsius) # 25print(temp.fahrenheit) # 77.0
temp.fahrenheit = 100print(temp.celsius) # 37.777...Complete Example
Here’s a complete example combining multiple concepts:
class BankAccount: # Class variable account_count = 0
def __init__(self, owner, initial_balance=0): self.owner = owner self._balance = initial_balance # Private by convention BankAccount.account_count += 1 self.account_number = BankAccount.account_count
def deposit(self, amount): if amount > 0: self._balance += amount return True return False
def withdraw(self, amount): if 0 < amount <= self._balance: self._balance -= amount return True return False
@property def balance(self): return self._balance
@classmethod def get_account_count(cls): return cls.account_count
@staticmethod def validate_amount(amount): return isinstance(amount, (int, float)) and amount > 0
def __str__(self): return f"Account {self.account_number}: {self.owner}, Balance: ${self._balance:.2f}"
def __repr__(self): return f"BankAccount('{self.owner}', {self._balance})"
account1 = BankAccount("Alice", 1000)account2 = BankAccount("Bob", 500)
print(account1) # Account 1: Alice, Balance: $1000.00account1.deposit(200)account1.withdraw(150)print(account1.balance) # 1050.0print(BankAccount.get_account_count()) # 2Exercises
Exercise 1: Basic Class and Object
Create a class called Car with instance variables brand and model. Create an object and set these values, then print them.
Basic Class and Object
class Car: def __init__(self, brand, model): self.brand = brand self.model = model
brand = input()model = input()car = Car(brand, model)print(f"Brand: {car.brand}, Model: {car.model}")Exercise 2: Instance Methods
Create a class called Circle with a constructor that takes radius. Add methods area() and circumference() that return the area and circumference of the circle. Read the radius from input and display both values.
Instance Methods
import math
class Circle: def __init__(self, radius): self.radius = radius
def area(self): return math.pi * self.radius ** 2
def circumference(self): return 2 * math.pi * self.radius
radius = float(input())circle = Circle(radius)print(f"Area: {circle.area():.2f}")print(f"Circumference: {circle.circumference():.2f}")Exercise 3: Class Variables and Methods
Create a class called Book with a class variable total_books = 0. Increment this counter in the constructor. Add a class method get_total() that returns the total number of books created. Create 3 books and print the total.
Class Variables and Methods
class Book: total_books = 0
def __init__(self, title): self.title = title Book.total_books += 1
@classmethod def get_total(cls): return cls.total_books
book1 = Book("Python Basics")book2 = Book("Advanced Python")book3 = Book("Python Mastery")
print(f"Total books: {Book.get_total()}")Exercise 4: Special Methods
Create a class called Fraction with numerator and denominator. Implement __str__ to display as “numerator/denominator” and __eq__ to compare two fractions. Read two fractions and check if they are equal.
Special Methods
class Fraction: def __init__(self, numerator, denominator): self.numerator = numerator self.denominator = denominator
def __str__(self): return f"{self.numerator}/{self.denominator}"
def __eq__(self, other): if isinstance(other, Fraction): return (self.numerator * other.denominator == self.denominator * other.numerator) return False
num1 = int(input())den1 = int(input())num2 = int(input())den2 = int(input())
f1 = Fraction(num1, den1)f2 = Fraction(num2, den2)
print(f1)print(f2)if f1 == f2: print("Fractions are equal")else: print("Fractions are not equal")Exercise 5: Properties
Create a class called Rectangle with private _width and _height. Use properties to get and set these values, ensuring they are always positive. Read width and height, create a rectangle, then read new values and update them.
Properties
class Rectangle: def __init__(self, width, height): self._width = width self._height = height
@property def width(self): return self._width
@width.setter def width(self, value): if value > 0: self._width = value
@property def height(self): return self._height
@height.setter def height(self, value): if value > 0: self._height = value
w1 = float(input())h1 = float(input())w2 = float(input())h2 = float(input())
rect = Rectangle(w1, h1)print(f"Width: {rect.width}, Height: {rect.height}")
rect.width = w2rect.height = h2print(f"Width: {rect.width}, Height: {rect.height}")