python - 就地自定义对象使用 __getitem__ python 3.5 与 python 3.6 解包不同的行为

标签 python python-3.x python-3.5 python-3.6 python-internals

关于 this question 的后续问题: 我在 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)

如前一个问题所述

TypeError: type object got multiple values for keyword argument 'b'

在 python 3.6 上。但是在 python 3.5 上我得到了异常:

KeyError: 0

此外,如果我不引发 KeyError 而只是在 __getitem__ 中打印出 keyreturn:

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

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

所以我的问题是:

  • 版本之间发生了什么变化,导致其行为如此不同?

  • 这些整数是从哪里来的?


UPDATE :如 λuser 的评论中所述: 实现 __iter__ 将改变 python 3.5 的行为以匹配 python 3.6 的行为:

def __iter__(self):
    return iter(Container.KEYS)

最佳答案

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

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

case BUILD_MAP_UNPACK_WITH_CALL:
    return 1 - oparg;

话虽如此,让我们看一下示例的字节码(在 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
>>> 

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

现在它返回 0 作为 __getitem__ 方法的 key 参数会发生什么?

每当解释器在解包过程中遇到异常时,在本例中是 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

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

BUILD_TUPLE(count)

Creates a tuple consuming count items from the stack, and pushes the >resulting tuple onto the stack.

在我看来,这就是为什么您没有收到关键错误而是收到 TypeError 的原因。因为在创建参数元组期间它遇到了重复的名称,因此正确地返回了 TypeError

关于python - 就地自定义对象使用 __getitem__ python 3.5 与 python 3.6 解包不同的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50384549/

相关文章:

python - 将输出保存(打印)为 xml 文件不起作用

python - pyodbc 安装不支持 python 3.5.1

python - Python 中特定于包的导入 Hook

python - TensorFlow 训练 - "Batch size"和 tf.unpack - 解包非 "batch sized"动态值?

python - PyQt5 中的 FocusReason?

python - 为什么 range() 函数比乘法项目慢以获取嵌套列表中的副本?

python - 为什么对于整数而不是 float ,ndigits=None 上的 round 加注?

python - 为什么 os.scandir() 速度变慢/如何重组大目录?

Python Setter 和 Getter 命名

python - PyQt 在处理事件时避免陷入无限循环