Classes#
While functions
are common to almost all types of programming languages, the concept of Class
is specific to object-oriented languages. Classes let you create your own custom object types that can contain both data (attributes) and behavior (methods).
In this section, we’ll learn how classes help organize code in a more structured way compared to using separate functions and variables.
Why Use Classes?#
Let’s revisit the assay analysis from the previous Functions section. We had two lists for grades
and depths
, and we wanted to find depths where values exceeded some threshold.
grades = [0.1, 0.02, 1.3, 2.4, 3.5, 0.01]
depths = [10, 55, 80, 105, 115, 120]
Creating a Class#
Let’s create an Assay
class that brings together the data (grades and depths) and the functions we created previously:
class Assay:
"""Simple Assay class that stores and analyzes grade measurements at different depths."""
def __init__(self, arg_1: list, arg_2: list, threshold: float = 1.0):
self.grades = arg_1
self.depths = arg_2
self.threshold = threshold
def anomalous(self):
"""
Find the elements of a list above threshold.
:return: A list of True/False values indicating which grades exceed the threshold
"""
return [val > self.threshold for val in self.grades]
def get_depths(self):
"""
Extract depths where grades exceed the threshold.
:return: A list of depths where grades are above the threshold
"""
output = []
for val, cond in zip(self.depths, self.anomalous()):
if cond:
output.append(val)
return output
def __call__(self):
return self.get_depths()
Class Anatomy#
Let’s break down the components of our class:
class
Keyword#
The class
keyword tells Python we’re creating a new object type, followed by the name of the class (typically using CamelCase, where each word starts with a capital letter).
__init__
Method (Constructor)#
The __init__
method is special - it runs automatically when we create a new instance of the class. It’s like a setup function that prepares our object with initial values:
def __init__(self, arg_1: list, arg_2: list, threshold: float = 1.0):
self.grades = arg_1
self.depths = arg_2
self.threshold = threshold
self
refers to the specific instance of the class being createdThe other parameters are values we want to provide when creating the object
We store the values as attributes of the object using
self.attribute_name
Creating an Instance#
To create an instance of our class (an actual object), we call the class name like a function:
# Create a new Assay object
assay = Assay(grades, depths)
# What type of object is it?
print(f"Type of 'assay': {type(assay)}")
Type of 'assay': <class '__main__.Assay'>
# Accessing its attributes
print(f"First few grades: {assay.grades[0:3]}") # get items in indices 0, 1, 2 of the list 'assay.grades' using `[0:3]`
print(f"Default threshold: {assay.threshold}")
First few grades: [0.1, 0.02, 1.3]
Default threshold: 1.0
The self
Parameter#
The self parameter is a reference to the current instance of the class. It:
Must be the first parameter of every method in the class
Allows methods to access and modify the object’s attributes
Lets methods call other methods within the same object
For example, in our anomalous() method:
def anomalous(self):
return [val > self.threshold for val in self.grades]
The method uses self
to access the threshold
and grades
attributes we defined in __init__
.
# Calling a method of our object
high_grade_locations = assay.anomalous()
print("Is each sample above the threshold?")
print(high_grade_locations)
Is each sample above the threshold?
[False, False, True, True, True, False]
Methods vs Functions#
Methods are functions that belong to a class. The main differences are:
Methods are defined inside a class
Methods always take
self
as their first parameterMethods are called using the dot notation on an object:
object.method()
The __call__
Method#
The __call__
method lets you use your object as if it were a function. When you add parentheses after your object name (like assay()
), Python looks for a __call__
method to execute.
In our case, we defined __call__
to run the get_depths()
method:
def __call__(self):
return self.get_depths()
This gives us a handy shortcut:
# These two lines do the same thing:
print("Using the get_depths method:", assay.get_depths())
print("Using the __call__ method:", assay())
Using the get_depths method: [80, 105, 115]
Using the __call__ method: [80, 105, 115]
Properties and Attribute Control#
Sometimes we want to add validation or special behaviour when getting or setting attributes. Python’s @property
decorator lets us do this:
class Assay2:
"""
Improved Assay class with property validation.
"""
def __init__(self, arg_1: list, arg_2: list, threshold: float = 1.0):
self.grades = arg_1
self.depths = arg_2
self._threshold = threshold # Note the underscore
def anomalous(self):
"""
Find the elements of a list above threshold.
"""
return [val > self.threshold for val in self.grades]
def get_depths(self):
"""
Extract depths where grades exceed the threshold.
"""
output = []
for val, cond in zip(self.depths, self.anomalous()):
if cond:
output.append(val)
return output
@property
def threshold(self) -> float:
"""Cutoff value for anomalous assays."""
return self._threshold # Note: accessing _threshold, not threshold
@threshold.setter
def threshold(self, value):
"""Validate threshold value when it's changed."""
if not isinstance(value, float):
raise ValueError("The value for threshold must be a float.")
self._threshold = value
def __call__(self):
return self.get_depths()
Private Attributes with Underscores#
In Python, adding a single underscore before an attribute name (like _threshold
) is a convention that indicates this attribute should be considered “private” or “internal” to the class:
It signals to other developers: “please don’t access this directly from outside the class”
It’s not enforced by Python (unlike in other langauges), but it’s a widely respected convention
It helps prevent accidental changes to important internal variables
Property Decorators#
The @property
decorator transforms a method into an attribute-like accessor. This gives you control over how attributes are accessed:
@property
def threshold(self) -> float:
"""Cutoff value fo anomalous assays."""
return self._threshold
When someone accesses assay.threshold
, this method runs and returns the value.
The @threshold.setter
decorator creates a method that runs when someone tries to change the attribute:
@threshold.setter
def threshold(self, value):
"""Validate threshold value when it's changed."""
if not isinstance(value, float):
raise ValueError("The value for threshold must be a float.")
self._threshold = value
This lets us validate the new value before accepting it:
# Create our improved assay
assay2 = Assay2(grades, depths)
# This works fine because 2.0 is a float
assay2.threshold = 2.0
print(f"New threshold: {assay2.threshold}")
# This would raise an error because it's not a float
# assay2.threshold = "high" # uncomment to see error
New threshold: 2.0
When to Use Classes#
Classes are most useful when:
You have data and behaviour that logically belong together
You want to create multiple similar objects with the same structure
You want to maintain state of an object across multiple operations
You want to organize related functionality into a single unit
You’re building a larger system where code organization is important
For simple scripts or one-off tasks, functions might be simpler. But as your programs grow in complexity, classes help keep your code organized and maintainable.
Summary: What You’ve Learned#
Classes are a fundamental feature of object-oriented programming that allow you to create your own custom data types. While functions help organize code into reusable blocks, classes go further by combining related data (attributes) and behavior (methods) into a single unit.
In this notebook, you learned:
Classes are blueprints for creating objects with attributes (data) and methods (behavior)
The
class
keyword defines a new class in PythonThe
__init__
method initializes new objects when they’re createdself
refers to the current instance and must be the first parameter in instance methodsClasses help organize related data and behavior in a more structured way
Objects
are instances of classes that store their own stateMethods
are functions that belong to a class and can access/modify its attributesObject-oriented programming helps keep code organized, reusable, and maintainable
By using classes, you can create custom data types specific to your problem domain, making your code more intuitive and easier to maintain.
Knowledge Check: Python Classes#
1. What keyword is used to define a class in Python?#
a) struct
b) def
c) object
d) class
2. What is the purpose of the __init__
method in a class?#
a) To delete an object when it’s no longer needed
b) To initialize a new object with attributes when it’s created
c) To import required modules for the class
d) To define which methods can be accessed from outside the class
3. What does the self
parameter refer to in a class method?#
a) The class itself
b) The method being called
c) The current instance of the class
d) The attribute assigned to self
in the class definition
4. How do you create an instance of a class named Student?#
a) Student.create()
b) Student(self)
c) Student()
d) Student.__init__()
5. In object-oriented programming, what are attributes?#
a) The names of classes in your program
b) The data or variables that belong to an object
c) The keywords used to define a class
d) The modules required by a class
6. What’s the main benefit of using classes instead of separate functions and variables?#
a) Classes organize related data and behavior together
b) Classes use less memory than functions
c) Classes always execute faster than functions
d) Classes allow you to execute multiple functions at once
7. Which of the following is true about methods in a class?#
a) Methods always require at least two parameters
b) Methods always require exactly one parameter
c) Methods can access and modify object attributes using self
d) Methods must always return a value
Answers#
d
b
c
c
b
a
c
Copyright (c) 2022 Mira Geoscience Ltd.