Classes and Objects in Python
Object-Oriented Programming (OOP) in Python revolves around the concept of objects and classes. It is a paradigm that allows you to organize your code into reusable components.
What are Classes and Objects?
- Class: A blueprint for creating objects. It defines a set of attributes (data) and methods (functions) that the objects created from the class will have.
- Object: An instance of a class. It is created using the class definition and contains the attributes and methods defined by the class.
Let's delve into the details with examples.
Defining a Class
To define a class in Python, use the class
keyword followed by the class name and a colon. By convention, class names are written in CamelCase.
class Dog:
pass # This is a placeholder for future code
Here, Dog
is a class with no attributes or methods. The pass
statement indicates that the class is currently empty.
Creating an Object
An object is created by calling the class as if it were a function.
my_dog = Dog()
Now, my_dog
is an instance (object) of the Dog
class. However, the Dog
class is still empty and does nothing yet.
Adding Attributes
Attributes are variables that belong to a class. They are defined within a method called __init__
, which is a special method called a constructor. This method is called automatically when a new instance of the class is created.
class Dog:
def __init__(self, name, age):
self.name = name # Attribute
self.age = age # Attribute
Here, name
and age
are attributes of the Dog
class. The __init__
method initializes these attributes when an object is created.
my_dog = Dog("Buddy", 3)
print(my_dog.name) # Output: Buddy
print(my_dog.age) # Output: 3
The self
parameter is a reference to the current instance of the class and is used to access variables that belong to the class.
Understanding self
The self
parameter in the __init__
method (and other methods) is a reference to the instance of the class being created. When you create an object, self
allows the object to refer to itself. For example:
self
class Dog:
def __init__(self, name, age):
self.name = name # self.name refers to the attribute of the instance
self.age = age # self.age refers to the attribute of the instance
def bark(self):
return self.name + " says woof!"
my_dog = Dog("Buddy", 3)
print(my_dog.bark()) # Output: Buddy says woof!
In this example, self.name
and self.age
are attributes of the instance my_dog
. When bark
is called, self.name
is used to refer to the name
attribute of my_dog
.
Adding Methods
Methods are functions that belong to a class. They are defined within the class and have at least one parameter, self
.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
return self.name + " says woof!"
my_dog = Dog("Buddy", 3)
print(my_dog.bark()) # Output: Buddy says woof!
In this example, bark
is a method of the Dog
class. Methods typically operate on data contained in the class.
Example: A Simple Class
Let's create a simple class for a car.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def description(self):
return str(self.year) + " " + self.make + " " + self.model
my_car = Car("Toyota", "Corolla", 2020)
print(my_car.description()) # Output: 2020 Toyota Corolla
In this example:
- The
Car
class has three attributes:make
,model
, andyear
. - The
description
method returns a string that describes the car.
Example: A More Complex Class
Let's create a class with multiple methods and attributes to handle a bank account.
class BankAccount:
def __init__(self, account_number, balance=0):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds"
self.balance -= amount
return self.balance
def get_balance(self):
return self.balance
# Create an account
my_account = BankAccount("12345678", 1000)
# Deposit money
print(my_account.deposit(500)) # Output: 1500
# Withdraw money
print(my_account.withdraw(200)) # Output: 1300
# Check balance
print(my_account.get_balance()) # Output: 1300
In this example:
- The
BankAccount
class has two attributes:account_number
andbalance
. - The
deposit
method increases the balance. - The
withdraw
method decreases the balance, checking for sufficient funds. - The
get_balance
method returns the current balance.
Practical Application
Imagine we want to create a class to represent a library book.
class Book:
def __init__(self, title, author, pages, available=True):
self.title = title
self.author = author
self.pages = pages
self.available = available
def check_out(self):
if self.available:
self.available = False
return "Book checked out"
return "Book is not available"
def return_book(self):
self.available = True
return "Book returned"
def book_info(self):
availability = "available" if self.available else "not available"
return "'" + self.title + "' by " + self.author + ", " + str(self.pages) + " pages - " + availability
# Create a book instance
book = Book("The Great Gatsby", "F. Scott Fitzgerald", 180)
# Check book information
print(book.book_info()) # Output: 'The Great Gatsby' by F. Scott Fitzgerald, 180 pages - available
# Check out the book
print(book.check_out()) # Output: Book checked out
print(book.book_info()) # Output: 'The Great Gatsby' by F. Scott Fitzgerald, 180 pages - not available
# Return the book
print(book.return_book()) # Output: Book returned
print(book.book_info()) # Output: 'The Great Gatsby' by F. Scott Fitzgerald, 180 pages - available
This example demonstrates how you can create a class to manage the state and behavior of a library book.
Attributes and Methods in Python
In Object-Oriented Programming (OOP), attributes and methods are essential components of a class. Attributes are the data stored in an object, while methods are the functions that define the behavior of an object. Let’s explore these concepts in detail with examples.
Attributes
Attributes are variables that belong to an object. There are two types of attributes:
- Instance Attributes: Belong to a particular instance of a class.
- Class Attributes: Belong to the class itself and are shared among all instances.
Instance Attributes
Instance attributes are defined in the __init__
method of the class. Each instance of the class has its own set of instance attributes.
class Dog:
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute
my_dog = Dog("Buddy", 3)
print(my_dog.name) # Output: Buddy
print(my_dog.age) # Output: 3
your_dog = Dog("Lucy", 5)
print(your_dog.name) # Output: Lucy
print(your_dog.age) # Output: 5
In this example:
name
andage
are instance attributes. EachDog
object has its ownname
andage
.my_dog
andyour_dog
are two different instances with their own attributes.
Class Attributes
Class attributes are defined outside of any method and are shared among all instances of the class.
class Dog:
species = "Canis lupus familiaris" # Class attribute
def __init__(self, name, age):
self.name = name
self.age = age
print(Dog.species) # Output: Canis lupus familiaris
my_dog = Dog("Buddy", 3)
print(my_dog.species) # Output: Canis lupus familiaris
your_dog = Dog("Lucy", 5)
print(your_dog.species) # Output: Canis lupus familiaris
In this example:
species
is a class attribute. It is shared by all instances of theDog
class.- Both
my_dog
andyour_dog
have access to thespecies
attribute.
Methods
Methods are functions defined within a class that describe the behaviors of an object. There are three types of methods:
- Instance Methods: Operate on an instance of the class.
- Class Methods: Operate on the class itself.
- Static Methods: Do not operate on an instance or class but are included in the class’s namespace.
Instance Methods
Instance methods are the most common type of methods. They operate on an instance of the class and can access and modify instance attributes.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
return self.name + " says woof!"
def birthday(self):
self.age += 1
return self.age
my_dog = Dog("Buddy", 3)
print(my_dog.bark()) # Output: Buddy says woof!
print(my_dog.birthday()) # Output: 4
print(my_dog.age) # Output: 4
In this example:
bark
andbirthday
are instance methods. They useself
to access and modify instance attributes.- The
birthday
method increments theage
attribute of themy_dog
instance.
Class Methods
Class methods are methods that operate on the class itself rather than on instances. They are defined using the @classmethod
decorator and take cls
as the first parameter.
class Dog:
species = "Canis lupus familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def common_species(cls):
return cls.species
print(Dog.common_species()) # Output: Canis lupus familiaris
In this example:
common_species
is a class method. It usescls
to refer to the class and access the class attributespecies
.
Static Methods
Static methods do not operate on an instance or class. They are defined using the @staticmethod
decorator and do not take self
or cls
as a parameter.
class Dog:
@staticmethod
def bark_sound():
return "Woof!"
print(Dog.bark_sound()) # Output: Woof!
my_dog = Dog()
print(my_dog.bark_sound()) # Output: Woof!
In this example:
bark_sound
is a static method. It does not useself
orcls
and can be called on the class or an instance.
Example: Combining Attributes and Methods
Let's combine these concepts to create a more complex class.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
self.current_page = 0
def read(self, pages):
if self.current_page + pages <= self.pages:
self.current_page += pages
return "You read " + str(pages) + " pages."
else:
return "Not enough pages left to read."
def book_info(self):
return "Title: " + self.title + ", Author: " + self.author + ", Pages: " + str(self.pages) + ", Current page: " + str(self.current_page)
my_book = Book("1984", "George Orwell", 328)
print(my_book.book_info()) # Output: Title: 1984, Author: George Orwell, Pages: 328, Current page: 0
print(my_book.read(50)) # Output: You read 50 pages.
print(my_book.book_info()) # Output: Title: 1984, Author: George Orwell, Pages: 328, Current page: 50
In this example:
- The
Book
class has attributes:title
,author
,pages
, andcurrent_page
. - The
read
method updatescurrent_page
and provides feedback on reading. - The
book_info
method returns the book's information as a string.
The __init__
Method in Python
The __init__
method, also known as the constructor, is a special method in Python classes. It is automatically called when a new instance of the class is created, allowing you to initialize the object's attributes. The __init__
method can take additional arguments to initialize the object's state.
Defining the __init__
Method
The __init__
method is defined within a class and takes at least one parameter: self
. The self
parameter is a reference to the current instance of the class and is used to access the attributes and methods of the class.
__init__
Method
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
my_dog = Dog("Buddy", 3)
print(my_dog.name) # Output: Buddy
print(my_dog.age) # Output: 3
In this example:
- The
__init__
method initializes thename
andage
attributes of theDog
class. my_dog
is an instance of theDog
class with thename
"Buddy" andage
3.
More Detailed Explanation of self
The self
parameter in the __init__
method (and other methods) is a reference to the instance of the class being created. When you create an object, self
allows the object to refer to itself. For example:
self
class Dog:
def __init__(self, name, age):
self.name = name # self.name refers to the attribute of the instance
self.age = age # self.age refers to the attribute of the instance
def bark(self):
return self.name + " says woof!"
my_dog = Dog("Buddy", 3)
print(my_dog.bark()) # Output: Buddy says woof!
In this example:
self.name
andself.age
are attributes of the instancemy_dog
.- When
bark
is called,self.name
is used to refer to thename
attribute ofmy_dog
.
Example: Initializing Multiple Attributes
The __init__
method can initialize multiple attributes, allowing for more complex object creation.
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
def description(self):
return str(self.year) + " " + self.make + " " + self.model + " in " + self.color
my_car = Car("Toyota", "Corolla", 2020, "blue")
print(my_car.description()) # Output: 2020 Toyota Corolla in blue
In this example:
- The
__init__
method initializes themake
,model
,year
, andcolor
attributes of theCar
class. - The
description
method returns a string that describes the car.
Example: Using Default Values in __init__
You can provide default values for parameters in the __init__
method, making them optional when creating an object.
__init__
class Book:
def __init__(self, title, author, pages, available=True):
self.title = title
self.author = author
self.pages = pages
self.available = available
def book_info(self):
availability = "available" if self.available else "not available"
return "'" + self.title + "' by " + self.author + ", " + str(self.pages) + " pages - " + availability
book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 180)
book2 = Book("1984", "George Orwell", 328, available=False)
print(book1.book_info()) # Output: 'The Great Gatsby' by F. Scott Fitzgerald, 180 pages - available
print(book2.book_info()) # Output: '1984' by George Orwell, 328 pages - not available
In this example:
- The
available
parameter in the__init__
method has a default value ofTrue
. book1
is created with the defaultavailable
value, whilebook2
is created withavailable=False
.
Example: Validating Input in __init__
The __init__
method can include logic to validate the input parameters and ensure that the object's state is consistent.
__init__
class BankAccount:
def __init__(self, account_number, balance=0):
if balance < 0:
raise ValueError("Balance cannot be negative")
self.account_number = account_number
self.balance = balance
def get_balance(self):
return self.balance
# Create an account with a positive balance
account1 = BankAccount("12345678", 1000)
print(account1.get_balance()) # Output: 1000
# Attempt to create an account with a negative balance
try:
account2 = BankAccount("87654321", -500)
except ValueError as e:
print(e) # Output: Balance cannot be negative
In this example:
- The
__init__
method checks if thebalance
parameter is negative and raises aValueError
if it is. - This ensures that a
BankAccount
object cannot be created with an invalid balance.
Practical Application: A Library System
Let's create a more complex class to represent a library system.
class Library:
def __init__(self, name):
self.name = name
self.books = []
def add_book(self, book):
self.books.append(book)
def remove_book(self, book):
if book in self.books:
self.books.remove(book)
else:
print("Book not found in the library")
def list_books(self):
if not self.books:
return "No books in the library"
return "Books in " + self.name + ":\n" + "\n".join(self.books)
# Create a library instance
my_library = Library("City Library")
# Add books to the library
my_library.add_book("The Great Gatsby")
my_library.add_book("1984")
# List books in the library
print(my_library.list_books())
# Output:
# Books in City Library:
# The Great Gatsby
# 1984
# Remove a book from the library
my_library.remove_book("1984")
print(my_library.list_books())
# Output:
# Books in City Library:
# The Great Gatsby
In this example:
- The
Library
class has an__init__
method that initializes thename
andbooks
attributes. - The
add_book
,remove_book
, andlist_books
methods manage the books in the library.
Inheritance in Python
Inheritance is a fundamental concept in object-oriented programming that allows a class to inherit attributes and methods from another class. This enables code reuse and the creation of a hierarchical class structure.
Basic Concept of Inheritance
- Parent Class (Super Class or Base Class): The class whose attributes and methods are inherited.
- Child Class (Sub Class or Derived Class): The class that inherits from the parent class.
Creating a Parent Class
Let's start by creating a simple parent class called Animal
.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
def info(self):
return "I am an animal named " + self.name
In this example:
- The
Animal
class has an__init__
method that initializes thename
attribute. - The
speak
method is meant to be overridden by subclasses. It raises aNotImplementedError
to indicate that it should be implemented in the subclass. - The
info
method returns a string with the animal's name.
Creating a Child Class
A child class is created by specifying the parent class in parentheses after the child class name.
class Dog(Animal):
def speak(self):
return self.name + " says woof!"
# Create an instance of Dog
my_dog = Dog("Buddy")
print(my_dog.speak()) # Output: Buddy says woof!
print(my_dog.info()) # Output: I am an animal named Buddy
In this example:
- The
Dog
class inherits from theAnimal
class. - The
speak
method is overridden to provide a specific implementation for dogs. - The
info
method is inherited from theAnimal
class and can be used without modification.
Adding More Child Classes
You can create multiple child classes that inherit from the same parent class.
class Cat(Animal):
def speak(self):
return self.name + " says meow!"
class Bird(Animal):
def speak(self):
return self.name + " says tweet!"
# Create instances of Cat and Bird
my_cat = Cat("Whiskers")
my_bird = Bird("Tweety")
print(my_cat.speak()) # Output: Whiskers says meow!
print(my_cat.info()) # Output: I am an animal named Whiskers
print(my_bird.speak()) # Output: Tweety says tweet!
print(my_bird.info()) # Output: I am an animal named Tweety
In this example:
- The
Cat
andBird
classes inherit from theAnimal
class. - Each subclass provides its own implementation of the
speak
method.
Extending Functionality in Child Classes
Child classes can also have their own attributes and methods in addition to those inherited from the parent class.
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def speak(self):
return self.name + " says woof!"
def info(self):
return "I am a " + self.breed + " named " + self.name
# Create an instance of Dog
my_dog = Dog("Buddy", "Golden Retriever")
print(my_dog.speak()) # Output: Buddy says woof!
print(my_dog.info()) # Output: I am a Golden Retriever named Buddy
In this example:
- The
Dog
class adds a new attributebreed
. - The
__init__
method of theDog
class calls the__init__
method of theAnimal
class usingsuper()
. - The
info
method is overridden to include the breed information.
Practical Application: A Vehicle Hierarchy
Let's create a more complex example with a vehicle hierarchy.
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def info(self):
return str(self.year) + " " + self.make + " " + self.model
class Car(Vehicle):
def __init__(self, make, model, year, doors):
super().__init__(make, model, year)
self.doors = doors
def car_info(self):
return self.info() + " with " + str(self.doors) + " doors"
class Motorcycle(Vehicle):
def __init__(self, make, model, year, cc):
super().__init__(make, model, year)
self.cc = cc
def bike_info(self):
return self.info() + " with " + str(self.cc) + "cc engine"
# Create instances of Car and Motorcycle
my_car = Car("Toyota", "Corolla", 2020, 4)
my_motorcycle = Motorcycle("Yamaha", "MT-07", 2021, 689)
print(my_car.car_info()) # Output: 2020 Toyota Corolla with 4 doors
print(my_motorcycle.bike_info()) # Output: 2021 Yamaha MT-07 with 689cc engine
In this example:
- The
Vehicle
class is the parent class with attributesmake
,model
, andyear
, and a methodinfo
. - The
Car
andMotorcycle
classes inherit fromVehicle
and add their own specific attributes and methods.
Overriding Methods
When a method in a child class has the same name as a method in the parent class, the method in the child class overrides the method in the parent class.
class Animal:
def speak(self):
return "Some generic animal sound"
class Dog(Animal):
def speak(self):
return "Woof!"
my_animal = Animal()
my_dog = Dog()
print(my_animal.speak()) # Output: Some generic animal sound
print(my_dog.speak()) # Output: Woof!
In this example:
- The
speak
method in theDog
class overrides thespeak
method in theAnimal
class.
Polymorphism in Python
Polymorphism is a key concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass. It is a way to perform a single action in different ways. Polymorphism simplifies code and enhances flexibility by allowing functions and methods to use objects of different types through a common interface.
Understanding Polymorphism
Polymorphism allows you to define methods in a base class and override them in derived classes. When you call a method on an object, the version of the method that is executed depends on the object's class.
Example of Polymorphism with Inheritance
Consider a base class Animal
and derived classes Dog
and Cat
:
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def make_animal_speak(animal):
return animal.speak()
# Create instances of Dog and Cat
my_dog = Dog()
my_cat = Cat()
# Call the make_animal_speak function
print(make_animal_speak(my_dog)) # Output: Woof!
print(make_animal_speak(my_cat)) # Output: Meow!
In this example:
- The
Animal
class defines aspeak
method. - The
Dog
andCat
classes override thespeak
method. - The
make_animal_speak
function takes anAnimal
object and calls itsspeak
method. The correct version of the method is executed based on the object's class.
Polymorphism with Different Classes
Polymorphism is not limited to classes that share a common base class. You can achieve polymorphism with different classes as long as they implement a common method.
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Bird:
def speak(self):
return "Tweet!"
def make_animal_speak(animal):
return animal.speak()
# Create instances of Dog, Cat, and Bird
my_dog = Dog()
my_cat = Cat()
my_bird = Bird()
# Call the make_animal_speak function
print(make_animal_speak(my_dog)) # Output: Woof!
print(make_animal_speak(my_cat)) # Output: Meow!
print(make_animal_speak(my_bird)) # Output: Tweet!
In this example:
- The
Dog
,Cat
, andBird
classes each implement aspeak
method. - The
make_animal_speak
function can call thespeak
method on any object that implements it, demonstrating polymorphism.
Polymorphism in Practice
Polymorphism is often used in scenarios where you want to write generic code that can work with objects of different types. For example, consider a scenario where you want to calculate the area of different shapes.
class Shape:
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
def print_area(shape):
print("The area is:", shape.area())
# Create instances of Circle, Rectangle, and Triangle
my_circle = Circle(5)
my_rectangle = Rectangle(4, 6)
my_triangle = Triangle(3, 4)
# Print the area of each shape
print_area(my_circle) # Output: The area is: 78.5
print_area(my_rectangle) # Output: The area is: 24
print_area(my_triangle) # Output: The area is: 6.0
In this example:
- The
Shape
class defines anarea
method. - The
Circle
,Rectangle
, andTriangle
classes implement thearea
method. - The
print_area
function takes aShape
object and prints its area, demonstrating polymorphism.
Polymorphism with Interfaces (Abstract Base Classes)
In Python, you can use abstract base classes to enforce that certain methods are implemented in derived classes. This is particularly useful for polymorphism.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def make_animal_speak(animal):
return animal.speak()
# Create instances of Dog and Cat
my_dog = Dog()
my_cat = Cat()
# Call the make_animal_speak function
print(make_animal_speak(my_dog)) # Output: Woof!
print(make_animal_speak(my_cat)) # Output: Meow!
In this example:
- The
Animal
class is an abstract base class with an abstract methodspeak
. - The
Dog
andCat
classes inherit fromAnimal
and implement thespeak
method. - The
make_animal_speak
function demonstrates polymorphism.
Practical Application: A Payment System
Let's create a more complex example with a payment system that processes different types of payments.
class Payment:
def process(self):
pass
class CreditCardPayment(Payment):
def process(self):
return "Processing credit card payment"
class PayPalPayment(Payment):
def process(self):
return "Processing PayPal payment"
class BitcoinPayment(Payment):
def process(self):
return "Processing Bitcoin payment"
def process_payment(payment):
return payment.process()
# Create instances of payment types
credit_card = CreditCardPayment()
paypal = PayPalPayment()
bitcoin = BitcoinPayment()
# Process each payment type
print(process_payment(credit_card)) # Output: Processing credit card payment
print(process_payment(paypal)) # Output: Processing PayPal payment
print(process_payment(bitcoin)) # Output: Processing Bitcoin payment
In this example:
- The
Payment
class defines aprocess
method. - The
CreditCardPayment
,PayPalPayment
, andBitcoinPayment
classes implement theprocess
method. - The
process_payment
function takes aPayment
object and calls itsprocess
method, demonstrating polymorphism.
Encapsulation in Python
Encapsulation is a fundamental concept in object-oriented programming that involves bundling data (attributes) and methods (functions) that operate on the data into a single unit, typically a class. It restricts direct access to some of the object's components, which can prevent the accidental modification of data. Encapsulation is achieved by making attributes private and providing public methods to access and modify them.
Understanding Encapsulation
Encapsulation allows you to:
- Protect the internal state of an object: By restricting access to attributes, you can prevent external code from directly modifying the object's state in an unexpected way.
- Control how data is accessed and modified: By providing getter and setter methods, you can control how the attributes of an object are accessed and modified.
Example of Encapsulation
Consider a class Person
that encapsulates personal information:
class Person:
def __init__(self, name, age):
self.__name = name # Private attribute
self.__age = age # Private attribute
# Getter method for name
def get_name(self):
return self.__name
# Setter method for name
def set_name(self, name):
self.__name = name
# Getter method for age
def get_age(self):
return self.__age
# Setter method for age
def set_age(self, age):
if age > 0:
self.__age = age
else:
raise ValueError("Age must be positive")
# Create an instance of Person
person = Person("Alice", 30)
# Accessing private attributes using getter methods
print(person.get_name()) # Output: Alice
print(person.get_age()) # Output: 30
# Modifying private attributes using setter methods
person.set_name("Bob")
person.set_age(25)
print(person.get_name()) # Output: Bob
print(person.get_age()) # Output: 25
In this example:
- The attributes
__name
and__age
are private, indicated by the double underscores. - The
get_name
andget_age
methods are getter methods that provide access to the private attributes. - The
set_name
andset_age
methods are setter methods that allow modification of the private attributes.
Private and Protected Attributes
In Python, you can use a single underscore (_
) to indicate that an attribute is protected (intended for internal use within the class and its subclasses) and double underscores (__
) to indicate that an attribute is private (intended for internal use only within the class).
Example of Protected Attribute
class Employee:
def __init__(self, name, salary):
self._name = name # Protected attribute
self._salary = salary # Protected attribute
def get_salary(self):
return self._salary
def set_salary(self, salary):
if salary > 0:
self._salary = salary
else:
raise ValueError("Salary must be positive")
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary)
self._department = department # Protected attribute
def get_department(self):
return self._department
# Create an instance of Manager
manager = Manager("Carol", 50000, "IT")
# Accessing protected attributes
print(manager._name) # Output: Carol
print(manager.get_salary()) # Output: 50000
print(manager.get_department()) # Output: IT
In this example:
- The attributes
_name
,_salary
, and_department
are protected. - The
Manager
class inherits from theEmployee
class and can access the protected attributes.
Example of Private Attribute
class Account:
def __init__(self, account_number, balance):
self.__account_number = account_number # Private attribute
self.__balance = balance # Private attribute
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Deposit amount must be positive")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
raise ValueError("Invalid withdrawal amount")
# Create an instance of Account
account = Account("12345678", 1000)
# Accessing private attributes using getter methods
print(account.get_balance()) # Output: 1000
# Modifying private attributes using methods
account.deposit(500)
print(account.get_balance()) # Output: 1500
account.withdraw(200)
print(account.get_balance()) # Output: 1300
In this example:
- The attributes
__account_number
and__balance
are private. - The
get_balance
,deposit
, andwithdraw
methods provide controlled access to the private attributes.
Practical Application: Encapsulation in a Library System
Let's create a more complex example with a library system that encapsulates book details and manages borrowing and returning of books.
class Book:
def __init__(self, title, author, total_copies):
self.__title = title # Private attribute
self.__author = author # Private attribute
self.__total_copies = total_copies # Private attribute
self.__borrowed_copies = 0 # Private attribute
def borrow_book(self):
if self.__borrowed_copies < self.__total_copies:
self.__borrowed_copies += 1
return "Book borrowed successfully"
else:
return "No copies available"
def return_book(self):
if self.__borrowed_copies > 0:
self.__borrowed_copies -= 1
return "Book returned successfully"
else:
return "No borrowed copies to return"
def get_book_info(self):
return "Title: " + self.__title + ", Author: " + self.__author + ", Total Copies: " + str(self.__total_copies) + ", Available Copies: " + str(self.__total_copies - self.__borrowed_copies)
# Create an instance of Book
book = Book("1984", "George Orwell", 3)
# Access book information
print(book.get_book_info())
# Output: Title: 1984, Author: George Orwell, Total Copies: 3, Available Copies: 3
# Borrow a book
print(book.borrow_book()) # Output: Book borrowed successfully
print(book.get_book_info()) # Output: Title: 1984, Author: George Orwell, Total Copies: 3, Available Copies: 2
# Return a book
print(book.return_book()) # Output: Book returned successfully
print(book.get_book_info()) # Output: Title: 1984, Author: George Orwell, Total Copies: 3, Available Copies: 3
In this example:
- The
Book
class encapsulates the book details with private attributes. - The
borrow_book
andreturn_book
methods manage the borrowing and returning of books. - The
get_book_info
method provides controlled access to the book details.
Special Methods (Magic Methods) in Python
Special methods, also known as magic methods or dunder methods (due to their double underscores), are predefined methods in Python that you can override to customize the behavior of your classes. These methods are called automatically in certain situations and can be used to implement operator overloading, manage context (with statements), and more.
Common Special Methods
Here are some commonly used special methods:
__init__(self, ...)
- Object initialization (constructor)__str__(self)
- String representation__repr__(self)
- Official string representation__len__(self)
- Length__getitem__(self, key)
- Getting an item__setitem__(self, key, value)
- Setting an item__delitem__(self, key)
- Deleting an item__iter__(self)
- Iteration__next__(self)
- Next item__contains__(self, item)
- Membership test__add__(self, other)
- Addition__eq__(self, other)
- Equality
Example: Using __str__
and __repr__
The __str__
and __repr__
methods define the string representation of an object. The __str__
method is used by the str()
function and the print
statement, while __repr__
is used by the repr()
function and in interactive sessions.
__str__
and __repr__
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Person(name={self.name}, age={self.age})"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
person = Person("Alice", 30)
print(person) # Output: Person(name=Alice, age=30)
print(repr(person)) # Output: Person('Alice', 30)
In this example:
- The
__str__
method returns a user-friendly string representation of thePerson
object. - The
__repr__
method returns a string that can be used to recreate the object.
Example: Using __len__
, __getitem__
, __setitem__
, and __delitem__
These methods allow you to define custom behavior for the len()
function, indexing, item assignment, and item deletion.
__len__
, __getitem__
, __setitem__
, and __delitem__
class CustomList:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def __delitem__(self, index):
del self.items[index]
my_list = CustomList([1, 2, 3, 4])
print(len(my_list)) # Output: 4
print(my_list[2]) # Output: 3
my_list[2] = 10
print(my_list[2]) # Output: 10
del my_list[2]
print(len(my_list)) # Output: 3
print(my_list[2]) # Output: 4
In this example:
- The
__len__
method returns the length of the list. - The
__getitem__
method allows indexing. - The
__setitem__
method allows item assignment. - The
__delitem__
method allows item deletion.
Example: Using __iter__
and __next__
These methods allow you to define custom iteration behavior for your objects, making them iterable.
__iter__
and __next__
class Fibonacci:
def __init__(self, max):
self.max = max
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.a > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return self.a
fib = Fibonacci(10)
for num in fib:
print(num, end=" ") # Output: 1 1 2 3 5 8
In this example:
- The
__iter__
method returns the iterator object itself. - The
__next__
method returns the next value in the sequence and raisesStopIteration
when the sequence is exhausted.
Example: Using __add__
and __eq__
These methods allow you to define custom behavior for the +
operator and the ==
operator.
__add__
and __eq__
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 __eq__(self, other):
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # Output: Vector(4, 6)
print(v1 == v2) # Output: False
print(v1 == Vector(1, 2)) # Output: True
In this example:
- The
__add__
method allows you to add twoVector
objects. - The
__eq__
method allows you to compare twoVector
objects for equality.
Practical Application: A Fraction Class
Let's create a more complex example with a Fraction
class that supports addition and comparison of fractions.
import math
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("Denominator cannot be zero")
common = math.gcd(numerator, denominator)
self.numerator = numerator // common
self.denominator = denominator // common
def __add__(self, other):
new_numerator = self.numerator * other.denominator + other.numerator * self.denominator
new_denominator = self.denominator * other.denominator
return Fraction(new_numerator, new_denominator)
def __eq__(self, other):
return self.numerator == other.numerator and self.denominator == other.denominator
def __str__(self):
return f"{self.numerator}/{self.denominator}"
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
# Create instances of Fraction
frac1 = Fraction(1, 2)
frac2 = Fraction(3, 4)
frac3 = frac1 + frac2
print(frac3) # Output: 5/4
print(frac1 == frac2) # Output: False
print(frac1 == Fraction(2, 4)) # Output: True
In this example:
- The
Fraction
class represents a mathematical fraction. - The
__add__
method allows you to add twoFraction
objects. - The
__eq__
method allows you to compare twoFraction
objects for equality. - The
__str__
and__repr__
methods provide string representations of theFraction
objects.