Can I restrict objects in Python3 so that only attributes that I make a setter for are allowed?
The short answer is "Yes, you can."
The follow-up question is "Why?" One of the strengths of Python is the remarkable dynamism, and by restricting that ability you are actually making your class less useful (but see edit at bottom).
However, there are good reasons to be restrictive, and if you do choose to go down that route you will need to modify your __setattr__
method:
def __setattr__(self, name, value): if name not in ('my', 'attribute', 'names',): raise AttributeError('attribute %s not allowed' % name) else: super().__setattr__(name, value)
There is no need to mess with __getattr__
nor __getattribute__
since they will not return an attribute that doesn't exist.
Here is your code, slightly modified -- I added the __setattr__
method to Node
, and added an _allowed_attributes
to Definition
and Theorem
.
class Node: def __setattr__(self, name, value): if name not in self._allowed_attributes: raise AttributeError('attribute %s does not and cannot exist' % name) super().__setattr__(name, value)class Definition(Node): _allowed_attributes = '_plural', 'type' def __init__(self,dic): self.type = "definition" super().__init__(dic) self.plural = move_attribute(dic, {'plural', 'pl'}, strict=False) @property def plural(self): return self._plural @plural.setter def plural(self, new_plural): if new_plural is None: self._plural = None else: clean_plural = check_type_and_clean(new_plural, str) assert dunderscore_count(clean_plural)>=2 self._plural = clean_pluralclass Theorem(Node): _allowed_attributes = 'type', 'proofs' def __init__(self, dic): self.type = "theorem" super().__init__(dic) self.proofs = move_attribute(dic, {'proofs', 'proof'}, strict=False)
In use it looks like this:
>>> theorem = Theorem(...)>>> theorem.plural = 3Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 6, in __setattr__AttributeError: attribute plural does not and cannot exist
edit
Having thought about this some more, I think a good compromise for what you want, and to actually answer the part of your question about restricting allowed changes to setters only, would be to:
- use a metaclass to inspect the class at creation time and dynamically build the
_allowed_attributes
tuple - modify the
__setattr__
ofNode
to always allow modification/creation of attributes with at least one leading_
This gives you some protection against both misspellings and creation of attributes you don't want, while still allowing programmers to work around or enhance the classes for their own needs.
Okay, the new meta class looks like:
class NodeMeta(type): def __new__(metacls, cls, bases, classdict): node_cls = super().__new__(metacls, cls, bases, classdict) allowed_attributes = [] for base in (node_cls, ) + bases: for name, obj in base.__dict__.items(): if isinstance(obj, property) and hasattr(obj, '__fset__'): allowed_attributes.append(name) node_cls._allowed_attributes = tuple(allowed_attributes) return node_cls
The Node
class has two adjustments: include the NodeMeta
metaclass and adjust __setattr__
to only block non-underscore leading attributes:
class Node(metaclass=NodeMeta): def __init__(self, dic): self._dic = dic def __setattr__(self, name, value): if not name[0] == '_' and name not in self._allowed_attributes: raise AttributeError('attribute %s does not and cannot exist' % name) super().__setattr__(name, value)
Finally, the Node
subclasses Theorem
and Definition
have the type
attribute moved into the class namespace so there is no issue with setting them -- and as a side note, type
is a bad name as it is also a built-in function -- maybe node_type
instead?
class Definition(Node): type = "definition" ...class Theorem(Node): type = "theorem" ...
As a final note: even this method is not immune to somebody actually adding or changing attributes, as object.__setattr__(theorum_instance, 'an_attr', 99)
can still be used -- or (even simpler) the _allowed_attributes
can be modified; however, if somebody is going to all that work they hopefully know what they are doing... and if not, they own all the pieces. ;)
You can check for the attribute everytime you access it.
class Theorem(Node): ... def __getattribute__(self, name): if name not in ["allowed", "attribute", "names"]: raise MyException("attribute "+name+" not allowed") else: return self.__dict__[name] def __setattr__(self, name, value): if name not in ["allowed", "attribute", "names"]: raise MyException("attribute "+name+" not allowed") else: self.__dict__[name] = value
You can build the allowed method list dynamically as a side effect of a decorator:
allowed_attrs = [] def allowed(f): allowed_attrs.append(f.__name__) return f
You would also need to add non method attributes manually.
If you really want to prevent all other dynamic attributes. I assume there's a well-defined time window that you want to allow adding attributes.
Below I allow it until object initialisation is finished. (you can control it with allow_dynamic_attribute
variable.
class A: def __init__(self): self.allow_dynamic_attribute = True self.abc = "hello" self._plural = None # need to give default value # A.__setattr__ = types.MethodType(__setattr__, A) self.allow_dynamic_attribute = False def __setattr__(self, name, value): if hasattr(self, 'allow_dynamic_attribute'): if not self.allow_dynamic_attribute: if not hasattr(self, name): raise Exception super().__setattr__(name, value) @property def plural(self): return self._plural @plural.setter def plural(self, new_plural): self._plural = new_plurala = A()print(a.abc) # finea.plural = "yes" # fineprint(a.plural) # finea.dkk = "bed" # raise exception
Or it can be more compact this way, I couldn't figure out how MethodType + super can get along together.
import typesdef __setattr__(self, name, value): if not hasattr(self, name): raise Exception else: super().__setattr__(name,value) # this doesn't work for reason I don't knowclass A: def __init__(self): self.foo = "hello" # after this point, there's no more setattr for you A.__setattr__ = types.MethodType(__setattr__, A) a = A()print(a.foo) # finea.bar = "bed" # raise exception