Python метакласс для обеспечения неизменности пользовательских типов - PullRequest
3 голосов
/ 28 марта 2010

Ища способ обеспечить неизменность пользовательских типов и не найдя удовлетворительного ответа, я придумал собственный способ решения в форме метакласса:

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 ]

4 голосов
/ 28 марта 2010

Специальные методы всегда ищутся для типа , а не для экземпляра .Поэтому 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__') будет вводить в заблуждение, поскольку она может ошибочно «перехватить» атрибут для каждого экземпляра __eq__, определенный в самом c, который не будет действовать как специальный метод (примечаниечто в конкретном случае __eq__ вы всегда увидите результат True из hasattr, потому что класс предка object определяет его, и наследование может только когда-либо «добавлять» атрибуты, но никогда не «вычитать» какие-либо; -).

Чтобы проверить , какой класс предка сначала определил атрибут (и, следовательно, какое точное определение будет использоваться, когда поиск выполняется только для типа):

import inspect

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

Лучше использовать inspect для таких задач, так как он будет работать более широко, чем прямой доступ к атрибуту __mro__ в type(c).

0 голосов
/ 15 июня 2013

Этот метакласс обеспечивает "мелкую" неизменность. Например, это не мешает

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, поэтому я полагаю, что ваш метакласс не имеет возможности перехватывать изменения состояния в них.

...