Sam
Sam

Reputation: 565

Is the way my class inherits list class methods pythonicly correct?

A little example will help clarify my question:

I define two classes: Security and Universe which I would like to behave as a list of Secutity objects.

Here is my example code:

class Security(object):
    def __init__(self, name):
        self.name = name

class Universe(object):
    def __init__(self, securities):
        self.securities = securities

s1 = Security('name1')
s2 = Security('name2')
u = Universe([s1, s2])

I would like my Universe Class to be able to use usual list features such as enumerate(), len(), __getitem__()... :

enumerate(u)
len(u)
u[0]

So I defined my Class as:

class Universe(list, object):
    def __init__(self, securities):
        super(Universe, self).__init__(iter(securities))
        self.securities = securities

It seems to work, but is it the appropriate pythonic way to do it ?

[EDIT]

The above solution does not work as I wish when I subset the list:

>>> s1 = Security('name1')
>>> s2 = Security('name2')
>>> s3 = Security('name3')
>>> u = Universe([s1, s2, s3])
>>> sub_u = u[0:2]
>>> type(u)
<class '__main__.Universe'>
>>> type(sub_u)
<type 'list'>

I would like my variable sub_u to remain of type Universe.

Upvotes: 3

Views: 2538

Answers (2)

abarnert
abarnert

Reputation: 365915

You don't have to actually be a list to use those features. That's the whole point of duck typing. Anything that defines __getitem__(self, i) automatically handles x[i], for i in x, iter(x), enumerate(x), and various other things. Also define __len__(self) and len(x), list(x), etc. also work. Or you can define __iter__ instead of __getitem__. Or both. It depends on exactly how list-y you want to be.

The documentation on Python's special methods explains what each one is for, and organizes them pretty nicely.

For example:

class FakeList(object):
    def __getitem__(self, i):
        return -i
fl = FakeList()
print(fl[20])
for i, e in enumerate(fl):
    print(i)
    if e < -2: break

No list in sight.

If you actually have a real list and want to represent its data as your own, there are two ways to do that: delegation, and inheritance. Both work, and both are appropriate in different cases.

If your object really is a list plus some extra stuff, use inheritance. If you find yourself stepping on the base class's behavior, you may want to switch to delegation anyway, but at least start with inheritance. This is easy:

class Universe(list): # don't add object also, just list
    def __init__(self, securities):
        super(Universe, self).__init__(iter(securities))
        # don't also store `securities`--you already have `self`!

You may also want to override __new__, which allows you to get the iter(securities) into the list at creation time rather than initialization time, but this doesn't usually matter for a list. (It's more important for immutable types like str.)

If the fact that your object owns a list rather than being one is inherent in its design, use delegation.

The simplest way to delegate is explicitly. Define the exact same methods you'd define to fake being a list, and make them all just forward to the list you own:

class Universe(object):
    def __init__(self, securities):
        self.securities = list(securities)
    def __getitem__(self, index):
        return self.securities[index] # or .__getitem__[index] if you prefer
    # ... etc.

You can also do delegation through __getattr__:

class Universe(object):
    def __init__(self, securities):
        self.securities = list(securities)
    # no __getitem__, __len__, etc.
    def __getattr__(self, name):
        if name in ('__getitem__', '__len__',
                    # and so on
                   ):
            return getattr(self.securities, name)
        raise AttributeError("'{}' object has no attribute '{}'"
                             .format(self.__class__.__name__), name)

Note that many of list's methods will return a new list. If you want them to return a new Universe instead, you need to wrap those methods. But keep in mind that some of those methods are binary operators—for example, should a + b return a Universe only if a is one, or only if both are, or if either are?

Also, __getitem__ is a little tricky, because they can return either a list or a single object, and you only want to wrap the former in a Universe. You can do that by checking the return value for isinstance(ret, list), or by checking the index for isinstance(index, slice); which one is appropriate depends on whether you can have lists as element of a Universe, and whether they should be treated as a list or as a Universe when extracted. Plus, if you're using inheritance, in Python 2, you also need to wrap the deprecated __getslice__ and friends, because list does support them (although __getslice__ always returns a sub-list, not an element, so it's pretty easy).

Once you decide those things, the implementations are easy, if a bit tedious. Here are examples for all three versions, using __getitem__ because it's tricky, and the one you asked about in a comment. I'll show a way to use generic helpers for wrapping, even though in this case you may only need it for one method, so it may be overkill.

Inheritance:

class Universe(list): # don't add object also, just list
    @classmethod
    def _wrap_if_needed(cls, value):
        if isinstance(value, list):
            return cls(value)
        else:
            return value
    def __getitem__(self, index):
        ret = super(Universe, self).__getitem__(index)
        return _wrap_if_needed(ret)

Explicit delegation:

class Universe(object):
    # same _wrap_if_needed
    def __getitem__(self, index):
        ret = self.securities.__getitem__(index)
        return self._wrap_if_needed(ret)

Dynamic delegation:

class Universe(object):
    # same _wrap_if_needed
    @classmethod
    def _wrap_func(cls, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return cls._wrap_if_needed(func(*args, **kwargs))
    def __getattr__(self, name):
        if name in ('__getitem__'):
            return self._wrap_func(getattr(self.securities, name))
        elif name in ('__len__',
                      # and so on
                      ):
            return getattr(self.securities, name)
        raise AttributeError("'{}' object has no attribute '{}'"
                             .format(self.__class__.__name__), name)        

As I said, this may be overkill in this case, especially for the __getattr__ version. If you just want to override one method, like __getitem__, and delegate everything else, you can always define __getitem__ explicitly, and let __getattr__ handle everything else.

If you find yourself doing this kind of wrapping a lot, you can write a function that generates wrapper classes, or a class decorator that lets you write skeleton wrappers and fills in the details, etc. Because the details depend on your use case (all those issues I mentioned above that can go one way or the other), there's no one-size-fits-all library that just magically does what you want, but there are a number of recipes on ActiveState that show more complete details—and there are even a few wrappers in the standard library source.

Upvotes: 6

BrenBarn
BrenBarn

Reputation: 251428

That is a reasonable way to do it, although you don't need to inherit from both list and object. list alone is enough. Also, if your class is a list, you don't need to store self.securities; it will be stored as the contents of the list.

However, depending on what you want to use your class for, you may find it easier to define a class that stores a list internally (as you were storing self.securities), and then define methods on your class that (sometimes) pass through to the methods of this stored list, instead of inheriting from list. The Python builtin types don't define a rigorous interface in terms of which methods depend on which other ones (e.g., whether append depends on insert), so you can run into confusing behavior if you try to do any nontrivial manipulations of the contents of your list-class.

Edit: As you discovered, any operation that returns a new list falls into this category. If you subclass list without overriding its methods, then you call methods on your object (explicitly or implicitly), the underlying list methods will be called. These methods are hardcoded to return a plain Python list and do not check what the actual class of the object is, so they will return a plain Python list.

Upvotes: 5

Related Questions