Skip to content

Mapping Views

Thor Whalen edited this page Nov 12, 2021 · 6 revisions

When you do a .keys(), a .values() or .items() what are you actually getting?

Most think they're just getting an iterable over the keys, values or (key, value) pairs. This is true, but you're actually getting more. You're getting a Mapping View instance. This object is Iterable (and Sized), yes, but is also a Container (which means you can do a x in container on it, and in the case of subclasses KeysView and ItemsView, even has the Set interface (think of the core of the builtin set type, though the builtin set has a few extra methods).

(TODO: Include some links to a tutorial about them, and recipes around them).

You can take that for granted most of the time, but you may need to, or want to, customize behavior here too. You might want to change how an existing functionality works (say, implement database-specific optimizations of the iteration, or containment) or add some functionality. ou'll need to know a little bit about what these MappingViews must be like.

Looking into the code of CPython, you'll end up in C code, so I looked at pypy code:

class MappingView(Sized):

    __slots__ = '_mapping',

    def __init__(self, mapping):
        self._mapping = mapping

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

    def __repr__(self):
        return '{0.__class__.__name__}({0._mapping!r})'.format(self)


class KeysView(MappingView, Set):

    __slots__ = ()

    @classmethod
    def _from_iterable(self, it):
        return set(it)

    def __contains__(self, key):
        return key in self._mapping

    def __iter__(self):
        yield from self._mapping


class ItemsView(MappingView, Set):

    __slots__ = ()

    @classmethod
    def _from_iterable(self, it):
        return set(it)

    def __contains__(self, item):
        key, value = item
        try:
            v = self._mapping[key]
        except KeyError:
            return False
        else:
            return v is value or v == value

    def __iter__(self):
        for key in self._mapping:
            yield (key, self._mapping[key])


class ValuesView(MappingView, Collection):

    __slots__ = ()

    def __contains__(self, value):
        for key in self._mapping:
            v = self._mapping[key]
            if v is value or v == value:
                return True
        return False

    def __iter__(self):
        for key in self._mapping:
            yield self._mapping[key]

Custom Mapping Views

Sometimes you might want to bring your own custom mapping views. But in doing so, it's important to understand when it might be a good idea, when in might not, and some mistakes you might want to keep an eye on.

So when would you want to make your custom mapping views?

Optimize derived view methods

By "derived" methods I mean those that are implemented using the existing methods (__contains__, __iter__, __len__...). For example, the base KeysView and ValuesView subclass Set, which uses those methods to carry out set operations.

Let's have a look at one of these: is_disjoint.

    def isdisjoint(self, other):
        'Return True if two sets have a null intersection.'
        for value in other:
            if value in self:
                return False
        return True

Perhaps your backend is a remote DB and it would be wasteful to ask it if value is in it, for every value of other, one at a time. Instead, there's a DB trick you know of that allows you to make a single bulk request.

The reason why we said "derived" view methods is because methods like __contains__ and __iter__ really pass on the work to the wrapped _mapping's __contains__ and __iter__. So if you're here to optimize those, you should really optimize them at the level of the _mapping that has those views. That way, the views' __contains__ and __iter__ automatically enjoy that optimization, but so does the _mapping!

Add new methods

Maybe you want a .unique() or .distinct() method that will give you unique values. It's not in the base ValuesView, so you make it.

Warning

You must take caution when customizing existing methods or adding your own. If you do so using some DB trick that bi-passes the _mapping, it will not play well with store wrappers, so you need to make arrangements so they do.