如何定义可以用`**`解包的自制对象?

How to define self-made object that can be unpacked by `**`?

今天我正在学习使用 *** 来解压参数。
我发现 list, str, tuple, dict 都可以被 *.
解包 我猜是因为它们都是可迭代的。所以我自己做了 class.

# FILE CONTENT
def print_args(*args):
    for i in args:
        print i

class MyIterator(object):
    count = 0
    def __iter__(self):
        while self.count < 5:
            yield self.count
            self.count += 1
        self.count = 0

my_iterator = MyIterator()

# INTERPRETOR TEST
In [1]: print_args(*my_iterator)
0
1
2
3
4

有效!但是如何在 python 中创建一个像 dict 这样的 mapping 对象,以便 ** 对其进行解包?有可能这样做吗?除了 dict 之外,python 中是否已经存在另一种 mapping 对象?


PS: 我知道我可以让一个对象继承自 dict class 以使其成为一个映射对象。但是是否有像 __iter__ 这样的键 magic_method 来创建没有 class 继承的映射对象?


PS2: 在@mgilson 的回答的帮助下,我制作了一个可以由 ** 解压的对象,而无需从当前映射对象继承:

# FILE CONTENT
def print_kwargs(**kwargs):
    for i, j in kwargs.items():
        print i, '\t', j

class MyMapping(object):
    def __getitem__(self, key):
        if int(key) in range(5):
            return "Mapping and unpacking!"

    def keys(self):
        return map(str, range(5))

my_mapping = MyMapping()
print_kwargs(**my_mapping)

# RESULTS
1   Mapping and unpacking!
0   Mapping and unpacking!
3   Mapping and unpacking!
2   Mapping and unpacking!
4   Mapping and unpacking!

请注意,使用 ** 解包时,映射对象中的键类型应为 str,否则将引发 TypeError。

首先,让我们定义解包:

def unpack(**kwargs): 
    """
    Collect all keyword arguments under one hood
    and print them as 'key: value' pairs
    """
    for key_value in kwargs.items(): 
       print('key: %s, value: %s' % key_value)

现在,结构:两个可用的内置选项是 collections.abc.Mapping and collections.UserDict。由于还有另一个探索高度可定制的 Mapping 类型的答案,我将专注于 UserDict:如果您只需要一个带有一些扭曲的基本字典结构,那么 UserDict 可以更容易开始。定义后,底层 UserDict 字典也可以作为 .data 属性访问。

1.It 可以内联使用,像这样:

from collections import UserDict
>>> d = UserDict({'key':'value'})

>>> # UserDict makes it feel like it's a regular dict
>>> d, d.data
({'key':'value'}, {'key':'value'})

UserDict 分成键=值对:

>>> unpack(**d)
key: key, value: value
>>> unpack(**d.data) # same a above
key: key, value: value

2.If子classing,你所要做的就是在__init__中定义self.data。请注意,我使用 (self+other) 'magic' 方法扩展了 class 的附加功能:

class CustomDict(UserDict):
    def __init__(self, dct={}):
        self.data = dct

    def __add__(self, other={}):
        """Returning new object of the same type
           In case of UserDict, unpacking self is the same as unpacking self.data
        """
        return __class__({**self.data, **other})

    def __iadd__(self, other={}):
        """Returning same object, modified in-place"""
        self.update(other)
        return self

用法是:

>>> d = CustomDict({'key': 'value', 'key2': 'value2'})
>>> d
{'key': 'value', 'key2': 'value2'}
>>> type(d), id(d)
(<class '__main__.CustomDict'>, 4323059136)

向其添加其他字典(或任何 mapping 类型)将调用 __add__,returning 新对象:

>>> mixin = {'a': 'aaa', 'b': 'bbb'}
>>> d_new = d + mixin # __add__
>>> d_new
{'key': 'value', 'a': 'aaa', 'key2': 'value2', 'b': 'bbb'} 
>>>type(d_new), id(d_new)
(<class '__main__.CustomDict'>, 4323059248) # new object
>>> d # unmodified
{'key': 'value', 'key2': 'value2'}

使用 __iadd__ 的就地修改将 return 相同的对象(内存中的相同 id)

>>> d += {'a': 'aaa', 'b': 'bbb'} # __iadd__
>>> d
{'key': 'value', 'a': 'aaa', 'key2': 'value2', 'b': 'bbb'}
>>> type(d), id(d)
(<class '__main__.CustomDict'>, 4323059136)

顺便说一句,我同意其他贡献者的看法,您也应该熟悉 collections.abc.Mapping 和其他类型的弟兄们。对于基本的词典探索,UserDict 具有所有相同的功能,并且不需要您在可用之前重写抽象方法。

可以使用任何映射。我建议您从 collections.Mappingcollections.MutableMapping1 继承。它们是抽象基础 classes -- 你提供几个方法,基础 class 填充其余部分。

这是您可以使用的 "frozendict" 示例:

from collections import Mapping

class FrozenDict(Mapping):
    """Immutable dictionary.

    Abstract methods required by Mapping are
    1. `__getitem__`
    2. `__iter__`
    3. `__len__`
    """

    def __init__(self, *args, **kwargs):
        self._data = dict(*args, **kwargs)

    def __getitem__(self, key):
        return self._data[key]

    def __iter__(self):
        return iter(self._data)

    def __len__(self):
        return len(self._data)

用法是:

def printer(**kwargs):
    print(kwargs)

d = FrozenDict({'a': 1, 'b': 2})
printer(**d)

回答你的问题,关于哪些 "magic" 方法是允许解包所必需的——仅基于实验——在 Cpython 中 class 和 __getitem__ keys 足以让它用 ** 解包。话虽如此,没有 guarantee 适用于其他实现(或 CPython 的未来版本)。要获得保证,您需要实现完整的映射接口(通常在我上面使用的基础 class 的帮助下)。

在python2.x中还有UserDict.UserDict which can be accessed in python3.x as collections.UserDict -- However if you're going to use this one, you can frequently just subclass from dict.

1请注意,从 Python3.3 开始,那些 classes 已移至 collections.abc 模块.