一个作为解包映射的类

76

如果不使用 dict 的子类,一个类想要被视为映射(mapping)并且可以被传递给带有 ** 参数的方法,需要满足什么条件?

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
至少要让它抛出映射功能缺失的错误,这样我才能开始实现。

我已经查看了模拟容器类型的内容,但仅定义魔术方法没有效果,并且使用 ABCMeta 来覆盖并将其注册为字典会将其断言为子类,但在使用 isinstance(o, dict) 时失败。理想情况下,我甚至不想使用 ABCMeta

关于Python3请参见:https://dev59.com/78Pra4cB1Zd3GeqPl68X#71145561 - Drecker
4个回答

102

__getitem__()keys() 方法就够了:

>>> 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'}

34

如果你想创建一个映射(不只是满足传递给函数的要求),那么你应该继承自collections.abc.Mapping。如文档所述,你只需要实现:

__getitem__
__len__
__iter__

Mixin会为您实现其他所有内容:__contains__,keys,items,values,get,__eq__和__ne__。

超出范围,但具有相关性和信息性。谢谢。 - dskinner
很有用。现在已移至collections.abc包,网址为:https://docs.python.org/3/library/collections.abc.html#module-collections.abc - Stuart Buckingham
1
@StuartBuckingham,我很欣赏你的谦虚,但下次请随意编辑我的答案 :) - Neil G
1
啊,我没想到那是可能的。谢谢@NeilG。 - Stuart Buckingham

3

可以通过查看源代码找到答案。

当尝试使用一个非映射对象与**一起使用时,会出现以下错误:

TypeError: 'Foo' object is not a mapping

如果我们在CPython源代码中搜索该错误,我们可以找到引发此错误的代码:链接
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 实际上是 dict_merge,当 dict_merge 返回负数时会抛出错误。如果我们查看 dict_merge 的源码,我们可以看到何种情况下会返回 -1:

/* 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;

关键部分如下:

对于后者,我们只需要支持PyMapping_Keys()和PyObject_GetItem()。


这是否意味着只需要定义 getitem 和 keys 函数就可以为 ti 进行定义? - Max
@Max 一年多过去了,所以我不记得之前发现的所有内容,但是是的,评论中暗示了这一点。这个发现也在最佳答案中得到了反映。我发布这个答案是为了展示如何找到类似的东西。 - Carcigenicate

1

使用dataclasses

使用dataclass可以使代码更加简洁,最终在质量方面也更好。它还有助于将正确的对象返回给keys方法。

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接