Словарь Python plus-equal поведение

Я пытаюсь понять точный механизм обновления словаря Python с помощью d[key] += diff. У меня есть несколько вспомогательных классов для отслеживания вызовов магических методов:

class sdict(dict):
    def __setitem__(self, *args, **kargs):
        print "sdict.__setitem__"
        return super(sdict, self).__setitem__(*args, **kargs)
    def __delitem__(self, *args, **kargs):
        print "sdict.__delitem__"
        return super(sdict, self).__delitem__(*args, **kargs)
    def __getitem__(self, *args, **kargs):
        print "sdict.__getitem__"
        return super(sdict, self).__getitem__(*args, **kargs)
    def __iadd__(self, *args, **kargs):
        print "sdict.__iadd__"
        return super(sdict, self).__iadd__(*args, **kargs)
    def __add__(self, *args, **kargs):
        print "sdict.__add__"
        return super(sdict, self).__add__(*args, **kargs)

class mutable(object):
    def __init__(self, val=0):
        self.value = val
    def __iadd__(self, val):
        print "mutable.__iadd__"
        self.value = self.value + val
        return self
    def __add__(self, val):
        print "mutable.__add__"
        return mutable(self.value + val)

С этими инструментами приступим к погружению:

>>> d = sdict()
>>> d["a"] = 0
sdict.__setitem__
>>> d["a"] += 1
sdict.__getitem__
sdict.__setitem__
>>> d["a"]
sdict.__getitem__
1

Мы не видим, чтобы здесь вызывалась какая-либо операция __iadd__, что имеет смысл, поскольку левое выражение d["a"] возвращает целое число, которое не реализует метод __iadd__. Мы видим, как Python волшебным образом преобразует оператор += в вызовы __getitem__ и __setitem__.

Продолжение:

>>> d["m"] = mutable()
sdict.__setitem__
>>> d["m"] += 1
sdict.__getitem__
mutable.__iadd__
sdict.__setitem__
>>> d["m"]
sdict.__getitem__
<__main__.mutable object at 0x106c4b710>

Здесь оператор += успешно вызывает метод __iadd__. Похоже, что оператор += используется дважды:

  • Один раз за волшебный перевод на __getitem__ и __setitem__ звонки
  • Второй раз для вызова __iadd__.

Мне нужна помощь в следующем:

  • Каков точный технический механизм преобразования оператора += в вызовы __getitem__ и __setitem__?
  • Почему во втором примере оператор += используется дважды? Разве python не переводит оператор в d["m"] = d["m"] + 1 (в этом случае мы не увидим, что __add__ вызывается вместо __iadd__?)

person moatra    schedule 21.03.2014    source источник
comment
Я предполагаю, что, поскольку оператор получения значения был реализован, оператор считается эквивалентным этому: X = get; Х += 1; установить (Х);   -  person Lasse V. Karlsen    schedule 22.03.2014
comment
Все это выглядит довольно просто для меня. sdict.__iadd__ никогда не вызывается, потому что у вас нет кода, который выполняет += для объекта sdict. Вы выполняете только += для элементов в sdict - в первом случае целое число, а во втором случае mutable. Но вы уже продемонстрировали, что понимаете это, поэтому я не уверен, в чем вопрос.   -  person dg99    schedule 22.03.2014


Ответы (2)


В первом примере вы не применяли к словарю оператор +=. Вы применили его к значению, хранящемуся в ключе d['a'], и это совершенно другой объект.

Другими словами, Python извлечет d['m'] (вызов __getitem__), применит к нему оператор +=, а затем установит результат этого выражения обратно в d['m'] (вызов __setitem__).

Метод __iadd__ либо возвращает self измененный на месте, либо новый объект, но Python не может точно знать, что вернул метод. Так что он должен всегда вызывать d.__setitem__('m', <return_value_from_d['m'].__iadd__(1)>).

Точно то же самое произойдет, если вы сделали:

m = d['m']
m += 1
d['m'] = m

но без дополнительного имени m в глобальном пространстве имен.

Если бы экземпляр mutable() хранился не в словаре, а в глобальном пространстве имен, происходит точно такая же последовательность событий, но непосредственно в словаре globals(), и вы не увидите вызовы __getitem__ и __setitem__.

Это описано в справочной документации по расширенному назначению:

Расширенное присваивание оценивает цель (которая, в отличие от обычных операторов присваивания, не может быть распаковкой) и список выражений, выполняет бинарную операцию, специфичную для типа присваивания двух операндов, и присваивает результат исходной цели.

где d['m'] — цель; оценка цели здесь включает __getitem__, присвоение результата обратно исходной цели вызывает __setitem__.

person Martijn Pieters    schedule 21.03.2014
comment
Я думаю, что меня смущает фактический механизм, в котором python переводит += в __getitem__ и __setitem__ вызывает словарь. Во втором примере кажется, что он использует один оператор += дважды: один раз для вызова __getitem__ и __setitem__, а второй раз для вызова __iadd__ для mutable. Как это работает? - person moatra; 22.03.2014
comment
@moatra: Вы следовали последней части моего ответа? Для работы += Python должен сначала получить объект в левой части уравнения (__getitem__), затем применить +=, а затем, когда это будет сделано, вернуть объект туда, откуда он был получен (__setitem__). - person Martijn Pieters; 22.03.2014
comment
Насколько я понимаю, Python переводит d["m"] += 1 в d["m"] = d["m"] + 1. Вместо этого он переводит его на d["m"] = d["m"] += 1? Независимо от того, где указано это поведение? - person moatra; 22.03.2014
comment
Ваше понимание неверно. См. документацию по расширенному назначению. - person Martijn Pieters; 22.03.2014
comment
Если метод __iadd__ не определен для d['m'], то будет применен d['m'] = d['m'] + 1, но здесь это не так. - person Martijn Pieters; 22.03.2014
comment
Ах. Мое замешательство возникло из-за того, что неправильное мышление += было реализовано чисто магическим методом --- на самом деле это грамматическая конструкция. - person moatra; 22.03.2014
comment
@MartijnPieters Клянусь, я никогда не видел, чтобы кто-то был таким профессионалом @python, и точка. Если бы я мог, я бы поставил палец вверх в 1000 раз за каждый ваш ответ. Спасибо, сэр! Надеюсь, ты никогда не уйдешь на пенсию! - person Marius Mucenicu; 25.07.2018

Поскольку, как указано в документах, __iadd__ может выполнить операцию в -place, но результатом будет либо сам объект, либо новый объект, поэтому вызывается __setitem__.

person Maciej Gol    schedule 21.03.2014