На месте пользовательский объект распаковки другое поведение с getitem 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)

как указано в предыдущем вопросе, это вызывает

ошибку TypeError: объект типа есть несколько значений для аргумента 'б'

на 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)

это выводит int последовательность 0, 1, 2, 3, 4, .... (python 3.5)

Итак, мои вопросы:

  • что изменилось между версиями, что делает это вести себя так по-разному?

  • откуда берутся эти целые числа?


обновление : как упомянуто в комментарий λuser реализация __iter__ изменит поведение на python 3.5, чтобы соответствовать тому, что делает python 3.6:

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

1 ответов


на самом деле это сложный конфликт между несколькими внутренними операциями при распаковке пользовательского объекта сопоставления и создании аргументов вызывающего. Поэтому, если вы хотите полностью понять основные причины, я бы предложил вам изучить исходный код. Однако вот некоторые подсказки и отправные точки, которые вы можете изучить для получения более подробной информации.

внутренне, когда вы распаковываете на уровне вызывающего абонента, байтовый код BUILD_MAP_UNPACK_WITH_CALL(count) соз графа сопоставления из стека, объединяет их в один словарь и толкает результат. С другой стороны, эффект стека этого кода операции с аргументом oparg определяется следующим образом:

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__ способ?

всякий раз, когда интерпретатор сталкивается с исключением во время распаковки, которое в этом случае является KeyError, он перестает продолжать поток push / pop и вместо того, чтобы возвращать реальное значение вашей переменной, он возвращает эффект стека, поэтому сначала ключ равен 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 который отвечает за создание кортежа и потребление элементов count из стека.

BUILD_TUPLE (count)

создает кортеж, потребляющий элементы count из стека, и толкает > результирующий Кортеж на стек.

и это, ИМО, почему вы не получаете ключевую ошибку, а вместо этого получаете TypeError. Потому что во время создания кортежа аргументов он сталкивается с повторяющимся именем и, следовательно, правильно возвращает TypeError.