虽然我从来都不需要这样做,但我突然意识到用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

但是你可以通过self[0]和self[1]访问a和b变量,这很烦人。

这在Pure Python中可行吗?如果不是,我该如何用C扩展来做呢?

(只能在python3中工作的答案是可以接受的)。

更新:

从Python 3.7开始,要使用的方法是使用@dataclass装饰器,参见最新接受的答案。


当前回答

使用冻结的数据类

对于Python 3.7+,你可以使用带frozen=True选项的数据类,这是一种非常Python化和可维护的方式来做你想做的事情。

它看起来是这样的:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
    a: Any
    b: Any

由于数据类的字段需要类型提示,所以我使用了typing模块中的Any。

不使用命名元组的原因

在Python 3.7之前,经常可以看到命名元组被用作不可变对象。它在很多方面都很棘手,其中之一是命名元组之间的__eq__方法不考虑对象的类。例如:

from collections import namedtuple

ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])

obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)

obj1 == obj2  # will be True

如你所见,即使obj1和obj2的类型不同,即使它们的字段名称不同,obj1 == obj2仍然给出True。这是因为使用的__eq__方法是元组的方法,它只比较给定位置的字段的值。这可能是一个巨大的错误来源,特别是如果您是子类化这些类。

其他回答

这种方式不停止对象。__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__)。

我使用了与Alex相同的想法:一个元类和一个“init marker”,但结合重写__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]

使用冻结的数据类

对于Python 3.7+,你可以使用带frozen=True选项的数据类,这是一种非常Python化和可维护的方式来做你想做的事情。

它看起来是这样的:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
    a: Any
    b: Any

由于数据类的字段需要类型提示,所以我使用了typing模块中的Any。

不使用命名元组的原因

在Python 3.7之前,经常可以看到命名元组被用作不可变对象。它在很多方面都很棘手,其中之一是命名元组之间的__eq__方法不考虑对象的类。例如:

from collections import namedtuple

ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])

obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)

obj1 == obj2  # will be True

如你所见,即使obj1和obj2的类型不同,即使它们的字段名称不同,obj1 == obj2仍然给出True。这是因为使用的__eq__方法是元组的方法,它只比较给定位置的字段的值。这可能是一个巨大的错误来源,特别是如果您是子类化这些类。

你可以在init的最后一条语句中重写setAttr。那么你可以构建,但不能改变。显然,你仍然可以用usint对象重写。但在实践中,大多数语言都有某种形式的反射,因此不可变始终是一个有漏洞的抽象。不可变性更多的是防止客户端意外地违反对象的契约。我使用:

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

最初提供的解决方案是不正确的,这是基于使用这里的解决方案的评论而更新的

原来的解决方案是错误的,这是一种有趣的方式,所以它被包括在底部。

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

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

另一种方法是创建一个使实例不可变的包装器。

class Immutable(object):

    def __init__(self, wrapped):
        super(Immutable, self).__init__()
        object.__setattr__(self, '_wrapped', wrapped)

    def __getattribute__(self, item):
        return object.__getattribute__(self, '_wrapped').__getattribute__(item)

    def __setattr__(self, key, value):
        raise ImmutableError('Object {0} is immutable.'.format(self._wrapped))

    __delattr__ = __setattr__

    def __iter__(self):
        return object.__getattribute__(self, '_wrapped').__iter__()

    def next(self):
        return object.__getattribute__(self, '_wrapped').next()

    def __getitem__(self, item):
        return object.__getattribute__(self, '_wrapped').__getitem__(item)

immutable_instance = Immutable(my_instance)

这在只有一些实例必须是不可变的情况下很有用(比如函数调用的默认参数)。

也可以用于不可变工厂,如:

@classmethod
def immutable_factory(cls, *args, **kwargs):
    return Immutable(cls.__init__(*args, **kwargs))

也保护对象。__setattr__,但由于Python的动态特性,可能会被其他技巧所绊倒。