For some context, I am making a Roguelike game in Python using the Doyren library (libtcod). I am more used to C++ where objects are strongly typed.
I'm writing several classes, such as a GameMap, GameObject, etc. Many of these classes contain methods which expect certain types, for example:
class GameMap:
...
def add_object(self, game_object, x, y):
...
This method adds the GameObject game_object to the coordinate (x, y) on the map. There are clearly several ways this function can be misused:
- A non-GameObject could be passed as game_object
- A non-integer could be passed as x or y
- A negative integer could be passed as x or y
- An integer that is beyond the map's width could be passed as x
- An integer that is beyond the map's height could be passed as y
My question is this: What is the Pythonic way to handle method misuse?
I see several possibilities:
Option 1: Lay out a series of assertions at the start of the method:
def add_object(self, game_object, x, y):
assert(isinstance(game_object, GameObject)
assert(type(x) == type(y) == int)
assert(0 <= x < self.map_width and 0 <= y < self.map_height)
...
These assertions get fairly repetitive as I copy+paste them into many of my methods in GameMap, which is why I also provide option 2:
Option 2: Write assertions in their own functions, and call those when needed to prevent copy+pasting
def check_game_object(self, game_object):
assert(isinstance(game_object, GameObject)
def check_coordinate(self, x, y):
assert(type(x) == type(y) == int)
assert(0 <= x < self.map_width and 0 <= y < self.map_height)
def add_object(self, game_object, x, y):
check_game_object(game_object)
check_coordinate(x, y)
...
Option 3: Lay out a series of custom exceptions at the start of the method:
def add_object(self, game_object, x, y):
if not isinstance(game_object, GameObject):
raise InvalidParameterException("game_object not a GameObject")
elif not type(x) == type(y) == int:
raise InvalidParameterException("(x, y) not integers")
elif not (0 <= x < self.map_width and 0 <= y < map.self_height)
raise InvalidMapCell("x, y do not represent a valid map cell)
...
Option 4: Return failure indicator, and handle the issue at a higher level
def add_object(self, game_object, x, y):
if not isinstance(game_object, GameObject):
return False
elif not type(x) == type(y) == int:
return False
elif not (0 <= x < self.map_width and 0 <= y < map.self_height)
return False
...
Option X: Something else?
Any advice here would be greatly appreciated! I want to make sure I'm following a useful and maintainable pattern as I go on.