views:

154

answers:

4

Hi,

I want to create a list that can only accept certain types. As such, I'm trying to inherit from a list in Python, and overriding the append() method like so:

class TypedList(list):
    def __init__(self, type):
        self.type = type

    def append(item)
        if not isinstance(item, type):
            raise TypeError, 'item is not of type %s' % type
        self.append(item)  #append the item to itself (the list)

This will of cause an infinite loop because the body of append() calls itself, but I'm not sure what to do other than using self.append(item).

How I should go about doing this?

+2  A: 

instead of self.append(item) use super(TypedList, self).append(item) (see http://docs.python.org/library/functions.html#super)

Gabi Purcaru
I tried that, but it will not allow me to.# Error: TypeError: super(type, obj): obj must be an instance or subtype of type
chaindriver
Update: That TypeError seems to be because this TypedList class and the class calling it are in different modules. Let me do some check...
chaindriver
Yes it seems that if I put TypedList into another module, then super() doesn't work anymore.
chaindriver
Can you post the updated code? I am curious to see how/where `obj` is involved.
Manoj Govindan
`if I put TypedList into another module, then super() doesn't work anymore`. That is **very** strange. Can you post source code of `TypedList` as well as the client who is calling it?
Manoj Govindan
Hey Manoj, it's working now. The problem seems to be because of the wrong order of loading the modules.The class that I'm feeding into TypedList comes from another module, say X.I was loading the modules in this order:1) X2) module with a class calling TypedList3) module for TypedList.It works once I change it to:1) X2) module for TypedList3) module with a class calling TypedList
chaindriver
A: 

By not doing it in the first place.

If you don't want something of type X in your list, why are you putting it there?

This is not a sarcastic response. Adding type restrictions as you are trying is either unnecessary or affirmatively counter-productive. It is however, a common request from people coming from a language background which has strict compile-time type checking.

For the same reason you wouldn't attempt 'a string' / 2.0, you have the same control over what gets put in a list. Since a list will happily contain heterogeneous types, at best TypedList will move a run-time TypeError from the point where you use the item forward in time to where you append it to the list. Given Python's duck-typing explicitly checking isinstance precludes later expansion of the list to contain non-type instances while providing no benefit.

added OrderedDict information:

Per request in comment, assuming Python 2.7 or greater collections.OrderedDict will do it. Per that documentation page, given 2.4 or greater you have to add one.

msw
The list will be used by other users and I'm trying to prevent them from placing in invalid types. Or should I be doing this whole thing in another way?
chaindriver
I expanded my answer. If the library has a contract of acceptability, declare it to the library user and let them shoot their own foot if they choose. cf. the Pythonic "Easier to Ask Forgiveness than Permission" principle.
msw
I see, thanks for the insight. I'll keep that in mind!Actually I have another reason for implementing this TypedList. The __getitem__ method takes in either a string or an int. If it takes in a string, I'm searching through each item in the list, comparing the string to an attribute called .name for the item. This .name attribute only exists for the specified class that I have created. I could have used a dictionary for this, but the order in which the items are added is important, so I have to use a list. Any suggestions on a better way to handle this?
chaindriver
@chaindriver: added above
msw
That's awesome. I'll try it out. Thanks!
chaindriver
@msw, you're totally ignoring the possibility of using as the type a suitable ABC which can ensure the _intention_ of a type is to respect a certain semantic contract. Using a _concrete_ type is silly (for the same reasons I expounded at length in my answer), using an abstract one is *just fine*. You're guilty of pre-2.6 thinking, or underestimating the **huge** importance of ABCs in 2.6 and later -- e.g., solving the old "problem of the `draw`" method, where concrete classes `Painter`, `Lottery` and `Gunman` could easily have accidental homonyms with **very** different semantics!-)
Alex Martelli
@Alex: I am now in no doubt that I fail to see the importance of ABCs (which isn't a new deficit as I recall the same reaction in the days of Stroustoup's `cfront`). Thanks for pointing out the key element I need to learn more about.
msw
@msw, you're welcome! Python ABCs are functionally richer than C++'s (by supporting "registration" with the ABC independent of inheritance... an idea tantalizingly close, though alas not quite as powerful, to Haskell's typeclasses, or the full-fledged adaptation I had proposed back in PEP 246, sigh;-), but the part about the usefulness in "asserting intention" is really quite similar (even Java's interfaces sometimes are used that way -- e.g., `Cloneable`).
Alex Martelli
+5  A: 

I have made some changes to your class. This seems to be working.

A couple of suggestions: don't use type as a keyword - type is a built in function. Python instance variables are accessed using the self. prefix. So use self.<variable name>.

class TypedList(list):
    def __init__(self, type):
        self.type = type

    def append(self, item):
        if not isinstance(item, self.type):
            raise TypeError, 'item is not of type %s' % self.type
        super(TypedList, self).append(item)  #append the item to itself (the list)

from types import *
tl = TypedList(StringType)
tl.append('abc')
tl.append(None)
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    tl.append(None)
  File "<pyshell#22>", line 7, in append
    raise TypeError, 'item is not of type %s' % self.type
TypeError: item is not of type <type 'str'>
Manoj Govindan
Thanks for the help!
chaindriver
+8  A: 

I want to create a list that can only accept certain types. As such, I'm trying to inherit from a list in Python

Not the best approach! Python lists have so many mutating methods that you'd have to be overriding a bunch (and would probably forget some).

Rather, wrap a list, inherit from collections.MutableSequence, and add your checks at the very few "choke point" methods on which MutableSequence relies to implement all others.

import collections

class TypedList(collections.MutableSequence):

    def __init__(self, oktypes, *args):
        self.oktypes = oktypes
        self.list = list()
        self.extend(list(args))

    def check(self, v):
        if not isinstance(v, self.oktypes):
            raise TypeError, v

    def __len__(self): return len(self.list)

    def __getitem__(self, i): return self.list[i]

    def __delitem__(self, i): del self.list[i]

    def __setitem__(self, i, v):
        self.check(v)
        self.list[i] = v

    def insert(self, i, v):
        self.check(v)
        self.list.insert(i, v)

    def __str__(self):
        return str(self.list)

The oktypes argument is normally a tuple of types that you want to allow, but it's OK to pass a single type there of course (and, by making that one type an abstract base class, ABC, you can easily perform any kind of type-checking of your choice that way -- but, that's a different issue).

Here's some example code using this class:

x = TypedList((str, unicode), 'foo', 'bar')
x.append('zap')
print x
x.append(23)

the output is:

['foo', 'bar', 'zap']
Traceback (most recent call last):
  File "tl.py", line 35, in <module>
    x.append(23)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/_abcoll.py", line 556, in append
    self.insert(len(self), value)
  File "tl.py", line 25, in insert
    self.check(v)
  File "tl.py", line 12, in check
    raise TypeError, v
TypeError: 23

Note in particular that we have not overridden append -- yet append is there and behaves just as expected.

The not-so-secret behind that bit of magic is revealed in the traceback: _abcoll.py (the implementation module for the abstract base classes in the collections module), at line 556, implements append by calling our insert -- which we have, of course, properly overridden.

This "template method design pattern" (absolutely precious for all kinds of OOP -- look for my talks on design patterns on youtube and you'll find out why;-), among other advantages, gives us the "choke point effect" I mentioned earlier: by adding some checks at a very few methods that you must implement, you gain the advantage that those checks apply to all_ the other relevant methods (and mutable sequences in Python have a lot of those;-).

It's no surprise that we end up with a very powerful and classic design pattern "behind the scenes", because the whole idea behind this implementation strategy comes right out of the immortal classic "Design Patterns" book (whose authors are often collectively referred to as the gang of four";-): prefer object composition over inheritance. Inheritance (from concrete classes) is a very rigid coupling mechanism, full of "gotchas" as soon as you're trying to use it to do anything even just slightly outside its rigorous limits; composition is extremely flexible and useful, and inheritance from appropriate abstract classes can complete the picture very nicely.

Scott Meyers' excellent "Effective C++", item 33, puts it even more strongly: make non-leaf classes abstract. Since by "non-leaf" he means "any class that's ever inherited from", an equivalent phrasing would be "never inherit from a concrete class".

Scott is writing in a C++ context, of course, but Paul Haahr gives exactly the same advice for Java, phrased as Don't subclass concrete classes -- and I generally second it for Python, though I do favor the gang-of-four's softer phrasing, prefer composition over (concrete class) inheritance (but I understand that both Scott and Paul are often writing for an audience which needs very direct and strongly phrased advice, almost phrased as "commandments" rather than advice, not softer phrased one that they might too easily ignore in the name of their convenience;-).

Alex Martelli
You said "Not the best approach!" provided philosophical reasons about OOP and subclassing all of which are indeed correct. However you don't address "should this be done at all?" or "what facility does such a mechanism provide?". As I noted in my answer, I believe that all this does afford is moving run-time type errors forward in time at the cost of extensibility. I'd love to be shown if I'm mistaken.
msw
@msw, consider: "I want a list of mutable sequences", so that for example their order is ensured, and when I do `for x in los: x[0]=23` I'll be *sure* that the _first_ subitem of each item is then 23, and later `for y in los[0]:` will always start with 23. `TypedList(collections.MutableSequence)` does it right. If `los` was "just a list", `los.append(adict)` would appear to work, the `x[0]=23` would appear to work (but set an _arbitrary_ item of `adict`, **not** "the first"!)... then the `for y in los[0]:` would be a subtle _semantic_ failure -- no run-time type errors, hard-to-find bugs!
Alex Martelli
Thanks Alex, this reply is very very useful. You have answered so many doubts I had when I was trying to implement this class e.g. how to override ALL the methods in a list? Inherting from MutableSequence is an excellent method!You have mentioned a lot of good software engineering concepts too!I still have one question though: in the __init__ method, why did you create an empty list and then extend it with args, rather than just assigning args to list like so: self.list = list(args)?
chaindriver
@chain, `list(args)` alone would of course not check that every item (if any) given in `args` satisfy this instance's constraints (neither would `self.list.extend`, which was a thinko I've just edited to correct;-) -- `self.extend(list(args))` as implemented under the covers by `MutableSequence` loops over `list(args)`'s items, inserting each item by `self.insert`, which _does_ perform the necessary checks. (Sorry for the thinko which probably obscured this point!-).
Alex Martelli
I see. Everything makes sense now. Thanks a lot for the excellent answers!
chaindriver