Bagaimana cara membuat objek yang tidak dapat diubah dengan Python?


181

Meskipun saya tidak pernah membutuhkan ini, saya baru sadar bahwa membuat objek yang tidak dapat diubah dengan Python bisa sedikit rumit. Anda tidak bisa hanya menimpanya __setattr__, karena Anda bahkan tidak dapat mengatur atribut di __init__. Subkelas tuple adalah trik yang berfungsi:

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

Tapi kemudian Anda memiliki akses ke adan bvariabel melalui self[0]dan self[1], yang menjengkelkan.

Apakah ini dimungkinkan dengan Pure Python? Jika tidak, bagaimana saya melakukannya dengan ekstensi C?

(Jawaban yang hanya berfungsi di Python 3 dapat diterima).

Memperbarui:

Jadi subclassing tuple adalah cara untuk melakukannya dalam Pure Python, yang berfungsi dengan baik kecuali untuk kemungkinan tambahan mengakses data dengan [0], [1]dll. Jadi, untuk menyelesaikan pertanyaan ini semua yang hilang adalah bagaimana melakukannya dengan "benar" di C, yang saya menduga akan cukup sederhana, dengan hanya tidak melaksanakan setiap geititematau setattribute, dll Tapi bukannya melakukan sendiri, saya menawarkan karunia untuk itu, karena aku malas. :)


2
Bukankah kode Anda memfasilitasi akses ke atribut melalui .adan .b? Lagipula itulah sifat dari properti itu.
Sven Marnach

1
@Ven Marnach: Ya, tetapi [0] dan [1] masih berfungsi, dan mengapa mereka melakukannya? Saya tidak menginginkan mereka. :) Mungkin gagasan tentang objek abadi dengan atribut adalah omong kosong? :-)
Lennart Regebro

2
Hanya catatan lain: NotImplementedhanya dimaksudkan sebagai nilai balik untuk perbandingan kaya. Nilai pengembalian __setatt__()agak tidak berguna, karena Anda biasanya tidak akan melihatnya sama sekali. Kode suka immutable.x = 42diam-diam tidak akan melakukan apa pun. Anda harus menaikkan sebagai TypeErrorgantinya.
Sven Marnach

1
@Ven Marnach: OK, saya terkejut, karena saya pikir Anda bisa meningkatkan NotImplemented dalam situasi ini, tetapi itu memberikan kesalahan aneh. Jadi saya mengembalikannya, dan sepertinya berhasil. TypeError masuk akal begitu saya melihat Anda menggunakannya.
Lennart Regebro

1
@Lennart: Anda dapat menaikkan NotImplementedError, tetapi TypeErrormerupakan kenaikan tuple jika Anda mencoba memodifikasinya.
Sven Marnach

Jawaban:


115

Namun solusi lain yang baru saja saya pikirkan: Cara paling sederhana untuk mendapatkan perilaku yang sama dengan kode asli Anda adalah

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

Itu tidak memecahkan masalah yang atribut dapat diakses melalui [0]dll, tapi setidaknya itu jauh lebih pendek dan memberikan keuntungan tambahan yang kompatibel dengan pickledan copy.

namedtuplemenciptakan jenis yang mirip dengan apa yang saya jelaskan dalam jawaban ini , yaitu berasal dari tupledan menggunakan __slots__. Ini tersedia dalam Python 2.6 atau lebih tinggi.


7
Keuntungan dari varian ini dibandingkan dengan analog tulisan tangan (bahkan pada Python 2.5 (menggunakan verboseparameter ke namedtuplekode dengan mudah dihasilkan)) adalah antarmuka tunggal / implementasi a namedtuplelebih disukai oleh puluhan antarmuka tulisan tangan yang sedikit berbeda / implementasi yang mengimplementasikan lakukan hal yang hampir sama.
jfs

2
OK, Anda mendapatkan "jawaban terbaik", karena itu cara termudah untuk melakukannya. Sebastian mendapat hadiah karena memberikan implementasi Cython pendek. Bersulang!
Lennart Regebro

1
Karakteristik lain dari objek yang tidak dapat diubah adalah ketika Anda melewatkannya sebagai parameter melalui suatu fungsi, mereka akan disalin oleh nilai, bukan referensi lain yang dibuat. Apakah namedtupleakan disalin oleh nilai ketika melewati fungsi?
hlin117

4
@ hlin117: Setiap parameter dilewatkan sebagai referensi ke objek dengan Python, terlepas dari apakah itu bisa berubah atau tidak berubah. Untuk objek yang tidak bisa diubah, akan sangat tidak berguna untuk membuat salinan - karena Anda toh tidak dapat mengubah objeknya, Anda bisa meneruskan referensi ke objek asli.
Sven Marnach

Bisakah Anda menggunakan namestuple secara internal di dalam kelas alih-alih instantiate objek secara eksternal? Saya sangat baru untuk python tetapi keuntungan untuk jawaban Anda yang lain adalah bahwa saya dapat memiliki kelas menyembunyikan detail dan juga memiliki kekuatan hal-hal seperti parameter opsional. Jika saya hanya melihat jawaban ini, sepertinya saya perlu memiliki semua yang menggunakan instantiate kelas saya bernama tuple. Terima kasih atas kedua jawabannya.
Asaf

78

Cara termudah untuk melakukannya adalah menggunakan __slots__:

class A(object):
    __slots__ = []

Contoh dari Asekarang tidak dapat diubah, karena Anda tidak dapat menetapkan atribut apa pun padanya.

Jika Anda ingin instance kelas berisi data, Anda dapat menggabungkan ini dengan berasal dari 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

Sunting : Jika Anda ingin menyingkirkan pengindeksan, Anda dapat mengganti __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

Perhatikan bahwa Anda tidak dapat menggunakan operator.itemgetteruntuk properti dalam hal ini, karena ini akan bergantung pada Point.__getitem__()bukan tuple.__getitem__(). Terlebih lagi ini tidak akan mencegah penggunaan tuple.__getitem__(p, 0), tapi saya tidak bisa membayangkan bagaimana ini bisa menjadi masalah.

Saya tidak berpikir cara "benar" untuk membuat objek yang tidak berubah adalah menulis ekstensi C. Python biasanya bergantung pada pelaksana perpustakaan dan pengguna perpustakaan yang menyetujui orang dewasa , dan bukannya benar-benar menegakkan antarmuka, antarmuka harus dinyatakan dengan jelas dalam dokumentasi. Inilah sebabnya saya tidak mempertimbangkan kemungkinan menghindari ditimpa __setattr__()dengan memanggil object.__setattr__()masalah. Jika seseorang melakukan ini, itu adalah risikonya sendiri.


1
Bukankah lebih baik menggunakan di tuplesini __slots__ = (), bukan __slots__ = []? (Hanya mengklarifikasi)
user225312

1
@sukhbir: Saya pikir ini tidak masalah sama sekali. Mengapa Anda lebih memilih tuple?
Sven Marnach

1
@Ven: Saya setuju itu tidak masalah (kecuali bagian kecepatan, yang bisa kita abaikan), tapi saya memikirkannya seperti ini: __slots__tidak akan diubah, kan? Tujuannya adalah untuk mengidentifikasi sekali atribut mana yang dapat ditetapkan. Jadi tidak seorang tupletampak lebih alami pilihan dalam kasus seperti itu?
user225312

5
Tetapi dengan kosong __slots__saya tidak dapat mengatur atribut apa pun . Dan jika saya punya __slots__ = ('a', 'b')maka atribut a dan b masih bisa berubah.
Lennart Regebro

Tapi solusi Anda lebih baik daripada mengesampingkan, __setattr__jadi ini lebih baik daripada solusi saya. +1 :)
Lennart Regebro

50

..howto melakukannya "dengan benar" di C ..

Anda bisa menggunakan Cython untuk membuat tipe ekstensi untuk 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

Ini berfungsi baik Python 2.x dan 3.

Tes

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

Jika Anda tidak keberatan dengan dukungan pengindeksan maka collections.namedtupledisarankan oleh @ven Marnach lebih disukai :

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

@Lennart: Contoh namedtuple(atau lebih tepatnya dari jenis yang dikembalikan oleh fungsi namedtuple()) tidak berubah. Pastinya.
Sven Marnach

@Lennart Regebro: namedtuplelulus semua tes (kecuali dukungan pengindeksan). Persyaratan apa yang saya lewatkan?
jfs

Ya, Anda benar, saya membuat jenis namedTuple, instantiated, dan kemudian melakukan tes pada tipe bukan instance. Heh. :-)
Lennart Regebro

bolehkah saya bertanya mengapa orang perlu referensi lemah di sini?
McSinyx

1
@McSinyx: jika tidak, objek tidak dapat digunakan dalam koleksi lemahref. Apa sebenarnya yang ada __weakref__di Python?
jfs

40

Gagasan lain adalah sepenuhnya tidak diizinkan __setattr__dan digunakan object.__setattr__dalam konstruktor:

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

Tentu saja Anda dapat menggunakan object.__setattr__(p, "x", 3)untuk memodifikasi sebuah Pointinstance p, tetapi implementasi asli Anda mengalami masalah yang sama (coba tuple.__setattr__(i, "x", 42)pada sebuah Immutableinstance).

Anda dapat menerapkan trik yang sama dalam implementasi asli Anda: singkirkan __getitem__(), dan gunakan tuple.__getitem__()dalam fungsi properti Anda.


11
Saya tidak akan peduli dengan seseorang yang secara sengaja memodifikasi objek menggunakan superclass ' __setattr__, karena intinya bukan untuk menjadi sangat mudah. Intinya adalah untuk memperjelas bahwa itu tidak boleh dimodifikasi dan untuk mencegah modifikasi secara tidak sengaja.
zvone

18

Anda bisa membuat @immutabledekorator yang menimpa __setattr__ dan mengubah __slots__ke daftar kosong, lalu hiasi __init__metode dengan itu.

Sunting: Seperti yang dicatat OP, mengubah __slots__atribut hanya mencegah pembuatan atribut baru , bukan modifikasi.

Edit2: Berikut ini implementasinya:

Sunting3: Menggunakan __slots__istirahat kode ini, karena jika menghentikan pembuatan objek __dict__. Saya mencari alternatif.

Edit4: Yah, itu dia. Ini tapi retas, tetapi berfungsi sebagai latihan :-)

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
Membuat dekorator (kelas?) Atau metaclass keluar dari solusi memang ide yang bagus, tetapi pertanyaannya adalah apa solusinya. :)
Lennart Regebro

3
object.__setattr__()istirahatkan stackoverflow.com/questions/4828080/…
jfs

Memang. Saya hanya melanjutkan latihan sebagai dekorator.
PaoloVictor

13

Menggunakan Dataclass Beku

Untuk Python 3.7+ Anda dapat menggunakan Kelas Data dengan frozen=Trueopsi , yang merupakan cara yang sangat pythonic dan dapat dipelihara untuk melakukan apa yang Anda inginkan.

Itu akan terlihat seperti itu:

from dataclasses import dataclass

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

Karena petunjuk jenis diperlukan untuk bidang dataclasses, saya telah menggunakan Any dari typingmodul .

Alasan untuk TIDAK menggunakan Namedtuple

Sebelum Python 3.7, sering kali melihat namesuple digunakan sebagai objek yang tidak berubah. Ini bisa rumit dalam banyak hal, salah satunya adalah bahwa __eq__metode antara namesuple tidak mempertimbangkan kelas objek. Sebagai contoh:

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

Seperti yang Anda lihat, bahkan jika jenis obj1dan obj2berbeda, bahkan jika nama bidangnya berbeda, obj1 == obj2tetap memberi True. Itu karena __eq__metode yang digunakan adalah metode tuple, yang hanya membandingkan nilai bidang yang diberikan posisi mereka. Itu bisa menjadi sumber kesalahan besar, khususnya jika Anda mensubkelas kelas-kelas ini.


10

Saya tidak berpikir itu sepenuhnya mungkin kecuali dengan menggunakan tuple atau namesuple. Apa pun itu, jika Anda menimpa __setattr__(), pengguna selalu dapat memintasnya dengan menelepon object.__setattr__()langsung. Solusi apa pun yang bergantung pada __setattr__dijamin tidak akan berfungsi.

Berikut ini tentang yang terdekat yang bisa Anda dapatkan tanpa menggunakan semacam tuple:

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__

tetapi rusak jika Anda berusaha cukup keras:

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

tapi penggunaan Sven untuk namedtuple benar-benar abadi.

Memperbarui

Karena pertanyaan telah diperbarui untuk menanyakan bagaimana melakukannya dengan benar di C, inilah jawaban saya tentang bagaimana melakukannya dengan benar di Cython:

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

dan a setup.pyuntuk mengkompilasinya (menggunakan perintah 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
)

Kemudian untuk mencobanya:

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

Terima kasih untuk kode Cython, Cython luar biasa. Implementasi JF Sebastians dengan readonly lebih rapi dan tiba lebih dulu, jadi dia mendapat hadiah.
Lennart Regebro

5

Saya telah membuat kelas yang tidak dapat diubah dengan mengesampingkan __setattr__, dan mengizinkan set jika pemanggilnya adalah __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)

Ini belum cukup, karena memungkinkan siapa pun ___init__untuk mengubah objek, tetapi Anda mendapatkan idenya.


object.__setattr__()istirahatkan stackoverflow.com/questions/4828080/…
jfs

3
Menggunakan inspeksi tumpukan untuk memastikan penelepon __init__tidak terlalu memuaskan.
gb.

5

Selain jawaban lain yang sangat baik saya ingin menambahkan metode untuk python 3.4 (atau mungkin 3.3). Jawaban ini dibangun berdasarkan beberapa jawaban sebelumnya untuk pertanyaan ini.

Di python 3.4, Anda bisa menggunakan properti tanpa setter untuk membuat anggota kelas yang tidak bisa dimodifikasi. (Dalam versi sebelumnya, menugaskan ke properti tanpa setter dimungkinkan.)

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

Anda bisa menggunakannya seperti ini:

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

yang akan dicetak "constant"

Tetapi panggilan instance.a=10akan menyebabkan:

AttributeError: can't set attribute

Penjelasan: properti tanpa setter adalah fitur yang sangat baru dari python 3.4 (dan saya pikir 3.3). Jika Anda mencoba untuk menetapkan properti seperti itu, sebuah Kesalahan akan muncul. Menggunakan slot, saya membatasi variabel member untuk __A_a(yang __a).

Masalah: Menugaskan _A__amasih dimungkinkan ( instance._A__a=2). Tetapi jika Anda menetapkan ke variabel pribadi, itu adalah kesalahan Anda sendiri ...

Namun, jawaban ini antara lain tidak mendukung penggunaan __slots__. Menggunakan cara lain untuk mencegah pembuatan atribut mungkin lebih disukai.


propertytersedia di Python 2 juga (lihat kode dalam pertanyaan itu sendiri). Itu tidak membuat objek abadi, coba tes dari jawaban saya , misalnya, instance.b = 1membuat batribut baru .
jfs

Benar, pertanyaannya adalah benar-benar bagaimana mencegah melakukan A().b = "foo"yaitu tidak mengizinkan pengaturan atribut baru.
Lennart Regebro

Propertis tanpa setter memunculkan kesalahan dalam python 3.4 jika Anda mencoba untuk menugaskan ke properti itu. Dalam versi sebelumnya, setter dibuat secara tidak langsung.
Bernhard

@Lennart: Solusi saya adalah jawaban untuk subset kasus penggunaan untuk objek yang tidak dapat diubah dan tambahan untuk jawaban sebelumnya. Salah satu alasan saya mungkin menginginkan objek yang tidak dapat diubah adalah agar saya dapat membuatnya menjadi hashable, sehingga solusi saya bisa berfungsi. Tapi Anda benar, ini bukan objek abadi.
Bernhard

@ jf-sebastian: Mengubah jawaban saya untuk menggunakan slot untuk mencegah pembuatan atribut. Apa yang baru dalam jawaban saya dibandingkan dengan jawaban lain, adalah bahwa saya menggunakan properti python3.4 untuk menghindari mengubah atribut yang ada. Sementara hal yang sama dicapai dalam jawaban sebelumnya, kode saya lebih pendek karena perubahan perilaku properti.
Bernhard

5

Inilah solusi elegan :

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

Mewarisi dari kelas ini, inisialisasi bidang Anda di konstruktor, dan Anda siap.


1
tetapi dengan logika ini memungkinkan untuk menetapkan atribut baru ke objek
javed

3

Jika Anda tertarik pada objek dengan perilaku, maka namesuple hampir menjadi solusi Anda.

Seperti dijelaskan di bagian bawah dokumentasi namedtuple , Anda dapat menurunkan kelas Anda sendiri dari namedtuple; dan kemudian, Anda bisa menambahkan perilaku yang Anda inginkan.

Misalnya (kode diambil langsung dari dokumentasi ):

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)

Ini akan menghasilkan:

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

Pendekatan ini berfungsi untuk Python 3 dan Python 2.7 (diuji pada IronPython juga).
Satu-satunya downside adalah bahwa pohon warisan agak aneh; tapi ini bukan sesuatu yang biasa kamu mainkan.


1
Python 3.6+ mendukung ini secara langsung, menggunakanclass Point(typing.NamedTuple):
Elazar

3

Kelas yang mewarisi dari Immutablekelas berikut tidak dapat diubah, seperti contoh mereka, setelah __init__metode mereka selesai dijalankan. Karena itu adalah python murni, seperti yang telah ditunjukkan orang lain, tidak ada yang menghentikan seseorang untuk menggunakan metode khusus bermutasi dari basis objectdantype , tetapi ini cukup untuk menghentikan siapa pun dari bermutasi kelas / instance secara tidak sengaja.

Ini bekerja dengan membajak proses pembuatan kelas dengan metaclass.

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

Saya membutuhkan ini beberapa waktu yang lalu dan memutuskan untuk membuat paket Python untuk itu. Versi awal ada di PyPI sekarang:

$ pip install immutable

Menggunakan:

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

Dokumentasi lengkap di sini: https://github.com/theengineear/immutable

Semoga ini bisa membantu, ia membungkus sebuah namesuple seperti yang telah dibahas, tetapi membuat instantiasi menjadi lebih sederhana.


2

Cara ini tidak berhenti object.__setattr__bekerja, tetapi saya masih merasa berguna:

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)

Anda mungkin perlu mengganti lebih banyak barang (seperti __setitem__) tergantung pada use case.


Saya datang dengan sesuatu yang serupa sebelum saya melihat ini, tetapi digunakan getattrsehingga saya bisa memberikan nilai default untuk frozen. Hal-hal yang disederhanakan sedikit. stackoverflow.com/a/22545808/5987
Mark Ransom

Saya suka pendekatan ini yang terbaik, tetapi Anda tidak perlu __new__menimpanya. Di dalam __setattr__hanya mengganti kondisi denganif name != '_frozen' and getattr(self, "_frozen", False)
Pete Cacioppi

Juga, tidak perlu membekukan kelas saat konstruksi. Anda dapat membekukannya kapan saja jika Anda memberikanfreeze() fungsi. Objek kemudian akan "dibekukan sekali". Akhirnya, mengkhawatirkan object.__setattr__itu konyol, karena "kita semua dewasa di sini".
Pete Cacioppi

2

Pada Python 3.7, Anda dapat menggunakan @dataclassdekorator di kelas Anda dan itu tidak akan berubah seperti struct! Meskipun, itu mungkin atau mungkin tidak menambahkan __hash__()metode ke kelas Anda. Mengutip:

hash () digunakan oleh hash built-in (), dan ketika objek ditambahkan ke koleksi hash seperti kamus dan set. Memiliki hash () menyiratkan bahwa instance kelas tidak dapat diubah. Mutability adalah properti rumit yang tergantung pada niat programmer, keberadaan dan perilaku eq (), dan nilai-nilai dari eq dan flag beku di dekorator dataclass ().

Secara default, dataclass () tidak akan secara implisit menambahkan metode hash () kecuali aman untuk melakukannya. Tidak juga akan menambah atau mengubah metode hash () yang ada secara eksplisit . Mengatur atribut kelas hash = Tidak ada yang memiliki arti khusus untuk Python, seperti yang dijelaskan dalam dokumentasi hash ().

Jika hash () tidak didefinisikan secara eksplisit, atau jika diatur ke Tidak ada, maka dataclass () dapat menambahkan metode hash () implisit . Meskipun tidak disarankan, Anda dapat memaksa dataclass () untuk membuat metode hash () dengan unsafe_hash = True. Ini mungkin terjadi jika kelas Anda secara logika tidak dapat diubah tetapi tetap dapat dimutasi. Ini adalah kasus penggunaan khusus dan harus dipertimbangkan dengan cermat.

Berikut ini contoh dari dokumen yang ditautkan di atas:

@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
Anda perlu menggunakan frozen, yaitu @dataclass(frozen=True), tetapi pada dasarnya memblokir penggunaan __setattr__dan __delattr__suka di sebagian besar jawaban lain di sini. Itu hanya melakukannya dengan cara yang kompatibel dengan opsi lain dari kacamata.
CS

2

Anda bisa mengesampingkan setattr dan masih menggunakan init untuk mengatur variabel. Anda akan menggunakan kelas super setattr . ini kodenya.

kelas Tidak Berubah:
    __slots__ = ('a', 'b')
    def __init __ (self, a, b):
        super () .__ setattr __ ('a', a)
        super () .__ setattr __ ('b', b)

    def __str __ (mandiri):
        return "" .format (self.a, self.b)

    def __setattr __ (mandiri, * diabaikan):
        meningkatkan NotImplementedError

    def __delattr __ (mandiri, * diabaikan):
        meningkatkan NotImplementedError

Atau passalih-alihraise NotImplementedError
jonathan.scholbach

Ini sama sekali bukan ide yang baik untuk melakukan "lulus" dalam __setattr__ dan __delattr__ dalam kasus ini. Alasan sederhana adalah bahwa jika seseorang memberikan nilai pada bidang / properti, maka mereka secara alami berharap bahwa bidang tersebut akan berubah. Jika Anda ingin mengikuti jalan "paling tidak mengejutkan" (seperti seharusnya), maka Anda harus memunculkan kesalahan. Tapi saya tidak yakin apakah NotImplementedError adalah yang tepat untuk dimunculkan. Saya akan mengangkat sesuatu seperti "Field / property is immutable." kesalahan ... Saya pikir pengecualian khusus harus dibuang.
darlove

1

attrModul pihak ketiga menyediakan fungsionalitas ini .

Sunting: python 3.7 telah mengadopsi gagasan ini ke dalam stdlib with @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

attrmengimplementasikan kelas beku dengan mengesampingkan __setattr__dan memiliki dampak kinerja kecil pada setiap waktu instantiation, menurut dokumentasi.

Jika Anda terbiasa menggunakan kelas sebagai tipe data, attrmungkin sangat berguna karena menangani boilerplate untuk Anda (tetapi tidak melakukan sihir apa pun). Secara khusus, ia menulis sembilan metode dunder (__X__) untuk Anda (kecuali Anda mematikannya), termasuk repr, init, hash dan semua fungsi perbandingan.

attrjuga menyediakan pembantu untuk__slots__ .


1

Jadi, saya menulis masing-masing python 3:

I) dengan bantuan dekorator kelas data dan set beku = True. kita dapat membuat objek yang tidak dapat diubah dengan python.

untuk ini perlu mengimpor data kelas dari kelas data lib dan perlu menetapkan beku = True

ex.

dari dataclasses impor dataclass

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

o / p:

l = Lokasi ("Delhi", 112.345, 234.788) l.name 'Delhi' l.panjang 112.345 l.latitude 234.788 l.name = dataclasses Kolkata.FrozenInstanceError: tidak dapat menetapkan ke bidang 'nama'

Sumber: https://realpython.com/python-data-classes/


0

Pendekatan alternatif adalah membuat pembungkus yang membuat instance menjadi tidak dapat diubah.

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)

Ini berguna dalam situasi di mana hanya beberapa instance harus tidak berubah (seperti argumen default panggilan fungsi).

Dapat juga digunakan di pabrik yang tidak berubah seperti:

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

Juga melindungi dari object.__setattr__, tetapi jatuh ke trik lain karena sifat dinamis Python.


0

Saya menggunakan ide yang sama dengan Alex: kelas-meta dan "init marker", tetapi dalam kombinasi dengan __setattr__ yang ditulis berlebihan:

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

Catatan: Saya memanggil meta-class secara langsung untuk membuatnya berfungsi baik untuk Python 2.x dan 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.

Ini juga berfungsi dengan slot ...:

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

... dan banyak warisan:

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

Perhatikan, bagaimanapun, atribut yang bisa berubah tetap dapat berubah:

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

0

Satu hal yang tidak benar-benar dimasukkan di sini adalah total immutability ... bukan hanya objek induk, tetapi semua anak juga. tuple / frozenset mungkin tidak dapat diubah misalnya, tetapi objek yang menjadi bagiannya mungkin tidak berubah. Berikut adalah versi kecil (tidak lengkap) yang melakukan pekerjaan yang layak untuk menegakkan keabadian sepanjang jalan:

# 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

Anda bisa mengesampingkan setAttr dalam pernyataan akhir init. Kemudian Anda dapat membangun tetapi tidak berubah. Jelas Anda masih bisa menimpa oleh objek usint. setAttr tetapi dalam praktiknya sebagian besar bahasa memiliki beberapa bentuk refleksi sehingga ketidakmampuan selalu merupakan abstraksi yang bocor. Kekekalan lebih lanjut tentang mencegah klien dari secara tidak sengaja melanggar kontrak suatu objek. Saya menggunakan:

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

Solusi asli yang ditawarkan salah, ini diperbarui berdasarkan komentar menggunakan solusi dari sini

Solusi asli salah dengan cara yang menarik, sehingga termasuk di bagian bawah.

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

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

Output:

1
2
Attempted To Modify Immutable Object
1
2

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

Implementasi Asli:

Itu ditunjukkan dalam komentar, dengan benar, bahwa ini sebenarnya tidak berhasil, karena mencegah penciptaan lebih dari satu objek saat Anda mengganti metode setattr kelas, yang berarti yang kedua tidak dapat dibuat sebagai self.a = akan gagal pada inisialisasi kedua.

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
Itu tidak akan berhasil: Anda mengganti metode di kelas , sehingga Anda akan mendapatkan NotImplementedError segera setelah Anda mencoba membuat instance kedua.
slinkp

1
Jika Anda ingin mengejar pendekatan ini, perhatikan bahwa sulit untuk mengganti metode khusus saat runtime: lihat stackoverflow.com/a/16426447/137635 untuk beberapa solusi untuk ini.
slinkp

0

Solusi dasar di bawah ini membahas skenario berikut:

  • __init__() dapat ditulis mengakses atribut seperti biasa.
  • SETELAH OBJECT dibekukan untuk perubahan atribut saja:

Idenya adalah untuk mengganti __setattr__metode dan mengganti implementasinya setiap kali status beku objek diubah.

Jadi kita memerlukan beberapa metode ( _freeze) yang menyimpan dua implementasi dan sakelar di antara keduanya saat diminta.

Mekanisme ini dapat diterapkan di dalam kelas pengguna atau diwarisi dari Freezerkelas khusus seperti yang ditunjukkan di bawah ini:

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

Seperti a dict

Saya memiliki perpustakaan open source di mana saya melakukan hal-hal dengan cara fungsional sehingga memindahkan data di dalam objek yang tidak berubah sangat membantu. Namun, saya tidak ingin harus mengubah objek data saya agar klien dapat berinteraksi dengan mereka. Jadi, saya datang dengan ini - ini memberi Anda dict seperti objek thats abadi + beberapa metode pembantu.

Penghargaan untuk Sven Marnach dalam jawabannya atas implementasi dasar pembatasan pembaruan dan penghapusan properti.

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__

Metode pembantu

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)

Contohnya

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
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.