In this lecture, we'll delve into the realm of "object-oriented programming," or OOP. This is a programming paradigm in which concepts and actions are "packaged" using the abstraction of objects: modeling the system after real-world phenomena, both to aid our own understanding of the program and to enforce a good design paradigm. By the end of this lecture, you should be able to:
Poll: how many people have programmed in Java or C++?
How many have heard of object-oriented programming?
Up until now (and after this lecture), we've stuck mainly with procedural programming: focusing on the actions.
Object-oriented programming, by contrast, focuses on the objects.
Sort of a "verbs" (procedural programming) versus "nouns" (object-oriented programming) thing.
Main idea: you design objects, usually modeled after real-world constructs, that interact with each other.
These designs are called classes. Think of them as a blueprint that detail out the various properties and capabilities of your object.
From the designs (classes), you can create an object by instantiating the class, or creating an instance of the class.
If the class is the blueprint, the object is the physical manifestation from the blueprint.
A car can have several properties--steering column, fuel injector, brake pads, airbags--but an instantiation of car would be a 2015 Honda Accord. Another instantiation would be a 2016 Tesla Model S. These are instances of a car.
In some [abstract] sense, these both derive from a common blueprint of a car, but their specific details differ. This is precisely how to think of the difference between classes and objects.
Every object in Python has certain things in common.
Let's start with the first step of designing a class: its actual definition. We'll stick with the car example.
In [1]:
class Car():
""" A simple representation of a car. """
pass
To define a new class, you need a class
keyword, followed by the name (in this case, Car
). The parentheses are important, but for now we'll leave them empty. Like loops and functions and conditionals, everything that belongs to the class--variables, methods, etc--are indented underneath.
We can then instantiate this class using the following:
In [2]:
my_car = Car()
print(my_car)
Now my_car
holds an instance of the Car
class! It doesn't do much, but it's a valid object.
The first step in making an interesting class is by creating a constructor. It's a special kind of function that provides a customized recipe for how an instance of that class is built.
It takes a special form, too:
In [5]:
class Car():
def __init__(self):
print("This is the constructor!")
In [6]:
my_car = Car()
Let's look at this method in more detail.
In [5]:
def __init__(self):
pass
def
is normal: the Python keyword we use to identify a function definition.__init__
is the name of our method. It's an interesting name for sure, and turns out this is a very specific name Python is looking for: whenever you instantiate an object, this is the method that's run. If you don't explicitly write a constructor, Python implicitly supplies a "default" one (where basically nothing really happens).self
, and why--if an argument is required--didn't we supply one when we executed my_car = Car()
?This is how the object refers to itself from inside the object. We'll see this in greater detail once we get to attributes.
Every method in a class must have self
as the first argument. Even though you don't actually supply this argument yourself when you call the method, it still has to be in the function definition.
Otherwise, you'll get some weird error messages:
Attributes are variables contained inside a class, and which take certain values when the class is instantiated.
The most common practice is to define these attributes within the constructor of the class.
In [2]:
class Car():
def __init__(self, year, make, model):
# All three of these are class attributes.
self.year = year
self.make = make
self.model = model
In [4]:
my_car = Car(2015, "Honda", "Accord") # Again, note that we don't specify something for "self" here.
print(my_car.year)
These attributes are accessible from anywhere inside the class, but direct access to them from outside (as did in the print(my_car.year)
statement) is heavily frowned upon.
Instead, good object-oriented design stipulates that these attributes be treated as private variables to the class.
To be modified or otherwise used, the classes should have public methods that expose very specific avenues for interaction with the class attributes.
This is the concept of encapsulation: restricting direct access to attributes, and instead encouraging the use of class methods to interact with the attributes in very specific ways.
Methods are functions attached to the class, but which are accessible from outside the class, and define the ways in which the instances of the class can interact with the outside world.
Whereas classes are usually nouns, the methods are typically the verbs. For example, what would a Car
class do?
In [7]:
class Car():
def __init__(self, year, make, model):
self.year = year
self.make = make
self.model = model
self.mileage = 0
def drive(self, mileage = 0):
if mileage == 0:
print("Driving!")
else:
self.mileage += mileage
print("Driven {} miles total.".format(self.mileage))
In [8]:
my_car = Car(2016, "Tesla", "Model S")
my_car.drive(100)
my_car.drive()
my_car.drive(50)
Classes can have as many methods as you want, named whatever you'd like (though usually named so they reflect their purpose).
Methods are what are ultimately allowed to edit the class attributes (the self.
variables), as per the concept of encapsulation. For example, the self.mileage
attribute in the previous example that stores the total mileage driven by that instance.
Like the constructor, all the class methods must have self
as the first argument in their headers, even though you don't explicitly supply it when you call the methods.
Inheritance is easily the most complicated aspect of object-oriented programming, but is most certainly where OOP derives its power for modular design.
When considering cars, certainly most are very similar and can be modeled effectively with one class, but eventually there are enough differences to necessitate the creation of a separate class. For example, a class for gas-powered cars and one for EVs.
But considering how much overlap they still share, it'd be highly redundant to make wholly separate classes for both.
In [ ]:
class GasCar():
def __init__(self, make, model, year, tank_size):
# Set up attributes.
pass
def drive(self, mileage = 0):
# Driving functionality.
pass
In [ ]:
class ElectricCar():
def __init__(self, make, model, year, battery_cycles):
# Set up attributes.
pass
def drive(self, mileage = 0):
# Driving functionality, probably identical to GasCar.
pass
Enter inheritance: the ability to create subclasses of existing classes that retain all the functionality of the parent, while requiring the implementation only of the things that differentiate the child from the parent.
In [12]:
class Car(): # Parent class.
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.mileage = 0
def drive(self, mileage = 0):
self.mileage += mileage
print("Driven {} miles.".format(self.mileage))
In [15]:
class EV(Car): # Child class--explicitly mentions "Car" as the parent!
def __init__(self, make, model, year, charge_range):
Car.__init__(self, make, model, year)
self.charge_range = charge_range
def charge_remaining(self):
if self.mileage < self.charge_range:
print("Still {} miles left.".format(self.charge_range - self.mileage))
else:
print("Battery depleted! Find a SuperCharger station.")
In [16]:
tesla = EV(2016, "Tesla", "Model S", 250)
tesla.drive(100)
tesla.charge_remaining()
tesla.drive(150)
tesla.charge_remaining()
Hopefully you noticed--we could call tesla.drive()
and it worked as it was defined in the parent Car
class, without us having to write it again!
This is the power of inheritance: every child class inherits all the functionality of the parent class.
With ONE exception: if you override a parent attribute or method in the child class, then that takes precedence.
In [23]:
class Hybrid(Car):
def drive(self, mileage, mpg):
self.mileage += mileage
print("Driven {} miles at {:.1f} MPG.".format(self.mileage, mpg))
In [24]:
hybrid = Hybrid(2015, "Toyota", "Prius")
hybrid.drive(100, 35.5)
Using inheritance, you can build an entire hierarchy of classes and subclasses, inheriting functionality where needed and overriding it where necessary.
This illustrates the concept of polymorphism (meaning "many forms"): all cars are vehicles; therefore, any functions a vehicle has, a car will also have.
All transporters are vehicles--and also cars--and have all the associated functionality defined in those classes.
However, it does NOT work in reverse: not all vehicles are motorcycles! Thus, as you move down the hierarchy, the objects become more specialized.
Just a quick note on this, for all the Java converts--
Python does support multiple inheritance, meaning a child class can directly inherit from multiple parent classes.
In [27]:
class DerivedClassName(EV, Hybrid):
pass
This can get very complicated (and is why Java restricts "multiple inheritance" to interfaces only) in terms of what method and attribute definitions takes precedence when found in multiple parent classes.
As such, we won't explore this very much if at all in this class.
1: Buzzword bingo: define encapsulation, inheritance, polymorphism, instantiation, and the difference between objects and classes.
2: For those who are Java converts, you may recall public
and private
methods and variables. Python makes no such distinction; everything is intrinsically public. In this case, why still use methods to interact with classes, instead of directly accessing the class attributes?
3: What is the difference between method overriding and method overloading? Does Python support one, both, or neither?
4: Class variable scope exists when working with objects in Python. If I define a variable x
outside a class, define x
again inside the class method, and refer to x
after the class definition, which x
is accessed? If x
is also an attribute of the class, how do you access it from outside the class? How can you access the x
defined outside the class from inside a class method?
5: Design a class hierarchy for different kinds of drinks. Include as much detail as you can. Where are attributes and methods inherited, where are they overridden, and where are new attributes and methods defined?
How is A4 going?
Volunteers for tomorrow's flipped lecture?
Review session #2 on Thursday! Come with questions!