python
= 8
width = 5 height
So far, the kinds of objects of our own that we’ve created in python are variables and functions.
Variables:
Functions:
python
Today we’re going to introduce a new type called classes. You’ve already been interacting with classes from other python packages, so this might demystify some of how python works.
Classes are python’s implementation of what’s known as “object oriented programming”. They let us bundle together lots of different values, and functions for operating and updating those values in one single “object”.
For example, I’m currently working on a project that will need to bundle together lots of different speaker demographics, so I’ve been working on a Speaker
class.
The class
keyword says that what follows will be a new kind of class. By convention, class names are in TitleCase
.
To tell a class what kind of values it should be “initialized” with, we need to define a special function inside the class called __init__()
.
python
__init__()
should always take a special variable called self
as the first argument. The other arguments I defined all get assigned to an attribute of self
. Here’s how that looks in use.
python
We can then get the attributes of the joe
object with the .
syntax.
We can create another instance of Speaker
.
__init__()
.Above, the __init__()
function just assigns each input value to the same name attribute of the instance. But we can do more.
python
python
Right now, if we say print(joe)
, we’re going to get a pretty ugly output.
We can tell a class what it should print by defining a __repr__()
method inside of it.
python
class Speaker():
def __init__(
self,
name = None,
pronoun = None,
age = None,
year = None
):
self.name = name
self.pronoun = pronoun
self.age = age
self.year = year
self.dob = year - age
def __repr__(self):
return f"{self.name} was interviewed in "\
f"{self.year} when {self.pronoun} "\
f"was {self.age} years old."
python
There might be things you want to calculate or otherwise derive from the values within class instance, or values you want to update. You can do this by defining additional methods.
python
class Speaker():
def __init__(
self,
name = None,
pronoun = None,
age = None,
year = None
):
self.name = name
self.pronoun = pronoun
self.age = age
self.year = year
self.dob = year - age
def __repr__(self):
return f"{self.name} was interviewed in "\
f"{self.year} when {self.pronoun} "\
f"was {self.age} years old."
def age_in(self, year):
age = year - self.dob
if age < 0:
return np.nan
return age
def year_when(self, age):
return self.dob + age
python
If we wanted to see how old noam
was when joe
was born, we can now do this
We’ll get nan
if we do the reverse, because we wrote the age_in()
method not to return negative numbers.
There was a notable event in 1971 when Noam C
met Michel F
over a large carafe of orange juice. Let’s see how old he was.
If Joe F
wants to reach a similar landmark meeting by that age, what year will he have to do it in?
Classes won’t always be useful for a one-off usage, but they can be really useful when used in batches. Here’s some sample demographics from the Philadelphia Neighborhood Corpus.
python
We can create a list of speaker objects from this data.
python
python
[Adam M was interviewed in 2004 when he was 83 years old.,
Adan F was interviewed in 1981 when he was 35 years old.,
Adrienne M was interviewed in 1991 when she was 35 years old.,
Agnes was interviewed in 1979 when she was 59 years old.,
Agnes P was interviewed in 1974 when she was 50 years old.]
We can get the dates of birth for everyone now with relatively little code.
python
array([1921, 1946, 1956, 1920, 1924])
Two large events in Philadelphia were papal visits in 1979 and 2015. We can figure out how old everyone was on those two dates now.
python
array([58, 33, 23, 59, 55])
There are some more advanced features we can build into classes in python. A lot of them are accessed by these methods that begin and end with double underscores __
, so-called “dunder methods”. Here’s nice blog post about them.
For example, with our Speaker
class, theres no way to compare two instances with <
or ==
.
But, we can tell a class how to behave with respect to these operators. Let’s say that the dimension we wanted to compare people on was their date of birth.
python
class Speaker():
def __init__(
self,
name = None,
pronoun = None,
age = None,
year = None
):
self.name = name
self.pronoun = pronoun
self.age = age
self.year = year
self.dob = year - age
def __repr__(self):
return f"{self.name} was interviewed in "\
f"{self.year} when {self.pronoun} "\
f"was {self.age} years old."
# defines < between self and another obj
def __lt__(self, obj):
return self.dob < obj.dob
# defines <= self and another obj
def __le__(self, obj):
return self.dob <= obj.dob
def age_in(self, year):
age = year - self.dob
if age < 0:
return np.nan
return age
def year_when(self, age):
return self.dob + age
As it turns out, defining just __lt__()
and __le__()
is enough to get all of the comparison operators to work.
python
Having these less than and greater than methods defines also allows us to sort a list of objects.
python
The order of objects in this list is just the order they appeared in in the original lists/arrays.
python
[Adam M was interviewed in 2004 when he was 83 years old.,
Adan F was interviewed in 1981 when he was 35 years old.,
Adrienne M was interviewed in 1991 when she was 35 years old.,
Agnes was interviewed in 1979 when she was 59 years old.,
Agnes P was interviewed in 1974 when she was 50 years old.]
But, with all of the comparison methods defined, we can get the max()
and min()
of this list.
python
Adrienne M was interviewed in 1991 when she was 35 years old.
And we can sort the list.
python
[Agnes was interviewed in 1979 when she was 59 years old.,
Adam M was interviewed in 2004 when he was 83 years old.,
Agnes P was interviewed in 1974 when she was 50 years old.,
Adan F was interviewed in 1981 when he was 35 years old.,
Adrienne M was interviewed in 1991 when she was 35 years old.]
Sometimes, you might want to define dynamic properties of classes. For example, let’s look at noam
.
Let’s say we actually typoed the year, and Noam C
was actually interviewed in 2023. We can update the year, but the date of birth won’t be correctly updated.
We can fix this by moving the definition of dob
outside of the __init__()
method, and into its own method decorated with @property
.
python
class Speaker():
def __init__(
self,
name = None,
pronoun = None,
age = None,
year = None
):
self.name = name
self.pronoun = pronoun
self.age = age
self.year = year
def __repr__(self):
return f"{self.name} was interviewed in "\
f"{self.year} when {self.pronoun} "\
f"was {self.age} years old."
# defines < between self and another obj
def __lt__(self, obj):
return self.dob < obj.dob
# defines <= self and another obj
def __le__(self, obj):
return self.dob <= obj.dob
# dynamically define dob
@property
def dob(self):
return self.year - self.age
def age_in(self, year):
age = year - self.dob
if age < 0:
return np.nan
return age
def year_when(self, age):
return self.dob + age
python
The basic values have remained the same,
But now, if we change the value of noam.year
, the date of birth will be dynamically updated.
Let’s say we were going to follow some new practices in asking speakers about their gender, and gave them a rating scale between 0 and 10 for masculine
and feminine
. We could define a new Gender
class like so:
python
We can then include a gender
parameter within the Speaker
class.
python
class Speaker():
def __init__(
self,
name = None,
pronoun = None,
gender = Gender(),
age = None,
year = None
):
self.name = name
self.pronoun = pronoun
self.gender = gender
self.age = age
self.year = year
def __repr__(self):
return f"{self.name} was interviewed in "\
f"{self.year} when {self.pronoun} "\
f"was {self.age} years old."
# defines < between self and another obj
def __lt__(self, obj):
return self.dob < obj.dob
# defines <= self and another obj
def __le__(self, obj):
return self.dob <= obj.dob
# dynamically define dob
@property
def dob(self):
return self.year - self.age
def age_in(self, year):
age = year - self.dob
if age < 0:
return np.nan
return age
def year_when(self, age):
return self.dob + age
python
We can access the gender attribute with the .
syntax.
And we can access the attributes of that object as well.
Now, let’s say we wanted to generalize our Gender
class to have any given set of labels we wanted. We can create the more generic class like so:
python
Now, we can make a subclass called BinaryGender
from the more general class.
python
Passing Gender
as an argument in defining the class tells python that BinaryGender
is going to be a “subclass”. That means any methods or properties we defined for Gender
will be automatically available to BinaryGender
. So, for example, we never defined the __repr__()
function for BinaryGender
, so it’ll just use the one from Gender
Inside of the __init__()
method, we called this special super().__init__()
function. That tells python to run all of the __init__()
code from the superclass (Gender
) with the specified arguments.
@online{fruehwald2024,
author = {Fruehwald, Josef},
title = {Classes},
date = {2024-04-09},
url = {https://lin511-2024.github.io/notes/programming/08_classes.html},
langid = {en}
}