1

I'm trying to construct a class that allows an instance to point to another class, but I want these to eventually form a loop (so instance A → Instance B → Instance C → Instance A)

I tried the following, but I'm getting a NameError:

class CyclicClass:
    def __init__(self, name, next_item):
        self.name = name
        self.next_item = next_item

    def print_next(self):
        print(self.next_item)

item_a = CyclicClass("Item A", item_b)
item_b = CyclicClass("Item B", item_a)

Is this an inappropriate pattern in Python? If so, what would be the correct way to implement this? This seems similar but not the same as the following, since the class definition itself is not circular: Circular dependency between python classes

martineau
  • 119,623
  • 25
  • 170
  • 301
Allen Wang
  • 2,426
  • 2
  • 24
  • 48

2 Answers2

7

You need to create the objects first, then link them.

item_a = CyclicClass("Item A", None)
item_b = CyclicClass("Item B", item_a)
item_a.next_item = item_b

Think of the second argument to CyclicClass as a convenience, rather than the primary way of linking two objects. You can emphasize that by making None the default parameter value.

class CyclicClass:
    def __init__(self, name, next_item=None):
        self.name = name
        self.next_item = next_item

    def print_next(self):
        print(self.next_item)

item_a = CyclicClass("Item A")
# item_b = CyclicClass("Item B")
# item_b.next_item = item_a
item_b = CyclicClass("Item B", item_a)
item_a.next_item = item_b
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Seems like this is something they could improve with the python interpreter? – Allen Wang Jun 02 '17 at 19:55
  • 3
    @AllenWang How could they? And how would this work? You want to interpreter to have some sort of "wildcard" value for names that aren't defined yet? – juanpa.arrivillaga Jun 02 '17 at 19:55
  • How does the interpreter see functions that are defined further down in the code? – Allen Wang Jun 02 '17 at 19:58
  • @AllenWang Python is interpreted from the top of the file to the bottom. The interpreter can't know about an object you haven't defined yet. – Will Da Silva Jun 02 '17 at 19:58
  • 1
    @AllenWang it works the same way. Try to call a function before you define it, or refer to it in any way for that matter. – juanpa.arrivillaga Jun 02 '17 at 19:59
  • @AllenWang It doesn't; names aren't looked up until they are actually *used*. – chepner Jun 02 '17 at 19:59
  • 1
    @AllenWang If `foo` calls `bar`, you don't have to *define* `bar` before you define `foo`, but you certainly have to define `bar` before you *call* `foo`. – chepner Jun 02 '17 at 19:59
  • 1
    @AllenWang I think maybe the confusing part is that a *function body* doesn't actually get executed until the function is *called*. So you can have undefined names in the function body as long as they are defined before the function is called. – juanpa.arrivillaga Jun 02 '17 at 20:01
1

Reading the comments on chepner's answer, it looks like you want a lazy approach to be able to do the binding. Please note that allowing late assignment of "next_item" as in the other answer is still the "right thing to do".

That can easily be done, but them, you'd still depend on the hardcoded other-instance name. Some ORM frameworks for example, since they allow one to define inter-relationships between classes, allow you to insert other classes as strings rather than actual class objects.

But strings will work nice for objects defined on a module top-level, since they can be fetched as a global variable - but won't work if you are using your cyclic class inside a function or method. A callable that will return the non-local variable with the instance name could work:

from types import FunctionType

class CyclicClass:
    def __init__(self, name, next_item=None):
        self.name = name
        self.next_item = next_item

    def print_next(self):
        print(self.next_item)


    @property
    def next_item(self):
        if isinstance(self._next_item, FunctionType):
            self._next_item = self._next_item()
        return self._next_item
    @next_item.setter

    def next_item(self, value):
        self._next_item = value

And testing on the interactive interpreter:

In [23]: def test():
    ...:     inst1 = CyclicClass("inst1", lambda: inst2)
    ...:     inst2 = CyclicClass("inst2", inst1)
    ...:     return inst1, inst2
    ...: 

In [24]: i1, i2 = test()

In [25]: i1.next_item.name
Out[25]: 'inst2'

But that approach is rather naive - and won't work if you re putting yur isntances into a list or other data-structures, unless you have a good timing triggering the attribute rendering into a real reference - at which point it is just better to allow late assignment to next_item anyway.

Not that if the "name" attributes are meant to be unique, you could modify the code above to have a global-registry of all your instances, and pass a string to identify your instances. That might suit your needs - but still will add more complications than allowing a late setting of next_item

cyclic_names = {}

class CyclicClass:
    ...

    @property
    def next_item(self):
        if isinstance(self._next_item, str):
            self._next_item = cyclic_names[self._next_item]
        return self._next_item
    @next_item.setter

    def next_item(self, value):
        self._next_item = value

    @property
    def name(self):
        return self._name

    @name.setter(self)
    def name(self, value):
        if hasattr(self, "_name"):
            del cyclic_names[value]
        if value in cyclic_names:
            raise ValueError("An object with this name already exists")

        cyclic_names[value] = self

    def __del__(self):
        del cyclic_names[self._name]

As you can see, the complexity for doing this work properly escalates quickly, and it may be a source of defects in your project - but still can be done if one thinks up of all nuances. (I'd use weakrefs on the global object index, for example)

Allen Wang
  • 2,426
  • 2
  • 24
  • 48
jsbueno
  • 99,910
  • 10
  • 151
  • 209