Object Oriented Programming

Video

Theory

In python, there are two fundamental concepts - functions, which we have already discussed, and objects - which encapsulate everything else. This should be inherently familiar to category theorists - there is great power in defining what is and how it relates.

Languages with this model are called object-oriented, and allow for object oriented programming - a philosophical take on programs that ties data together with its representation and the things you can do with it.

Object oriented programming is particularly valuable in mathematics because of mathematicians' tendency to define new objects to contain our ideas. OOP allows us to define analogous objects in Python, and to describe their behaviors.

Objects

In Python, everything is an object. Even functions are objects - objects which contain behaviors. When we define a new integer or float, it is an object.

Objects have methods - a special kind of function that is tied to their behavior.

We have already seen an example of a method - lists have the .append method, allowing you to add elements:

example_list = []
example_list.append(1)
assert example_list == [1]

To see what methods an object has, you can use the dir command - for example,

a = 5
dir(a)

If you do that, you will see a large number of methods which begin and end with double underscores.

We have actually been using these methods the entire time - python defines what are called magic methods, which are methods that have certain behaviors. For example, if we add 2 to a value a, that is actually converting it to:

a=3
assert a+2 == a.__add__(2)

Magic methods start and end with double underscores. You can look in the documentation for a full list, but here is a table of relevant magic:

method name example
__init__ Initialize a=int(3.0)
__add__ Addition a+3
__sub__ Subtraction a-3
__mul__ Multiplication a*3
__truediv__ Division a/3
__floordiv__ Floor Division a//3
__mod__ Modulo a%3
__pow__ Power a%3
__eq__ Equality a==3
__ne__ Nonequality a!=3
__lt__ Inequality a<3
__gt__ Inequality a>3
__le__ Inequality a<=3
__ge__ Inequality a>=3
__neg__ Negation -a
__abs__ Absolute Value abs(a)
__str__ To String str(a)
__int__ To Integer int(a)
__float__ To Float float(a)
__repr__ Reproduction repr(a)

Note that in an operator, the magic is called on the left operand for infix operators. Each infix operator also has a right version - for example, a.__rmul__ - which is only called if the method of the left operand fails. (or in some complicated subclass shenanigans.)

This magic is particularly useful for mathematicians, as we often want to use our familiar symbols on our new imagined objects.

Classes

Another familiar concept to mathematicians is Classes. A more general concept than sets - you may imagine the class of all sets that contain themselves - we have a similar relationship with members: 3 is an object; int is a class.

When you want to define new types of objects, you first define a new class.

In Python, this is done with the class statement:

# A class starts with a class statement, and a name. In python, the convention is PascalCase - no separation between words, with the first letter capitalized.
class Rational:
    # A class should contain an __init__ method, which tells Python how to make it.

    #`self` refers to the object being created.
    def __init__(self,numerator, denominator):

        # By using the self keyword, we can store values as "properties."
        self.numerator = numerator
        self.denominator = denominator

Representing and Printing Classes

Try creating a Rational, and you will see a very unhelpful string.

To spice it up, we will define the __repr__ - and eventually __str__ - magic.

    def __repr__(self):
        return "Rational({},{})".format(self.numerator, self.denominator)

Try it out and now you will see that it prints out a reproduction of our rational - that is, a Python statement that would create our object. That is what __repr__ is for.

Notice that I used the string method .format. .format is a quick python templating system, allowing you to fill in the {} in a string. (to escape a {, write {{).

There are lots of customization options for string formatting, but the default is to use the __str__ of the object being formatted - or the __repr__, if it doesn't have one.

Plain {} are positional arguments, just like normal functions. We can make them named arguments by putting a variable name in the function, and passing a named parameter to .format:

"{},{third},{}".format(1,2,third=3)

Attributes are stored in the __dict__ magic. With the ** Dictionary Unpacking magic, we can turn a dictionary into named arguments:

    def __repr__(self):
        return "Rational({numerator},{denominator})".format(**self.__dict__)

Methods to our Magic

Methods are functions defined within the class, that always take self as an implied first element.

In fact, when we called __add__ earlier, that was still a shorthand:

a = 3
assert a+2 == int.__add__(a,2)

Remember we can get the type - the class that a value is an instance of - with the type function. More generally, addition is implemented as:

a = 3
assert a+2 == type(a).__add__(a,2)

We are going to add a method - __add__ - to this example Rational class.

    def __add__(self,other):
        new_numerator = self.numerator*other.denominator+self.denominator*other.numerator
        new_denominator = self.denominator*other.denominator
        return Rational(new_numerator, new_denominator)

This works great for adding rationals, but what if someone tries adding something else? Well, the first thing we can do is tell them we haven't written it yet:

    def __add__(self,other):
        if isinstance(other, Rational):
            new_numerator = self.numerator*other.denominator+self.denominator*other.numerator
            new_denominator = self.denominator*other.denominator
            return Rational(new_numerator, new_denominator)
        raise NotImplementedError("Cannot Add Types {} and {}".format("Rational",type(other)))

Now we're safer - we are honestly warning people about what we can and cannot do when they try to do it.

Let's add a bit of code to handle integers:

    def __add__(self,other):
        if isinstance(other, Rational):
            new_numerator = self.numerator*other.denominator+self.denominator*other.numerator
            new_denominator = self.denominator*other.denominator
            return Rational(new_numerator, new_denominator)
        if isinstance(other, int):
            new_numerator = self.numerator + other*self.denominator
            return Rational(new_numerator,new_denominator)
        raise NotImplementedError("Cannot Add Types {} and {}".format("Rational",type(other)))

Now let's add subtraction. At its core, subtraction of rationals is shorthand for addition of the negation of the other term:

def __sub__(self,other):
    return self+(-other)

Now we can subtract integers from our rationals! But to subtract other rationals, we need to tell it what the negation is.

def __neg__(self):
    return Rational(-self.numerator,self.denominator)

This lets us do Rational(1,2)+5, but what about 5+Rational(1,2)? For that, we need to define the right version of the operators:

def __radd__(self,other):
    return self+other
def __rsub__(self,other):
    return -self+other

Inheritance

Sometimes, we have chains of classes that inherit behavior. For example, all fields are euclidean domains, but not all euclidean domains are fields.

What mathematicians call class inclusions, programmers call Inheritance.

For example, if we have already defined a Euclidean domain class EuclideanDomain, we can define:

class Field(EuclideanDomain):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)

Now any functionality we have created that works on Euclidean Domains will automatically work on Fields!

Note the super().__init__(*args,**kwargs). super is another special function that allows you to call methods of parent objects - in this case, to do all the Euclidean domain interaction.

*args and **kwargs are catchalls - for the positional arguments, and keyword arguments, respectively. They store these values in a list args and a dictionary kwargs.

Multiple Inheritance

Just like in Mathematics, class inheritance isn't always a neat ascending chain.

We know that Dedekind Domains are Integral Domains, but not necessarily Unique Factorization Domains - nor are Unique Factorization Domains necessarily Dedekind Domains. They don't inherit behavior from eachother, even though they share a parent class.

However, Principal Ideal Domains are both Dedekind Domains and Unique Factorization Domains - and can (and should) inherit all the benefits and behaviors of both.

In Python, this is called Multiple Inheritance:

class PrincipalIdealDomain(DedekindDomain,UniqueFactorizationDomain):
    def __init__(self, *args,**kwargs):
        super().__init__(*args,**kwargs)

Note that we only need to call super once; it will go through all the classes in the order you parented them. It will even continue up the tree - if you have super in each parent, as you should - and only initialize once for each parent, even if it is inherited from multiple times.

isinstance

We have covered checking type using type, but you'll notice doing that would be prohibitively expensive if all we want is to know if our ring is a Unique Factorization Domain.

Enter isinstance, a versatile type-checker that scans the entire parent tree for the desired class.

For example,

isinstance(R,UniqueFactorizationDomain)

Would be true on any of our derived classes - EuclideanDomain, PrincipalIdealDomain, Field and FiniteField included.

That way we can check for the desired behavior - unique factorization, and any methods we have defined relying only on that - without worrying about all the extra stuff.

Worksheet

Today's worksheet has you finishing up the Rational class, and defining some more class magic.

Department of Mathematics, Purdue University
150 N. University Street, West Lafayette, IN 47907-2067
Phone: (765) 494-1901 - FAX: (765) 494-0548
Contact the Webmaster for technical and content concerns about this webpage.
Copyright© 2018, Purdue University, all rights reserved.
West Lafayette, IN 47907 USA, 765-494-4600
An equal access/equal opportunity university
Accessibility issues? Contact the Web Editor (webeditor@math.purdue.edu).