Welcome, guest | Sign In | My Account | Store | Cart

Allows for type hierarchies of immutable types by faking inheritance using dict updates and abc's.

Python, 72 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import collections as co
import abc

class ImmutableMeta(abc.ABCMeta):

    _classes = {}

    def __new__(meta, clsname, bases, clsdict):
        attributes = clsdict.pop('_attributes_')

        if bases[0] is object:
            # We're creating a class 'from scratch'
            methods = clsdict
        else:
            # we're 'inheriting' from an existing class.
            # Pull the base class's attributes and methods from the registry and
            # combine them with the new class's attributes and methods.
            base = bases[0]
            attributes = meta._classes[base]['attributes'] + ' ' + attributes
            base_methods = meta._classes[base]['methods'].copy()
            base_methods.update(clsdict)
            methods = base_methods

        # construct the actual base class and create the return class
        new_base = co.namedtuple(clsname + 'Base', attributes)
        cls = super(ImmutableMeta, meta).__new__(meta, clsname, (new_base,),
                                                 methods)

        # register the data necessary to 'inherit' from the class
        # and make sure that it passes typechecking
        meta._classes[cls] = {'attributes': attributes,
                              'methods': methods}
        if bases[0] is not object:
            base.register(cls)
        return cls

# Demo

class Foo(object):
    __metaclass__ = ImmutableMeta
    _attributes_ = 'a b'

    def sayhi(self):
        print "Hello from {0}".format(type(self).__name__)

class Bar(Foo):
    _attributes_ = 'c'

    def saybye(self):
        print "Goodbye from {0}".format(type(self).__name__)

a = Foo(1, 2)
a.sayhi()

b = Bar(1, 2, 3)
b.sayhi()  # 'inherited' from class Foo
b.saybye()

try:
    b.c = 1         # will raise an AttributeError
except AttributeError:
    print "Immutable"

print "issubclass(Bar, Foo): {0}".format(issubclass(Bar, Foo))
# We get this for free with abc

try:
   d =  {b: 1}        # No problems
except TypeError:
    print "Cant put it in a dict"
else:
    print "Can put it in a dict"

This came up on a Stackoverflow question and this was my solution. Nobody seems to care for it there but I think it's fairly slick so thought I'd share it here.

One alternative way to do the 'inheritance' would be to equip each class with a __getattr__ method that did the lookup in ImmutableMeta._classes. It could be defined in the __new__ method so that it had access to base to perform the lookup through closure. It doesn't currently support multiple inheritance but that should be fairly easy to add on.

One thing I've noticed is that I can define mutable attributes as normal class attributes without affecting either the hashability of the class as a whole or the immutable nature of the attributes that are defined the 'right' way. It might be desirable to remove such elements from clsdict in the constructor. A simple call to callable across the values should suffice.