OOP Python for Dummies
Tags: python, oop, beginners, programming
Published: 2023-06-27 18:39:55

**~For my TLDR people skip to TLDR (last section)~**

## Introduction

As **beginner programmers**, there's a substantial amount of **information** we need to **learn** and **absorb**. And I mean a **SUBSTANTIAL** amount. On top of the **basic fundamentals** etc., we also encounter different **programming paradigms**. 

**Programming Paradigms**
>"Programming paradigms are different ways or styles in which a given program or programming language can be organized." 
-[freecodecamp.org](https://www.freecodecamp.org/news/an-introduction-to-programming-paradigms/) -- Germán Cocca

A popular **paradigm**, one that I'm sure many of you have heard before, is **Object-Oriented Programming** or **OOP** for short. If you've ever watched a **Python** youtube tutorial, you'll know **exactly** what I'm talking about. **OOP** has become quite popular due to its ability to **organize code**, **enhance reusability**, and **improve maintainability**.

**Object-Oriented Programming**
>"In basic terms, OOP is a programming pattern that is built around objects or entities, so it's called object-oriented programming." 
-[freecodecamp.org](https://www.freecodecamp.org/news/what-is-object-oriented-programming/) -- Hillary Nyakundi

So, let's not waste your time and dive right in.

## Defining a `class`
To start using **OOP** structure in **Python** we first need to define a `class`. To do this, **first** type `class`, **followed** by what we want to **name** our `class`, **followed** by **colons**. Here's what that looks like:

```Python
class ClassName:
    # Class attributes/constants  
    # Instance attributes
    # Constructor __init__
    # Instance and Class methods
```
Now that we know how to **define** a `class` let's talk about what goes **into** the `class`. 

## `__init__` Constructor 

In order to properly **instantiate**(create) objects of our `class` (hence where **OOP** comes from), we're going to need to **define** a method called `__init__`. This method is known as a **constructor** and it allows us to initialize **object attributes** and perform any setup required for our `class`. Here's what that looks likes:

```Python
class VideoGame:
    # Class attributes/constants

    def __init__(self, game, system)
        # Instance attributes

    # Instance and Class methods
```

In the example above, I created a `class` called `VideoGame` which we're going to build upon. 

`self` is referring to the [*instance*](https://towardsdatascience.com/practical-python-class-vs-instance-variables-431fd16430d) of our `class`. Meaning it's referring to the **instance object** we're creating. 

`game` and `system` are [*instance attributes*](https://www.ibm.com/docs/en/spss-modeler/saas?topic=programming-defining-class-attributes-methods). These are **variables** that are bound to the **instance object** itself. We'll dive deeper into this in the next section. 

## Instance Attributes

Now that we've defined our `class` and added our `__init__` method, we can start adding some **instance attributes**. Here's what that looks like:

```Python
class VideoGame:
    # Class attributes/constants

    def __init__(self, game, system):
        self.game = game
        self.system = system 

    # Instance and Class methods
```

When we **instantiate** an object to our `class`, we're going to pass in **parameters** to match our **instance attributes**. So in this case, we would pass 2 parameters (`game` and `system`) which will be **bound** to that **instance object**. **Instantiating** an object looks something like this:

```bash
# instantiate 
game1 = VideoGame("TOTK", "Switch")

# view our game object
game1
# => <__main__.VideoGame object at 0x7f39496ae390>
game1.game
# => 'TOTK'
game1.system
# => 'Switch'
```
Here, we call `VideoGame` and pass in our 2 parameters which **correlate** to `game` and `system`. We assign this to a **variable** so we can access it's **elements**. 

When we type `game1`, it returns the **instance object**. We can change this by using the function `__repr__` to return something that human's can **actually understand**. I will cover this in the instance/class methods section. 

When we type `game1.game` and `game1.system`, it returns our object's `game` and `system` that we **instantiated** respectively.

## Class Attributes
Before we move on, let's talk about **class attributes**. These refer to **variables** that are shared between all **instance object's** of the `class`. This **differs** from **instance attributes** which are **accessed** only by the **instance objects**. Let's add a **class variable** to our `class`:

```Python
class VideoGame:
    games = []

    def __init__(self, game, system):
        self.game = game
        self.system = system 

    # Instance and Class methods
```

Here I made an empty list for the **entire** `class`. Let's add our **instance objects** to this list for future reference:

```Python
class VideoGame:
    games_list = []

    def __init__(self, game, system):
        self.game = game
        self.system = system 
        VideoGame.games_list.append(self)

    # Instance and Class methods
```

Now everytime we **instantiate** a new object, it'll be added to `games_list`. We can call `games_list` by typing `VideoGame.games_list` which will return a list of all **instance objects** in `VideoGame`. Right now it'll look something like this until we add the `__repr__` function:

```bash
# instantiate object
game1 = VideoGame("TOTK", "Switch")

VideoGame.games_list
# => [<__main__.VideoGame object at 0x7f555c9467d0>]
```
## Class Constants
**Class constants** are almost identical to **class attributes**. The **key differences** are that **class constants** are used to store data that **doesn't change** and the syntax used to define them requires **all uppercase**. Let's say for example our `class` only takes *3 options* for systems; `Xbox`, `Playstation`, `Switch`. If the user tries to add a game from **any other system** it **won't** add it unless it's **from our 3**. Let's define a **class constant** with these systems and we'll write out our `check_systems` method later in class methods:

```Python
class VideoGame:
    # SYSTEMS_LIST = ["Xbox", "Playstation", "Switch"]
    games_list = []

    def __init__(self, game, system):
        # if self.check_system(system)
        self.game = game
        self.system = system 
        VideoGame.games_list.append(self)

    # Instance and Class methods
```

Let's move on to methods.
 
## Instance methods

**Instance methods** are basically **functions** that you can call on any **object** within the `class`. Let's write 2 **instance methods** that will start our game and close our game:

```Python
class VideoGame:
    games_list = []

    def __init__(self, game, system):
        self.game = game
        self.system = system
        VideoGame.games_list.append(self)
    
    def open_game(self):
        return f"Booting {self.game} on the {self.system}"
    
    def close_game(self):
        return f"Closing {self.game} on the {self.system}"
```

Let's see what happens when we **call** these **methods** on a **instance object**:

```Python
# Instantiate object
game1 = VideoGame("TOTK", "Switch")

# Call our methods
game1.open_game()
# => 'Booting TOTK on the Switch'
game1.close_game()
# => 'Closing TOTK on the Switch'
```

After calling our **instance methods**(functions) they run on our **instance objects**. Our **instance methods** are very basic and just return an object's `game` and `system` however, these **instance methods** work like **functions**, which means they can be much **more complex** and take in **additional parameters**. 

## Class methods 
**Class methods** are extremely similar to **instance methods**. The **key differences** are **class methods** refer to the `class` and not the **instance object**; also the syntax is different. If you recall our **class constant** from earlier `SYSTEMS_LIST` we can add a **class method** that checks if our object's `system` matches a `system` in our list. If it **matches**, we add the game to our list, otherwise we won't:
```Python
class VideoGame:
    SYSTEMS_LIST = ["Xbox", "Playstation", "Switch"]
    games_list = []

    def __init__(self, game, system):
        if self.check_system(system):
            self.game = game
            self.system = system
            VideoGame.games_list.append(self)

    # instance methods

    @classmethod
    def check_system(cls, system):
        return system in cls.SYSTEMS_LIST
```
To define a **class method** we first need to include `@classmethod` above our function. Instead of `self` (used for **instance methods**) we use `cls` (refers to the `class`) in our **parameters**. We also passed `system` from our **instance object** to see if it's in our list. If done correctly only objects with systems from our **list** will added:
```bash
# Instantiate objects
game1 = VideoGame("TOTK", "Switch")
game2 = VideoGame("MH World", "Playstation")
game3 = VideoGame("Metal Gear Rising", "PC")

VideoGame.games_list
# => [<__main__.VideoGame object at 0x7f8790df6e50>, <__main__.VideoGame object at 0x7f8790df6e90>] 
```
## `__repr__` Function
Okay, let's finally talk about `__repr__`. In the code above I have **no idea** what `<__main__.VideoGame object at 0x7f8790df6e50>` points too. If you do, that's great, but let's **translate** this into something regular humans can **read**. The syntax is very close to the `__int__` method: 
```python 
class VideoGame:
    # Class constants
    # Class variables
    def __init__(self, game, system):
        if self.check_system(system):
            self.game = game
            self.system = system
            VideoGame.games_list.append(self)

    def __repr__(self):
        # '+\' combines our lines together
        return f"Game(game={self.game}, " +\
            f"system={self.system})"

    # Instance methods
    # Class Methods
```
The **parameter** we pass is `self` and the function will return a string with our **instance object's** `game` and `system`. Now instead of returning the **instance object** itself we get something that's **readable**: 
```bash
# Instantiate objects
game1 = VideoGame("TOTK", "Switch")
game2 = VideoGame("MHW", "Playstation")
game3 = VideoGame("Metal Gear Rising", "PC")

VideoGame.games_list
# => [Game(game=TOTK, system=Switch), Game(game=MHW, system=Playstation)]
```
![satisfaction meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n8ymiy089j5jw7g90p8v.jpg)
## Conclusion
**OOP** can be **daunting** at first, with all it's new **vocabulary**. In **theory** everything sounds so simple but in **practice** it's a whole nother story. I hope this read helped you out a little bit to better understand **OOP in Python**. If you want to see a **short summary** of everything you can read the **TLDR** section. Happy coding!

## TLDR

### ~Key Vocab~

> **Python class:**
*a blueprint or template for creating objects (instances).*
**Instance Object:**
*a specific occurrence of a class.*
**Instantiate:**
*another word for create.*
**Class Attributes:**
*varibales bound to the class. Accessible to all instances.*
**Class Constants:**
*varibales bound to the class(data cannot be changed).*
**Constructor `__init__`:**
*used to initialize objects attributes and perform other set up.*
**Instance Attributes:**
*variables bound to the instance.*
**`__repr__` Function:**
*a function that makes instance objects readable.*
**Instance Methods:**
*functions that are bond to the instance(takes `self` as argument).*
**Class methods:**
*functions that are bond to the class(takes `cls` as argument).*

###~Example~
```Python
# Python class
class VideoGame:
    # Class Constants
    SYSTEMS_LIST = ["Xbox", "Playstation", "Switch"]
    # Class Attributes (All instance objects in class can access)
    games_list = []
    
    # Constructor (initialize Object Attributes and other set up) 
    def __init__(self, game, system):
        # Refers to Class Method check_system
        if self.check_system(system):

            # Instance Attributes
            self.game = game
            self.system = system

            #adds to game_list Class Attribute
            VideoGame.games_list.append(self)
     
    # Makes Instance Object's readable
    def __repr__(self):
        return f"Game(game={self.game}, " +\
            f"system={self.system})"
    
    # Instance Methods
    def open_game(self):
        return f"Booting {self.game} on the {self.system}"
    
    def close_game(self):
        return f"Closing {self.game} on the {self.system}"
    
    # Class methods 
    @classmethod
    def check_system(cls, system):
        return system in cls.SYSTEMS_LIST

# Instantiate objects
game1 = VideoGame("TOTK", "Switch")
game2 = VideoGame("MHW", "Playstation")

# Omitted from game_list due to system (refer to check system)
game3 = VideoGame("Metal Gear Rising", "PC")
```
##Sources
- https://www.freecodecamp.org/news/an-introduction-to-programming-paradigms/
- https://www.freecodecamp.org/news/what-is-object-oriented-programming/
- https://towardsdatascience.com/practical-python-class-vs-instance-variables-431fd16430d
- https://www.ibm.com/docs/en/spss-modeler/saas?topic=programming-defining-class-attributes-methods
- https://www.programiz.com/python-programming/methods/built-in/classmethod#:~:text=A%20class%20method%20is%20a,just%20deals%20with%20the%20parameters
- https://python-course.eu/oop/class-instance-attributes.php
- https://www.programiz.com/python-programming/methods/built-in/repr
- https://betterdatascience.com/python-constants/