views:

192

answers:

2

Can anyone here identify why the TypeError is being raised at the bottom of this example shown below?

>>> import threading
>>> class SessionManager(threading.Thread, threading._RLock, dict):

    UPDATE = 60 * 60

    def run(self):
        while True:
            time.sleep(self.UPDATE)
            with self:
                for key in tuple(self):
                    if not self[key]:
                        del self[key]

    def __getitem__(self, key):
        session = super()[key]
        session.wakeup()
        return session

>>> SM = SessionManager()
>>> SM.daemon = True
>>> SM.start()
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    SM.start()
TypeError: unhashable type: 'SessionManager'
>>> 

Edit:

What follows is the finished version of the module started above. It is utilized in the VerseMatch program.

#! /usr/bin/env python
"""Oversee the timely destruction of unused sessions.

The two classes in this module allow automated memory cleanup to be regularly
performed and timed actions to be executed within reasonable time periods."""

################################################################################

__author__ = 'Stephen "Zero" Chappell <[email protected]>'
__date__ = '11 February 2010'
__version__ = '$Revision: 3 $'

################################################################################

import threading
import time

################################################################################

class SessionManager(threading.Thread, threading._RLock, dict):

    """Manage session objects along with associated data.

    This class acts as dictionary with a data-protection mutex.
    It can run a cleanup routine at regular intervals if needed."""

    def __init__(self, sleep_interval):
        """Initialize variables in SessionManager's parent classes."""
        threading.Thread.__init__(self)
        threading._RLock.__init__(self)
        self.__sleep_interval = sleep_interval

    def run(self):
        """Remove old sessions from memory as needed.

        This method is executed by calling .start() on a SessionManager
        object. The "daemon" attribute may need be set to True before
        activating this feature. Please note that once this cleanup
        routine begins, it must run until the program terminates."""
        while True:
            time.sleep(self.__sleep_interval)
            with self:
                for key in tuple(self):
                    if not super().__getitem__(key):
                        del self[key]

    def __setitem__(self, key, value):
        """Add manager attribute to value before storing it."""
        value.manager = self
        super().__setitem__(key, value)

    def __getitem__(self, key):
        """Retrieve the session specified by the given key.

        Like a normal dictionary, the value is returned to the caller
        if it was found. However, the wakeup method on the session is
        called first. This effectively delays the session's deletion."""
        session = super().__getitem__(key)
        session.wakeup()
        return session

    def __hash__(self):
        """Compute a hash as required by Thread objects."""
        return id(self)

################################################################################

class Session:

    """Store session variables for a limited time period.

    The only functionality this class directly supports is calling an event
    handler when the instance is destroyed. Session objects given to a
    SessionManager are automatically cleared out of memory when their "time to
    live" is exceeded. The manager must be started for such functionality."""

    def __init__(self, time_to_live, on_destroyed=None):
        """Initialize timeout setting and deletion handler."""
        self.__time_to_live = time_to_live
        self.__on_destroyed = on_destroyed
        self.wakeup()

    def wakeup(self):
        """Refresh the last-accessed time of this session object.

        This method is automatically called by the class initializer.
        Instances also get a wakeup call when retrieved from a manager."""
        self.__time = time.time()

    def __bool__(self):
        """Calculate liveliness of object for manager."""
        return time.time() - self.__time <= self.__time_to_live

    def __del__(self):
        """Call deletion event handler if present.

        Completely optional: an on_destroyed handler may be specified
        when the object is created. Exception handling is non-existent."""
        if self.__on_destroyed is not None:
            self.__on_destroyed()

+2  A: 

The problem is coming from threading.py, and can be reproduced more simply as follows:

>>> import threading
>>> class SessionManager(threading.Thread, threading._RLock, dict): pass
... 
>>> s = SessionManager()
>>> s.start()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/threading.py", line 469, in start
    _limbo[self] = self
TypeError: unhashable type: 'SessionManager'

You can study threading.py to see exactly why thread objects need to be hashable, but the fix is easy too: just override two more methods

def __eq__(self, other): return self is other
def __hash__(self): return hash(id(self))

this makes your class's instances hashable.

Alex Martelli
Thank you! I'm guessing the following documentation is wrong then: *User-defined classes have __eq__() and __hash__() methods by default; with them, all objects compare unequal (except with themselves) and x.__hash__() returns id(x).*
Noctis Skytower
No, the documentation is right; what it doesn't mention, though, is that the default `__hash__` only works if the default `__eq__` is also used. If a class overrides `__eq__`, the default `__hash__` will raise an error.
Thomas Wouters
A: 

Alex's on-point diagnosis of the problem notwithstanding, I would strongly argue that you should not multiply inherit from dict in this case (or in general, for that matter.) While it may seem convenient to subclass from it and automatically inherit all the dict behaviour, dicts (and builtin types in general) are often subject to shortcuts internally. For example, the 'get' method won't call your modified __getitem__, even when it does get the item:

>>> class MyDict(dict):
...     def __getitem__(self, key):
...         print("in __getitem__(%r)" % (key,))
...         return super(MyDict, self).__getitem__(key)
... 
>>> d = MyDict({'a': 'b', 'c': 'd'})
>>> d['a']
in __getitem__('a')
'b'
>>> d.get('c')
'd'
>>>

(And there are numerous such cases.)

Additionally, multiple inheritance involving builtin types requires that the in-memory layout of instances of all the types is compatible. It just so happens that threading.Thread and threading._Rlock are Python classes (which means they have a very simple in-memory layout that is compatible with dict) but if that were to change in the future, or you wanted to include other types, it would fail.

It is truly a bad idea.

Thomas Wouters