更改 *splat 和 **splatty-splat 操作符对我的对象的作用
如何重写解包语法 *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.mul
和 operator.pow
,但这些函数只涉及两个操作数的使用,比如 a*b
和 a**b
,似乎和解包操作没有关系。
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
,所以它其实并没有像看起来那样真正重写了展开操作。
*
是用来遍历一个对象的,它会把对象里的元素当作参数来使用。而 **
则是遍历对象的 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
。