[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

그러나 당신은 aand를 b통해 self[0]and 변수에 액세스 할 수 있습니다 self[1].

Pure Python에서 가능합니까? 그렇지 않은 경우 C 확장으로 어떻게합니까?

(Python 3에서만 작동하는 답변이 허용됩니다).

최신 정보:

그래서 서브 클래스 튜플 잘하여 데이터를 액세스하는 추가 가능성을 제외하고 작동 순수 파이썬, 그것을 할 수있는 방법이다 [0], [1]등등 그래서이 누락되는 모든 C에 “제대로”그것을 할 HOWTO이다이 질문을 완료하는 나는 단지 geititem또는 setattribute등을 구현하지 않음으로써 매우 간단하다고 생각합니다 . 그러나 나 자신을 대신하는 대신 게으 르기 때문에 그에 대한 현상금을 제공합니다. 🙂



답변

방금 생각한 또 다른 솔루션 : 원래 코드와 동일한 동작을 얻는 가장 간단한 방법은

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

이 속성을 통해 액세스 할 수 있는지 문제가 해결되지 않는 [0]등,하지만 적어도 그것은 상당히 짧다과 호환되는 부가적인 장점 제공 picklecopy.

namedtuple내가 설명 것과 유사한 유형의 생성 이 답변 에서 파생하여 예 tuple및 사용 __slots__. Python 2.6 이상에서 사용할 수 있습니다.


답변

가장 쉬운 방법은 다음을 사용하는 것입니다 __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 확장을 작성한다고 생각하지 않습니다. 파이썬은 일반적으로 성인동의하는 라이브러리 구현 자 및 라이브러리 사용자에 의존하며 실제로 인터페이스를 시행하는 대신 설명서에 인터페이스를 명확하게 명시해야합니다. 이것이 내가 문제 __setattr__()를 부름 으로써 재정의를 피할 가능성을 고려하지 않는 이유 object.__setattr__()입니다. 누군가이 일을하면 위험을 감수해야합니다.


답변

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'

인덱싱 지원이 마음에 들지 않으면 @Sven Marnachcollections.namedtuple제안하는 것이 좋습니다 .

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


답변

또 다른 아이디어는 생성자에서 완전히 허용하지 __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__()속성 함수에서 사용 하십시오.


답변

당신은 만들 수 @immutable중 하나는보다 우선 장식을 __setattr__ 하고 을 변경 __slots__빈 목록에 다음 장식 __init__함께 방법을.

편집 : OP가 언급했듯이 __slots__속성을 변경 하면 수정이 아닌 새 속성을 만들 수 없습니다.

Edit2 : 구현은 다음과 같습니다.

Edit3 :를 사용 __slots__하면 객체의 생성이 중지 되므로이 코드가 중단됩니다 __dict__. 대안을 찾고 있습니다.

Edit4 : 글쎄, 그게 다야. 그것은 hackish이지만 운동으로 작동합니다 🙂

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


답변

고정 된 데이터 클래스 사용

Python 3.7 이상 에서는 옵션 과 함께 데이터 클래스 를 사용할 수 있습니다frozen=True 은 원하는 것을 수행하는 매우 파이썬적이고 유지 관리 가능한 방법입니다.

그것은 다음과 같이 보일 것입니다 :

from dataclasses import dataclass

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

데이터 클래스의 필드에 유형 힌트가 필요 하므로 모듈의 Any를 사용했습니다.typing .

Namedtuple을 사용하지 않는 이유

파이썬 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__사용 된 방법은 튜플의 방법 이기 때문에 위치가 주어진 필드의 값만 비교합니다. 특히 이러한 클래스를 서브 클래 싱하는 경우 큰 오류의 원인이 될 수 있습니다.


답변

나는 튜플이나 명명 된 튜플을 사용하는 것을 제외하고는 완전히 가능하다고 생각하지 않습니다. 어쨌든 __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

그러나 Sven의 사용 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)
>>>