首页 文章

如何在Python中创建不可变对象?

提问于
浏览 147
149

虽然我从来没有需要这个,但让我感到震惊的是在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] 访问 ab 变量,这很烦人 .

这在Pure Python中是否可行?如果没有,我将如何使用C扩展?

(仅适用于Python 3的答案是可以接受的) .

Update:

因此,子类化元组是在纯Python中实现它的方法,除了通过 [0][1] 等访问数据的额外可能性之外,它运行良好 . 因此,要完成此问题,所有缺少的是如何在C中执行此操作"properly"我怀疑是非常简单的,只是没有实施任何 geititemsetattribute 等 . 但我不是自己做,而是为此提供赏金,因为我很懒 . :)

21 回答

  • 95

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

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

    它没有解决可以通过 [0] 等访问属性的问题,但至少它要短得多,并且提供了与 picklecopy 兼容的额外优势 .

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

  • 67

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

    最简单的方法是使用 __slots__

    class A(object):
        __slots__ = []
    

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

    如果希望类实例包含数据,可以将其与 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
    

    Edit :如果你想摆脱索引,你可以覆盖 __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__() . 此外,这不会阻止使用 tuple.__getitem__(p, 0) ,但我很难想象这应该如何构成一个问题 .

    我不认为"right"创建不可变对象的方式是编写C扩展 . Python通常依赖于库实现者和库用户consenting adults,而不是真正强制执行接口,应该在文档中明确说明接口 . 这就是为什么我不考虑通过调用 object.__setattr__() 来解决被覆盖的 __setattr__() 的可能性 . 如果有人这样做,那就是她自己的风险 .

  • 34

    这种方式不会阻止 object.__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__ ) .

  • 17

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

    如namedtuple documentation的底部所述,您可以从namedtuple派生自己的类;然后,您可以添加所需的行为 .

    例如(直接从documentation获取的代码):

    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上测试) .
    唯一的缺点是继承树有点奇怪;但这不是你经常玩的东西 .

  • 9

    下面的基本解决方案解决了以下情况:

    可以像往常一样写入

    • __init__() 来访问属性 .

    • OBJECT仅冻结属性更改后:

    我们的想法是覆盖 __setattr__ 方法,并在每次更改对象冻结状态时替换其实现 .

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

    此机制可以在用户类中实现,也可以从特殊的 Freezer 类继承,如下所示:

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

    ..如何在C中“正确”地做到这一点 .

    您可以使用Cython为Python创建扩展类型:

    cdef class Immutable:
        cdef readonly object a, b
        cdef object __weakref__ # enable weak referencing support
    
        def __init__(self, a, b):
            self.a, self.b = a, b
    

    它适用于Python 2.x和3 .

    测试

    # compile on-the-fly
    import pyximport; pyximport.install() # $ pip install cython
    from immutable import Immutable
    
    o = Immutable(1, 2)
    assert o.a == 1, str(o.a)
    assert o.b == 2
    
    try: o.a = 3
    except AttributeError:
        pass
    else:
        assert 0, 'attribute must be readonly'
    
    try: o[1]
    except TypeError:
        pass
    else:
        assert 0, 'indexing must not be supported'
    
    try: o.c = 1
    except AttributeError:
        pass
    else:
        assert 0, 'no new attributes are allowed'
    
    o = Immutable('a', [])
    assert o.a == 'a'
    assert o.b == []
    
    o.b.append(3) # attribute may contain mutable object
    assert o.b == [3]
    
    try: o.c
    except AttributeError:
        pass
    else:
        assert 0, 'no c attribute'
    
    o = Immutable(b=3,a=1)
    assert o.a == 1 and o.b == 3
    
    try: del o.b
    except AttributeError:
        pass
    else:
        assert 0, "can't delete attribute"
    
    d = dict(b=3, a=1)
    o = Immutable(**d)
    assert o.a == d['a'] and o.b == d['b']
    
    o = Immutable(1,b=3)
    assert o.a == 1 and o.b == 3
    
    try: object.__setattr__(o, 'a', 1)
    except AttributeError:
        pass
    else:
        assert 0, 'attributes are readonly'
    
    try: object.__setattr__(o, 'c', 1)
    except AttributeError:
        pass
    else:
        assert 0, 'no new attributes'
    
    try: Immutable(1,c=3)
    except TypeError:
        pass
    else:
        assert 0, 'accept only a,b keywords'
    
    for kwd in [dict(a=1), dict(b=2)]:
        try: Immutable(**kwd)
        except TypeError:
            pass
        else:
            assert 0, 'Immutable requires exactly 2 arguments'
    

    如果您不介意索引支持,那么@Sven Marnach建议collections.namedtuple是可取的:

    Immutable = collections.namedtuple("Immutable", "a b")
    
  • 3

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

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

    Edit2:这是一个实现:

    Edit3:使用 __slots__ 中断了这段代码,因为如果停止创建对象的 __dict__ . 我正在寻找替代方案 .

    编辑4:嗯,就是这样 . 这是一个但很神圣,但作为一个练习:-)

    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
    
  • 3

    我使用与Alex相同的想法:元类和“初始标记”,但结合覆盖__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)
    

    注意:我正在打电话给meta-class直接使它适用于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]
    
  • 2

    除了使用元组或命名元组之外,我认为完全不可能 . 无论如何,如果你覆盖 __setattr__() ,用户总是可以通过直接调用_769429来绕过它 . 任何依赖于 __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
    

    但是斯文对_769433的使用确实是不可改变的 .

    Update

    由于问题已经更新以询问如何在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)
    >>>
    
  • 2

    除了优秀的其他答案,我还想为python 3.4(或者3.3)添加一个方法 . 这个答案 Build 在这个问题的几个前置答案之上 .

    在python 3.4中,您可以使用不带setter的属性来创建无法修改的类成员 . (在早期版本中,可以分配给没有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"

    但是调用 instance.a=10 会导致:

    AttributeError: can't set attribute
    

    解释:没有setter的属性是python 3.4的最新特性(我认为3.3) . 如果您尝试分配给此类属性,则会引发错误 . 使用插槽我将membervariables限制为 __A_a (即 __a ) .

    问题:仍然可以分配给 _A__ainstance._A__a=2 ) . 但是如果你分配给一个私有变量,这是你自己的错......

    然而,This answer不鼓励使用 __slots__ . 使用其他方法来防止属性创建可能是首选 .

  • 2

    我不久前需要这个,并决定为它制作一个Python包 . 最初的版本现在是PyPI:

    $ pip install immutable
    

    使用:

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

    完整文档:https://github.com/theengineear/immutable

    希望它有所帮助,它包含了一个已经讨论过的命名元组,但使实例化变得更加简单 .

  • 2

    从Python 3.7开始,您可以在类中使用@dataclass decorator,它将像struct一样不可变!但是,它可能会也可能不会为您的 class 添加hash()方法 . 引用:

    hash()由内置的hash()使用,并且当对象被添加到散列集合(如字典和集合)时 . 有一个hash()意味着该类的实例是不可变的 . 可变性是一个复杂的属性,它取决于程序员的意图,eq()的存在和行为,以及dataclass()装饰器中的eq和冻结标志的值 . 默认情况下,dataclass()不会隐式添加hash()方法,除非这样做是安全的 . 它也不会添加或更改现有的显式定义的hash()方法 . 设置类属性hash = None对Python具有特定含义,如hash()文档中所述 . 如果未明确定义hash(),或者将其设置为None,则dataclass()可能会添加隐式hash()方法 . 虽然不推荐,但您可以强制dataclass()使用unsafe_hash = True创建hash()方法 . 如果您的类在逻辑上是不可变的但仍然可以变异,则可能就是这种情况 . 这是一个专门的用例,应该仔细考虑 .

    这里是上面链接的文档中的示例:

    @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
    
  • 1

    我通过重写 __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)
    

    这不是改变对象,但你明白了 .

  • 1

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

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

    同样可以防止 object.__setattr__ ,但由于Python的动态特性而导致其他技巧瘫痪 .

  • 1

    这里没有真正包含的一件事是完全不变性......不仅仅是父对象,还有所有孩子 . 例如,元组/ 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)
    
  • 0

    另一个想法是完全禁止 __setattr__ 并在构造函数中使用 object.__setattr__

    class Point(object):
        def __init__(self, x, y):
            object.__setattr__(self, "x", x)
            object.__setattr__(self, "y", y)
        def __setattr__(self, *args):
            raise TypeError
        def __delattr__(self, *args):
            raise TypeError
    

    当然,您可以使用 object.__setattr__(p, "x", 3) 来修改 Point 实例 p ,但您的原始实现会遇到同样的问题(在 Immutable 实例上尝试 tuple.__setattr__(i, "x", 42) ) .

    您可以在原始实现中应用相同的技巧:摆脱 __getitem__() ,并在属性函数中使用 tuple.__getitem__() .

  • 0

    这是一个 elegant 解决方案:

    class Immutable(object):
        def __setattr__(self, key, value):
            if not hasattr(self, key):
                super().__setattr__(key, value)
            else:
                raise RuntimeError("Can't modify immutable object's attribute: {}".format(key))
    

    从这个类继承,在构造函数中初始化你的字段,并且你都设置好了 .

  • 0

    继承自以下 Immutable 类的类在其 __init__ 方法完成执行后是不可变的,它们的实例也是不可变的 . 因为它没有阻止某人使用基础 objecttype 中的变异特殊方法,但这足以阻止任何人意外地改变类/实例 .

    它的工作原理是使用元类劫持类创建过程 .

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

    第三方attr模块提供this functionality .

    编辑: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 可能特别有用,因为它会为您处理样板(但不会产生任何魔法) . 特别是,它为您编写了九个dunder(X)方法(除非您关闭其中任何一个),包括repr,init,hash和所有比较函数 .

    attr 还提供了helper for slots .

  • 0

    您可以在init的最终语句中覆盖setAttr . 你可以建造但不能改变 . 显然你仍然可以通过usint对象覆盖 . setAttr 但实际上大多数语言都有某种形式的反思,所以不可变性总是一个漏洞的抽象 . 不变性更多的是防止客户意外违反对象的 Contract . 我用:

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

    提供的原始解决方案不正确,这是基于使用here解决方案的评论更新的

    原始解决方案以一种有趣的方式是错误的,因此它包含在底部 .

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

    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 = will第二次初始化失败 .

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

相关问题