Skip to content

Under Under Magic

January 26, 2013

I was going to post about Cython next, but I’ve decided to give some background that will hopefully make my first post more intelligible. I’m going to talk about Python’s magic __ (read: under under) methods. We’re going to build up a mutable quaternion class, with a built-in Hamiltonian product, and we’re going to include list-style indexing so that we can use Numpy routines on our quaternions.

One of my favorite parts about using Python is the ability to easily ask the question, “What happens if…”. I originally discovered __ methods by trying to access elements of an object with named elements via an index, e.g. x[0] (for the sake of this post, when I type x, I mean an instantiation of our class). We might want to do that here so that we can use some of Numpy’s matrix multiplication routines. We could use different Numpy tensors to define the Hamiltonian product and the Lie bracket, or whatever abstract product we want without having to type out x.i, x.j, … a whole bunch of times. Combine this with some Cython, and we could have a pretty fast calculation.

Let’s start with a minimal quaternion class:

class Quaternion(object):
  """
  A list-like quaternion class to make Numpy happy.
  Indices: s, i, j, k
  """
  def __init__(self, s, i, j, k):
    self.s = s
    self.i = i
    self.j = j
    self.k = k

If you are at all familiar with classes and objects in Python, that should look completely standard to you. However, I want to make a quick aside about __init__(), since this post is all about the __ methods. Note that the first argument of __init__() is ‘self’. What that means is that when __init__ is executed, the object already exists. If you are trying to make an immutable object, once __init__ gets hit, you are screwed. __init__ will only change an existing instance of a class in place, hence ‘self’. If you really want some crazy Python-fu, you need to start your class out with __new__(), which is what Python calls to create a new instance of a class. __new__() takes cls as a first argument, the class __new__() is operating on. __new__() then calls __init__() after it has created your instance. In our code above, we are calling object.__new__() creating a generic object before calling our own __init__() to modify that object in place.

Alright, enough of the esoteric Python class discussion, let’s go back to creating our quaternion class. When we type x[0], Python calls __getitem__(). Respectively, when we type x[0] = y, Python calls __setitem__(). Let’s add a naive implementation of __getitem__():

class Quaternion(object):
  """
  A list-like quaternion class to make Numpy happy.
  Indices: s, i, j, k
  """
  def __init__(self, s, i, j, k):
    self.s = s
    self.i = i
    self.j = j
    self.k = k
  def __getitem__(self, n):
    if n == 0:
      return self.s
    if n == 1:
      return self.i
    if n == 2:
      return self.j
    if n == 3:
      return self.k
    else:
      raise IndexError ('index out of range')

There are about twelve things wrong with that. I only implemented __getitem__(), because I got sick of typing. I would be shocked if I didn’t have several typos by the time I was finished. Also, that implementation raises an error for x[-1]. That shouldn’t make anyone happy. Python stores the values of our object in a dictionary named __dict__. We can simply create a tuple that handles our indexing:

class Quaternion(object):
  """
  A list-like quaternion class to make Numpy happy.
  Indices: s, i, j, k
  """
  def __init__(self, s, i, j, k):
    self.s = s
    self.i = i
    self.j = j
    self.k = k
    self._index = ('s', 'i', 'j', 'k')
  def __getitem__(self, n):
    try:
      return self.__dict__[self._index[n]]
    except IndexError:
      raise IndexError ('Quaternion object only has four items')
  def __setitem__(self, n, x):
    try:
      self.__dict__[self._index[n]] = x
    except IndexError:
      raise IndexError ('Quaternion object only has four items')

The tuple _index we created now handles catching the index for __getitem__(). We used try: … except … so that users don’t get a confusing error about a tuple. If we wanted to let users add or delete elements of this class, we could use a list, but we want things to stay as they are. Also, we used the underscore in _index to tell the user to beware of modifying this element. In general. x.var is something the user has control of, x._var means user beware, and x.__var__ is Pythonic magic.

Now, we have a new problem: slices. When you try to access or modify

class Quaternion(object):
  """
  A list-like quaternion class to make Numpy happy.
  Indices: s, i, j, k
  """
  def __init__(self, s, i, j, k):
    self.s = s
    self.i = i
    self.j = j
    self.k = k
    self._index = ('s', 'i', 'j', 'k')
  def __getitem__(self, n):
    try:
      if not isinstance(n, slice):
        return self.__dict__[self._index[n]]
      else:
        new_quat = Quaternion(None, None, None, None)
        step = q if n.step is None else n.step
        for m in xrange(n.start, n.stop, step):
          new_quat.__dict__[self._index[m]] = \
            self.__dict__[self._index[m]]
        return new_quat
    except IndexError:
        raise IndexError ('Quaternion object only has four items')
  def __setitem__(self, n, x):
    if not isinstance(n, slice):
      try:
        self.__dict__[self._index[n]] = x
      except IndexError:
        raise IndexError ('Quaternion object only has four items')      
    else:
      step = 1 if n.step is None else n.step
      for m in xrange(n.start, n.stop, step):
        try:
          self.__dict__[self._index[m]] = x[m]
        except IndexError:
          pass

When Python uses slices, if there is not a __getslice__() or __setslice__() sitting around, it passes a slice object to __getitem__() or __setitem(). __getslice__() and ___setslice__() are now deprecated, so we just add a check to see if n is an index or a slice object. We simply process the slice object to get our indices from xrange(), and away we go. Finally, in __setitem__(), we catch index errors so that if the slice on our object is too large it sets what it can and moves on.

As this has become more complicated than I expected, I’m going to take a break here. Implementing this with Cython is going to be a little difficult because of the presence of slice objects in the code.

Advertisements

From → Python tricks

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: