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

Len*_*bro 165 python immutability python-3.x

虽然我从来没有需要这个,但让我感到震惊的是在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
Run Code Online (Sandbox Code Playgroud)

但是你可以通过和访问ab变量,这很烦人.self[0]self[1]

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

(只能在Python 3中使用的答案是可以接受的).

更新:

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

Sve*_*ach 105

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

Immutable = collections.namedtuple("Immutable", ["a", "b"])
Run Code Online (Sandbox Code Playgroud)

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

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

  • 与手写模拟相比,这种变体的优势(即使在Python 2.5上(使用`verbose`参数到`namedtuple`代码很容易生成))是`namedtuple`的单一接口/实现,可以优先使用几十个*所以稍微*不同的手写界面/实现,几乎*相同的东西. (7认同)
  • @ hlin117:每个参数都作为Python中对象的引用传递,无论它是可变的还是不可变的.对于不可变对象,制作副本尤其毫无意义 - 因为无论如何都无法更改对象,您也可以将引用传递给原始对象. (4认同)
  • 好的,你得到了"最佳答案",因为这是最简单的方法.塞巴斯蒂安获得了一个简短的Cython实施的赏金.干杯! (2认同)
  • 不可变对象的另一个特征是,当您通过函数将它们作为参数传递时,它们是按值复制的,而不是进行另一个引用。通过函数传递时,“namedtuple”会按值复制吗? (2认同)

Sve*_*ach 74

最简单的方法是使用__slots__:

class A(object):
    __slots__ = []
Run Code Online (Sandbox Code Playgroud)

实例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
Run Code Online (Sandbox Code Playgroud)

编辑:如果你想摆脱索引,你可以覆盖__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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 但是空的`__slots__`我无法设置*any*属性.如果我有`__slots__ =('a','b')`那么a和b属性仍然是可变的. (5认同)
  • 在这里使用“元组”“__slots__ = ()”而不是“__slots__ = []”不是更好吗?(只是澄清一下) (2认同)

jfs*_*jfs 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
Run Code Online (Sandbox Code Playgroud)

它适用于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'
Run Code Online (Sandbox Code Playgroud)

如果您不介意索引支持,那么@Sven Marnachcollections.namedtuple建议您更喜欢:

Immutable = collections.namedtuple("Immutable", "a b")
Run Code Online (Sandbox Code Playgroud)


Jun*_*ius 40

使用冻结的数据类

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

它看起来像这样:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
    a: Any
    b: Any
Run Code Online (Sandbox Code Playgroud)

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

不使用 Namedtuple 的原因

在 Python 3.7 之前,经常看到命名元组被用作不可变对象。这在很多方面都很棘手,其中之一是__eq__命名元组之间的方法不考虑对象的类。例如:

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
Run Code Online (Sandbox Code Playgroud)

如您所见,即使obj1和的类型obj2不同,即使它们的字段名称不同,obj1 == obj2仍然会给出True. 那是因为使用的__eq__方法是元组的方法,它只比较给定位置的字段的值。这可能是一个巨大的错误来源,特别是如果您要对这些类进行子类化。

  • 为什么没有选择解决方案?2021年还在工作 (4认同)
  • 因为它不允许用户定义的\_\_init\_\_函数设置属性。数据类(普通旧数据)和不变性的思想是正交的。 (3认同)
  • @Brent 当然,您可以使用数据类自定义 init 函数。https://www.python.org/dev/peps/pep-0557/#custom-init-method (3认同)
  • 经过更多研究后,似乎您实际上也不必设置 (init=False),引用手册中的内容:“init:如果 true (默认值),将生成 __init__() 方法。如果类已经定义了 __init__(),该参数被忽略。” https://docs.python.org/3/library/dataclasses.html (3认同)

Sve*_*ach 38

另一个想法是在构造函数中完全禁止__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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 我不关心有人故意使用超类"__setattr__"修改对象,因为这一点并非万无一失.关键是要明确不应该修改它并防止错误修改. (10认同)

Pao*_*tor 18

您可以创建一个@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
Run Code Online (Sandbox Code Playgroud)

  • `object .__ setattr __()`打破它http://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python/4829374#4829374 (3认同)

Dun*_*can 10

我不认为除了使用元组或命名元组之外完全可能.无论如何,如果你覆盖__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__
Run Code Online (Sandbox Code Playgroud)

但如果你努力尝试就会破裂:

>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2
Run Code Online (Sandbox Code Playgroud)

但斯文的使用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)
Run Code Online (Sandbox Code Playgroud)

和a 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
)
Run Code Online (Sandbox Code Playgroud)

然后尝试一下:

>>> 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)
>>>      
Run Code Online (Sandbox Code Playgroud)


Ned*_*der 6

我通过覆盖创建了不可变的类__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)
Run Code Online (Sandbox Code Playgroud)

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

  • 使用堆栈检查来确保调用者是 `__init__` 不是很令人满意。 (3认同)

Ber*_*ard 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
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它:

instance=A("constant")
print (instance.a)
Run Code Online (Sandbox Code Playgroud)

这将打印 "constant"

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

AttributeError: can't set attribute
Run Code Online (Sandbox Code Playgroud)

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

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

然而,这个答案不鼓励使用__slots__. 使用其他方法来阻止属性创建可能是更可取的。


Ale*_*hov 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))
Run Code Online (Sandbox Code Playgroud)

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

  • 但通过这种逻辑,可以为对象分配新属性 (5认同)

小智 5

所以,我正在编写 python 3 的相应内容:

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

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

前任。

从数据类导入数据类

@dataclass(frozen=True)
class Location:
    name: str
    longitude: float = 0.0
    latitude: float = 0.0
Run Code Online (Sandbox Code Playgroud)

输出:

>>> l = Location("Delhi", 112.345, 234.788)
>>> l.name
'Delhi'
>>> l.longitude
112.345
>>> l.latitude
234.788
>>> l.name = "Kolkata"
dataclasses.FrozenInstanceError: cannot assign to field 'name'
>>> 
Run Code Online (Sandbox Code Playgroud)

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