วิธีสร้างวัตถุที่ไม่เปลี่ยนรูปใน Python


189

แม้ว่าฉันไม่เคยต้องการสิ่งนี้ แต่ก็ทำให้ฉันรู้สึกว่าการสร้างวัตถุที่ไม่เปลี่ยนรูปใน Python อาจเป็นเรื่องยุ่งยากเล็กน้อย คุณไม่สามารถลบล้าง__setattr__ได้เนื่องจากคุณไม่สามารถกำหนดแอตทริบิวต์ในไฟล์__init__. Subclassing tuple เป็นเคล็ดลับที่ใช้ได้ผล:

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 เท่านั้นที่ยอมรับได้)

อัปเดต:

ดังนั้น subclassing tuple เป็นวิธีที่จะทำมันในเพียวหลามซึ่งทำงานได้ดียกเว้นสำหรับความเป็นไปได้ที่เพิ่มขึ้นของการเข้าถึงข้อมูลโดย[0], [1]ฯลฯ ดังนั้นเพื่อให้คำถามนี้เป็นสิ่งที่ขาดหายไปคือ HOWTO ทำมัน "อย่างถูกต้อง" ใน C ซึ่ง ฉันสงสัยว่ามันจะค่อนข้างง่ายเพียงแค่ไม่ใช้geititemหรือsetattributeอื่น ๆ แต่แทนที่จะทำด้วยตัวเองฉันเสนอเงินรางวัลสำหรับสิ่งนั้นเพราะฉันขี้เกียจ :)


2
รหัสของคุณไม่อำนวยความสะดวกในการเข้าถึงแอตทริบิวต์ผ่าน.aและ.b? นั่นคือสิ่งที่คุณสมบัติดูเหมือนจะมีอยู่จริง
Sven Marnach

1
@Sven Marnach: ใช่ แต่ [0] และ [1] ยังใช้งานได้และทำไมถึงเป็นเช่นนั้น ฉันไม่ต้องการพวกเขา :) บางทีความคิดของวัตถุไม่เปลี่ยนรูปที่มีคุณลักษณะเป็นเรื่องไร้สาระ? :-)
Lennart Regebro

2
หมายเหตุอีกประการหนึ่ง: NotImplementedหมายถึงเฉพาะค่าตอบแทนสำหรับการเปรียบเทียบที่สมบูรณ์ ค่าส่งคืนสำหรับ__setatt__()ค่อนข้างไม่มีจุดหมายอย่างไรก็ตามโดยปกติคุณจะไม่เห็นเลย โค้ดเหมือนimmutable.x = 42จะเงียบไม่ทำอะไรเลย คุณควรเพิ่มTypeErrorแทน
Sven Marnach

1
@Sven Marnach: ตกลงฉันแปลกใจเพราะฉันคิดว่าคุณสามารถเพิ่ม NotImplemented ในสถานการณ์นี้ได้ แต่นั่นทำให้เกิดข้อผิดพลาดแปลก ๆ ฉันเลยส่งคืนแทนและดูเหมือนว่าจะได้ผล TypeError ทำให้รู้สึกชัดเจนเมื่อฉันเห็นคุณใช้มัน
Lennart Regebro

1
@ เลนนาร์ท: คุณสามารถเพิ่มNotImplementedErrorได้ แต่TypeErrorสิ่งที่ทูเปิลเพิ่มขึ้นถ้าคุณพยายามแก้ไข
Sven Marnach

คำตอบ:


120

วิธีแก้ปัญหาอื่นที่ฉันเพิ่งคิด: วิธีที่ง่ายที่สุดในการรับพฤติกรรมเดียวกันกับรหัสเดิมของคุณคือ

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

มันไม่ได้แก้ปัญหาที่แอตทริบิวต์สามารถเข้าถึงได้ผ่าน[0]ฯลฯ แต่อย่างน้อยมันสั้นมากและให้ประโยชน์เพิ่มเติมของการเข้ากันได้กับและpicklecopy

namedtupleสร้างประเภทที่คล้ายกันกับสิ่งที่ผมอธิบายไว้ในคำตอบนี้คือมาจากและการใช้tuple __slots__มีอยู่ใน Python 2.6 หรือสูงกว่า


7
ข้อได้เปรียบของตัวแปรนี้เมื่อเทียบกับแอนะล็อกที่เขียนด้วยมือ (แม้ใน Python 2.5 (การใช้verboseพารามิเตอร์กับnamedtupleโค้ดก็สร้างได้อย่างง่ายดาย)) คืออินเทอร์เฟซเดียว / การนำไปใช้งานของ a namedtupleเป็นที่นิยมมากกว่าอินเตอร์เฟส / การใช้งานที่เขียนด้วยมือที่แตกต่างกันเล็กน้อยทำเกือบเหมือนกัน
jfs

2
ตกลงคุณได้ "คำตอบที่ดีที่สุด" เพราะเป็นวิธีที่ง่ายที่สุด เซบาสเตียนได้รับเงินรางวัลจากการใช้งาน Cython สั้น ๆ ไชโย!
Lennart Regebro

1
ลักษณะเฉพาะอีกประการหนึ่งของออบเจ็กต์ที่ไม่เปลี่ยนรูปก็คือเมื่อคุณส่งผ่านพวกมันเป็นพารามิเตอร์ผ่านฟังก์ชันพวกมันจะถูกคัดลอกด้วยค่าแทนที่จะเป็นการอ้างอิงอื่น จะnamedtupleถูกคัดลอกโดยค่าเมื่อส่งผ่านฟังก์ชันหรือไม่?
hlin117

4
@ hlin117: ทุกพารามิเตอร์จะถูกส่งผ่านเป็นการอ้างอิงไปยังออบเจ็กต์ใน Python ไม่ว่าจะไม่เปลี่ยนแปลงหรือไม่เปลี่ยนรูป สำหรับวัตถุที่ไม่เปลี่ยนรูปการทำสำเนาจะไม่มีจุดหมายโดยเฉพาะอย่างยิ่งเนื่องจากคุณไม่สามารถเปลี่ยนวัตถุได้คุณอาจส่งต่อการอ้างอิงไปยังวัตถุต้นฉบับได้เช่นกัน
Sven Marnach

คุณสามารถใช้ namedtuple ภายในคลาสแทนการสร้างอินสแตนซ์วัตถุภายนอกได้หรือไม่? ฉันใหม่มากสำหรับ python แต่ข้อดีสำหรับคำตอบอื่น ๆ ของคุณคือฉันสามารถมีคลาสซ่อนรายละเอียดและยังมีพลังของสิ่งต่างๆเช่นพารามิเตอร์เสริม ถ้าฉันดูแค่คำตอบนี้ดูเหมือนว่าฉันจำเป็นต้องมีทุกอย่างที่ใช้การสร้างอินสแตนซ์ชั้นเรียนของฉันที่มีชื่อว่า tuples ขอบคุณสำหรับทั้งสองคำตอบ
Asaf

82

วิธีที่ง่ายที่สุดคือใช้__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สำหรับคุณสมบัติในกรณี thise ตั้งแต่นี้จะพึ่งพาแทนPoint.__getitem__() tuple.__getitem__()ยิ่งไปกว่านั้นสิ่งนี้จะไม่ป้องกันการใช้งานtuple.__getitem__(p, 0)แต่ฉันแทบไม่สามารถจินตนาการได้ว่าสิ่งนี้น่าจะเป็นปัญหาได้อย่างไร

ฉันไม่คิดว่าวิธีที่ "ถูกต้อง" ในการสร้างวัตถุที่ไม่เปลี่ยนรูปคือการเขียนนามสกุล C Python มักจะอาศัยผู้ใช้ไลบรารีและผู้ใช้ไลบรารีที่ยินยอมจากผู้ใหญ่และแทนที่จะบังคับใช้อินเทอร์เฟซจริงๆควรระบุอินเทอร์เฟซไว้อย่างชัดเจนในเอกสารประกอบ นี่คือเหตุผลที่ฉันไม่พิจารณาความเป็นไปได้ในการหลีกเลี่ยงการลบล้าง__setattr__()โดยการโทรหาobject.__setattr__()ปัญหา หากมีใครทำสิ่งนี้ก็เป็นความเสี่ยงของเธอเอง


1
มันจะไม่เป็นความคิดที่ดีที่จะใช้tupleที่นี่__slots__ = ()มากกว่า__slots__ = []? (แค่ชี้แจง)
user225312

1
@sukhbir: ฉันคิดว่ามันไม่สำคัญเลย ทำไมคุณถึงชอบทูเพิล?
Sven Marnach

1
@Sven: ฉันยอมรับว่ามันไม่สำคัญหรอก (ยกเว้นส่วนความเร็วซึ่งเราสามารถเพิกเฉยได้) แต่ฉันคิดแบบนี้: __slots__จะไม่มีการเปลี่ยนแปลงใช่ไหม มีจุดประสงค์เพื่อระบุว่าสามารถตั้งค่าแอตทริบิวต์ใดได้ ดังนั้นไม่ได้เป็นtupleดูเหมือนมากธรรมชาติทางเลือกในกรณีเช่นนี้?
user225312

5
แต่ด้วยความว่างเปล่า__slots__ฉันไม่สามารถตั้งค่าแอตทริบิวต์ใด ๆ และถ้าฉันมี__slots__ = ('a', 'b')แล้วแอตทริบิวต์ a และ b จะยังคงเปลี่ยนแปลงได้
Lennart Regebro

แต่วิธีแก้ปัญหาของคุณดีกว่าการลบล้าง__setattr__ดังนั้นจึงเป็นการปรับปรุงของฉัน +1 :)
Lennart Regebro

52

.. ทำอย่างไรจึงจะ "ถูกต้อง" ใน 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")

@ เลนนาร์ท: อินสแตนซ์ของnamedtuple(หรืออย่างแม่นยำมากขึ้นของประเภทที่ส่งคืนโดยฟังก์ชันnamedtuple()) ไม่เปลี่ยนรูป อย่างแน่นอน.
Sven Marnach

@ Lennart Regebro: namedtupleผ่านการทดสอบทั้งหมด (ยกเว้นการสนับสนุนการจัดทำดัชนี) ฉันพลาดข้อกำหนดอะไร
jfs

ใช่คุณพูดถูกฉันสร้างชื่อประเภททูเปิลสร้างอินสแตนซ์จากนั้นทำการทดสอบประเภทแทนอินสแตนซ์ เฮ้. :-)
Lennart Regebro

ฉันขอถามได้ไหมว่าทำไมถึงต้องการการอ้างอิงที่อ่อนแอที่นี่?
McSinyx

1
@McSinyx มิฉะนั้นจะไม่สามารถใช้วัตถุในคอลเลกชันของจุดอ่อนได้ สิ่งที่แน่นอนคือ__weakref__ในงูใหญ่?
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__()ในฟังก์ชันคุณสมบัติของคุณ


12
ฉันจะไม่สนใจว่ามีใครบางคนจงใจดัดแปลงวัตถุโดยใช้ superclass ' __setattr__เพราะประเด็นนี้ไม่สามารถเข้าใจผิดได้ ประเด็นคือต้องทำให้ชัดเจนว่าไม่ควรแก้ไขและป้องกันการแก้ไขโดยไม่ได้ตั้งใจ
zvone

18

คุณสามารถสร้าง@immutableมัณฑนากรที่ลบล้าง__setattr__ และเปลี่ยนรายการ__slots__เป็นรายการว่างจากนั้นตกแต่ง__init__วิธีการด้วย

แก้ไข: ตามที่ OP ระบุไว้การเปลี่ยน__slots__แอตทริบิวต์จะป้องกันการสร้างแอตทริบิวต์ใหม่เท่านั้นไม่ใช่การแก้ไข

แก้ไข 2: นี่คือการใช้งาน:

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
การสร้างมัณฑนากร (คลาส?) หรือเมตาคลาสจากโซลูชันนั้นเป็นความคิดที่ดี แต่คำถามคือวิธีแก้ปัญหาคืออะไร :)
Lennart Regebro

3
object.__setattr__()แตกมันstackoverflow.com/questions/4828080/…
jfs

แน่นอน. ฉันแค่ทำตามแบบฝึกหัดเกี่ยวกับมัณฑนากร
PaoloVictor

17

ใช้ Frozen Dataclass

สำหรับ Python 3.7+ คุณสามารถใช้Data Classพร้อมfrozen=Trueตัวเลือกซึ่งเป็นวิธีที่ไพ ธ อนและบำรุงรักษาได้มากในการทำสิ่งที่คุณต้องการ

มันจะมีลักษณะดังนี้:

from dataclasses import dataclass

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

ในฐานะที่เป็นประเภทเค้าเป็นสิ่งจำเป็นสำหรับเขตข้อมูล dataclasses' ฉันได้ใช้ใด ๆ จากtypingโมดูล

เหตุผลที่ไม่ควรใช้ Namedtuple

ก่อนหน้า Python 3.7 มักจะเห็น Nametuples ถูกใช้เป็นวัตถุที่ไม่เปลี่ยนรูป อาจเป็นเรื่องยุ่งยากในหลาย ๆ วิธีหนึ่งในนั้นคือ__eq__วิธีการระหว่าง namestuples ไม่พิจารณาคลาสของวัตถุ ตัวอย่างเช่น:

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

ฉันไม่คิดว่ามันจะเป็นไปได้ทั้งหมดยกเว้นการใช้ tuple หรือ nametuple ไม่ว่าจะเกิดอะไรขึ้นหากคุณลบล้าง__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)
>>>      

ขอบคุณสำหรับรหัส Cython Cython นั้นยอดเยี่ยมมาก การใช้งาน JF Sebastians ด้วยการอ่านอย่างเดียวนั้นดูดีกว่าและมาถึงก่อนดังนั้นเขาจึงได้รับค่าหัว
Lennart Regebro

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__ไม่พอใจมากนัก
gb.

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

คำอธิบาย: คุณสมบัติที่ไม่มี setters เป็นคุณสมบัติล่าสุดของ 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"เช่นไม่อนุญาตให้ตั้งค่าคุณลักษณะใหม่
Lennart Regebro

Propertis โดยไม่มี setter จะเพิ่มข้อผิดพลาดใน python 3.4 หากคุณพยายามกำหนดให้กับคุณสมบัตินั้น ในเวอร์ชันก่อนหน้านี้ setter ถูกสร้างขึ้นโดยปริยาย
Bernhard

@ เลนนาร์ท: วิธีแก้ปัญหาของฉันคือคำตอบสำหรับชุดย่อยของกรณีการใช้งานสำหรับวัตถุที่ไม่เปลี่ยนรูปและส่วนเพิ่มเติมจากคำตอบก่อนหน้านี้ เหตุผลหนึ่งที่ฉันอาจต้องการวัตถุที่ไม่เปลี่ยนรูปก็เพื่อให้สามารถแฮชได้ซึ่งในกรณีนี้โซลูชันของฉันอาจใช้งานได้ แต่คุณถูกต้องนี่ไม่ใช่วัตถุที่ไม่เปลี่ยนรูป
Bernhard

@ jf-sebastian: เปลี่ยนคำตอบของฉันให้ใช้ช่องเพื่อป้องกันการสร้างแอตทริบิวต์ มีอะไรใหม่ในคำตอบของฉันเมื่อเทียบกับคำตอบอื่น ๆ คือฉันใช้คุณสมบัติของ python3.4 เพื่อหลีกเลี่ยงการเปลี่ยนคุณสมบัติที่มีอยู่ ในขณะที่คำตอบก่อนหน้านี้ทำได้เช่นเดียวกัน แต่รหัสของฉันสั้นลงเนื่องจากการเปลี่ยนแปลงพฤติกรรมของคุณสมบัติ
Bernhard

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

หากคุณมีความสนใจในวัตถุที่มีพฤติกรรมการตั้งชื่อเกือบจะเป็นทางออกของคุณ

ตามที่อธิบายไว้ที่ด้านล่างของเอกสารประกอบที่ระบุชื่อชื่อคุณสามารถได้รับคลาสของคุณเองจาก 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)
Pete Cacioppi

นอกจากนี้ไม่จำเป็นต้องตรึงชั้นเรียนเมื่อมีการก่อสร้าง คุณสามารถหยุดได้ทุกเมื่อหากคุณมีfreeze()ฟังก์ชัน จากนั้นวัตถุจะ "หยุดนิ่งหนึ่งครั้ง" สุดท้ายการกังวลobject.__setattr__ก็เป็นเรื่องไร้สาระเพราะ "เราเป็นผู้ใหญ่กันหมดแล้ว"
Pete Cacioppi

2

ใน Python 3.7 คุณสามารถใช้@dataclassมัณฑนากรในคลาสของคุณและมันจะไม่เปลี่ยนรูปเหมือนโครงสร้าง! แม้ว่าอาจเพิ่ม__hash__()วิธีการในชั้นเรียนของคุณหรือไม่ก็ได้ อ้าง:

แฮช () ถูกใช้โดยแฮชในตัว () และเมื่ออ็อบเจ็กต์ถูกเพิ่มลงในคอลเล็กชันที่แฮชเช่นพจนานุกรมและชุด การมีแฮช () หมายความว่าอินสแตนซ์ของคลาสไม่เปลี่ยนรูป ความสามารถในการเปลี่ยนแปลงเป็นคุณสมบัติที่ซับซ้อนซึ่งขึ้นอยู่กับเจตนาของโปรแกรมเมอร์การดำรงอยู่และพฤติกรรมของeq () และค่าของแฟล็ก eq และตรึงใน dataclass () มัณฑนากร

ตามค่าเริ่มต้น dataclass () จะไม่เพิ่มเมธอดhash () โดยปริยายเว้นแต่จะทำได้อย่างปลอดภัย จะไม่เพิ่มหรือเปลี่ยนแปลงวิธีการแฮช () ที่กำหนดไว้อย่างชัดเจนที่มีอยู่ การตั้งค่าคลาสแอ็ตทริบิวต์แฮช = ไม่มีมีความหมายเฉพาะสำหรับ Python ตามที่อธิบายไว้ในแฮช ()

หากไม่ได้กำหนดhash () ไว้อย่างชัดเจนหรือหากตั้งค่าเป็น None dataclass () อาจเพิ่มเมธอดimplicit 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__ชอบในคำตอบอื่น ๆ ส่วนใหญ่ที่นี่ มันทำในลักษณะที่เข้ากันได้กับตัวเลือกอื่น ๆ ของ dataclasses
ลูกค้า

2

คุณสามารถแทนที่setattrและยังคงใช้initเพื่อตั้งค่าตัวแปร คุณจะใช้ super class setattr setattrนี่คือรหัส

คลาสไม่เปลี่ยนรูป:
    __slots__ = ('a', 'b')
    def __init __ (ตัวเอง a, b):
        ซุปเปอร์ () .__ setattr __ ('a', a)
        ซุปเปอร์ () .__ setattr __ ('b', b)

    def __str __ (ตัวเอง):
        return "" .format (self.a, self.b)

    def __setattr __ (ตัวเอง * ละเว้น):
        เพิ่ม NotImplementedError

    def __delattr __ (ตัวเอง * ละเว้น):
        เพิ่ม NotImplementedError

หรือpassแทนที่จะเป็นraise NotImplementedError
jonathan.scholbach

ไม่ใช่ความคิดที่ดีเลยที่จะทำ "pass" ใน __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อาจมีประโยชน์อย่างยิ่งเนื่องจากดูแลต้นแบบสำหรับคุณ (แต่ไม่ได้ใช้เวทมนตร์ใด ๆ ) โดยเฉพาะอย่างยิ่งมันเขียนวิธี dunder (__X__) เก้าวิธีสำหรับคุณ (เว้นแต่คุณจะปิดวิธีใด ๆ ) รวมถึง repr, init, hash และฟังก์ชันการเปรียบเทียบทั้งหมด

attrนอกจากนี้ยังมีผู้ช่วยสำหรับ__slots__


1

เช่นเดียวกับ 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

1

ดังนั้นฉันกำลังเขียนตามลำดับของ python 3:

I) ด้วยความช่วยเหลือของมัณฑนากรชั้นข้อมูลและตั้งค่า Frozen = True เราสามารถสร้างวัตถุที่ไม่เปลี่ยนรูปได้ใน python

สำหรับสิ่งนี้จำเป็นต้องนำเข้าคลาสข้อมูลจากคลาสข้อมูล lib และต้องตั้งค่า Frozen = True

เช่น

จาก dataclasses นำเข้า dataclass

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

o / p:

>>> l = Location("Delhi", 112.345, 234.788)
>>> l.name
'Delhi'
>>> l.longitude
112.345
>>> l.latitude
234.788
>>> l.name = "Kolkata"
dataclasses.FrozenInstanceError: cannot assign to field 'name'
>>> 

ที่มา: https://realpython.com/python-data-classes/


0

อีกทางเลือกหนึ่งคือการสร้าง Wrapper ซึ่งทำให้อินสแตนซ์ไม่เปลี่ยนรูป

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

ฉันใช้แนวคิดเดียวกับ Alex: meta-class และ "init marker" แต่ใช้ร่วมกับ __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)

หมายเหตุ: ฉันกำลังเรียกใช้ meta-class โดยตรงเพื่อให้ทำงานได้ทั้ง 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

สิ่งหนึ่งที่ไม่ได้รวมไว้ที่นี่คือความไม่เปลี่ยนรูปโดยสิ้นเชิง ... ไม่ใช่แค่วัตถุแม่ แต่รวมถึงลูก ๆ ทุกคนด้วย ตัวอย่างเช่น tuples / frozensets อาจไม่เปลี่ยนรูป แต่วัตถุที่เป็นส่วนหนึ่งอาจไม่ใช่ นี่คือเวอร์ชันขนาดเล็ก (ไม่สมบูรณ์) ที่ทำงานได้ดีในการบังคับใช้ความไม่สามารถเปลี่ยนแปลงได้ตลอดทาง:

# 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 object ได้ 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

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

การใช้งานดั้งเดิม:

มีการระบุไว้ในความคิดเห็นอย่างถูกต้องว่าสิ่งนี้ไม่ได้ผลในความเป็นจริงเนื่องจากป้องกันการสร้างมากกว่าหนึ่งวัตถุในขณะที่คุณกำลังแทนที่เมธอด class 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__() สามารถเขียนเข้าถึงแอตทริบิวต์ได้ตามปกติ
  • หลังจากที่ OBJECT ถูกตรึงสำหรับการเปลี่ยนแปลงแอตทริบิวต์เท่านั้น:

แนวคิดคือการแทนที่__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()
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.