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


当前回答

如果您对具有行为的对象感兴趣,那么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上也进行了测试)。 唯一的缺点是继承树有点奇怪;但这不是你经常玩的东西。

其他回答

从Python 3.7开始,你可以在你的类中使用@dataclass装饰器,它将像结构体一样是不可变的!不过,它可能会也可能不会将__hash__()方法添加到类中。引用:

hash() is used by built-in hash(), and when objects are added to hashed collections such as dictionaries and sets. Having a hash() implies that instances of the class are immutable. Mutability is a complicated property that depends on the programmer’s intent, the existence and behavior of eq(), and the values of the eq and frozen flags in the dataclass() decorator. By default, dataclass() will not implicitly add a hash() method unless it is safe to do so. Neither will it add or change an existing explicitly defined hash() method. Setting the class attribute hash = None has a specific meaning to Python, as described in the hash() documentation. If hash() is not explicit defined, or if it is set to None, then dataclass() may add an implicit hash() method. Although not recommended, you can force dataclass() to create a hash() method with unsafe_hash=True. This might be the case if your class is logically immutable but can nonetheless be mutated. This is a specialized use case and should be considered carefully.

下面是上面链接的文档中的例子:

@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

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

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

这里没有包括的是完全不可变性……不仅仅是父对象,还有所有的子对象。例如,元组/frozensets可能是不可变的,但它所属的对象可能不是。下面是一个小的(不完整的)版本,它在执行不变性方面做得很好:

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

我不认为这是完全可能的,除非使用一个元组或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)
>>>      

你可以在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")