Introduction to object-oriented programming (OOP)#
What you will learn in this lesson:
Classes and objects
Attributes and methods
Inheritance
Introduction#
Python is an object-oriented programming (OOP) language. The object-oriented programming is kind of paradigm that helps you group state (attributes) and behavior (methods) together in handy packets of functionality. It also allows for useful specialized mechanisms such as inheritance.
The fact that python is an OOP language does not mean you are force to follow this paradigm (unlike some other OOP languages). You can still do procedural programming as we have learned so far, with modules and functions. In Python, you can select the paradigm that fits best for your your purposes, or even mix paradigms.
OOP is about grouping DATA with the FUNCTIONS that manipulate that data and hiding HOW it manipulates it so you can MODIFY the behavior through INHERITANCE.
Advantages of OOP programming:
MAINTAINABILITY. Object-oriented programming methods make code more maintainable. Identifying the source of errors is easier because objects are self-contained.
REUSABILITY. Because objects contain both data and methods that act on data, objects can be thought of as self-contained black boxes. This feature makes it easy to reuse code in new systems.Messages provide a predefined interface to an object’s data and functionality. With this interface, an object can be used in any context.
SCALABILITY. Object-oriented programs are also scalable. As an object’s interface provides a road map for reusing the object in new software, and provides all the information needed to replace the object without affecting other code. This way aging code can be replaced with faster algorithms and newer technology.
Classes and Objects#
You’ve heard this phrase quite a few times throughout the course. But what is an object?
An object is like a special kind of data structure (like list, dictionaries, etc) that not only stores data (attributes) but also has functions (methods) that can do things with that data.
Objects arise from classes. What is a class?
Classes are much like a template or scheme for creating objects. It defines what data (attributes) the objects will store and what actions (methods) they can perform.
Defining a class#
The syntax to define a class is the following:
class NameOfTheClass(object):
Attributes
...
...
Methods
Let’s break this syntax down a bit:
We start with the keyword
class
to let Python now we are going to define a class.Then we provide a name for the class. (Note: Pythonists like to use CamelCase naming convention for this…)
A parenthesis specifying the inheritance of the class. Here we used
object
, which is the most basic class that all other classes inherit from. If no inheritence is specified, stick withobject
. We will come to inheritance later.We end the header of the definition with a
:
, to specify that whatever comes next belongs to the class.After that, in an indented block, we include the attributes and methods of the class.
# Example
class Animal(object):
pass
Instances / Objects are actual, concrete implementations of a class with specific values.
my_animal = Animal() # Creating a Animal object
print(type(my_animal)) # Show that it's a Animal object
<class '__main__.Animal'>
Attributes and methods#
What are attributes?
Attributes are the values/fields specific to each instance (or object) of a class.
class Animal(object):
age = 11 #years
weight = 5 # kgs
name = "Kenny"
wild = False
Therefore, you may think of classes as a kind of data structure (e.g. Dictionaries) to store information.
Once you define an object, you can access its attributes using append a .
and calling its specific attribute:
my_animal = Animal() # Creating a Animal object
print(my_animal.age)
print(my_animal.name)
11
Kenny
And what are methods?
Methods are functions available within and specific to a class. Methods may consume attributes or other parameters.
class Animal(object):
age = 11 #years
weight = 5 # kgs
name = "Kenny"
wild = False
def greet(self):
print(f"Greetings, human. I am an animal.")
So the difference with typical data structures is that classes can also implement methods, which can perform an specific operation.
Once you define an object, you can again use one of its methods by appending a .
to the name of the object and calling its specific method.
Methods are just functions, so therefore they should follow their syntax to call them:
animal = Animal()
animal.greet()
Greetings, human. I am an animal.
Did we see this kind of behavior of using a .
to call methods before?
Yes! For example:
my_string = "hello"
my_string.upper()
'HELLO'
my_dict = {"age": 11, "name": "Kenny", "wild": False}
my_dict.values()
dict_values([11, 'Kenny', False])
Reiterating, this is because EVERYTHING DEFINED IN PYTHON IS AN OBJECT.
Finally, methods can use the information stored in the attributes:
class Animal(object):
age = 11 #years
weight = 5 # kgs
name = "Kenny"
wild = False
def greet(self): # Do not worry about the 'self' here
print(f"Greetings, human. I am an animal. My name is {self.name}. I am {self.age} years old and I weight {self.weight} kgs.")
animal = Animal()
animal.greet()
Greetings, human. I am an animal. My name is Kenny. I am 11 years old and I weight 5 kgs.
Initialize classes#
Ok, so classes can save information in the form of attributes, and also implement methods aimed at performing specific operations.
We also mentioned that classes are a template for creating specific kinds of objects (e.g. Animals).
However, in our previous example, our animal was too specific. What if we want an animal with a different set of attributes (e.g. a different name)?
We could redefine the class with the new attribute values:
class Animal(object):
age = 11 #years
weight = 5 # kgs
name = "Firulais"
wild = False
def greet(self): # Do not worry about the 'self' here
print(f"Greetings, human. I am an animal. My name is {self.name}. I am {self.age} years old and I weight {self.weight} kgs.")
new_animal = Animal()
new_animal.greet()
Greetings, human. I am an animal. My name is Firulais. I am 11 years old and I weight 5 kgs.
Obviously, this is pretty inefficient when you want to scalate things, lacking of reusability. And reusability was one of the advantages of classes.
How could we redefine our new attribute values without having to redefine our whole class again?
We could use the built-in method __setattr__
, which all classes in Python have, to set a new attribute value.
animal.greet()
animal.__setattr__("name", "Firulais")
animal.greet()
Greetings, human. I am an animal. My name is Kenny. I am 11 years old and I weight 5 kgs.
Greetings, human. I am an animal. My name is Firulais. I am 11 years old and I weight 5 kgs.
(N.B. This function can be also used to add new attributes)
new_animal.__setattr__("eyes", "blue")
new_animal.eyes
'blue'
But this changes the original animal we created. What if we want to have separate animals, each with its own unique attribute values?
We can do this when we instantiate the class—that is, when we create our objects. For that, we need to specify which attributes in the class should be initialized. This is done including the __init__
method in the class definition as follows:
class Animal(object):
def __init__(self, # Do not worry about the 'self' here
age,
weight,
name,
wild
):
self.age = age
self.weight = weight
self.name = name
self.wild = wild
def greet(self): # Do not worry about the 'self' here
print(f"Greetings, human. I am an animal. My name is {self.name}. I am {self.age} years old and I weight {self.weight} kgs.")
animal_kenny = Animal(age=11, weight=5, name = "Kenny", wild = False)
animal_firulais = Animal(age = 2, weight = 2, name = "Firulais", wild = True)
print(f"This animal is named: {animal_kenny.name}")
animal_kenny.greet()
print(f"This animal is named: {animal_firulais.name}")
animal_firulais.greet()
This animal is named: Kenny
Greetings, human. I am an animal. My name is Kenny. I am 11 years old and I weight 5 kgs.
This animal is named: Firulais
Greetings, human. I am an animal. My name is Firulais. I am 2 years old and I weight 2 kgs.
As you can see, we can create as many objects of a specific class as we want, each with its own attribute values, because we now have the template (the class) to do this!
Try it yourself!: Create your own Animal object and assigning values to its attributes when we define it, as we’ve just done.
# Your answer here
We can also define classes that take initial attributes by default. We just need to specify these values in the __init__
function (N.B. In the end, __init__
is a function, so it follows the same rules when it comes to parameter definition and order).
class Animal(object):
def __init__(self,
age = 1,
weight = 2,
name = "Max",
wild = True
):
self.age = age
self.weight = weight
self.name = name
self.wild = wild
def greet(self): # Do not worry about the 'self' here
print(f"Greetings, human. I am an animal. My name is {self.name}. I am {self.age} years old and I weight {self.weight} kgs.")
another_animal = Animal(age = 12)
another_animal.greet()
Greetings, human. I am an animal. My name is Max. I am 12 years old and I weight 2 kgs.
What is self
?#
self
is a way for an object to refer to itself. When you create an object from a class, that object needs a way to access its own data (attributes) and perform its own actions (methods).
Think of it like this: when you’re talking about yourself, you use the word ‘I’ or ‘me’. In Python, self works the same way for objects—it’s how the object refers to itself.
In the Animal
example, when we write self.name, it means “the name of this specific animal”. So if we create an animal like buddy = Animal(name="Buddy")
, the self.name
for that object is “Buddy”.
self
is only needed inside the class definition to refer to the specific object you're working with. When you're defining methods inside the class, self
is how the object refers to its own attributes and methods.
When you're using the object outside of the class, you don’t need to include self. The object already knows how to refer to itself.
buddy = Animal(name="Buddy")
buddy.greet() # You don’t need to use 'self' here
Inheritance#
What if we want a new template (class) that is just an extension of a previous one, to make it more specific, for example?
Do we need to redefine everything from scratch? NO! This is where classes become really handy. You can make them inherit from other (parent) classes. This allows us to create a new (child) class that takes on the properties (attributes) and behaviors (methods) of an existing (parent) class, while also allowing us to add new features or modify existing ones.
To specify inheritance in class, we write the parent class name in parentheses after the new (child) class name in the class definition:
# Here Dog will inherit from Animal
class Dog(Animal):
pass
Remember when we included object
in our class definition? That’s because all classes in Python, by default, inherit from object. While it’s not strictly necessary to specify this inheritance, it’s considered good practice to include it.
my_dog = Dog(age = 5, weight=10, name="Mordisquitos")
my_dog.greet()
Greetings, human. I am an animal. My name is Mordisquitos. I am 5 years old and I weight 10 kgs.
Mmm, but here, when the dog salutes us, it says it is an animal, which is true, but we may want it to be more specific. We can do this by redefining its greet
method:
# Here Dog will inherit from Animal, and redefine its greet method
class Dog(Animal):
def greet(self):
print(f"Greetings, human. I am a dog. My name is {self.name}. I am {self.age} years old and I weight {self.weight} kgs.")
my_dog = Dog(age = 5, weight=10, name="Pretzels")
my_dog.greet()
Greetings, human. I am a dog. My name is Pretzels. I am 5 years old and I weight 10 kgs.
Note that this has only affected the greet
method in the Dog class. The Animal class still has the original greet
method
my_animal = Animal(age = 5, weight=10, name="Pretzels")
my_animal.greet()
Greetings, human. I am an animal. My name is Pretzels. I am 5 years old and I weight 10 kgs.
Finally, our “child” class may have their own specific methods too, in addition to the ones they inherit from its parent(s):
# Here Dog will inherit from Animal, redefine its greet method and add a new method call "bark"
class Dog(Animal):
def greet(self):
print(f"Greetings, human. I am a dog. My name is {self.name}. I am {self.age} years old and I weight {self.weight} kgs.")
# This is a new method we add to this class
def bark(self):
print("wow! woof! bow-wow!")
my_dog = Dog(age = 5, weight=10, name="Pretzels")
my_dog.bark()
wow! woof! bow-wow!
This method belongs ONLY to the Dog class. Therefore, the following will give an error:
my_dog = Animal(age = 5, weight=10, name="Pretzels")
my_dog.bark()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[27], line 2
1 my_dog = Animal(age = 5, weight=10, name="Pretzels")
----> 2 my_dog.bark()
AttributeError: 'Animal' object has no attribute 'bark'
Practice excersises#
Let’s create our recipe for a terminator!
1- Define a class that can be initilize to a given terminator name (e.g. T-800). As a second attribute, define the name of the person this terminator is supposed to terminate (e.g. John Connor).
2- Now, this class should have a method ask_and_decide
, which, given an input name of person, it decides whether to terminate that person. Show this decision as print message (Hint: You can use the second attribute and if/else
for this).
3-Test the class by creating an object with a specific terminator and target, and then call ask_and_decide
with different names to see how the terminator behaves.
# Your answers from here
As a Cyberdyne Systems engineer, you’ve been tasked with developing a new, more advanced terminator. This new terminator should extend the functionalities of the one you created in the previous exercise by adding a new ability. This could be anything you like—be creative! You can also add new attributes if you’d like.
Remember to use inheritance for this exercise to build on the existing terminator class.
# Your answers from here