This post originated from an RSS feed registered with Python Buzz
by Ben Last.
Original Post: Dementors At Every Entrance
Feed Title: The Law Of Unintended Consequences
Feed URL: http://benlast.livejournal.com/data/rss
Feed Description: The Law Of Unintended Consequences
I had a need for lists that could hold only items of a certain type. A nice little problem, and one that's cropped up before, not least in the arrays module. Unfortunately, that only stores certain simple types, indicated by a string typecode, and these don't map to Python's types in a way useful to me. So I whipped up a subclass of list. Until quite recently, as a couple of previous bits of code in this blog show, I have mostly been working with Python versions before 2.0, so this was an useful way to play some more with new style classes and their associated variations on a theme.
The list subclass is especially interesting, since there are so many different methods that do stuff to it. In order to make it "type safe" (that is, only allow it to contain items of a given type), I only needed to worry about those that changed the values in the list, specifically: __setitem__, __setslice__, append, extend and insert. __init__ of course also takes a sequence. So, we need to check all the ways that rogue values might get into a list, and there are a few gotchas.
Firstly, as I mentioned, there's not very much reuse of methods in list; extend doesn't call append, for example, nor do other methods call __setitem__. For good reasons of efficiency, of course; making every single modification of the list items go through __setitem__ method would make list easy to subclass, but since lists are used so much in all areas of Pythoning compared to the number of times one needs to subclass them, that would result in a sacrifice of efficiency for the sake of an exception. A Bad Idea, that. Thus they all need overriding.
Secondly, there's the question of overloaded operators. If I have a regular list a and a type-safe list b, what should a+b yield? I elected to make life easy - if I really do want the result of adding a regular list to a type-safe one, there's always extend, and I can always use the result of list arithmetic as the initializer for a new type-safe list, the equivalent of list() itself in its usage as a "cast".
Finally, there's the question of how to set the type to which the list items are to be constrained. A drop-in replacement for list needs to support initialization from an existing sequence, but there's also a common usage where the type-safe list is created empty and then appended to. To support either, I allow __init__ to be given either a sequence (which is checked for type consistency and then the type of the first item used as the constraint) or a type (which is used directly as the constraint) or a class. The class option is useful for a class-safe variant on the idea (see below). I needed also to consider what to do when there was an empty initial sequence and chose to treat that as an exception; I need to get the constraint type from somewhere, and Python's rule of thumb is to be explicit, rather than to make assumptions.
So, here it is. No doubt there are errors and inefficiencies galore...
class TypeSafeList(list):
"""List that limits members to that of a given type (doesn't require typecodes)."""
def _typeOf(self, x):
"""Return the type of x. Abstracted into a method so that it can be
overridden."""
return type(x)
def __init__(self, parameter=None):
"""Generate a TypeSafeList that will hold only objects of the given type, initialised from the given sequence
(first member) or of the given type.
If parameter is iterable, we assume it's a sequence."""
try:
iter(parameter)
#Take the type of the first parameter - an empty list will raise
#an exception.
self._type = self._typeOf(parameter[0])
initlist=parameter
except TypeError: #if it's not a sequence...
#Verify that parameter is a type or a class.
if isinstance(parameter, types.TypeType) or type(parameter) == types.ClassType:
self._type = parameter
initlist=[]
else:
raise TypeError, "Parameter must be non-empty sequence, type or class."
#Validate initlist for type consistency.
map(self._checkType, initlist)
#Do parent init.
list.__init__(self, initlist)
def _checkType(self, x):
if type (x) != self._type:
raise TypeError, "Value %s (%s) is the wrong type for this sequence; should be %s" % (str(x), str(type(x)), str(self._type))
def __setitem__(self, i, y):
"""Set entry i to value y"""
self._checkType(y)
list.__setitem__(self, i, y)
def __setslice__(self, i, j, seq):
"""Replace items i to j with the items from sequence seq."""
map(self._checkType, seq)
list.__setslice__(self, i, j, seq)
def append(self, x):
"""Append a to the list."""
self._checkType(x)
list.append(self, x)
def insert(self, i, x):
"""same as s[i:i] = [x]."""
self._checkType(x)
list.insert(self, i, x)
def extend(self, seq):
"""Append contents of sequence seq to the list."""
map(self.append, seq)
An interesting and useful fact: all the types in the types module are of the same type (types.TypeType). That is, type(types.BooleanType) == types(types.IntType) and so forth. And here's a subclass that constrains members to be an instance of a defined class or subclass; a class-safe list. This is actually what's most useful to me:
class ObjectList(TypeSafeList):
"""Type-safe list of objects; only members of the fixed class or subclasses allowed."""
def _typeOf(self, x):
"""Return the type of x. Abstracted into a method so that it can be
overridden."""
return x.__class__
def _checkType(self, x):
"""Verify that x is an instance of the right class."""
if not isinstance(x, self._type):
raise TypeError, "Object of type '%s' is the wrong type for this sequence; should be '%s'" % (str(x.__class__), str(self._type))
As the saying goes, the journey is the reward, and this was interesting to do. What more can one ask for of a Monday morning?