Как сделать неизменный объект в Python? - PullRequest
159 голосов
/ 28 января 2011

Хотя мне это никогда не было нужно, меня просто поразило, что создание неизменяемого объекта в 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, являются приемлемыми).

Обновление:

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

Ответы [ 21 ]

102 голосов
/ 28 января 2011

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

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

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

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

71 голосов
/ 28 января 2011

Самый простой способ сделать это - использовать __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

Редактировать : если вы хотите избавиться от индексации либо, вы можетеoverride __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__() проблемой.Если кто-то делает это, он на свой страх и риск.

49 голосов
/ 31 января 2011

.. как это сделать "правильно" в 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, предложенный @ Свен Марнах предпочтительнее:

Immutable = collections.namedtuple("Immutable", "a b")
38 голосов
/ 28 января 2011

Другая идея состоит в том, чтобы полностью запретить __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__() в вашей собственности функции.

18 голосов
/ 28 января 2011

Вы можете создать декоратор @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
10 голосов
/ 28 января 2011

Я не думаю, что это вполне возможно, за исключением использования кортежа или именованного кортежа.Независимо от того, что, если вы переопределите __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)
>>>      
4 голосов
/ 02 июля 2015

В дополнение к отличным другим ответам мне нравится добавлять метод для 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__. Использование других способов предотвращения создания атрибутов может быть предпочтительным.

4 голосов
/ 29 июня 2018

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

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

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

3 голосов
/ 29 января 2016

Если вас интересуют объекты с поведением, то 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).
Единственный недостаток - дерево наследования немного странно;но это не то, с чем вы обычно играете.

3 голосов
/ 28 января 2011

Я создал неизменяемые классы, переопределив __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__ чтобы изменить объект, но вы поняли.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...