Change what the *splat and **splatty-splat operators do to my object

前端 未结 2 819
猫巷女王i
猫巷女王i 2020-12-01 14:55

How do you override the result of unpacking syntax *obj and **obj?

For example, can you somehow create an object thing which b

相关标签:
2条回答
  • 2020-12-01 15:09

    I did succeed in making an object that behaves how I described in my question, but I really had to cheat. So just posting this here for fun, really -

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

    The iterator protocol is satisfied by a generator object returned from __iter__ (note that a Thing() instance itself is not an iterator, though it is iterable). The mapping protocol is satisfied by the presence of keys() and __getitem__. Yet, in case it wasn't already obvious, you can't call *thing twice in a row and have it unpack a,b,c twice in a row - so it's not really overriding splat like it pretends to be doing.

    0 讨论(0)
  • 2020-12-01 15:29

    * iterates over an object and uses its elements as arguments. ** iterates over an object's keys and uses __getitem__ (equivalent to bracket notation) to fetch key-value pairs. To customize *, simply make your object iterable, and to customize **, make your object a mapping:

    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
    

    If you want * and ** to do something besides what's described above, you can't. I don't have a documentation reference for that statement (since it's easier to find documentation for "you can do this" than "you can't do this"), but I have a source quote. The bytecode interpreter loop in PyEval_EvalFrameEx calls ext_do_call to implement function calls with * or ** arguments. ext_do_call contains the following code:

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

    which, if the ** argument is not a dict, creates a dict and performs an ordinary update to initialize it from the keyword arguments (except that PyDict_Update won't accept a list of key-value pairs). Thus, you can't customize ** separately from implementing the mapping protocol.

    Similarly, for * arguments, ext_do_call performs

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

    which is equivalent to tuple(args). Thus, you can't customize * separately from ordinary iteration.

    It'd be horribly confusing if f(*thing) and f(*iter(thing)) did different things. In any case, * and ** are part of the function call syntax, not separate operators, so customizing them (if possible) would be the callable's job, not the argument's. I suppose there could be use cases for allowing the callable to customize them, perhaps to pass dict subclasses like defaultdict through...

    0 讨论(0)
提交回复
热议问题