Как сделать неизменный объект в Python?


181

Хотя мне это никогда не было нужно, меня просто поразило, что создание неизменяемого объекта в Python может быть немного сложнее. Вы не можете просто переопределить __setattr__, потому что тогда вы не можете даже установить атрибуты в __init__. Подклассы кортежа - это трюк, который работает:

class Immutable(tuple):

    def __new__(cls, a, b):
        return tuple.__new__(cls, (a, b))

    @property
    def a(self):
        return self[0]

    @property
    def b(self):
        return self[1]

    def __str__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError

Но тогда у вас есть доступ к aи bпеременным через self[0]и self[1], что раздражает.

Возможно ли это в Pure Python? Если нет, то как бы я сделал это с расширением C?

(Ответы, которые работают только в Python 3, являются приемлемыми).

Обновить:

Так подклассы кортежа является способом сделать это в чистом Python, который работает хорошо для дополнительной возможности доступа к данным , за исключением элементов [0], и [1]т.д. Таким образом, чтобы закончить этот вопрос все , что не хватает на МЕТОДИЧЕСКОМ сделать это «правильно» в С, Я подозреваю, что было бы довольно просто, просто не реализовав ничего geititemили setattributeи т. Д. Но вместо того, чтобы делать это самостоятельно, я предлагаю вознаграждение за это, потому что я ленивый. :)


2
Разве ваш код не облегчает доступ к атрибутам через .aи .b? Это то, что свойства, кажется, существуют в конце концов.
Свен Марнах

1
@ Свен Марнах: Да, но [0] и [1] все еще работают, и с чего бы это? Я не хочу их. :) Может быть, идея неизменяемого объекта с атрибутами является бессмысленной? :-)
Леннарт Регебро

2
Еще одно примечание: NotImplementedподразумевается только как возвращаемое значение для богатых сравнений. Возвращаемое значение в __setatt__()любом случае довольно бессмысленно, так как обычно вы его вообще не увидите. Код вроде immutable.x = 42молча ничего не сделает. Вы должны поднять TypeErrorвместо.
Свен Марнач

1
@ Свен Марнах: Хорошо, я был удивлен, потому что я думал, что вы можете поднять NotImplemented в этой ситуации, но это дает странную ошибку. Таким образом я возвратил это вместо этого, и это, казалось, работало. TypeError имел очевидный смысл, как только я увидел, что вы использовали его.
Леннарт Регебро

1
@ Леннарт: Вы могли бы повысить NotImplementedError, но TypeErrorэто то, что поднимает кортеж, если вы пытаетесь изменить его.
Свен Марнах

Ответы:


115

Еще одно решение, о котором я только что подумал: самый простой способ получить то же поведение, что и ваш исходный код, это

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

Это не решает проблему доступа к атрибутам через [0]и т. Д., Но, по крайней мере, она значительно короче и обеспечивает дополнительное преимущество совместимости с pickleи copy.

namedtupleсоздает тип, подобный тому, что я описал в этом ответе , т.е. происходит от tupleи использует __slots__. Он доступен в Python 2.6 или выше.


7
Преимущество этого варианта по сравнению с рукописным аналогом (даже в Python 2.5 (использование verboseпараметра для namedtupleкода легко генерируется)) заключается в том, что единый интерфейс / реализация namedtupleпредпочтительнее десятков очень немного отличающихся рукописных интерфейсов / реализаций, которые сделать почти то же самое.
JFS

2
ОК, вы получите «лучший ответ», потому что это самый простой способ сделать это. Себастьян получает награду за короткую реализацию Cython. Ура!
Леннарт Регебро

1
Другой характеристикой неизменяемых объектов является то, что когда вы передаете их как параметр через функцию, они копируются по значению, а не по другой ссылке. Будет ли namedtuples копироваться по значению при прохождении через функции?
hlin117

4
@ hlin117: каждый параметр передается как ссылка на объект в Python, независимо от того, является ли он изменяемым или неизменным. Для неизменяемых объектов было бы особенно бессмысленно делать копию - поскольку вы все равно не можете изменить объект, вы также можете передать ссылку на исходный объект.
Свен Марнах

Можете ли вы использовать namedtuple внутри класса вместо создания экземпляра объекта извне? Я очень плохо знаком с Python, но преимущество вашего другого ответа в том, что я могу позволить классу скрывать детали, а также обладать мощью таких вещей, как необязательные параметры. Если я только посмотрю на этот ответ, мне кажется, что мне нужно иметь все, что использует мой экземпляр класса с именем tuples. Спасибо за оба ответа.
Асаф

78

Самый простой способ сделать это с помощью __slots__:

class A(object):
    __slots__ = []

Экземпляры Aявляются неизменяемыми, поскольку вы не можете устанавливать для них какие-либо атрибуты.

Если вы хотите, чтобы экземпляры класса содержали данные, вы можете комбинировать это с производным от tuple:

from operator import itemgetter
class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    x = property(itemgetter(0))
    y = property(itemgetter(1))

p = Point(2, 3)
p.x
# 2
p.y
# 3

Изменить : Если вы хотите избавиться от индексации, вы можете переопределить __getitem__():

class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    @property
    def x(self):
        return tuple.__getitem__(self, 0)
    @property
    def y(self):
        return tuple.__getitem__(self, 1)
    def __getitem__(self, item):
        raise TypeError

Обратите внимание, что вы не можете использовать operator.itemgetterдля свойств в этом случае, так как это будет полагаться Point.__getitem__()вместо tuple.__getitem__(). Более того, это не помешает использованию tuple.__getitem__(p, 0), но я с трудом представляю, как это должно представлять проблему.

Я не думаю, что «правильным» способом создания неизменяемого объекта является написание C-расширения. Python, как правило, полагается на то, что разработчики библиотек и пользователи библиотек согласны на взрослость , и вместо того, чтобы действительно применять интерфейс, интерфейс должен быть четко указан в документации. Вот почему я не рассматриваю возможность обойти переопределенное __setattr__(), называя object.__setattr__()проблему. Если кто-то делает это, это на свой страх и риск.


1
Не лучше ли было бы использовать tupleздесь __slots__ = (), а не __slots__ = []? (Просто
уточняю

1
@sukhbir: я думаю, что это не имеет значения вообще. Почему вы предпочитаете кортеж?
Свен Марнах

1
@ Свен: Я согласен, что это не имеет значения (кроме части скорости, которую мы можем игнорировать), но я подумал об этом следующим образом: __slots__не изменится ли правильно? Его цель - определить, какие атрибуты можно установить. Так что не tupleкажется ли это естественным выбором в таком случае?
user225312

5
Но с пустым __slots__я не могу установить какие-либо атрибуты. И если у меня есть, __slots__ = ('a', 'b')то атрибуты a и b все еще изменчивы.
Леннарт Регебро

Но ваше решение лучше переопределения, __setattr__поэтому оно лучше моего. +1 :)
Леннарт Регебро

50

.. как сделать это "правильно" в C ..

Вы можете использовать Cython для создания типа расширения для 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

Работает как на Python 2.x, так и на 3.

тесты

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

Если вы не возражаете против поддержки индексирования, то collections.namedtupleпредложение @Sven Marnach предпочтительнее:

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

@Lennart: экземпляры namedtuple(или, точнее, типа, возвращаемого функцией namedtuple()) являются неизменяемыми. Определенно.
Свен Марнач

@Lennart Regebro: namedtupleпроходит все тесты (кроме поддержки индексации). Какое требование я пропустил?
Jfs

Да, вы правы, я создал тип namedtuple, создал его, а затем провел тест на тип, а не на экземпляр. Хех. :-)
Леннарт Регебро

могу я спросить, зачем здесь слабая ссылка?
Максиникс

1
@McSinyx: в противном случае объекты нельзя использовать в коллекциях слабых ссылок. Что именно __weakref__в Python?
JFS

40

Другая идея - полностью запретить __setattr__и использовать object.__setattr__в конструкторе:

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

Конечно, вы можете использовать object.__setattr__(p, "x", 3)для изменения Pointэкземпляра p, но ваша первоначальная реализация страдает от той же проблемы (попробуйте tuple.__setattr__(i, "x", 42)на Immutableэкземпляре).

Вы можете применить тот же трюк в своей первоначальной реализации: избавиться от него __getitem__()и использовать его tuple.__getitem__()в функциях свойств.


11
Я бы не заботился о том, чтобы кто-то преднамеренно модифицировал объект, используя суперкласс » __setattr__, потому что дело не в том, чтобы быть надежным. Смысл в том, чтобы прояснить, что это не должно быть изменено и чтобы предотвратить изменение по ошибке.
Звон

18

Вы можете создать @immutableдекоратор, который либо переопределит, __setattr__ и заменит __slots__пустой список, а затем украсит __init__метод им.

Редактировать: как отмечено в OP, изменение __slots__атрибута только предотвращает создание новых атрибутов , но не их изменение.

Edit2: вот реализация:

Edit3: Использование __slots__ломает этот код, потому что если останавливает создание объекта__dict__ . Я ищу альтернативу.

Edit4: ну вот и все. Это хакерский, но работает как упражнение :-)

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
Создание (класса?) Декоратора или метакласса из решения действительно хорошая идея, но вопрос в том, каково решение. :)
Леннарт Регебро

3
object.__setattr__()ломает это stackoverflow.com/questions/4828080/…
jfs

На самом деле. Я просто продолжил как упражнение на декораторов.
PaoloVictor

13

Использование замороженного класса данных

Для Python 3.7+ вы можете использовать класс данных с frozen=Trueопцией , которая является очень питонным и поддерживаемым способом сделать то, что вы хотите.

Это будет выглядеть примерно так:

from dataclasses import dataclass

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

Так как подсказка типа требуется для полей классов данных, я использовал Any из typingмодуля .

Причины НЕ использовать Namedtuple

До Python 3.7 часто встречались именованные кортежи, используемые как неизменяемые объекты. Это может быть сложно во многих отношениях, один из них заключается в том, что __eq__метод между именованными кортами не учитывает классы объектов. Например:

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

Как видите, даже если типы obj1и obj2отличаются, даже если имена их полей различны, obj1 == obj2все равно выдает True. Это потому, что __eq__используемый метод является кортежем, который сравнивает только значения полей с учетом их позиций. Это может быть огромным источником ошибок, особенно если вы создаете подклассы этих классов.


10

Я не думаю, что это вполне возможно, за исключением использования кортежа или именованного кортежа. Независимо от того, что, если вы переопределите, __setattr__()пользователь всегда может обойти его, позвонив object.__setattr__()напрямую. Любое решение, от которого зависит, __setattr__гарантированно не будет работать.

Ниже приведено описание ближайшего, которое вы можете получить без использования какого-либо кортежа:

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__

но это сломается, если вы попытаетесь достаточно сильно:

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

но использование Свеном namedtupleявляется действительно неизменным.

Обновить

Так как вопрос был обновлен, чтобы спросить, как сделать это правильно в C, вот мой ответ о том, как сделать это правильно в Cython:

Первый 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)

и setup.pyдля его компиляции (используя команду 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
)

Тогда, чтобы попробовать это:

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

Спасибо за код Cython, Cython потрясающий. Реализация JF Sebastians с readonly более аккуратна и появилась первой, поэтому он получает награду.
Леннарт Регебро

5

Я создал неизменяемые классы, переопределив __setattr__и разрешив набор, если вызывающий объект __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)

Этого еще не достаточно, так как это позволяет кому-либо ___init__менять объект, но вы понимаете.


object.__setattr__()ломает это stackoverflow.com/questions/4828080/…
jfs

3
Использование проверки стека, чтобы убедиться, что вызывающая сторона __init__не очень удовлетворительна.
Гб.

5

В дополнение к отличным другим ответам я хотел бы добавить метод для Python 3.4 (или, возможно, 3.3). Этот ответ основан на нескольких предыдущих ответах на этот вопрос.

В Python 3.4 вы можете использовать свойства без установщиков для создания членов класса, которые нельзя изменить. (В более ранних версиях присваивание свойств без установщика было возможно.)

class A:
    __slots__=['_A__a']
    def __init__(self, aValue):
      self.__a=aValue
    @property
    def a(self):
        return self.__a

Вы можете использовать это так:

instance=A("constant")
print (instance.a)

который напечатает "constant"

Но вызов instance.a=10вызовет:

AttributeError: can't set attribute

Объяснение: свойства без сеттеров - очень недавняя особенность Python 3.4 (и я думаю, 3.3). Если вы попытаетесь присвоить такое свойство, возникнет ошибка. Используя слоты, я ограничиваю переменные-члены __A_a(__a ).

Проблема: Назначение по- _A__aпрежнему возможно (instance._A__a=2 ). Но если вы присваиваете частную переменную, это ваша собственная ошибка ...

Этот ответ среди других, однако, препятствует использованию __slots__. Использование других способов предотвращения создания атрибутов может быть предпочтительным.


propertyтакже доступен на Python 2 (посмотрите на код в самом вопросе). Это не создает неизменный объект, попробуйте тесты из моего ответа, например, instance.b = 1создает новый bатрибут.
Jfs

Правильно, вопрос действительно в том, как предотвратить выполнение, A().b = "foo"т.е. не разрешить установку новых атрибутов.
Леннарт Регебро

Propertis без установщика выдает ошибку в python 3.4, если вы попытаетесь присвоить это свойство. В более ранних версиях сеттер создавался неявно.
Бернхард

@Lennart: Мое решение - это ответ на подмножество вариантов использования для неизменяемых объектов и дополнение к предыдущим ответам. Одна из причин, по которой мне может понадобиться неизменный объект, заключается в том, что я могу сделать его хешируемым, и в этом случае моё решение может работать. Но вы правы, это не неизменный объект.
Бернхард

@ jf-sebastian: изменил мой ответ, чтобы использовать слоты для предотвращения создания атрибутов. Что нового в моем ответе по сравнению с другими ответами, так это то, что я использую свойства python3.4, чтобы избежать изменения существующих атрибутов. Хотя то же самое достигается в предыдущих ответах, мой код короче из-за изменения поведения свойств.
Бернхард

5

Вот элегантное решение:

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

Наследуйте от этого класса, инициализируйте ваши поля в конструкторе, и все готово.


1
но с помощью этой логики можно присвоить объекту новые атрибуты
Джавед

3

Если вас интересуют объекты с поведением, то namedtuple - почти ваше решение.

Как описано в нижней части документации namedtuple , вы можете получить свой собственный класс из namedtuple; а затем вы можете добавить поведение, которое вы хотите.

Например (код взят непосредственно из документации ):

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)

Это приведет к:

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

Этот подход работает как для Python 3, так и для Python 2.7 (также протестирован на IronPython).
Единственным недостатком является то, что дерево наследования немного странно; но это не то, с чем ты обычно играешь.


1
Python 3.6 + поддерживает это напрямую, используяclass Point(typing.NamedTuple):
Элазар

3

Классы, которые наследуются от следующего Immutableкласса, являются неизменными, как и их экземпляры, после __init__завершения выполнения их метода. Поскольку это чистый python, как уже отмечали другие, ничто не мешает кому-либо использовать мутирующие специальные методы из базы objectиtype , но этого достаточно , чтобы остановить любой из мутирует класс / экземпляр случайно.

Он работает, угоняя процесс создания класса с метаклассом.

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

Я нуждался в этом некоторое время назад и решил сделать для него пакет Python. Начальная версия сейчас на PyPI:

$ pip install immutable

Использовать:

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

Полные документы здесь: https://github.com/theengineear/immutable

Надеюсь, что это поможет, он оборачивает именованный кортеж, как уже обсуждалось, но делает создание экземпляров намного проще


2

Этот способ не перестает object.__setattr__работать, но я все еще нашел его полезным:

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)

вам может потребоваться переопределить больше вещей (например __setitem__) в зависимости от варианта использования.


Я придумал что-то похожее, прежде чем увидел это, но использовал, getattrчтобы я мог предоставить значение по умолчанию для frozen. Это немного упростило ситуацию. stackoverflow.com/a/22545808/5987
Марк Рэнсом

Мне больше нравится этот подход, но вам не нужно __new__переопределять. Внутри __setattr__просто замените условное наif name != '_frozen' and getattr(self, "_frozen", False)
Пит Качиоппи

Кроме того, нет необходимости заморозить класс при строительстве. Вы можете заморозить его в любой момент, если предоставите freeze()функцию. Затем объект будет "один раз заморозить". Наконец, беспокоиться о object.__setattr__глупости, потому что «мы все здесь взрослые».
Пит Качиоппи

2

Начиная с Python 3.7, вы можете использовать @dataclassдекоратор в своем классе, и он будет неизменным, как структура! Тем не менее, он может или не может добавить __hash__()метод к вашему классу. Quote:

hash () используется встроенным hash () и когда объекты добавляются в хешированные коллекции, такие как словари и наборы. Наличие hash () подразумевает, что экземпляры класса являются неизменяемыми. Изменчивость - это сложное свойство, которое зависит от намерения программиста, существования и поведения eq (), а также значений флагов eq и frozen в декораторе dataclass ().

По умолчанию dataclass () не будет неявно добавлять метод hash (), если это не безопасно. Он также не добавит и не изменит существующий явно определенный метод hash (). Установка атрибута класса hash = None имеет особое значение для Python, как описано в документации по hash ().

Если hash () не определен явно, или если он установлен в None, тогда dataclass () может добавить неявный метод hash (). Хотя это и не рекомендуется, вы можете заставить dataclass () создать метод hash () с unsafe_hash = True. Это может иметь место, если ваш класс является логически неизменным, но, тем не менее, может быть видоизменен. Это специализированный вариант использования, и его следует тщательно рассмотреть.

Вот пример из документов, связанных выше:

@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
Вы должны использовать frozen, то есть @dataclass(frozen=True), но это в основном блокирует использование __setattr__и __delattr__как в большинстве других ответов здесь. Он просто делает это способом, совместимым с другими параметрами классов данных.
CS

2

Вы можете переопределить setattr и все еще использовать init для установки переменной. Вы бы использовали суперкласс setattr . вот код

Неизменный класс:
    __slots__ = ('a', 'b')
    def __init __ (self, a, b):
        супер () .__ SetAttr __ ( 'а', а)
        супер () .__ SetAttr __ ( 'б', б)

    def __str __ (self):
        вернуть "" .формат (self.a, self.b)

    def __setattr __ (self, * игнорируется):
        поднять NotImplementedError

    def __delattr __ (self, * игнорируется):
        поднять NotImplementedError

Или просто passвместоraise NotImplementedError
jonathan.scholbach

В этом случае вообще не рекомендуется делать «pass» в __setattr__ и __delattr__. Причина проста: если кто-то присваивает значение полю / свойству, он, естественно, ожидает, что поле будет изменено. Если вы хотите следовать по пути «наименьшего удивления» (как вам следует), то вы должны вызвать ошибку. Но я не уверен, что NotImplementedError - это то, что нужно поднять. Я бы поднял что-то вроде «Поле / свойство является неизменным». ошибка ... Я думаю, что пользовательское исключение должно быть выброшено.
Дарлов

1

Сторонний attrмодуль обеспечивает эту функциональность .

Изменить: Python 3.7 принял эту идею в stdlib с @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в соответствии с документацией реализует замороженные классы путем переопределения __setattr__и оказывает незначительное влияние на производительность при каждом создании экземпляра.

Если вы привыкли использовать классы в качестве типов данных, это attrможет быть особенно полезно, поскольку оно заботится о шаблоне для вас (но не использует магию). В частности, он пишет для вас девять методов dunder (__X__) (если вы не отключите ни один из них), включая repr, init, hash и все функции сравнения.

attrтакже предоставляет помощника для__slots__ .


1

Итак, я пишу соответствующие Python 3:

Я) с помощью декоратора класса данных и набора замороженных = True. мы можем создавать неизменные объекты в Python.

для этого нужно импортировать класс данных из классов данных lib и нужно установить frozen = True

ех.

из классов данных импорт данных

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

о / р:

l = Местоположение («Дели», 112.345, 234.788)

Источник: https://realpython.com/python-data-classes/


0

Альтернативный подход заключается в создании оболочки, которая делает экземпляр неизменным.

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)

Это полезно в ситуациях, когда только некоторые экземпляры должны быть неизменяемыми (например, аргументы вызовов функций по умолчанию).

Может также использоваться на неизменных фабриках как:

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

Также защищает от object.__setattr__других трюков, но допускает их из-за динамической природы Python.


0

Я использовал ту же идею, что и Алекс: мета-класс и «маркер инициализации», но в сочетании с перезаписью __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)

Примечание: я вызываю мета-класс напрямую, чтобы он работал как для Python 2.x, так и для 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.

Это работает и со слотами ...:

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

... и множественное наследование:

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

Обратите внимание, что изменяемые атрибуты остаются изменяемыми:

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

0

Одна вещь, которая на самом деле здесь не включена, это полная неизменность ... не только родительский объект, но и все дочерние объекты. например, кортежи / фрозенсеты могут быть неизменными, но объекты, частью которых он является, могут не быть. Вот небольшая (неполная) версия, которая делает достойную работу по обеспечению неизменности:

# 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

Вы можете просто переопределить setAttr в последнем утверждении init. Тогда вы можете построить, но не изменить. Очевидно, вы можете переопределить объект usint. setAttr, но на практике большинство языков имеют некоторую форму отражения, поэтому неизменность всегда является утечкой абстракции. Неизменность - это скорее предотвращение случайного нарушения клиентами договора на объект. Я использую:

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

Первоначальное предлагаемое решение было неверным, оно было обновлено на основе комментариев, используя решение отсюда

Интересное оригинальное решение неверно, поэтому оно включено внизу.

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

class ImmutablePair(object):

    __initialised = False # a class level variable that should always stay false.
    def __init__(self, a, b):
        try :
            self.a = a
            self.b = b
        finally:
            self.__initialised = True #an instance level variable

    def __setattr__(self, key, value):
        if self.__initialised:
            self._raise_error()
        else :
            super(ImmutablePair, self).__setattr__(key, value)

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")

if __name__ == "__main__":

    immutable_object = ImmutablePair(1,2)

    print immutable_object.a
    print immutable_object.b

    try :
        immutable_object.a = 3
    except Exception as e:
        print e

    print immutable_object.a
    print immutable_object.b

Вывод :

1
2
Attempted To Modify Immutable Object
1
2

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

Оригинальная реализация:

В комментариях было правильно указано, что это на самом деле не работает, поскольку предотвращает создание более одного объекта, так как вы переопределяете метод класса setattr, что означает, что секунда не может быть создана как self.a = will Ошибка при второй инициализации.

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
Это не сработает: вы переопределяете метод класса , поэтому вы получите NotImplementedError, как только попытаетесь создать второй экземпляр.
slinkp

1
Если вы хотите использовать этот подход, обратите внимание, что трудно переопределить специальные методы во время выполнения: см. Обходные пути для stackoverflow.com/a/16426447/137635 .
slinkp

0

Ниже приведено базовое решение для следующего сценария:

  • __init__() может быть написано доступ к атрибутам, как обычно.
  • ПОСЛЕ ТОГО, что ОБЪЕКТ заморожен только для атрибутов :

Идея состоит в том, чтобы переопределять __setattr__метод и заменять его реализацию при каждом изменении замороженного состояния объекта.

Поэтому нам нужен метод ( _freeze), который хранит эти две реализации и переключается между ними по запросу.

Этот механизм может быть реализован внутри пользовательского класса или унаследован от специального Freezerкласса, как показано ниже:

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

Просто как dict

У меня есть библиотека с открытым исходным кодом, где я делаю вещи функционально , поэтому полезно перемещать данные в неизменяемом объекте. Однако я не хочу преобразовывать мой объект данных, чтобы клиент взаимодействовал с ними. Итак, я придумал это - он дает вам похожий на dict объект, который неизменен + некоторые вспомогательные методы.

Кредит Sven Marnach в своем ответе на базовой реализации ограничения обновления свойств и удаления.

import json 
# ^^ optional - If you don't care if it prints like a dict
# then rip this and __str__ and __repr__ out

class Immutable(object):

    def __init__(self, **kwargs):
        """Sets all values once given
        whatever is passed in kwargs
        """
        for k,v in kwargs.items():
            object.__setattr__(self, k, v)

    def __setattr__(self, *args):
        """Disables setting attributes via
        item.prop = val or item['prop'] = val
        """
        raise TypeError('Immutable objects cannot have properties set after init')

    def __delattr__(self, *args):
        """Disables deleting properties"""
        raise TypeError('Immutable objects cannot have properties deleted')

    def __getitem__(self, item):
        """Allows for dict like access of properties
        val = item['prop']
        """
        return self.__dict__[item]

    def __repr__(self):
        """Print to repl in a dict like fashion"""
        return self.pprint()

    def __str__(self):
        """Convert to a str in a dict like fashion"""
        return self.pprint()

    def __eq__(self, other):
        """Supports equality operator
        immutable({'a': 2}) == immutable({'a': 2})"""
        if other is None:
            return False
        return self.dict() == other.dict()

    def keys(self):
        """Paired with __getitem__ supports **unpacking
        new = { **item, **other }
        """
        return self.__dict__.keys()

    def get(self, *args, **kwargs):
        """Allows for dict like property access
        item.get('prop')
        """
        return self.__dict__.get(*args, **kwargs)

    def pprint(self):
        """Helper method used for printing that
        formats in a dict like way
        """
        return json.dumps(self,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4)

    def dict(self):
        """Helper method for getting the raw dict value
        of the immutable object"""
        return self.__dict__

Вспомогательные методы

def update(obj, **kwargs):
    """Returns a new instance of the given object with
    all key/val in kwargs set on it
    """
    return immutable({
        **obj,
        **kwargs
    })

def immutable(obj):
    return Immutable(**obj)

Примеры

obj = immutable({
    'alpha': 1,
    'beta': 2,
    'dalet': 4
})

obj.alpha # 1
obj['alpha'] # 1
obj.get('beta') # 2

del obj['alpha'] # TypeError
obj.alpha = 2 # TypeError

new_obj = update(obj, alpha=10)

new_obj is not obj # True
new_obj.get('alpha') == 10 # True
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.