How can I resolve circular references between two instances of a class in Python?

154 Views Asked by At

I have two instances of a class that compete in a simulation where they try to shoot at each other. The class contains a position variable and a target variable. The second instance's target variable references the first instance's position object, and vice-versa.

I have a chicken-and-egg problem when creating the two instances, and when trying to bind the references after instantiating, they don't update properly

This is a simplified version of my code:

class Thing():
    def __init__(self, position, target):
        self.position = position
        self.target = target

    def move(self):
        self.position += 10

## thing1 = Thing(position = 0, target = thing2.position)   # Ideally this line would work...
thing1 = Thing(position = 0, target = 0)                      
thing2 = Thing(position = 100, target = thing1.position)

print(thing1.target)
thing1.target = thing2.position
print(thing1.target)
thing2.move()
print(thing1.target)

The output I get is 0,100,100, and the output I want is 0,100,110.

4

There are 4 best solutions below

3
Blckknght On BEST ANSWER

I think there are two parts to your question: How to I get my references to another object's position to stay synced up, and how do you initialize objects that reference each other in a cycle.

For the first part, I'd suggest a slightly simpler approach that the other answers: Don't reference the target position directly, target the Thing. You can get the position via self.target.position (or the equivalent) whenever you need it.

For the second part, you need some way to set up the reference cycle. The simplest approach is to start the way you have so far, initializing one object without a reference to its target, and then passing a reference to this first object to the second object. Then in another step, give a reference to the second object to the first. You're kind of doing this amid your print calls where you do thing1.target = thing2.position, but because you're referencing the position directly, you don't see updates.

I'd solve both problems like this:

class Thing():
    def __init__(self, position, target=None):    # target is now optional
        self.position = position
        self.target = target

    def move(self):
        self.position += 10

thing1 = Thing(0)             # no target passed, so it defaults to None (for now)
thing2 = Thing(100, thing1)   # initialize thing 2 to immediately target thing1
thing1.target = thing2        # update the target of thing1, now that thing2 exists

print(thing1.target.position) # get thing2's position via thing1's target reference
thing2.move()
print(thing1.target.position)
0
pyjedy On

Using target as a reference to the position of another Thing instance seems to be the solution in your case.

class Thing():
    def __init__(self, position, target):
        self.position = position
        self._target = target
        self.thing = None

    @property
    def target(self):
        position = 0
        if isinstance(self.thing, Thing):
            position = self.thing.position
        return self._target + position

    @target.setter
    def target(self, value):
        self._target = value

    def move(self):
        self.position += 10


thing1 = Thing(position=0, target=0)
thing2 = Thing(position=100, target=thing1.position)

print(thing1.target)
thing1.thing = thing2
print(thing1.target)
thing2.move()
print(thing1.target)
# 0
# 100
# 110
0
flakes On

You could factor out the component that changes.. in this case the positions.

The Thing class can compose a set of positions which can be created independently. When creating instances of Thing, assign the positions as appropriate.

class Position:
    def __init__(self, value):
        self.value = value

class Thing:
    def __init__(self, position, target):
        self.position = position
        self.target = target

    def move(self):
        self.position.value += 10
pos1 = Position(value=0)
pos2 = Position(value=100)

thing1 = Thing(position=pos1, target=pos2)
thing2 = Thing(position=pos2, target=pos1)
0
Acccumulation On

This is a crude solution, but something to get you started:

class Thing():
    universe = []
    def __init__(self, position, speed = 0):
        self.position = position
        self.speed = speed
        Thing.universe.append(self)
    def get_position(self):
        return self.position
    def set_target(self, target):
        self.target = target
    def get_target(self):
        if callable(self.target):
            return self.target()
        return self.target
    @classmethod
    def advance_time(self):
        for thing in Thing.universe:
            thing.position += thing.speed

thing1 = Thing(position = 0, speed = 10)
thing2 = Thing(position = 100)
thing1.set_target(0)                      
thing2.set_target(thing1.get_position)
Thing.advance_time()
print(thing2.get_target())

I made thing2.target a method so that when you access it, it will update the target to match the position.

Note that I removed move as an instance method and instead have a class method that moves all things. That's because you probably want everything moving simultaneously, and if you want some things to not move, you can accomplish that by giving them zero speed, rather than not applying the call method on them. But if you really want time to advance for one thing but not for another, you can keep it as an instance method. Also, if you want to have an object's speed depend on its target, and its target depend on the position of another thing, then you should cache the position, speed, and targets of every thing at the beginning of advance_time, and not update them until after all the new position, speeds, and targets are calculated.