Description: In this lecture, Dr. Bell continues the discussion of Object Oriented Programming in Python, with an emphasis on data control, inheritance, and subclasses.
Instructor: Dr. Ana Bell
The following content is provided under a Creative Commons license. Your support will help MIT OpenCourseWare continue to offer high-quality educational resources for free. To make a donation or view additional materials from hundreds of MIT courses, visit MIT OpenCourseWare at ocw.mit.edu.
ANA BELL: All right, everyone, let's get started. So today is going to be the second lecture on object-oriented programming. So just a quick recap of last time-- on Monday, we saw-- we were introduced to this idea of object-oriented programming, and we saw these things called abstract data types. And these abstract data types we implemented through Python classes. And they allowed us to create our own data types that sort of abstracted a general object of our choosing, right?
So we've used lists before, for example. But with abstract data types, we were able to create objects that were of our own types. We saw the coordinate example. And then at the end of the class, we saw the fraction example.
So today we're going to talk a little bit more about object-oriented programming and classes. We're going to see a few more examples. And we're going to talk about a few other nuances of classes, talk about information hiding and class variables. And in the second half of the lecture, we're going to talk about the idea of inheritance. So we're going to use object-oriented programming to simulate how real life works. So in real life, you have inheritance. And in object-oriented programming, you can also simulate that.
OK, so the first few slides are going to be a little bit of recap just to make sure that everyone's on the same page before I introduce a couple of new concepts related to classes. So recall that when-- in the last lecture, we talked about writing code from two different perspectives, right? The first was from someone who wanted to implement a class. So implementing the class meant defining your own object type.
So you defined the object type when you defined the class. And then you decided what data attributes you wanted to define in your object. So what data makes up the object? What is the object, OK?
In addition to data attributes, we also saw these things called methods. And methods were ways to tell someone how to use your data type. So what are ways that someone can interact with the data type, OK? So that's from the point of view of someone who wants to write their own object type. So you're implementing a class.
And the other perspective was to write code from the point of view of someone who wanted to use a class that was already written, OK? So this involved creating instances of objects. So you're using the object type. Once you created instances of objects, you were able to do operations on them. So you were able to see what methods whoever implemented the class added. And then, you can use those methods in order to do operations with your instances.
So just looking at the coordinate example we saw last time, a little bit more in detail about what that meant-- so we had a class definition of an object type, which included deciding what the class name was. And the class name basically told Python what type of an object this was, OK? In this case, we decided we wanted to name a coordinate-- we wanted to create a Coordinate object. And the type of this object was therefore going to be a coordinate.
We defined the class in the sort of general way, OK? So we needed a way to be able to access data attributes of any instance. So we use this self variable, OK? And the self variable we used to refer to any instance-- to the data attributes of any instance in a general way without actually having a particular instance in mind, OK?
So whenever we access data attributes, we would say something like self dot to access a data attribute. You'd access the attribute directly with self.x. Or if you wanted to access a method, you would say self, dot, and then the method name-- for example, distance.
And really, the bottom line of the class definition is that your class defines all of the data-- so data attributes-- and all of the methods that are going to be common across all of the instances. So any instance that you create of a particular object type, that instance is going to have this exact same structure, OK? The difference is that every instance's values are going to be different.
So when you're creating instances of classes, you can create more than one instance of the same class. So we can create a Coordinate object here using this syntax right here. So you say the type, and then, whatever values it takes in. And you can create more than one Coordinate object.
Each Coordinate object is going to have different data attributes. Sorry, it's going to have different data attribute values, OK? Every Coordinate object is going to have an x value and a y value. But the x and y values among different instances are going to vary, OK? So that's the difference between defining a class and looking at a particular instance of a class. So instances have the structure of the class. So for a coordinate, all instances have an x value and a y value. But the actual values are going to vary between the different instances.
OK, so ultimately, why do we want to use object-oriented programming? So, so far, the examples that we've seen were numerical, right-- a coordinate, a fraction. But using object-oriented programming, you can create objects that mimic real life. So if I wanted to create objects of-- an object that defined a cat and an object that defined a rabbit, I could do that with object-oriented programming. I would just have to decide, as a programmer, what data and what methods I'd want to assign to these groups of objects, OK?
So using object-oriented programming, each one of these is considered a different object. And as a different object, I can decide that a cat is going to have a name, an age, and maybe a color associated with it. And these three here, on the right, each one of these rabbits is also an object. And I'm going to decide that I'm going to represent a rabbit by just an age and a color, OK? And with object-oriented programming, using these attributes, I can group these three objects together and these three objects together, OK?
So I'm grouping sets of objects that are going to have the same attributes together. And attributes-- this is also a recap of last time-- come in two forms, right, data attributes and procedural attributes. So the data attributes are basically things that define what the object is. So how do you represent a cat as an object? And it's up to you, as the programmer, to decide how you want to do that.
For a coordinate, it was pretty straightforward. You had an x and a y value. If we're representing something more abstract like an animal, then maybe I would say, well, I'm going to represent an animal by an age and a name, OK? So it's really up to you to decide how you want to represent-- what data attributes you want to represent your object with.
Procedural attributes were also known as methods. And the methods are essentially asking, what can your object do, OK? So how can someone who wants to use your object-- how can someone interact with it? So for a coordinate, we saw that you could find the distance between two coordinates. Maybe for our abstract Animal object, you might have it make a sound, OK, by maybe printing to the screen or something like that.
OK, this slide's also a recap of how to create a class just to make sure everyone's on the same page before we go on. So we defined a class using this class keyword. And we said, class, the name of the class. So now we're going to create a more abstract Animal class. We're going to see, in the second half of the lecture, what it means to put something else in the parentheses. But for now, we say that an animal is an object in Python. So that means it's going to have all of the properties that any other object in Python has.
And as we're creating this animal, we're going to define how to create an instance of this class. So we say def. And this __init__ was the special method that told Python how to create an object. Inside the parentheses, remember, we have the self, which is a variable that we use to refer to any instance of the class, OK? We don't have a particular instance in mind, we just want to be able to refer to any instance, OK? So we use this self variable.
And then, the second parameter here is going to represent what other data we use to initialize our object with. So in this case, I'm going to say, I'm going to initialize an Animal object with an age, OK? So when I create an animal, I need to give it an age.
Inside the __init__ are any initializations that I want to make. So the first thing is, I'm going to assign an instance variable, age-- so this is going to be the data attribute age-- to be whatever is passed in. And then, I'm also making another assignment here, where I'm assigning the data attribute name to be None originally.
Later on in the code, when I want to create an Animal object, I say the class name. And then I pass it in whatever parameters it takes-- in this case, the age. And I'm assigning it to this instance here, OK?
All right, so now we have this class, Animal. We've done the first part here, which is to initialize the class, right? So we've told Python how to create an object of this type. There's a few other methods here that I've implemented. Next two we call getters, and the two after that we call setters, OK? And getters and setters are very commonly used when implementing a class. So getters essentially return the values of any of the data attributes, OK?
So if you look carefully, get_age() is just returning self.age, and get_name() just returns self.name. So they're very simple methods. Similarly, set_age() and set_name()-- we're going to see what this funny equal sign is doing here in the next couple of slides. But setters do a very similar thing where they're going to set the data attributes to whatever is passed in, OK?
So those are getters and setters. And then, the last thing down here is this __str__ method. And this __str__ method is used to tell Python how to print an object of this type Animal. So if you didn't have this __str__ method, if you remember from last lecture, what ends up happening is you're going to get some message when you print your object that says, this is an object of type Animal at this memory location, which is very uninformative, right? So you implement this method here, which tells Python how to print an object of this type, OK?
So the big point of this slide is that you should be using getters and setters-- you should be implementing getters and setters for your classes. And we're going to see, in the next couple of slides, why exactly. But basically, they're going to prevent bugs from coming into play later on if someone decides to change implementation.
So we saw how to-- so the previous slide, this slide here, shows the implementation of the Animal class. And here we can see how we can create an instance of this object. So we can say a = Animal(3). So this is going to create a new Animal object with an age of 3. And we can access the object through the variable a.
Dot notation, recall, is a way for you to access data attributes and methods of a class, OK? So you can say a.age later on in your program, and that is allowed. It'll try to access the age data attribute of this particular instance of the class, a. So this is going to give you 3.
However, it's actually not recommended to access data attributes directly. So this is the reason-- so you're going to see in the next slide, the reason-- why we're going to use getters and setters. Instead, you should use the get_age() getter method to get the age of the animal. So this is going to return, also, 3. So these are going to do the same thing.
And the reason why you'd want to use getters and setters is this idea of information hiding, OK? So the whole reason why we're using classes in object-oriented programming is so that you can abstract certain data from the user, OK? One of the things you should be abstracting is these data attributes. So users shouldn't really need to know how a class is implemented. They should just know how to use the class, OK?
So consider the following case. Let's say whoever wrote the Animal class wants to change the implementation. And they've decided they don't want to call the data attribute "age" anymore, they want to call it "years," OK? So when they initialize an animal they say self.years = age. So an animal still gets initialized by its age. And the age gets passed into a data attribute named "years," OK?
Since I'm implementing this class, I want to have a getter, which is going to return self.years. So I'm not returning self.age anymore, because age is no longer the data attribute I'm using. So with this new implementation, if someone was using this implementation and was accessing age directly as-- was accessing the data attribute age directly-- with this new implementation, they'd actually get an error, right? Because this animal that they created using my old implementation no longer has an attribute named "age." And so Python's going to spit out an error saying no attribute found or something like that, OK?
If they were using the getter a.get_age()-- the person who implemented the class re-implemented get_age() to work correctly, right, with their new data attribute, years, as opposed to age-- so if I was using the getter get_age(), I wouldn't have run into the bug, OK? So things to remember-- write getters and setters for your classes. And later on in your code, use getters and setters to prevent bugs and to promote easy to maintain code.
OK, so information hiding is great. But having said that, Python's actually not very great at information hiding, OK? Python allows you to do certain things that you should never be doing. OK. So the first, we've just seen. The first is to access data attributes from outside of the class, OK? So if I were to say a.age, Python allows me to do that without using a getter and setter.
Python also allows you to write to data attributes from outside the class. So if I implemented the class Animal assuming that age was a number, an integer, and all of my methods work as long as age is an integer, but someone decided to be smart and, outside of the class, set age to be infinite as a string, that might cause the code to crash, OK? Python allows you to do that. But now you're breaking the fact that age has to be an integer, right? So now the methods should probably be checking the fact that age is an integer all the time.
The other thing that you're allowed to do is to create data attributes outside of the class definition, OK? So if I wanted to create a new data attribute called "size" for this particular instance, Python also allows me to do that. And I can set it to whatever I want, OK? So Python allows you to do all these things, but it's actually not good style to do any of them. So just don't do it. All right.
So the last thing I want to mention-- the last thing about classes before we go on to inheritance-- is this idea called default arguments. And default arguments are passed into methods. And since methods are functions, you can also pass in different arguments to functions.
So for example, this set_name() method had self. And then, this new name is equal to this empty string here, OK? We haven't seen this before. But this is called a default argument. And you can use the function in one of two ways.
The first way is so we can create a new instance of an Animal type object with this line here, a = Animal(3). And then we can say a.set_name(). So this calls the setter method to set the name. And notice, we've always said that you have to put in parameters for everything other than self, OK? But here we have no parameters passed in.
But that's OK, because newname actually has a default argument, OK? So that tells Python, if no parameter is passed in for this particular formal parameter, then use whatever is up here by default. So if I haven't passed in the parameter a.get_na-- a.set_name(), sorry-- a.sett_name() is going to be setting the name to the empty string, because that's what the default parameter is. So in the next line, when I print a.get_name(), this is just going to print the empty string, OK?
If you do want to pass in a parameter, you can do so as normal. So you can say a = Animal(3), a.set_name(), and then pass in a parameter here. And then, newname is going to be assigned to whatever parameter is passed in like that. Whatever you pass in overrides the default argument, and everything is good. So when I print a.get_name(), this is going to print out the name that you've passed in. Questions about default? Yeah.
ANA BELL: What if you don't provide a default value for--
AUDIENCE: For newname?
ANA BELL: For newname? If you don't provide a default argument for newname and you do this case here, then that's going to give you an error. So Python's going to say something like, expected one argument, got zero, or something like that. Great question. OK.
All right, so let's move on to this idea of hierarchies, OK? So the great thing about object-oriented programming is that it allows us to add layers of abstraction to our code, all right? So we don't need to know how very, very low-level things are implemented in order to use them. And we can build up our code to be more and more complex as we use up these different abstractions.
So consider every one of these things on this slide as being a separate object, all right? Every one of these things can be considered to be an animal, OK? According to our implementation of an animal, the one thing that an animal has is an age, OK? And that's probably true, right? Every one of these things has an age.
But now I want to build up on this and create separate groups, right? And each one of these separate groups that I create on top of Animal is going to have its own functionality, right? They're going to be a little bit more specific, a little more specialized.
So I can create these three groups now, a cat, a rabbit, and a person group. And for example-- so they're all animals, right? They all have an age. But for example, maybe a person's going to have a list of friends whereas a cat and a rabbit do not. Maybe a cat has a data attribute for the number of lives they have left, right, whereas a person and a rabbit do not, OK?
So you can think of adding these more specialized-- adding functionality to each one of these subgroups, OK? So they're going to be more and more specialized, but all of them retaining the fact that they are animals. So they all have an age, for example. So on top of these, we can add another layer and say that a student is a person and is an animal, OK? But in addition to having an age and maybe also having a list of friends, a student might also have a major or-- they're pretty, so maybe-- their favorite subject in school.
So that's the general idea of hierarchies, OK? So we can sort of abstract the previous slide into this one and say that we have parent classes and child classes, OK? The Animal class is like our parent class. It's the highest-level class.
Inheriting from the Animal class, we have these child classes or subclasses, OK? Whatever an animal can do, a person can do. Whatever an animal can do, a cat can do. And whatever an animal can do, a rabbit can do, OK-- that is, have an age and maybe some really basic functionality, OK? But between person, cat, and rabbit, they're going to be varying wildly as to the kinds of things that they can do, right? But they can all do whatever Animal can do.
So child classes inherit all of the data attributes and all of the methods, or behaviors, that their parent's classes have, OK? But child classes can add more information. Like for example, a person can have a list of friends whereas a general animal will not.
It can add more behavior. Like, maybe a cat can climb trees whereas people and rabbits cannot. Or you can also override behavior. So in the previous one, we had animal, person, student. So maybe we have, an animal doesn't speak at all, but a person can speak. So that's added functionality to the person.
And maybe a person can only say hello. But then, when we talk to a student, we can override the fact-- override the speak() method of a person and say that a student can say, you know, I have homework, or I need sleep, or something like that, OK? So we have the same speak() method for both person and student, because they can both speak. But student will override the fact that they say hello with something else.
OK, so let's look at some code to put this into perspective. So we have this Animal class, which we've seen before. This is the parent class, OK? It inherits from object, which means that everything that a basic object can do in Python, an animal can do, which is things like binding variables, you know, very low-level things, OK? We've seen the __init__. We've seen the two getters, the setters, and the string method to print an object of type Animal.
All right, now, let's create a subclass of Animal. We'll call it Cat, OK? We create a class named Cat. In parentheses, instead of putting "object," we now put "Animal." And this tells Python that Cat's parent class is Animal. So everything that an animal can do, a cat can do. So that includes all of the attributes, which was age and name, and all of the methods. So all the getters, the setters, the __str__, the __init__, everything that the animal had, now the cat has-- the Cat class has.
In the Cat class, we're going to add two more methods though. The first is speak(). So speak() is going to be a method that's going to just take in the self, OK-- no other parameters. And all it's doing is printing "meow" to the screen-- very simple, OK? So through this speak(), we've added new functionality to the class. So an animal couldn't speak, whereas a cat says "meow."
Additionally, through this __str__ method here, we're overriding the animal __str__, OK? So if we go back to the previous slide, we can see that the animal's __str__ had animal, plus the name, plus the age here whereas the cat's __str__ now says "cat," name, and the age, OK? So this is just how I chose to implement this, OK? So here I've overridden the __str__ method of the Animal class.
Notice that this class doesn't have an __init__, and that's OK. Because Python's actually going to say, well, if there's no __init__ in this particular method-- sorry, in this particular class-- then look to my parents and say, do my parents have an __init__, OK? And if so, use that __init__. So that's actually true for any other methods. So the idea here is, when you have hierarchies, you have a parent class, you have a child class, you could have a child class to that child class, and so on and so on. So you can have multiple levels of inheritance.
What happens when you create an object that is of type something that's been-- of a type that's the child class of a child class of a child class, right? What happens when you call a method on that object? Well, Python's are going to say, does a method with that name exist in my current class definition? And if so, use that.
But if not, then, look to my parents. Do my parents know how to do that, right? Do my parents have a method for whatever I want to do? If so, use that. If not, look to their parents, and so on and so on. So you're sort of tracing back up your ancestry to figure out if you can do this method or not.
So let's look at a slightly more complicated example. We have a class named Person. It's going to inherit from Animal. Inside this person, I'm going to create my own-- I'm going to create an __init__ method. And the __init__ method is going to do something different than what the animal's __init__ method is doing. It's going to take in self, as usual. And it's going to take in two parameters as opposed to one, a name and an age.
First thing the __init__ method's doing is it's calling the animal's __init__ method. Why am I doing that? Well, I could theoretically initialize the name and the age data attributes that Animal initializes in this method. But I'm using the fact that I've already written code that initializes those two data attributes. So why not just use it, OK?
So here, this says, I'm going to call the class Animal. I'm going to call its __init__ method. And I'm going to leave it up to you to-- not you as the class, but I'm talking as the programs is running-- I'm going to leave it up to you to figure out how to initialize an animal with this particular age and what to name it. So Python says, yep, I know how to do this, so I'm going to go ahead and do that for you. So now it says person is an animal. And I've initialized the age and the name for you.
The next thing I'm doing in the __init__ is I'm going to set the name to whatever name was passed in, OK? So in the __init__, notice, I can do whatever I want, including calling methods. And then, the last thing I'm doing here is I'm going to create a new data attribute for Person, which is a list of friends, OK? So an animal didn't have a list of friends, but a person is going to.
The next four methods here are-- this one's a getter, so it's going to return the list of friends. This is going to append a friend to the end of my list. I want to make a note that I actually didn't write a method to remove friends. So once you get a friend, they're friends for life. But that's OK.
The next method here is speak(), which is going to print "hello" to the screen. And the last method here is going to get the age difference between two people. So that just basically subtracts their age and says it's a five-year age difference, or whatever it is. And down here, I have an __str__ method, which I've overridden from the Animal, which, instead of "animal: name," it's going to say "person: name : age," OK?
So we can run this code. So that's down here. I have an animal person here. So I'm going to run this code. And what did I do? I created a new person. I gave it a name and an age. I created another person, a name and an age. And here I've just run some methods on it, which was get_name(), get_age(), get_name(), and get_age() for each of the two people. So that printed, Jack is 30, Jill is 25.
If I print p1, this is going to use the __str__ method of Person. So it's to print "person:", their name, and then, their age. p1.speak() just says "hello." And then, the age difference between p1 and p2 is just 5. So that's just subtracting and then printing that out to the screen.
OK, so that's my person. Let's add another class. This class is going to be a student, and it's going to be a subclass of Person. Since it's a subclass of Person, it's going to-- a student is going inherit all the attributes of a person, and therefore, all the attributes of an animal.
The __init__ method of a student is going to be a little different from the one of Person. We're going to give it a name, an age, and a major. Notice we're using default arguments here. So if I create a student without giving it a major, the major is going to be set to None originally.
Once again, this line here, Person.__init__(self, name, age), tells Python, hey, you already know how to initialize a person for me with this name and this age. So can you just do that? And Python says, yes, I can do that for you. And so that saves you, maybe, like five lines of code just by calling the __init__ method that you've already written through Person, OK?
So Student has been initialized to be a person. And additionally, we're going to set another data attribute for the student to be the major. And we're going to set the major to be None. The student is going to get this setter here, this setter method, which is going to change the major to whatever else they want if they want to change it. And then, I'm going to override the speak() method.
So the speak method for the person, recall, just said "hello." A student is going to be a little bit more complex. I'm going to use the fact that someone created this random class, OK? So this is where we can write more interesting code by reusing code that other people have written. So someone wrote a random class that can do cool things with random numbers.
So if I want to use random numbers in my code, I'm going to put this "import random" at the top of my code, which essentially brings in all of the methods from the Random class, one of the methods being this random() method. So random() is a random() method from the Random class. And this essentially gives me a number between 0 and 1, including 0 but not including 1, OK?
So this random number I get here is going to help me write my method for speak(), where it's going to-- with 25% probability, it's either going to say, "I have homework," "I need sleep," "I should eat," or "I'm watching TV," OK? So a student is going to say one of those four things. And the last thing I'm doing down here is overwriting the __str__ method.
So let's look at the code. I'm going to comment this part out, and uncomment the student, and see what we get. OK, so here, I am creating the student. I'm creating one student whose major is CS, name is Alice, and age is 20. s2 is going to be another student-- name-- Beth, age-- 18. And the major is going to be None, because I didn't pass in any major here. So by default, using the default argument, it's going to be None.
If I print s1, s2, that's going to print out these two things over here just by whatever __str__ method does. And then I'm going to get the students to speak. And if I run it multiple times, you can see that it's going to print different things each time. So "I need sleep," "I have homework," "I need sleep," "I have homework," yeah. So every time, it's going to print something different. OK, questions about inheritance in this example? OK.
Last thing we're going to talk about in this class is an idea of-- or in this lecture, is the idea of-- a class variable, OK? So to illustrate this, I'm going to create yet another subclass of my animal called a rabbit. So class variables-- so so far, we've seen-- sorry, let me back up. So so far, we've seen instance variables, right? So things like self.name, self.age, those are all instance variables. So they're variables that are specif-- they are common across all of the instances of the class, right? Every instance of the class has this particular variable. But the value of the variable is going to be different between all of the different instances.
So class variables are going to be variables whose values are shared between all of the instances in the class. So if one instance of the class modifies this class variable, then, any other instance of the class is going to see the modified value. So it's sort of shared among all of the different instances. So we're going to use class variables to keep track of rabbits.
OK, so we're creating this class, Rabbit. tag = 1. We haven't seen something like this before. So tag is our class variable. Class variables are typically defined inside the class definition but outside of the __init__. So tag is going to be a class variable, and I'm initializing it to 1.
Inside the __init__, this tells us how to create a Rabbit object. So I'm going to give it self as usual, an age, and then two parents. Don't worry about the two parents for now. Inside the __init__-- sorry, inside the __init__-- I'm going to call the __init__ of the animal just to do less work. Python already knows how to initialize an animal for me, so let's do that. So that's going to set the two data attributes, name and age.
I'm going to set the data attributes for parent1, parent2 for a rabbit to be whatever's passed in. And then, this is where I'm going to use this class variable. So I'm creating this data attribute instance variable particular to a specific instance called rid, OK? And I'm assigning this instance variable to the class variable. And I access class variables using not self, but the class name-- so in this case, rabbit.tag.
So initially, tag is going to be 1. And then, the __init__ is going to increment the tag by 1 here, OK? So that means that, from now on, if I create any other instances, the other instances are going to be accessing the updated value of tag instead of being 1.
So let's do a quick drawing to show you what I mean. So let's say I have Rabbit.tag here, OK? So initially, tag is going to be 1, OK? And then I'm going to create a new Rabbit object. So this is as I'm calling the code, OK? So let's say this is a rabbit object-- oh boy, OK-- r1.
You know, I actually googled how to draw a rabbit, but that didn't help at all. OK, so r1 is going to be a new rabbit that we create. Initially, what happens is, when I first create this new rabbit, it's going to access the class variable, which, it's current value is 1. So when I create the rabbit ID-- the rabbit ID, r1.rid-- this is going to get the value 1. And according to the code, after I set the rabbit ID to whatever tag is, I'm going to increment the tag. So this is going to say, OK, now that I've said it, I'm going to go back up here and increment the tag to be 2. OK.
So let's say I create another Rabbit object, OK? All right, there-- that's a sad rabbit, r2. The ID of r2 is going to be what? Well, according to the way we create a new Rabbit object is it's going to access whatever the value of tag is, which is a class variable. It was changed by the previous creation of my rabbit, so now I'm going to access that, right? So the value is going to be 2.
And according to the code, the next thing I do after I create the instance rid is I'm going to increment tag. So I'm incrementing the class variable to be 3, OK? So notice that all of my instances are accessing this shared resource, this shared variable called tag.
So as I'm creating more and more rabbits, they're all going to be incrementing the value of tag, because it's shared among all of the instances. And so this value, this tag class variable, keeps track of how many different instances of a rab-- of how many different instances of rabbits I've created throughout my entire program, OK? So the big idea here is that class variables are shared across all the instances. So they can all modify them. But these rids, right, these instance variables, are only for that particular instance. So r2 can't have access to r1's ID value, nor could change it. But it won't change it across all of the different instances, OK?
So that's how the __init__ method works of Rabbit, OK? So we have these tags that keep track of how many rabbits we've created. We have a couple of getter-- we have some getters here to get all the parents. So now let's add a somewhat more interesting function. Oh, I just want to mention, when I'm getting the rid, I'm actually using this cool zfill() function here, or method, which actually pads the beginning of any number with however many zeros in order to get to that number here. So the number 1 becomes 001 and so on. So it ensures that I have this nice-looking ID type thing that's always three digits long.
So let's try to work with this Rabbit object. Let's define what happens when you add two rabbits together, OK-- in this class, not in the real world. OK. So if I want to use the plus operator between two rabbit instances, I have to implement this __add__ method, OK? So all I'm doing here is I'm returning a new Rabbit object, OK? Whoops, sorry about that.
And let's recall the __init__ method of the rabbit, OK? So when I'm returning a new Rabbit object, I'm returning a new Rabbit object that's going to have an age of 0. Self-- so the Rabbit object I'm calling this method on is going to be the parent of the new rabbit. And other is going to be the other parent of the new rabbit, OK?
So if we look at the code, and I run it, this part here, I'm creating three rabbits, r1, r2, and r3. Notice this class variable is working as expected, because the IDs of each of my rabbits increments as I create more rabbits. So we have 001, 002, 003. If I print r1, and r2, and r3-- that was these three lines over here-- the parents of r1 and r2 are None, because that's just the default-- yes, the default arguments for creating a rabbit.
To add two rabbits together, I use the plus operator between two Rabbit objects. And on the right here, I'm testing rabbit addition. And I can print out the IDs of all my rabbits. And notice that, when I've created this new rabbit, r4, the ID of it still kept incrementing. So now, the ID of the fourth rabbit is 004. And then, when I get r4's parents, they are as we want them to be, so r1 and r2.
The other thing I want to do is to compare two rabbits. So if I want to compare two rabbits, I want to make sure that their parents are the same. So I can compare the first parent of the first rabbit with the first parent of the second rabbit and the second parent of the first rabbit to the second parent of second rabbit or getting the combinations of those two. So that's what these two Booleans are doing.
So these are going to tell me-- these are going to be Boolean values, either True or False. And I'm going to return either they have the same parents of that type or the same parents criss-crossed, OK? So here, notice that I'm actually comparing the IDs of the rabbits as opposed to the Rabbit objects directly, OK? So if, instead of comparing the IDs in here, I was comparing the parents themselves, directly, what would end up happening is this function, this method, eq(), would get called over and over again. Because here, we have parents that are rabbits.
And at some point, the parents of the very, very first rabbits ever created by this program are None. And so when I try to call-- when I try to call the parent one of None, that's going to give me an error, OK, something like an attribute error where None doesn't have this parent attribute, OK? So that's why I'm comparing IDs here, OK? And the code in the lecture here shows you some tests about whether rabbits have the same parents. And I've created new rabbits here, r3 and r4, the addition of those two. And r5 and r6 are going to have the same parents down here-- True-- but r4 and r6 don't, OK?
So just to wrap it up, object-oriented programming is the idea of creating your own collections of data where you can organize the information in a very consistent manner. So every single type of object that you create of this particular type that you create-- sorry, every object instance of a particular type is going to have the exact same data attributes and the exact same methods, OK? So this really comes back to the idea of decomposition and abstraction in programming. All right, thanks, everyone.