9

I am trying to have a list returned when I call list() on a class. What's the best way to do this.

class Test():
    def __init__(self):
        self.data = [1,2,3]
    def aslist(self):
        return self.data
a = Test()
list(a)
[1,2,3]

I want when list(a) is called for it to run the aslist function and ideally I'd like to implement asdict that works when dict() is called

I'd like to be able to do this with dict, int and all other type casts

Tim Diekmann
  • 7,755
  • 11
  • 41
  • 69
Marc Frame
  • 923
  • 1
  • 13
  • 26
  • `list({1:2,3:4})` returns `[1,3]`. Is that not what you want? – John Zwinck Jul 07 '18 at 02:53
  • thats just an example, I want when you do list(a) for it to run the aslist function – Marc Frame Jul 07 '18 at 02:55
  • You need to implement the iterator protocol. With a `dict`, it accepts any *mapping* or an iterable of key-value pairs. – juanpa.arrivillaga Jul 07 '18 at 03:11
  • 1
    [The docs on each builtin type](https://docs.python.org/3/library/functions.html) give a brief idea of what they each want. [The data model chapter](https://docs.python.org/3/reference/datamodel.html#special-method-names) explains the special methods for all of the protocols in detail. The only really tricky bit is the cases (`list`, etc.) where you can satisfy the iterator protocol _or_ the (not-as-clearly-documented) old-style sequence protocol. (Some of the others have fallbacks, like `__bool__` to `__len__`, but they're more obvious.) – abarnert Jul 07 '18 at 03:24
  • you could use `__call__` method but it will return a list when you type `a()` and not `a` – Mauricio Cortazar Jul 07 '18 at 04:01

2 Answers2

13

Unlike many other languages you might be used to (e.g., C++), Python doesn't have any notion of "type casts" or "conversion operators" or anything like that.

Instead, Python types' constructors are generally written to some more generic (duck-typed) protocol.


The first thing to do is to go to the documentation for whichever constructor you care about and see what it wants. Start in Builtin Functions, even if most of them will link you to an entry in Builtin Types.

Many of them will link to an entry for the relevant special method in the Data Model chapter.


For example, int says:

… If x defines __int__(), int(x) returns x.__int__(). If x defines __trunc__(), it returns x.__trunc__()

You can then follow the link to __int__, although in this case there's not much extra information:

Called to implement the built-in functions complex(), int() and float(). Should return a value of the appropriate type.

So, you want to define an __int__ method, and it should return an int:

class MySpecialZero:
    def __int__(self):
        return 0 

The sequence and set types (like list, tuple, set, frozenset) are a bit more complicated. They all want an iterable:

An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements Sequence semantics.

This is explained a bit better under the iter function, which may not be the most obvious place to look:

object must be a collection object which supports the iteration protocol (the __iter__() method), or it must support the sequence protocol (the __getitem__() method with integer arguments starting at 0) …

And under __iter__ in the Data Model:

This method is called when an iterator is required for a container. This method should return a new iterator object that can iterate over all the objects in the container. For mappings, it should iterate over the keys of the container.

Iterator objects also need to implement this method; they are required to return themselves. For more information on iterator objects, see Iterator Types.

So, for your example, you want to be an object that iterates over the elements of self.data, which means you want an __iter__ method that returns an iterator over those elements. The easiest way to do that is to just call iter on self.data—or, if you want that aslist method for other reasons, maybe call iter on what that method returns:

class Test():
    def __init__(self):
        self.data = [1,2,3]
    def aslist(self):
        return self.data
    def __iter__(self):
        return iter(self.aslist())

Notice that, as Edward Minnix explained, Iterator and Iterable are separate things. An Iterable is something that can produce an Iterator when you call its __iter__ method. All Iterators are Iterables (they produce themselves), but many Iterables are not Iterators (Sequences like list, for example).


dict (and OrderedDict, etc.) is also a bit complicated. Check the docs, and you'll see that it wants either a mapping (that is, something like a dict) or an iterable of key-value pairs (those pairs themselves being iterables). In this case, unless you're implementing a full mapping, you probably want the fallback:

class Dictable:
    def __init__(self):
        self.names, self.values = ['a', 'b', 'c'], [1, 2, 3]
    def __iter__(self):
        return zip(self.names, self.values)

Almost everything else is easy, like int—but notice that str, bytes, and bytearray are sequences.


Meanwhile, if you want your object to be convertible to an int or to a list or to a set, you might want it to also act a lot like one in other ways. If that's the case, look at collections.abc and numbers, which not provide helpers that are not only abstract base classes (used if you need to check whether some type meets some protocol), but also mixins (used to help you implement the protocol).

For example, a full Sequence is expected to provide most of the same methods as a tuple—about 7 of them—but if you use the mixin, you only need to define 2 yourself:

class MySeq(collections.abc.Sequence):
    def __init__(self, iterable):
        self.data = tuple(iterable)
    def __getitem__(self, idx):
        return self.data[idx]
    def __len__(self):
        return len(self.data)

Now you can use a MySeq almost anywhere you could use a tuple—including constructing a list from it, of course.

For some types, like MutableSequence, the shortcuts help even more—you get 17 methods for the price of 5.


If you want the same object to be list-able and dict-able… well, then you run into a limitation of the design. list wants an iterable. dict wants an iterable of pairs, or a mapping—which is a kind of iterable. So, rather than infinite choices, you only really have two:

  • Iterate keys and implement __getitem__ with those keys for dict, so list gives a list of those keys.
  • Iterate key-value pairs for dict, so list gives a list of those key-value pairs.

Obviously if you want to actually act like a Mapping, you only have one choice, the first one.

The fact that the sequence and mapping protocols overlap has been part of Python from the beginning, inherent in the fact that you can use the [] operator on both of them, and has been retained with every major change since, even though it's made other features (like the whole ABC model) more complicated. I don't know if anyone's ever given a reason, but presumably it's similar to the reason for the extended-slicing design. In other words, making dicts and other mappings a lot easier and more readable to use is worth the cost of making them a little more complicated and less flexible to implement.

Community
  • 1
  • 1
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • how would you have a class utilise both list() and dict() when they both use the __itter__() data model? is that possible? – Marc Frame Jul 07 '18 at 19:15
  • @MarcFrame Well, if you want `list` to give you a list of key-value pairs, `Dictable` will actually do that. The alternative is to implement the mapping protocol, in which case `list` will give you a list of keys, the same way it does for a dict. See [this example](https://repl.it/repls/AusterePrettyDatamining). Which one you want depends on whether your object is supposed to be a mapping or not. If you want something completely different than either of the two… well, then you're fighting Python, and it's probably not a good idea. – abarnert Jul 07 '18 at 19:26
1

This can be done with overloading special methods. You will need to define the __iter__ method for your class, making it iterable. This means anything expecting an iterable (like most collections constructors like list, set, etc.) will then work with your object.

class Test:
    ...

    def __iter__(self):
        return iter(self.data)

Note: You will need to wrap the returned object with iter() so that it is an iterator (there is a difference between iterable and iterator). A list is iterable (can be iterated over), but not an iterator (supports __next__, raises StopIteration when done etc.)

Edward Minnix
  • 2,889
  • 1
  • 13
  • 26