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. AUDIENCE: [INAUDIBLE] 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.