Object-Oriented Programming
Classes and Instances in Python: Part 1
Classes serve as blueprints for creating reusable code structures.
Ryan McBride
Ryan McBride
alt

Source: Kimberly Farmer on Unsplash

Key Concepts in Object-Oriented Programming (OOP)

1. Classes as Blueprints: The Foundation of Reusability

Expansion: A class serves as a conceptual blueprint or a template for creating objects. Think of it like the architectural drawings for a house. The blueprint itself isn't a house you can live in, but it provides all the specifications—number of rooms, where the kitchen goes, plumbing, electrical—to build many identical or similar houses.

In programming, a class defines the common characteristics (data/attributes) and behaviors (functions/methods) that all objects created from that class will possess. This logical grouping is fundamental to OOP because it promotes:

  • Modularity: Code is organized into self-contained units (classes), making it easier to understand, manage, and debug.
  • Reusability: Once a class is defined, you can create multiple objects (instances) from it, each with its own unique data, without rewriting the underlying logic. This saves time and reduces errors.
  • Maintainability: Changes or updates to the class's logic can be applied in one place, automatically affecting all instances created from it.

2. Attributes and Methods: Defining "What It Is" and "What It Does"

Expansion: These are the core components that bring a class to life:

  • Attributes (Data): Attributes represent the state or characteristics of an object. They are like the nouns that describe an object. For an Employee class, first, last, pay, and email are attributes. Each instance of an Employee will have its own specific values for these attributes (e.g., employee_1 might have first='John', last='Doe', while employee_2 has first='Jane', last='Smith'). Attributes can be of various data types (strings, numbers, booleans, lists, even other objects).

  • Methods (Functions): Methods represent the actions or behaviors that an object can perform. They are like the verbs that describe what an object does. The full_name method for an Employee class is an action that returns the employee's full name. Methods often operate on the object's attributes to achieve their purpose. They encapsulate the logic related to an object's behavior, keeping the code organized and relevant to the object itself.

3. Creating a Simple Class: The pass Keyword

Example Revisited:

class Employee:
    pass

Expansion: The pass keyword is a null operation; when executed, nothing happens. It's used as a placeholder in Python where a statement is syntactically required but you don't want any code to execute. In the context of a class, pass allows you to define an empty class. This is useful for:

  • Scaffolding: When you're just starting to design your program and want to define the class structure before implementing any attributes or methods.
  • Inheritance: You might have a base class that simply serves as an interface or a common type, and its subclasses will add specific implementations.

An empty class, while syntactically correct, doesn't do much on its own. Its real power comes when you add attributes and methods to it.

4. Instances of a Class: Bringing Blueprints to Life

Expansion: While a class is the blueprint, an instance is a concrete, tangible object created from that blueprint. It's like taking the house blueprint and actually building a physical house. Each instance is a unique entity with its own memory space, even if they share the same class definition.

To create an instance, you "call" the class as if it were a function:

employee_1 = Employee() # Creates an instance of the Employee class
employee_2 = Employee() # Creates another, distinct instance

At this stage (before __init__), both employee_1 and employee_2 are just empty Employee objects. They don't yet have any specific data associated with them because we haven't defined how to initialize that data.

5. Instance Variables: Unique Data for Each Object

Expansion: Instance variables are crucial because they hold the data that is specific to each individual instance of a class. When you create employee_1 and employee_2 from the Employee class, they will each have their own first, last, pay, and email instance variables, and the values stored in these variables can be different for each employee.

Think of it this way: all houses built from the same blueprint have a number_of_bedrooms attribute, but one house might have 3 bedrooms while another has 4 (if the blueprint allows for variations). The number_of_bedrooms for a specific house is its instance variable.

These variables are typically set within the __init__ method, which ensures that an instance is properly configured when it's created.

6. The __init__ Method: The Constructor's Role

Example Revisited:

class Employee:
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first + '.' + last + '@company.com'

Expansion: The __init__ method (pronounced "dunder init" for "double underscore") is a special method in Python classes that gets called automatically every time a new instance of the class is created. Its primary purpose is to initialize the attributes of the newly created object.

  • self: This is the most important argument to __init__ and all other instance methods. It always refers to the instance of the class itself. When you call Employee('John', 'Doe', 50000), Python internally passes the newly created (but uninitialized) employee_1 object as the self argument to __init__. Inside __init__, self.first = first means "set the first attribute of this specific instance to the value passed in as first."

  • Parameters: __init__ can take other parameters (like first, last, pay in our example) that provide the initial values for the instance's attributes.

  • Initialization Logic: Inside __init__, you define how the instance variables are created and assigned their initial values. This can involve simple assignments (like self.first = first) or more complex logic (like generating the email based on first and last).

Without __init__, you would have to manually set each attribute after creating an instance, which would be cumbersome and error-prone.

7. Methods: Defining Object Behavior

Example Revisited:

class Employee:
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first + '.' + last + '@company.com'

    def full_name(self):
        return f'{self.first} {self.last}'

Expansion: Methods are simply functions that are defined inside a class. They operate on the data (attributes) of the instance they are called on.

  • Encapsulation: Methods encapsulate the logic related to an object's behavior. The full_name method, for example, knows how to combine first and last to produce a full name, and this logic is neatly contained within the Employee class.

  • Accessing Attributes: Methods typically access the instance's attributes using the self argument (e.g., self.first). This is how a method knows which specific instance's data to work with.

  • Flexibility: Methods can perform calculations, modify attributes, interact with other objects, or return values, making classes dynamic and powerful.

8. The self Argument: The Instance's Identity

Expansion: The self parameter is a convention in Python (though you could technically name it anything, it's strongly discouraged) that refers to the instance of the class on which the method was called. Python automatically passes the instance as the first argument to any instance method.

When you write:

employee_1 = Employee('John', 'Doe', 50000)
print(employee_1.full_name())

Python effectively translates employee_1.full_name() into Employee.full_name(employee_1). The employee_1 object is implicitly passed as the self argument to the full_name method. This allows full_name to know which specific employee's first and last names it should retrieve and combine.

self is the mechanism that connects a method call to the specific data of a particular instance.

9. Calling Methods: Instance vs. Class Calls

Expansion: There are two primary ways to call methods:

Calling on an Instance (Most Common): This is the typical way you interact with an object's methods. When you call employee_1.full_name(), you are telling the employee_1 instance to execute its full_name method. This is the more intuitive and object-oriented way, as it implies that the instance itself is performing the action.

employee_1 = Employee('Alice', 'Wonderland', 60000)
print(employee_1.full_name()) # Output: Alice Wonderland

Calling on the Class (Less Common for Instance Methods): You can call an instance method directly on the class, but in this case, you must explicitly pass the instance as the first argument (which corresponds to self).

employee_1 = Employee('Bob', 'The Builder', 70000)
print(Employee.full_name(employee_1)) # Output: Bob The Builder

While syntactically possible, calling instance methods on the class directly is less common and usually indicates that you might be misunderstanding the object-oriented paradigm. The primary use case for calling methods on the class itself typically involves class methods and static methods, which are different types of methods that don't implicitly take an instance as their first argument (or any instance at all, in the case of static methods). These are advanced concepts you'll encounter as you delve deeper into OOP.