Introduction to objects and classes

  • Introduce myself
  • Express my desire to have people ask me questions

Why is abstraction important?

  • Object oriented programming is a form of abstraction
  • Allows the programmer to express what they want, not how they want it to happen
  • Software is about modelling things
  • Improves clarity and correctness of our models
$ cat > hello.asm
        SECTION .data
msg:    db `Hello world!\n`
len:    equ $-msg

        SECTION .text

        global _start
_start:
        mov     rax,1
        mov     rdi,1
        mov     rsi,msg
        mov     rdx,len
        syscall
        mov     rax,60
        xor     rdi,rdi
        syscall

$ nasm -f elf64 hello.asm
$ ld hello.o -o hello
$ ./hello 
Hello world!
  • This is a program with almost no abstractions
  • Hello world for linux x64 in nasm
  • Very literally says "How to print hello world"
  • Hard to guage intent

In [1]:
print('Hello World!')


Hello World!
  • This is the same program with a high level of abstraction
  • Python
  • Expresses exactly what we want to do
  • Easy to guage the intent
  • Same output as the x64 nasm
  • We don't need to worry about how it happens
  • Allows us to name this concept to be reused later

Definitions

  • We need to define some primitives before we discuss OOP

State

Facts that describe something.

  • Timmy the cat has orange fur
  • Timmy is 3 years old
  • Timmy cat has long fur

Behavior

Actions that something can take.

  • Cats can run
  • Cats can purr
  • Cats can jump

Object

Something that has state and behavior.

  • A cat can be thought of as an object
  • A conceptual grouping

Class

A blueprint or specification for objects.

  • The concept of cats can be a class

Exampe: list

  • A list is an ordered collection of other things
  • Like a grocery list

[1, 2, 3]

  • This is an object

  • It has state:

    • Length 3
    • Contains 1, 2, and 3
  • It has behavior

    • We can add things to our list
    • We can remove things from our list
    • We can change the order
    • various other operations

Rationals

  • Also called fractions
  • A pair of integers
  • Numerator
  • Denominator
  • Denominator cannot be zero
  • Let's try to define a very simple model for this

In [6]:
# x = 1 / 3
x_numer = 1
x_denom = 3
  • Very simple representation for rational
  • Two variables to represent the two pieces of state

In [7]:
# y = 3 / 6
y_numer = 3
y_denom = 6
  • Same as x
  • Notice that this is a valid ratio
  • Not the most efficient form

In [9]:
# add x and y
res_numer = (x_numer * y_denom) + (y_numer * x_denom)
res_denom = x_denom * y_denom

res_numer, res_denom


Out[9]:
(15, 18)
  • Here is an algorithm to add x and y
  • Result is also not the most efficient
  • Let's clean that up

In [10]:
from fractions import gcd

res_gcd = gcd(res_numer, res_denom)
res_numer = res_numer // res_gcd
res_denom = res_denom // res_gcd

res_numer, res_denom


Out[10]:
(5, 6)
  • Here we can reduce the ratio into a more simple form
  • Still equivalent
  • Notice that we need to keep track of these two value seperatly
  • Easy to mess something up
  • No in-code way of keeping them together

In [11]:
def make_rational(numer, denom):
    """Create a pair of integers representing a rational.
    
    Parameters
    ----------
    numer : int
        The numerator of the rational.
    denom : int
        The denominator of the rational.
        
    Returns
    -------
    rational : tuple
        A pair of integers.
    """
    return (numer, denom)
  • Group the numerator and denominator into a single entity
  • Allows us to pass them around as one unit
  • They cannot get mixed up now

In [12]:
# x = 1 / 3
x = make_rational(numer=1, denom=3)

x


Out[12]:
(1, 3)
  • x is now a single entity that has the numerator and the denominator
  • This is closer to our model of rationals
  • Morally a single value

In [13]:
# y = 3 / 6
y = make_rational(3, 6)

y


Out[13]:
(3, 6)
  • We can omit the numer= and denom=
  • same concept as x

In [15]:
# res = x + y
res = make_rational((x[0] * y[1]) + (y[0] * x[1]), x[1] * y[1])

res


Out[15]:
(15, 18)
  • We lose some readability here
  • x[0] -> x subscript 0 -> x sub 0
  • The first element in x
  • Need to manually manage the numerator and denominator still
  • Still hard to reason about

In [17]:
res_gcd = gcd(res[0], res[1])
res = make_rational(res[0] // res_gcd, res[1] // res_gcd)

res


Out[17]:
(5, 6)

In [21]:
def add_rationals(x, y):
    """Adds two rational numbers.
    
    Parameters
    ----------
    x : tuple
        The first rational.
    y : tuple
        The second rational.

    Returns
    -------
    sum : tuple
        The result of x + y
    """
    return (x[0] * y[1]) + (y[0] * x[1]), x[1] * y[1]
  • Add a layer of abstraction by naming the algorithm
  • Consumers don't need to know how to add two rationals
  • More expressive

In [22]:
x = make_rational(1, 3)
y = make_rational(2, 4)
z = make_rational(3, 7)
  • Here we can create a bunch of rationals

In [23]:
sum_xy = add_rationals(x, y)
sum_xz = add_rationals(x, z)
sum_yz = add_rationals(y, z)
  • This code is very expressive
  • Tells us what we are trying to do
  • Not how we want to do it
  • No longer need to identify the add algorithm

In [26]:
print(sum_xy)
print(sum_xz)
print(sum_yz)


(10, 12)
(16, 21)
(26, 28)
  • Still get the same results
  • Notice that 10 / 12 amd 28 / 28 are not reduced
  • We want to provide a way to do that
  • We can use the same abstraction rules as before

In [27]:
def reduce_rational(r):
    """Reduce a rational number to the most simple form.
    
    Parameters
    ----------
    r : tuple
        A rational.

    Returns
    -------
    reduced : tuple
        The most reduced form of r.
    """
    g = gcd(r[0], r[1])
    return r[0] // g, r[1] // g
  • Allows us to name this behavior
  • Again, we don't need to know how to do this

In [7]:
x = make_rational(1, 3)
y = make_rational(2, 4)
z = make_rational(3, 7)


x + y = 5 / 6
x + z = 16 / 21
y + z = 13 / 14

In [ ]:
sum_xy = add_rationals(x, y)
sum_xz = add_rationals(x, z)
sum_yz = add_rationals(y, z)

In [ ]:
print(reduce_rational(sum_xy))
print(reduce_rational(sum_xz))
print(reduce_rational(sum_yz))
  • Now things are reduced
  • We are responsible for keeping the data reduced
  • Still no way to make this happen for us

In [28]:
def rational_to_string(r):
    """Convert a rational number to a human readable
    string representation.
    
    Parameters
    ----------
    r : tuple
        A rational.

    Returns
    -------
    cs : str
        A string representation of r.
    """
    return '{numer} / {denom}'.format(numer=r[0], denom=r[1])
  • Behavior to convert the data into human readable form
  • Isolates the caller from the implementation of the data

In [29]:
x = make_rational(1, 3)
rational_to_string(x)


Out[29]:
'1 / 3'
  • Let's us represent rationals the way we want
  • More expressive for readers.
  • We have identified 2 pieces of state for rationals
    1. The numerator
    2. The denominator
  • We also have identified 4 behaviors of a rational
    1. Setting up its state
    2. Being added with another rational
    3. Being reduced to the most simple form
    4. Being converted to a human readable form
  • We need a way to group these things together
  • We can group these with a class

In [32]:
class Rational:
    """A rational number.
    
    Parameters
    ----------
    numer : int
        The numerator.
    denom : int
        The denominator.
    """
    def __init__(self, numer, denom):
        self.numer = numer
        self.denom = denom
        
    def add(self, other):
        return Rational(
            numer=(self.numer * other.denom) + (other.numer * self.denom),
            denom=self.denom * other.denom,
        )
    
    def eq(self, other):
        return self.numer == other.numer and self.denom == other.denom
    
    def reduce(self):
        g = gcd(self.numer, self.denom)
        return Rational(
            numer=self.numer // g,
            denom=self.denom // g,
        )
    
    def __repr__(self):
        return '{numer} / {denom}'.format(numer=self.numer, denom=self.denom)
  • This is the class that defines Rational objects
  • The first line is the class statement
  • The indented region following is the class body
  • Functions in the body define the behavior
  • We call these behaviors methods
  • The first behavior is special (__init__)
  • This behavior is special because it tells the Rational object how to initialize its state
  • self represents the state of our object
  • store the numerator and denominator on the object
  • This is like make_rational
  • Now we have add
  • we can drop the _rationals suffix because we now have some context
  • This behaviour depends on our state, and the state of another rational other
  • Same algorithm as before
  • More clear because we can name the state
  • Returns another rational
  • This keeps the behavior linked to the new state
  • We have added an eq behavior for checking equality
  • We also have reduce_rational
  • We can drop the suffix
  • Also returns a rational
  • __repr__ is also special
  • This is the implementation of rational_to_str
  • This tells the class how objects should display themselves
  • The python interface methods will be covered by Cliff in the next talk

In [35]:
x = Rational(1, 3)
y = Rational(2, 4)
  • Here x and y instances of Rational

In [36]:
sum_xy = x.add(y)

sum_xy


Out[36]:
10 / 12
  • x.add(y) says: "Invoke the add behavior of the x object with y as the second argument"
  • This means that we should look at the add method of the Rational class
  • show that self is the state of x
  • Human readable representation

In [37]:
sum_xy.reduce()


Out[37]:
5 / 6
  • Here we invoke the reduce method of sum_xy
  • No other arguments

In [38]:
x = Rational(1, 2)
y = Rational(2, 4)

print(x)
print(y)


1 / 2
2 / 4
  • We have some issues with our model
  • These appear differently
  • These are conceptually the same

In [39]:
x.eq(y)


Out[39]:
False
  • This actually causes problems

In [40]:
z = Rational(1, 0)
z


Out[40]:
1 / 0
  • We can also create invalid rationals
  • This is not a rational number but our model allows it
  • We need to update our blueprint (class)
  • Provide some constraints

In [47]:
class Rational:
    """A rational number.
    
    Parameters
    ----------
    numer : int
        The numerator.
    denom : int
        The denominator.
    """
    def __init__(self, numer, denom):
        if denom == 0:
            raise ValueError('denom cannot be zero')

        g = gcd(numer, denom)
        self.numer = numer // g
        self.denom = denom // g
        
    def add(self, other):
        return Rational(
            numer=(self.numer * other.denom) + (other.numer * self.denom),
            denom=self.denom * other.denom,
        )
    
    def eq(self, other):
        return self.numer == other.numer and self.denom == other.denom
    
    def __repr__(self):
        return '{numer} / {denom}'.format(numer=self.numer, denom=self.denom)
  • Here we guard against the invalid rational
  • Classes can constrain the state to better match the concept being modeled
  • This makes our class more accurate
  • Classes can also manage our state for us
  • We remove the reduce behavior
  • Rationals are always reduced

In [48]:
x = Rational(1, 2)
y = Rational(2, 4)

x, y


Out[48]:
(1 / 2, 1 / 2)
  • Now x and y are displayed the same
  • This is a more correct display

In [44]:
x.eq(y)


Out[44]:
True
  • The state normalization corrects some issues
  • No longer need to manually reduce
  • More abstract than "pair of integers"

In [45]:
z = Rational(1, 0)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-45-3b3cf4810869> in <module>()
----> 1 z = Rational(1, 0)

<ipython-input-42-b153d814d8ba> in __init__(self, numer, denom)
     11     def __init__(self, numer, denom):
     12         if denom == 0:
---> 13             raise ValueError('denom cannot be zero')
     14 
     15         g = gcd(numer, denom)

ValueError: denom cannot be zero
  • Can no longer create invalid rationals
  • Better model for rationals
  • No longer need to worry about this problem

Subclassing

  • What is a subclass
  • A subclass is a special case or kind of another class
  • Example: Kittens are a sub class of cats
  • All kittens are cats
  • Kittens can do the same things as cats
  • Kittens do some of those things differently
  • Kittens can some things that cats cant
  • Cat is a super class of kitten
  • Not all cats are kittens
  • Allows us to represent hierarchies

Integer as a subclass of Rational

  • Integers are rationals with a denominator of 1
  • Why do we want to represent an integer as a subclass of rational?
  • Integers can do everything that rationals can
  • Integers do some things differently
  • Integers have exclusivly more state (let's pretend)
  • Integers can be even or odd, non-integers cannot

In [55]:
class Integer(Rational):
    """A ratio whose denominator is 1.
    
    Parameters
    ----------
    value : int
        The value of this integer.
    """
    def __init__(self, value):
        super().__init__(value, 1)
        self.is_odd = bool(value & 1)
        self.is_even = not self.is_odd
        
    def add(self, other):
        if isinstance(other, Integer):
            return Integer(self.numer + other.numer)
        else:
            return super().add(other)
        
    def __repr__(self):
        return str(self.numer)
  • The (Rational) here means that Integer is a subclass of Rational

  • Be default, we get all of the behavior of Rational

  • We are overriding the initialization behavior

  • We still refer to the original behavior with super
  • This says, "Call our super class's __init__
  • Add behavior but always refer to the super class's implementation
  • Integers have exclusivly more state
  • is_even and is_odd

  • Do the same with add

  • Still reference the original add

  • Notice we didn't change eq

  • We can still use eq just fine

In [56]:
r = Rational(1, 2)
n = Integer(1)

print(isinstance(r, Rational))
print(isinstance(n, Integer))


True
True
  • Let r be a Rational
  • Let n be an Integer
  • remember that an object created from a class is refered to as an instance of the class
  • r is an instance of Rational -> r is a Rational
  • n is an instance of Integer -> n is an Integer

In [52]:
print(isinstance(n, Rational))
print(isinstance(r, Integer))


True
False
  • because Integer is a subclass of Rational, n is a Rational

  • subclassing is a directional relationship

  • r does not become an Integer

In [63]:
n.is_even


Out[63]:
False

In [59]:
n.is_odd


Out[59]:
True
  • We can retrieve our new state

In [60]:
n.eq(r)


Out[60]:
False

In [61]:
n.eq(Integer(1))


Out[61]:
True
  • We can still use the eq behavior.
  • This was inherited from Rational
  • Subclasses may also add behavior.
  • Sometimes our special cases can do more things

Natural as a subclass of Integer

  • A natural number is an integer that is greater than zero
  • Why do we want this?
  • All natural numbers are integers
  • Natural numbers can do exlusivly more things than arbitrary integers

In [72]:
class Natural(Integer):
    """An integer that is greater than zero.
    
    Parameters
    ----------
    value : int
        The value of the natural.
    """
    def __init__(self, value):
        if value <= 0:
            raise ValueError('value must be greater than zero')
        super().__init__(value)
        
    def factorial(self):
        fac = 1
        for n in range(2, self.numer + 1):
            fac *= n
        return fac
  • Natural numbers have all the same state as Integers
  • They can also have exclusivly more state than Rationals
  • Here, we define factorial as a method of Natural.
  • Natural numbers can compute their factorial; however, not all integers can do this

Modularity through subclassing

  • Modularity is the ability to break up code into smaller pieces
  • These pieces can then be used later to solve different kinds of problems
  • Allows others to define general purpose behavior for us
  • Use more general things to solve specific problems
  • This is one of the reasons OOP is so popular
  • Allows us to build on eachother's work
  • Web stuff is complicated
  • We probably don't care about that
  • We just want to write our service
  • A webserver is basically a mapping of endpoints to function calls
  • When someone goes to an endpoint, call a function
  • Send the result to the user
  • Flask is a web framework
  • Flask provides tools for web programmers to make a web server
  • Uses abstractions like objects and classes to represent a web server
  • We can define a class to manage one of these end points.

In [79]:
from flask.views import MethodView


class HelloEndpoint(MethodView):
    """An endpoint handler greets the user.
    """
    def get(self):
        return 'hello from my new webserver'
  • Here we can import a class from flask.
  • Importing is like bringing in a class someone else wrote so that we can use it.
  • Remember that a class is a blueprint for an object
  • We make a new subclass of MethodView
  • This subclass extends MethodView by adding our own get behavior
  • MethodView knows what to do with this.

In [80]:
from flask import Flask

app = Flask('my_app')
app.add_url_rule('/hello', view_func=HelloEndpoint.as_view('hello'))
  • Let's jump to an interactive session quick
  • Here we are going to import the Flask class from flask
  • app is now an instance of Flask
  • app is an object that represents a web server
  • Here we can tell the server to use our new endpoint class to manage our /hello endpoint
  • Remember, webservers are really complicated
  • There is networking
  • listening to sockets
  • HTTP verbs
  • Some unicode encoding stuff that Ned can tell you about
  • A lot of other stuff that I don't even know
  • It is beautiful because I don't need to know how this works
  • I can build on the abstractions provided by others
  • Show the endpoint in my browser
  • it just works (tm)

In [21]:
class LoggingRestEndpoint(HelloEndpoint):
    def get(self):
        print('logging: get')
        return super().get()
  • There is nothing special about the MethodView class
  • We can extend our own subclasses
  • We can then give these to our friends so they can use our abstractions

Thank you!

github.com/llllllllll/boston-python-oop-talk

(10 lowercase L's)