14

I'm trying to use Python's @property decorator on a dict in a class. The idea is that I want a certain value (call it 'message') to be cleared after it is accessed. But I also want another value (call it 'last_message') to contain the last set message, and keep it until another message is set. In my mind, this code would work:

>>> class A(object):
...     def __init__(self):
...             self._b = {"message": "", 
...                        "last_message": ""}
...     @property
...     def b(self):
...             b = self._b
...             self._b["message"] = ""
...             return b
...     @b.setter
...     def b(self, value):
...             self._b = value
...             self._b["last_message"] = value["message"]
...
>>>

However, it doesn't seem to:

>>> a = A()
>>> a.b["message"] = "hello"
>>> a.b["message"]
''
>>> a.b["last_message"]
''
>>>

I'm not sure what I have done wrong? It seems to me like @property doesn't work like I would expect it to on dicts, but maybe I'm doing something else fundamentally wrong?

Also, I know that I could just use individual values in the class. But this is implemented as a session in a web application and I need it to be a dict. I could either make this work, or make the whole session object to pretend it's a dict, or use individual variables and hack it into workingness throughout the rest of the code base. I would much rather just get this to work.

martineau
  • 119,623
  • 25
  • 170
  • 301
Carson Myers
  • 37,678
  • 39
  • 126
  • 176
  • 2
    you should put your answer into a new _(self-)answer_ instead of editing into your _question_ – Tobias Kienzler Jun 20 '14 at 09:51
  • btw, instead of a regular `dict` you might be interested in using [`__slots__ = ['message', 'last_message']`](https://docs.python.org/2/reference/datamodel.html?highlight=slots#__slots__) if those are the only two (fixed) keys. Or modify a `collections.namedtuple` – Tobias Kienzler Jun 20 '14 at 09:58

2 Answers2

21
class MyDict(dict):
    def __setitem__(self, key, value):
        if key == 'message':
            super().__setitem__('message', '')
            super().__setitem__('last_message', value) 
        else:
            super().__setitem__(key, value)

class A(object):
    def __init__(self):
        self._b = MyDict({"message": "", 
                          "last_message": ""})

    @property
    def b(self):
        return self._b

a = A()
a.b['message'] = 'hello'
print(a.b['message'])
# ''
print(a.b['last_message'])
# hello

As I think you've discovered, the reason why your setter wasn't working is because

a.b['message']='hello'

first accesses a.b, which calls the b property's getter, not its setter. The getter returns the dict self._b. Then self._b['message']='hello' causes the dict's __setitem__ is called .

So to fix the problem, you need a special dict (like MyDict).

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thank you, I made a special dict and it's now working. I dropped properties altogether and just moved all of the functionality into the dict, though (with `__getitem__`). I'll add my own code to the question as well. – Carson Myers Jun 29 '10 at 04:16
  • 2
    You (and @Carson) should use `super(MyDict, self).__setitem__(key, value)` instead of `dict.__setitem__(self, key, value)` etc (otherwise subclasses of yours with multiple ancestors will run into trouble) – Tobias Kienzler Jun 20 '14 at 09:53
3

I may be missing what you are trying to do here, but does this solve your problem?

class A(object):
    def __init__(self):
        self._b = {'message':'',
                   'last_message': ''}

    @property
    def b(self):
        b = self._b.copy()
        self._b['message'] = ''
        return b

    @b.setter
    def b(self, value):
        self._b['message'] = value
        self._b['last_message'] = value


if __name__ == "__main__":
    a = A()
    a.b = "hello"
    print a.b
    print a.b
    print a.b["last_message"]

$ python dictPropTest.py
{'last_message': 'hello', 'message': 'hello'}
{'last_message': 'hello', 'message': ''}
hello
sberry
  • 128,281
  • 18
  • 138
  • 165
  • 2
    close, I actually just discovered that assigning to a key of a setter doesn't really work. I was thinking that `a.b['message'] = "hello"` would pass a dict containing a `message` key to the setter, but that's not the case. But what I really want is to be able to assign to a key of a setter – Carson Myers Jun 29 '10 at 03:58