immutable objects in Python that can have weak references

Viewed 394

I've been subclassing tuple or using namedtuple blissfully for a few years, but now I have a use case where I need a class that can be used as a weak referent. And today I learned tuples don't support weak references.

Is there another way to create an immutable object in Python with a fixed set of attributes? I don't need the numeric indexing or variable width of a tuple.

class SimpleThingWithMethods(object):
    def __init__(self, n, x):
        # I just need to store n and x as read-only attributes 
    ... ??? ...

I guess this raises the obvious question of why immutable; "Pythonic" code usually just assumes we're all adults here and no one in their right mind would reach into a class and muck with its values if it risks ruining the class invariants. In my case I have a class in a library and I am worried about accidental modification of objects by end-users. The people I work with sometimes make incorrect assumptions about my code and start doing things I did not expect, so it's much cleaner if I can raise an error if they accidentally modify my code.

I'm not so worried about bulletproof immutability; if someone really nefarious wants to go and modify things, ok, fine, they're on their own. I just want to make it hard to accidentally modify my objects.

3 Answers

well, this isn't a great answer but it looks like I can modify the answer in https://stackoverflow.com/a/4828492/44330 --- essentially overriding __setattr__ and __delattr__ to meet my needs at least against accidental modification. (but not as nice as subclassing tuple)

class Point(object):
    __slots__ = ('x','y','__weakref__')
    def __init__(self, x, y):
        object.__setattr__(self, "x", x)
        object.__setattr__(self, "y", y)
    def __setattr__(self, *args):
        raise TypeError
    def __delattr__(self, *args):
        raise TypeError
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    def __hash__(self):
        return self.x.__hash__() * 31 + self.y.__hash__()

Implementing @Elazar's idea:

class Point(object):
    __slots__ = ('x','y','__weakref__')
    def __new__(cls, x, y):
        thing = object.__new__(cls) 
        object.__setattr__(thing, "x", x)
        object.__setattr__(thing, "y", y)
        return thing
    def __setattr__(self, *args):
        raise TypeError
    def __delattr__(self, *args):
        raise TypeError
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    def __hash__(self):
        return self.x.__hash__() * 31 + self.y.__hash__()    

If you don't worry about isinstance checks, you can strengthen you answer:

def Point(x, y):
    class Point(object):
        __slots__ = ('x','y','__weakref__')
        def __setattr__(self, *args):
            raise TypeError
        def __delattr__(self, *args):
            raise TypeError
        def __eq__(self, other):
            return x == other.x and y == other.y
        def __hash__(self):
            return x.__hash__() * 31 + y.__hash__()
    p = Point()
    object.__setattr__(p, "x", x)
    object.__setattr__(p, "y", y)
    return p

I don't really recommend it (every invocation creates a class!), just wanted to note the possibility.

It is also possible to go javascript all the way, and supply __getattr__ that will access the local variables. But that will also slow down access, in addition to creation. Now we don't need these slots at all:

class MetaImmutable:
    def __setattr__(self, name, val):
        raise TypeError

def Point(x, y):
    class Point(object):
        __metaclass__ = MetaImmutable
        __slots__ = ('__weakref__',)
        def __getattr__(self, name):
            if name == 'x': return x
            if name == 'y': return y
            raise TypeError
        @property
        def x(self): return x
        @property
        def y(self): return y
        def __eq__(self, other):
            return x == other.x and y == other.y
        def __hash__(self):
            return x.__hash__() * 31 + y.__hash__()
    return Point()

Test it:

>>> p = Point(1, 2)
>>> p.y
2
>>> p.z
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __getattr__
TypeError
>>> p.z = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'
>>> object.__setattr__(p, 'z', 5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'
>>> from weakref import ref
>>> ref(p)().x
1
>>> type(p).x = property(lambda self: 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __setattr__
TypeError

And finally, you can still break it:

>>> type.__setattr__(type(p), 'x', property(lambda self: 5))
>>> p.x
5

Again, nothing here is recommended. Use @Jasons implementation.

What about using encapsulation and abstraction on the parameter (getter?):

class SimpleThingWithMethods(object):
    def __init__(self, n, x):
        self._n = n
        self._x = x

    def x(self):
      return self._x

    def n(self):
      return self._n

    SimpleThingWithMethods(2,3).x()

=> 3

Related