Comment faire un objet immuable en Python?
bien que je n'ai jamais eu besoin de cela, il vient de me frapper que faire un objet immuable en Python pourrait être un peu délicat. Tu ne peux pas annuler __setattr__
, parce qu'alors vous ne pouvez même pas définir les attributs __init__
. Sous-classement d'un n-uplet est un truc qui marche:
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
mais alors vous avez accès aux variables a
et b
à travers self[0]
et self[1]
, ce qui est ennuyeux.
est-ce possible en Python pur? Dans la négative, comment le ferais-je avec une extension C?
(les réponses qui ne fonctionnent qu'en Python 3 sont acceptables).
mise à jour:
donc subclassing tuple est la façon de le faire en Python pur, ce qui fonctionne bien sauf pour la possibilité supplémentaire d'accéder aux données par [0]
, [1]
etc. Donc, pour compléter ceci question tout ce qui manque, c'est de savoir comment le faire "correctement" en C, ce qui , je le soupçonne, serait assez simple, en ne mettant tout simplement pas en œuvre les geititem
ou setattribute
, etc. Mais au lieu de le faire moi-même, j'offre une prime pour ça, parce que je suis paresseux. :)
20 réponses
encore une autre solution à laquelle je viens de penser: la façon la plus simple d'obtenir le même comportement que votre code d'origine est
Immutable = collections.namedtuple("Immutable", ["a", "b"])
Il ne résout pas le problème que les attributs peuvent être accessibles via des [0]
etc., mais au moins il est beaucoup plus court et fournit l'avantage supplémentaire d'être compatible avec pickle
et copy
.
namedtuple
crée un type similaire à ce que j'ai décrit dans , cette réponse , c'est-à-dire dérivée de tuple
et utilisant __slots__
. Il est disponible en Python 2.6 ou supérieur.
la façon la plus facile de faire ceci est d'utiliser __slots__
:
class A(object):
__slots__ = []
les Instances de A
sont immuables maintenant, puisque vous ne pouvez pas leur attribuer d'attributs.
si vous voulez que les instances de classe contiennent des données, vous pouvez combiner cela avec dériver de 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
Modifier : Si vous voulez vous débarrasser de l'indexation, vous pouvez remplacer __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
notez que vous ne pouvez pas utiliser operator.itemgetter
pour les propriétés dans ce cas, car cela se baserait sur Point.__getitem__()
au lieu de tuple.__getitem__()
. De plus, cela n'empêchera pas l'utilisation de tuple.__getitem__(p, 0)
, mais je ne peux pas imaginer comment cela devrait constituer un problème.
Je ne pense pas que la" bonne " façon de créer un objet immuable est d'écrire une extension C. Python s'appuie généralement sur les responsables de la mise en œuvre de la bibliothèque et les utilisateurs de la bibliothèque étant adultes consentants , et au lieu de vraiment imposer une interface, l'interface devrait être clairement indiquée dans la documentation. C'est pourquoi je ne considère pas la possibilité de contourner un outrepassé __setattr__()
en appelant object.__setattr__()
un problème. Si quelqu'un fait ça, c'est à ses risques et périls.
..comment le faire "correctement" en C..
vous pouvez utiliser Cython pour créer un type d'extension pour 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
il fonctionne à la fois Python 2.x et 3.
Essais
# 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'
si vous ne vous opposez pas à un support d'indexation, alors collections.namedtuple
suggéré par @Sven Marnach est préférable:
Immutable = collections.namedtuple("Immutable", "a b")
une autre idée serait de rejeter complètement __setattr__
et d'utiliser object.__setattr__
dans le constructeur:
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
bien sûr , vous pouvez utiliser object.__setattr__(p, "x", 3)
pour modifier une Point
instance p
, mais votre implémentation originale souffre du même problème (essayez tuple.__setattr__(i, "x", 42)
sur une instance Immutable
).
vous pouvez appliquer la même astuce dans votre mise en œuvre originale: se débarrasser de __getitem__()
, et utiliser tuple.__getitem__()
dans vos fonctions de propriété.
vous pouvez créer un décorateur @immutable
qui remplace __setattr__
et changer le __slots__
à une liste vide, puis décorer la méthode __init__
avec elle.
modifier: comme L'a noté L'OP, changer l'attribut __slots__
empêche seulement la la création de nouveaux attributs , pas la modification.
Edit2: Voici une implémentation:
Edit 3: Utiliser __slots__
casse ce code, parce que si arrête la création de l'objet __dict__
. Je suis à la recherche d'une alternative.
Edit 4: Eh bien, c'est tout. C'est un but hackish, mais fonctionne comme un exercice: -)
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
Je ne pense pas que c'est tout à fait possible sauf en utilisant un tuple ou un tuple nommé. Quoi qu'il en soit, si vous outrepassez __setattr__()
, l'utilisateur peut toujours le contourner en appelant object.__setattr__()
directement. Toute solution qui dépend de __setattr__
est garanti de ne pas travailler.
ce qui suit est à peu près le plus proche que vous pouvez obtenir sans utiliser une sorte de 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__
mais il casse si vous essayez assez fort:
>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2
mais L'utilisation de namedtuple
par Sven est vraiment immuable.
mise à Jour
depuis que la question a été mise à jour pour demander comment le faire correctement en C, voici ma réponse sur la façon de le faire correctement en Cython:
Première 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)
et un setup.py
pour le compiler (en utilisant la commande 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
)
puis essayer:
>>> 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)
>>>
en plus des excellentes autres réponses que j'aime ajouter une méthode pour python 3.4 (ou peut-être 3.3). Cette réponse s'appuie sur plusieurs réponses antérieures à cette question.
en Python 3.4, vous pouvez utiliser les propriétés sans setters pour créer des membres de classe qui ne peuvent pas être modifiés. (Dans les versions précédentes, il était possible d'attribuer des propriétés sans setter.)
class A:
__slots__=['_A__a']
def __init__(self, aValue):
self.__a=aValue
@property
def a(self):
return self.__a
Vous pouvez l'utiliser comme ceci:
instance=A("constant")
print (instance.a)
qui imprimera "constant"
mais appeler instance.a=10
causera:
AttributeError: can't set attribute
explication: les propriétés sans setters sont une caractéristique très récente de python 3.4 (et je pense 3.3). Si vous essayez d'attribuer une telle propriété, une Erreur sera générée.
En utilisant des slots, Je limite les membervariables à __A_a
(qui est __a
).
problème: assigner à _A__a
est toujours possible ( instance._A__a=2
). Mais si vous affecter à une variable privée, c'est votre propre faute...
Cette réponse parmi d'autres, cependant, décourage l'utilisation de __slots__
. Utiliser d'autres moyens pour empêcher la création d'attributs pourrait être préférable.
j'ai créé des classes immuables en remplaçant __setattr__
, et en permettant l'ensemble si l'appelant est __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)
ce n'est pas tout à fait suffisant, car il permet à n'importe qui ___init__
de changer l'objet, mais vous obtenez l'idée.
si vous êtes intéressé par les objets avec un comportement, alors l'exemple est presque votre solution.
comme décrit au bas de l'exemple documentation , vous pouvez dériver votre propre classe de namedtuple; et puis, vous pouvez ajouter le comportement que vous voulez.
par exemple (code repris directement de la "documentation 151980920"):
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)
ce sera résultat:
Point: x= 3.000 y= 4.000 hypot= 5.000
Point: x=14.000 y= 0.714 hypot=14.018
cette approche fonctionne pour Python 3 et python 2.7 (testé sur IronPython aussi bien).
Le seul inconvénient est que l'arbre de l'héritage est un peu bizarre, mais ce n'est pas quelque chose que vous jouez habituellement avec.
j'en avais besoin il y a peu de temps et j'ai décidé de faire un paquet Python pour ça. La version initiale est sur PyPI maintenant:
$ pip install immutable
à utiliser:
>>> from immutable import ImmutableFactory
>>> MyImmutable = ImmitableFactory.create(prop1=1, prop2=2, prop3=3)
>>> MyImmutable.prop1
1
Plein de docs ici: https://github.com/theengineear/immutable
espérons qu'il aide, il enroule une question de nom comme il a été discuté, mais rend l'instanciation beaucoup plus simple.
cette façon ne s'arrête pas object.__setattr__
de travailler, mais je l'ai toujours trouvé utile:
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)
vous pouvez avoir besoin d'annuler plus de choses (comme __setitem__
), selon le cas d'utilisation.
Voici une élégante solution:
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))
héritez de cette classe, initialisez vos champs dans le constructeur, et tout est prêt.
à partir de python 3.7, vous pouvez utiliser le @dataclass
declarator dans votre classe et il sera immuable comme une structure! Bien que, il peut ou ne peut pas ajouter un __hash__()
méthode à votre classe. Citation:
hash () est utilisé par built-in hash(), et lorsque des objets sont ajoutés à des collections hachées telles que des dictionnaires et des ensembles. Avoir un hash () implique que les instances de la classe sont immuables. La mutabilité est une propriété compliquée qui dépend de l'intention du programmeur, de l'existence et du comportement de eq (), et des valeurs de l'eq et des drapeaux gelés dans le décorateur dataclass ().
par défaut, dataclass() n'ajoutera pas implicitement une méthode hash () à moins qu'il ne soit pas dangereux de le faire. Elle n'ajoutera ni ne modifiera pas non plus un hash "1519120920 explicitement défini." () méthode. Définir l'attribut de classe hash = aucun n'a de signification spécifique pour Python, comme décrit dans la documentation hash ().
si hash () n'est pas défini explicitement, ou si elle est définie à None, alors dataclass() peut ajouter une méthode implicite hash (). Bien que non recommandé, vous pouvez forcer dataclass () à créer une méthode hash () avec unsafe_hash=True. Ce pourrait être le cas si votre classe est logiquement immuable mais peut néanmoins être muté. Il s'agit d'un cas d'utilisation spécialisée qui doit être examiné attentivement.
Voici l'exemple sur les docs liés:
@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
qui héritent de la classe Immutable
suivante sont immuables, comme le sont leurs instances, après l'exécution de leur méthode __init__
. Puisque c'est python pur, comme d'autres l'ont souligné, il n'y a rien qui empêche quelqu'un d'utiliser les méthodes spéciales de mutation de la base object
et type
, mais c'est suffisant pour empêcher quelqu'un de muter une classe/instance par accident.
il fonctionne en détournant le processus de création de classe avec une métaclasse.
"""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__')
Le tiers attr
module fournit cette fonctionnalité .
$ 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
implémente les classes gelées en remplaçant __setattr__
et a un impact mineur sur les performances à chaque instant d'instanciation, selon la documentation.
si vous avez l'habitude d'utiliser des classes comme types de données, attr
peut être particulièrement utile car il prend soin du boilerplate pour vous (mais ne fait pas de la magie). En particulier, il écrit neuf méthodes dunder (__X__) pour vous (sauf si vous désactivez l'une d'elles), y compris repr, init, hash et toutes les fonctions de comparaison.
attr
fournit également un aide pour __slots__
.
une autre approche consiste à créer un wrapper qui rend une instance immuable.
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)
ceci est utile dans les situations où seulement quelques instances doivent être immuables (comme les arguments par défaut des appels de fonction).
peut également être utilisé dans des usines immuables comme:
@classmethod
def immutable_factory(cls, *args, **kwargs):
return Immutable(cls.__init__(*args, **kwargs))
protège aussi de object.__setattr__
, mais peut tomber à d'autres trucs en raison de la nature dynamique de Python.
j'ai utilisé la même idée Qu'Alex: une méta-classe et un "marqueur init", mais en combinaison avec la sur-écriture _ _ 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)
Note: j'appelle directement la méta-classe pour qu'elle fonctionne à la fois pour Python 2.x et 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.
il fonctionne aussi avec des fentes ...:
>>> 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.
... et l'héritage multiple:
>>> 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.
noter, cependant, que les attributs mutables restent pour être mutable:
>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
une chose qui n'est pas vraiment incluse ici est l'immuabilité totale... pas seulement l'objet parent, mais tous les enfants. les tuples / frozensets peuvent être immuables par exemple, mais les objets dont ils font partie peuvent ne pas l'être. Voici une petite version (incomplète) qui fait un travail décent de renforcement de l'immutabilité tout le chemin vers le bas:
# 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)
vous pouvez simplement annuler setAttr dans la déclaration finale d'init. Alors vous pouvez construire mais pas changer. Évidemment, vous pouvez toujours annuler par objet usint. setAttr mais dans la pratique la plupart des langues ont une forme de réflexion si immutable est toujours une abstraction qui fuit. Immutabilité est plus sur la prévention des clients de violer accidentellement le contrat d'un objet. J'utilise:
=============================
La solution originale proposée était incorrecte, elle a été mise à jour sur la base des commentaires en utilisant la solution de ici
la solution originale est erronée d'une manière intéressante, il est donc inclus au bas.
===============================
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
sortie:
1
2
Attempted To Modify Immutable Object
1
2
======================================
Mise En Œuvre Initiale:
il a été souligné dans les commentaires, à juste titre, que cela ne fonctionne pas en fait, car il empêche la création de plus d'un objet car vous supplantez la méthode setattr de classe, ce qui signifie qu'une seconde ne peut pas être créée en tant que soi.a = échouera lors de la deuxième initialisation.
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")
la solution de base ci-dessous traite du scénario suivant:
-
__init__()
peut être écrit en utilisant les attributs comme d'habitude. - après que L'objet est gelé pour attributs changements seulement:
l'idée est de remplacer la méthode __setattr__
et de remplacer son implémentation chaque fois que le statut gelé objet est changé.
donc nous avons besoin d'un peu méthode ( _freeze
) qui stocke ces deux implémentations et passe entre elles sur demande.
ce mécanisme peut être mis en œuvre à l'intérieur de la classe d'utilisateur ou hérité d'une classe spéciale Freezer
comme indiqué ci-dessous:
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()