虽然我从来都不需要这样做,但我突然意识到用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包。最初的版本现在在PyPI上:

$ pip install immutable

使用方法:

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

完整的文档在这里:https://github.com/theengineear/immutable

希望它有帮助,它包装了一个namedtuple,但使实例化更简单。

其他回答

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

如果您对具有行为的对象感兴趣,那么namedtuple几乎是您的解决方案。

正如namedtuple文档底部所描述的,您可以从namedtuple派生自己的类;然后,你可以添加你想要的行为。

例如(代码直接取自文档):

class Point(namedtuple('Point', 'x y')):
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

for p in Point(3, 4), Point(14, 5/7):
    print(p)

这将导致:

Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

这种方法适用于Python 3和Python 2.7(在IronPython上也进行了测试)。 唯一的缺点是继承树有点奇怪;但这不是你经常玩的东西。

你可以创建一个@immutable装饰器,它覆盖__setattr__并将__slots__更改为一个空列表,然后用它装饰__init__方法。

编辑:正如OP所指出的,改变__slots__属性只会阻止新属性的创建,而不会阻止修改。

Edit2:下面是一个实现:

Edit3:使用__slots__会破坏这段代码,因为if会停止对象__dict__的创建。我正在寻找替代方案。

Edit4:嗯,就是这样。这是一个很粗鄙的问题,但可以作为练习:-)

class immutable(object):
    def __init__(self, immutable_params):
        self.immutable_params = immutable_params

    def __call__(self, new):
        params = self.immutable_params

        def __set_if_unset__(self, name, value):
            if name in self.__dict__:
                raise Exception("Attribute %s has already been set" % name)

            if not name in params:
                raise Exception("Cannot create atribute %s" % name)

            self.__dict__[name] = value;

        def __new__(cls, *args, **kws):
            cls.__setattr__ = __set_if_unset__

            return super(cls.__class__, cls).__new__(cls, *args, **kws)

        return __new__

class Point(object):
    @immutable(['x', 'y'])
    def __new__(): pass

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2) 
p.x = 3 # Exception: Attribute x has already been set
p.z = 4 # Exception: Cannot create atribute z

下面的基本解决方案针对以下场景:

__init__()可以像往常一样访问属性。 在此之后,对象仅冻结属性更改:

其思想是覆盖__setattr__方法,并在每次对象冻结状态改变时替换其实现。

因此,我们需要一些方法(_freeze)来存储这两个实现,并在请求时在它们之间切换。

这个机制可以在用户类内部实现,也可以从一个特殊的freeze类继承,如下所示:

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

我使用了与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]