views:

163

answers:

4

A side effect of this question is that I was lead to this post, which states:

Whenever isinstance is used, control flow forks; one type of object goes down one code path, and other types of object go down the other --- even if they implement the same interface!

and suggests that this is a bad thing.

However, I've used code like this before, in what I thought was an OO way. Something like the following:

class MyTime(object):
    def __init__(self, h=0, m=0, s=0):
        self.h = 0
        self.m = 0
        self.s = 0
    def __iadd__(self, other):
        if isinstance(other, MyTime):
            self.h += other.h
            self.m += other.m
            self.s += other.s
        elif isintance(other, int):
            self.h += other/3600
            other %= 3600
            self.m += other/60
            other %= 60
            self.s += other
        else:
            raise TypeError('Addition not supported for ' + type(other).__name__)

So my question:

Is this use of isinstance "pythonic" and "good" OOP?

+2  A: 

The first use is fine, the second is not. Pass the argument to int() instead so that you can use number-like types.

Ignacio Vazquez-Abrams
+3  A: 

Not in general. An object's interface should define its behavior. In your example above, it would be better if other used a consistent interface:

def __iadd__(self, other):
    self.h += other.h
    self.m += other.m
    self.s += other.s

Even though this looks like it is less functional, conceptually it is much cleaner. Now you leave it to the language to throw an exception if other does not match the interface. You can solve the problem of adding int times by - for example - creating a MyTime "constructor" using the integer's "interface". This keeps the code cleaner and leaves fewer surprises for the next guy.

Others may disagree, but I feel there may be a place for isinstance if you are using reflection in special cases such as when implementing a plugin architecture.

Justin Ethier
I agree. Justin is advocating that only MyTime objects can be added to other MyTime objects, which is the best approach. The part that may be a little vague is that this technique may look like it is just pushing the isinstance call off on the constructor. However, there are other pythonic ways of having objects that can be constructed in multiple ways, either via factory methods or keyword arguments -- my favorite being the latter. See http://stackoverflow.com/questions/356718/how-to-handle-constructors-or-methods-with-a-different-set-or-type-of-arguments/357004#357004
Brandon Corfman
How would you implement the constructor using the integer's "interface"? Would you add the call inside `__iadd__` or some other way?
Wayne Werner
+1  A: 

isinstance, since Python 2.6, has become quite nice as long as you follow the "key rule of good design" as explained in the classic "gang of 4" book: design to an interface, not to an implementation. Specifically, 2.6's new Abstract Base Classes are the only things you should be using for isinstance and issubclass checks, not concrete "implementation" types.

Unfortunately there is no abstract class in 2.6's standard library to summarize the concept of "this number is Integral", but you can make one such ABC by checking whether the class has a special method __index__ (don't use __int__, which is also supplied by such definitely non-integral classes as float and str -- __index__ was introduced specifically to assert "instances of this class can be made into integers with no loss of important information") and use isinstance on that "interface" (abstract base class) rather than the specific implementation int, which is way too restrictive.

You could also make an ABC summarizing the concept of "having m, h and s attributes" (might be useful to accept attribute synonyms so as to tolerate a datetime.time or maybe timedelta instance, for example -- not sure whether you're representing an instant or a lapse of time with your MyTime class, the name suggests the former but the existence of addition suggests the latter), again to avoid the very restrictive implications of isinstance with a concrete implementation cass.

Alex Martelli
The case that I pulled my example from was a little of both. I was creating a visual timer, and it was a lot easier to just create an object that I could "print" than writing some functions to handle datetime. Of course it's been about 2 years since I wrote that, so I like to think I'm developing better habits (such as asking much more experienced folks) and probably wouldn't implement it quite the same way.
Wayne Werner
+1  A: 

To elaborate further on the comment I made under Justin's answer, I would keep his code for __iadd__ (i.e., so MyTime objects can only be added to other MyTime objects) and rewrite __init__ in this way:

def __init__(self, **params):
    if params.get('sec'):
        t = params['sec']
        self.h = t/3600
        t %= 3600
        self.m = t/60
        t %= 60
        self.s = t
    elif params.get('time'):
        t = params['time']
        self.h = t.h
        self.m = t.m
        self.s = t.s
    else:
        if params:
            raise TypeError("__init__() got unexpected keyword argument '%s'" % params.keys()[0])
        else:
            raise TypeError("__init__() expected keyword argument 'sec' or 'time'")

# example usage
t1 = MyTime(sec=30)
t2 = MyTime(sec=60)
t2 += t1 
t3 = MyTime(time=t1)

I just tried to pick short keyword arguments, but you may want to get more descriptive than I did.

Brandon Corfman
So is this better OOP/more pythonic? Also, any reason (other than preference, etc.) not to just do `__init__(self, h=0, m=0, s=0)`, and then just do `self.h = h + s/3600`, and so on?
Wayne Werner
This fits the Python way of being explicit and readable, plus it easily allows you to add new arguments if needed. It has an added advantage for the calling code in that I don't have to look at the actual constructor code to understand the passed arguments; the explanation is supplied by the keyword. If you were to design with 3 arguments like you mentioned, compare the readability of MyTime(2,5,20) with MyTime(hr=2,min=5,sec=20) as an example.
Brandon Corfman
P.S. like I mentioned in the other comment, another way of making object construction explicit and readable is to use factory methods. Both are different ways to accomplish the same goal; use whichever you prefer. Factory methods make construction easier (one method for each type) but non-standard since you're using class methods instead of `__init__`. Keyword arguments are more standard (one `__init__` method) but it can be more difficult to handle all possible arguments or report errors.
Brandon Corfman