Как сделать неизменный объект в Python? - PullRequest
159 голосов
/ 28 января 2011

Хотя мне это никогда не было нужно, меня просто поразило, что создание неизменяемого объекта в Python может быть немного сложнее.Вы не можете просто переопределить __setattr__, потому что тогда вы даже не можете установить атрибуты в __init__.Подклассы кортежа - это хитрость, которая работает:

class Immutable(tuple):

    def __new__(cls, a, b):
        return tuple.__new__(cls, (a, b))

    @property
    def a(self):
        return self[0]

    @property
    def b(self):
        return self[1]

    def __str__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError

Но тогда у вас есть доступ к переменным a и b через self[0] и self[1], что раздражает.

Возможно ли это в Pure Python?Если нет, то как мне это сделать с расширением C?

(ответы, которые работают только в Python 3, являются приемлемыми).

Обновление:

Таким образом, кортеж подклассов - это способ сделать это в Pure Python, который работает хорошо, за исключением дополнительной возможности доступа к данным с помощью [0], [1] и т. Д. Итак, для завершения этого вопроса все, что не хватает, так это как это сделать«правильно» в C, что, я подозреваю, было бы довольно просто, просто не реализовав geititem или setattribute и т. д. Но вместо того, чтобы делать это самому, я предлагаю вознаграждение за это, потому что я ленивый.:)

Ответы [ 21 ]

2 голосов
/ 06 сентября 2018

Начиная с Python 3.7, вы можете использовать декоратор @dataclass в вашем классе, и он будет неизменным, как структура!Тем не менее, он может или не может добавить метод __hash__() в ваш класс.Цитата:

hash () используется встроенным hash () и при добавлении объектов в хешированные коллекции, такие как словари и наборы.Наличие хеша () подразумевает, что экземпляры класса являются неизменяемыми.Изменчивость - это сложное свойство, которое зависит от намерения программиста, существования и поведения eq (), а также значений флагов eq и frozen в декораторе dataclass ().

ByПо умолчанию dataclass () не будет неявно добавлять метод hash (), если это не безопасно.Он также не добавит и не изменит существующий явно определенный метод hash ().Установка атрибута класса hash = None имеет особое значение для Python, как описано в документации hash ().

Если hash () явно не определено, или если для него задано значение Нет, то dataclass () может добавить неявный метод hash ().Хотя это и не рекомендуется, вы можете заставить dataclass () создать метод hash () с unsafe_hash = True.Это может быть в том случае, если ваш класс логически неизменен, но, тем не менее, может быть видоизменен.Это специализированный вариант использования, и его следует внимательно изучить.

Вот пример из документов, связанных выше:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand
2 голосов
/ 04 июня 2015

Мне это понадобилось совсем недавно, и я решил сделать для него пакет Python.Начальная версия сейчас на PyPI:

$ pip install immutable

Для использования:

>>> from immutable import ImmutableFactory
>>> MyImmutable = ImmitableFactory.create(prop1=1, prop2=2, prop3=3)
>>> MyImmutable.prop1
1

Полные документы здесь: https://github.com/theengineear/immutable

Надеюсь, это поможет, это обернет именованный кортежкак уже обсуждалось, но значительно упрощает реализацию.

2 голосов
/ 07 августа 2013

Этот способ не останавливает object.__setattr__ от работы, но я все еще нашел его полезным:

class A(object):

    def __new__(cls, children, *args, **kwargs):
        self = super(A, cls).__new__(cls)
        self._frozen = False  # allow mutation from here to end of  __init__
        # other stuff you need to do in __new__ goes here
        return self

    def __init__(self, *args, **kwargs):
        super(A, self).__init__()
        self._frozen = True  # prevent future mutation

    def __setattr__(self, name, value):
        # need to special case setting _frozen.
        if name != '_frozen' and self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__setattr__(name, value)

    def __delattr__(self, name):
        if self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__delattr__(name)

вам может потребоваться переопределить больше вещей (например, __setitem__) в зависимости от использованияслучай.

1 голос
/ 25 октября 2018

Вы можете переопределить setattr и все еще использовать init для установки переменной. Вы бы использовали суперкласс setattr . вот код.

class Immutable:
    __slots__ = ('a','b')
    def __init__(self, a , b):
        super().__setattr__('a',a)
        super().__setattr__('b',b)

    def __str__(self):
        return "".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError
1 голос
/ 14 мая 2017

Сторонний attr модуль обеспечивает эту функциональность .

Редактировать: Python 3.7 принял эту идею в stdlib с @dataclass.

$ pip install attrs
$ python
>>> @attr.s(frozen=True)
... class C(object):
...     x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
   ...
attr.exceptions.FrozenInstanceError: can't set attribute

attr реализует замороженные классы путем переопределения __setattr__ и оказывает незначительное влияние на производительность при каждом создании, согласно документации.

Если вы привыкли использовать классы в качестве типов данных, attr может быть особенно полезен, поскольку он заботится о шаблоне для вас (но не делает никакой магии). В частности, он пишет для вас девять методов dunder (__X__) (если вы не отключите ни один из них), включая repr, init, hash и все функции сравнения.

attr также предоставляет помощника для __slots__.

1 голос
/ 27 февраля 2016

Классы, которые наследуются от следующего класса Immutable, являются неизменными, как и их экземпляры, после завершения выполнения метода __init__. Поскольку это чистый python, как уже отмечали другие, ничто не мешает кому-либо использовать специальные методы мутации из базовых object и type, но этого достаточно, чтобы никто не мог случайно мутировать класс / экземпляр.

Он работает, угоняя процесс создания класса с метаклассом.

"""Subclasses of class Immutable are immutable after their __init__ has run, in
the sense that all special methods with mutation semantics (in-place operators,
setattr, etc.) are forbidden.

"""  

# Enumerate the mutating special methods
mutation_methods = set()
# Arithmetic methods with in-place operations
iarithmetic = '''add sub mul div mod divmod pow neg pos abs bool invert lshift
                 rshift and xor or floordiv truediv matmul'''.split()
for op in iarithmetic:
    mutation_methods.add('__i%s__' % op)
# Operations on instance components (attributes, items, slices)
for verb in ['set', 'del']:
    for component in '''attr item slice'''.split():
        mutation_methods.add('__%s%s__' % (verb, component))
# Operations on properties
mutation_methods.update(['__set__', '__delete__'])


def checked_call(_self, name, method, *args, **kwargs):
    """Calls special method method(*args, **kw) on self if mutable."""
    self = args[0] if isinstance(_self, object) else _self
    if not getattr(self, '__mutable__', True):
        # self told us it's immutable, so raise an error
        cname= (self if isinstance(self, type) else self.__class__).__name__
        raise TypeError('%s is immutable, %s disallowed' % (cname, name))
    return method(*args, **kwargs)


def method_wrapper(_self, name):
    "Wrap a special method to check for mutability."
    method = getattr(_self, name)
    def wrapper(*args, **kwargs):
        return checked_call(_self, name, method, *args, **kwargs)
    wrapper.__name__ = name
    wrapper.__doc__ = method.__doc__
    return wrapper


def wrap_mutating_methods(_self):
    "Place the wrapper methods on mutative special methods of _self"
    for name in mutation_methods:
        if hasattr(_self, name):
            method = method_wrapper(_self, name)
            type.__setattr__(_self, name, method)


def set_mutability(self, ismutable):
    "Set __mutable__ by using the unprotected __setattr__"
    b = _MetaImmutable if isinstance(self, type) else Immutable
    super(b, self).__setattr__('__mutable__', ismutable)


class _MetaImmutable(type):

    '''The metaclass of Immutable. Wraps __init__ methods via __call__.'''

    def __init__(cls, *args, **kwargs):
        # Make class mutable for wrapping special methods
        set_mutability(cls, True)
        wrap_mutating_methods(cls)
        # Disable mutability
        set_mutability(cls, False)

    def __call__(cls, *args, **kwargs):
        '''Make an immutable instance of cls'''
        self = cls.__new__(cls)
        # Make the instance mutable for initialization
        set_mutability(self, True)
        # Execute cls's custom initialization on this instance
        self.__init__(*args, **kwargs)
        # Disable mutability
        set_mutability(self, False)
        return self

    # Given a class T(metaclass=_MetaImmutable), mutative special methods which
    # already exist on _MetaImmutable (a basic type) cannot be over-ridden
    # programmatically during _MetaImmutable's instantiation of T, because the
    # first place python looks for a method on an object is on the object's
    # __class__, and T.__class__ is _MetaImmutable. The two extant special
    # methods on a basic type are __setattr__ and __delattr__, so those have to
    # be explicitly overridden here.

    def __setattr__(cls, name, value):
        checked_call(cls, '__setattr__', type.__setattr__, cls, name, value)

    def __delattr__(cls, name, value):
        checked_call(cls, '__delattr__', type.__delattr__, cls, name, value)


class Immutable(object):

    """Inherit from this class to make an immutable object.

    __init__ methods of subclasses are executed by _MetaImmutable.__call__,
    which enables mutability for the duration.

    """

    __metaclass__ = _MetaImmutable


class T(int, Immutable):  # Checks it works with multiple inheritance, too.

    "Class for testing immutability semantics"

    def __init__(self, b):
        self.b = b

    @classmethod
    def class_mutation(cls):
        cls.a = 5

    def instance_mutation(self):
        self.c = 1

    def __iadd__(self, o):
        pass

    def not_so_special_mutation(self):
        self +=1

def immutabilityTest(f, name):
    "Call f, which should try to mutate class T or T instance."
    try:
        f()
    except TypeError, e:
        assert 'T is immutable, %s disallowed' % name in e.args
    else:
        raise RuntimeError('Immutability failed!')

immutabilityTest(T.class_mutation, '__setattr__')
immutabilityTest(T(6).instance_mutation, '__setattr__')
immutabilityTest(T(6).not_so_special_mutation, '__iadd__')
0 голосов
/ 16 сентября 2018

Ниже приведено базовое решение для следующего сценария:

  • __init__() может быть записан с доступом к атрибутам, как обычно.
  • ПОСЛЕ того, что ОБЪЕКТ заморожен для атрибутов только изменения:

Идея состоит в том, чтобы переопределить метод __setattr__ и заменять его реализацию каждый раз, когда изменяется статус замороженного объекта.

Итак, нам нужен какой-то метод (_freeze), который сохраняет эти две реализации и переключается между ними по запросу.

Этот механизм может быть реализован внутри пользовательского класса или унаследован от специального Freezer класса, как показано ниже:

class Freezer:
    def _freeze(self, do_freeze=True):
        def raise_sa(*args):            
            raise AttributeError("Attributes are frozen and can not be changed!")
        super().__setattr__('_active_setattr', (super().__setattr__, raise_sa)[do_freeze])

    def __setattr__(self, key, value):        
        return self._active_setattr(key, value)

class A(Freezer):    
    def __init__(self):
        self._freeze(False)
        self.x = 10
        self._freeze()
0 голосов
/ 23 марта 2017

Вы можете просто переопределить setAttr в последнем утверждении init.Тогда вы можете построить, но не изменить.Очевидно, что вы все равно можете переопределить объект usint. setAttr , но на практике большинство языков имеют некоторую форму отражения, поэтому неизменность всегда является дырявой абстракцией.Неизменность - это скорее предотвращение случайного нарушения клиентами договора на объект.Я использую:

====================================

Исходное предлагаемое решение было неверным, это было обновлено на основе комментариев, использующих решение из здесь

Оригинальное решение неверно в интересной форме, поэтому оно включено внизу.

===============================

class ImmutablePair(object):

    __initialised = False # a class level variable that should always stay false.
    def __init__(self, a, b):
        try :
            self.a = a
            self.b = b
        finally:
            self.__initialised = True #an instance level variable

    def __setattr__(self, key, value):
        if self.__initialised:
            self._raise_error()
        else :
            super(ImmutablePair, self).__setattr__(key, value)

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")

if __name__ == "__main__":

    immutable_object = ImmutablePair(1,2)

    print immutable_object.a
    print immutable_object.b

    try :
        immutable_object.a = 3
    except Exception as e:
        print e

    print immutable_object.a
    print immutable_object.b

Вывод:

1
2
Attempted To Modify Immutable Object
1
2

============================================

Исходная реализация:

В комментариях было правильно указано, что это на самом деле не работает, поскольку предотвращает создание более одного объекта, так как вы переопределяете метод класса setattr, что означает, что секунда не может быть создана как self.a = потерпит неудачу при второй инициализации.

class ImmutablePair(object):

    def __init__(self, a, b):
        self.a = a
        self.b = b
        ImmutablePair.__setattr__ = self._raise_error

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")
0 голосов
/ 12 мая 2016

Одна вещь, которая здесь на самом деле не включена, это полная неизменность ... не только родительский объект, но и все дочерние объекты. например, кортежи / фрозенсеты могут быть неизменными, но объекты, частью которых он является, могут не быть. Вот небольшая (неполная) версия, которая делает достойную работу по обеспечению неизменности на всем пути:

# Initialize lists
a = [1,2,3]
b = [4,5,6]
c = [7,8,9]

l = [a,b]

# We can reassign in a list 
l[0] = c

# But not a tuple
t = (a,b)
#t[0] = c -> Throws exception
# But elements can be modified
t[0][1] = 4
t
([1, 4, 3], [4, 5, 6])
# Fix it back
t[0][1] = 2

li = ImmutableObject(l)
li
[[1, 2, 3], [4, 5, 6]]
# Can't assign
#li[0] = c will fail
# Can reference
li[0]
[1, 2, 3]
# But immutability conferred on returned object too
#li[0][1] = 4 will throw an exception

# Full solution should wrap all the comparison e.g. decorators.
# Also, you'd usually want to add a hash function, i didn't put
# an interface for that.

class ImmutableObject(object):
    def __init__(self, inobj):
        self._inited = False
        self._inobj = inobj
        self._inited = True

    def __repr__(self):
        return self._inobj.__repr__()

    def __str__(self):
        return self._inobj.__str__()

    def __getitem__(self, key):
        return ImmutableObject(self._inobj.__getitem__(key))

    def __iter__(self):
        return self._inobj.__iter__()

    def __setitem__(self, key, value):
        raise AttributeError, 'Object is read-only'

    def __getattr__(self, key):
        x = getattr(self._inobj, key)
        if callable(x):
              return x
        else:
              return ImmutableObject(x)

    def __hash__(self):
        return self._inobj.__hash__()

    def __eq__(self, second):
        return self._inobj.__eq__(second)

    def __setattr__(self, attr, value):
        if attr not in  ['_inobj', '_inited'] and self._inited == True:
            raise AttributeError, 'Object is read-only'
        object.__setattr__(self, attr, value)
0 голосов
/ 08 марта 2016

Я использовал ту же идею, что и Алекс: мета-класс и «маркер инициализации», но в сочетании с перезаписью __setattr __:

>>> from abc import ABCMeta
>>> _INIT_MARKER = '_@_in_init_@_'
>>> class _ImmutableMeta(ABCMeta):
... 
...     """Meta class to construct Immutable."""
... 
...     def __call__(cls, *args, **kwds):
...         obj = cls.__new__(cls, *args, **kwds)
...         object.__setattr__(obj, _INIT_MARKER, True)
...         cls.__init__(obj, *args, **kwds)
...         object.__delattr__(obj, _INIT_MARKER)
...         return obj
...
>>> def _setattr(self, name, value):
...     if hasattr(self, _INIT_MARKER):
...         object.__setattr__(self, name, value)
...     else:
...         raise AttributeError("Instance of '%s' is immutable."
...                              % self.__class__.__name__)
...
>>> def _delattr(self, name):
...     raise AttributeError("Instance of '%s' is immutable."
...                          % self.__class__.__name__)
...
>>> _im_dict = {
...     '__doc__': "Mix-in class for immutable objects.",
...     '__copy__': lambda self: self,   # self is immutable, so just return it
...     '__setattr__': _setattr,
...     '__delattr__': _delattr}
...
>>> Immutable = _ImmutableMeta('Immutable', (), _im_dict)

Примечание: я вызываю мета-класснапрямую, чтобы заставить его работать как для Python 2.x, так и для 3.x.

>>> class T1(Immutable):
... 
...     def __init__(self, x=1, y=2):
...         self.x = x
...         self.y = y
...
>>> t1 = T1(y=8)
>>> t1.x, t1.y
(1, 8)
>>> t1.x = 7
AttributeError: Instance of 'T1' is immutable.

Он также работает со слотами ...:

>>> class T2(Immutable):
... 
...     __slots__ = 's1', 's2'
... 
...     def __init__(self, s1, s2):
...         self.s1 = s1
...         self.s2 = s2
...
>>> t2 = T2('abc', 'xyz')
>>> t2.s1, t2.s2
('abc', 'xyz')
>>> t2.s1 += 'd'
AttributeError: Instance of 'T2' is immutable.

... и множественным наследованием:

>>> class T3(T1, T2):
... 
...     def __init__(self, x, y, s1, s2):
...         T1.__init__(self, x, y)
...         T2.__init__(self, s1, s2)
...
>>> t3 = T3(12, 4, 'a', 'b')
>>> t3.x, t3.y, t3.s1, t3.s2
(12, 4, 'a', 'b')
>>> t3.y -= 3
AttributeError: Instance of 'T3' is immutable.

Обратите внимание, что изменяемые атрибуты остаются изменяемыми:

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...