虽然我从来都不需要这样做,但我突然意识到用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
但是你可以通过self[0]和self[1]访问a和b变量,这很烦人。
这在Pure Python中可行吗?如果不是,我该如何用C扩展来做呢?
(只能在python3中工作的答案是可以接受的)。
更新:
从Python 3.7开始,要使用的方法是使用@dataclass装饰器,参见最新接受的答案。
最简单的方法是使用__slots__:
class A(object):
__slots__ = []
A的实例现在是不可变的,因为您不能在它们上设置任何属性。
如果你想让类实例包含数据,你可以将this和derived from 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__()。此外,这不会阻止使用元组。__getitem__(p, 0),但我很难想象这应该如何构成一个问题。
我不认为创建不可变对象的“正确”方法是编写C扩展。Python通常依赖于库实现者和库用户是成年人,而不是真正强制执行接口,接口应该在文档中清楚地说明。这就是为什么我不认为通过调用object.__setattr__()来规避被重写的__setattr__()是一个问题的可能性。如果有人这么做,风险自负。
你可以在init的最后一条语句中重写setAttr。那么你可以构建,但不能改变。显然,你仍然可以用usint对象重写。但在实践中,大多数语言都有某种形式的反射,因此不可变始终是一个有漏洞的抽象。不可变性更多的是防止客户端意外地违反对象的契约。我使用:
=============================
最初提供的解决方案是不正确的,这是基于使用这里的解决方案的评论而更新的
原来的解决方案是错误的,这是一种有趣的方式,所以它被包括在底部。
===============================
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")
就像字典一样
我有一个开源库,在那里我以函数的方式做事情,所以在不可变对象中移动数据是有帮助的。但是,我不希望必须转换我的数据对象以便客户机与它们交互。所以,我想到了这个-它给你一个字典一样的对象,这是不可变的+一些帮助方法。
这要归功于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
我已经创建了一个小型类装饰器decorator,以使类不可变(除了在__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
最简单的方法是使用__slots__:
class A(object):
__slots__ = []
A的实例现在是不可变的,因为您不能在它们上设置任何属性。
如果你想让类实例包含数据,你可以将this和derived from 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__()。此外,这不会阻止使用元组。__getitem__(p, 0),但我很难想象这应该如何构成一个问题。
我不认为创建不可变对象的“正确”方法是编写C扩展。Python通常依赖于库实现者和库用户是成年人,而不是真正强制执行接口,接口应该在文档中清楚地说明。这就是为什么我不认为通过调用object.__setattr__()来规避被重写的__setattr__()是一个问题的可能性。如果有人这么做,风险自负。