Defining New Classes in Python

Classes are quite different in python than in other languages. One way in which they are different is that you can add attributes, or member variables and function, any time, even long after the class is defined.

Say we want to define a class to hold a node in a search tree.
The simplest class definition is

In [ ]:
class Node:
    pass

Python uses pass/or an empty statement or body.

This class has no attributes, right? Watch this.

In [ ]:
n = Node()
n
In [ ]:
n.a = 42
In [ ]:
n
In [ ]:
n.a
In [ ]:
n.a = 43
n.a

Simply assigning a value to something that looks like you are accessing a member variable creates it, but only for that instance.

In [ ]:
one = Node()
two = Node()
one.x = 42
In [ ]:
one.x
In [ ]:
two.x

We can assign a new class attribute, though.

In [ ]:
Node.cx = 'node class'
In [ ]:
one.cx
In [ ]:
two.cx

So, python is very flexible, too flexible some say. We really should define the attributes in class methods, like the constructor. The constructor for a class has the special name __init__.

Here is how we should define our Node class. Let's say it should hold a state, and values for h, g, and f.

In [ ]:
class Node:
    def __init__(self, state, f,g,h):
        self.state = state
        self.f = f
        self.g = g
        self.h = h

Now we can use it like this.

In [ ]:
a = Node([1, 2, 3], 0, 0, 0)
In [ ]:
a.f
In [ ]:
a

The form that is printed when you evaluate it is kind of ugly. In python, there are two kinds of toString type of methods, for two purposes:

  • __repr__ is meant to display a valid python expression that could be used to generate the value
  • __str__ is meant to display a more human-oriented string that is not meant to be valid python code. Sometimes the __repr__ result is good enough for humans, too.

Here is an example for our Node class.

In [ ]:
class Node:
    
    def __init__(self, state, f, g, h):
        self.state = state
        self.f = f
        self.g = g
        self.h = h
        
    def __repr__(self):
        return 'Node({}, {}, {}, {})'.format(self.state, self.f, self.g, self.h)
In [ ]:
a = Node([1, 2, 3], 0, 0, 0)
In [ ]:
a

We can define default values in the constructor, too. This allows f, g, and h to be entered as keyword arguments. And, therefore, the __repr__ form becomes even more readable.

In [ ]:
class Node:
    
    def __init__(self, state, f=0, g=0, h=0):
        self.state = state
        self.f = f
        self.g = g
        self.h = h
        
    def __repr__(self):
        return 'Node({}, f={}, g={}, h={})'.format(self.state, self.f, self.g, self.h)
In [ ]:
a = Node([1, 2, 3], 0, 0, 0)
In [ ]:
a
In [ ]:
b = Node([3,2,3])
In [ ]:
b

Sorting Lists

Sorting a list is easy. sorted produces a new list that is sorted. The sort method destructively sorts the list.

In [ ]:
nums = [5, 2, 44, 8, 322, 54, 22]
In [ ]:
numsSorted = sorted(nums)
In [ ]:
numsSorted
In [ ]:
nums
In [ ]:
nums.sort()
In [ ]:
nums

But, what if the things we are sorting are structured and you want to sort by just one or some of the values? Say you have a list of tuples and want to sort by the second value? The sorted and sort functions take a key argument whose value is a function.

In [ ]:
pairs = [('a',54), ('b',52), ('c', 2), ('d', 21), ('e', 31)]
pairs
In [ ]:
pairs
In [ ]:
sorted(pairs, key = lambda p: p[1])
In [ ]:
pairs
In [ ]:
pairs.sort(key=lambda p: p[1])
In [ ]:
pairs

Hey, how about sorting nodes??? Here is list of unexpanded nodes, maybe from someplace in the middle of an A* search.

In [ ]:
unExpanded = [Node([3,2,1],2,1,1),
    Node([2,1,3],4,2,2),
    Node([3,1,2],3,1,2),
    Node([1,3,2],1,1,0)]
unExpanded

What do we want to order them by? How would you do this in python?

In [ ]:
unExpanded.sort()
unExpanded

Hummm.....nope. How about

In [ ]:
unExpanded.sort(key=lambda n: n.f)
unExpanded

That's better. Now we can get the lowest-f node by unExpanded[0] or get and remove it by unExpanded.pop(0). We can also get the second-lowest f node by unExpanded[1].

In [ ]:
best = unExpanded[0]
best.f
In [ ]:
best.state

Conditional Expressions

The multiple lines of an if-else block can be written more compactly, and some might say more intuitively. See this PEP on conditional expressions. (Hey, what does PEP stand for?)

What happens when you try to index beyond the end of a list?

In [ ]:
stuff = ['a', 'c', 'x']
In [ ]:
stuff[0]
In [ ]:
stuff[2]
In [ ]:
stuff[3]

So we should surround cases like this with try-except blocks. But, what if we just want an empty list if our index is beyond the end?

In [ ]:
i = 4
In [ ]:
if i < len(stuff):
    result = stuff[i]
else:
    result = []
    
result

That's a bit clunky. Conditional expressions to the rescue.

In [ ]:
result = stuff[i] if i < len(stuff) else []
result

The first expression is not evaluated if the if condition is false.

Arrays, from numpy module

We are going to play with some robot movement problems where the robot can move in discrete steps across the floor. To represent a bird's-eye view of the world, let's use an array.

The numpy module in python is an efficient implementation of arrays. Let's create a 4x4 array of characters to represent a world in which the robot can be in 16 different positions. The position of the robot is marked with 'r' and every other element is a blank.

In [ ]:
import numpy as np

world = np.array([
        [' ', ' ', ' ', ' '],
        [' ', 'r', ' ', ' '],
        [' ', ' ', ' ', ' '],
        [' ', ' ', ' ', ' ']])
world

How can we move the robot down one step? Index into the array with two indices. But first, here is a cool python idiom for swapping values.

In [ ]:
x = 42
y = 100
(x, y)
In [ ]:
x, y = y, x
In [ ]:
x, y

So, the 'down' step can be done by

In [ ]:
world
In [ ]:
world[2,1], world[1,1] = world[1,1], world[2,1]
world