1

I am trying to create the following relationships in Python:

  • A can be followed by B or C
  • B can be followed by A
  • C can be followed by A

My current approach is based on this class

class Element:
    def __init__(self, name):
        self.name = name
        self.followers = {}

    def add_follower(self, follower):
        self.followers[follower.name] = follower
        setattr(self, follower.name.lower(), follower)

    def help(self):
        return f"Help for {self.name}"

element_a = Element('A')
element_b = Element('B')
element_c = Element('C')

element_a.add_follower(element_b)
element_a.add_follower(element_c)
element_b.add_follower(element_a)
element_c.add_follower(element_a)

This works nicely. When I type element_a.b.a.c.help() in the REPL, I get 'Help for C'. Furthermore, while typing in the REPL, e.g. element_a.b.a. I get b and c as completion candidates which is exactly what I want (always seeing what the next allowed elements are).

However, now I would like to have a history method which returns the taken sequence, e.g. element_a.b.a.c.history() should return ["A", "B", "A", "C"]. And this is where I am stuck.

My first idea was to have add_follower() to set a parent attribute which I could use to reconstruct the chain but this did not work. Calling ....history() would only return the last element, e.g. ...a.c.history() returns ["A"].

As far as I understand, this makes sense, because add_follower() creates a reference to the object and creates it only once. Thus, the parent will only be set one time.

Thus, is there a way to

  • set the relationship between the elements and have them available while typing (completion candidates)
  • recunstruct the sequence upon demand?

Thanks a lot!

1 Answer 1

1

That seems quite complicated and convoluted.

What you are doing directly is not possible: you have only one "A" in all this. So, there can't be a different response in element_a.history() and element_a.b.a.b.a.c.a.history(). Those are the same object.

Or more simply, element_a.b is the same object as b, so element_a.b.history() cannot be different than b.history()

So, if you need it to be different, element_a.b must not return element_b. It must return a new b. A new element whose name is b. And that has a pointer to its ancestor element_a, so that it is able to tells its history.

So element_a must have an attribute b and an attribute c whose ancestor is element_a.
And element_a.b must have an attribute a whose ancestor is element_a.b. So obviously not the same as element_a
And that element_a.b.a must have an attribute b and an attribute c whose ancestor are element_a.b.a. Etc.

All those are different instances, with different ancestors. Otherwise, no specific history.

It could be tempting to do something like

class Element:
    def __init__(self, name, ancestor=None):
        self.name=name
        self.daddy=ancestor
        if name=='A':
            self.b=Element('B', self)
            self.c=Element('C', self)
        elif name=='B':
            self.a=Element('A', self)
        elif name=='C':
            self.a=Element('A', self)

But then, of course, the construction itself enters an infinite recursion loop. Which, after all, makes sense, since this is exactly what you want: to have implicitly defined an infinite tree of attributes.

So, the only possibility I see is that the element is created only when accessing it (which is implicily what you want, when you talk about the REPL completion... I don't get exactly what you are trying to do. But it looks like some game with the interactive interpreter)

This is possible through __getattr__ and __getattribute__.

So, a closer possibility would be

class Element:
    def __init__(self, name, daddy=None):
        self.name=name
        self.daddy=daddy
    def __getattr__(self, x):
        if x in ['a', 'b', 'c']:
            return Element(x.upper(), self)
    def history(self):
        res=[]
        s=self
        while s is not None:
            res.append(s.name)
            s=s.daddy
        return res[::-1]

Then you can try Element('A').a.b.a.c.a.c.a.b.a.history().

Sure, I haven't done anything yet to prevent Element('A').a.a.a.b.b.b.history() to work as well. I could (in getattr for example). But that is not the correct way yet, since here we don't have something that seems important to you: the b and c completion when trying to "TAB" from a A value.

Because there is no attribute b and c to Element('A'). They will be created only when we access them. But they don't exist before. So, they have no reason to appear in the list of attributes for the completion.

So for that, we would like to have those attributes existing

class Element:
    def __init__(self, name, daddy=None):
        self.name=name
        self.daddy=daddy
        if name=='A':
             self.b=True
             self.c=True
        elif name in ['B', 'C']:
             self.a=True
    def __getattr__(self, x):
        if x in ['a', 'b', 'c']:
            return Element(x.upper(), self)
    def history(self):
        res=[]
        s=self
        while s is not None:
            res.append(s.name)
            s=s.daddy
        return res[::-1]

Here attributes exist for REPL. But when I try a=Element('A') and then a.b I just get True. And therefore a.b.history() just fails.

That is because __getattr__ is not even called when attribute already exist.

So, we need the next thing: __getattribute__ which is trickier to handle. __getattribute__ is called even if attribute exist. Which means that any attribute we use anywhere in the code (.name, .history(), .daddy, ...) goes through this method. So it better be working. But if this method returns self.name when it is asked for value of name attribute... it calls itself, since self.name is self.__getattribute__('name'), and we fall back in an endless recursion loop.

The method is to call explicitly the object.__getattribute__ generic getattribute method to access our attributes.

But to make an execption for a, b and c attributes.

Hence my answer:

class Element:
    def __init__(self, name, daddy=None):
        self.name = name
        self.daddy = daddy
        if name=='A':
            self.b=True
            self.c=True
        elif name=='B':
            self.a=True
        elif name=='C':
            self.a=True

    def __getattribute__(self, x):
        r=object.__getattribute__(self, x)
        if x in ['a','b','c']:
            return Element(x.upper(), self)
        return r

    def history(self):
        l=[]
        t=self
        while t is not None:
            l.append(t.name)
            t=t.daddy
        return l[::-1]

    def help(self):
        return f"Help for {self.name}"

a=Element("A")
b=Element("B")
c=Element("C")

So here a.b.a.b.a.c.a.b.history() works. And a.a.history() wouldn't.

The existence of attributes a, b, c (depending on the names) ensure that the REPL completion works.

The fact that we return the actual value of the attributes in __getattribute__ (that is called even if those attributes exists) ensure that it is not the True value that we return, but a new Element. If we try to get an attribute that doesn't exist, object.__getattribute__ will raise an error, as expected.

And because I put the r=object.__getattribute__(self,x) before dealing with the a,b,c exceptions, I ensure that it will raise an error also if we are trying to access a,b or c when it is not supposed to exist. So I don't have to reimplement here the succession rules. All I need to do is "if this is one of the special attribute, and I am still alive — if it wasn't authorized, I would have failed before, in object.__getattribute__ — then this is how we access those special attributes"

If I had written

def __getattribute__(self, x):
        if x in ['a','b','c']:
            return Element(x.upper(), self)
        return object.__getattribute__(self, x)

instead, then a.a.history() would return ['A', 'A']. Even tho a.<TAB> would not show a as a possible completion for a., a.a would still work once typed anyway without the use of completion. But because I called object.__getattribute__ before, the call would fail even before reaching the if x in ... when trying to access to a.a

So, the attributes a, b, c whose value is True serve 2 purposes

  • Ensure that they appear in attributes list, so that completion shows them
  • Enforce the "successor" rule: if that attribute is not present, not only it would not appear in the completion list, but because we called inconditionaly (even for the special cases of a,b,c) object.__getattribute__ an access to an illegal successor would fail
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.