0

I need to create several objects that have (often circular) references to each other. In general, several objects might be involved, but here's one simple case where I need just a pair of cars that refer to each other:

class Car:
  def __init__(self, position, speed):
    self.position = position
    self.speed = speed
  def set_reference(self, other_car):
    self.other_car = other_car
  # ...


def main():
  # ...
  car1 = Car(pos1, spd1)
  car2 = Car(pos2, spd2)
  car1.set_reference(car2)
  car2.set_reference(car1)

A car without a reference to the other car is not a valid object. So ideally, I'd like to perform set_reference from inside the __init__ method; this would be both safer (no chance of using invalid objects) and cleaner (all initialization will be performed in __init__ as one might expect).

Is there any neat solution that achieves this goal? I don't mind creating a pair of cars at once, but each car is a standalone entity, and so it needs to be an instance in its own right.

I'm also aware that circular references are troublesome for GC; I'll deal with that.

Use case:

Each car serves as a backup for the other. Car instances are "smart", i.e., they can do a lot of work. It's annoying if the a car instance doesn't know its backup, since it prevents a car from completing actions without requiring a reference from outside every time.

max
  • 49,282
  • 56
  • 208
  • 355
  • 1
    It's worth noting that setters are a bad idea in Python, just use an attribute. – Gareth Latty Apr 30 '12 at 19:21
  • Perhaps it would be clearer what your code is doing if you use a factory method/function to create the two cars at the same time? Just thinking about how someone reading/maintaining the code would fare... – Art Swri Apr 30 '12 at 19:21
  • 2
    Have you considered trying a factory pattern where every time you call the factory, you have to provide it parameters for 2 cars, or parameters for 1 car and an existing car object? You kind of have a chicken and egg problem here, so the solution might be a ChickenAndEggMaker. – Silas Ray Apr 30 '12 at 19:22
  • @ArtSwri @sr2222 What if I require `n` cars, referring to each other in some pattern (say a loop, or each to each)? The factory method would be troublesome, since it would need parameters for `n` cars. – max Apr 30 '12 at 19:23
  • @max, no, you'd iterate on the factory method, starting by creating 2, then proceeding to take the last car made as the parameter to pass to the next iteration of the factory. – Silas Ray Apr 30 '12 at 19:24
  • Every implementation will have problematic edge cases. If you really want to make loops of cars, I'd suggest separating the logic for creating loops of things from that creating cars, then put the cars in to the looplist data structure. – Silas Ray Apr 30 '12 at 19:27

3 Answers3

2

I don't think there is a good way to move the set_reference() call into __init__(), because the other car might not yet exist. I would probably do something like this.

class Car:
  def __init__(self, position, speed):
    self.position = position
    self.speed = speed
  def set_reference(self, other_car):
    self.other_car = other_car
  @classmethod
  def create_pair(cls, car1_args, car2_args):
    car1 = cls(*car1_args)
    car2 = cls(*car2_args)
    car1.set_reference(car2)
    car2.set_reference(car1)
    return car1, car2

def main():
  car1, car2 = Car.create_pair((pos1, spd1), (pos2, spd2))

Here is how you could expand this same concept for a larger circular reference structure:

class Car:
  # ...
  @classmethod
  def create_loop(cls, *args):
    cars = [cls(*car_args) for car_args in args]
    for i, car in enumerate(cars[:-1]):
        car.set_reference(cars[i+1])
    cars[-1].set_reference(cars[0])
    return cars

You could then call it like this (with any number of cars):

car1, car2, car3 = Car.create_loop((pos1, spd1), (pos2, spd2), (pos3, spd3))

You should up with the references set up like this:

>>> car1.other_car is car2 and car2.other_car is car3 and car3.other_car is car1
True
Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
0

If you really don't want to use a factory method, then the only real option is to force the user to configure them afterwards. (Taking on-board what you said about potentially wanting loops of dependency in the comments):

class Car:
  def __init__(self, position, speed):
    self.position = position
    self.speed = speed

  @staticmethod
  def setup(*args):
    for car, next in zip(args, args[1:]):
      car.other_car = next
    args[-1].other_car = args[0]

def main():
  ...
  car1 = Car(pos1, spd1)
  car2 = Car(pos2, spd2)
  Car.setup(car1, car2)

Another option is to split off the grouping:

class CarGroup(list):
    def other_car(self, car):
        index = self.index(car)+1
        index = 0 if index >= len(self) else index
        return self[index]

class Car:
    def __init__(self, position, speed, group):
        self.position = position
        self.speed = speed
        self.group = group
        self.group.append(self)

    @property
    def other_car(self):
        return self.group.other_car(self)

def main():
    ...
    group = CarGroup()
    car1 = Car(pos1, spd1, group)
    car2 = Car(pos2, spd2, group)
Gareth Latty
  • 86,389
  • 17
  • 178
  • 183
0

I'd suggest separating the car data structure from the data structure that references a sequence of cars. other_car doesn't really seem like data that strictly belongs in car, but rather represented in some iterable sequence of cars. Thus the simplest and most logically consistent solution would be to define a car, then put it in a sequence of cars.

Silas Ray
  • 25,682
  • 5
  • 48
  • 63