更改 *splat 和 **splatty-splat 操作符对我的对象的作用

19 投票
2 回答
3914 浏览
提问于 2025-04-17 22:02

如何重写解包语法 *obj**obj 的结果呢?

举个例子,你能不能创建一个对象 thing,让它的表现像这样:

>>> [*thing]
['a', 'b', 'c']
>>> [x for x in thing]
['d', 'e', 'f']
>>> {**thing}
{'hello world': 'I am a potato!!'}

注意:通过 __iter__ 进行迭代(比如“for x in thing”)返回的元素和使用 *splat 解包时返回的元素是不同的。

我查看了 operator.muloperator.pow,但这些函数只涉及两个操作数的使用,比如 a*ba**b,似乎和解包操作没有关系。

2 个回答

2

我确实成功地做出了一个对象,它的行为和我在问题中描述的一样,但我其实是走了些捷径。所以我只是把这个发出来,纯粹是为了好玩。

class Thing:
    def __init__(self):
        self.mode = 'abc'
    def __iter__(self):
        if self.mode == 'abc':
            yield 'a'
            yield 'b'
            yield 'c'
            self.mode = 'def'
        else:
            yield 'd'
            yield 'e'
            yield 'f'
            self.mode = 'abc'
    def __getitem__(self, item):
        return 'I am a potato!!'
    def keys(self):
        return ['hello world']

迭代器协议是通过从 __iter__ 返回的生成器对象来满足的(注意,Thing() 的实例本身并不是一个迭代器,但它是可迭代的)。映射协议则是通过存在 keys()__getitem__ 来满足的。不过,如果你还没注意到的话,你不能连续两次调用 *thing,让它两次都解包出 a,b,c,所以它其实并没有像看起来那样真正重写了展开操作。

28

* 是用来遍历一个对象的,它会把对象里的元素当作参数来使用。而 ** 则是遍历对象的 keys,然后用 __getitem__(这就像用方括号取值一样)来获取键值对。如果你想让 * 有特别的行为,只需要让你的对象可以被遍历;而如果想让 ** 有特别的行为,就需要把你的对象变成一个映射。

class MyIterable(object):
    def __iter__(self):
        return iter([1, 2, 3])

class MyMapping(collections.Mapping):
    def __iter__(self):
        return iter('123')
    def __getitem__(self, item):
        return int(item)
    def __len__(self):
        return 3

如果你希望 *** 做一些与上面描述的不同的事情,那是不可能的。我没有相关的文档来支持这个说法(因为找到“你可以这样做”的文档比“你不能这样做”的文档容易),但我有一个来源引用。Python的字节码解释器循环在 PyEval_EvalFrameEx 中调用 ext_do_call 来实现使用 *** 参数的函数调用。 ext_do_call 包含以下代码:

        if (!PyDict_Check(kwdict)) {
            PyObject *d;
            d = PyDict_New();
            if (d == NULL)
                goto ext_call_fail;
            if (PyDict_Update(d, kwdict) != 0) {

如果 ** 的参数不是一个字典,它会创建一个字典,并通过普通的 update 方法来从关键字参数初始化它(不过 PyDict_Update 不接受键值对的列表)。因此,你不能单独定制 **,而是需要实现映射协议。

同样,对于 * 参数,ext_do_call 会执行

        if (!PyTuple_Check(stararg)) {
            PyObject *t = NULL;
            t = PySequence_Tuple(stararg);

这相当于 tuple(args)。所以,你也不能单独定制 *,而是需要普通的遍历。

如果 f(*thing)f(*iter(thing)) 做不同的事情,那就会非常混乱。总之,*** 是函数调用语法的一部分,而不是独立的操作符,所以如果有可能定制它们,那应该是可调用对象的工作,而不是参数的工作。我想可能会有一些用例需要允许可调用对象定制它们,比如传递 dict 的子类,比如 defaultdict

撰写回答