web-dev-qa-db-fra.com

Comment faire un objet immuable en Python?

Bien que je n’aie jamais eu besoin de cela, je me suis rendu compte que faire un objet immuable en Python pouvait être un peu délicat. Vous ne pouvez pas simplement remplacer __setattr__ , car vous ne pourrez même pas définir d'attributs dans le __init__ . Sous-classer un tuple est une astuce qui fonctionne:

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 vous avez alors accès aux variables a et b via self[0] et self[1], ce qui est agaçant.

Est-ce possible en Pure Python? Sinon, comment le ferais-je avec une extension C?

(Les réponses qui ne fonctionnent que dans Python 3 sont acceptables).

Mettre à jour: 

Donc, sous-classer Tuple est le moyen de le faire en Pure Python, ce qui fonctionne bien, sauf en ce qui concerne la possibilité supplémentaire d'accéder aux données par [0], [1], etc. Je suppose que cela serait assez simple, en ne mettant en œuvre aucune geititem ou setattribute, etc. Mais au lieu de le faire moi-même, j'offre une prime pour cela, car je suis paresseux. :)

151
Lennart Regebro

Encore une autre solution à laquelle je viens de penser: le moyen le plus simple d’obtenir le même comportement que votre code original est

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

Cela ne résout pas le problème de l'accès aux attributs via [0], etc., mais au moins, il est considérablement plus court et offre 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.

95
Sven Marnach

Pour ce faire, le plus simple consiste à utiliser __slots__:

class A(object):
    __slots__ = []

Les instances de A sont immuables, car vous ne pouvez leur attribuer aucun attribut.

Si vous souhaitez que les instances de la classe contiennent des données, vous pouvez combiner cela avec la dérivation 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

Edit: Si vous souhaitez 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 reposerait sur Point.__getitem__() au lieu de Tuple.__getitem__(). De plus, cela n’empêchera pas l’utilisation de Tuple.__getitem__(p, 0), mais j’imagine mal en quoi cela devrait poser problème.

Je ne pense pas que la "bonne" façon de créer un objet immuable consiste à écrire une extension en C. Python repose généralement sur les implémenteurs et les utilisateurs de bibliothèque qui sont adultes consentants , et au lieu d'imposer réellement une interface, l'interface doit être clairement définie dans la documentation. C'est pourquoi je ne considère pas la possibilité de contourner une __setattr__() surchargée en appelant object.__setattr__() comme un problème. Si quelqu'un fait cela, c'est à ses risques et périls.

70
Sven Marnach

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

Cela fonctionne à la fois Python 2.x et 3.

Des tests

# 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 le support d'indexation ne vous dérange pas, alors collections.namedtuple suggéré par @Sven Marnach est préférable

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

Une autre idée serait d’interdire 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 instance Point, p, mais votre implémentation d'origine présente le même problème (essayez Tuple.__setattr__(i, "x", 42) sur une instance Immutable.

Vous pouvez appliquer la même astuce à votre implémentation initiale: supprimez __getitem__() et utilisez Tuple.__getitem__() dans vos fonctions de propriété.

36
Sven Marnach

Vous pouvez créer un décorateur @immutable qui substitue soit le __setattr__et changer le __slots__ à une liste vide, puis décorer la méthode __init__ avec celle-ci.

Éditer: comme l’a noté le PO, modifier l’attribut __slots__ empêche uniquement le création de nouveaux attributs, pas la modification.

Edit2: Voici une implémentation:

Edit3: Utiliser __slots__ rompt ce code, car if arrête la création du __dict__ de l'objet. Je cherche une alternative.

Edit4: Eh bien, c'est tout. C'est un peu rigolo, 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
17
PaoloVictor

Je ne pense pas que ce soit tout à fait possible sauf en utilisant soit un tuple, soit un nom nommé. Quoi qu'il en soit, si vous remplacez __setattr__(), l'utilisateur peut toujours le contourner en appelant directement object.__setattr__(). Toute solution qui dépend de __setattr__ est garantie de ne pas fonctionner.

Ce qui suit concerne 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 ça casse si vous essayez assez fort:

>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2

mais l'utilisation par Sven de namedtuple est véritablement immuable.

Mettre à jour

Depuis que la question a été mise à jour pour demander comment le faire correctement en C, voici ma réponse pour le faire correctement en Cython:

Premier 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 pour l'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)
>>>      
9
Duncan

En plus des excellentes autres réponses, j'aime bien ajouter une méthode pour python 3.4 (ou peut-être 3.3). Cette réponse repose sur plusieurs réponses précédentes à cette question.

En python 3.4, vous pouvez utiliser les propriétés sans les installateurs pour créer des membres de classe qui ne peuvent pas être modifiés. (Dans les versions précédentes, il était possible d'affecter des propriétés sans paramètre.)

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 entraînera:

AttributeError: can't set attribute

Explication: les propriétés sans setters sont une fonctionnalité très récente de Python 3.4 (et je pense que 3.3). Si vous essayez d'attribuer à une telle propriété, une erreur sera générée . En utilisant les emplacements, je limite les variables membre à __A_a (qui est __a).

Problème: L’attribution à _A__a est toujours possible (instance._A__a=2). Mais si vous assignez une variable privée, c'est votre faute à vous ...

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.

4
TheEspinosa

Voici une solution élégante:

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 vous êtes tous ensemble.

3
Alexander Ryzhov

Si vous êtes intéressé par les objets avec comportement, alors namedtuple est presque votre solution. 

Comme décrit au bas de la commande namedtuple documentation , vous pouvez dériver votre propre classe à partir de namedtuple; et ensuite, vous pouvez ajouter le comportement que vous souhaitez.

Par exemple (code extrait directement de documentation ):

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)

Cela se traduira par:

Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

Cette approche fonctionne à la fois pour Python 3 et Python 2.7 (également testé sur IronPython).
Le seul inconvénient est que l’arbre de l’héritage est un peu bizarre; mais ce n'est pas quelque chose avec lequel vous jouez habituellement.

3
rob

J'ai créé des classes immuables en surchargeant __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 encore assez, car cela permet à ___init__ de modifier l'objet, mais vous en avez l'idée.

3
Ned Batchelder

A partir de Python 3.7, vous pouvez utiliser le @dataclass decorator de votre classe et ce sera immuable comme un struct! Cependant, il est possible que la méthode __hash__() soit ou non ajoutée à votre classe. Citation:

hash () est utilisé par hash () intégré, 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é complexe qui dépend de l’intention du programmeur, de l’existence et du comportement de eq () et des valeurs des indicateurs eq et frozen dans le décorateur dataclass ().

Par défaut, dataclass () n’ajoutera pas implicitement une méthode hash () à moins que cela ne soit sans danger. Il ne va pas non plus ajouter ou modifier une méthode hash () existante explicitement définie. La définition de l'attribut de classe hash = None a une signification particulière pour Python, comme décrit dans la documentation hash ().

Si hash () n'est pas défini explicitement ou s'il est défini sur None, alors dataclass () peut ajouter une méthode implicite hash (). Bien que cela ne soit pas recommandé, vous pouvez forcer dataclass () à créer une méthode hash () avec unsafe_hash = True. Cela pourrait être le cas si votre classe est logiquement immuable mais peut néanmoins être mutée. Ceci est un cas d'utilisation spécialisé et doit être considéré avec soin.

Voici l'exemple des documents liés ci-dessus: 

@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
2
user10018213

Cette méthode n'empêche pas object.__setattr__ de fonctionner, 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 devrez peut-être remplacer d'autres éléments (tels que __setitem__) en fonction du cas d'utilisation.

2
dangirsh

J'en avais besoin il y a un moment et j'ai décidé de créer un paquet Python. La version initiale est maintenant sur PyPI:

$ pip install immutable

Utiliser:

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

Documents complets ici: https://github.com/theengineear/immutable

J'espère que cela vous aidera, cela englobera un nom nommé comme il a été discuté, mais rend l'instanciation beaucoup plus simple.

2
theengineear

Vous pouvez remplacer setattr tout en utilisant init pour définir la variable. Vous utiliseriez super class setattr . voici le code.

 classe Immutable: 
 __slots__ = ('a', 'b') 
 def __init __ (self, a, b): 
 super () .__ setattr __ ('a', a) 
 super () .__ setattr __ ('b', b) 

 def __str __ (self): 
 return "" .format (self.a, self.b) 

 def __setattr __ (self, * ignored): 
 soulève NotImplementedError 

 def __delattr __ (self, * ignored): 
 soulève NotImplementedError 
1
Shameer Ali

Les classes qui héritent de la classe Immutable suivante sont immuables, de même que leurs instances, une fois leur méthode __init__ terminée. Comme c’est du pur python, comme d’autres l'ont déjà souligné, rien n'empêche quelqu'un d'utiliser les méthodes spéciales mutantes des bases object et type, mais cela suffit pour empêcher qui que ce soit de muter une classe/instance par accident.

Cela 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
    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__')
1
Alex Coventry

Le tiers attr module fournit cette fonctionnalité .

Edit: python 3.7 a adopté cette idée dans la bibliothèque stdlib avec @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 implémente les classes gelées en surchargeant __setattr__ et a un impact mineur sur les performances à chaque heure d'instanciation, selon la documentation.

Si vous avez l'habitude d'utiliser les classes comme types de données, attr peut être particulièrement utile car il s'occupe du standard pour vous (mais ne fait pas de magie). En particulier, il écrit neuf méthodes dunder (__X__) pour vous (sauf si vous en désactivez une), y compris les fonctions repr, init, hash et toutes les fonctions de comparaison.

attr fournit également un helper pour __slots__ .

1
cmc

J'ai utilisé la même idée qu'Alex: une méta-classe et un "marqueur d'init", mais en combinaison avec l'écriture de substitution

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

Remarque: J'appelle directement la méta-classe pour qu'elle fonctionne à la fois avec 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.

Cela fonctionne aussi avec les machines à sous ...:

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

Notez cependant que les attributs modifiables restent mutables:

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
0
Michael Amrhein

Une chose qui n'est pas vraiment incluse ici est l'immutabilité totale… pas seulement l'objet parent, mais aussi 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 en appliquant l’immutabilité jusqu’au bout:

# 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)
0
Corley Brigman

Vous pouvez simplement remplacer setAttr dans la déclaration finale d'init. Ensuite, vous pouvez construire mais pas changer. Bien entendu, vous pouvez toujours remplacer l'objet usint .setAttr, mais en pratique, la plupart des langages ont une forme de réflexion de sorte que l'immutablilité est toujours une abstraction qui fuit. L’immuabilité vise plutôt à empêcher les clients de violer accidentellement le contrat d’un objet. J'utilise:

=============================

La solution originale proposée était incorrecte, elle a été mise à jour en fonction des commentaires utilisant la solution de ici

La solution initiale est fausse de manière intéressante, elle est donc incluse en 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 __== "__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 originale:

Cela a été souligné correctement dans les commentaires, car cela empêche la création de plusieurs objets, car vous surchargez la méthode de classe setattr, ce qui signifie qu'un deuxième ne peut pas être créé de manière autonome. échouer lors de la seconde 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")
0
phil_20686

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ù seules certaines instances doivent être immuables (comme les arguments par défaut des appels de fonction).

Peut aussi être utilisé dans des usines immuables comme:

@classmethod
def immutable_factory(cls, *args, **kwargs):
    return Immutable(cls.__init__(*args, **kwargs))

Protège également de object.__setattr__, mais peut être remplacé par d'autres astuces en raison de la nature dynamique de Python.

0
Mark Horvath

La solution de base ci-dessous résout le scénario suivant:

  • __init__() peut être écrit en accédant aux attributs comme d’habitude. 
  • APRES que l’OBJET soit gelé pour attributs change uniquement:

L'idée est de surcharger la méthode __setattr__ et de remplacer sa mise en œuvre chaque fois que l'état gelé de l'objet est modifié.

Nous avons donc besoin d’une méthode (_freeze) qui stocke ces deux implémentations et permute entre elles lorsque cela est demandé.

Ce mécanisme peut être implémenté au sein de la classe d'utilisateurs ou hérité d'une classe Freezer spéciale, 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()
0
ipap