如何在Python中制作一个不变的对象?


179

尽管我从不需要它,但让我感到惊讶的是,在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

但是然后您可以通过和来访问ab变量,这很烦人。self[0]self[1]

在纯Python中这可能吗?如果没有,我将如何使用C扩展名呢?

(仅在Python 3中有效的答案是可以接受的)。

更新:

因此,将元组子类化是在纯Python 中完成此操作的方法[0],除了通过[1]等访问数据的附加可能性外,该方法行之有效。要解决此问题,所有遗漏的是如何在C中“正确”进行操作,这我怀疑这很简单,只是不执行任何geititemor setattribute等等。但是我没有为此做,而是为此提供了赏金,因为我很懒。:)


2
您的代码是否不方便通过.a和访问属性.b?毕竟这就是属性的存在。
Sven Marnach 2011年

1
@Sven Marnach:是的,但是[0]和[1]仍然有效,为什么呢?我不要 :)也许带有属性的不可变对象的想法是胡说?:-)
Lennart Regebro

2
另一个注意事项:NotImplemented仅作为丰富比较的返回值。__setatt__()无论如何,返回值是毫无意义的,因为您通常根本看不到它。像这样的代码immutable.x = 42将默默地执行任何操作。您应该提出一个TypeError替代。
Sven Marnach 2011年

1
@Sven Marnach:好的,我很惊讶,因为我认为您可以在这种情况下提出NotImplemented,但这会带来一个奇怪的错误。因此,我退回了它,它似乎起作用了。一旦我看到您使用TypeError,它就很明显了。
Lennart Regebro

1
@Lennart:您可以引发NotImplementedError,但是TypeError如果您尝试修改它,则是元组引发的。
Sven Marnach 2011年

Answers:


115

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

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

它不能解决可以通过[0]等访问属性的问题,但至少它要短得多,并具有与pickle和兼容的附加优点。copy

namedtuple创建与我在此答案中描述的类型相似的类型,即派生自tuple并使用__slots__。它在Python 2.6或更高版本中可用。


7
与手写模拟(即使在Python 2.5上(使用代码的verbose参数namedtuple很容易生成))相比,此变体的优点是a的单个接口/实现namedtuple优于几十种稍有不同的手写接口/实现,做几乎相同的事情。
jfs

2
好的,您会得到“最佳答案”,因为这是最简单的方法。塞巴斯蒂安因提供简短的Cython实现而获得赏金。干杯!
Lennart Regebro

1
不变对象的另一个特征是,当您通过函数将它们作为参数传递时,它们将按值复制,而不是进行其他引用。namedtuple通过函数传递值时会被复制吗?
hlin117

4
@ hlin117:每个参数都作为对Python中对象的引用传递,无论它是可变的还是不可变的。对于不可变的对象,进行复制特别没有意义-由于您无论如何都无法更改对象,因此最好将引用传递给原始对象。
Sven Marnach 2015年

您可以在类内部内部使用namedtuple而不是在外部实例化对象吗?我是python的新手,但是另一个答案的好处是,我可以让一个类隐藏细节,还具有诸如可选参数之类的功能。如果只看这个答案,似乎我需要拥有所有使用类实例化的元组的东西。谢谢你的两个答案。
Asaf

78

最简单的方法是使用__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

编辑:如果要摆脱索引之一,则可以覆盖__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),但是我几乎无法想象这应该如何构成问题。

我认为创建不可变对象的“正确”方法不是编写C扩展。Python通常依赖于图书馆实现者和图书馆用户征得成年人的同意,而不是真正强制执行接口,而应在文档中明确说明该接口。这就是为什么我不考虑__setattr__()通过提出object.__setattr__()问题来规避被覆盖的可能性的原因。如果有人这样做,则后果自负。


1
tuple在这里使用a __slots__ = ()而不是一个更好的主意__slots__ = []吗?(只需澄清)
2011年

1
@sukhbir:我认为这根本没有关系。你为什么更喜欢一个元组?
Sven Marnach 2011年

1
@Sven:我同意这无关紧要(除了速度部分,我们可以忽略),但是我这样想:__slots__不会改变吗?目的是一次确定可以设置哪些属性。那么在这种情况下tuple似乎不是很自然的选择吗?
user225312 2011年

5
但是__slots__如果设置为空,则无法设置任何属性。如果我有,__slots__ = ('a', 'b')那么a和b属性仍然是可变的。
Lennart Regebro

但是您的解决方案比覆盖更好,__setattr__因此是对我的改进。+1 :)
Lennart Regebro

50

..如何在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")

@Lennart :(namedtuple或更确切地说是函数返回的类型namedtuple())的实例是不可变的。绝对是
Sven Marnach 2011年

@Lennart Regebro:namedtuple通过所有测试(索引支持除外)。我错过了什么要求?
jfs

是的,您是对的,我创建了一个namedtuple类型,实例化了它,然后对该类型而不是实例进行了测试。嘿。:-)
Lennart Regebro 2011年

请问为什么这里需要弱引用?
McSinyx

1
@McSinyx:否则,这些对象不能在weakref的集合中使用。Python 到底是什么__weakref__
jfs

40

另一个想法是完全禁止在构造函数中__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,但同样的问题,你原来患有实施(试行tuple.__setattr__(i, "x", 42)Immutable实例)。

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


11
我不会在乎有人使用超类故意修改对象__setattr__,因为这并不是万无一失的。重点是要明确指出不应修改它,并防止错误修改。
zvone 2012年

18

您可以创建一个@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

1
从解决方案中制作一个(类?)装饰器或元类确实是一个好主意,但问题是解决方案是什么。:)
Lennart Regebro

3
object.__setattr__()打破它stackoverflow.com/questions/4828080/…–
jfs

确实。我只是在做装饰工的练习。
PaoloVictor

12

使用冻结的数据类

对于Python 3.7+,您可以使用带有option数据类,这是一种非常Python化且可维护的方式来完成您想要的事情。frozen=True

它看起来像这样:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
    a: Any
    b: Any

由于数据类的字段需要类型提示,因此我typing模块中使用了Any

不使用命名元组的原因

在Python 3.7之前,经常会看到namedtuple被用作不可变对象。在许多方面可能很棘手,其中之一是__eq__namedtuple之间的方法不考虑对象的类。例如:

from collections import namedtuple

ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])

obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)

obj1 == obj2  # will be True

如您所见,即使obj1和的类型obj2不同,即使它们的字段名称不同,obj1 == obj2仍会给出True。这是因为所__eq__使用的方法是元组的方法,该方法仅比较给定位置的字段的值。这可能是错误的巨大来源,特别是如果您将这些类作为子类。


10

除了使用元组或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)
>>>      

感谢您的Cython代码,Cython非常棒。JF Sebastian的readonly实施更加整洁,虽然先到达,但他获得了赏金。
Lennart Regebro

5

我通过重写来实现了不可变的类__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__更改对象,但是您明白了。


object.__setattr__()打破它stackoverflow.com/questions/4828080/…–
jfs

3
使用堆栈检查来确保调用者的__init__满意度不是很高。
gb。

5

除了出色的其他答案外,我还想为python 3.4(或3.3)添加一个方法。该答案建立在对该问题的多个先前答案的基础上。

在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__a仍然可以分配到(instance._A__a=2)。但是如果您分配一个私有变量,那是您自己的错...

但是,此答案除其他外,不鼓励使用__slots__。最好使用其他方式来防止属性创建。


property在Python 2上也可用(请查看问题本身中的代码)。它不会创建一个不变的对象,请尝试从我的答案中进行测试,例如instance.b = 1创建一个新b属性。
jfs

是的,问题实际上是如何防止这样做,A().b = "foo"即不允许设置新属性。
Lennart Regebro

如果您尝试分配给该属性,则没有setter的Propertis将在python 3.4中引发错误。在较早的版本中,setter是隐式生成的。
2015年

@Lennart:我的解决方案是对不可变对象用例子集的答案,以及先前答案的补充。我可能想要一个不可变对象的原因之一是可以使它成为可散列的,在这种情况下,我的解决方案可能会起作用。但是您是正确的,这不是一个不变的对象。
2015年

@ jf-sebastian:更改了我的答案,以使用插槽来防止属性创建。与其他答案相比,我的答案中的新内容是,我使用python3.4的属性来避免更改现有属性。虽然在先前的答案中也能达到相同的目的,但由于属性行为的变化,我的代码更短。
2015年

5

这是一个优雅的解决方案:

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

从此类继承,初始化构造函数中的字段,一切就绪。


1
但与此逻辑能够分配新的属性的对象
JAVED

3

如果您对具有行为的对象感兴趣,那么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和2.7(也已在IronPython上测试)。
唯一的缺点是继承树有点怪异。但这不是您通常玩的东西。


1
Python 3.6+直接支持此功能,使用class Point(typing.NamedTuple):
Elazar

3

Immutable__init__方法完成执行后,继承自以下类的类及其实例是不可变的。正如其他人所指出的那样,由于它是纯python,所以没有什么可以阻止某人使用从base object和的变异特殊方法的type,但这足以阻止任何人无意间变异一个类/实例。

它通过劫持一个元类的类创建过程来工作。

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

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

希望它能有所帮助,它如前所述包装了一个namedtuple,但是使实例化变得更加简单。


2

这种方法不会停止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__根据用例覆盖更多的内容(例如)。


在看到之前,我想出了类似的方法,但是使用了它,getattr因此可以为提供默认值frozen。那简化了一点。stackoverflow.com/a/22545808/5987
Mark Ransom 2014年

我最喜欢这种方法,但是您不需要__new__覆盖。内部__setattr__只需将条件替换为if name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi,2015年

同样,也不需要在构建时冻结类。如果提供freeze()功能,则可以随时冻结它。然后该对象将“冻结一次”。最后,担心object.__setattr__是愚蠢的,因为“我们都是成年人”。
Pete Cacioppi

2

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

hash()由内置的hash()以及将对象添加到哈希集合(如字典和集合)时使用。带有hash()表示该类的实例是不可变的。可变性是一个复杂的属性,它取决于程序员的意图,eq()的存在和行为以及dataclass()装饰器中的eq和冻结标志的值。

默认情况下,除非这样做是安全的,否则dataclass()不会隐式添加hash()方法。它不会添加或更改现有的显式定义的hash()方法。如hash()文档中所述,设置类属性hash = None对Python具有特定的含义。

如果未明确定义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
你需要使用frozen,即@dataclass(frozen=True),但它基本上是块使用__setattr__,并__delattr__在大多数其他的答案在这里等。它只是以与数据类的其他选项兼容的方式来执行此操作。
CS CS

2

您可以覆盖setattr并仍然使用init来设置变量。您将使用超类setattr。这是代码。

不可变的类别:
    __slots__ =('a','b')
    def __init __(self,a,b):
        super().__ setattr __('a',a)
        super().__ setattr __('b',b)

    def __str __():
        返回“” .format(self.a,self.b)

    def __setattr __(自己,*忽略):
        引发NotImplementedError

    def __delattr __(自己,*忽略):
        引发NotImplementedError

或者只是pass代替raise NotImplementedError
jonathan.scholbach

在这种情况下,在__setattr__和__delattr__中进行“通过”根本不是一个好主意。原因很简单,如果有人为某个字段/属性分配值,那么他们自然会期望该字段将被更改。如果您想遵循“最少惊喜”的方式(应该这样做),则必须提出一个错误。但是我不确定NotImplementedError是否合适。我会提出类似“字段/属性不可变”的内容。错误...我认为应该引发自定义异常。
darlove

1

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

编辑:python 3.7已将该思想采纳到stdlib中@dataclass

$ 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还为提供了帮助__slots__


1

因此,我正在分别编写python 3:

I)在数据类修饰器的帮助下并设置Frozen = True。我们可以在python中创建不可变的对象。

为此,需要从数据类库中导入数据类,并需要设置Frozen = True

例如

从数据类导入数据类

@dataclass(frozen=True)
class Location:
    name: str
    longitude: float = 0.0
    latitude: float = 0.0

o / p:

l =位置(“ Delhi”,112.345,234.788)l.name'Delhi'l。经度112.345 l.latitude 234.788 l.name =“加尔各答”数据类。FrozenInstanceError:无法分配给字段'name'

资料来源:https : //realpython.com/python-data-classes/


0

一种替代方法是创建使实例不可变的包装器。

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的动态特性,也可以防止,但也容易出错。


0

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

注意:我直接调用元类以使其适用于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]

0

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

您可以在init的最终声明中覆盖setAttr。这样您就可以构建但不能改变。显然,您仍然可以使用usint对象覆盖。setAttr但实际上大多数语言都有某种形式的反射,因此始终是泄漏的抽象。不变性更多是关于防止客户意外违反对象合同。我用:

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

提供的原始解决方案不正确,已根据注释使用此处的解决方案进行了更新

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

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

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

1
那是行不通的:您将覆盖类上的方法,因此在尝试创建第二个实例时会立即收到NotImplementedError。
slinkp

1
如果要采用这种方法,请注意,在运行时很难覆盖特殊方法:有关此问题的几种解决方法,请参见stackoverflow.com/a/16426447/137635
slinkp

0

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

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

想法是在__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()

0

就像一个 dict

我有一个开放源代码库,在这里我以一种功能性的方式来做事,因此在不可变对象中移动数据很有帮助。但是,我不需要转换数据对象以使客户端与之交互。因此,我想出了这一点- 它为您提供了一个像对象一样不变的字典 +一些辅助方法。

感谢斯文Marnach在他回答的基本实施限制产权更新和删除的。

import json 
# ^^ optional - If you don't care if it prints like a dict
# then rip this and __str__ and __repr__ out

class Immutable(object):

    def __init__(self, **kwargs):
        """Sets all values once given
        whatever is passed in kwargs
        """
        for k,v in kwargs.items():
            object.__setattr__(self, k, v)

    def __setattr__(self, *args):
        """Disables setting attributes via
        item.prop = val or item['prop'] = val
        """
        raise TypeError('Immutable objects cannot have properties set after init')

    def __delattr__(self, *args):
        """Disables deleting properties"""
        raise TypeError('Immutable objects cannot have properties deleted')

    def __getitem__(self, item):
        """Allows for dict like access of properties
        val = item['prop']
        """
        return self.__dict__[item]

    def __repr__(self):
        """Print to repl in a dict like fashion"""
        return self.pprint()

    def __str__(self):
        """Convert to a str in a dict like fashion"""
        return self.pprint()

    def __eq__(self, other):
        """Supports equality operator
        immutable({'a': 2}) == immutable({'a': 2})"""
        if other is None:
            return False
        return self.dict() == other.dict()

    def keys(self):
        """Paired with __getitem__ supports **unpacking
        new = { **item, **other }
        """
        return self.__dict__.keys()

    def get(self, *args, **kwargs):
        """Allows for dict like property access
        item.get('prop')
        """
        return self.__dict__.get(*args, **kwargs)

    def pprint(self):
        """Helper method used for printing that
        formats in a dict like way
        """
        return json.dumps(self,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4)

    def dict(self):
        """Helper method for getting the raw dict value
        of the immutable object"""
        return self.__dict__

辅助方法

def update(obj, **kwargs):
    """Returns a new instance of the given object with
    all key/val in kwargs set on it
    """
    return immutable({
        **obj,
        **kwargs
    })

def immutable(obj):
    return Immutable(**obj)

例子

obj = immutable({
    'alpha': 1,
    'beta': 2,
    'dalet': 4
})

obj.alpha # 1
obj['alpha'] # 1
obj.get('beta') # 2

del obj['alpha'] # TypeError
obj.alpha = 2 # TypeError

new_obj = update(obj, alpha=10)

new_obj is not obj # True
new_obj.get('alpha') == 10 # True
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.