[Python-ideas] __iter__(), keys(), and the mapping protocol

Jonathan Fine jfine2358 at gmail.com
Thu Sep 13 04:07:33 EDT 2018


Someone wrote:

    Granted, my only strong argument is that the ** unpacking operator
    depends on this method to do its job, and it's currently alone amongst
    Python's operators in depending on a non-dunder to do so

I like this argument. And I think it's important. Here's some background facts

class dict(object)
 |  dict() -> new empty dictionary
 |  dict(mapping) -> new dictionary initialized from a mapping object's
 |      (key, value) pairs
 |  dict(iterable) -> new dictionary initialized as if via:
 |      d = {}
 |      for k, v in iterable:
 |          d[k] = v
 |  dict(**kwargs) -> new dictionary initialized with the name=value pairs
 |      in the keyword argument list.  For example:  dict(one=1, two=2)

    >>> list(zip('abc', range(3)))
    [('a', 0), ('b', 1), ('c', 2)]
    >>> dict(list(zip('abc', range(3))))
    {'b': 1, 'a': 0, 'c': 2}
    >>> dict(zip('abc', range(3)))
    {'b': 1, 'a': 0, 'c': 2}

    >>> dict(**zip('abc', range(3)))
    TypeError: type object argument after ** must be a mapping, not zip
    >>> dict(**list(zip('abc', range(3))))
    TypeError: type object argument after ** must be a mapping, not list


Now for my opinions. (Yours might be different.)

First, it is my opinion that it is not reasonable to insist that the
argument after ** must be a mapping. All that is required to construct
a dictionary is a sequence of (key, value) pairs. The dict(iterable)
construction proves that point.

Second, relaxing the ** condition is useful. Consider the following.

    >>> class NS: pass
    >>> ns = NS()

    >>> ns.a = 3
    >>> ns.b = 5

    >>> ns.__dict__
    {'b': 5, 'a': 3}

    >>> def fn(**kwargs): return kwargs

    >>> fn(**ns)
    TypeError: fn() argument after ** must be a mapping, not NS

    >>> fn(**ns.__dict__)
    {'b': 5, 'a': 3}


The Zen of Python says

    Namespaces are one honking great idea -- let's do more of those!

I see many advantages in using a namespace to build up the keyword
arguments for a function call. For example, it could do data
validation (of both keys/names and values). And once we have the
namespace, used for this purpose, I find it natural to call it like so

        >>> fn(**ns)

I don't see any way to do this, other than defining NS.keys and
NS.__getitem__. But why should Python itself force me to expose
ns.__dict__ in that way. I don't want my users getting a value via
ns[key].

By the way, in JavaScript the constructs obj.aaa and obj['aaa'] are
always equivalent.


POSTSCRIPT:

Here are some additional relevant facts.

    >>> fn(**dict(ns))
    TypeError: 'NS' object is not iterable

    >>> def tmp(self): return iter(self.__dict__.items())
    >>> NS.__iter__ = tmp

    >>> fn(**dict(ns))
    {'b': 5, 'a': 3}

    >>> list(ns)
    [('b', 5), ('a', 3)]

I find allowing f(**dict(ns)) but forbidding f(**ns) to be a
restriction of functionality removes, rather than adds, values.

Perhaps (I've not thought it through), *args and **kwargs should be
treated as special contexts. Just as bool(obj) calls obj.__bool__ if
available.

    https://docs.python.org/3.3/reference/datamodel.html#object.__bool__

In other words, have *args call __star__ if available, and **kwargs
call __starstar__ if available. But I've not thought this through.

-- 
Jonathan


More information about the Python-ideas mailing list