2

I'm working on a simple game in python (which I'm fairly new to as a language) using pygame and it seems as though python really hates circular dependencies (although I'm aware there are ways around this).

Generally for collision detection, I would have a callback function that is called, once a collision is detected, for each object involved in the collision. The problem is that in doing so, each object involved in the collision would need to know about the other in order to resolve the collision in the correct way, thus resulting in a circular dependency which I would prefer to avoid (see below).

Here is the Enemy.py module:

from Player include * #dependency on player

class Enemy():
    def handle_collision(other_object):
        if isinstance(other_object,Player) #this check requires the Enemy class to know about Player

Here is the Player.py module:

from enemy include * #dependency on enemy

class Player():
    def handle_collision(other_object):
        if isinstance(other_object,Wall): 
            #do what we need to do to resolve a wall collision
        elif isinstance(other_object,Enemy): #this check requires that we include the Enemy class
            #do what we need to do to resolve an enemy collision    

Any suggestions or insight into how this is generally handled would be great.

MaddawgX9
  • 131
  • 8
  • Don't really see the circularity here. Can you explain more? – tobias_k Jan 27 '14 at 15:58
  • Not sure what you are asking, but (1) in `Player.handle_collision` handle _only_ what results from the collision for the player, not for other objects (handle those in their `handle_collision` methods); (2) create a queue for "actions" or "updates", and apply those _after_ all the collisions have been detected. – tobias_k Jan 27 '14 at 16:06
  • Specifically, the dependency results from the if check in the callback. e.g. if isinstance(other_object, "Wall"): – MaddawgX9 Jan 27 '14 at 16:27
  • I guess technically I could pass the handler a string indicating which object it collided with? – MaddawgX9 Jan 27 '14 at 16:28
  • 1
    When two objects collide, call the handler for both, with the other as parameter. Still see no cyclicity. Maybe you should include both sides of the dependency to make it clear. – tobias_k Jan 27 '14 at 16:29
  • updated to include the enemy class python pseudocode, hopefully that clarifies things. – MaddawgX9 Jan 27 '14 at 18:33
  • Ah, now I got it! :-) Well, I guess the best would be to put the two classes in the same module. Generally, Python does not have this "one file, one class" paradigm that Java has. I found this disturbing at first, too, but it's not. Alternatively, you could use comething like `other_object.__class__.name`, but the first way is much better. – tobias_k Jan 27 '14 at 18:35
  • As an unrelated not, I just realized that your method does not have `self` as a first parameter. I guess this is not your actual code? – tobias_k Jan 27 '14 at 19:00

2 Answers2

1

Cyclic dependencies like these, where the module player.py imports the module enemy.py and vice versa are indeed rather tricky in Python. But there are (at least) two ways to work around those:

First, you do not need to import a module to use classes from that module. Even without importing the enemy.py module, you can use an instance of the Enemy class, e.g. after it has been passed to the handle_collision method as a parameter by some other module. Then, you could work around your problem by checking e.g. for other_object.__class__.__name__ == 'Enemy'.

This works, but it is not very nice, and will cause problems with, e.g., subclasses of Enemy.

Second, you do not have to put each class in it's own module/file, as it is common in, e.g., Java. In Python, it is perfectly normal and good practice to put many related classes in one and the same module. (One of the reasons why cyclic dependencies between modules are discouraged in Python is because needing them is considered a sign of bad system design.)

So my advice would be to put all of your "entity" classes into the same module, e.g. entities.py

class Entity:
    def handle_collision(self, other_object):
        pass

class Player(Entity):
    def handle_collision(self, other_object):
        if isinstance(other_object, Enemy):
            # do stuff

class Enemy(Entity):
    def handle_collision(self, other_object):
        if isinstance(other_object, Player):
            # do other stuff

Note that circular imports are in fact possible in Python, but should be used with care. For more about this, you can take a look at these two related posts.

Community
  • 1
  • 1
tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • 1
    Ah, I come from a C++/Java background where classes are generally separated into their own files. I was hoping to keep them in separate files for reusability/compartmentalization. I think the class name check is probably the route I'll end up going in this case since I really want to preserve that separation, but I will keep your other point in mind. Thanks for the help. – MaddawgX9 Jan 27 '14 at 19:15
  • Another way to fix the errors you get from circular imports is to use basic `import module` syntax (and `module.Class` later), rather than `from module import Class`. The former will work even if the imported module imports us back. – Blckknght Jan 27 '14 at 20:09
  • @Blckknght It seems my first sentence, about those being "not possible" was a bit hasty. It seems it is very well possible, as pointed out by jdhideb and yourself. Still, I think I'll not change my answer much, since it was more about alternatives to circular imports in the first place. ;-) – tobias_k Jan 27 '14 at 20:27
0

You can generally eliminate circular dependency errors by moving the import from the module-level into a function or method where you actually need it.

In Player.py, try moving from enemy include * (which should be IMPORT, not INCLUDE) to be within your handle_collision method:

def handle_collision(other_object):
    from enemy import Enemy
    if isinstance(other_object,Wall): 
        #do what we need to do to resolve a wall collision
        pass
    elif isinstance(other_object,Enemy): 
        #this check requires that we include the Enemy class
        pass
        #do what we need to do to resolve an enemy collision    

Also you should try to avoid importing * as it makes it difficult to see where your various imported names come from.

jdhildeb
  • 3,322
  • 3
  • 17
  • 25
  • I've heard this is generally bad practice. – MaddawgX9 Jan 27 '14 at 19:16
  • 1
    @user3241047: I haven't heard that, but I can see that you'd want to minimize use of this technique, i.e. by refactoring and moving code around to eliminate the circular dependency. – jdhildeb Jan 28 '14 at 01:25