Python Dictionary Views

Chapter 28 11 mins

Learning outcomes:

  1. What are dictionary views
  2. Dynamic nature of views
  3. Membership tests
  4. Set-like nature of views
  5. Performance benefit of using views
  6. Iterating over views

Introduction

In the precious chapter we got addressed on the basics of Python dictionaries. We saw a couple of ways to create lists, how to add, remove and modify key-value pairs. We also saw how to iterate over a dictionary and search for a key, a value or a complete key-value pair.

In this regards, we saw three different methods, each of which defined a sequence: keys(), values() and items(). Now in this chapter, we shall understand what exactly are these methods, how do they work and what operation can we perform on them.

What are dictionary views?

As the name suggests:

A dictionary view is simply a sight over a dictionary

It shows us something regarding a dictionary.

In Python, there are three dictionary view objects:

  1. keys() — shows us all the keys of a given dictionary, as a sequence.
  2. values() — shows us all values of a dictionary, as a sequence.
  3. items() — shows us all the key-value pairs of a dictionary, once again as a sequence.

It's very important to take note of the word 'show' here — it emphasizes the fact that all these view objects do not construct any copies of the respective sequences and return them; rather they just show what's currently there in a given dictionary.

If we change the dictionary, these views would also change since they merely showcase whatever is there in it.

This is why dictionary view objects are said to be dynamic — they change as soon as the underlying dictionary receives a change.

You can think of a view as a camera and the dictionary as an object. If the object gets changed somehow, whatever is shown to you in the camera would also change since it is a view over the object.

Anyways, now that it's clear what keys(), values() and items() are, let's dig deeper into them.

Dynamic nature of views

First we'll see what does it really mean in terms of code when we way that dictionary views are dynamic.

Below we create a dictionary d with three keys and then a view over all its keys, via the keys() method:

d = {'x': 1, 'y': 2, 'z': 10}
keys_view = d.keys()

print(keys_view)
dict_keys(['x', 'y', 'z'])

Now let's delete a key from d and see what happens to the view:

del d['x']

print(keysview)
dict_keys(['y', 'z'])

As is apparent, the view has changed. We did absolutely nothing to the view. It changed on its own because of the change in the dictionary d made by deleting the key x.

Let's consider another example, this time with values().

As before, we define a dictionary and then a view over it. This time the view is over all the values of the dictionary:

d = {'a': 10, 'b': 20, 'c': 30}
values_view = d.values()

print(values_view)
dict_values([10, 20, 30])

Now let's add a new key-value pair to this dictionary and then inspect the view:

d['d'] = 40

print(values_view)
dict_values([10, 20, 30, 40])

Not surprisingly, the view has changed to encompass the modifications made in the dictionary d.

Restating this idea, a view is directly linked to a given dictionary — it isn't a standalone entity. If a change gets made in the dictionary, the view changes likewise.

Membership tests

We've already seen in the previous Python Dictionary Basics chapter, that view objects support membership tests using the in operator. Let's quickly review this feature of views..

Say we want to check whether a dictionary contains a given key. We can do something like this:

d = {'x': 1, 'y': 2, 'z': 10}

print('x' in d.keys())
True

The key x is searched for in the d.keys() view. Since x does exist in the dictionary, we get True printed in the shell.

When a key needs to searched for in a dictionary d, you're better off at writing key in d, rather than key in d.keys().

In another occasion, suppose we want to check whether a given value exists in a dictionary. We can do so by using in on the values() view.

d = {'x': 1, 'y': 2, 'z': 10}

print(10 in d.values())
print('1' in d.values())
True
False

Since 10 is present in d, the expression in line 3 returns True. However, since '1' doesn't exist in the dictionary, the expression in line 4 evaluates to False.

To check for a specific key-value pair, we can use in on the items() view:

d = {'x': 1, 'y': 2, 'z': 10}

print(('x', 1) in d.values()) # True
print(('x', 2) in d.values()) # False

The reason we use a tuple as the left operand of the in operator here, in lines 3 and 4, is because items() shows each key-value pair in a given dictionary as a tuple, whose first element is the key and the second one is the value of the key.

Where we can check for the existence of a given key, value or key-value pair using the in operator on the views keys(), values() and items() respectively, we can check for the non-existence of the same entities using the negated not in operator.

Consider the following snippet:

d = {'x': 1, 'y': 2, 'z': 10}
'x' not in d.keys()
False
10 not in d.values()
False
('x', '1') not in d.items()
True

Set-like views

Python considers the keys() and the items() views as set-like. That is, they enjoy most of the operations available to sets in the language, including intersection, union, difference, and so on.

But why is this so?

Since both the views keys() and items() define sequences of unique elements, they resemble the idea of sets. Sets contain unique elements and so do the keys() and items() views.

Hence, these two views support many set operations.

Shown below is an illustration:

d1 = {'x': 1, 'y': 2, 'z': 10}
d2 = {'a': 9, 'b': 5, 'z': 30}

d1_keys = d1.keys()
d2_keys = d2.keys()

print('Union:', d1_keys | d2_keys)
print('Intersection:', d1_keys & d2_keys)

The values() view doesn't enjoy this set-like nature, since the values in a dictionary can appear more than once i.e. not define a sequence of unique elements.

Performance benefit

It's all good to know what are views and how do views work. But it's even more convincing to know exactly why Python uses views. Why doesn't the language make a copy of all the keys, or values, or key-value pairs, whatever is desired?

In Python 3, when a dictionary view is created by either invoking keys(), values() or items() on a dictionary object, nothing new is created in memory. The sequence of the keys, values, or key-value pairs returned doesn't exist on its own — rather it just operates on the existing data of the dictionary in memory.

In contrary to this, in Python 2, these same methods (keys(), values() or items()) don't return views on the respective dictionary, rather they return a list of all keys, values, or pairs respectively.

And this list is constructed from scratch by iterating over the respective entities in the original dictionary.

This replication of data done in Python 2, is time-consuming and inefficient. Not only this, but in cases where the dictionary gets changed later on, these lists don't obviously remain in sync with the changed dictionary.

The idea of dictionary views exists in Python 2 as well, but in another naming.

In Python 2, the methods keysview(), valuesview() and itemsview() do what keys(), values() and items() do in Python 3 — they return views instead of performing any replication of data over a given dictionary.

The benefits of these views are that:

  1. They are efficient in terms of memory by not creating any new sequences
  2. They always remain in sync with the original dictionary

In short, views are incredible!

Iterating over views

As we've seen in the previous chapter, iteration over views is possible using the for loop.

Below we show iteration over all three views keys(), values() and items(), one-by-one.

Going over all keys:

d = {'a': 'Hello', 'b': 'World!', 'c': 10, 'd': True}

for key in d.keys():
    print("Value of d['" + key + "']:", d[key])
Value of d['a']: Hello Value of d['b']: World! Value of d['c']: 10 Value of d['d']: True

Going over all values:

d = {'a': 'Hello', 'b': 'World!', 'c': 10, 'd': True}

for value in d.values():
    print("Value:", value)
Value: Hello Value: World! Value: 10 Value: True

Going over all items (key-value pairs):

d = {'a': 'Hello', 'b': 'World!', 'c': 10, 'd': True}

for k, v in d.item():
    print("Value of d['" + k + "']:", v)
Value of d['a']: Hello Value of d['b']: World! Value of d['c']: 10 Value of d['d']: True

One important thing to note here is that it's invalid to alter a dictionary while iterating over its view. This can be seen as follows:

d = {'a': 'Hello', 'b': 'World!', 'c': 10, 'd': True}

for k, v in d.item():
    # altering the dictionary
    d['a'] = 10

    print("Value of d['" + k + "']:", v)
Value of d['a']: Hello
Traceback (most recent call last): File "stdin", line 3, in <module> for k, v in d.items(): RuntimeError: dictionary changed size during iteration