Mapping Views
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]
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?
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
!
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.
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.