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:
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.
The assertion is to make sure that objects, results, return, etc are what we expect them to be. Though they can be used for variable's type checking, that's not what their real purpose is and it gets repetitive.
In your case, I would suggest to use python EAFP way of doing things. Let the operation be performed on function input and catch the exception if it's not what is expected. From Python glossary :
EAFP: Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements. The technique contrasts with the LBYL(Look before you leap) style common to many other languages such as C.
A quick example:
def f(x):
"""If x is str a TypeError is raised"""
return 1 + x
try:
f('a')
except TypeError as e:
# something here or raise a custom exception
raise