# OOP & Classes

Object-oriented programming (OOP) allows us to structure and organize code by representing real-world objects or concepts as classes, which encapsulate data and behaviour.

This offers several advantages including code reusability, modularity, and abstraction. The latter allows you to hide implementation details and only expose relevant interfaces, making code easier to read and maintain.

#### Blueprints

All cars have things that make them a `Car`. Although the details might be different, every type of car has the same basics — it's based off the same blueprint, with the same properties and actions.

![Classes and Objects](https://s3.amazonaws.com/ga-instruction/assets/programming-fundamentals/class-objects-car.png)

* Property: A type (could be hatchback or sedan).
* Property: A color (could be red, black, blue, or silver).
* Property: Seats (could be between 2 and 7).
* Action: Can drive.
* Action: Can park.
* When we make a car, we can vary the values of these properties. Some cars have economy engines, some have performance engines. Some have four doors, others have two. However, they are all types of `Car`s.

#### Defining Classes

Class definitions are similar to function definitions, but instead of `def`, we use `class`.

Let's declare a class for `Dog`:

```python
class Dog:
    # We'll define the class here.
    # Our dog will have two attributes: name and age.
    pass
```

![Dog with name and age](https://s3.amazonaws.com/ga-instruction/assets/programming-fundamentals/class-dog-name-age.png)

**Pro tip:** While objects are named in snake\_case, classes are conventionally named in TitleCase.

#### The `__init__` Method

What first? Every class starts with an `__init__` method. It's:

* Where we define the class' variables.
* Short for "initialize." This is the constructor function.

```python
class Dog:
    def __init__(self, name, age):
        self.name = name  # All dogs have a name.
        self.age = age  # All dogs have an age.
```

*Note: `self` means "each individual object made from this class." Not every "dog" has the same name!*

* The first argument passed to the `__init__` function, `self`, is required when defining methods for classes. The `self` argument is a reference to a future instantiation of the class. In other words, `self` refers to each individual dog.
* This lets each object made from a class keep references to its own data and function members. Not every "dog" has the same attributes, so we want individual dogs to maintain their own attributes.

#### Adding a `bark_hello()` Method

```python
class Dog:
    def __init__(self, name, age):
        self.name = name  # All dogs have a name.
        self.age = age  # All dogs have an age.

    # All dogs have a bark_hello function.
    def bark_hello(self):
        print("Woof! I am called", self.name, "; I am", self.age, "human-years old.")
```

#### Instantiating Objects From Classes

Now we have a `Dog` blueprint!

Each `dog` object we make from this blueprint:

* Has a name.
* Has an age.
* Can bark.

#### How Do We Make a `Dog` Object?

We call our class name like we call a function — passing in arguments, which go to the `__init__` method of the class.

Add this under your class (non-indented!):

```python
# Declare the objects.
tonto = Dog("Tonto", 5)
scout = Dog("Scout", 7)
sailor = Dog("Sailor", 3)

# Test them out!
tonto.bark_hello()
print("This dog's name is", tonto.name)
print("This dog's age is", tonto.age)
scout.bark_hello()
sailor.bark_hello()
```
