I'm currently trying to create a class that inherits from set, but allows calling for subsets with attributes.
Since I want to create this class such that I can use it in any context, I want to have it be able to create attributes from any given string. I have succeeded in this, but I want the subsets to only be changed via a method, such that it also adds them to the 'main' set. Otherwise, someone could add items to a subset without adding them to the main.
Right now someone could simply set the attributes after they are created. Is there any way to create read-only attributes dynamically?
This is what I have so far:
class SetWithSubset(set):
"""
Set should only be initialized with arguments if they should not be
in a subset. Read Only.
"""
def create_subset(self, name, subset = None):
if type(subset) != set:
subset = set()
self.update(subset)
setattr(self, name, subset)
def add_to_subset(self, name, element):
getattr(self, name).add(element)
self.add(element)
I have read things about changing __setattr__, but if I change that to raise an exception, it also raises the error when the method tries to change it.
Edit: There was an unrelated problem in the code which I changed
Limited (im)mutability
As I am sure you are aware already, there isn't really any way to make something truly immutable (or "read-only") in Python. All you can do, is make it more difficult (or more involved) to change attributes of objects. So no approach will guarantee you fool-proof read-only attributes.
If you want, you can actually meaningfully override the
__setattr__method like this:But your approach has a major drawback IMHO. Dynamically setting attributes (via the
setattrfunction) makes the true interface of the object more opaque to any static type checker. This includes your friendly IDE engine. So you can say goodbye to many helpful auto-suggestions or warnings, when you try to access attributes that actually exist or don't exist on the given object.Additionally, while the "main" set does not allow re-setting existing subsets, nothing prevents you from simply mutating them, if they are simply accessible as attributes of your main set, without this having any effect on the main set:
Allow me to suggest a slight shift in paradigm and offer a different implementation that may accomplish what you seem to be aiming at.
Inherit from the ABC
The first change I would suggest is inheriting from the abstract base class
collections.abc.MutableSetinstead ofbuiltins.set. This is just good practice when emulating built-in types and gives you more explicit control over how the core methods of your class behave. All you need to do, is implement the five abstract methods listed here and you get all the rest of the expectedset-methods (exceptupdatefor some reason) automatically mixed-in.Internally you would still keep a regular old
setas the container for all the items as a protected attribute. Thus you should also define your own__init__method.Subsets in a dictionary
Secondly, instead of dynamically assigning attributes to an instance of your class, keep a dedicated internal dictionary mapping subset names to subset instances. If the user tries to create a new subset with a name that already exists as a key in that dictionary, you can raise an error. But internally you can still easily mutate that dictionary as you deem necessary.
Keep a parent reference
You can link nested sets by allowing a parent set to be defined upon instantiation. Then every method that mutates the set can explicitly call the same method with the same arguments on its parent set. If your nested sets are all instances of your custom class, this allows theoretically unlimited nesting of subsets.
If you are careful about ensuring a set is always mutated in tandem with its parent, you can be sure that a parent will always essentially reflect the union of its subsets.
This is rather easy in terms of adding items to a subset, but may become a bit trickier with removing items because even though one subset may want to discard an item, the parent may have another subset that still holds on to the same item. How you want to deal with this is really up to you and your desired outcome. You may e.g. simply disallow discarding elements or you may on the other hand propagate removal to all subsets as well as the parent.
Make it subscriptable
If you want, you can additionally define the
__getitem__and__setitem__methods on your class as the mechanism for accessing existing or creating new subsets respectively. That way you'll partially expose the interface of the internal dictionary and create a sort of hybrid between a mutable set and a (sort-of-not-really mutable) Mapping.That way creating a subset is as easy as plain
dictassignment via subscript (the[]brackets) and you can e.g. raise an error if a subset with the given name/key already exists. And adding items to a subset becomes as easy asdictaccess and then calling the desired method on the returned subset (add,updateor what have you).Make it generic and annotate it properly
Using the Python's type annotation capabilities as consistently as possible is good style anyway and it serves a very obvious practical purpose. The more static type checkers know about the objects you are dealing with, the better they can help you by providing useful auto-suggestions, warnings, inspection etc.. Luckily there is not much to do here, since the ABC is generic already, you just need to consistently type-hint all relevant methods with a type variable of your choice.
Example implementation
Demo
Variations
Obviously, you can use any reasonable combination of the suggestions and approaches I laid out above. If you don't like the
dict-like access and instead want regular methods for creating/getting subsets, you can easily adjust the methods accordingly.If you don't care that type checkers and IDEs may be confused or complain, you can of course also define your own
__setattr__and__getattr__in a similar fashion as the methods shown above.Also, instead of raising an error when someone tries to call the "setter" method (whatever it may be in the end), you may consider implementing some mechanism that mutates that set or discards it (and its elements from the parent) and replaces it.
There are countless options. Hopefully some of this is useful.