Class that acts as mapping for **unpacking

Question:

Without subclassing dict, what would a class need to be considered a mapping so that it can be passed to a method with **.

from abc import ABCMeta

class uobj:
    __metaclass__ = ABCMeta

uobj.register(dict)

def f(**k): return k

o = uobj()
f(**o)

# outputs: f() argument after ** must be a mapping, not uobj

At least to the point where it throws errors of missing functionality of mapping, so I can begin implementing.

I reviewed emulating container types but simply defining magic methods has no effect, and using ABCMeta to override and register it as a dict validates assertions as subclass, but fails isinstance(o, dict). Ideally, I dont even want to use ABCMeta.

Asked By: dskinner

||

Answers:

The __getitem__() and keys() methods will suffice:

>>> class D:
        def keys(self):
            return ['a', 'b']
        def __getitem__(self, key):
            return key.upper()


>>> def f(**kwds):
        print kwds


>>> f(**D())
{'a': 'A', 'b': 'B'}
Answered By: Raymond Hettinger

If you’re trying to create a Mapping — not just satisfy the requirements for passing to a function — then you really should inherit from collections.abc.Mapping. As described in the documentation, you need to implement just:

__getitem__
__len__
__iter__

The Mixin will implement everything else for you: __contains__, keys, items, values, get, __eq__, and __ne__.

Answered By: Neil G

The answer can be found by digging through the source.

When attempting to use a non-mapping object with **, the following error is given:

TypeError: 'Foo' object is not a mapping

If we search CPython’s source for that error, we can find the code that causes that error to be raised:

case TARGET(DICT_UPDATE): {
    PyObject *update = POP();
    PyObject *dict = PEEK(oparg);
    if (PyDict_Update(dict, update) < 0) {
        if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) {
            _PyErr_Format(tstate, PyExc_TypeError,
                            "'%.200s' object is not a mapping",
                            Py_TYPE(update)->tp_name);

PyDict_Update is actually dict_merge, and the error is thrown when dict_merge returns a negative number. If we check the source for dict_merge, we can see what leads to -1 being returned:

/* We accept for the argument either a concrete dictionary object,
 * or an abstract "mapping" object.  For the former, we can do
 * things quite efficiently.  For the latter, we only require that
 * PyMapping_Keys() and PyObject_GetItem() be supported.
 */
if (a == NULL || !PyDict_Check(a) || b == NULL) {
    PyErr_BadInternalCall();
    return -1;

The key part being:

For the latter, we only require that PyMapping_Keys() and PyObject_GetItem() be supported.

Answered By: Carcigenicate

Using dataclasses

Cleaner and ultimatelly turns out to be better in terms of quality, the usage of dataclass, which helps also return the correct object to keys method.

from dataclasses import dataclass


@dataclass(frozen=True)
class Person:
    name: str
    surname: str
    age: int
    
    def __getitem__(self, key):
        return getattr(self, key)

    def keys(self):
        return self.__annotations__.keys()


josh_doe: Person = Person("John", "Doe", 31)
print(f"John object : {josh_doe}")
user_data = {**josh_doe}
print(user_data)
Answered By: Federico Baù