Search code examples
python-3.xinheritancesetattr

Allow super to control __setattr__ on subclasses


This question is about the read-only problem for objects that are based on a super() and if/how super can/should control __setattr__ on subclasses.

Context:

Is there a way to write a meta class or a descriptor such that all classes that are subclasses of a class containing the attribute self.read_only = True cannot execute subclassed functions where getattr is starting with "set_", but where self.read_only = False can?

I'm thinking that an override of object.__setattr__(self, name, value):

Called when an attribute assignment is attempted. This is called instead of the normal mechanism (i.e. store the value in the instance dictionary). name is the attribute name, value is the value to be assigned to it.

...is the right direction, but am in doubt whether my interpretation of the documentation is correct.

Example:

Super as intended by the system designer:

class BaseMessage(object):
    def __init__(self, sender, receiver, read_only=True):
        self.sender = sender
        self.receiver = receiver
        self.read_only = read_only

    def __setattr__(self, name, value):
        if self.read_only:
            raise AttributeError("Can't set attributes as message is read only.")
        else:
            # ? set attribute...(suggestion welcome)

    def get_sender(self):  # Is a "get" and not "set" call, so it should be callable disregarding self.read_only's value.
        return self.sender

    def get_receiver(self):
        return self.receiver

Sub made by the system-extender who has limited understanding of all consequences:

class MutableMessage(BaseMessage):
    def __init__(self, sender, receiver, topic, read_only=False):
        super().__init__(sender=sender, receiver=receiver, read_only=read_only)
        self.topic = topic

    # this call should be okay as super's property is read_only=False.
    def set_topic_after_init(new_topic):
        self.topic = topic

class ImmutableMessage(BaseMessage):
    def __init__(self, sender, receiver, topic):  # read_only=True !
        super().__init__(sender=sender, receiver=receiver, read_only=read_only)
        self.topic = topic

    # this call should fail as super's property is read_only=True.
    def set_topic_after_init(new_topic):  
        self.topic = topic

Commentary to example

In the MutableMessage the system-extender explicitly declares that read_only is False and is knowingly aware that of the consequences of adding the function set_topic.

In the ImmutableMessage (below), the system-extender forgets to declare that message should be read_only=False which should result in supers __setattr__ to raise AttributeError:

Core question: Will a usage as shown in the example below suffice to apply consistently to all classes who are based on the BaseMessage class?

Think of me as new to meta-programming. Therefore an explanation of any misunderstandings and/or extension and correction of my example would be supreme. I understand the hierarchy [1] but do not have insight to what python does behind the curtains during the inheritance process.

Thanks...

[1]: The hierarchy

The search order that Python uses for attributes goes like this:

  1. __getattribute__ and __setattr__
  2. Data descriptors, like property
  3. Instance variables from the object's __dict__
  4. Non-Data descriptors (like methods) and other class variables
  5. __getattr__

Since __setattr__ is first in line, if you have one you need to make it smart unless want it to handle all attribute setting for your class. It can be smart in either of two ways.

a. Make it handle a specific set attributes only, or,

b. make it handle all but some set of attributes.

For the ones you don't want it to handle, call super().__setattr__.

Related questions:


Solution

  • This kinda works:

    class BaseMessage(object):
        def __init__(self, sender, receiver, read_only=True):
            self._sender = sender
            self._receiver = receiver
            self.read_only = read_only
    
        def __setattr__(self, name, value):
            # Have to use gettattr - read_only may not be defined yet.
            if getattr(self,"read_only",False):
                raise AttributeError("Can't set attributes as message is read only.")
            else:
                self.__dict__[name] = value
    
        def get_sender(self):    # Is a "get" and not "set" call, so it should be callable 
            return self._sender  # disregarding self.read_only's value.
    
        def get_receiver(self):
            return self._receiver
    
    
    class ImmutableMessage(BaseMessage):
        def __init__(self, sender, receiver, topic):  # read_only=True !
            # Have to make it read-write to start with
            super().__init__(sender=sender, receiver=receiver, read_only=False)
            # ... so we can set the topic
            self.topic = topic
            # Now make it read-only
            self.read_only = True
    
        # this call should fail as super's property is read_only=True.
        def set_topic_after_init(self,new_topic):  
            self.topic = new_topic
    
    • Setting the attribute is easy (just set __dict__).
    • Obviously none of this will stop somebody deliberating overriding __setattr__.
    • The biggest wart is that you have to create the subclass as read-write so that you can create the attributes in the first place, and then mark it read-only. I can't think of a way to automate that.
      • You might be able to make the super.__init__ call the last call in the derived __init__ - but that doesn't feel very natural.
      • Alternatively, __setattr__ could walk the stack and discover if it is being called from __init__ - but that feels very hairy.