Reputation: 113
What's the best way in python to create a subclass of an existing class, in such a way that a custom error is raised whenever you attempt to modify the object?
The code below shows what I want.
class ImmutableModifyError(Exception):
pass
class ImmutableList(list):
def __init__(self, err = "", *argv):
self.err = err
super().__init__(*argv)
def append(self, *argv):
raise ImmutableModifyError(self.err)
def extend(self, *argv):
raise ImmutableModifyError(self.err)
def clear(self, *argv):
raise ImmutableModifyError(self.err)
def insert(self, *argv):
raise ImmutableModifyError(self.err)
def pop(self, *argv):
raise ImmutableModifyError(self.err)
def remove(self, *argv):
raise ImmutableModifyError(self.err)
def sort(self, *argv):
raise ImmutableModifyError(self.err)
def reverse(self, *argv):
raise ImmutableModifyError(self.err)
If I use other immutable types, an AttributeError is thrown instead of the custom error whose message is created along the object. As you can see, this code is too repetitive and would be error-prone if the class changed. I have not taken into account hidden methods and operators. Is there a better way to achieve this?
Upvotes: 1
Views: 38
Reputation: 197
There are plenty of more scalable and less error-prone way to achieve this is to dynamically block all mutating methods.
Setattr
to indentify all mutating methods dynamically and override them [Best Method to implement - Pycon 2017]class ImmutableModifyError(Exception):
pass
class ImmutableList(list):
def __init__(self, *args, err="Immutable list cannot be modified"):
self.err = err
super().__init__(*args)
def __raise_error(self, *args, **kwargs):
raise ImmutableModifyError(self.err)
_mutating_methods = {
"append", "extend", "clear", "insert", "pop",
"remove", "sort", "reverse", "__setitem__", "__delitem__",
"__iadd__", "__imul__"
}
for met in _mutating_methods:
setattr(ImmutableList, method, ImmutableList.__raise_error) ## Don't use map here
class ImmutableModifyError(Exception):
pass
class ImmutableMeta(type):
def __new__(cls, name, bases, namespace):
def raise_error(self, *args, **kwargs):
raise ImmutableModifyError(f"Cannot modify {self.__class__.__name__} object")
mutating_methods = {
attr for base in bases
for attr in dir(base)
if callable(getattr(base, attr, None)) and
attr in {"__setitem__", "__delitem__", "__iadd__", "__imul__",
"append", "extend", "clear", "insert", "pop",
"remove", "sort", "reverse"}
}
for method in mutating_methods:
namespace[method] = raise_error
return super().__new__(cls, name, bases, namespace)
class ImmutableList(list, metaclass=ImmutableMeta):
pass
One of benefits you get that if you want to use a dict
instead of list
just one line of code is needed
class ImmutableDict(dict, metaclass=ImmutableMeta):
pass
getitem
[most least popular not safe ](for x in lst)
will also fail.class ImmutableModifyError(Exception):
pass
class ImmutableList(list):
def __init__(self, *args, err="Immutable list cannot be modified"):
self.err = err
super().__init__(*args)
def __getitem__(self, index):
raise ImmutableModifyError(self.err)
One example why it is not good to implement is as it breaks Read-Only Operations and if you override getitem, you won't be able to retrieve values anymore:
lst = ImmutableList([1, 2, 3])
print(lst[0]) # Should return 1, but would raise an error instead.
Upvotes: 2