Classes: Building Our Own Data Structures
As Data Structures students, we’ve worked a lot with Python’s built-in data types like strings, lists, and dictionaries. We’ve also talked about more intricate data types like stacks, queues and trees. Now, it’s time to level up your skills by creating your very own custom data types using Classes. Classes are the foundation of object-oriented programming (OOP) and allow you to bundle data and functionality together in a way that mirrors real-world objects.
What is a Class?
A Class is like a blueprint for creating objects. Think of it as a template that defines:
- Attributes (variables) - the data that describes an object
- Methods (functions) - the behaviors or actions an object can perform
For example, if we were creating a video game, we might have a Player class that contains attributes like health, position, and inventory, along with methods like move, attack, and use_item.
Creating Your First Class
Mrs. Johnson really wants a dog. I’m sure a digital Dog that lives in terminal memory will be good enough, right? Let’s start by creating a Dog class:
class Dog:
# This is the initializer method (constructor)
def __init__(self, name, breed, age):
self.name = name # Attribute
self.breed = breed # Attribute
self.age = age # Attribute
self.is_sitting = False
# Methods define what the dog can do
def bark(self):
return f"{self.name} says Woof!"
def sit(self):
self.is_sitting = True
return f"{self.name} is now sitting"
def stand(self):
self.is_sitting = False
return f"{self.name} is now standing"
def birthday(self):
self.age += 1
return f"{self.name} is now {self.age} years old!"
Creating Objects from a Class
Once you’ve defined a class, you can create instances (objects) of that class:
# Create two dog objects
my_dog = Dog("Snoopy", "Cocker Spaniel", 3)
your_dog = Dog("Flinn", "French Poodle", 2)
# Use the objects
print(my_dog.name) # Output: Snoopy
print(your_dog.breed) # Output: French Poodle
print(my_dog.bark()) # Output: Snoopy says Woof!
print(your_dog.sit()) # Output: Flinn is now sitting
print(your_dog.is_sitting) # Output: True
print(my_dog.is_sitting) # Output: False
Each Dog object has its own separate set of attributes, but they share the same methods.
The self Parameter
You might have noticed that methods in a class take a parameter called self
. This parameter represents the specific instance of the class that’s calling the method. When you call my_dog.bark()
, Python automatically passes my_dog
as the self
parameter to the bark
method.
A More Complex Example
Let’s create a BankAccount class to manage a simple bank account:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transaction_history = []
def deposit(self, amount):
if amount > 0:
self.balance += amount
self.transaction_history.append(f"Deposit: +${amount}")
return f"Deposited ${amount}. New balance: ${self.balance}"
else:
return "Deposit amount must be positive"
def withdraw(self, amount):
if amount > 0:
if amount <= self.balance:
self.balance -= amount
self.transaction_history.append(f"Withdrawal: -${amount}")
return f"Withdrew ${amount}. New balance: ${self.balance}"
else:
return "Insufficient funds"
else:
return "Withdrawal amount must be positive"
def get_balance(self):
return f"Current balance: ${self.balance}"
def show_transactions(self):
if not self.transaction_history:
return "No transactions yet"
return "\n".join(self.transaction_history)
Let’s put the BankAccount class into action:
# Create a new account
account = BankAccount("Samuel", 100)
# Perform some transactions
print(account.deposit(50)) # Output: Deposited $50. New balance: $150
print(account.withdraw(30)) # Output: Withdrew $30. New balance: $120
print(account.withdraw(200)) # Output: Insufficient funds
print(account.get_balance()) # Output: Current balance: $120
# Check transaction history
print(account.show_transactions())
# Output:
# Deposit: +$50
# Withdrawal: -$30
Inheritance: Building on Existing Classes
One of the most powerful features of OOP is inheritance, which allows you to create new classes based on existing ones. The new class (subclass) inherits attributes and methods from the existing class (superclass) and can add or override them.
Let’s create a SavingsAccount class that inherits from our BankAccount class:
class SavingsAccount(BankAccount):
def __init__(self, owner, balance=0, interest_rate=0.01):
# Call the parent class's __init__ method
super().__init__(owner, balance)
self.interest_rate = interest_rate
def add_interest(self):
interest = self.balance * self.interest_rate
self.deposit(interest)
return f"Added ${interest:.2f} interest"
# Override the withdraw method to add a limit
def withdraw(self, amount):
if self.balance - amount < 50:
return "Savings account must maintain a minimum balance of $50"
return super().withdraw(amount)
Let’s put the SavingsAccount class into action:
# Create a savings account
savings = SavingsAccount("Bob", 1000, 0.05)
print(savings.get_balance()) # Output: Current balance: $1000
print(savings.add_interest()) # Output: Added $50.00 interest
print(savings.get_balance()) # Output: Current balance: $1050
print(savings.withdraw(1000)) # Output: Savings account must maintain a minimum balance of $50
print(savings.withdraw(500)) # Output: Withdrew $500. New balance: $550
Class Variables vs. Instance Variables
So far, we’ve been using instance variables (attached to self), which are unique to each object. Python also supports class variables, which are shared among all instances of a class:
class Student:
# Class variable - shared by all instances
school_name = "Mooseheart Child City & School"
student_count = 0
def __init__(self, name, grade):
self.name = name # Instance variable - unique to each instance
self.grade = grade # Instance variable
Student.student_count += 1 # Incrementing the class variable
def display_info(self):
return f"{self.name} is in grade {self.grade} at {Student.school_name}"
# Create some students
student1 = Student("Jimmy", 12)
student2 = Student("Frank", 10)
print(student1.display_info()) # Output: Jimmy is in grade 12 at Mooseheart Child City & School
print(student2.display_info()) # Output: Frank is in grade 10 at Mooseheart Child City & School
print(f"Total students: {Student.student_count}") # Output: Total students: 2
# Change the class variable
Student.school_name = "Batavia High School"
# Both students are affected by the change
print(student1.display_info()) # Output: Jimmy is in grade 12 at Batavia High School
print(student2.display_info()) # Output: Frank is in grade 10 at Batavia High School
Encapsulation: Protecting Data
Encapsulation means bundling data and methods together and restricting direct access to some of an object’s components. In Python, we use naming conventions to indicate that attributes or methods should be treated as public, private or protected.
class CreditCard:
def __init__(self, number, holder, limit):
self.holder = holder # Public attribute
self._limit = limit # Protected attribute (single underscore)
self.__number = number # Private attribute (double underscore)
self.__balance = 0 # Private attribute
def get_balance(self):
return f"Current balance: ${self.__balance}"
def charge(self, amount):
if self.__balance + amount > self._limit:
return "Transaction declined: over limit"
self.__balance += amount
return f"Charged ${amount}. New balance: ${self.__balance}"
def payment(self, amount):
self.__balance -= amount
return f"Payment of ${amount} received. New balance: ${self.__balance}"
card = CreditCard("1234-5678-9012-3456", "John Doe", 2000)
print(card.holder) # Works fine: John Doe
# print(card.__number) # Error! Can't access private attribute directly
# print(card.__balance) # Error! Can't access private attribute directly
print(card._limit) # Works, but convention suggests you shouldn't access it: 2000
print(card.charge(500)) # Output: Charged $500. New balance: $500
print(card.get_balance()) # Output: Current balance: $500
print(card.payment(200)) # Output: Payment of $200 received. New balance: $300
Properties: Controlled Access to Attributes
A more Pythonic way to implement getters and setters is using properties:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15: # Absolute zero
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) # Output: 25
print(temp.fahrenheit) # Output: 77.0
temp.celsius = 30
print(temp.fahrenheit) # Output: 86.0
temp.fahrenheit = 50
print(temp.celsius) # Output: 10.0
# temp.celsius = -300 # Raises ValueError: Temperature cannot be below absolute zero
Static Methods and Class Methods
Python classes can also have methods that don’t operate on instances:
- Static methods: Don’t access instance or class data
- Class methods: Can access and modify class-level data
class MathHelper:
pi = 3.14159
def __init__(self, value):
self.value = value
def square(self):
return self.value ** 2
@staticmethod
def is_prime(num):
"""Check if a number is prime"""
if num < 2:
return False
for i in range(2, int(num**0.5) + 1):
if num % i == 0:
return False
return True
@classmethod
def circle_area(cls, radius):
"""Calculate circle area using the class's PI value"""
return cls.pi * radius ** 2
@classmethod
def update_pi(cls, new_pi):
"""Update the PI value for all instances"""
cls.pi = new_pi
# Static method - doesn't need an instance
print(MathHelper.is_prime(17)) # Output: True
print(MathHelper.is_prime(15)) # Output: False
# Class method - works with class variables
print(MathHelper.circle_area(5)) # Output: 78.53975
# Create an instance for instance methods
math = MathHelper(8)
print(math.square()) # Output: 64
# Update class variable using class method
MathHelper.update_pi(3.14)
print(MathHelper.circle_area(5)) # Output: 78.5
Classes in Python allow you to create organized, reusable code that models real-world objects and concepts. As you’ve seen, they provide powerful features like inheritance, encapsulation, and properties that help you write cleaner, more maintainable code. When designing classes, think about:
- What attributes (data) the object should have
- What actions (methods) the object should be able to perform
- How objects will interact with each other
- Whether you need inheritance relationships between classes
With these principles in mind, you’re well on your way to building complex, object-oriented programs in Python. Thanks for reading and happy coding!
Statement: Unless otherwise specified, the copyright belongs to Erick Johnson . Reprint please indicate the link of this article.
(The content is authorized with CC BY-NC-SA 4.0 protocol)
Title:《 Classes: Building Our Own Data Structures 》
Link:https://erickrj.tech/python/classes-building-our-own-data-structures/
The last update of this article was days ago, so it may be outdated!