Object Oriented Programming Quick Review

Object = Something with unique characteristics and functions

what is the point of object oriented programming?

Sometimes, when coding, we want to create many of the same type of object. Rather than defining the same type of thing over and over again, we can create what is called a class, which is sort of like a template for a particular object, and every time we want to create a new one of these objects, we can create what is called an instance of a class. This prevents us from having to write a lot of redundant code. Every class contains an init method, which is what is called whenever we create a new instance of a particular class.

class Car:
    num_wheels = 4
    def __init__(self, color, make):
        self.color = color
        self.make = make
    def drive(self):
        return 'vroom vroom'
    def park(self):
        if self.num_wheels == 4:
            return 'In between the white lines!'
        else:
            return 'Oof, you better find a new spot :('
    def paint(self):
        return 'Added new ' + self.color + ' paint'
jeep = Car('black', 'jeep')

some important definitions

Instance attribute = A variable that is specific to a particular instance of a class

  • In the car class, color and make are examples of instance attributes because they are not the same for all cars

Class attribute = A variable that is a part of the class, and is thus shared across all instances of a class

  • num_wheels is a class attribute because it is defined outside of the init method, and thus shared across all instances

Whenever we try to find the value of a particular attribute (such as jeep.color), we always look in the instance first (at instance attributes), then at the class (class attributes), then to any parent classes (if there is any inheritance, see more about that below).

Methods = Functions within a class

  • init, drive, park, and paint are methods of the Car class


dot notation

When we access methods and variables in object oriented programming, we use what’s called dot notation. Since we are now working with methods, not regular functions, we can no longer just say things like drive() and park() because we may be working with multiple classes that have a drive() or a park() method (and then which one would we choose???  Who knows?). To access any method inside of a class, you can always do class_name.method(parameters).

For example, we can do Car.drive(jeep), which will call Car’s drive method with jeep passed in as self.

Now you may be wondering, what is this self thing? Well, as a python convention, in a class we generally use self to refer to an instance of that particular class. That way, when we write self.some_attribute, we can get access to instance or class attributes associated with a particular instance. However, self is not a special keyword in python, and technically you could pass something that is not an instance of the current class in as self (such as the number 1), but depending on the body of the method, you may run into some errors if self doesn’t have all of the attributes that are requested in the method.  For example, what if we tried to get self.color and we had passed in 1 for self? We would get an error.

Since self is the variable we use to represent an instance of the class that a method is a part of, instead of doing class_name.method(parameters), we can also do instance_name.method(any params other than self), which will call the method inside instance_name’s class with instance_name as the self parameter. We can do this because an instance of a class has access to all of the methods of its class.

For example, if we wanted to access jeep’s color, we would say:

jeep.color

And if we wanted to call the drive method with jeep as self, we could say:

jeep.drive()

Notice that the parentheses after drive are empty because the drive method has no other parameters other than self. Also, note that if jeep was not an instance of Car and was instead an instance of another class, Car.drive(jeep) would not be the same as jeep.drive() because jeep’s class’s drive would not be Car’s drive method.


python’s magic methods

It seems weird that every time we create an instance of a class, like when we defined jeep above, we just “called” the Car class, and it just knew to call the init method. Also what’s up with the double underscores?

Turns out that python has a bunch of “magic methods”, written as __name__, that we’ve actually been working with since the very beginning of 61A. For each of these magic methods, there are shortcut ways to implicitly call them without writing out the full name with double underscores and everything. One example that we’ve used as a lot is __eq__, which is implicitly called every time we use the == operator.

In the case of __init__, we can call this implicitly by following a class name with parentheses (and passing in any parameters as needed).

There are two other magic methods that are important for 61A: _repr_ and _str_. The repr method of a class is implicitly called whenever we request an instance of that class. The str method of a class is implicitly called when we print an instance of that class. As with any of these methods, we can call them implicitly or explicitly, but repr and str perform slightly differently depending on whether the call is implicit or explicit. Let’s look at an example to see what I mean:

class Car:
    num_wheels = 4
    def __init__(self, color):
        self.color = color
    def __str__(self):
        return “Love that “ + self.color + “ car!”
    def __repr__(self):
        return “Car has “ + self.num_wheels + “ wheels”
jeep = Car(“powder blue”)
>>> jeep
Car has 4 wheels
>>> jeep.repr()
“Car has 4 wheels”
>>> print(jeep)
Love that powder blue car!
>>> jeep.str()
“Love that powder blue car!”

Since jeep is an instance of car, requesting the variable jeep will call the __repr__ method of the car class implicitly.  Note that when we explicitly call repr, there are quotes, but when we implicitly call it there are no quotes. This is because when we call repr implicitly, we are actually printing the result of calling repr, and printing gets rid of the quotes.  When we directly call it, there is no printing involved, so the quotes are still there.

Similarly, printing the variable jeep will call the __str__ method of the car class implicitly.  When we explicitly call str, there are quotes, but when we implicitly call it there are no quotes.  This is because when we call str implicitly, just as with repr, we are actually printing the result of calling str, and printing gets rid of the quotes.  When we directly call it, there is no printing involved, so the quotes are still there.


inheritance

Sometimes, we want to create an object that shares some attributes/methods with another existing object. Rather than creating a whole new object and copying a lot of code from our existing object, we can make our new object inherit from our existing object. This is very similar to how we worked with higher order/parent functions earlier in the class, because a child class basically has access to all of the attributes of its parent class.  To declare a child class, all you have to do is put the parent class name in parentheses after the child class name in the class definition.

For example, if we wanted to make a motorcycle class inherit from a car class we could do this:

class Motorcycle(Car):

Now, any instance of the Motorcycle class has access to all of the attributes and methods of the Car class, so if we create an instance of the Motorcycle class called yamaha, we can do things like yamaha.drive(). We can also choose to override some of the attributes and methods from the Car class by redefining them in the Motorcycle class. If we decide that we want Motorcycles to only have two wheels instead of four, we can define a num_wheels attribute in the Motorcycle class and set it equal to 2. If we do that yamaha.num_wheels will return 2, but jeep.num_wheels will still return 4.