In a very simplicistic view, Classes
are just pre-defined storage containers with additional functionality that knows how to access the currently stored data. (=methods
).
So, for example, the simplest class possible is actually an empty container you can just use to store stuff in, very similar to dictionaries, or structures in other languages:
In [1]:
class Mammal:
pass
This is sometimes useful to just quickly store things together in the same object for logical separations:
In [2]:
mammal = Mammal() # now I have an "object" or "instance" of a class
In [3]:
mammal
Out[3]:
In [4]:
mammal.n_of_legs = 4 # this is a new data attribute.
# as usual, Python doesn't need it to be defined before.
In [5]:
mammal.noise = 'blarg'
In [6]:
mammal.__dict__ # this is a useful internal attribute listing all data attributes
Out[6]:
In [7]:
print(mammal.n_of_legs) # note this attribute becomes <tab>-complete-able
Now, for a mid-size to bigger project, we don't want to define things only on the go.
Creating a class with some structure and functionality will increase our efficiency when working with a lot of data that has a lot of sub-structure.
In [8]:
class Mammal:
# these are data attributes of the class
name = 'Mammal'
n_of_legs = 0
noise = None # indicating non-functionality
nutrition_status = 0
# this is a method of the class
# methods are just like functions, but they always refer back to the
# current object with the first argument being 'self', and after that
# can take other arguments for functionality.
def make_noise(self):
if self.noise is not None:
print(self.noise)
else:
print("Not implemented yet.")
def feed(self, units):
"This is a minimalist docstring."
self.nutrition_status += units
print("Was fed {} units of nutrition.".format(units))
In [9]:
print(Mammal.n_of_legs)
Let's create an object of this class:
In [10]:
mammal = Mammal() # this is called "instantiation of an object"
Coding standards The usual applied coding style is that classes are defined with capital letters, and the instantiated object is often called the same name but with small letters. (Unless the object becomes more specific, see later).
Now, let's use a method:
In [11]:
mammal.make_noise()
The noise
attribute isn't set yet, so that's what we get.
In [12]:
mammal.noise = 'snort'
In [13]:
mammal.noise = None
In [14]:
mammal.make_noise()
"Under the hood" we have changed an attribute of the method being used. In other words, how a method works can be highly status dependent.
This status-like programming style is both hard to follow at times, but also creates great opportunities, for example, to write methods that just automatically do the right thing, because they would read-out its own status from class attributes that have been set when a status changed.
Note:
First, # 1: Independence
In [15]:
mammal2 = Mammal()
mammal2.make_noise()
In [16]:
mammal2.noise = 'burp'
In [17]:
for m in [mammal, mammal2]:
m.make_noise()
In [18]:
mammal.feed(5)
In [19]:
for m in [mammal, mammal2]:
print(m.nutrition_status)
Now #2:
Classes can be used for accessing class-based data, that are NOT supposed to change between instances, like the name 'mammal' for example.
In [20]:
Mammal.name
Out[20]:
In [21]:
Mammal.n_of_legs
Out[21]:
In [22]:
import datetime as dt
class Mammal:
# these are data attributes of the class
name = 'Mammal'
n_of_legs = 0
noise = None
nutrition_status = 0
# Always refer to self first in class methods!
# This 'self' is used to attach data to itself when being
# 'alive' as an instance later on!
def __init__(self, noise, legs):
"""The initialization method. Always called __init__ ! """
self.noise = noise
self.n_of_legs = legs
self.creation_time = dt.datetime.now().isoformat()
# this is a method of the class
# methods are just like functions, but they always refer back to the
# current object with the first argument being 'self', and after that
# can take other arguments for functionality.
def make_noise(self):
if self.noise is not None:
print(self.noise)
else:
print("Not implemented yet.")
def feed(self, units):
"This is a minimalist docstring."
self.nutrition_status += units
print("Was fed {} units of nutrition.".format(units))
In [23]:
mammal = Mammal('bark', 4)
In [24]:
mammal.make_noise()
In [25]:
mammal.n_of_legs
Out[25]:
Note the difference between class attributes and instance attributes.
Instance attributes only exist after instantiation of an object (an 'alive' version of the mere theoretical class).
While class attributes always exist.
In [26]:
Mammal.creation_time
In [27]:
mammal.creation_time
Out[27]:
In [28]:
Mammal.name
# i did not create this attribute with the __init__ method, yet it exists
Out[28]:
The __init__
function is often used to execute something that takes a bit more time than standard operation, maybe like connecting to a remote database and read out some data.
By generating an instance of that class, the data that was read out then stays alive within that object and can be accessed whenever required later on.
For more applicability, let's leave the Mammals alone for now and talk about how to apply classes to Planets, but keep them in mind for later when we discuss inheritance.
In [29]:
# Define the class. Planet is the name of the class.
class Planet:
# note how i can make an argument optional, just like for functions
def __init__(self, name, diameter=5000):
"""The initialization of my class.
This is a special method (function) that gets called every time you
create a new class. Every method in a class has "self" as the first
parameter. The additional parameters to __init__ are the class's
input parameters (in this case name and diameter). You can set a
default value to each parameter (diameter has a default of 5000).
"""
self.name = name
self.diameter = diameter
def __str__(self):
s = "This is {} with a diameter of {}".format(self.name, self.diameter)
return s
# def __repr__(self):
# return self.__str__()
To recap:
To use a class you've written, you first need to create an "instance" of the class. Very much like with lists or dictionaries (in fact, dictionaries and lists are classes!). Some classes have required input parameters. Some classes have optional input parameters. Some classes have no input parameters at all.
In [30]:
# Create 2 planets. The first by passing in as input the name and diameter.
# With the second planet we just pass in the name so the diameter takes on
# the default value (5000 in this case).
planet1 = Planet("Crypton", 13000)
planet2 = Planet("Eternia")
In [31]:
print(planet1)
In [32]:
planet1
Out[32]:
In [33]:
# Define the class. Planet is the name of the class.
class Planet:
# note how i can make an argument optional, just like for functions
def __init__(self, name, diameter = 5000):
"""The initialization of my class.
This is a special method (function) that gets called every time you
create a new class. Every method in a class has "self" as the first
parameter. The additional parameters to __init__ are the class's
input parameters (in this case name and diameter). You can set a
default value to each parameter (diameter has a default of 5000).
"""
self.name = name
self.diameter = diameter
def __str__(self):
s = "This is {} with a diameter of {}".format(self.name, self.diameter)
return s
def __repr__(self):
return self.__str__()
In [34]:
p = Planet('Rubycon', 10000)
p
Out[34]:
In [35]:
class Mammal:
n_of_legs = 0
noise = None
def make_noise(self):
print(self.noise)
class Dog(Mammal):
n_of_legs=4
noise = 'bark'
In [36]:
dog = Dog()
In [37]:
dog.make_noise()
The Dog
class has inherited the method make_noise
from the Mammal class, because we assume all mammals make some kind of noise.
The Dog classes fixes the noise to the pre-defined noise and from now on, we would not have to deal with setting the noise anymore, because the Dog class specizalized
it for us.
In [46]:
class Earth(Planet):
def __init__(self):
""" This is the initialization method for Earth. This will run every time
an Earth is created. Notice there are no input parameters.
"""
# This is the initialization method for the mother class Planet.
# Notice we pass in "Earth" and "12700".
# Now every Earth will have the name "Earth" and a diameter of "12700".
super().__init__("Earth", 12700)
print("THIS IS EARTH!")
# Here we are adding more variables (data). These belong to Earth and
# not Planet. Again, the "self" in front of the variable allows us to
# use these variables anywhere in this class.
self.oceans = ["Pacific", "Atlantic", "Indian", "Southern", "Artic"]
self.continents = ["North America", "South America", "Antarctica", \
"Africa", "Europe", "Asia", "Australia"]
In [47]:
# Create a new Earth
earth = Earth()
In [48]:
earth.diameter
Out[48]:
In [49]:
earth
Out[49]:
In [50]:
# Print the Earth's oceans list and continents list. Note that calling the
# methods found in the Earth class look the same as calling methods found in
# the Planet class.
print("Ocean List: ", earth.oceans)
print("Continent List:", earth.continents)
In [51]:
earth.mass = 6e24
In [52]:
earth.__dict__
Out[52]:
In [ ]: