如何在 Python 中创建不可变对象?
- 2025-01-20 09:07:00
- admin 原创
- 83
问题描述:
虽然我从未需要过这个,但我突然意识到在 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
但是然后你就可以通过和访问a
和b
变量,这很烦人。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 等进行子类化即可实现此目的的方法。您需要做的就是在启动后禁用setattr和delattr (如果您想使集合不可变,还要禁用setitem和delitem ):
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 步:声明不可变结构
声明Point
和Vector
分类:
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