Подкласс Python словарь для переопределения __setitem__ - PullRequest
40 голосов
/ 14 января 2010

Я создаю класс, который подклассов dict и переопределяет __setitem__. Я хотел бы быть уверен, что мой метод будет вызываться во всех случаях, когда возможно установить элементы словаря.

Я обнаружил три ситуации, когда Python (в данном случае 2.6.4) не вызывает мой переопределенный метод __setitem__ при установке значений, а вместо этого вызывает PyDict_SetItem напрямую

  1. В конструкторе
  2. В методе setdefault
  3. В методе update

Как очень простой тест:

class MyDict(dict):
    def __setitem__(self, key, value):
        print "Here"
        super(MyDict, self).__setitem__(key, str(value).upper())

>>> a = MyDict(abc=123)
>>> a['def'] = 234
Here
>>> a.update({'ghi': 345})
>>> a.setdefault('jkl', 456)
456
>>> print a
{'jkl': 456, 'abc': 123, 'ghi': 345, 'def': '234'}

Вы видите, что переопределенный метод вызывается только при явной установке элементов. Чтобы Python всегда вызывал мой метод __setitem__, мне пришлось переопределить эти три метода, например:

class MyUpdateDict(dict):
    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    def __setitem__(self, key, value):
        print "Here"
        super(MyUpdateDict, self).__setitem__(key, value)

    def update(self, *args, **kwargs):
        if args:
            if len(args) > 1:
                raise TypeError("update expected at most 1 arguments, got %d" % len(args))
            other = dict(args[0])
            for key in other:
                self[key] = other[key]
        for key in kwargs:
            self[key] = kwargs[key]

    def setdefault(self, key, value=None):
        if key not in self:
            self[key] = value
        return self[key]

Существуют ли другие методы, которые мне нужно переопределить, чтобы знать, что Python будет всегда вызывать мой __setitem__ метод?

UPDATE

Согласно предложению gs, я попытался создать подкласс UserDict (фактически, IterableUserDict, так как я хочу перебирать ключи) следующим образом:

from UserDict import *;
class MyUserDict(IterableUserDict):
    def __init__(self, *args, **kwargs):
        UserDict.__init__(self,*args,**kwargs)

    def __setitem__(self, key, value):
        print "Here"
        UserDict.__setitem__(self,key, value)

Этот класс, кажется, правильно вызывает my __setitem__ на setdefault, но не вызывает его на update или когда начальные данные передаются конструктору.

ОБНОВЛЕНИЕ 2

Предложение Питера Хансена заставило меня более внимательно посмотреть на dictobject.c, и я понял, что метод обновления можно немного упростить, поскольку встроенный конструктор словаря просто вызывает встроенный метод обновления в любом случае. Теперь это выглядит так:

def update(self, *args, **kwargs):
    if len(args) > 1:
        raise TypeError("update expected at most 1 arguments, got %d" % len(args))
    other = dict(*args, **kwargs)
    for key in other:
        self[key] = other[key]

Ответы [ 4 ]

47 голосов
/ 07 апреля 2010

Я отвечаю на свой вопрос, так как в конце концов решил, что я действительно делаю хочу создать подкласс Dict, а не создавать новый класс отображения, а UserDict все еще откладывается на базовый объект Dict в некоторых случаях вместо использования предоставленной __setitem__.

После прочтения и перечитывания исходного кода Python 2.6.4 (в основном Objects/dictobject.c, но я нашел информацию в другом месте, чтобы узнать, где используются различные методы), я понимаю, что следующий код равен достаточно, чтобы мой __setitem__ вызывался каждый раз, когда объект изменяется, и в противном случае ведет себя точно так же, как Python Dict:

Предложение Питера Хансена заставило меня более внимательно взглянуть на dictobject.c, и я понял, что метод обновления в моем исходном ответе можно немного упростить, поскольку встроенный конструктор словаря в любом случае просто вызывает встроенный метод обновления , Поэтому второе обновление в моем ответе было добавлено в код ниже (кем-то полезным; -).

class MyUpdateDict(dict):
    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    def __setitem__(self, key, value):
        # optional processing here
        super(MyUpdateDict, self).__setitem__(key, value)

    def update(self, *args, **kwargs):
        if args:
            if len(args) > 1:
                raise TypeError("update expected at most 1 arguments, "
                                "got %d" % len(args))
            other = dict(args[0])
            for key in other:
                self[key] = other[key]
        for key in kwargs:
            self[key] = kwargs[key]

    def setdefault(self, key, value=None):
        if key not in self:
            self[key] = value
        return self[key]

Я проверил это с помощью этого кода:

def test_updates(dictish):
    dictish['abc'] = 123
    dictish.update({'def': 234})
    dictish.update(red=1, blue=2)
    dictish.update([('orange', 3), ('green',4)])
    dictish.update({'hello': 'kitty'}, black='white')
    dictish.update({'yellow': 5}, yellow=6)
    dictish.setdefault('brown',7)
    dictish.setdefault('pink')
    try:
        dictish.update({'gold': 8}, [('purple', 9)], silver=10)
    except TypeError:
        pass
    else:
        raise RunTimeException("Error did not occur as planned")

python_dict = dict([('b',2),('c',3)],a=1)
test_updates(python_dict)

my_dict = MyUpdateDict([('b',2),('c',3)],a=1)
test_updates(my_dict)

и это проходит. Все другие реализации, которые я пробовал, в какой-то момент потерпели неудачу. Я все равно приму ответы, которые покажут мне, что я что-то пропустил, но в противном случае я отмечаю галочку рядом с этим через пару дней и называю это правильным ответом:)

4 голосов
/ 14 января 2010

Каков ваш вариант использования для подкласса dict?

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

Лучший способ выполнить то, что вам нужно, это, вероятно, абстрактный базовый класс MutableMapping. PEP 3119 - Представление абстрактных базовых классов

Это также поможет вам ответить на вопрос «Есть ли другие методы, которые мне нужно переопределить?». Вам нужно будет переопределить все абстрактные методы. Для MutableMapping: абстрактные методы включают setitem , delitem . Конкретные методы включают в себя pop, popitem, clear, update.

3 голосов
/ 23 июля 2014

Я нашел ответ и комментарии Яна очень полезными и понятными.Я просто хотел бы отметить, что, возможно, первый вызов метода суперкласса __init__ может быть более безопасным, когда в этом нет необходимости: недавно мне нужно было реализовать пользовательский OrderedDict (я работаю с Python 2.7): после реализации и модификации моего кода в соответствии с предложенной реализацией MyUpdateDict я обнаружил, что, просто заменив

class MyUpdateDict(dict):

на:

from collections import OrderedDict
class MyUpdateDict(OrderedDict):

, тестовый код, опубликованный выше, не удался:

Traceback (most recent call last):
File "Desktop/test_updates.py", line 52, in <module>
    my_dict = MyUpdateDict([('b',2),('c',3)],a=1)
File "Desktop/test_updates.py", line 5, in __init__
    self.update(*args, **kwargs)
File "Desktop/test_updates.py", line 18, in update
    self[key] = other[key]
File "Desktop/test_updates.py", line 9, in __setitem__
    super(MyUpdateDict, self).__setitem__(key, value)
File "/usr/lib/python2.7/collections.py", line 59, in __setitem__
    root = self.__root
AttributeError: 'MyUpdateDict' object has no attribute '_OrderedDict__root'

Глядя на код collection.py , выясняется, что OrderedDict требуется его __init__ метод для вызова для инициализации и настройки необходимых пользовательскихатрибутов.

Поэтому, просто добавив первый вызов к методу super __init__,

from collections import OrderedDict
class MyUpdateDict(Orderedict):
def __init__(self, *args, **kwargs):
    super(MyUpdateDict, self).__init__() #<-- HERE call to super __init__
    self.update(*args, **kwargs)

, мы получаем более общее решение, которое, очевидно, работает как для dict, так и для OrderedDict.

Я не могу сказать, является ли это решение в целом допустимым, потому что я тестировал его только с помощью OrderedDict.Однако вполне вероятно, что вызов метода super __init__ либо безвреден, либо необходим, а не вреден, при попытке расширить другие подклассы dict

0 голосов
/ 08 апреля 2013

Используйте object.keyname = значение вместо объекта ["keyname"] = значение

...