I don’t deny there is a remarkable quantity of ivory—mostly fossil. We must save it, at all events—but look how precarious the position is—and why? Because the method is unsound. | ||
— Joseph Conrad, Heart of Darkness, Chapter III |
The word
class
means “data type”,
and the word
object
means “value”.
For example,
the following code puts a value of type
int
into the variable
i
,
and puts an object of class
Date
into the variable
d
.
i = 10 s = "hello" m = ["January", "February", "March"] d = Date(12, 31, 2019)
I’ve oversimplified a bit.
The above code does put the value 10 into
i
.
But we already know that the above code does not actually put a
str
ing
into
s
,
or a
list
into
m
.
It merely puts a
reference
to the string into
s
,
and a reference to the list into
m
,
and a reference to an object into
d
.
We first encountered references in
Sort.
A class is a data type that is not built into the Python language.
We must therefore create the class ourselves.
To create a class, we have to execute the
class
definition
in
lines
10–85
of
classDate.py
.
This class definition is a blueprint
for each object of the class that we create.
For example, it is a blueprint for the object
d
in
line
89.
(More precisely, it is the blueprint for the object of class
Date
which we create in
line
89
and to which the variable
d
refers.)
The
d
in
line
89
contains three
int
variables named
year
,
month
,
and
day
.
(In fact,
every object of class
Date
would contain three
int
variables named
year
,
month
,
and
day
.)
The variables contained in an object are called the
instance attributes
of that object;
they are created in
lines
36–38.
Line
99
shows that
d
also has a function named
getMonth
that belongs to it.
(In fact, every object of class
Date
would have a function named
getMonth
that belongs to it.)
A function that belongs to an object is called an
instance method
of that object.
It looks like
line
99
is calling
getMonth
with no arguments,
print(f"month = {d.getMonth()}") #Call the instance method in line 46.but line 99 is really doing this:
print(f"month = {getMonth(d)}") #Call the instance method in line 46.When
getMonth
is called from
line
99,
the argument
self
in
line
46
is therefore the object
d
in
line
99.
The first argument of an instance method is always named
self
,
and is always the object to which the instance method belongs.
When we say that the three instance attributes (i.e., variables)
year
,
month
,
and
day
“belong to”
d
,
we mean that they are stored inside of
d
.
Each object of class
Date
has its own trio of instance attributes stored inside of it.
We could have
d = Date(12, 31, 2019) e = Date( 7, 4, 1776)
But when we say that
line
99
calls the instance method (i.e., function) named
getMonth
that
“belongs to”
d
,
we do not mean that the instance method is stored inside of
d
.
All we mean is that a reference to
d
is being passed by
line
99
as the
self
argument of the function.
Both of the following statements call the same
getMonth
function,
but they pass it different arguments.
print(d.getMonth()) #really doing print(getMonth(d)) print(e.getMonth()) #really doing print(getMonth(e))
__str__
has two underscores in front,
and two in back.
Line
99
calls the
getMonth
function that belongs to
d
(i.e., it passes
d
as the
self
argument of the instance method
getMonth
in
line
46).
Similarly,
line
96
calls the
__str__
function that belongs to
d
(i.e., it passes
d
as the
self
argument of the instance method
__str__
in
line
54).
The
__str__
instance method of an object
should always return a string containing a picture of the
values of the instance attributes inside of the object.
See also
__str__
vs.
__repr__
.
Line
95
does the same thing as
96.
In other words, when you pass an object to the
str
function,
you’re really passing the object to the
__str__
function that belongs to the object.
Line
94
does the same thing as
95.
In other words, when you
print
an object,
you’re really converting the object to a
string
and printing the string.
The following instance methods return information about the content of the object to which they belong:
getYear
(lines
42,
101,
and
104)getMonth
(lines
46
and
99)getDay
(lines
50
and
100)__str__
(lines
54
and
94–96)dayOfYear
(lines
58
and
104)The following instance methods can change the content of the object to which they belong:
Note that
line
78
is an example of one instance method
(nextDays
)
calling another instance method
(nextDay
)
belonging to the same object.
Like
__str__
,
the
__init__
method has two underscores in front,
two in back.
Line
89
calls the
__init__
instance method in
line
32.
It looks like
line
89
is passing three arguments to this method,
but it is actually passing four.
Whenever you call an instance method,
you are actually passing one extra argument in addition to the arguments
you write in the parentheses.
We saw this for the first time when
line
99
called the
getMonth
in
line
46.
An
__init__
method should error-check all its arguments
(lines
33–35)
before storing any of them in the instance attributes of the newborn object
(lines
36–38).
In other words,
the object in the throes of birth should end up being 100% constructed
or 0% constructed.
A partially constructed object
is a greater source of bugs than no object at all.
Instead of
assert
,
an
__init__
method could also respond to a bad argument by
raise
’ing
an exception such as
ValueError
.
And instead of the
assignment
statements
in
lines
36–38,
we could have called the
built-in function
setattr
.
But
setattr
is intended for situations where you don’t know the name of the instance attribute when you’re writing the script;
we’ll see an example in
line 88
of
models.py
in
Tweepy.
See also
__init__
vs.
__new__
.
Each object of class
Date
contains its own
year
instance attribute (the
self.year
in lines
36,
44,
56,
and
72).
If you create 100 objects of class
Date
,
you will have 100
year
variables.
But all the objects of class
Date
share the same
lengths
variable (the
list
in lines
16,
35,
60,
64,
and
83).
If you
instantiate
(create) 100 objects of class
Date
,
you will have exactly one
lengths
variable.
In fact,
you will also have exactly one
lengths
variable before you instantiate any object of class
Date
,
and also after the last object of class
Date
has been destroyed,
and also even if your script never instantiates any objects of class
Date
.
A variable that is shared by all the objects of a class is called a
class attribute.
lengths
is a class attribute
because the point at which it was created,
in lines
16–30,
is within the
class
definition
(lines 10–85)
but outside the definition of any method (function).
Since a class attribute belongs to no object,
do not write the word
self
in front of it when you use it
(lines
35,
60,
64,
and
83).
Write the name of the class
(Date
)
in front of it.
The method
monthsInYear
(lines
81
and
88)
always returns 12.
This method belongs to class
Date
because its definition in
lines
80–83
is within the class definition in
lines 10–85.
But this method does not belong to any object of class
Date
,
i.e.,
it does not receive any
self
argument in
line
81.
Let’s call this a
selfless method,
although the official name is
staticmethod
(line
80).
A selfless method can use the class attributes (e.g., the
Date.lengths
in
line
83),
but it has no
self
object to which it belongs
(i.e., it receives no
self
argument),
and therefore cannot mention
self.year
,
self.month
,
or
self.day
.
(We will see later that there is another kinds of selfless method,
called a
class
method
.)
To emphasize that
monthsInYear
belongs to no object,
we deliberately call this selfless method in
line
88
at a time when no object of class
Date
exists.
The first
Date
object is not created until later, in
line
89.
Why write a function that always returns the same number? Just in case we ever have to generalize this class to work with non-Gregorian calendars. In the Jewish calendar, for example, some years have 13 months.
months in year = 12 type(d) = <class '__main__.Date'> d = 12/31/2019 d = 12/31/2019 d = 12/31/2019 month = 12 day = 31 year = 2019 12/31/2019 is day number 365 of the year 2019. 01/01/2020 is the next day. 01/08/2020 is a week after that.
print(f"month = {d.getMonth()}") #Print the return value of the instance method in line 46.to
print(f"month = {d.month}") #Print the value of the instance attribute in line 37.Then change it back because you should never do this. Code that is outside of the class definition (i.e., outside of lines 10–85) should never mention the attributes of a class. (One exception: an object of the class
Person
in
Attributes
had no methods.
Its only purpose was to carry around a bunch of instance attributes
without using them in any calculations.
It would be okay to mention the attributes of a
Person
outside of the class definition.)
int
variables.
january = 1 february = 2 march = 3 april = 4 may = 5 june = 6 july = 7 august = 8 september = 9 october = 10 november = 11 december = 12Then change line 89 from
d = Date(12, 31, 2019)to
d = Date(Date.december, 31, 2019)to make it clear what the number 12 means. After all, suppose the date had been
d = Date(12, 11, 2019) #December 11 or November 12?
if self.month < len(Date.lengths) - 1:to
if self.month < Date.monthsInYear():And don’t hardcode in the number 12 into line 34. Change this line from
assert isinstance(month, int) and 1 <= month <= 12to
assert isinstance(month, int) and 1 <= month <= Date.monthsInYear()
monthsInYear
in
line
88
without using any object:
print(f"months in year = {Date.monthsInYear()}")Verify that we can also call this method with an object and a dot in front of it:
#after creating d in line 89 print(f"months in year = {d.monthsInYear()}")This is a bit misleading because it makes it look like the method uses the instance attributes inside of
d
,
which it doesn’t.
The
function
decorator
@staticmethod
in
line
80
is what makes it possible to write
d.monthsInYear()
anyway, as well as the more sensible
Date.monthsInYear()
.
For the commercial at sign
@
,
see
decorator
expressions,
decorated
classes,
and the other function decorator
@classmethod
below.
date
.
Name it
daysInYear
,
decorate it with
@staticmethod
,
give it no arguments (not even a
self
),
and have it return the sum of the numbers in
lengths[1:]
.
(It should return 365.)
Do not accidentally include the
None
in
line
17
in the sum.
Use the
sum
function as in
line
60.
Date
an additional
__init__
method that could be called with one argument:
newYearsDay = Date(2019) #Create a Date containing January 1, 2019.Here’s how to get almost the same effect. Add the following method to class
Date
immediately after the
__init__
method.
I wanted to name the first argument
class
,
but I couldn’t do that because
class
is a keyword.
@classmethod def newYearsDay(cls, year): print(f"cls = {cls}") print(f"type(cls) = {type(cls)}") return cls(1, 1, year) #calls Date(1, 1, year)Create another object of class
Date
after creating
d
.
newYearsDay = Date.newYearsDay(2019) #ultimately calls Date(1, 1, 2019) print(f"newYearsDay = {newYearsDay}")
cls = <class '__main__.Date'> type(cls) = <class 'type'> newYearsDay = 01/01/2019
date
,
named
prevDay
and
prevDays
.
They should be just like
nextDay
and
nextDays
,
except that they should go backwards.
Make sure that the day before a New Year’s Day is the
New Year’s Eve of the previous year.
Do not inadvertently reintroduce a hardcoded number 12
like the one you eliminated in the above exercise 3.
Do not change the contents of the class attribute
lengths
in
lines
16–30.
For example,
lengths
should continue to hold only one copy of the length of December.
Date
objects by using the subtraction operator:
d = Date(Date.december, 31, 2020) e = Date(Date.december, 31, 2019) print(d - e) #means print(__sub__(d, e))
365Add the following instance method to class
Date
.
Two underscores in front, two in back.
def __sub__(self, other): """ Return the distance in days between the two Date objects. The return value is positive is self is later than other, negative if self is earlier than other, and zero if they're the same Date. """ iself = 365 * self.year + self.dayOfYear() iother = 365 * other.year + other.dayOfYear() return iself - iother
Date
objects with a
<
sign to find out if the left one is earlier than the right one.
d = Date(Date.december, 31, 2020) e = Date(Date.december, 31, 2019) if e < d: #means if __lt__(e, d): print(f"{e} is earlier than {d}.") else: print(f"{e} is equal to or later than {d}.")
12/31/2019 is earlier than 12/31/2020.Add the following instance method to class
Date
.
Lowercase LT stands for “less than”;
two underscores in front, two in back.
__lt__
does almost all of its work by calling the
__sub__
we wrote in the previous exercise.
def __lt__(self, other): "Return True if self is earlier than the other Date, False otherwise." return self - other < 0 #means return __sub__(self, other) < 0Could you write a
__lt__
that does its work without calling
__sub__
?
Would it be faster than the above
__lt__
?
Date
only to show you how to create a class and objects thereof.
But if all you wanted to do was perform date arithmetic,
you could do it with Python’s existing classes
datetime.date
and
datetime.timedelta
:
import datetime print("months in year =", datetime.date.max.month) d = datetime.date(2019, 12, 31) #d is an object of class datetime.date print(f"type(d) = {type(d)}") print() #These three statements do the same thing: print(f"d = {d}") print(f"d = {str(d)}") print(f"d = {d.__str__()}") print() print(f"month = {d.month}") print(f"day = {d.day}") print(f"year = {d.year}") print() tuple = d.timetuple() #tuple is a named tuple dayOfYear = tuple.tm_yday #dayOfYear is an int print(f"{d} is day number {dayOfYear} of the year {d.year}.") delta = datetime.timedelta(days = 1) d += delta #means d = d + delta print(f"{d} is the next day.") delta = datetime.timedelta(days = 7) d += delta #means d = d + delta print(f"{d} is a week after that.")
months in year = 12 type(d) = <class 'datetime.date'> d = 2019-12-31 d = 2019-12-31 d = 2019-12-31 month = 12 day = 31 year = 2019 2019-12-31 is day number 365 of the year 2019. 2020-01-01 is the next day. 2020-01-08 is a week after that.