How to make an immutable object in Python?
Yet another solution I just thought of: The simplest way to get the same behaviour as your original code is
Immutable = collections.namedtuple("Immutable", ["a", "b"])
It does not solve the problem that attributes can be accessed via [0]
etc., but at least it's considerably shorter and provides the additional advantage of being compatible with pickle
and copy
.
namedtuple
creates a type similar to what I described in this answer, i.e. derived from tuple
and using __slots__
. It is available in Python 2.6 or above.
The easiest way to do this is using __slots__
:
class A(object): __slots__ = []
Instances of A
are immutable now, since you can't set any attributes on them.
If you want the class instances to contain data, you can combine this with deriving from tuple
:
from operator import itemgetterclass Point(tuple): __slots__ = [] def __new__(cls, x, y): return tuple.__new__(cls, (x, y)) x = property(itemgetter(0)) y = property(itemgetter(1))p = Point(2, 3)p.x# 2p.y# 3
Edit: If you want to get rid of indexing either, you can override __getitem__()
:
class Point(tuple): __slots__ = [] def __new__(cls, x, y): return tuple.__new__(cls, (x, y)) @property def x(self): return tuple.__getitem__(self, 0) @property def y(self): return tuple.__getitem__(self, 1) def __getitem__(self, item): raise TypeError
Note that you can't use operator.itemgetter
for the properties in thise case, since this would rely on Point.__getitem__()
instead of tuple.__getitem__()
. Fuerthermore this won't prevent the use of tuple.__getitem__(p, 0)
, but I can hardly imagine how this should constitute a problem.
I don't think the "right" way of creating an immutable object is writing a C extension. Python usually relies on library implementers and library users being consenting adults, and instead of really enforcing an interface, the interface should be clearly stated in the documentation. This is why I don't consider the possibility of circumventing an overridden __setattr__()
by calling object.__setattr__()
a problem. If someone does this, it's on her own risk.
..howto do it "properly" in C..
You could use Cython to create an extension type for Python:
cdef class Immutable: cdef readonly object a, b cdef object __weakref__ # enable weak referencing support def __init__(self, a, b): self.a, self.b = a, b
It works both Python 2.x and 3.
Tests
# compile on-the-flyimport pyximport; pyximport.install() # $ pip install cythonfrom immutable import Immutableo = Immutable(1, 2)assert o.a == 1, str(o.a)assert o.b == 2try: o.a = 3except AttributeError: passelse: assert 0, 'attribute must be readonly'try: o[1]except TypeError: passelse: assert 0, 'indexing must not be supported'try: o.c = 1except AttributeError: passelse: assert 0, 'no new attributes are allowed'o = Immutable('a', [])assert o.a == 'a'assert o.b == []o.b.append(3) # attribute may contain mutable objectassert o.b == [3]try: o.cexcept AttributeError: passelse: assert 0, 'no c attribute'o = Immutable(b=3,a=1)assert o.a == 1 and o.b == 3try: del o.bexcept AttributeError: passelse: assert 0, "can't delete attribute"d = dict(b=3, a=1)o = Immutable(**d)assert o.a == d['a'] and o.b == d['b']o = Immutable(1,b=3)assert o.a == 1 and o.b == 3try: object.__setattr__(o, 'a', 1)except AttributeError: passelse: assert 0, 'attributes are readonly'try: object.__setattr__(o, 'c', 1)except AttributeError: passelse: assert 0, 'no new attributes'try: Immutable(1,c=3)except TypeError: passelse: assert 0, 'accept only a,b keywords'for kwd in [dict(a=1), dict(b=2)]: try: Immutable(**kwd) except TypeError: pass else: assert 0, 'Immutable requires exactly 2 arguments'
If you don't mind indexing support then collections.namedtuple
suggested by @Sven Marnach is preferrable:
Immutable = collections.namedtuple("Immutable", "a b")