Having searched for a way to enforce immutability of custom types and not having found a satisfactory answer I came up with my own shot at a solution in form of a metaclass:
class ImmutableTypeException( Exception ): pass
class Immutable( type ):
'''
Enforce some aspects of the immutability contract for new-style classes:
- attributes must not be created, modified or deleted after object construction
- immutable types must implement __eq__ and __hash__
'''
def __new__( meta, classname, bases, classDict ):
instance = type.__new__开发者_JAVA技巧( meta, classname, bases, classDict )
# Make sure __eq__ and __hash__ have been implemented by the immutable type.
# In the case of __hash__ also make sure the object default implementation has been overridden.
# TODO: the check for eq and hash functions could probably be done more directly and thus more efficiently
# (hasattr does not seem to traverse the type hierarchy)
if not '__eq__' in dir( instance ):
raise ImmutableTypeException( 'Immutable types must implement __eq__.' )
if not '__hash__' in dir( instance ):
raise ImmutableTypeException( 'Immutable types must implement __hash__.' )
if _methodFromObjectType( instance.__hash__ ):
raise ImmutableTypeException( 'Immutable types must override object.__hash__.' )
instance.__setattr__ = _setattr
instance.__delattr__ = _delattr
return instance
def __call__( self, *args, **kwargs ):
obj = type.__call__( self, *args, **kwargs )
obj.__immutable__ = True
return obj
def _setattr( self, attr, value ):
if '__immutable__' in self.__dict__ and self.__immutable__:
raise AttributeError( "'%s' must not be modified because '%s' is immutable" % ( attr, self ) )
object.__setattr__( self, attr, value )
def _delattr( self, attr ):
raise AttributeError( "'%s' must not be deleted because '%s' is immutable" % ( attr, self ) )
def _methodFromObjectType( method ):
'''
Return True if the given method has been defined by object, False otherwise.
'''
try:
# TODO: Are we exploiting an implementation detail here? Find better solution!
return isinstance( method.__objclass__, object )
except:
return False
However, while the general approach seems to be working rather well there are still some iffy implementation details (also see TODO comments in code):
- How do I check if a particular method has been implemented anywhere in the type hierarchy?
- How do I check which type is the origin of a method declaration (i.e. as part of which type a method has been defined)?
Special methods are always looked up on the type, not the instance. So hasattr
must also be applied to the type. E.g.:
>>> class A(object): pass
...
>>> class B(A): __eq__ = lambda *_: 1
...
>>> class C(B): pass
...
>>> c = C()
>>> hasattr(type(c), '__eq__')
True
Checking hasattr(c, '__eq__')
would be misleading as it might erroneously "catch" a per-instance attribute __eq__
defined in c
itself, which would not act as a special method (note that in the specific case of __eq__
you'll always see a True
result from hasattr
, because ancestor class object
defines it, and inheritance can only ever "add" attributes, never "subtract" any;-).
To check which ancestor class first defined an attribute (and thus which exact definition will be used when the lookup is only on the type):
import inspect
def whichancestor(c, attname):
for ancestor in inspect.getmro(type(c)):
if attname in ancestor.__dict__:
return ancestor
return None
It's best to use inspect
for such tasks, as it will work more broadly than a direct access of the __mro__
attribute on type(c)
.
This metaclass enforces "shallow" immutability. For example, it doesn't prevent
immutable_obj.attr.attrs_attr = new_value
immutable_obj.attr[2] = new_value
Depending if attrs_attr is owned by the object or not, that might be considered to violate true immutability. E.g. it might result in the following which should not happen for an immutable type:
>>> a = ImmutableClass(value)
>>> b = ImmutableClass(value)
>>> c = a
>>> a == b
True
>>> b == c
True
>>> a.attr.attrs_attr = new_value
>>> b == c
False
Possibly you could fix that deficiency by overriding getattr as well to return some kind of immutable wrapper for any attribute it returns. It might be complicated. Blocking direct setattr calls could be done, but what about methods of the attribute that set its attributes in their code? I can think of ideas but it will get pretty meta, alright.
Also, I thought this would be a clever use of your class:
class Tuple(list):
__metaclass__ = Immutable
But it didn't make a tuple as I had hoped.
>>> t = Tuple([1,2,3])
>>> t.append(4)
>>> t
[1, 2, 3, 4]
>>> u = t
>>> t += (5,)
>>> t
[1, 2, 3, 4, 5]
>>> u
[1, 2, 3, 4, 5]
I guess list's methods are mostly or completely implemented at the C level, so I suppose your metaclass has no opportunity to intercept state changes in them.
精彩评论