改变 *splat 和 **splatty-splat 运算符对我的对象执行的操作

19

怎样覆盖解包语法*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,似乎与*splat操作无关。


2
我99%确定你不能...但是我很想在这里被证明是错误的(请参见https://dev59.com/hWHVa4cB1Zd3GeqPoJNl) - Joran Beasley
你应该能够只实现可迭代或映射协议。但是我在让映射正常工作方面遇到了一些奇怪的问题。 - user2357112
2个回答

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

如果你想让***做除上述描述之外的其他事情,是不可能的。我没有这方面的文档参考(因为“你可以这样做”比“你不能这样做”更容易找到文档),但我有一个源引用。在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))执行不同的操作,那将会非常令人困惑。无论如何,***都是函数调用语法的一部分,而不是单独的运算符,因此定制它们(如果可能)将是可调用对象的工作,而不是参数的工作。我想允许可调用对象自定义它们可能有用例,例如通过传递像defaultdict这样的dict子类...


1
@wim:那不行。这会非常混乱。 - user2357112
1
@wim:它们不是单独的运算符,而是函数调用语法的一部分。由于您无法自定义传递常规参数时发生的情况,因此您无法单独定制它们。 - user2357112
1
@wim:文档只是说“这就是***的作用”,而没有说“你不能让它们做其他事情”。我想我会去查找哪个部分实现了相关的操作码。 - user2357112
1
我看到了一个关于两个新魔法方法的 PEP:__splat____splatty_splat__,在正常情况下它们默认回退到 __iter____iter__-with-__getitem__ - Mad Physicist
2
@mapf:在当前的CPython实现中,它技术上只查找keys__getitem__,但是语言参考指定展开的参数必须是映射:“如果函数调用中出现语法**expression,则expression必须评估为映射,其内容被视为其他关键字参数。”仅实现足以使**接受您的对象的功能会导致错误和混乱,因此我故意从我的答案中排除了它。 - user2357112
显示剩余13条评论

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 - 因此它实际上并没有像它所假装的那样覆盖了splat。


很容易让 *thing**thing 总是按照您的意愿行事,而不依赖于顺序 - 只需定义 def keys(self): return ('hello world',) - Nick Matteo
你没让 __len__ 返回 1 有什么特别的原因吗?还有,你需要扩展 Mapping 的任何原因吗? - Mad Physicist
@MadPhysicist 如果您不继承“Mapping”,则需要像映射一样呱呱叫。在“Thing”的上下文中,这意味着我们必须定义一个“keys”方法。如果您继承了“Mapping”,则需要定义抽象方法“__len__”,但我不关心它在这里返回什么 - 只要名称解析即可。 - wim
@wim。我一直以为你只需要返回1个__len__就可以了。有趣的发现。 - Mad Physicist

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