Inheritance can make debugging a challenge because when you invoke a method on an object, you might not know which method will be invoked.
Suppose you are writing a function that works with Hand objects. You would like it to work with all kinds of Hands, like PokerHands, BridgeHands, etc. If you invoke a method like shuffle, you might get the one defined in Deck, but if any of the subclasses override this method, you’ll get that version instead.
Any time you are unsure about the flow of execution through your program, the simplest solution is to add print statements at the beginning of the relevant methods. If Deck.shuffle prints a message that says something like Running Deck.shuffle, then as the program runs it traces the flow of execution.
As an alternative, you could use this function, which takes an object and a method name (as a string) and returns the class that provides the definition of the method:
def find_defining_class(obj, meth_name):for ty in type(obj).mro():if meth_name in ty.__dict__:return ty
Here’s an example:
>>> hand = Hand()>>> print find_defining_class(hand, 'shuffle')<class 'Card.Deck'>
So the shuffle method for this Hand is the one in Deck.
find_defining_class uses the mro method to get the list of class objects (types) that will be searched for methods. “MRO” stands for “method resolution order.”
Here’s a program design suggestion: whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand.
If you violate this rule, your code will collapse like (sorry) a house of cards.