26  Objects and classes

Author

Andres Patrignani

Published

January 4, 2024

Python’s programming approach is centered around objects, a concept that encapsulates data and functionality. In Python, everything you use —be it variables, modules, or figures— is an object. This approach is part of what we call object-oriented programming (OOP), which is useful because it organizes code into small, reusable pieces, making it easier to understand and maintain. Think of objects as mini-programs that have their own properties (like characteristics or data) and methods (functions or actions they can perform).

In Python, objects often have both properties and methods. A property is like an attribute of an object, while a method is a function that belongs to the object and usually performs some action or computation related to the object. Methods are called with parentheses and may take arguments.

Syntax

Syntax for accessing object properties and methods:

    object.property
    object.method()

Syntax for defining our own objects using classes:

class ClassName:
    def __init__(self, attributes):
        # Constructor method for initializing instance attributes

    def method_name(self, parameters):
        # Method of the class
        

ClassName is the name of the class.
__init__ is the constructor method.
method_name is a method of the class.

Let’s look at these concepts using a simple example and then we will create our own object.

Properties and methods example

I often find that using the NumPy module is a simple and clear way to illustrate the difference between properties/attributes and methods/functions.

Jargon note

The terms property and attribute are used interchangeably to represent characteristics of the object.

Similarly, the term method is an another word to denote a function inside of an object. Methods (or functions) represent actions that can be performed on the object and are only available within a specific object.

import numpy as np

# Define an array (this is an object)
A = np.array([1, 2, 3, 4, 5, 6])

# Properties of the Numpy array object
print('Properties')
print(A.shape) # Dimensions of the array
print(A.size) # Total number of elements
print(A.dtype) # Data type
print(A.ndim) # NUmber of array dimensions

# Methods/Functions of the Numpy array object
print('') # Add a blank line
print('Methods')
print(A.mean()) # Method to compute average of all values
print(A.sum()) # Method to compute sum of all values
print(A.cumsum()) # Method to compute running sum of all values
print(A.reshape(3,2)) # Reshape to a 3 by 2 matrix
Properties
(6,)
6
int64
1

Methods
3.5
21
[ 1  3  6 10 15 21]
[[1 2]
 [3 4]
 [5 6]]

Class example: Laboratory sample

Consider a scenario where we operate a soil analysis laboratory receiving samples from various clients, such as farmers, gardeners, and golf course superintendents. Each soil sample possesses unique attributes that we need to record and analyze. These attributes might include the client’s full name, the date the sample was received, a unique identifier, the location of sample collection, the analyses requested, and the results of these analyses. In this context, the primary unit of interest is the individual sample, and all the additional information represents its metadata.

To efficiently manage this data, we can create a Python class specifically for our soil samples. This class will allow us to create a structured record for each new sample we receive, demonstrating Python’s flexibility in creating custom objects tailored to our specific needs. We will use Python’s uuid module to generate a unique identifier for each sample and the datetime module to timestamp when each sample object is created.

Note

Classes are like blueprints for objects since they define what properties and methods an object will have. For more information check Python’s official documentation about objects and classes

import uuid
import datetime
import pprint

class SoilSample:
    """ Class that defines attributes and methods for new soil samples"""
    
    def __init__(self, customer_name, location, analyses_requested):
        """Attributes of the soil sample generated upon sample entry.
        
        Inputs
        ----------
        customer_name : string
            Customer's full name
        location : tuple
            Geographic location (lat,lon) of sample collection
        analyses_requested : list
            Requested analyses
        """
        self.sample_id = str(uuid.uuid4())  # Unique identifier for each sample
        self.timestamp = datetime.datetime.now().strftime("%d-%b-%Y %H:%M:%S") # Timestamp of sample entry
        self.customer_name = customer_name  # Customer's full name
        self.location = location # Geographic location of sample collection
        self.analyses_requested = analyses_requested  # List of requested analyses
        self.results = {}  # Dictionary to store results of analyses

    def add_results(self, analysis_type, result):
        """Function that adds the name and results of a specific soil analysis."""
        self.results[analysis_type] = result  # Add analysis results

    def summary(self):
        """Function that prints summary information for the sample."""
        info = (f"Sample ID: {self.sample_id}",
                f"Timestamp: {self.timestamp}",
                f"Customer: {self.customer_name}",
                f"Location: {self.location}",
                f"Requested Analyses: {self.analyses_requested}",
                f"Results: {self.results}")
        return pprint.pprint(info)
What is __init__()?

The __init__() function at the beginning of the class is a special method that gets called (another term for this action is invoked) automatically when we create a new instance of the class. Think of __init__ as the setup of the object, where the initial state of a new object is defined by assigning values to its properties.

What is self?

When defining a class, the word self is used to refer to the instance of the class itself. It’s a way for the class to reference its own attributes and methods and is usually defined with the words self or this (but it can be anything else you want). Typically it is a short word that is meaningful and easy to type. We will use self to match the official Python documentation.

Imagine each class as a blueprint for building a house. Each house built from the blueprint is an instance (an occurrence) of the class. In this context, self is like saying this particular house, rather than the general blueprint.

# Access our own documentation
SoilSample?
Init signature: SoilSample(customer_name, location, analyses_requested)
Docstring:      <no docstring>
Init docstring:
Attributes of the soil sample generated upon sample entry.
Inputs
----------
customer_name : string
    Customer's full name
location : tuple
    Geographic location (lat,lon) of sample collection
analyses_requested : list
    Requested analyses
Type:           type
Subclasses:     
# Example usage. Create or instantiate a new object, in this case a new sample.
new_sample = SoilSample("Andres Patrignani", (39.210089, -96.590213), ["pH", "Nitrogen"])
What does instantiation mean?

Instantiation is the term used in Python for the process of creating a new object from a blueprint, the class we just defined.

# Access properties generated when we created the new sample
print(new_sample.customer_name)
print(new_sample.timestamp)
Andres Patrignani
04-Jan-2024 16:52:03
# Use the add_results() method to add some information to our sample object
new_sample.add_results("pH", 6.5)
new_sample.add_results("Nitrogen", 20)
# Use the summary() method to print the information available for our sample
new_sample.summary()
('Sample ID: 322a5b20-ed71-4c4e-b181-789fb6574d8d',
 'Timestamp: 04-Jan-2024 16:52:03',
 'Customer: Andres Patrignani',
 'Location: (39.210089, -96.590213)',
 "Requested Analyses: ['pH', 'Nitrogen']",
 "Results: {'pH': 6.5, 'Nitrogen': 20}")
Inheritance

A cool feature of classes in Python is their ability to inherit properties and methods from an already pre-defined class. This is called inheritance, and it allows programmers to build upon and extend the functionality of existing classes, creating new, more specialized versions without reinventing the wheel. After mastering the basics of classes and objects, exploring class inheritance is a must to take your coding skills to the next level.

Practice

To take this exercise to the next level try the following improvements:

  • Add one new attribute and one new methods to the class

  • Use the input() function to request the required data for each sample from users

  • Use a for loop to pre-populate the results with None for each of the requested soil analyses. This will ensure that only those analyses are entered into the sample.

  • Create a way to store multiple sample entries. You can simply append each new sample to a variable defined at the beginning of your script, append the new entries to a text or .json file, use the pickle module, or use a databases like sqlite3, MySQL, or TinyDB module