虽然我从来都不需要这样做,但我突然意识到用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装饰器,参见最新接受的答案。


当前回答

我刚刚想到的另一个解决方案是:获得与原始代码相同行为的最简单方法是

Immutable = collections.namedtuple("Immutable", ["a", "b"])

它并没有解决属性可以通过[0]等访问的问题,但至少它相当简短,并提供了与pickle和copy兼容的额外优势。

namedtuple创建了一个类似于我在这个答案中描述的类型,即从tuple派生并使用__slots__。它在Python 2.6或更高版本中可用。

其他回答

您可以覆盖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

我不认为这是完全可能的,除非使用一个元组或namedtuple。无论如何,如果你重写了__setattr__(),用户总是可以通过直接调用object.__setattr__()来绕过它。任何依赖__setattr__的解决方案都保证不起作用。

以下是不使用某种元组可以得到的最接近的结果:

class Immutable:
    __slots__ = ['a', 'b']
    def __init__(self, a, b):
        object.__setattr__(self, 'a', a)
        object.__setattr__(self, 'b', b)
    def __setattr__(self, *ignored):
        raise NotImplementedError
    __delattr__ = __setattr__

但如果你足够努力,它就会破裂:

>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2

但Sven对namedtuple的使用确实是不可变的。

更新

由于这个问题已经更新为询问如何在C中正确地做这件事,下面是我关于如何在Cython中正确地做这件事的答案:

第一个immutable.pyx:

cdef class Immutable:
    cdef object _a, _b

    def __init__(self, a, b):
        self._a = a
        self._b = b

    property a:
        def __get__(self):
            return self._a

    property b:
        def __get__(self):
            return self._b

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

和一个setup.py来编译它(使用命令setup.py build_ext——inplace:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("immutable", ["immutable.pyx"])]

setup(
  name = 'Immutable object',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

然后试试吧:

>>> from immutable import Immutable
>>> p = Immutable(2, 3)
>>> p
<Immutable 2, 3>
>>> p.a = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> object.__setattr__(p, 'a', 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> p.a, p.b
(2, 3)
>>>      

除了其他优秀的答案之外,我喜欢为python 3.4(或者可能是3.3)添加一个方法。这个答案建立在之前对这个问题的几个答案的基础上。

在python 3.4中,可以使用不带设置符的属性来创建不可修改的类成员。(在早期版本中,可以不使用setter为属性赋值。)

class A:
    __slots__=['_A__a']
    def __init__(self, aValue):
      self.__a=aValue
    @property
    def a(self):
        return self.__a

你可以这样使用它:

instance=A("constant")
print (instance.a)

它会输出constant

而是调用实例。A =10会导致:

AttributeError: can't set attribute

解释:不带设置符的属性是python 3.4(我认为是3.3)的最新特性。如果您尝试给这样的属性赋值,则会引发Error。 使用插槽,我将成员变量限制为__A_a(即__a)。

问题:赋值给_aa仍然是可能的(instance. _aa =2)。但是如果你给一个私有变量赋值,那是你自己的错…

然而,这个答案不鼓励使用__slots__。使用其他方法来阻止属性创建可能更可取。

最简单的方法是使用__slots__:

class A(object):
    __slots__ = []

A的实例现在是不可变的,因为您不能在它们上设置任何属性。

如果你想让类实例包含数据,你可以将this和derived from tuple结合起来:

from operator import itemgetter
class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    x = property(itemgetter(0))
    y = property(itemgetter(1))

p = Point(2, 3)
p.x
# 2
p.y
# 3

编辑:如果你想摆脱索引,你可以重写__getitem__():

class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    @property
    def x(self):
        return tuple.__getitem__(self, 0)
    @property
    def y(self):
        return tuple.__getitem__(self, 1)
    def __getitem__(self, item):
        raise TypeError

注意,不能使用operator。在这种情况下,属性的itemgetter,因为这将依赖于Point.__getitem__()而不是tuple.__getitem__()。此外,这不会阻止使用元组。__getitem__(p, 0),但我很难想象这应该如何构成一个问题。

我不认为创建不可变对象的“正确”方法是编写C扩展。Python通常依赖于库实现者和库用户是成年人,而不是真正强制执行接口,接口应该在文档中清楚地说明。这就是为什么我不认为通过调用object.__setattr__()来规避被重写的__setattr__()是一个问题的可能性。如果有人这么做,风险自负。

我通过重写__setattr__创建了不可变类,并且如果调用者是__init__,则允许该集合:

import inspect
class Immutable(object):
    def __setattr__(self, name, value):
        if inspect.stack()[2][3] != "__init__":
            raise Exception("Can't mutate an Immutable: self.%s = %r" % (name, value))
        object.__setattr__(self, name, value)

这还不够,因为它允许任何人的___init__来改变对象,但你懂的。