使用__getitem__ python 3.5 vs python 3.6解压缩不同行为的就地自定义对象

hir*_*ist 7 python python-3.x python-internals python-3.5 python-3.6

关于这个问题的后续问题:我在python 3.5和python 3.6上运行了下面的代码 - 结果非常不同:

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)
Run Code Online (Sandbox Code Playgroud)

正如上一个问题所述,这引起了人们的注意

TypeError:type object为关键字参数'b'获取了多个值

在python 3.6上.但在python 3.5上我得到了例外:

KeyError:0

而且如果我不提高KeyError但只是打印出来key并且return__getitem__:

def __getitem__(self, key):
    if key not in Container.KEYS:
        # raise KeyError(key)
        print(key)
        return
    return getattr(self, key)
Run Code Online (Sandbox Code Playgroud)

这将打印出int序列0, 1, 2, 3, 4, ....(python 3.5)

所以我的问题是:

  • 这些版本之间发生了什么变化,这使得它的表现如此不同?

  • 这些整数来自哪里?


更新:正如λuser的评论中所提到的:实现__iter__将改变python 3.5上的行为以匹配python 3.6的作用:

def __iter__(self):
    return iter(Container.KEYS)
Run Code Online (Sandbox Code Playgroud)

Kas*_*mvd 4

这实际上是解包自定义映射对象和创建调用者参数期间多个内部操作之间的复杂冲突。因此,如果您想彻底了解根本原因,我建议您查看源代码。不过,您可以查看以下一些提示和起点以了解更多详细信息。

在内部,当您在调用者级别解包时,字节代码会从堆栈中BUILD_MAP_UNPACK_WITH_CALL(count)弹出计数映射,将它们合并到单个字典中并推送结果。另一方面,带有参数的操作码的堆栈效果oparg 定义如下

case BUILD_MAP_UNPACK_WITH_CALL:
    return 1 - oparg;
Run Code Online (Sandbox Code Playgroud)

话虽如此,让我们看一下示例(Python-3.5 中)的字节码,看看它的实际效果:

>>> def bar(data0):foo(**data0, b=4)
... 
>>> 
>>> dis.dis(bar)
  1           0 LOAD_GLOBAL              0 (foo)
              3 LOAD_FAST                0 (data0)
              6 LOAD_CONST               1 ('b')
              9 LOAD_CONST               2 (4)
             12 BUILD_MAP                1
             15 BUILD_MAP_UNPACK_WITH_CALL   258
             18 CALL_FUNCTION_KW         0 (0 positional, 0 keyword pair)
             21 POP_TOP
             22 LOAD_CONST               0 (None)
             25 RETURN_VALUE
>>> 
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,在偏移量 15 处,我们有BUILD_MAP_UNPACK_WITH_CALL负责解包的字节码。

现在,如果它返回 0 作为key__getitem__方法的参数,会发生什么情况?

每当解释器在解包过程中遇到异常(在本例中为 a )时KeyError,它就会停止继续推入/弹出流程,而不是返回变量的实际值,而是返回堆栈效果,这就是为什么键首先为 0 并且如果每次获得递增结果时(由于堆栈大小),您不会处理异常。

现在,如果您在 Python-3.6 中执行相同的反汇编,您将得到以下结果:

>>> dis.dis(bar)
  1           0 LOAD_GLOBAL              0 (foo)
              2 BUILD_TUPLE              0
              4 LOAD_FAST                0 (data0)
              6 LOAD_CONST               1 ('b')
              8 LOAD_CONST               2 (4)
             10 BUILD_MAP                1
             12 BUILD_MAP_UNPACK_WITH_CALL     2
             14 CALL_FUNCTION_EX         1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

在创建局部变量 ( LOAD_FAST) 之前和之后,LOAD_GLOBAL有一个BUILD_TUPLE负责创建元组并消耗堆栈中的计数项。

BUILD_TUPLE(计数)

创建一个消耗堆栈中 count 个项目的元组,并将 >结果元组推送到堆栈上。

这就是,IMO,为什么你没有得到关键错误,而是得到TypeError. 因为在创建参数元组期间,它遇到重复的名称,因此正确地返回TypeError.