Classes & Object Oriented Programming

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.

Today: Recap basic structure, nomenclature, __call__()

Next few lectures/tutorials: applications.

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]:
<ufunc 'sin'>
Arrays are also objects:

In [4]:
x = np.arange(10)
Every array has many attributes, such as

In [5]:
x.size       # size is an attribute: just a number


Out[5]:
10

In [7]:
x.mean()     # mean is an attribute: a method


Out[7]:
4.5

Anytime you use "dot-access" (e.g., np.sin) the thing after the dot is an attribute. It could be a number, a function, an array, etc.

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!
Test a single value (note the self argument is not passed when the class is called):

In [9]:
test = Line(1, 5)  # This sets the slope and intercept
test         # and creates an instance of Line


Out[9]:
<__main__.Line at 0x115c891d0>
The variable "test" is now **an instance of the Line class** (We have not calculated a y for a give x yet.) We can access its attributes like so:

In [10]:
test.m, test.b


Out[10]:
(1, 5)
We can call it like a function too (uses __call__ method):

In [12]:
test(2)  # Now we calculate a y for a given x.


Out[12]:
7
Note again that "self" is not passed as an argument. Now make a table of values:

In [15]:
print(test.table(0, 4, 5))


           0            5
           1            6
           2            7
           3            8
           4            9

We could create an instance of Line, and build the table in a single line:

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.


           0            5
           1            6
           2            7
           3            8
           4            9

A Class for Parabolas

$$y = ax^2 + bx + c$$

Note that a straight line is a special case of this where $a = 0$


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]:
13

Q. Before we do this, let's review: what are the 1, 1, 1, and x=3 values?


In [20]:
# Make a table of values
print(test.table(0, 5, 6))


           0            1
           1            3
           2            7
           3           13
           4           21
           5           31

Q. What are the 0, 5, 6 values?

Which is equivalent to


In [21]:
# Q. What does the next line do?

print(Parabola(1, 1, 1).table(0, 5, 6))


           0            1
           1            3
           2            7
           3           13
           4           21
           5           31

A Class for Parabolas Using Inheritance

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).
Now, use it:

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)


11
1
2
3

Q. How would we access b and c? (That is, b and c are attributes of Parabola, and therefore test. How can we see what those attributes are?)


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]:
(1, 2, 3)

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]:
['__call__',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'a',
 'b',
 'm',
 'table']

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))


           0            3
           1            6
           2           11
           3           18
           4           27
           5           38

To here -- Lecture 1.

Review:

Q. Which is the parent class and which is the child class?

Q. Can you think of any disadvantages of using inheritance?

Review -- Jargon summary:

  • Object-oriented programming

    • Python programming is always object oriented, so this phrase really refers to class-based programming.
  • Class

    • It's the definition for an object. A collection of related data and/or methods. But not alive unless instantiated!
  • Object

    • The instance of a class. Instances of the same class are independent of each other.
  • Attributes

    • Data or methods that belong to objects.
  • Constructor

    • A special method that initializes a class instance.
  • Inheritance

    • Passing functionality from a parent class to a child class.
    • This is similar to importing everything from a module,

i.e.,

from ___ import * 

so be careful! You might get more than you expect.

Being Careful: Checking occurance for instances, checking for subclasses, and checking the class type

An instance is an occurance (usage) of a user-defined object. The command: isinstance(i, t) checks whether instance i is of class type t:

In [30]:
l = Line(0, 1)
isinstance(l, Line)

# Q. What should the output be?


Out[30]:
True

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]:
True

In [32]:
# Q. And this?

isinstance(l, Parabola)


Out[32]:
False
We can also test whether classes are subclasses:

In [33]:
# Is Parabola a subclass of Line?

issubclass(Parabola, Line)


Out[33]:
True

In [34]:
# Q. Should this be true or false?
issubclass(Line, Parabola)


Out[34]:
False

A couple of attributes of classes:

Every instance has an attribute __class__ that holds the type of class.

In [35]:
# This should tell us that instance p is a Parabola type of class:

p.__class__


Out[35]:
__main__.Parabola
Q. How can we tell whether the attribute __class__ is Parabola using Boolean logic?

In [36]:
p.__class__ == Parabola


Out[36]:
True
There is also an attribute __name__ that contains the class name string:

In [38]:
p.__class__.__name__


Out[38]:
'Parabola'

In [39]:
# You can see it's a string from the tick
# marks, but also:

type(p.__class__.__name__)


Out[39]:
str

Careful Distinction: Attribute vs. Inheritance

a.k.a. the infamous "has a ..." vs "is a ..." question.


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]:
11

In [43]:
# Q. Verifying:  Is test an instance of Parabola?

isinstance(test, Parabola)


Out[43]:
True

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]:
True
As we have been discussing, there are some disadvantages to inheritance (e.g., the appearance of attributes from the parent class can be confusing). Instead, we could make class Parabola have a class Line instance as an attribute:

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]:
11

In [49]:
# Is this still true?

isinstance(test, Parabola)


Out[49]:
True

In [50]:
# Is this still true?

issubclass(Parabola, Line)


Out[50]:
False

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)
So, Parabola has not inherited table from Line.

In [51]:
# To see this, list the attributes of Parabola with dir(Parabola):

dir(Parabola)


Out[51]:
['__call__',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']

In [52]:
# BUT, test is an instance of Parabola and has an attribute line:

dir(test)


Out[52]:
['__call__',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'a',
 'b',
 'c',
 'line']

In [53]:
# AND line has an attribute table:

dir(test.line)


Out[53]:
['__call__',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'b',
 'm',
 'table']

In [54]:
# Hence:

print( test.line.table(0, 5, 6))


           0            3
           1            5
           2            7
           3            9
           4           11
           5           13


In [55]:
# And the attributes of test.line have attributes!

dir(test.line.m)


Out[55]:
['__abs__',
 '__add__',
 '__and__',
 '__bool__',
 '__ceil__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__divmod__',
 '__doc__',
 '__eq__',
 '__float__',
 '__floor__',
 '__floordiv__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__index__',
 '__init__',
 '__int__',
 '__invert__',
 '__le__',
 '__lshift__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__neg__',
 '__new__',
 '__or__',
 '__pos__',
 '__pow__',
 '__radd__',
 '__rand__',
 '__rdivmod__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rfloordiv__',
 '__rlshift__',
 '__rmod__',
 '__rmul__',
 '__ror__',
 '__round__',
 '__rpow__',
 '__rrshift__',
 '__rshift__',
 '__rsub__',
 '__rtruediv__',
 '__rxor__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__truediv__',
 '__trunc__',
 '__xor__',
 'bit_length',
 'conjugate',
 'denominator',
 'from_bytes',
 'imag',
 'numerator',
 'real',
 'to_bytes']

In [57]:
# The slope of the line is m:

test.line.m


Out[57]:
2

In [58]:
# Focusing on two of the attributes, 
# m is a real number:

test.line.m.real


Out[58]:
2

In [59]:
# So, its imaginary component is zero:

test.line.m.imag


Out[59]:
0

In [ ]:
type(test.line.m)

In [60]:
a = 5

In [61]:
a.real


Out[61]:
5

In [62]:
a.imag


Out[62]:
0

In [ ]: