user31415629
user31415629

Reputation: 1005

Why don't python dict keys/values quack like a duck?

Python is duck typed, and generally this avoids casting faff when dealing with primitive objects.

The canonical example (and the reason behind the name) is the duck test: If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

However one notable exception is dict keys/values, which look like a duck and swim like a duck, but notably do not quack like a duck.

>>> ls = ['hello']
>>> d = {'foo': 'bar'}
>>> for key in d.keys():
..      print(key)
..
'foo'
>>> ls + d.keys()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "dict_keys") to list

Can someone enlighten me as to why this is?

Upvotes: 3

Views: 2759

Answers (3)

blhsing
blhsing

Reputation: 106768

Dict keys actually implements the set's interface rather than the list's, so you can perform set operations with dict keys directly with other sets:

d.keys() & {'foo', 'bar'} # returns {'foo'}

But it doesn't implement the __getitem__, __setitem__, __delitem__, and insert methods, which are required to "quack" like a list, so it cannot perform any of the list operations without being explicitly converted to a list first:

ls + list(d.keys()) # returns ['hello', 'foo']

Upvotes: 8

Jean-Fran&#231;ois Fabre
Jean-Fran&#231;ois Fabre

Reputation: 140196

There is an explicit check for list type (or its children) in python source code (so even tuple doesn't qualify):

static PyObject *
list_concat(PyListObject *a, PyObject *bb)
{
    Py_ssize_t size;
    Py_ssize_t i;
    PyObject **src, **dest;
    PyListObject *np;
    if (!PyList_Check(bb)) {
        PyErr_Format(PyExc_TypeError,
                  "can only concatenate list (not \"%.200s\") to list",
                  bb->ob_type->tp_name);
        return NULL;
    }

so python can compute size very quickly and reallocate the result without trying all containers or iterate on the right hand to find out, providing very fast list addition.

#define b ((PyListObject *)bb)
    size = Py_SIZE(a) + Py_SIZE(b);
    if (size < 0)
        return PyErr_NoMemory();
    np = (PyListObject *) PyList_New(size);
    if (np == NULL) {
        return NULL;
    }

One way to workaround this is to use in-place extension/addition:

my_list += my_dict  # adding .keys() is useless

because in that case, in-place add iterates on the right hand side: so every collection qualifies.

(or of course force iteration of the right hand: + list(my_dict))

So it could accept any type but I suspect that the makers of python didn't find it worth it and were satisfied with a simple & fast implementation which is used 99% of the time.

Upvotes: 1

Arghya Saha
Arghya Saha

Reputation: 5713

If you go into the definition of d.keys() the you can see the following.

def keys(self): # real signature unknown; restored from __doc__
    """ D.keys() -> a set-like object providing a view on D's keys """
    pass

Or use this statement:

print(d.keys.__doc__)

It clearly mentions that the output is set-like object.

Now you are trying to append a set to a list.

You need to convert the set into list and then append it.

x = ls + list(d.keys())
print(x)
# ['hello', 'foo']

Upvotes: 0

Related Questions