#!/usr/bin/env python # coding: utf-8 # # 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](http://docs.python.org/whatsnew/2.5.html). (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