In Python 3, everything is an object! We can examine what kind of object using the type() function. We've seen examples of this with built-in data types like booleans, strings, and integers. In this chapter, we'll create our own data types, in a way, using classes. Any instances of that class that we create will have a type equal to that class.

What is Object Oriented Programming?

Object oriented programming is a method of programming that attempts to model some process or thing in the world as a class or object. Conceptually, you can think of a class or object as something that has data and can perform operations on that data. With object oriented programming, the goal is to encapsulate your code into logical groupings using classes so that you can reason about your code at a higher level. Before we get ahead of ourselves, though, let's define some terminology and see an example:

Class - A blueprint for objects. Classes can contain methods and properties. We commonly use classes to reduce code duplication.

Instance - objects that are made from a class by calling the class. Instances share a similar structure because they all come from the same blueprint (i.e. class).

Poker Example

Say we want to model a game of poker in our program. We could write the program using a list to represent the deck of cards, and then other lists to represent what each player has in their hand. Then we'd have to write a lot of functions to do things like deal, draw cards, see who wins, etc.

When you end up writing large functions and lots of code in one file, there is usually a better way to organize your code. Instead of trying to do everything at once, we could separate concerns.

When thinking about a game of poker, some larger processes and objects stand out that you will want to capture in your code:

Card

Deck of cards

Poker hand

Poker game

Discard pile (maybe)

Player

Bets

Each one of these components could be a class in your program. Let's pick one of the potential classes and figure out the data that it will hold and the functions that it should be able to perform:

Deck of cards

Cards - the deck should have 52 different playing cards

Shuffle - the deck should be able to shuffle itself

Deal a card - remove a card from the deck and deal it to a player

Deal a hand - The deck may also be used to deal a hand to a player, or a set of players

Now that we can conceptualize how a problem can be broken down into classes, let's talk about why programming this way can be useful.

Encapsulation

Encapsulation is the idea that data and processes on that data are owned by a class. Other functions or classes outside of that class should not be able to directly change the data.

In our deck class, we have 52 cards. The player class should not be able to choose any card he or she wants from the deck or change the order of a deck manually. Instead a player can only be dealt a hand. The contents of the deck is said to be encapsulated into the deck class because the deck owns the list of cards and it will not allow other classes to access it directly.

Abstraction

Abstraction is the result of a good object oriented design. Rather than thinking about the details of how a class works internally, you can think about it at a higher level. You can see all of the functions that are made available by the class and understand what the class does without having to see all of the code or worry about implementation details.

Continuing with our example, if you had a deck of cards class and you saw that you could call the .shuffle() function or the .deal() function, you would have a good understanding of what the class does without having to understand how the functions are working internally.

Other hallmarks of object oriented programming include inheritance and polymorphism. We'll discuss these later.

Creating a class

Every class needs an __init__ method. Every time you create an instance from a class in Python, that instance will get run through the __init__ method. self inside of this method refers to the instance.

To create an instance from a class, we instantiate the class using () and pass in the values to initialize the instance (these are defined in __init__).

v = Vehicle('toyota', 'corolla', 2012)

Methods and properties for instances

To add methods on instances, we simply define them in the class. When defining instance methods, the first argument should always be called self, and refers to the current instance. If you need to pass other arguments, do so after passing in self. Note that when you call these instance methods, you never pass in self. Here are a couple of examples:

Notice that we must add self as the first parameter to each of our instance methods.

Writing Class Methods

We've seen how to add methods on instances. But what if we want to add a method on the class itself? To do this, we use a decorator! We'll talk more about decorators later. For now, it's enough to know that there are two decorators we can use to create class methods:

Similar to instance methods, the first argument in a class method has special meaning. For an instance method, the first argument refers to the instance, and is called self. For a class method, the first argument refers to the class, and is typically written as cls.