Як зробити незмінний об’єкт в 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], що дратує.

Чи можливо це в чистому пітоні? Якщо ні, то як би це зробити з розширенням C?

(Відповіді, які працюють лише в Python 3, прийнятні).

Оновлення:

Таким чином, підкласифікація кортежу - це спосіб зробити це в Pure Python, який добре працює, за винятком додаткової можливості доступу до даних [0], [1]тощо. Отже, для завершення цього питання все, що не вистачає, - це як це зробити "належним чином" в C, який Я підозрюю, що це було б досить просто, просто не втілюючи жодного geititemчи іншого setattribute, і т. Д. Але замість того, щоб робити це сам, я пропоную за це щедрість, бо я лінивий. :)


2
Чи не полегшує ваш код доступ до атрибутів через .aта .b? Ось для чого, здається, властивості зрештою існують.
Свен Марнах

1
@Sven Marnach: Так, але [0] та [1] все ще працюють, і навіщо це робити? Я не хочу їх. :) Може, ідея незмінного об’єкта з атрибутами - це нісенітниця? :-)
Леннарт Регебро

2
Ще одна примітка: NotImplementedмається на увазі лише як повернене значення для багатих порівнянь. Повернення значення в __setatt__()будь-якому випадку досить безглуздо, оскільки зазвичай його взагалі не бачиш. Кодек як ніби immutable.x = 42нічого не зробить Ви повинні TypeErrorзамість цього підняти .
Свен Марнах

1
@Sven Marnach: Гаразд, я був здивований, тому що я думав, що ти можеш підняти NotImmented в цій ситуації, але це дає дивну помилку. Тому я повернув її замість цього, і, здавалося, це спрацювало. TypeError отримав очевидний сенс, як тільки я побачив, як ви його використовували.
Леннарт Регебро

1
@Lennart: Ви могли б підняти NotImplementedError, але TypeErrorце те, що виникає кортеж, якщо ви спробуєте його змінити.
Свен Марнах

Відповіді:


115

Ще одне рішення, про яке я щойно думав: Найпростіший спосіб отримати таку поведінку, як і ваш вихідний код

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

Це не вирішує проблему, через яку можна отримати доступ до атрибутів через [0]etc., але, принаймні, це значно коротше і забезпечує додаткову перевагу сумісності з pickleі copy.

namedtupleстворює тип, подібний до того, що я описав у цій відповіді , тобто отриманий із tupleта використання __slots__. Він доступний в Python 2.6 або вище.


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

2
Гаразд, ви отримуєте "найкращу відповідь", адже це найпростіший спосіб зробити це. Себастьян отримує винагороду за те, що він дав коротку реалізацію Cython. Ура!
Леннарт Регебро

1
Ще одна характеристика незмінних об'єктів полягає в тому, що коли ви передаєте їх як параметр через функцію, вони скопіюються за значенням, а не робляться інші посилання. Чи namedtupleбуде скопійовано значення за значенням при передачі через функції?
hlin117

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

Чи можете ви використовувати nametuple всередині класу, а не інстанціювати об'єкт зовні? Я дуже новачок у python, але перевага вашої іншої відповіді полягає в тому, що я можу мати клас приховати деталі, а також мати силу таких речей, як необов'язкові параметри. Якщо я дивлюся лише на цю відповідь, то здається, що мені потрібно мати все, що використовує мою екземпляру класу з назвою кортежі. Дякую за обидві відповіді.
Асаф

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
@Sven: Я погоджуюся, що це не має значення (крім швидкості, яку ми можемо ігнорувати), але я подумав про це так: __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

Так, ви маєте рацію, я створив назви типуtuple, інстанціював його, а потім зробив тест на тип замість екземпляра. Хе. :-)
Леннарт Регебро

Чи можу я запитати, чому тут потрібні слабкі посилання?
McSinyx

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__, бо справа не в тому, щоб бути дурним. Сенс полягає в тому, щоб зрозуміти, що вона не повинна бути модифікована, а також не допустити модифікації помилково.
zvone

18

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

Редагувати: Як зазначає ОП, зміна __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)

і a 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"не допустити встановлення нових атрибутів.
Леннарт Регебро

Професіонали без сеттера викликають помилку в 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
але з цією логікою можна присвоїти новим атрибутам об’єкт
javed

3

Якщо вас цікавлять об'єкти з поведінкою, то nametuple - це майже ваше рішення.

Як описано внизу документації 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):
Elazar

3

Класи, які успадковують наступний Immutableклас, незмінні, як і їхні випадки, після того, як їх __init__метод закінчується виконанням. Оскільки це чиста пітона, як і інші відзначали, що немає нічого зупиняючи кого - то від використання мене спеціальних методів з бази 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
Mark Ransom

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

Також не потрібно заморожувати клас при будівництві. Ви можете заморозити його в будь-якій точці, якщо надаєте freeze()функцію. Потім об'єкт буде "заморожено один раз". Нарешті, турбуватися про object.__setattr__це нерозумно, адже «ми всі тут дорослі».
Піт Касіоппі

2

Як і в Python 3.7, ви можете використовувати @dataclassдекоратор у своєму класі, і він буде непорушним, як структура! Хоча він може або не може додати __hash__()метод до вашого класу. Цитата:

хеш () використовується вбудованим хешем (), і коли об’єкти додаються до колекцій хешів, таких як словники та набори. Наявність хеша () означає, що екземпляри класу незмінні. Змінюваність - це складна властивість, яка залежить від наміру програміста, існування та поведінки eq (), а також значень eq та заморожених прапорів у декораторі даних ().

За замовчуванням dataclass () не буде неявно додавати метод хеш (), якщо це не безпечно. Він також не додасть і не змінить існуючий явно визначений метод хеш (). Встановлення атрибута класу hash = None не має конкретного значення для Python, як описано в документації хеш ().

Якщо хеш () не визначено явним чином, або якщо він встановлено "Немає", тоді dataclass () може додати неявний метод хеш (). Хоча це не рекомендується, ви можете змусити dataclass () створити метод хеш () за допомогою 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 для встановлення змінної. Ви б використовували сетаттр суперкласу . ось код.

клас Незмінний:
    __slots__ = ('a', 'b')
    def __init __ (self, a, b):
        super () .__ setattr __ ('a', a)
        super () .__ setattr __ ('b', b)

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

    def __setattr __ (самостійно, * ігнорується):
        підняти NotImPLmentedError

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

Або просто passзамістьraise NotImplementedError
jonathan.scholbach

Зовсім не годиться робити "пропуск" у __setattr__ та __delattr__ у цьому випадку. Проста причина полягає в тому, що якщо хтось присвоює значення полі / властивості, вони, природно, очікують, що поле буде змінено. Якщо ви хочете пройти шлях "найменшого здивування" (як слід), тоді вам доведеться підняти помилку. Але я не впевнений, чи правильно підняти NotImplementedError. Я б підніс щось на кшталт "Поле / властивість незмінне". помилка ... Я думаю, що спеціальний виняток потрібно кинути
darlove

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може бути особливо корисним, оскільки воно піклується про панель котлів для вас (але не робить ніякої магії). Зокрема, він пише для вас дев'ять методів (__X__) (якщо ви не відключите їх), включаючи repr, init, хеш та всі функції порівняння.

attrтакож надає помічник для__slots__ .


1

Отже, я пишу відповідний python 3:

I) за допомогою декоратора класу даних і встановити заморожений = True. ми можемо створити незмінні об’єкти в python.

для цього потрібно імпортувати клас даних з lib класів даних і потрібно встановити заморожене = True

колишній

з класів даних імпортує клас даних

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

о / р:

l = Місцезнаходження ("Делі", 112.345, 234.788) l.name "Делі" l. Довгота 112.345 л. Широта 234.788 l.name = "Колката" даних класів. FrozenInstanceError: не можна призначити полю "ім'я"

Джерело: 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__інших хитрощів, але порушується завдяки динамічній природі Пітона.


0

Я використовував ту саму ідею, що й Алекс: мета-клас та "init маркер", але в поєднанні із надмірним написанням __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

У мене є бібліотека з відкритим кодом, де я роблю речі функціонально, тому переміщення даних у незмінному об'єкті корисно. Однак мені не хочеться перетворювати мій об’єкт даних для того, щоб клієнт взаємодіяв з ними. Отже, я придумав це - це дає тобі дік, як предмет, незмінний + деякі допоміжні методи.

Кредит 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.