This chapter will give you an overall taste of Object-Oriented Programming. All these topics will be covered later in this book.
We'll start by introducing some terminology and then dive into some examples.
The Object-Oriented Programming (OOP) paradigm was created to deal with the growing complexity of large software systems. Programmers discovered early on that large and complex programs became challenging to maintain. One small change at any point in the program would trigger a ripple effect of errors due to dependencies throughout the program.
Programmers needed a way to create containers for data that could be changed and manipulated without affecting the entire program. They needed a way to section off areas of code that performed specific procedures so that their programs could become the interaction of many small parts as opposed to one massive blob of dependency.
Enter OOP.
Classes form the basis of Python's type system. You can think of classes as blueprints, templates, or molds from which we create objects of a given type. A class, by default, can create many different objects of the same type.
Every class defines a type, and every type has a class. These two terms are so fundamentally similar that we often use them interchangeably.
Objects are the individual things created by classes. You can think of objects as the values for a given type. For instance, the Python str
class lets us create string objects, so those strings are the values for the string type. Again, these two ideas are so similar that we can use them interchangeably. However, object is the preferred term in OOP.
Objects are also called instances or instance objects. When we create a new object from a class, we say that we created a new class instance. Once again, separating objects from instances is hard, so don't try. Use whichever term fits the situation best.
Everything we've used to represent data, from strings to integers to lists to dictionaries, are, in fact, objects. Even functions and classes themselves are objects. We'll dig deeper into this soon.
If you hang around Python people for a few days, you'll soon hear the phrase, "Everything in Python is an object!" That's not true, though; Not everything in Python is an object. However, anything that can be said to have a value is an object. That includes numbers, strings, lists, functions, modules, and even classes. However, a few things are not objects: statements, keywords, and variables are three that stand out.
We typically use nouns when discussing classes and objects. Classes and objects represent concrete (non-abstract) things. Many, but not all, methods are named with verbs; they represent actions and behaviors. The objects we create from classes are specific instances of the class nouns, and we manipulate them using the verbs provided by the method names.
Instantiation is the process of creating a new object. It's a fancy word for a fundamentally simple idea, but it makes you sound smart. Okay, that's not the real reason we use the term; instantiation is more specific than creation. However, in Python, where nearly everything is an object, there is little difference between the two.
Hiding data and functionality from the rest of the code base is known as encapsulation. It's a form of data protection; data can't be manipulated or changed without obvious intent. It defines the boundaries in your application and lets your code achieve new levels of complexity. Like many other OO languages, Python accomplishes this with objects and interfaces (i.e., methods) that interact with them.
It's worth noting that Python doesn't really hide anything. At best, it hides by convention. That is, it relies on naming conventions and the good sense of programmers that use our classes. We'll learn more about this later.
At Launch School, we'll usually pretend that hiding by convention in Python is the same as hiding for encapsulation purposes. We'll let you know when we step outside this box.
Another benefit of encapsulation is that it lets the programmer think about things with a new level of abstraction. Objects are represented as real-world nouns and can be given methods that describe the behavior provided for those objects.
Polymorphism is the ability for different data types to respond to the same interface. Suppose we have a function that invokes a move
method on one of the function's arguments. We can pass the function any argument with a compatible move
method. The object might represent a human, a cat, a jellyfish, or, conceivably, even a car or planet. The function is polymorphic.
Python has dozens of polymorphic functions and methods. For instance, the oft-used list
function can take any iterable object as an argument: tuples, ranges, sets, dictionaries, and even other lists.
OOP gives us the flexibility to use pre-written code for new purposes.
The term "polymorphism" comes from the Greek words "poly" meaning "many", and "morph" meaning "form".
In inheritance, a class can acquire (inherit) all the behaviors and properties (we'll talk about properties shortly) of another class. This lets programmers define small, reusable classes and smaller, more specific classes for fine-grained, detailed behaviors.
In inheritance, the inheriting class is called a subclass of the inherited class. The inherited class is, in turn, called a superclass of the subclass.
The superclass/subclass relationship is akin to an ancestral relationship rather than a parent/child relationship. It's multigenerational. Thus, if class A subclasses B, class B subclasses C, and class C subclasses D, then B, C, and D are all superclasses of A. Likewise, A is a subclass of all 3 of the superclasses.
We'll learn more about these concepts in the following chapters.
Python classes describe the characteristics of its objects. Classes provide a blueprint of the information its instances store and what those objects can do. To define a class, we use syntax similar to defining a function. We replace def
with class
and use the PascalCase naming convention to define the class:
class GoodDog:
pass
sparky = GoodDog()
In that small example, we defined a GoodDog
class and then created an object of the class by calling GoodDog()
.
The GoodDog
class doesn't have any data or behaviors yet. However, we still need to include something in the class's block. The pass
statement is perfect; it does nothing. However, as a side effect, it silences Python's complaints.
Our GoodDog
object is now assigned to the sparky
variable. We say that sparky
references an object or instance of class GoodDog
. We can also say that we've instantiated an instance of the GoodDog
class.
One critical thing to observe is that we instantiate objects by calling a function with the same name as the class. It's as though GoodDog
is both a class and a function. (It is not a dessert topping or a floor wax, though.) We call this function the class constructor.
Most objects have data. Collectively, the data inside an object defines its state. An object's state is given by its instance variables, which store the object's data. These variables can be initialized, accessed, replaced, or mutated through the class's instance methods and from outside the class (we'll see this later).
A class's instance methods are functions that operate on instances of the class. Instance methods are shared by all class instances. The instance methods are often called behaviors.
In Python, the terms instance variable, attribute, and property sometimes get used interchangeably. However, they are different and shouldn't be misused:
Let's define a class that has state and behaviors:
class GoodDog:
def __init__(self, name):
# self.name is an instance variable (state)
self.name = name
print(f'Constructor for {self.name}')
# speak is an instance method (behavior)
def speak(self):
# We're using the self.name instance variable
print(f'{self.name} says Woof!')
# roll_over is an instance method (behavior)
def roll_over(self):
# We're using the self.name instance variable
print(f'{self.name} is rolling over.')
sparky = GoodDog('Sparky') # Constructor for Sparky
sparky.speak() # Sparky says Woof!
sparky.roll_over() # Sparky is rolling over.
rover = GoodDog('Rover') # Constructor for Rover
rover.speak() # Rover says Woof!
rover.roll_over() # Rover is rolling
Our updated class now has 3 methods:
__init__
is a magic method (also: dunder method) in Python. It's properly called the initializer method, the instance constructor, or the constructor. It initializes a new instance of an object.
Magic methods are any methods whose name begins and ends with a double underscore. We'll cover them in more detail later.
The speak
and roll_over
methods tell a GoodDog
instance to speak or roll over.
Note the distinction between class constructors and instance constructors. The class constructor, such as GoodDog()
, orchestrates the instantiation of an instance object. It first calls the static method __new__
to create an instance object of the class. The uninitialized object is then passed to the __init__
instance constructor where it gets initialized. (Confusingly, __new__
is sometimes called a constructor, but we won't do that.)
For clarity going forward, we'll use the following terminology:
GoodDog()
, list()
, range()
, etc.
__new__
for the __new__
method.
__init__
method.
We try to use this terminology consistently in this book and the remainder of the Python track courses.
The first parameter for all instance methods is conventionally named self
. It represents the object that invoked the method. For instance, when we call sparky.speak
on line 19, self
refers to the sparky
object. We use self.identifier
inside the method's body to create and access instance variables (identifier
is the name of the instance variable).
The above example shows several crucial aspects of classes and objects:
The following figure should help visualize these relationships:
As you can see, defining a class and creating a new class instance is straightforward. Sometimes, though, you might find yourself with several closely related classes whose instances have behaviors in common. Those behaviors can be identical across all or some of the classes, or they can be unique to a specific class. This is where inheritance comes in.
Consider these three classes:
class Dog:
def __init__(self, name):
self.name = name
type_name = type(self).__name__
print(f'I am {name}, a {type_name}.')
def speak(self):
print(f'{self.name} says Woof!')
def roll_over(self):
print(f'{self.name} is rolling over.')
class Cat:
def __init__(self, name):
self.name = name
type_name = type(self).__name__
print(f'I am {name}, a {type_name}.')
def speak(self):
print(f'{self.name} says Meow!')
class Parrot:
def __init__(self, name):
self.name = name
type_name = type(self).__name__
print(f'I am {name}, a {type_name}.')
def speak(self):
print(f'{self.name} wants a cracker!')
sparky = Dog('Sparky')
fluffy = Cat('Fluffy')
polly = Parrot('Polly')
sparky.roll_over()
for pet in [sparky, fluffy, polly]:
pet.speak()
I am Sparky, a Dog.
I am Fluffy, a Cat.
I am Polly, a Parrot.
Sparky is rolling over.
Sparky says Woof!
Fluffy says Meow!
Polly wants a cracker!
We have three classes in the above code, with a single object for each class. There are several things you may observe if you study this code and the output:
__init__
) method. That's a lot of duplicated code.
speak
method, each of which differs from the others. Given that, we can call the speak
method even if we don't know what kind of animal we have assigned to a variable. We can see this in action on the last 2 lines.
roll_over
method.
eat
behavior.
type(self).__name__
to programmatically determine the name of the class to which an object belongs.
Let's clean up this code. First, let's eliminate those duplicate constructors. We'll first create a Pet
class that contains the constructor's code, then let the Dog
, Cat
, and Parrot
classes inherit from Pet
:
#highlight
class Pet:
def __init__(self, name):
self.name = name
type_name = type(self).__name__
print(f'I am {name}, a {type_name}.')
#endhighlight
#highlight
class Dog(Pet):
# __init__ method removed
#endhighlight
def speak(self):
print(f'{self.name} says Woof!')
def roll_over(self):
print(f'{self.name} is rolling over.')
#highlight
class Cat(Pet):
# __init__ method removed
#endhighlight
def speak(self):
print(f'{self.name} says Meow!')
#highlight
class Parrot(Pet):
# __init__ method removed
#endhighlight
def speak(self):
print(f'{self.name} wants a cracker!')
sparky = Dog('Sparky')
fluffy = Cat('Fluffy')
polly = Parrot('Polly')
sparky.roll_over()
for pet in [sparky, fluffy, polly]:
pet.speak()
The output is unchanged, but the code has some significant changes. Of course, we've created the Pet
class and moved the __init__
out of the individual classes into the Pet
class. We've also added (Pet)
to each class name in the class
statements: this tells Python you want to inherit from the Pet
class. Pet
is the superclass of the other three classes, and the other three classes are subclasses of Pet
.
What does that mean for our program? That becomes apparent when you create an object from one of the subclasses. After allocating some memory for the instance, Python tries to call __init__
on the object's class. However, none of our subclasses has a __init__
method. Python persists! It looks to the superclass for a method with the same name. In this case, it finds __init__
in the Pet
class and invokes it. Pet.__init__
creates and initializes a self.name
instance variable in the new object and prints an informative message.
That informative message is identical to what you saw in the earlier version of this program. It includes the correct class name even though Python runs Pet.__init__
. The self
keyword makes that possible. We won't go into much detail right now, but self
always references the object used to call the method.
Let's get our poor, starving pets fed. Suppose we want a simple eat
method that looks like this:
def eat(self):
print(f"{self.name}: Yum-yum-yum!")
Where do you think we should put that method? We could, of course, put a copy in each subclass. However, we can use inheritance by adding it to the Pet
class:
class Pet:
def __init__(self, name):
self.name = name
type_name = type(self).__name__
print(f'I am {name}, a {type_name}.')
#highlight
def eat(self):
print(f"{self.name}: Yum-yum-yum!")
#endhighlight
# Code omitted for brevity
for pet in [sparky, fluffy, polly]:
pet.speak()
#highlight
pet.eat()
#endhighlight
Since none of our subclasses have an eat
method, Python calls Pet.eat
as it did with Pet.__init__
. The program now outputs:
I am Sparky, a Dog.
I am Fluffy, a Cat.
I am Polly, a Parrot.
Sparky is rolling over.
Sparky says Woof!
Sparky: Yum-yum-yum!
Fluffy says Meow!
Fluffy: Yum-yum-yum!
Polly wants a cracker!
Polly: Yum-yum-yum!
Ah, the magic of inheritance! We'll see much more about inheritance later in this book and again in the Core Curriculum.
By the way; we can rewrite type(self).__name__
as self.__class__.__name__
. type(self)
and self.__class__
return the same values. While type(self)
may be shorter, it's possible that self.__class__
may be clearer. Which you use is up to you. We'll use both in Core.
def __init__(self, name):
self.name = name
type_name = self.__class__.__name__
print(f'I am {name}, a {type_name}.')
That was a high-speed trip through OOP in Python. We'll spend the remaining chapters diving into the details. Lest you think we're going for a refreshing swim, OOP can be likened more to a mud bath. It's good for you, but you're going to get dirty.
How do we create a class and an object in Python?
Write a program that defines a class and creates two objects from that class. The class should have at least one instance variable that gets initialized by the initializer.
What class you create doesn't matter, provided it satisfies the above requirements.
We first need to define a class. We'll create a Person
class. It requires a __init__
method that initializes the Person
's name.
class Person:
def __init__(self, name):
self.name = name
bob = Person('Bob')
alice = Person('Alice')
Given an instance of a Foo
object, show two ways to print I am a Foo object
without hardcoding the word Foo
.
class Foo:
pass
foo = Foo()
print(f'I am a {type(foo).__name__} object')
print(f'I am a {foo.__class__.__name__} object')