Object-oriented programming or programming using classes is an abstraction that tries to apply the same abstraction rules used for categorizing nature to programming techniques.
This has the advantage that the process for creating a computer program that should simulate a detail of nature can apply similar principles than nature seems to apply. Hence, the thought process for creating a simulated world in your program becomes rather similar to the thought process of understanding nature via abstraction.
Let me show you by example what this abstract description means.
When classifying fauna, one important class for example, are mammals. But how did we define what mammals are? We do this classification abstraction by identifying features that are similar between the different animals, and once we are sure to have found a defining feature that is only true or applicable for this one class, we can save time by just checking or using this feature for identification.
A class is really just a container for related functions and values.
Classes are put together in families. The point of this is to make it easier to modify and extend programs. A family of classes is known as a class hierarchy
A class hierarchy has parent classes and child classes. Child classes can inherit data and methods from parent classes.
You may also hear parent classes referred to as super-classes or base-classes and child classes referred to as sub-classes or derived-classes
When people talk about object-oriented programming they are probably referring to programs that are class-based. Believe it or not, we have been doing object-oriented programming for a long time now because everything in Python is an object. This is why Python programmers typically reserve "object-oriented" to mean "programming with classes."
In [1]:
import numpy as np
The variable "np" is an object with many attributes such as:
In [3]:
np.sin
Out[3]:
In [4]:
x = np.arange(10)
In [5]:
x.size # size is an attribute: just a number
Out[5]:
In [7]:
x.mean() # mean is an attribute: a method
Out[7]:
An attribute could also be a class instance.
Let's learn about classes with a simple example: a class for straight lines.
$y = mx + b$
A class for parabolas will build on this.
In [13]:
class Line:
# __init__ is a special method used to create the class.
# It is referred to as "the constructor."
def __init__(self, m, b):
# self must be the first argument in every class method
# m and b are attributes of the Line class
self.m = m
self.b = b
# The special method __call__ will allow us to call Line
# with the syntax of a function.
# It is referred to as the call operator.
def __call__(self, x):
return self.m * x + self.b
# A class method for tabulating results
def table(self, L, R, n):
"""
Return a table with n points at L <= x <= R.
"""
s = '' # This is a string that will contain table lines
import numpy as np
for x in np.linspace(L, R, n):
# The self call yields self.m*x + self.b
y = self(x)
s += '%12g %12g\n' % (x, y)
return s
# Note that there is more than one return statement!
In [9]:
test = Line(1, 5) # This sets the slope and intercept
test # and creates an instance of Line
Out[9]:
In [10]:
test.m, test.b
Out[10]:
In [12]:
test(2) # Now we calculate a y for a given x.
Out[12]:
In [15]:
print(test.table(0, 4, 5))
In [16]:
print(Line(1, 5).table(0, 4, 5) )
# Table is an attribute, or function, or method of class Line
# Where 1 is the slope, 5 is the y-intercept,
# 0 to 5 is the range, and there are 5 points.
In [18]:
import numpy as np
class Parabola:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __call__(self, x):
return self.a * x**2 + self.b * x + self.c
def table(self, L, R, n):
"""
Return a table with n points at L <= x <= R.
"""
s = ''
for x in np.linspace(L, R, n):
y = self(x)
s += '%12g %12g\n' % (x, y)
return s
Test a single value:
In [19]:
test = Parabola(1, 1, 1) # We've created test, an instances of Parabola.
test1 = test(x=3) # Here we evaluate test at x = 3
# Q. What should test1 be?
test1
Out[19]:
In [20]:
# Make a table of values
print(test.table(0, 5, 6))
In [21]:
# Q. What does the next line do?
print(Parabola(1, 1, 1).table(0, 5, 6))
We can specify that class Parabola inherits all the code (attributes: data, functions...) from class Line by making the class statement
class Parabola(Line):
Thus, class Parabola is derived from class Line. Line is a superclass or parent class, Parabola is a subclass or child class.
(Note that if we implement the constructor and call operators in Parabola, they will override the inherited versions from Line.)
Aside: Any method in the superclass Line can be called with
super().methodname(arg1, arg2, ...)
Note that calling a method does not require self
as an argument, only defining them.
Unless you call a "class method" instead of an "object method", then there's no object around to attach things to, and one needs to provide self
. See difference shown below.
In [22]:
class Parabola(Line):
def __init__(self, a, b, c):
super().__init__(b, c) # Line stores b and c
self.a = a # a is a
def __call__(self, x):
return Line.__call__(self, x) + self.a*x**2
# When Parabola is called it returns a call to Line (+ a*x**2).
In [24]:
class Parabola(Line):
def __init__(self, a, b, c):
super().__init__(b, c) # Line stores b and c
self.a = a # a is a
def __call__(self, x):
return super().__call__(x) + self.a*x**2
# When Parabola is called it returns a call to Line (+ a*x**2).
In [25]:
# Test a single value:
test = Parabola(1, 2, 3) # Q. What does this do?
# And below self is test.
test1 = test(x=2) # Q. What does this do?
# (Note that the x= is not needed.)
print(test1)
print(test.a)
print(test.m)
print(test.b)
In [26]:
# test calculates a*x**2 + b*x + c,
# Line is a subclass of Parabola, and in Line
# m is b and b is c. So, the argument order
# is a, m, b.
# (Trace it!)
test.a, test.m, test.b
Out[26]:
To compare with later in the notebook, let's have a look at all attributes. It shows also the inherited attributes.
In [27]:
dir(test)
Out[27]:
In [28]:
# Hierarchy!
# Make a table of values:
# test is an attribute of Line,
# Line is a parent class or superclass of Parabola,
# and test is an instance of Parabola.
print(test.table(0, 5, 6))
Object-oriented programming
Class
alive
unless instantiated
!Object
instance
of a class. Instances of the same class are independent of each other.Attributes
Constructor
Inheritance
i.e.,
from ___ import *
so be careful! You might get more than you expect.
In [30]:
l = Line(0, 1)
isinstance(l, Line)
# Q. What should the output be?
Out[30]:
In [31]:
# MAKE SURE TO RUN ALL ABOVE CELLS FIRST!!
p = Parabola(1, 2, 3)
isinstance(p, Line)
# Q. And what about this?
Out[31]:
In [32]:
# Q. And this?
isinstance(l, Parabola)
Out[32]:
In [33]:
# Is Parabola a subclass of Line?
issubclass(Parabola, Line)
Out[33]:
In [34]:
# Q. Should this be true or false?
issubclass(Line, Parabola)
Out[34]:
In [35]:
# This should tell us that instance p is a Parabola type of class:
p.__class__
Out[35]:
In [36]:
p.__class__ == Parabola
Out[36]:
In [38]:
p.__class__.__name__
Out[38]:
In [39]:
# You can see it's a string from the tick
# marks, but also:
type(p.__class__.__name__)
Out[39]:
In [41]:
# What we have done so far is make class Parabola inherit class Line
# (by making Line an argument of Parabola in the class statement):
class Parabola(Line):
def __init__(self, a, b, c):
super().__init__(b, c) # Line stores b and c
self.a = a # a is a
def __call__(self, x):
# Recall equation: a*x**2 + b*x + c
return super().__call__(x) + self.a*x**2
test = Parabola(1, 2, 3)
test1 = test(x=2)
test1
Out[41]:
In [43]:
# Q. Verifying: Is test an instance of Parabola?
isinstance(test, Parabola)
Out[43]:
This also can be phrased as: "A Parabola object IS a Line object".
A more specialized one, but nonetheless!
In [44]:
# Q. Verifying: Is Parabola a subclass of Line?
issubclass(Parabola, Line)
Out[44]:
In [46]:
class Parabola: # Before "class Parabola(Line):", which
# made Parabola inherit the attributes of Line.
def __init__(self, a, b, c): # Same as before
self.line = Line(b, c) # Now Line will be an attribute of Parabola
# Before "Line.__init__(self, b, c)" constructed an instance of Line
self.a = a # Same as before
self.c = c
self.b = b
def __call__(self, x): # Same as before
return self.line(x) + self.a*x**2
# Before "return Line.__call__(self, x) + self.a*x**2",
# which returned an instance of Line evaluated at x.
# To summarize:
# 1. We have not made Parabola a subclass of line
# 2. Line is an attribute of Parabola
In [48]:
test = Parabola(1, 2, 3)
test1 = test(x=2)
test1
# And the result should be the same:
Out[48]:
In [49]:
# Is this still true?
isinstance(test, Parabola)
Out[49]:
In [50]:
# Is this still true?
issubclass(Parabola, Line)
Out[50]:
Now, the relationship has to be phrased as "The Parabola object HAS a Line object".
In [ ]:
# So, will this work? Why or why not?
test.table(0, 5, 6)
In [51]:
# To see this, list the attributes of Parabola with dir(Parabola):
dir(Parabola)
Out[51]:
In [52]:
# BUT, test is an instance of Parabola and has an attribute line:
dir(test)
Out[52]:
In [53]:
# AND line has an attribute table:
dir(test.line)
Out[53]:
In [54]:
# Hence:
print( test.line.table(0, 5, 6))
In [55]:
# And the attributes of test.line have attributes!
dir(test.line.m)
Out[55]:
In [57]:
# The slope of the line is m:
test.line.m
Out[57]:
In [58]:
# Focusing on two of the attributes,
# m is a real number:
test.line.m.real
Out[58]:
In [59]:
# So, its imaginary component is zero:
test.line.m.imag
Out[59]:
In [ ]:
type(test.line.m)
In [60]:
a = 5
In [61]:
a.real
Out[61]:
In [62]:
a.imag
Out[62]:
In [ ]: