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

2025-01-20 09:07:00
admin
原创
83
摘要:问题描述:虽然我从未需要过这个,但我突然意识到在 Python 中创建一个不可变对象可能有点棘手。你不能直接覆盖__setattr__,因为那样你甚至无法在中设置属性__init__。对元组进行子类化是一个有效的技巧:class Immutable(tuple): def __new__(c...

问题描述:

虽然我从未需要过这个,但我突然意识到在 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 中有效的答案是可以接受的。


解决方案 1:

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

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

它不能解决可以通过等访问属性的问题,但至少它相当短,并提供了与和[0]兼容的额外优势。pickle`copy`

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

解决方案 2:

使用冻结数据类

对于 Python 3.7+,您可以使用带有选项的数据类,这是一种非常符合 Python 风格且可维护的方式来完成您想要的操作。frozen=True

它看上去是这样的:

from dataclasses import dataclass

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

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

不使用 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

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

解决方案 3:

最简单的方法是使用__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__()问题来规避覆盖的可能性。如果有人这样做,风险由她自己承担。

解决方案 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 Marnachcollections.namedtuple的建议是可取的:

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

解决方案 5:

另一个想法是完全禁止__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__()在属性函数中使用。

解决方案 6:

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

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

编辑2:这是一个实现:

编辑3:使用__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

解决方案 7:

我认为除了使用元组或命名元组外,这完全不可能。无论如何,如果您覆盖,__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

但斯文的使用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)
>>>      

解决方案 8:

我通过覆盖创建了不可变的类__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__改变对象,但你明白了。

解决方案 9:

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

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

从此类继承,在构造函数中初始化你的字段,然后一切就完成了。

解决方案 10:

除了其他出色的答案之外,我还想添加一个适用于 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)的一个非常新的特性。如果您尝试分配这样的属性,将引发错误。使用 slot,我将成员变量限制为__A_a(即__a)。

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

然而,除了其他答案之外,这个答案不鼓励使用__slots__。使用其他方法来防止属性创建可能是更好的选择。

解决方案 11:

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

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

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

前任。

从数据类导入数据类

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

输出:

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

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

解决方案 12:

如果您对具有行为的对象感兴趣,那么 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 和 Python 2.7(也在 IronPython 上测试过)。

唯一的缺点是继承树有点奇怪;但这不是您通常会用到的东西。

解决方案 13:

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

hash () 由内置 hash() 使用,并且当对象被添加到散列集合(例如字典和集合)时也会使用。拥有hash () 意味着该类的实例是不可变的。可变性是一个复杂的属性,它取决于程序员的意图、eq () 的存在和行为以及 dataclass() 装饰器中的 eq 和 freeze 标志的值。

默认情况下,除非安全,否则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

解决方案 14:

就像dict

我有一个开源库,我在其中以函数式的方式做事,因此在不可变对象中移动数据很有帮助。但是,我不想为了与客户端交互而转换我的数据对象。所以,我想出了这个 -它为您提供了一个不可变的类似字典的对象+ 一些辅助方法。

感谢Sven 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

解决方案 15:

这种方法不会停止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__根据使用情况,您可能需要覆盖更多内容(例如)。

解决方案 16:

从以下类继承的类在其方法执行完成Immutable后是不可变的,其实例也是如此。由于它是纯 Python,正如其他人指出的那样,没有什么可以阻止某人使用来自基类和的变异特殊方法,但这足以阻止任何人意外地改变类/实例。__init__`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__')

解决方案 17:

第三方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__

解决方案 18:

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

不可变类:
    __slots__ = ('a','b')
    def __init__(self, a, b):
        超级()。__setattr__('a',a)
        超级()。__setattr__('b',b)

    def __str__(自身):
        返回“”格式(self.a,self.b)

    def __setattr__(self,*ignored):
        引发 NotImplementedError

    def __delattr__(self,*ignored):
        引发 NotImplementedError

解决方案 19:

下面的基本解决方案针对以下场景:

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

解决方案 20:

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

$ pip install immutable

使用方法:

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

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

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

解决方案 21:

我找到了一种无需对 tuple、namedtuple 等进行子类化即可实现此目的的方法。您需要做的就是启动后禁用setattrdelattr (如果您想使集合不可变,还要禁用setitemdelitem ):

def __init__(self, *args, **kwargs):
    # something here

    self.lock()

其中看起来像这样:

@classmethod
def lock(cls):
    def raiser(*a):
        raise TypeError('this instance is immutable')

    cls.__setattr__ = raiser
    cls.__delattr__ = raiser
    if hasattr(cls, '__setitem__'):
        cls.__setitem__ = raiser
        cls.__delitem__ = raiser

因此,您可以使用此方法创建Immutable类并按照我展示的方式使用它。

如果你不想在每个初始化中都编写self.lock(),你可以使用元类自动执行它:

class ImmutableType(type):
    @classmethod
    def change_init(mcs, original_init_method):
        def __new_init__(self, *args, **kwargs):
            if callable(original_init_method):
                original_init_method(self, *args, **kwargs)

            cls = self.__class__

            def raiser(*a):
                raise TypeError('this instance is immutable')

            cls.__setattr__ = raiser
            cls.__delattr__ = raiser
            if hasattr(cls, '__setitem__'):
                cls.__setitem__ = raiser
                cls.__delitem__ = raiser

        return __new_init__

    def __new__(mcs, name, parents, kwargs):
        kwargs['__init__'] = mcs.change_init(kwargs.get('__init__'))
        return type.__new__(mcs, name, parents, kwargs)


class Immutable(metaclass=ImmutableType):
    pass

测试

class SomeImmutableClass(Immutable):
    def __init__(self, some_value: int):
        self.important_attr = some_value

    def some_method(self):
        return 2 * self.important_attr


ins = SomeImmutableClass(3)
print(ins.some_method())  # 6
ins.important_attr += 1  # TypeError
ins.another_attr = 2  # TypeError

解决方案 22:

简短答案

使用 pandaticBaseModel并覆盖Config

from pydantic import BaseModel

class Point(BaseModel):
    x: float
    y: float

    class Config:
        allow_mutation = False

p = Point(x=3.14, y=2.72)

p.x = 0  # this operation raise TypeError, because the object is immutable

基于 OOP 的长答案

步骤 1:设置抽象

使用pyndatic-package 实现可重用的ImmutableModel

from abc import ABC
from pydantic import BaseModel


class ImmutableModel(BaseModel, ABC):
    """Base immutable model."""

    class Config:
        allow_mutation = False

第 2 步:声明不可变结构

声明PointVector分类:

class Point(ImmutableModel):
    """Immutable point."""

    x: float
    y: float
    z: float

class Vector(ImmutableModel):
    """Immutable vector."""

    start: Point
    end: Point

步骤3:测试结果

# Test Point immutability ----
p = Point(x=3.14, y=2.72, z=0)

assert p.x == 3.14 and p.y == 2.72 and p.z == 0

try:
    p.x = 0  # try to change X value
except TypeError as e:  # error when trying to modify value
    print(e)
finally:
    assert p.x == 3.14  # X value wasn't modified

print(p)


# Test Vector immutability ----
v = Vector(start=Point(x=0, y=0, z=0), end=Point(x=1, y=1, z=1))

assert v.start != p and v.end != p

try:
    v.start = p
except TypeError as e: # error when trying to modify value
    print(e)
finally:
    assert v.start != p  # start point wasn't modified

print(v)

解决方案 23:

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

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 的动态特性,容易受到其他技巧的攻击。

解决方案 24:

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

解决方案 25:

这里没有真正包括的一件事是完全不变性……不仅是父对象,而且所有子对象也是如此。例如,tuple/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)

解决方案 26:

您只需在 init 的最后一个语句中覆盖 setAttr 即可。然后您可以构造但不能更改。显然,您仍然可以通过使用 int object.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 = 将在第二次初始化时失败。

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

解决方案 27:

我创建了一个小类装饰器,使类不可变(内部除外__init__)。作为https://github.com/google/etils的一部分。

from etils import epy


@epy.frozen
class A:

  def __init__(self):
    self.x = 123  # Inside `__init__`, attribute can be assigned

a = A()
a.x = 456  # AttributeError

这也支持继承。

执行:

_Cls = TypeVar('_Cls')


def frozen(cls: _Cls) -> _Cls:
  """Class decorator which prevent mutating attributes after `__init__`."""
  if not isinstance(cls, type):
    raise TypeError(f'{cls.__name__} is not a class.')

  cls.__init__ = _wrap_init(cls.__init__)
  cls.__setattr__ = _wrap_setattr(cls.__setattr__)
  return cls


def _wrap_init(init_fn):
  """`__init__` wrapper."""

  @functools.wraps(init_fn)
  def new_init(self, *args, **kwargs):
    if hasattr(self, '_epy_is_init_done'):
      # `_epy_is_init_done` already created, so it means we're
      # a `super().__init__` call.
      return init_fn(self, *args, **kwargs)
    object.__setattr__(self, '_epy_is_init_done', False)
    init_fn(self, *args, **kwargs)
    object.__setattr__(self, '_epy_is_init_done', True)

  return new_init

def _wrap_setattr(setattr_fn):
  """`__setattr__` wrapper."""

  @functools.wraps(setattr_fn)
  def new_setattr(self, name, value):
    if not hasattr(self, '_epy_is_init_done'):
      raise ValueError(
          'Child of `@epy.frozen` class should be `@epy.frozen` too. (Error'
          f' raised by {type(self)})'
      )
    if not self._epy_is_init_done:  # pylint: disable=protected-access
      return setattr_fn(self, name, value)
    else:
      raise AttributeError(
          f'Cannot assign {name!r} in `@epy.frozen` class {type(self)}'
      )

  return new_setattr
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用