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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4828080/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-18 17:42:04  来源:igfitidea点击:

How to make an immutable object in Python?

pythonpython-3.ximmutability

提问by Lennart Regebro

Although I have never needed this, it just struck me that making an immutable object in Python could be slightly tricky. You can't just override __setattr__, because then you can't even set attributes in the __init__. Subclassing a tuple is a trick that works:

虽然我从来不需要这个,但让我震惊的是,在 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

But then you have access to the aand bvariables through self[0]and self[1], which is annoying.

但是你可以通过and访问aandb变量,这很烦人。self[0]self[1]

Is this possible in Pure Python? If not, how would I do it with a C extension?

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

(Answers that work only in Python 3 are acceptable).

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

Update:

更新:

So subclassing tuple is the way to do it in Pure Python, which works well except for the additional possibility of accessing the data by [0], [1]etc. So, to complete this question all that is missing is howto do it "properly" in C, which I suspect would be quite simple, by just not implementing any geititemor setattribute, etc. But instead of doing it myself, I offer a bounty for that, because I'm lazy. :)

因此,继承元组是做纯Python,效果很好,除了通过访问数据的另一种可能性的方式[0][1]等等。所以,要完成这个问题,所有缺少的是HOWTO做“正确的”,在C,其中我怀疑这很简单,只是不实施任何geititemsetattribute等。但是我没有自己做,而是为此提供赏金,因为我很懒。:)

采纳答案by Sven Marnach

Yet another solution I just thought of: The simplest way to get the same behaviour as your original code is

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

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

It does not solve the problem that attributes can be accessed via [0]etc., but at least it's considerably shorter and provides the additional advantage of being compatible with pickleand copy.

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

namedtuplecreates a type similar to what I described in this answer, i.e. derived from tupleand using __slots__. It is available in Python 2.6 or above.

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

回答by Sven Marnach

The easiest way to do this is using __slots__:

最简单的方法是使用__slots__

class A(object):
    __slots__ = []

Instances of Aare immutable now, since you can't set any attributes on them.

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

If you want the class instances to contain data, you can combine this with deriving from tuple:

如果您希望类实例包含数据,您可以将其与派生自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: If you want to get rid of indexing either, you can override __getitem__():

编辑:如果你想摆脱索引,你可以覆盖__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

Note that you can't use operator.itemgetterfor the properties in thise case, since this would rely on Point.__getitem__()instead of tuple.__getitem__(). Fuerthermore this won't prevent the use of tuple.__getitem__(p, 0), but I can hardly imagine how this should constitute a problem.

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

I don't think the "right" way of creating an immutable object is writing a C extension. Python usually relies on library implementers and library users being consenting adults, and instead of really enforcing an interface, the interface should be clearly stated in the documentation. This is why I don't consider the possibility of circumventing an overridden __setattr__()by calling object.__setattr__()a problem. If someone does this, it's on her own risk.

我不认为创建不可变对象的“正确”方法是编写 C 扩展。Python 通常依赖于库实现者和库用户的同意,而不是真正强制执行一个接口,接口应该在文档中明确说明。这就是为什么我不考虑__setattr__()通过调用object.__setattr__()问题来绕过覆盖的可能性。如果有人这样做,她自己承担风险。

回答by PaoloVictor

You could create a @immutabledecorator that either overrides the __setattr__andchange the __slots__to an empty list, then decorate the __init__method with it.

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

Edit: As the OP noted, changing the __slots__attribute only prevents the creation of new attributes, not the modification.

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

Edit2: Here's an implementation:

Edit2:这是一个实现:

Edit3: Using __slots__breaks this code, because if stops the creation of the object's __dict__. I'm looking for an alternative.

Edit3:使用__slots__会破坏此代码,因为如果停止创建对象的__dict__. 我正在寻找替代方案。

Edit4: Well, that's it. It's a but hackish, but works as an exercise :-)

Edit4:嗯,就是这样。这是一个但hackish,但可以作为练习:-)

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

回答by Ned Batchelder

I've made immutable classes by overriding __setattr__, and allowing the set if the caller is __init__:

我通过覆盖创建了不可变的类__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)

This isn't quite enough yet, since it allows anyone's ___init__to change the object, but you get the idea.

这还不够,因为它允许任何人___init__更改对象,但是您明白了。

回答by Sven Marnach

Another idea would be to completely disallow __setattr__and use object.__setattr__in the constructor:

另一个想法是在构造函数中完全禁止__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

Of course you could use object.__setattr__(p, "x", 3)to modify a Pointinstance p, but your original implementation suffers from the same problem (try tuple.__setattr__(i, "x", 42)on an Immutableinstance).

当然,你可以使用object.__setattr__(p, "x", 3)修改Point的实例p,但同样的问题,你原来患有实施(尝试tuple.__setattr__(i, "x", 42)在一个Immutable实例)。

You can apply the same trick in your original implementation: get rid of __getitem__(), and use tuple.__getitem__()in your property functions.

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

回答by Duncan

I don't think it is entirely possible except by using either a tuple or a namedtuple. No matter what, if you override __setattr__()the user can always bypass it by calling object.__setattr__()directly. Any solution that depends on __setattr__is guaranteed not to work.

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

The following is about the nearest you can get without using some sort of tuple:

以下是您在不使用某种元组的情况下可以获得的最近的结果:

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__

but it breaks if you try hard enough:

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

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

but Sven's use of namedtupleis genuinely immutable.

但 Sven 的使用namedtuple是真正不可变的。

Update

更新

Since the question has been updated to ask how to do it properly in C, here's my answer on how to do it properly in Cython:

由于问题已更新为询问如何在 C 中正确执行,以下是我关于如何在 Cython 中正确执行的答案:

First immutable.pyx:

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

and a setup.pyto compile it (using the command setup.py build_ext --inplace:

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
)

Then to try it out:

然后尝试一下:

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

回答by jfs

..howto do it "properly" in C..

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

You could use Cythonto create an extension type for Python:

您可以使用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

It works both Python 2.x and 3.

它适用于 Python 2.x 和 3。

Tests

测试

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

If you don't mind indexing support then collections.namedtuplesuggested by @Sven Marnachis preferrable:

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

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

回答by dangirsh

This way doesn't stop object.__setattr__from working, but I've still found it useful:

这种方式不会停止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)

you may need to override more stuff (like __setitem__) depending on the use case.

您可能需要__setitem__根据用例覆盖更多内容(如)。

回答by Mark Horvath

An alternative approach is to create a wrapper which makes an instance immutable.

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

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)

This is useful in situations where only some instances have to be immutable (like default arguments of function calls).

这在只有某些实例必须是不可变的(例如函数调用的默认参数)的情况下很有用。

Can also be used in immutable factories like:

也可以在不可变工厂中使用,例如:

@classmethod
def immutable_factory(cls, *args, **kwargs):
    return Immutable(cls.__init__(*args, **kwargs))

Also protects from object.__setattr__, but fallable to other tricks due to Python's dynamic nature.

还可以防止object.__setattr__,但由于 Python 的动态特性而容易受到其他技巧的影响。

回答by theengineear

I needed this a little while ago and decided to make a Python package for it. The initial version is on PyPI now:

不久前我需要这个,并决定为它制作一个 Python 包。初始版本现在在 PyPI 上:

$ pip install immutable

To use:

使用:

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

Full docs here: https://github.com/theengineear/immutable

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

Hope it helps, it wraps a namedtuple as has been discussed, but makes instantiation much simpler.

希望它有所帮助,它包装了一个已讨论过的命名元组,但使实例化更简单。