Python 3.5与3.6自定义对象**解包行为差异及相关疑问
Container(**data0, b=7)行为差异的解答 先还原问题场景中的核心代码:
class Container: KEYS = ('a', 'b', 'c') def __init__(self, a=None, b=None, c=None): self.a = a self.b = b self.c = c def keys(self): return Container.KEYS def __getitem__(self, key): if key not in Container.KEYS: raise KeyError(key) return getattr(self, key) def __str__(self): # Python 3.6写法 # return f'{self.__class__.__name__}(a={self.a}, b={self.b}, c={self.c})' # Python 3.5写法 return ('{self.__class__.__name__}(a={self.a}, b={self.b}, ' 'c={self.c})').format(self=self) data0 = Container(a=1, b=2, c=3) print(data0) data3 = Container(**data0, b=7) print(data3)
两种Python版本的行为差异:
- Python 3.6:抛出
TypeError: type object got multiple values for keyword argument 'b' - Python 3.5:抛出
KeyError: 0
问题1:Python 3.5与3.6之间的哪些变更导致了这种行为差异?
核心差异在于**操作符对自定义对象的处理逻辑:
在Python 3.5中,解释器判断一个对象能否被
**展开为键值对,严格依赖它是否属于collections.abc.Mapping类型(或完全符合mapping接口规范)。你的Container类虽然实现了keys()和__getitem__,但缺少__iter__方法,也没有继承自Mapping抽象类,因此不会被当作mapping处理。这时解释器会退而求其次,尝试将对象视为可迭代的(key, value)对序列。但因为类没有实现
__iter__,Python会启用默认迭代器逻辑:从整数0开始,依次调用__getitem__(0)、__getitem__(1)……直到抛出IndexError。而你的__getitem__只接受'a'/'b'/'c'作为键,所以第一次调用__getitem__(0)就抛出了KeyError: 0。在Python 3.6中,官方调整了
**操作符的处理逻辑:只要对象实现了keys()和__getitem__方法,就会被当作mapping来处理,直接遍历keys()返回的键生成对应键值对。因此**data0会正确展开为a=1, b=2, c=3,后续传入的b=7会和展开的b=2产生参数冲突,抛出TypeError——这其实是更符合预期的正确行为。
问题2:这些整数序列的来源是什么?
当Python尝试迭代一个没有实现__iter__方法的对象时,会自动启用默认迭代器实现:它会从整数0开始,依次以递增的整数作为参数调用__getitem__,直到调用时抛出IndexError才停止迭代。
你修改__getitem__后没有抛出异常,而是打印key并返回,所以这个默认迭代器会无限生成0、1、2、3……这样的连续整数,这就是你看到的整数序列的来源。
更新说明:实现__iter__后行为一致的原因
当你为Container添加以下__iter__方法后:
def __iter__(self): return iter(Container.KEYS)
Python 3.5的解释器会将Container对象视为可迭代的键序列(迭代'a'、'b'、'c'),此时**data0会遍历这些键生成对应的键值对a=1, b=2, c=3,后续的b=7同样会引发参数冲突,因此行为和Python 3.6完全一致。
内容的提问来源于stack exchange,提问作者hiro protagonist

