Classes and Objects: Representations of Our World in Code
Published: 2024-05-13 02:43:54

## An Issue of Translation

The challenge of translating real world objects, structures, and concepts into code is one that has been tackled many, many times over, and will continue to be something that's refined by people as time goes on. Object oriented programming (OOP) seeks to address this by using a methodology which revolves around the titular "object", a structure which stores data as well as ways (referred to as "methods") of interacting with that data. In the case of the Python programming language, objects are managed through a system based on "classes" which serve as the blueprints for creating objects. 

We can think of classes as the broader concept of a thing, such as a car. There are many different types of cars, all with their own unique features, and even cars of the same type can vary greatly in what parts they use under the hood, their interiors, and colors. Using classes allows us to create a blueprint for our objects, each of which is referred to as an "instance" of a class.

## Building a Class

Creating a class is fairly simple. One merely has to use the "class" keyword, followed by the class name (capitalized by convention), with its properties and methods defined in an indented block underneath.

```py
class Car():
    def __init__(self, make, model, color):
        self.make = make
        self.model = model
        self.color = color
    
    def drive():
        print('Vroom')
```

The "init" method is a special method that's called each time an instance of a class is created. It takes the essential data for initializing an object from the class as arguments.

```py
car1 = Car('Subaru', 'Forrester', 'Green')
car2 = Car('Chevrolet', 'Blazer', 'Red')
```

Here we create two instances of the car class, each with their own attributes. They are distinct from each other and have their own data, but are both members of the same class. In order to access the data and methods stored in these objects, we use dot notation. 

```py
print(car1.make) # Output: Subaru
car1.drive() # Output: Vroom
```

While classes are used as blueprints, they are themselves considered objects. As such, we are able to define variables that are shared with all instances of a class. In contrast, instance variables are unique to their instance.

```py
class Car():
    make = 'Subaru'
    
    def __init__(self, model, color):
        self.model = model
        self.color = color
    
car1 = Car('Forrester', 'Green')
car2= Car('Outback', 'Blue')

print(car1.model) # Output: Forrester
print(car2.model) # Output: Outback
print(car1.make) # Output: Subaru
print(car2.make) # Output: Subaru
```
## Inheritance

Python classes can also inherit attributes and methods from another class, making them incredibly versatile. The ability to create sub classes allows even more accurate modeling of the world around us.

```py
class Car():
    def __init__(self, make, model, color):
        self.make = make
        self.model = model
        self.color = color

    def horn():
        pass
    
class Sedan(Car):
    def __init__(self, make, model, color):
        super().__init__(make, model, color)
    
    def horn():
        print('HONK')

class Hatchback(Car):
    def __init__(self, make, model, color):
        super().__init__(make, model, color)
    
    def horn():
        print('beep')
```
In this example there are a couple of special things happening. The first is the use of super(). By using super to call the __init__ method from the Car class, we can avoid having the repetitious code of assigning each variable passed in to self. Super can be applied to any method, but is most often used for __init__. The second thing is the overriding of the horn() method in each of the subclasses. This allows each of the subclasses to apply their own unique behaviors to the same method defined in the superclass. This leads to a concept referred to as polymorphism, where different classes can use the same method name in their own unique way, leading to more flexible and modular code. 

## Taking it Further

A great example of how classes can be leveraged for more complex use is object relational mapping. A massive amount of information is stored in databases, broken down into columns and rows. Columns dictate a kind of data to be stored, whether that be a number, string, object(!), etc. Rows are comprised of one "cell" from each column, adding up to a collection of different values, oftentimes identified by a unique, numbered column. Relational databases are comprised of multiple tables related to one another. Using a database language like SQL in conjunction with Python we can map a class to the table, each column represented as an attribute or property in the class. By creating instances of that class using the data from each row, it's possible to perform complex operations both using, and on said data. While this is a subject worthy of many, many blog posts, it serves as one of the many powerful uses for classes in object oriented programming. 

All of these features allow classes to make code extremely robust, versatile, reusable, and expressive. The ways in which they reflect the world in which we live and the problems they are used to solve are fairly easy to grasp, but have immense potential. While it's incredibly straightforward to compare an object in Python to a tangible object in the real world (like a car, as in the previous examples), they can represent so much more. People, organizational structures, and even concepts can be represented by objects in code, and classes help us to organize and relate those objects to one another.