209 votes

Comment rendre un objet immuable en Python ?

Bien que je n'en aie jamais eu besoin, je me suis dit que la création d'un objet immuable en Python pouvait être un peu délicate. Vous ne pouvez pas simplement surcharger __setattr__ parce qu'alors, vous ne pouvez même pas définir les attributs dans l'interface utilisateur. __init__ . Le sous-classement d'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 à la a y b les variables par self[0] y self[1] ce qui est ennuyeux.

Est-ce possible en Pure Python ? Si non, comment le faire avec une extension C ?

(Les réponses qui ne fonctionnent qu'en Python 3 sont acceptables).

Mise à jour :

À partir de Python 3.7, la solution consiste à utiliser l'attribut @dataclass décorateur, voir la réponse nouvellement acceptée.

2 votes

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

2 votes

@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 un non-sens :-)

0 votes

@Lennart : J'ai initialement lu "alors vous avez accès aux a et b" comme "vous devez accéder aux a et b", d'où mon commentaire.

127voto

Sven Marnach Points 133943

Encore une autre solution à laquelle je viens de penser : La façon la plus simple d'obtenir le même comportement que votre code original est de

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

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

namedtuple crée un type similaire à celui que j'ai décrit dans cette réponse c'est-à-dire dérivé de tuple et en utilisant __slots__ . Il est disponible en Python 2.6 ou supérieur.

7 votes

L'avantage de cette variante par rapport à l'analogique écrit à la main (même sur Python 2.5 (en utilisant verbose pour namedtuple le code est facilement généré)) est l'interface/implémentation unique d'un système de gestion de l'environnement. namedtuple est préférable à des dizaines très légèrement différentes interfaces/implémentations écrites à la main qui font presque la même chose.

2 votes

OK, vous obtenez la "meilleure réponse", car c'est la façon la plus simple de le faire. Sebastian reçoit la prime pour avoir donné une courte implémentation Cython. A la vôtre !

1 votes

Une autre caractéristique des objets immuables est que lorsque vous les passez en paramètre dans une fonction, ils sont copiés par valeur, plutôt qu'une autre référence. Est-ce que namedtuple sont copiés par valeur lorsqu'ils sont transmis par des fonctions ?

89voto

Sven Marnach Points 133943

La façon la plus simple de le faire est d'utiliser __slots__ :

class A(object):
    __slots__ = []

Instances de A sont désormais immuables, puisque vous ne pouvez pas leur attribuer d'attributs.

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

Modifier : Si vous voulez vous débarrasser de l'indexation également, 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, puisque 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 je ne vois pas comment cela pourrait constituer un problème.

Je ne pense pas que la "bonne" façon de créer un objet immuable soit d'écrire une extension C. Python s'appuie généralement sur le fait que les implémenteurs de bibliothèques et les utilisateurs de bibliothèques soient adultes consentants Au lieu d'imposer une interface, l'interface devrait être clairement indiquée dans la documentation. C'est la raison pour laquelle je ne considère pas la possibilité de contourner une surcharge __setattr__() en appelant object.__setattr__() un problème. Si quelqu'un fait ça, c'est à ses risques et périls.

1 votes

Ne serait-ce pas une meilleure idée d'utiliser une tuple ici, __slots__ = () plutôt que __slots__ = [] ? (Juste pour clarifier)

1 votes

@sukhbir : Je pense que cela n'a pas du tout d'importance. Pourquoi préférer un tuple ?

1 votes

@Sven : Je suis d'accord pour dire que ça n'aurait pas d'importance (sauf la partie vitesse, que nous pouvons ignorer), mais j'y ai pensé de cette façon : __slots__ ne va pas être modifié, n'est-ce pas ? Son but est d'identifier pour une fois les attributs qui peuvent être définis. Donc, est-ce qu'un tuple semble beaucoup naturel choix dans un tel cas ?

57voto

J.F. Sebastian Points 102961

..comment le faire "correctement" en C..

Vous pourriez 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 aussi bien avec Python 2.x que 3.

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

0 votes

@Lennart : Instances de namedtuple (ou plus précisément du type retourné par la fonction namedtuple() ) sont immuables. Définitivement.

0 votes

@Lennart Regebro : namedtuple passe tous les tests (sauf le support de l'indexation). Quelle exigence ai-je manqué ?

0 votes

Oui, vous avez raison, j'ai créé un type namedtuple, je l'ai instancié, puis j'ai fait le test sur le type au lieu de l'instance. Heh. :-)

42voto

Sven Marnach Points 133943

Une autre idée serait d'interdire complètement __setattr__ et 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 un Point instance p mais votre implémentation originale souffre du même problème (essayez tuple.__setattr__(i, "x", 42) sur un Immutable instance).

Vous pouvez appliquer la même astuce dans votre implémentation originale : se débarrasser de la fonction __getitem__() et utiliser tuple.__getitem__() dans les fonctions de votre propriété.

14 votes

Je ne me préoccuperais pas du fait que quelqu'un modifie délibérément l'objet en utilisant la "superclasse". __setattr__ car le but n'est pas d'être infaillible. Il s'agit d'indiquer clairement qu'il ne doit pas être modifié et d'empêcher toute modification par erreur.

18voto

PaoloVictor Points 1079

Vous pourriez créer un @immutable qui soit remplace le __setattr__ et changer le __slots__ à une liste vide, puis décorer le __init__ avec elle.

Edit : Comme l'OP l'a noté, changer le __slots__ n'empêche que l'attribut création de nouveaux attributs et non la modification.

Edit2 : Voici une implémentation :

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

Edit4 : Voilà, c'est tout. C'est un peu bricolé, mais ça marche comme 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 votes

Faire de la solution un décorateur (de classe ?) ou une métaclasse est effectivement une bonne idée, mais la question est de savoir quelle est la solution :)

3 votes

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

0 votes

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X