76

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.

martineau
  • 119,623
  • 25
  • 170
  • 301
dskinner
  • 10,527
  • 3
  • 34
  • 43

4 Answers4

101

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'}
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
34

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__.

Neil G
  • 32,138
  • 39
  • 156
  • 257
3

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.

Carcigenicate
  • 43,494
  • 9
  • 68
  • 117
  • does this mean only __getitem__ and keys need to be defined for ti – Max Jan 22 '23 at 10:47
  • @Max It's more than a year later, so I don't remember all I previously discovered, but yes, that's what the comment suggests. That finding is also mirrored in the top answer. I posted this answer as a way to show how something like this could be found. – Carcigenicate Jan 22 '23 at 15:10
0

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)
Federico Baù
  • 6,013
  • 5
  • 30
  • 38