Comment créer un objet immuable en Python?


189

Bien que je n'ai jamais eu besoin de cela, cela m'a juste frappé que créer un objet immuable en Python pouvait être légèrement délicat. Vous ne pouvez pas simplement remplacer __setattr__, car vous ne pouvez 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 alors vous avez accès à aet les bvariables à travers self[0]et self[1], ce qui est gênant.

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

Mise à jour:

Donc, sous-classer le tuple est le moyen de le faire en Pure Python, qui fonctionne bien sauf pour la possibilité supplémentaire d'accéder aux données par [0], [1]etc. Donc, pour compléter cette question, tout ce qui manque est comment le faire "correctement" en C, ce qui Je suppose que ce serait assez simple, en n'implémentant aucun geititemou setattribute, etc. Mais au lieu de le faire moi-même, j'offre une prime pour cela, parce que je suis paresseux. :)


2
Votre code ne facilite-t-il pas l'accès aux attributs via .aet .b? C'est pour cela que les propriétés semblent exister après tout.
Sven Marnach

1
@Sven Marnach: Oui, mais [0] et [1] fonctionnent toujours, et pourquoi le feraient-ils? Je n'en veux pas. :) Peut-être que l'idée d'un objet immuable avec des attributs est absurde? :-)
Lennart Regebro

2
Juste une autre note: NotImplementedest uniquement conçue comme une valeur de retour pour les comparaisons riches. Une valeur de retour pour __setatt__()est de toute façon plutôt inutile, car vous ne la verrez généralement pas du tout. Le code comme immutable.x = 42ne fera rien en silence. Vous devriez lever un à la TypeErrorplace.
Sven Marnach

1
@Sven Marnach: OK, j'ai été surpris, car je pensais que vous pouviez lever NotImplemented dans cette situation, mais cela donne une erreur étrange. Alors je l'ai retourné à la place, et il a semblé fonctionner. TypeError avait un sens évident une fois que je vous ai vu l'utiliser.
Lennart Regebro

1
@Lennart: Vous pouvez augmenter NotImplementedError, mais TypeErrorc'est ce qu'un tuple déclenche si vous essayez de le modifier.
Sven Marnach

Réponses:


120

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

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

Cela ne résout pas le problème d'accès aux attributs via [0]etc., mais au moins est considérablement plus court et offre l'avantage supplémentaire d'être compatible avec pickleet copy.

namedtuplecrée un type similaire à ce que j'ai décrit dans cette réponse , c'est-à-dire dérivé de tupleet utilisant __slots__. Il est disponible en Python 2.6 ou supérieur.


7
L'avantage de cette variante par rapport à l'analogue manuscrite (même sur Python 2.5 (l'utilisation d'un verboseparamètre pour namedtuplele code est facilement générée)) est la seule interface / implémentation d'un namedtupleest préférable à des dizaines d' interfaces / implémentations manuscrites très légèrement différentes qui faire presque la même chose.
jfs

2
OK, vous obtenez la "meilleure réponse", car c'est la manière la plus simple de le faire. Sebastian obtient la prime pour avoir donné une courte implémentation Cython. À votre santé!
Lennart Regebro

1
Une autre caractéristique des objets immuables est que lorsque vous les passez en tant que paramètre via une fonction, ils sont copiés par valeur, plutôt que par une autre référence. Les namedtuples seraient-ils copiés par valeur lorsqu'ils sont passés à travers des fonctions?
hlin117 du

4
@ hlin117: Chaque paramètre est passé en tant que référence à un objet en Python, qu'il soit mutable ou immuable. Pour les objets immuables, il serait particulièrement inutile de faire une copie - puisque vous ne pouvez pas changer l'objet de toute façon, vous pouvez tout aussi bien passer une référence à l'objet d'origine.
Sven Marnach

Pouvez-vous utiliser namedtuple en interne dans la classe au lieu d'instancier l'objet en externe? Je suis très novice en python, mais l'avantage de votre autre réponse est que je peux demander à une classe de masquer les détails et d'avoir également le pouvoir de choses comme des paramètres facultatifs. Si je regarde seulement cette réponse, il semble que j'ai besoin de tout ce qui utilise ma classe instancie des tuples nommés. Merci pour les deux réponses.
Asaf

82

Le moyen le plus simple de le faire est d'utiliser __slots__:

class A(object):
    __slots__ = []

Les instances de Asont désormais immuables, car vous ne pouvez pas leur définir d'attributs.

Si vous souhaitez 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

Edit : Si vous souhaitez vous débarrasser de l'indexation non plus, 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.itemgetterpour les propriétés dans ce cas, car cela reposerait sur à la Point.__getitem__()place de tuple.__getitem__(). De plus, cela n'empêchera pas l'utilisation de tuple.__getitem__(p, 0), mais je peux difficilement 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 repose généralement sur le fait que les implémenteurs de bibliothèques et les utilisateurs de bibliothèques sont des adultes consentants , et au lieu d'appliquer vraiment une interface, l'interface doit être clairement indiquée dans la documentation. C'est pourquoi je ne considère pas la possibilité de contourner un overridden __setattr__()en appelant object.__setattr__()un problème. Si quelqu'un fait cela, c'est à ses risques et périls.


1
Ne serait-il pas préférable d'utiliser un tupleici __slots__ = (), plutôt que __slots__ = []? (Juste clarification)
user225312

1
@sukhbir: Je pense que cela n'a aucune importance. Pourquoi préféreriez-vous un tuple?
Sven Marnach le

1
@Sven: Je suis d'accord que cela n'a pas d'importance (sauf la partie vitesse, que nous pouvons ignorer), mais j'y ai pensé de cette façon: cela __slots__ne va pas être changé, non? Son but est d'identifier pour une fois quels attributs peuvent être définis. Cela ne tuplesemble- t-il donc pas un choix très naturel dans un tel cas?
user225312

5
Mais avec un vide, __slots__je ne peux définir aucun attribut. Et si j'ai __slots__ = ('a', 'b')alors les attributs a et b sont toujours mutables.
Lennart Regebro

Mais votre solution est meilleure que la priorité __setattr__, c'est donc une amélioration par rapport à la mienne. +1 :)
Lennart Regebro

52

..comment le faire "correctement" dans 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 cela ne vous dérange pas de prendre en charge l'indexation, alors collections.namedtuplesuggéré par @Sven Marnach est préférable :

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

@Lennart: Les instances de namedtuple(ou plus précisément du type retourné par la fonction namedtuple()) sont immuables. Absolument.
Sven Marnach

@Lennart Regebro: namedtuplepasse tous les tests (sauf le support d'indexation). Quelle exigence ai-je manqué?
jfs

Oui, vous avez raison, j'ai créé un type de multiplet nommé, je l'ai instancié, puis j'ai fait le test sur le type au lieu de l'instance. Il h. :-)
Lennart Regebro

puis-je demander pourquoi aurait-on besoin de références faibles ici?
McSinyx

1
@McSinyx: sinon, les objets ne peuvent pas être utilisés dans les collections de lowref. Qu'est-ce que __weakref__Python exactement ?
jfs

40

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 Pointinstance p, mais votre implémentation d'origine souffre du même problème (essayez tuple.__setattr__(i, "x", 42)sur une Immutableinstance).

Vous pouvez appliquer la même astuce dans votre implémentation d'origine: vous débarrasser __getitem__()et utiliser tuple.__getitem__()dans vos fonctions de propriété.


12
Je ne me soucierais pas que quelqu'un modifie délibérément l'objet en utilisant une superclasse » __setattr__, car le but n'est pas d'être infaillible. Il s'agit de préciser qu'il ne doit pas être modifié et d'empêcher toute modification par erreur.
zvone le

18

Vous pouvez créer un @immutabledécorateur qui remplace le __setattr__ et le remplace __slots__par une liste vide, puis décore la __init__méthode avec.

Edit: Comme l'a noté l'OP, la modification de l' __slots__attribut empêche uniquement la création de nouveaux attributs , pas la modification.

Edit2: Voici une implémentation:

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

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

1
Faire un décorateur (de classe?) Ou une métaclasse à partir de la solution est en effet une bonne idée, mais la question est de savoir quelle est la solution. :)
Lennart Regebro

3
object.__setattr__()le casse stackoverflow.com/questions/4828080/…
jfs

En effet. J'ai juste continué comme exercice sur les décorateurs.
PaoloVictor

17

Utilisation d'une classe de données gelée

Pour Python 3.7+, vous pouvez utiliser une classe de données avec une frozen=Trueoption , ce qui est un moyen très pythonique et maintenable de faire ce que vous voulez.

Cela ressemblerait à quelque chose comme ça:

from dataclasses import dataclass

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

Comme l' indication de type est requise pour les champs des classes de données, j'ai utilisé Any du typingmodule .

Raisons de NE PAS utiliser un Namedtuple

Avant Python 3.7, il était fréquent de voir des objets nommés utilisés comme des objets immuables. Cela peut être délicat à bien des égards, l'un d'eux est que la __eq__méthode entre les objets nommés ne prend pas en compte les classes des objets. Par exemple:

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

Comme vous le voyez, même si les types de obj1et obj2sont différents, même si les noms de leurs champs sont différents, obj1 == obj2donne toujours True. En effet, la __eq__méthode utilisée est celle du tuple, qui compare uniquement les valeurs des champs compte tenu de leur position. Cela peut être une énorme source d'erreurs, surtout si vous sous-classez ces classes.


10

Je ne pense pas que ce soit tout à fait possible, sauf en utilisant un tuple ou un namedtuple. Quoi qu'il en soit, si vous remplacez, __setattr__()l'utilisateur peut toujours le contourner en appelant object.__setattr__()directement. Toute solution qui en dépend __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 de Sven namedtupleest véritablement immuable.

Mise à jour

Puisque 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èrement 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 a setup.pypour 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
)

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

Merci pour le code Cython, Cython est génial. L'implémentation de JF Sebastians avec la lecture seule est plus soignée et est arrivée la première, donc il obtient la prime.
Lennart Regebro

5

J'ai rendu des classes immuables en remplaçant __setattr__et en autorisant 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 tout à fait suffisant, car cela permet à n'importe qui ___init__de changer l'objet, mais vous voyez l'idée.


object.__setattr__()le casse stackoverflow.com/questions/4828080/…
jfs

3
Utiliser l'inspection de pile pour s'assurer que l'appelant __init__n'est pas très satisfaisant.
gb.

5

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

Dans python 3.4, vous pouvez utiliser des 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, l'attribution de propriétés sans setter était possible.)

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=10provoquera:

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 3.3). Si vous essayez d'attribuer à une telle propriété, une erreur sera générée. En utilisant les slots, je limite les membreservariables à __A_a(ce qui est __a).

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

Cette réponse, entre autres, décourage cependant l'utilisation de __slots__. L'utilisation d'autres méthodes pour empêcher la création d'attributs peut être préférable.


propertyest également disponible sur Python 2 (regardez le code dans la question elle-même). Il ne crée pas un objet immuable, essayez les tests de ma réponse par exemple, instance.b = 1crée un nouvel battribut.
jfs

Bien, la question est vraiment de savoir comment éviter de faire, A().b = "foo"c'est- à- dire ne pas autoriser la définition de nouveaux attributs.
Lennart Regebro

Les propriétés sans setter génèrent une erreur dans python 3.4 si vous essayez d'assigner à cette propriété. Dans les versions antérieures, le setter était généré implicitement.
Bernhard

@Lennart: Ma solution est une réponse à un sous-ensemble de cas d'utilisation d'objets immuables et un ajout aux réponses précédentes. Une des raisons pour lesquelles je pourrais souhaiter un objet immuable est de le rendre hachable, auquel cas ma solution pourrait fonctionner. Mais vous avez raison, ce n'est pas un objet immuable.
Bernhard

@ jf-sebastian: J'ai changé ma réponse pour utiliser des emplacements pour empêcher la création d'attributs. Ce qui est nouveau dans ma réponse par rapport aux autres réponses, c'est que j'utilise les propriétés de python3.4 pour éviter de changer les attributs existants. Bien que la même chose soit obtenue dans les réponses précédentes, mon code est plus court en raison du changement de comportement des propriétés.
Bernhard

5

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.


1
mais avec cette logique, il est possible d'attribuer de nouveaux attributs à l'objet
javed

3

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

Comme décrit au bas de la documentation namedtuple , vous pouvez dériver votre propre classe de namedtuple; puis, vous pouvez ajouter le comportement souhaité.

Par exemple (code extrait directement de la 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 (testée également sur IronPython).
Le seul inconvénient est que l'arbre d'héritage est un peu bizarre; mais ce n'est pas quelque chose avec lequel vous jouez habituellement.


1
Python 3.6+ prend en charge cela directement, en utilisantclass Point(typing.NamedTuple):
Elazar

3

Les classes qui héritent de la Immutableclasse suivante sont immuables, tout comme leurs instances, une fois leur __init__méthode terminée. Puisqu'il s'agit de python pur, comme d'autres l'ont souligné, rien n'empêche quelqu'un d'utiliser les méthodes spéciales de mutation de la base objectettype , mais cela suffit pour empêcher quiconque 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__ = 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__')

2

J'en avais besoin il y a quelque temps et j'ai décidé de créer un package Python pour cela. 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 aide, il encapsule un nommément comme cela a été discuté, mais rend l'instanciation beaucoup plus simple.


2

Cette méthode ne s'arrête pas object.__setattr__de fonctionner, mais je l'ai toujours trouvée 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 plus de choses (comme __setitem__) selon le cas d'utilisation.


J'ai trouvé quelque chose de similaire avant de voir cela, mais utilisé getattrpour que je puisse fournir une valeur par défaut pour frozen. Cela simplifiait un peu les choses. stackoverflow.com/a/22545808/5987
Mark Ransom

J'aime le mieux cette approche, mais vous n'avez pas besoin du __new__remplacement. À l'intérieur il __setattr__suffit de remplacer le conditionnel parif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi

En outre, il n'est pas nécessaire de geler la classe lors de la construction. Vous pouvez le figer à tout moment si vous fournissez une freeze()fonction. L'objet sera alors "geler une fois". Enfin, s'inquiéter object.__setattr__est ridicule, car «nous sommes tous des adultes ici».
Pete Cacioppi

2

À partir de Python 3.7, vous pouvez utiliser le @dataclassdécorateur dans votre classe et il sera immuable comme une structure! Cependant, cela peut ou non ajouter une __hash__()méthode à 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é compliquée qui dépend de l'intention du programmeur, de l'existence et du comportement de eq (), et des valeurs des indicateurs eq et gelés dans le décorateur dataclass ().

Par défaut, dataclass () n'ajoutera pas implicitement une méthode hash () à moins qu'il ne soit sûr de le faire. Il n'ajoutera ni ne modifiera non plus une méthode hash () définie explicitement . La définition de l'attribut de classe hash = None a une signification spécifique pour Python, comme décrit dans la documentation de 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 peut être le cas si votre classe est logiquement immuable mais peut néanmoins être mutée. Il s'agit d'un cas d'utilisation spécialisé et doit être examiné attentivement.

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

1
vous devez utiliser frozen, c'est-à-dire @dataclass(frozen=True), mais cela bloque essentiellement l'utilisation de __setattr__et __delattr__comme dans la plupart des autres réponses ici. Il le fait simplement d'une manière compatible avec les autres options des classes de données.
CS

2

Vous pouvez remplacer setattr et toujours utiliser init pour définir la variable. Vous utiliseriez la super classe setattr . voici le code.

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

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

    def __setattr __ (self, * ignoré):
        lever NotImplementedError

    def __delattr __ (self, * ignoré):
        lever NotImplementedError

Ou juste passau lieu deraise NotImplementedError
jonathan.scholbach

Ce n'est pas du tout une bonne idée de faire "passer" dans __setattr__ et __delattr__ dans ce cas. La raison simple est que si quelqu'un attribue une valeur à un champ / propriété, il s'attend naturellement à ce que le champ soit modifié. Si vous voulez suivre le chemin de la "moindre surprise" (comme vous devriez), vous devez alors signaler une erreur. Mais je ne suis pas sûr que NotImplementedError soit le bon à lever. Je soulèverais quelque chose comme "Le champ / la propriété est immuable." erreur ... Je pense qu'une exception personnalisée devrait être levée.
darlove

1

Le attrmodule tiers fournit cette fonctionnalité .

Edit: python 3.7 a adopté cette idée dans la 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

attrimplémente les classes gelées en les 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, cela attrpeut être particulièrement utile car il s'occupe du passe-partout pour vous (mais ne fait aucune magie). En particulier, il écrit pour vous neuf méthodes dunder (__X__) (à moins que vous ne désactiviez l'une d'entre elles), y compris repr, init, hash et toutes les fonctions de comparaison.

attrfournit également une aide pour__slots__ .


1

Tout comme un dict

J'ai une bibliothèque open source dans laquelle je fais les choses de manière fonctionnelle , il est donc utile de déplacer des données dans un objet immuable. Cependant, je ne veux pas avoir à transformer mon objet de données pour que le client interagisse avec eux. Donc, je suis venu avec ceci - cela vous donne un objet comme un dict qui est immuable + quelques méthodes d'aide.

Crédit à Sven Marnach dans sa réponse à la mise en œuvre de base de restreindre la mise à jour de la propriété et la suppression.

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__

Méthodes d'assistance

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)

Exemples

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

1

Donc, j'écris respectivement de python 3:

I) à l'aide du décorateur de classe de données et définissez Frozen = True. nous pouvons créer des objets immuables en python.

pour ce besoin d'importer la classe de données à partir des classes de données lib et doit définir gelé = True

ex.

à partir de classes de données importer une classe de données

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

o / p:

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

Source: https://realpython.com/python-data-classes/


0

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 également être utilisé dans des usines immuables comme:

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

Protège également object.__setattr__, mais fallable à d' autres tours en raison de la nature dynamique de Python.


0

J'ai utilisé la même idée qu'Alex: une méta-classe et un "marqueur d'initialisation", mais en combinaison avec l'écrasement de __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)

Remarque: j'appelle la méta-classe directement 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.

Cela fonctionne aussi avec les slots ...:

>>> 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 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 mutables restent mutables:

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

0

Une chose qui n'est pas vraiment incluse ici est l'immuabilité 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 pour renforcer l'immuabilité tout en 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)

0

Vous pouvez simplement remplacer setAttr dans l'instruction finale de init. ALORS vous pouvez construire mais pas changer. Évidemment, vous pouvez toujours remplacer par l'objet usint. setAttr mais dans la pratique, la plupart des langages ont une forme de réflexion, donc l'immuabilité est toujours une abstraction qui fuit. L'immuabilité consiste davantage à 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 en utilisant la solution d' ici

La solution originale est erronée d'une 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 __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

Production :

1
2
Attempted To Modify Immutable Object
1
2

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

Mise en œuvre originale:

Il a été souligné dans les commentaires, correctement, que cela ne fonctionne pas en fait, car cela empêche la création de plus d'un objet lorsque vous remplacez la méthode de classe setattr, ce qui signifie qu'un second ne peut pas être créé en tant que self.a = will échouer à 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")

1
Cela ne fonctionnera pas: vous remplacez la méthode sur la classe , vous obtiendrez donc NotImplementedError dès que vous essayez de créer une deuxième instance.
slinkp

1
Si vous souhaitez poursuivre cette approche, notez qu'il est difficile de remplacer les méthodes spéciales au moment de l'exécution: consultez stackoverflow.com/a/16426447/137635 pour quelques solutions de contournement.
slinkp

0

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

  • __init__() peut être écrit en accédant aux attributs comme d'habitude.
  • APRÈS que l'OBJET soit gelé pour les changements d' attributs uniquement:

L'idée est de remplacer la __setattr__méthode et de remplacer son implémentation chaque fois que l'état figé de l'objet est modifié.

Nous avons donc besoin d'une méthode ( _freeze) qui stocke ces deux implémentations et bascule entre elles à la demande.

Ce mécanisme peut être implémenté à l'intérieur de la classe utilisateur ou hérité d'une Freezerclasse 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()
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.