There are several standard ways to make a class hashable, for example (borrowing from SO):

# assume X has 2 attributes: attr_a and attr_b
class X:
  def __key(self):
    return (self.attr_a, self.attr_b)

  def __eq__(x, y):
    return isinstance(y, x.__class__) and x.__key() == y.__key()

  def __hash__(self):
    return hash(self.__key())

Now suppose I have many classes that I want to make hashable. They are all immutable, with immutable attributes, and hashing all these attributes in bulk is acceptable (for a class with too many attributes, we would only want to hash a few attributes that are enough to avoid most collisions). Can I avoid writing __key() method by hand for every class?

Would it be a good idea to make a base class that defines __key(), __eq__, and __hash__ for them? In particular, I'm not sure whether finding all the instance attributes that should go into __hash__ is doable. I know this is generally impossible, but in this case we can assume more about the object (e.g., it's immutable - after __init__ is finished, its attributes are all hashable, etc.).

(If the inheritance hierarchy won't work, perhaps a decorator would?)

有帮助吗?

解决方案

Instances store their attributes in self.__dict__:

>>> class Foo(object):
...     def __init__(self, foo='bar', spam='eggs'):
...         self.foo = foo
...         self.spam = spam
... 
>>> f = Foo()
>>> f.__dict__
{'foo': 'bar', 'spam': 'eggs'}

Provided you don't store any methods on your instances, a default .__key() could be:

def __key(self):
    return tuple(v for k, v in sorted(self.__dict__.items()))

where we sort the items by attribute name; the tuple() call ensures we return an immutable sequence suitable for the hash() call.

For more complex setups you'd have to either test for the types returned by values() (skip functions and such) or use a specific pattern of attributes or repurpose __slots__ to list the appropriate attributes you can use.

Together with your __hash__ and __eq__ methods, that'd make a good base class to inherit from for all your immutable classes.

其他提示

You can do it if you assume conventions for your attributes. In your example, it would be quite trivial, as your attributes starts with "attr_" - so you coult write your __key method as :

def __key(self):
    return tuple (getattr(self, attr) for attr in self.__dict__ if attr.startswith("attr_") )

As you can see - any test you can find to put on the filter condition of the generator expression would fit your needs.

A suggestion I can give you is to have your classes using Python's __slots__ feature: that not only will make your attribute names easy to find, as will make your imutable objects more efficient to use and with smaller memory footprint.

class X:
    __slots__ = ("a", "b", "c")
    def __key(self):
        return tuple (getattr(self, attr) for attr in self.__class__.__slots__ )

edit Answering the first comment from the O.P.:

This works with inheritance, of course. If you will always use all of the object's attributes for they, you don't need the "if" part of the expression - write the function as _key (instead of __key which makes a unique name for each class internally) on a class on the top of your hierarchy, and it will work for all your classes.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top