用于强制自定义类型不可变性的Python类元。

3 投票
2 回答
1502 浏览
提问于 2025-04-15 20:57

我在寻找一种方法来确保自定义类型是不可变的,但没有找到满意的答案,于是我自己想出了一个解决方案,使用了元类的方式:

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__( 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

不过,虽然这个方法总体上看起来效果不错,但在实现细节上还有一些不太确定的地方(代码中也有TODO的注释):

  1. 我该如何检查某个特定的方法是否在类型层次结构中的某个地方被实现过?
  2. 我该如何检查某个方法声明的来源类型(也就是说,这个方法是在哪个类型中定义的)?

2 个回答

0

这个元类实现了“浅层”的不可变性。举个例子,它并不会阻止

immutable_obj.attr.attrs_attr = new_value
immutable_obj.attr[2] = new_value

根据 attrs_attr 是否属于这个对象,这可能会被认为是违反真正的不可变性。比如,它可能导致以下情况,这在不可变类型中是不应该发生的:

>>> a = ImmutableClass(value)
>>> b = ImmutableClass(value)
>>> c = a
>>> a == b
True
>>> b == c
True
>>> a.attr.attrs_attr = new_value
>>> b == c
False

你可能可以通过重写 getattr 来解决这个问题,让它返回某种不可变的包装器,来处理它返回的任何属性。不过这可能会比较复杂。直接阻止 setattr 的调用是可以做到的,但如果属性的方法在它们的代码中设置了属性呢?我能想到一些想法,但这会变得相当复杂。

另外,我觉得这会是你类的一个聪明用法:

class Tuple(list):
    __metaclass__ = Immutable

但它并没有像我希望的那样生成一个元组。

>>> 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]

我想列表的方法大部分或完全是在 C 语言层面实现的,所以我想你的元类没有机会去拦截它们的状态变化。

5

特殊方法总是根据类型来查找,而不是根据实例。所以,使用hasattr时也必须针对类型来检查。例如:

>>> class A(object): pass
... 
>>> class B(A): __eq__ = lambda *_: 1
... 
>>> class C(B): pass
... 
>>> c = C()
>>> hasattr(type(c), '__eq__')
True

检查hasattr(c, '__eq__')可能会让人误解,因为它可能错误地“捕捉”到在c实例中定义的一个实例属性__eq__,而这个属性并不会作为特殊方法来使用(需要注意的是,在__eq__的特定情况下,你总是会从hasattr得到True的结果,因为它的祖先类object定义了这个方法,而继承只会“添加”属性,而不会“删除”任何属性;-)。

如果想要检查哪个祖先类最早定义了某个属性(也就是说,当只在类型上查找时,究竟会使用哪个具体的定义):

import inspect

def whichancestor(c, attname):
  for ancestor in inspect.getmro(type(c)):
    if attname in ancestor.__dict__:
      return ancestor
  return None

最好使用inspect模块来完成这样的任务,因为它的适用范围比直接访问type(c)上的__mro__属性更广泛。

撰写回答