就地自定义对象使用 __getitem__ python 3.5 与 python 3.6 解包不同的行为 [英] In-place custom object unpacking different behavior with __getitem__ python 3.5 vs python 3.6

查看:63
本文介绍了就地自定义对象使用 __getitem__ python 3.5 与 python 3.6 解包不同的行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

类容器:键 = ('a', 'b', 'c')def __init__(self, a=None, b=None, c=None):self.a = a自我.b = bself.c = c定义键(自己):返回 Container.KEYSdef __getitem__(self, key):如果密钥不在 Container.KEYS 中:引发 KeyError(key)返回 getattr(self, key)def __str__(self):#蟒蛇3.6# return f'{self.__class__.__name__}(a={self.a}, b={self.b}, c={self.c})'# 蟒蛇 3.5return ('{self.__class__.__name__}(a={self.a}, b={self.b}, ''c={self.c})').format(self=self)data0 = 容器(a=1,b=2,c=3)打印(数据0)数据 3 = 容器(**数据 0,b=7)打印(数据3)

如上一个问题所述

<块引用>

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

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

<块引用>

密钥错误:0

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

def __getitem__(self, key):如果密钥不在 Container.KEYS 中:# 引发 KeyError(key)打印(键)返回返回 getattr(self, key)

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

所以我的问题是:

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

  • 这些整数来自哪里?

<小时>

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

def __iter__(self):返回迭代器(Container.KEYS)

解决方案

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

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

case BUILD_MAP_UNPACK_WITH_CALL:返回 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(数据 0)6 LOAD_CONST 1 ('b')9 LOAD_CONST 2 (4)12 BUILD_MAP 115 BUILD_MAP_UNPACK_WITH_CALL 25818 CALL_FUNCTION_KW 0(0 位置,0 关键字对)21 POP_TOP22 LOAD_CONST 0 (无)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 04 LOAD_FAST 0(数据 0)6 LOAD_CONST 1 ('b')8 LOAD_CONST 2 (4)10 BUILD_MAP 112 BUILD_MAP_UNPACK_WITH_CALL 214 CALL_FUNCTION_EX 116 POP_TOP18 LOAD_CONST 0 (无)20 RETURN_VALUE

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

<块引用>

BUILD_TUPLE(计数)

从堆栈中创建一个消耗 count 个项目的元组,并将 >结果元组压入堆栈.

这就是,IMO,为什么您没有收到关键错误,而是收到 TypeError.因为在创建参数元组的过程中,它遇到了重复的名称,因此正确地返回 TypeError.

a follow-up question on this question: i ran the code below on python 3.5 and python 3.6 - with very different results:

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)

as stated in the previous question this raises

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

on python 3.6. but on python 3.5 i get the exception:

KeyError: 0

moreover if i do not raise KeyError but just print out the key and return in __getitem__:

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

this will print out the int sequence 0, 1, 2, 3, 4, .... (python 3.5)

so my questions are:

  • what has changed between the releases that makes this behave so differently?

  • where are these integers coming from?


UPDATE : as mentioned in the comment by λuser: implementing __iter__ will change the behavior on python 3.5 to match what python 3.6 does:

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

解决方案

This is actually a complicated conflict between multiple internal operations during unpacking a custom mapping object and creating the caller's arguments. Therefore, if you wan to understand the underlying reasons thoroughly I'd suggest you to look into the source code. However, here are some hints and starting points that you can look into for greater details.

Internally, when you unpack at a caller level, the byte code BUILD_MAP_UNPACK_WITH_CALL(count) pops count mappings from the stack, merges them into a single dictionary and pushes the result. In other hand, the stack effect of this opcode with argument oparg is defined as following:

case BUILD_MAP_UNPACK_WITH_CALL:
    return 1 - oparg;

With that being said lets look at the byte codes of an example (in Python-3.5) to see this in action:

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

As you can see, at offset 15 we have BUILD_MAP_UNPACK_WITH_CALL byte code which is responsible for the unpacking.

Now what happens that it returns 0 as the key argument to the __getitem__ method?

Whenever the interpreter encounters an exception during unpacking, which in this case is a KeyError, It stops continuing the push/pop flow and instead of returning the real value of your variable it returns the stack effect which is why the key is 0 at first and if you don't handle the exception each time you get an incremented result (due to the stack size).

Now if you do the same disassembly in Python-3.6 you'll get the following result:

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

Before creating the local variables (LOAD_FAST) and after LOAD_GLOBAL there is a BUILD_TUPLE which is responsible for creating a tuple and consuming count items from the stack.

BUILD_TUPLE(count)

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

And this is, IMO, why you don't get a key error and instead you get TypeError. Because during the creation of a tuple of arguments it encounters a duplicate name and therefore, properly, returns the TypeError.

这篇关于就地自定义对象使用 __getitem__ python 3.5 与 python 3.6 解包不同的行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆