虽然我从来都不需要这样做,但我突然意识到用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__从工作,但我仍然发现它有用:

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

最简单的方法是使用__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__()是一个问题的可能性。如果有人这么做,风险自负。

除了其他优秀的答案之外,我喜欢为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__。使用其他方法来阻止属性创建可能更可取。

继承自以下Immutable类的类,在它们的__init__方法执行完成后,它们的实例也是不可变的。正如其他人指出的那样,因为它是纯python,所以没有什么可以阻止某人使用来自基对象和类型的特殊方法的突变,但这足以阻止任何人意外地突变类/实例。

它通过用元类劫持类创建过程来工作。

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

第三方attr模块提供了此功能。

编辑:python 3.7已经通过@dataclass在stdlib中采用了这个想法。

$ 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在每次实例化时都有轻微的性能影响。

如果您习惯使用类作为数据类型,attr可能特别有用,因为它为您处理样板文件(但没有任何魔力)。特别地,它为你编写了9个dunder (__X__)方法(除非你关闭其中任何一个),包括repr, init, hash和所有比较函数。

Attr还为__slots__提供了一个帮助器。