开发者

Python metaclass for enforcing immutability of custom types

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):

  1. How do I check if a particular method has been implemented anywhere in the type hierarchy?
  2. 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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜